From ca13ecce94e83b2f98742ddc7a074afdb03aa905 Mon Sep 17 00:00:00 2001 From: Lukas Wallrich Date: Mon, 9 Nov 2026 17:56:36 +0000 Subject: [PATCH 1/6] Render Markdown in glossary term titles and related terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Term titles in the A–Z list, breadcrumb and heading now go through markdownify, so 'Research Contribution Metric (*p*)' shows an italic p. The page , og:title and JSON-LD get the plain-text form. Related terms drop '*' and '\' before building the link slug and render their Markdown, so '*P*\-hacking' links to p-hacking and shows italic. check_seo_html.py gains check 17, which fails if a glossary page's <title> or <h1> shows paired asterisks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- layouts/glossary/single.html | 5 +++-- layouts/partials/doors/term_title.html | 8 +++++--- .../partials/functions/resource_metadata.html | 5 +++++ scripts/check_seo_html.py | 12 ++++++++++++ scripts/tests/test_check_seo_html.py | 17 +++++++++++++++++ 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/layouts/glossary/single.html b/layouts/glossary/single.html index 190aa49a9a8..2f3f3f60517 100644 --- a/layouts/glossary/single.html +++ b/layouts/glossary/single.html @@ -171,6 +171,7 @@ {{ range $index, $term := . }} {{ $term_str := printf "%s" $term }} <!-- Ensure $term is treated as a string --> {{ $term_clean := replace $term_str "\u00a0" " " }} <!-- Replace non-breaking space with space --> + {{ $term_clean = replaceRE `[*\\]` "" $term_clean }} <!-- Drop Markdown emphasis and escapes (*p*-value) so the slug matches the title's --> {{ $term_clean = replaceRE "\\s*\\(.*\\)" "" $term_clean }} <!-- Remove text in parentheses --> {{ $term_clean = replace $term_clean " " "_" }} <!-- Replace spaces with underscores --> {{ $term_clean = replaceRE "[^a-zA-Z0-9_]" "_" $term_clean }} <!-- Replace all non-alphanumeric characters with underscores --> @@ -182,9 +183,9 @@ {{ end }} {{ if $term_page }} {{ $term_url := $term_page.RelPermalink }} - {{ $related_links = $related_links | append (printf "<a href='%s'>%s</a>" $term_url $term) }} + {{ $related_links = $related_links | append (printf "<a href='%s'>%s</a>" $term_url ($term_str | htmlEscape | markdownify)) }} {{ else }} - {{ $related_links = $related_links | append $term }} + {{ $related_links = $related_links | append ($term_str | htmlEscape | markdownify | string) }} {{ end }} {{ end }} {{ $related_links_string := delimit $related_links ", " | safeHTML }} diff --git a/layouts/partials/doors/term_title.html b/layouts/partials/doors/term_title.html index b714e0b5815..e2bf277e63a 100644 --- a/layouts/partials/doors/term_title.html +++ b/layouts/partials/doors/term_title.html @@ -1,4 +1,6 @@ -{{/* A glossary term's title. Translated terms end in the English title in square brackets, which - must stay left to right inside an Arabic heading, or its brackets and words change places. */}} -{{ $out := replaceRE `^(.*) \[(.*)\]$` `$1 <bdi dir="ltr" lang="en">[$2]</bdi>` (. | htmlEscape) | safeHTML }} +{{/* A glossary term's title. Titles may carry inline Markdown, e.g. the italic p in "Research + Contribution Metric (*p*)"; the title is escaped first so it cannot inject HTML. Translated terms end + in the English title in square brackets, which must stay left to right inside an Arabic heading, + or its brackets and words change places. */}} +{{ $out := replaceRE `^(.*) \[(.*)\]$` `$1 <bdi dir="ltr" lang="en">[$2]</bdi>` (. | htmlEscape | markdownify | string) | safeHTML }} {{ return $out }} diff --git a/layouts/partials/functions/resource_metadata.html b/layouts/partials/functions/resource_metadata.html index 50d1364abbb..358c3adbd9c 100644 --- a/layouts/partials/functions/resource_metadata.html +++ b/layouts/partials/functions/resource_metadata.html @@ -2,6 +2,11 @@ pagination on other lists: their templates choose their own collections. */}} {{ $description := partialCached "functions/page_description.html" . .RelPermalink }} {{ $metadata := dict "url" .Permalink "title" .Title "breadcrumb_title" (.LinkTitle | default .Title) "description" $description "pages" .Pages }} +{{/* Glossary titles may carry inline Markdown (an italic *p*); plain text for <title>, og:title and JSON-LD. */}} +{{ if eq .Type "glossary" }} + {{ $plain := .Title | htmlEscape | markdownify | plainify | htmlUnescape }} + {{ $metadata = merge $metadata (dict "title" $plain "breadcrumb_title" $plain) }} +{{ end }} {{ if and .IsSection (eq .Section "resources") }} {{ $index := partialCached "doors/resource_index" . "resources" }} {{ $pager := .Paginate $index.pages 24 }} diff --git a/scripts/check_seo_html.py b/scripts/check_seo_html.py index 41ce3f7d0cb..ef4937e6f24 100644 --- a/scripts/check_seo_html.py +++ b/scripts/check_seo_html.py @@ -251,6 +251,17 @@ def check_glossary_links(pages): return failures +# Paired asterisks around text: Markdown emphasis shown as typed. A lone one ("G*Power") is fine. +LITERAL_EMPHASIS = re.compile(r"\*[^*\s][^*]*\*") + + +def check_glossary_title_markdown(pages): + """Glossary titles such as "Research Contribution Metric (*p*)" must render, not show asterisks.""" + return [p.path for p in pages + if p.path.replace(os.sep, "/").startswith("glossary/") + and any(LITERAL_EMPHASIS.search(text) for text in [p.title, *p.h1_texts])] + + def check_no_search_heading(pages): return [p.path for p in pages if any(t.lower() == "search" for t in p.h1_texts)] @@ -564,6 +575,7 @@ def main(public_dir): "14. no inline <script> over 200 KB": check_inline_scripts(pages), "15. every citation has a local reference target": check_citation_targets(pages), "16. glossary links reach existing English terms": check_glossary_links(pages), + "17. glossary titles show no Markdown asterisks": check_glossary_title_markdown(pages), } conditional = check_conditional_assets(pages) results["3. conditional highlight.js / mermaid loading"] = [ diff --git a/scripts/tests/test_check_seo_html.py b/scripts/tests/test_check_seo_html.py index 51fbbc6dfdd..bf57789ade1 100644 --- a/scripts/tests/test_check_seo_html.py +++ b/scripts/tests/test_check_seo_html.py @@ -217,5 +217,22 @@ def test_missing_non_english_external_and_empty_links_fail(self): self.assertEqual(len(self.check_links(links)), len(links)) +class GlossaryTitleMarkdownCheck(unittest.TestCase): + def check(self, title, h1, path="glossary/english/term/index.html"): + html = "<title>%s

