# 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: ```django {% extends "base/base.html" %} {# Page title (appears in 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 ```django {% 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 ```django {# 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 ```django {# 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 ```django {# 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 ```django {# 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 ```django {# 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 ```django {# 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 ```django {# 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: ```django {% 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:** ```django {{ 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:** ```django {{ 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 ```django {# 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: ```django {% 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_at` for 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-label` for icon-only buttons - [ ] `aria-describedby` for help text - [ ] `aria-invalid` for fields with errors - [ ] `role="alert"` for error messages - [ ] `aria-live` for dynamic content ### Skip Links Base template includes skip link to main content: ```html <a href="#main-content" class="sr-only focus:not-sr-only ..."> Skip to main content </a> ``` ### Landmarks ```html <nav role="navigation" aria-label="Main navigation"> <main role="main" aria-label="Main content"> <footer role="contentinfo"> ``` ## Security ### Safe HTML Rendering ```django {# 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 ```django {# Safe JSON for JavaScript #} <script> const data = {{ python_dict|json_safe }}; </script> ``` ## Component Documentation Template Each component should have a header comment: ```django {% 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` |