date-website

Template System & Association Overrides

How Templates Are Loaded

The project supports multiple site variants selected by PROJECT_NAME. Each variant has its own settings file under core/settings/<variant>.py, which builds the Django TEMPLATES DIRS list with the shared helpers in core/settings/common.py:

# Variant with no inherited templates (date, kk, pulterit):
TEMPLATES = build_templates('date')

# Variant inheriting another variant's templates (biocum, sf, impuls):
TEMPLATES = build_templates('biocum', parent_variants=('date',))

build_templates(variant, parent_variants=()) puts the variant’s own directory first, then the inherited parents, then the shared common dirs, so Django finds association templates before falling back to templates/common. Use the helpers when adding a variant; do not construct TEMPLATES by hand. Static dirs are built the same way with build_static_dirs(variant); note that static dirs do not inherit parent variants.

Override Pattern

Association templates extend the common equivalents using the same logical path:

templates/<association>/core/footer.html  →  {% extends "core/footer.html" %}
                                                               ↓
                                             templates/common/core/footer.html

Django resolves the path in {% extends %} by skipping the file currently being loaded (avoiding infinite recursion) and finding the next match — the common version.

This means:

Adding New Block Slots

If an association template needs to inject content into an area that has no block slot yet, the slot must be added to the common template — even as an empty block:


{# templates/common/core/some_template.html #}
{% block my_new_slot %}{% endblock %}

The association template then fills it:


{# templates/<association>/core/some_template.html #}
{% block my_new_slot %}
  ...association-specific content...
{% endblock %}

Adding an empty block to the common template is safe: associations that don’t override it render nothing, so existing variants are unaffected.

You cannot inject content into the middle of a parent template’s HTML from a child template — only into defined {% block %} slots. A block defined in a child template that has no corresponding slot in the parent is a no-op; the content is silently discarded.

Shared Components, Not Copies

A component that several associations render belongs in templates/common/, even when each association already carries its own working copy. The copies are what drift: the homepage event list existed as five copies, two of them byte identical, and every difference any of them carried was a handful of lines.

Put the whole component in the shared template, with a block for each piece that really differs, and give an association that differs a short file that extends it:

templates/common/date/components/events.html   the component, and a block per icon
templates/sf/date/components/events.html       {% extends %} plus four block overrides

The association file then holds only its difference, so a change to the shared structure reaches every association at once and a reviewer can see the whole per-association delta in one screen. The same rule covers the seasonal event pages: templates/common/events/arsfest_base.html is the shared page, and the association files should be overrides of it rather than standalone copies.

Two things that are not blocks:

Before adding a template under templates/<association>/, check whether the same path already exists for another association. Two association copies of one component are almost always one shared component plus two overrides, and templates/ should not gain a third.

core/tests/test_template_duplication.py enforces that. It compares the copies of every logical path across the six associations and the shared tree, and fails when two copies of 15 lines or more are 70 percent alike, unless the pair is frozen in core/tests/template_near_copy_baseline.json with a reason. A frozen pair that gets closer also fails, and so does a baseline entry whose copies have since diverged, so the frozen set stays honest. Regenerate it with uv run python -m core.tests.test_template_duplication --write-baseline after a deliberate exception, and add the reason to the REASONS mapping in the test.

Each association lists its footer buttons in CONTENT_VARIABLES["SOCIAL_BUTTONS"] as [icon, url] pairs; an entry with a blank URL is skipped. The shared footer (templates/common/core/footer.html, footer_social_buttons block) picks the icon for each entry:

  1. If the template core/svg/social/<icon>.svg resolves through the configured template search path, it is included inline. The social_icon_template tag in date/templatetags/social_icons.py asks Django’s template loader for that path, so the whole search path is used and not just the shared directory.
  2. Otherwise the entry renders as the Line Awesome icon-font class fab <icon>, e.g. fa-facebook-f.

So platforms Line Awesome covers keep using fa-* names, and a platform it lacks needs one SVG file to work for every association:

To give one association a different icon, put a file at the same path in its own template directory (templates/<association>/core/svg/social/<name>.svg); the search order finds it before the shared file. A variant that layers another variant’s templates (impuls and biocum layer date) or an app’s templates/ directory can supply or override an icon the same way.

Shared icons so far: tiktok, linktree (Font Awesome Free 7.3.1, CC BY 4.0). sf renders its own complete footer and does not use this lookup: templates/sf/core/footer.html has one branch per platform (instagram, facebook, x, linkedin, tiktok), so a new platform for sf needs a branch added there. Its tiktok branch includes the shared tiktok.svg.

Shared CSS And Element Selectors

An association’s stylesheets are loaded on every page of that association, and a selector that matches a bare HTML element therefore reaches parts of the page it was not written for. Two rules are enforced:

The rest of the contract, including why element selectors are the trap, the palette tokens, load order, !important, breakpoints, z-index and JavaScript hooks, is in Shared CSS & Association Overrides.