| name | jcstream-template-author |
| description | Use when editing or creating Jinja templates under web/templates/ in the JCStream project — base.html, index.html, inmate.html, stats.html, statute.html, data.html, _card.html, feed.xml. Covers css_version cache-busting, data-* filter hooks, body.is-table view toggle, lightbox inert focus management, lightbox/tier-tooltip data-* contracts, and base.html block overrides. Trigger phrases: "edit the inmate page", "add a section to the homepage", "fix template rendering", "edit the masthead/footer", "fix the lightbox", "add a filter", "tweak the table view", "update the RSS template". |
JCStream template author
You own web/templates/*.html and web/templates/feed.xml (rendered for feed.xml, booked.xml, released.xml via _render_feeds in web/build.py). The site is statically generated by web/build.py (Jinja2 with autoescape + trim_blocks + lstrip_blocks). No client-side framework — progressive enhancement only.
Conventions you must preserve
Cache-busting stylesheet link
Templates link the stylesheet with a content-hash query string set by web/build.py:
<link rel="stylesheet" href="{{ base_url }}/static/style.css?v={{ css_version | default('dev', true) }}">
Don't hardcode a version, don't key it on snapshot.generated_utc — the project rebuilds every 30 min and a data-keyed bust would invalidate the CSS on every cycle.
Filter hooks on cards
The homepage filter bar reads data-* attributes off .card-inmate, set in web/templates/_card.html:
<div class="card-inmate" data-tier="{{ d.tier }}" data-chap="{{ d.chap }}"
data-name="{{ d.name }}" data-search="{{ d.search }}">
The JS reader lives in web/templates/base.html (currentFilters()). If you add a new filter dimension, update both files in the same commit.
Table-view toggle
body.is-table flips month sections from card grid to a row list:
- Button:
web/templates/index.html — <button type="button" class="view-toggle" id="view-toggle" aria-pressed="false" hidden>. The hidden attribute and aria-pressed are load-bearing: JS un-hides at web/templates/base.html and toggles aria-pressed at :206.
- JS handler:
web/templates/base.html (toggle class + persist to localStorage as jcs-view)
- CSS:
web/static/style.css (body.is-table .month .card-inmate reshape)
Don't add inline styles on .card-inmate that compete with the body.is-table rules.
Lightbox markup + inert focus confinement
The shared lightbox dialog lives at web/templates/base.html — <div class="lightbox" id="lb" role="dialog" aria-modal="true"> — with backdrop, close button, #lb-img, and #lb-cap. Every thumbnail trigger is a delegated click handler (web/templates/base.html) that reads three attributes off the closest [data-photo]:
data-photo — full-size image URL
data-photo-cap — caption text
data-photo-alt — alt text
web/templates/_card.html is the canonical producer. Any new thumbnail must set all three.
web/templates/base.html sets inert=true on all other body children when the lightbox opens, :134-136 clears it on close, and :144-156 is the Tab-cycler fallback. Don't remove the fallback — older Safari versions ship without inert.
Tier-badge tooltip contract
One shared #tier-tip div at web/templates/base.html; the JS at web/templates/base.html reads [data-tip] (newline-separated, first line bold) on the hovered/focused element. web/templates/_card.html writes it via card_tip(inm). Don't move tooltip content into the cards — the cards rely on content-visibility:auto and an in-card tooltip would be clipped by paint containment.
noindex, noarchive robots meta
web/templates/base.html sets <meta name="robots" content="noindex, noarchive">. The noarchive is load-bearing for the ORC § 2953.32 expungement contract — if a record is purged here, a Google-cache snapshot mustn't outlive it. Don't relax this without coordinating with the legal-copy specialist.
Search type-ahead dropdown
web/templates/base.html lazy-loads /search.json from #search-box (web/templates/index.html) and writes results into #search-results. If you rename either id, update the JS in the same commit.
Block override conventions
{% block title %} — set per page; default is "JCStream"
{% block body_class %} — set detail on inmate/stats/statute (constrains max-width)
{% block sr_h1 %} — empty when the page has its own visible h1 so screen readers don't get a generic title first
{% block content %} — main body
Env-global awareness
Templates call dozens of helpers registered in web/build.py (cck_name_search, cck_case_summary, base_url, site_url, giscus, css_version, orc_title, primary_charge, primary_chapter, primary_tier, primary_degree, tier_max, tier_ladder, bond_context, recent_booked_inmates, timeline_markers, display_date, similar_by_statute, tier_counts, charge_tier, avatar_initials, card_data, card_tip, expand_race, expand_sex, approx_age, booking_seq, bond_by_tier, next_court_date, case_numbers, charge_status_summary, all_chapters, bond_total, days_in_custody, charges_by_chapter, crimes_of_month, inmates_by_id, orc_freq, codes_ohio_url, related_inmates, all_inmates_total). If you need a new helper, hand off to the jcstream-build-helper-author skill before continuing.
The giscus global (web/build.py) is read by the conditional comments block at web/templates/inmate.html: the policy paragraph always renders, but the Giscus widget only mounts when giscus.repo_id is non-empty. Don't gate the policy paragraph on repo_id.
Anti-patterns
- Inline
style="..." for static rules — move to style.css (Gemini Code Assist flags these). Computed values (style="width: {{ pct }}%") for per-row widths are fine.
- Embedding raw inmate fields with
| safe — autoescape exists for a reason.
- Adding a new
{% block %} to base.html without auditing every other template that extends it.
Verify your edit
JCSTREAM_SITE_BASE_URL="" JCSTREAM_CNAME="www.aretheyinjail.com" python -m web.build
python -m pytest -q
Spot-check a representative page (docs/index.html, docs/inmate/<id>/index.html, docs/stats/index.html).