Skip to content
5 changes: 3 additions & 2 deletions layouts/glossary/single.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand All @@ -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)) }}
Comment thread
LukasWallrich marked this conversation as resolved.
Resolved
{{ 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 }}
Expand Down
8 changes: 5 additions & 3 deletions layouts/partials/doors/term_title.html
Original file line number Diff line number Diff line change
@@ -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 }}
6 changes: 6 additions & 0 deletions layouts/partials/functions/plain_title.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{{/* Plain text of a page's title (or link title) for <title>, 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 }}
6 changes: 6 additions & 0 deletions layouts/partials/functions/resource_metadata.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion layouts/partials/jsonld/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
2 changes: 1 addition & 1 deletion layouts/partials/structured-data/glossary-term.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions scripts/check_seo_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)]

Expand Down Expand Up @@ -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),
Comment thread
LukasWallrich marked this conversation as resolved.
Resolved
"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),
Expand Down
6 changes: 6 additions & 0 deletions scripts/rebrand/allowed_changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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*)"
Comment thread
LukasWallrich marked this conversation as resolved.
Resolved
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
Expand Down
19 changes: 19 additions & 0 deletions scripts/tests/test_check_seo_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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</title><h1>%s</h1>" % (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 (<em>p</em>)"), [])
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:
Expand Down Expand Up @@ -227,3 +245,4 @@ def test_crumb_links_need_a_target(self):

if __name__ == "__main__":
unittest.main()

Loading