Hreflang alternates on category placeholder pages too

This commit is contained in:
2026-09-04 18:12:48 +00:00
parent 11a138e19f
commit 13fecd2118
3 changed files with 46 additions and 21 deletions
+3 -1
View File
@@ -106,7 +106,9 @@ Region tags normalize to their base subtag (`fi-FI` → `fi`).
language like content pages, but over the **subtree's** combined language like content pages, but over the **subtree's** combined
availability (`subtree_languages`) — they have no chunks of their own; availability (`subtree_languages`) — they have no chunks of their own;
the heading, navigation and card text localize from the title map and the heading, navigation and card text localize from the title map and
the target articles' translations. the target articles' translations. Their hreflang alternates are
computed exactly like a content page's (a translated title counts as
availability, so the language selector is offered there too).
- Card descriptions and cover picks run on the target article's hybrid - Card descriptions and cover picks run on the target article's hybrid
Markdown where that page is available in the served language, with Markdown where that page is available in the served language, with
per-card fallback to the original. per-card fallback to the original.
+1
View File
@@ -132,6 +132,7 @@ def _render_html(
data.theme, data.theme,
data.favicon, data.favicon,
data.brand_html, data.brand_html,
base_url,
transition=data.transition, transition=data.transition,
lang=lang, lang=lang,
translation=translation, translation=translation,
+42 -20
View File
@@ -1021,6 +1021,39 @@ def _social_meta(
} }
def _language_urls(
data: Data,
path: str,
node: Node,
lang: str,
original: str,
base_url: str,
) -> tuple[str, list[tuple[str, str]]]:
"""(canonical, hreflang alternates) for a page (docs/localization.md).
The canonical names the actually served language — the plain URL for
the original (for SEO the non-query URL means the article's language),
?lang= for a translation — regardless of how the language was arrived
at (query or header). The alternates list the languages the page is
actually available in (``node.langs``; a category label's title counts
as its content): x-default first (the plain, autodetecting URL), then
every available language — the original again by its plain URL,
translations by ?lang=. The public language selector keys off these.
("", []) without a base_url.
"""
if not base_url:
return "", []
url = f"{base_url}/{path}"
canonical = url if lang == original else f"{url}?lang={lang}"
alternates = []
if data.translate_langs:
alternates = [("x-default", url)] + [
(tag, url if tag == original else f"{url}?lang={tag}")
for tag in sorted({original, *node.langs})
]
return canonical, alternates
def render_page( def render_page(
menu: dict[str, Node], menu: dict[str, Node],
data: Data, data: Data,
@@ -1050,24 +1083,7 @@ def render_page(
title = _title(path.rpartition("/")[2], node, translation, path) title = _title(path.rpartition("/")[2], node, translation, path)
main = page_content(menu, data, path, translation, link_lang, lang) main = page_content(menu, data, path, translation, link_lang, lang)
social = _social_meta(node, path, title, str(main), brand, base_url) social = _social_meta(node, path, title, str(main), brand, base_url)
# Canonical/hreflang URLs (docs/localization.md): the canonical names canonical, alternates = _language_urls(data, path, node, lang, original, base_url)
# the actually served language — the plain URL for the original (for
# SEO the non-query URL means the article's language), ?lang= for a
# translation — regardless of how the language was arrived at (query
# or header). The alternates list the languages the page is actually
# available in: x-default first (the plain, autodetecting URL), then
# every available language — the original again by its plain URL,
# translations by ?lang=. The public language selector keys off these.
canonical = ""
alternates = []
if base_url:
url = f"{base_url}/{path}"
canonical = url if lang == original else f"{url}?lang={lang}"
if data.translate_langs:
alternates = [("x-default", url)] + [
(tag, url if tag == original else f"{url}?lang={tag}")
for tag in sorted({original, *node.langs})
]
return str( return str(
_layout( _layout(
*_page_assets(), *_page_assets(),
@@ -1100,6 +1116,7 @@ def render_category(
theme: str = "", theme: str = "",
favicon: str = "", favicon: str = "",
brand_html: str = "", brand_html: str = "",
base_url: str = "",
transition: str = "cube", transition: str = "cube",
lang: str = i18n.ORIGINAL_LANGUAGE, lang: str = i18n.ORIGINAL_LANGUAGE,
translation: Translation | None = None, translation: Translation | None = None,
@@ -1115,12 +1132,16 @@ def render_category(
With a translation (titles only — the category has no Markdown) the With a translation (titles only — the category has no Markdown) the
heading, navigation and card text localize per target article heading, navigation and card text localize per target article
(docs/localization.md); ``link_lang`` replicates the ?lang= override (docs/localization.md); ``link_lang`` replicates the ?lang= override
onto the navigation links as on content pages. onto the navigation links as on content pages. The hreflang alternates
are computed as on content pages — a translated title makes the
language available here too.
""" """
node = resolve(menu, path)[-1] node = resolve(menu, path)[-1]
original = i18n.primary_lang(menu, path)
if translation is None: if translation is None:
lang = i18n.primary_lang(menu, path) lang = original
title = _title(path.rpartition("/")[2], node, translation, path) title = _title(path.rpartition("/")[2], node, translation, path)
_, alternates = _language_urls(data, path, node, lang, original, base_url)
doc = E.article doc = E.article
with doc: with doc:
doc.h1(title) doc.h1(title)
@@ -1137,6 +1158,7 @@ def render_category(
transition, transition,
favicon, favicon,
lang=lang, lang=lang,
alternates=alternates,
)( )(
Title=f"{title} {brand}" if brand else title, Title=f"{title} {brand}" if brand else title,
Brand=_brand_link(brand, brand_html, link_lang), Brand=_brand_link(brand, brand_html, link_lang),