Files
thrillwiki_django_no_react/backend/templates/README.md

11 KiB

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_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

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