diff --git a/layouts/glossary/single.html b/layouts/glossary/single.html index 12f797527b0..9e97836b6c6 100644 --- a/layouts/glossary/single.html +++ b/layouts/glossary/single.html @@ -171,6 +171,7 @@ {{ range $index, $term := . }} {{ $term_str := printf "%s" $term }} {{ $term_clean := replace $term_str "\u00a0" " " }} + {{ $term_clean = replaceRE `[*\\]` "" $term_clean }} {{ $term_clean = replaceRE "\\s*\\(.*\\)" "" $term_clean }} {{ $term_clean = replace $term_clean " " "_" }} {{ $term_clean = replaceRE "[^a-zA-Z0-9_]" "_" $term_clean }} @@ -182,9 +183,9 @@ {{ end }} {{ if $term_page }} {{ $term_url := $term_page.RelPermalink }} - {{ $related_links = $related_links | append (printf "%s" $term_url $term) }} + {{ $related_links = $related_links | append (printf "%s" $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 [$2]` (. | 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 [$2]` (. | htmlEscape | markdownify | string) | safeHTML }} {{ return $out }} diff --git a/layouts/partials/functions/plain_title.html b/layouts/partials/functions/plain_title.html new file mode 100644 index 00000000000..00bfece6cd1 --- /dev/null +++ b/layouts/partials/functions/plain_title.html @@ -0,0 +1,6 @@ +{{/* Plain text of a page's title (or link title) for , og:title, breadcrumbs and JSON-LD. + Input: dict "page" and "text". Glossary titles may carry inline Markdown (an italic *p*), + which is rendered and then stripped; other pages' text passes through unchanged. */}} +{{ $text := .text }} +{{ if eq .page.Type "glossary" }}{{ $text = $text | htmlEscape | markdownify | plainify | htmlUnescape }}{{ end }} +{{ return $text }} diff --git a/layouts/partials/functions/resource_metadata.html b/layouts/partials/functions/resource_metadata.html index 50d1364abbb..2ff3bd37fbf 100644 --- a/layouts/partials/functions/resource_metadata.html +++ b/layouts/partials/functions/resource_metadata.html @@ -2,6 +2,12 @@ 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 }} +{{ if eq .Type "glossary" }} + {{ $metadata = merge $metadata (dict + "title" (partial "functions/plain_title.html" (dict "page" . "text" .Title)) + "breadcrumb_title" (partial "functions/plain_title.html" (dict "page" . "text" (.LinkTitle | default .Title))) + ) }} +{{ end }} {{ if and .IsSection (eq .Section "resources") }} {{ $index := partialCached "doors/resource_index" . "resources" }} {{ $pager := .Paginate $index.pages 24 }} diff --git a/layouts/partials/jsonld/main.html b/layouts/partials/jsonld/main.html index 5db5310bd3d..ac59dd75124 100644 --- a/layouts/partials/jsonld/main.html +++ b/layouts/partials/jsonld/main.html @@ -58,7 +58,7 @@ "@type" "ListItem" "position" (add $i 1) "url" $item.Permalink - "name" $item.LinkTitle + "name" (partial "functions/plain_title.html" (dict "page" $item "text" $item.LinkTitle)) ) }} {{ end }} diff --git a/layouts/partials/structured-data/glossary-term.html b/layouts/partials/structured-data/glossary-term.html index 69098748aa2..393c3d0c731 100644 --- a/layouts/partials/structured-data/glossary-term.html +++ b/layouts/partials/structured-data/glossary-term.html @@ -18,7 +18,7 @@ {{- $term := dict "@context" "https://schema.org" "@type" "DefinedTerm" - "name" .Title + "name" (partial "functions/plain_title.html" (dict "page" . "text" .Title)) "description" $desc "url" .Permalink "inLanguage" $lang diff --git a/scripts/check_seo_html.py b/scripts/check_seo_html.py index fedb1087569..6bb657a68c6 100644 --- a/scripts/check_seo_html.py +++ b/scripts/check_seo_html.py @@ -39,6 +39,7 @@ 16. Every glossary term link points to an existing English glossary page (#227). 17. Every link in a visible breadcrumb trail (nav.crumbs) has a non-empty href, so a section that is not rendered cannot leave an empty link in the trail. + 18. No glossary page's <title> or <h1> shows Markdown emphasis as typed, e.g. "(*p*)". Report-only sections (never fail): pages with no <h1>, the site-wide share of images that carry both width and height, pages whose @@ -262,6 +263,18 @@ def check_glossary_links(pages): return failures +# Paired asterisks around text: Markdown emphasis shown as typed. Asterisks inside a word +# ("G*Power", "P*Hack") are not delimiters. +LITERAL_EMPHASIS = re.compile(r"(?<!\w)\*[^*\s](?:[^*]*[^*\s])?\*(?!\w)") + + +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)] @@ -562,6 +575,7 @@ def main(public_dir): results = { '1. no <h1>Search</h1>': 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 <img> has an alt attribute": check_img_alt(pages), "5. every homepage <img> has width and height": check_homepage_img_dims(home), diff --git a/scripts/rebrand/allowed_changes.txt b/scripts/rebrand/allowed_changes.txt index 44fadc63fef..14fc276c728 100644 --- a/scripts/rebrand/allowed_changes.txt +++ b/scripts/rebrand/allowed_changes.txt @@ -30,6 +30,12 @@ post/*/featured_hu.jpg # The page's resized image is now WebP, so its post/*/featured_hu.png # The same ai-metascience/featured_hu.png # The same coc/index.html # Its title was empty (" | FORRT"), and it now says Code of Conduct +glossary/english/research_contribution_metric__p_/index.html # #1085: glossary titles render their Markdown, so the <title>, og:title and JSON-LD name read "(p)" instead of "(*p*)" +glossary/german/p/index.html # #1085: glossary titles render their Markdown, so the <title>, og:title and JSON-LD name read "(p)" instead of "(*p*)" +glossary/arabic/مقياس_المساهمة_البحثي_ة_نشر/index.html # #1085: glossary titles render their Markdown, so the <title>, og:title and JSON-LD name read "(p)" instead of "(*p*)" +glossary/arabic/p_مقياس_المساهمة_البحثي_ة_نشر/index.html # #1085: the same Arabic page under the older slug that the content committed on this branch still uses +glossary/chinese/研究贡献指标_p_/index.html # #1085: glossary titles render their Markdown, so the <title>, og:title and JSON-LD name read "(p)" instead of "(*p*)" +glossary/turkish/bilimsel_katkı_olcutu_p_/index.html # #1085: glossary titles render their Markdown, so the <title>, og:title and JSON-LD name read "(p)" instead of "(*p*)" about/charity/gep/index.html # #973: redirect from the address printed in the GEP PDF to /about/charity/, not a page data/glossary-terms.*.json # Build-time English glossary lookup, fetched on first term interaction (#227); not a page cv/index.html # Retitled as a CV template in the content review (#1079). Exempts all its SEO tags: remove once main has the new title diff --git a/scripts/tests/test_check_seo_html.py b/scripts/tests/test_check_seo_html.py index 23b8ce5e5ff..e625ed29865 100644 --- a/scripts/tests/test_check_seo_html.py +++ b/scripts/tests/test_check_seo_html.py @@ -192,6 +192,24 @@ def test_other_links_are_ignored(self): self.assertEqual(self.check('<a href="#elsewhere">Link</a>'), []) +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"), []) + self.assertEqual(self.check("G*Power and P*Hack | FORRT", "G*Power and P*Hack"), []) + + 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(len(self.check("x", "*p*-value")), 1) + self.assertEqual(self.check("x", "*p*", path="post/x/index.html"), []) + + class GlossaryLinkCheck(unittest.TestCase): def check_links(self, hrefs, base="https://forrt.org/"): with tempfile.TemporaryDirectory() as public: @@ -227,3 +245,4 @@ def test_crumb_links_need_a_target(self): if __name__ == "__main__": unittest.main() +