%s

" % (title, h1) + return seo.check_glossary_title_markdown([page_from_html(html, path)]) + + def test_rendered_emphasis_and_lone_asterisk_pass(self): + self.assertEqual(self.check("Research Contribution Metric (p) | FORRT", + "Research Contribution Metric (p)"), []) + self.assertEqual(self.check("G*Power | FORRT", "G*Power"), []) + + def test_literal_emphasis_fails_on_glossary_pages_only(self): + self.assertEqual(len(self.check("Research Contribution Metric (*p*) | FORRT", "x")), 1) + self.assertEqual(len(self.check("x", "Research Contribution Metric (*p*)")), 1) + self.assertEqual(self.check("x", "*p*", path="post/x/index.html"), []) + + if __name__ == "__main__": unittest.main() + From 1573f22079a0cd3e12acaf86fcf46231e3fcc785 Mon Sep 17 00:00:00 2001 From: Lukas Wallrich Date: Mon, 9 Nov 2026 18:01:39 +0000 Subject: [PATCH 2/6] Number the glossary title check 18 and keep it apart from the cluster title check Co-Authored-By: Claude Opus 5.5 --- scripts/check_seo_html.py | 2 +- scripts/tests/test_check_seo_html.py | 32 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/scripts/check_seo_html.py b/scripts/check_seo_html.py index ef4937e6f24..e0e5e9db047 100644 --- a/scripts/check_seo_html.py +++ b/scripts/check_seo_html.py @@ -562,6 +562,7 @@ def main(public_dir): results = { '1. no

Search

': check_no_search_heading(pages), + "18. glossary titles show no Markdown asterisks": check_glossary_title_markdown(pages), "2. homepage free of mermaid/highlight.js assets": check_homepage_assets(home), "4. every has an alt attribute": check_img_alt(pages), "5. every homepage has width and height": check_homepage_img_dims(home), @@ -575,7 +576,6 @@ def main(public_dir): "14. no inline