Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13fecd2118 | ||
|
|
11a138e19f | ||
|
|
ebd5911a38 |
+2
-2
@@ -73,10 +73,10 @@ Each `Client` record (shared by every event, keyed by hash):
|
||||
A reverse-DNS lookup is attempted for each new client and the result, when
|
||||
available, is stored as `host`; local/reserved/multicast addresses are
|
||||
skipped. If a DB-IP MMDB file (`dbip-*.mmdb` or `dbip-*.mmdb.gz`) is present
|
||||
in the repository root, it is loaded at startup and used to look up
|
||||
in the working directory, it is loaded at startup and used to look up
|
||||
`country`/`city`. These lookups run in background tasks after the event is
|
||||
stored, so WebSocket message handling is never delayed. The decompressed
|
||||
`dbip-*.mmdb` file is kept in the repository root and ignored by git. The
|
||||
`dbip-*.mmdb` file is kept in the working directory and ignored by git. The
|
||||
CLI flag `--dbip` (`uv run pagerite --dbip`) downloads the latest
|
||||
`dbip-city-lite-YYYY-MM.mmdb.gz` from DB-IP at startup (in the app lifespan,
|
||||
before the MMDB is opened), skipping the download when the local database is
|
||||
|
||||
@@ -106,7 +106,9 @@ Region tags normalize to their base subtag (`fi-FI` → `fi`).
|
||||
language like content pages, but over the **subtree's** combined
|
||||
availability (`subtree_languages`) — they have no chunks of their own;
|
||||
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
|
||||
Markdown where that page is available in the served language, with
|
||||
per-card fallback to the original.
|
||||
|
||||
@@ -132,6 +132,7 @@ def _render_html(
|
||||
data.theme,
|
||||
data.favicon,
|
||||
data.brand_html,
|
||||
base_url,
|
||||
transition=data.transition,
|
||||
lang=lang,
|
||||
translation=translation,
|
||||
|
||||
@@ -44,8 +44,9 @@ _analytics_ws_clients: set[WebSocket] = set()
|
||||
_analytics_broadcast_task: asyncio.Task | None = None
|
||||
|
||||
|
||||
# Repository root from this file's location (pagerite/tracking.py -> ..).
|
||||
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
# DB-IP databases persist in the working directory (one download serves all
|
||||
# sites run from it). Not the package directory: reinstalls/upgrades wipe it.
|
||||
_DBIP_DIR = Path.cwd()
|
||||
|
||||
DBIP_URL = "https://download.db-ip.com/free/dbip-city-lite-{month}.mmdb.gz"
|
||||
|
||||
@@ -60,7 +61,7 @@ def _download_dbip() -> None:
|
||||
|
||||
existing = sorted(
|
||||
p.stem.removeprefix("dbip-city-lite-").removesuffix(".mmdb")
|
||||
for p in _REPO_ROOT.glob("dbip-city-lite-*.mmdb*")
|
||||
for p in _DBIP_DIR.glob("dbip-city-lite-*.mmdb*")
|
||||
)
|
||||
if existing and existing[-1] >= months[0]:
|
||||
logger.info("DB-IP database is current (%s), skipping download", existing[-1])
|
||||
@@ -68,7 +69,7 @@ def _download_dbip() -> None:
|
||||
|
||||
for month in months:
|
||||
url = DBIP_URL.format(month=month)
|
||||
target = _REPO_ROOT / f"dbip-city-lite-{month}.mmdb.gz"
|
||||
target = _DBIP_DIR / f"dbip-city-lite-{month}.mmdb.gz"
|
||||
tmp = target.with_suffix(".mmdb.gz.tmp")
|
||||
logger.info("Downloading %s", url)
|
||||
try:
|
||||
@@ -93,7 +94,7 @@ def _download_dbip() -> None:
|
||||
continue
|
||||
os.replace(tmp, target)
|
||||
# Drop older databases so the app never picks up a stale one.
|
||||
for old in _REPO_ROOT.glob("dbip-city-lite-*.mmdb*"):
|
||||
for old in _DBIP_DIR.glob("dbip-city-lite-*.mmdb*"):
|
||||
if old.name != target.name:
|
||||
old.unlink()
|
||||
logger.info("DB-IP database updated to %s", target.name)
|
||||
@@ -102,13 +103,13 @@ def _download_dbip() -> None:
|
||||
|
||||
|
||||
def _geoip_db_path() -> Path | None:
|
||||
"""Find a DB-IP MMDB in the repo root, preferring an already-decompressed
|
||||
"""Find a DB-IP MMDB in the working directory, preferring an already-decompressed
|
||||
``.mmdb`` over the matching ``.mmdb.gz``. Returns None if none is present.
|
||||
"""
|
||||
mmdb = sorted(_REPO_ROOT.glob("dbip-*.mmdb"))
|
||||
mmdb = sorted(_DBIP_DIR.glob("dbip-*.mmdb"))
|
||||
if mmdb:
|
||||
return mmdb[0]
|
||||
gz = sorted(_REPO_ROOT.glob("dbip-*.mmdb.gz"))
|
||||
gz = sorted(_DBIP_DIR.glob("dbip-*.mmdb.gz"))
|
||||
if gz:
|
||||
return gz[0]
|
||||
return None
|
||||
|
||||
@@ -101,7 +101,8 @@ ClientMsg = Hello | Result
|
||||
def pending_items(data: Data, lang: str) -> list[TransItem]:
|
||||
"""Fragments of the site still untranslated for ``lang``, deduped by key.
|
||||
|
||||
Every page node (published or not) contributes its title and each chunk
|
||||
Every node (published or not, pages and pure category labels alike)
|
||||
contributes its title; pages also contribute each chunk
|
||||
that needs translation (``needs_translation``), is not editor-flagged
|
||||
no-translate (``node.no_trans``) and has no ``trans`` entry for ``lang``
|
||||
yet. Content-addressed text (shared paragraphs, repeated titles) appears
|
||||
@@ -133,8 +134,10 @@ def pending_items(data: Data, lang: str) -> list[TransItem]:
|
||||
path = f"{prefix}/{slug}" if prefix else slug
|
||||
# An article whose primary language IS the target needs no
|
||||
# translation into it — skip its title and chunks entirely.
|
||||
# Category labels (chunks is None) contribute only their title:
|
||||
# it is their nav-menu label.
|
||||
node_lang = node.language or inherited
|
||||
if node.chunks is not None and node_lang != lang:
|
||||
if node_lang != lang:
|
||||
if node.title:
|
||||
emit(
|
||||
chunk_key(node.title),
|
||||
@@ -143,7 +146,7 @@ def pending_items(data: Data, lang: str) -> list[TransItem]:
|
||||
"title",
|
||||
context=opening(node),
|
||||
)
|
||||
for h in node.chunks:
|
||||
for h in node.chunks or ():
|
||||
text = data.chunks.get(h)
|
||||
if (
|
||||
text is not None
|
||||
@@ -178,8 +181,8 @@ def store_results(data: Data, lang: str, items: list[TransResult]) -> list[str]:
|
||||
for slug, node in sorted_nodes(nodes):
|
||||
path = f"{prefix}/{slug}" if prefix else slug
|
||||
node_lang = node.language or inherited
|
||||
if node.chunks is not None and node_lang != lang:
|
||||
keys = set(node.chunks)
|
||||
if node_lang != lang:
|
||||
keys = set(node.chunks or ())
|
||||
if node.title:
|
||||
keys.add(chunk_key(node.title))
|
||||
if keys & stored:
|
||||
|
||||
+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(
|
||||
menu: dict[str, Node],
|
||||
data: Data,
|
||||
@@ -1050,24 +1083,7 @@ def render_page(
|
||||
title = _title(path.rpartition("/")[2], node, translation, path)
|
||||
main = page_content(menu, data, path, translation, link_lang, lang)
|
||||
social = _social_meta(node, path, title, str(main), brand, base_url)
|
||||
# Canonical/hreflang URLs (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: 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})
|
||||
]
|
||||
canonical, alternates = _language_urls(data, path, node, lang, original, base_url)
|
||||
return str(
|
||||
_layout(
|
||||
*_page_assets(),
|
||||
@@ -1100,6 +1116,7 @@ def render_category(
|
||||
theme: str = "",
|
||||
favicon: str = "",
|
||||
brand_html: str = "",
|
||||
base_url: str = "",
|
||||
transition: str = "cube",
|
||||
lang: str = i18n.ORIGINAL_LANGUAGE,
|
||||
translation: Translation | None = None,
|
||||
@@ -1115,12 +1132,16 @@ def render_category(
|
||||
With a translation (titles only — the category has no Markdown) the
|
||||
heading, navigation and card text localize per target article
|
||||
(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]
|
||||
original = i18n.primary_lang(menu, path)
|
||||
if translation is None:
|
||||
lang = i18n.primary_lang(menu, path)
|
||||
lang = original
|
||||
title = _title(path.rpartition("/")[2], node, translation, path)
|
||||
_, alternates = _language_urls(data, path, node, lang, original, base_url)
|
||||
doc = E.article
|
||||
with doc:
|
||||
doc.h1(title)
|
||||
@@ -1137,6 +1158,7 @@ def render_category(
|
||||
transition,
|
||||
favicon,
|
||||
lang=lang,
|
||||
alternates=alternates,
|
||||
)(
|
||||
Title=f"{title} – {brand}" if brand else title,
|
||||
Brand=_brand_link(brand, brand_html, link_lang),
|
||||
|
||||
Reference in New Issue
Block a user