From 233a8fbb4c2a0aa663a0d432b4716b7f50bca4ee Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Fri, 19 Jun 2026 18:13:22 -0700 Subject: [PATCH 1/2] fix(templates): convert 3 multi-line {# #} header comments to {% comment %} Caught by the new template-comment guard. These sit outside any {% block %} in child templates, so they don't render (verified on prod), but a multi-line {# #} is malformed Django and would leak if ever moved into a block. No output change. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/templates/website/member.html | 8 +++++--- website/templates/website/news_item.html | 8 +++++--- website/templates/website/project.html | 6 ++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/website/templates/website/member.html b/website/templates/website/member.html index b0f84304..e7b7e87a 100644 --- a/website/templates/website/member.html +++ b/website/templates/website/member.html @@ -50,9 +50,11 @@ {% load thumbnail %} {% load ml_tags %} -{# Title, description, og:type=profile, and canonical come from page_meta (see - website/views/member.py). Here we override the share image and add the - profile-specific structural OG tags. #} +{% comment %} +Title, description, og:type=profile, and canonical come from page_meta (see +website/views/member.py). Here we override the share image and add the +profile-specific structural OG tags. +{% endcomment %} {% block social_image %} {% thumbnail person.image '1200x630' box=person.cropping crop=True upscale=True as og_img %} {% if og_img %} diff --git a/website/templates/website/news_item.html b/website/templates/website/news_item.html index d4522bc4..680993a5 100644 --- a/website/templates/website/news_item.html +++ b/website/templates/website/news_item.html @@ -42,9 +42,11 @@ {% load thumbnail %} {% load ml_tags %} -{# Title, description, og:type=article, and canonical come from page_meta (see - website/views/news_item.py). Here we override the share image and add the - article-specific structural OG tags. #} +{% comment %} +Title, description, og:type=article, and canonical come from page_meta (see +website/views/news_item.py). Here we override the share image and add the +article-specific structural OG tags. +{% endcomment %} {% block social_image %} {% if news_item.image %}{% thumbnail news_item.image '1200x630' box=news_item.cropping crop=True upscale=True as og_img %}{% endif %} {% if og_img %} diff --git a/website/templates/website/project.html b/website/templates/website/project.html index 2b96e736..aeb16d5d 100644 --- a/website/templates/website/project.html +++ b/website/templates/website/project.html @@ -65,8 +65,10 @@ {% load thumbnail %} {% load ml_tags %} -{# Title, description, og:type, and canonical come from page_meta (see - website/views/project.py). Here we only override the social share image. #} +{% comment %} +Title, description, og:type, and canonical come from page_meta (see +website/views/project.py). Here we only override the social share image. +{% endcomment %} {% block social_image %} {% thumbnail project.gallery_image 1200x630 box=project.cropping crop=True upscale=True as og_img %} {% if og_img %} From 348dce5675718be07429d038e5f00aa2fc229c6e Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Fri, 19 Jun 2026 18:13:22 -0700 Subject: [PATCH 2/2] test(templates): CI guard against multi-line Django {# #} comments SimpleTestCase that scans templates and fails on any multi-line {# #} (which Django renders as visible text). Runs in the existing test.yml CI suite and locally via manage.py test; includes a self-test of the detector. Backstops the recurring bug documented in CLAUDE.md (last hit: 2.14.2). Co-Authored-By: Claude Opus 4.8 (1M context) --- website/tests/test_template_comments.py | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 website/tests/test_template_comments.py diff --git a/website/tests/test_template_comments.py b/website/tests/test_template_comments.py new file mode 100644 index 00000000..e8e7562e --- /dev/null +++ b/website/tests/test_template_comments.py @@ -0,0 +1,65 @@ +""" +Mechanical guard for a recurring bug: Django's ``{# … #}`` template comment is +**single-line only**. A ``{# … #}`` that spans multiple lines is NOT tokenized as +a comment — Django renders the whole thing (text and ``#}`` included) as visible +page content. This shipped to prod once, printing a note on every award card +(fixed in 2.14.2). For multi-line comments use ``{% comment %} … {% endcomment %}``. + +This is a ``SimpleTestCase`` (no DB), so it runs as part of the normal test suite +in CI (.github/workflows/test.yml) on every push/PR, and locally via +``manage.py test``. See the template-comment note in CLAUDE.md. +""" + +from pathlib import Path + +from django.test import SimpleTestCase + +# Repo root: website/tests/this_file.py -> parents[2]. +REPO_ROOT = Path(__file__).resolve().parents[2] +SKIP_DIRS = {".git", "node_modules", "static", "staticfiles", "media", + ".venv", "venv", "htmlcov", "__pycache__"} + + +def _template_html_files(): + """Yield every ``*.html`` living under any ``templates/`` directory in the repo.""" + for path in REPO_ROOT.rglob("*.html"): + if "templates" not in path.parts: + continue + if SKIP_DIRS.intersection(path.parts): + continue + yield path + + +def _multiline_comment_lines(text): + """Return the 1-based line numbers where a ``{#`` opens but doesn't close on + the same line (i.e. a multi-line ``{# #}`` comment).""" + bad = [] + for n, line in enumerate(text.splitlines(), 1): + idx = line.find("{#") + if idx != -1 and "#}" not in line[idx + 2:]: + bad.append(n) + return bad + + +class TemplateCommentLintTests(SimpleTestCase): + def test_no_multiline_django_comments(self): + offenders = [] + for path in _template_html_files(): + text = path.read_text(encoding="utf-8") + for n in _multiline_comment_lines(text): + offenders.append(f"{path.relative_to(REPO_ROOT)}:{n}") + + self.assertEqual( + offenders, [], + "Multi-line Django `{# #}` comment(s) found — Django renders these as " + "visible page text. Use `{% comment %}…{% endcomment %}` instead:\n " + + "\n ".join(offenders), + ) + + def test_detector_catches_a_multiline_comment(self): + # Pin the detection logic itself so the guard can't silently rot. + self.assertEqual(_multiline_comment_lines("{# one line ok #}\n

x

"), []) + self.assertEqual( + _multiline_comment_lines("

x

\n{# this opens\n and closes later #}\n"), + [2], + )