Hreflang alternates on category placeholder pages too
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user