mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 14:51:08 -05:00
ThrillWiki Template System
This document describes the template architecture, conventions, and best practices for ThrillWiki.
Directory Structure
templates/
├── base/ # Base templates
│ └── base.html # Root template all pages extend
├── components/ # Reusable UI components
│ ├── ui/ # UI primitives (button, card, toast)
│ ├── modals/ # Modal components
│ ├── pagination.html # Pagination (supports HTMX)
│ ├── status_badge.html # Status badge (parks/rides)
│ ├── stats_card.html # Statistics card
│ └── history_panel.html # History/audit trail
├── forms/ # Form-related templates
│ ├── partials/ # Form field components
│ │ ├── form_field.html
│ │ ├── field_error.html
│ │ └── field_success.html
│ └── layouts/ # Form layout templates
│ ├── stacked.html # Vertical layout
│ ├── inline.html # Horizontal layout
│ └── grid.html # Multi-column grid
├── htmx/ # HTMX-specific templates
│ ├── components/ # HTMX components
│ └── README.md # HTMX documentation
├── {app}/ # App-specific templates
│ ├── {model}_list.html # List views
│ ├── {model}_detail.html # Detail views
│ ├── {model}_form.html # Form views
│ └── partials/ # App-specific partials
└── README.md # This file
Template Inheritance
Base Template
All pages extend base/base.html. Available blocks:
{% extends "base/base.html" %}
{# Page title (appears in <title> and meta tags) #}
{% block title %}My Page - ThrillWiki{% endblock %}
{# Main page content #}
{% block content %}
<h1>Page Content</h1>
{% endblock %}
{# Additional CSS/meta in <head> #}
{% block extra_head %}
<link rel="stylesheet" href="...">
{% endblock %}
{# Additional JavaScript before </body> #}
{% block extra_js %}
<script src="..."></script>
{% endblock %}
{# Additional body classes #}
{% block body_class %}custom-page{% endblock %}
{# Additional main element classes #}
{% block main_class %}no-padding{% endblock %}
{# Override navigation (defaults to enhanced_header.html) #}
{% block navigation %}{% endblock %}
{# Override footer #}
{% block footer %}{% endblock %}
{# Meta tags for SEO #}
{% block meta_description %}Page description{% endblock %}
{% block og_title %}Open Graph title{% endblock %}
{% block og_description %}Open Graph description{% endblock %}
Inheritance Example
{% extends "base/base.html" %}
{% load static %}
{% load park_tags %}
{% block title %}{{ park.name }} - ThrillWiki{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'css/park-detail.css' %}">
{% endblock %}
{% block content %}
<div class="container">
<h1>{{ park.name }}</h1>
{% include "parks/partials/park_header_badge.html" %}
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/park-map.js' %}"></script>
{% endblock %}
Component Usage
Pagination
{# Standard pagination #}
{% include 'components/pagination.html' with page_obj=page_obj %}
{# HTMX-enabled pagination #}
{% include 'components/pagination.html' with page_obj=page_obj use_htmx=True hx_target='#results' %}
{# Small size #}
{% include 'components/pagination.html' with page_obj=page_obj size='sm' %}
Status Badge
{# Basic badge #}
{% include 'components/status_badge.html' with status=park.status %}
{# Interactive badge with HTMX refresh #}
{% include 'components/status_badge.html' with
status=park.status
badge_id='park-header-badge'
refresh_trigger='park-status-changed'
scroll_target='park-status-section'
can_edit=perms.parks.change_park
%}
Stats Card
{# Basic stat #}
{% include 'components/stats_card.html' with label='Total Rides' value=park.ride_count %}
{# Clickable stat #}
{% include 'components/stats_card.html' with label='Total Rides' value=42 link=rides_url %}
{# Priority stat (highlighted) #}
{% include 'components/stats_card.html' with label='Operator' value=park.operator.name priority=True %}
History Panel
{# Basic history #}
{% include 'components/history_panel.html' with history=history %}
{# With FSM toggle for moderators #}
{% include 'components/history_panel.html' with
history=history
show_fsm_toggle=True
fsm_history_url=fsm_url
model_type='park'
object_id=park.id
can_view_fsm=perms.parks.change_park
%}
Loading Indicator
{# Block indicator #}
{% include 'htmx/components/loading_indicator.html' with id='loading' message='Loading...' %}
{# Inline indicator (in buttons) #}
{% include 'htmx/components/loading_indicator.html' with id='btn-loading' inline=True size='sm' %}
{# Overlay indicator #}
{% include 'htmx/components/loading_indicator.html' with id='overlay' mode='overlay' %}
Form Rendering
Form Layouts
{# Stacked layout (default) #}
{% include 'forms/layouts/stacked.html' with form=form %}
{# Inline/horizontal layout #}
{% include 'forms/layouts/inline.html' with form=form %}
{# 2-column grid #}
{% include 'forms/layouts/grid.html' with form=form cols=2 %}
{# With excluded fields #}
{% include 'forms/layouts/stacked.html' with form=form exclude='password2' %}
{# Custom submit text #}
{% include 'forms/layouts/stacked.html' with form=form submit_text='Save Changes' %}
Individual Fields
{# Standard field #}
{% include 'forms/partials/form_field.html' with field=form.email %}
{# Field with custom label #}
{% include 'forms/partials/form_field.html' with field=form.email label='Email Address' %}
{# Field with HTMX validation #}
{% include 'forms/partials/form_field.html' with
field=form.username
hx_validate=True
hx_validate_url='/api/validate/username/'
%}
Template Tags
Loading Order
Always load template tags in this order:
{% load static %}
{% load i18n %}
{% load park_tags %} {# App-specific #}
{% load safe_html %} {# Sanitization #}
{% load common_filters %} {# Utility filters #}
{% load cache %} {# Caching (if used) #}
Available Filters
common_filters:
{{ datetime|humanize_timedelta }} {# "2 hours ago" #}
{{ text|truncate_smart:50 }} {# Truncate at word boundary #}
{{ number|format_number }} {# "1,234,567" #}
{{ number|format_compact }} {# "1.2K", "3.4M" #}
{{ dict|get_item:"key" }} {# Safe dict access #}
{{ count|pluralize_custom:"item,items" }}
{{ field|add_class:"form-control" }}
safe_html:
{{ content|sanitize }} {# Full HTML sanitization #}
{{ comment|sanitize_minimal }} {# Basic text only #}
{{ text|strip_html }} {# Remove all HTML #}
{{ data|json_safe }} {# Safe JSON for JS #}
{% icon "check" class="w-4 h-4" %} {# SVG icon #}
Context Variables
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Single object | Lowercase model name | park, ride, user |
| List/QuerySet | {model}_list |
park_list, ride_list |
| Paginated | page_obj |
Django standard |
| Single form | form |
Standard |
| Multiple forms | {purpose}_form |
login_form, signup_form |
Avoid
- Generic names:
object,item,obj,data - Abbreviated names:
p,r,f,frm
HTMX Patterns
See htmx/README.md for detailed HTMX documentation.
Quick Reference
{# Swap strategies #}
hx-swap="innerHTML" {# Replace content inside #}
hx-swap="outerHTML" {# Replace entire element #}
hx-swap="beforeend" {# Append #}
hx-swap="afterbegin" {# Prepend #}
{# Target naming #}
hx-target="#park-123" {# Specific object #}
hx-target="#results" {# Page section #}
hx-target="this" {# Self #}
{# Event naming #}
hx-trigger="park-status-changed from:body"
hx-trigger="auth-changed from:body"
Caching
Use fragment caching for expensive template sections:
{% load cache %}
{# Cache navigation for 5 minutes per user #}
{% cache 300 nav user.id %}
{% include 'components/layout/enhanced_header.html' %}
{% endcache %}
{# Cache stats with object version #}
{% cache 600 park_stats park.id park.updated_at %}
{% include 'parks/partials/park_stats.html' %}
{% endcache %}
Cache Keys
Include in cache key:
- User ID for personalized content
- Object ID for object-specific content
updated_atfor automatic invalidation
Do NOT cache:
- User-specific actions (edit buttons)
- Form CSRF tokens
- Real-time data
Accessibility
Checklist
- Single
<h1>per page - Heading hierarchy (h1 → h2 → h3)
- Labels for all form inputs
aria-labelfor icon-only buttonsaria-describedbyfor help textaria-invalidfor fields with errorsrole="alert"for error messagesaria-livefor dynamic content
Skip Links
Base template includes skip link to main content:
<a href="#main-content" class="sr-only focus:not-sr-only ...">
Skip to main content
</a>
Landmarks
<nav role="navigation" aria-label="Main navigation">
<main role="main" aria-label="Main content">
<footer role="contentinfo">
Security
Safe HTML Rendering
{# User content - ALWAYS sanitize #}
{{ user_description|sanitize }}
{# Comments - minimal formatting #}
{{ comment_text|sanitize_minimal }}
{# Remove all HTML #}
{{ raw_text|strip_html }}
{# NEVER use |safe for user content #}
{{ user_input|safe }} {# DANGEROUS! #}
JSON in Templates
{# Safe JSON for JavaScript #}
<script>
const data = {{ python_dict|json_safe }};
</script>
Component Documentation Template
Each component should have a header comment:
{% comment %}
Component Name
==============
Brief description of what the component does.
Purpose:
Detailed explanation of the component's purpose.
Usage Examples:
{% include 'components/example.html' with param='value' %}
Parameters:
- param1: Description (required/optional, default: value)
- param2: Description
Dependencies:
- Alpine.js for interactivity
- HTMX for dynamic updates
Security: Notes about sanitization, XSS prevention
{% endcomment %}
File Naming
| Type | Pattern | Example |
|---|---|---|
| List view | {model}_list.html |
park_list.html |
| Detail view | {model}_detail.html |
park_detail.html |
| Form view | {model}_form.html |
park_form.html |
| Partial | {model}_{purpose}.html |
park_header_badge.html |
| Component | {purpose}.html |
pagination.html |