fix(cms): open analytics DB in worker + fix per-language article upsert

Three bugs causing 0 articles after generation:

1. Worker never called open_analytics_db() — fetch_analytics always
   returned [] since _conn was None. Generation completed silently with
   0 rows from DuckDB.

2. generate_articles upserted by url_path alone — after the URL prefix
   fix, all languages share the same url_path (e.g. /markets/de/berlin).
   The EN article was overwriting the DE article on every generation.
   Fixed: deduplicate on (url_path, language).

3. article_page and _filter_articles didn't filter by language — with
   shared url_paths a DE request could serve an EN article, and the
   markets hub would show duplicate entries per city. Fixed: both now
   filter by g.lang from the /<lang> blueprint prefix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-24 03:04:01 +01:00
parent 7f3bde56b6
commit ebc944a6d3
3 changed files with 19 additions and 10 deletions

View File

@@ -434,9 +434,11 @@ async def generate_articles(
md_dir.mkdir(parents=True, exist_ok=True)
(md_dir / f"{article_slug}.md").write_text(body_md)
# Upsert article in SQLite
# Upsert article in SQLite — keyed by (url_path, language) since
# multiple languages share the same url_path
existing_article = await fetch_one(
"SELECT id FROM articles WHERE url_path = ?", (url_path,),
"SELECT id FROM articles WHERE url_path = ? AND language = ?",
(url_path, lang),
)
if existing_article:
await execute(
@@ -444,8 +446,8 @@ async def generate_articles(
SET title = ?, meta_description = ?, template_slug = ?,
language = ?, date_modified = ?, updated_at = ?,
seo_head = ?
WHERE url_path = ?""",
(title, meta_desc, slug, lang, now_iso, now_iso, seo_head, url_path),
WHERE url_path = ? AND language = ?""",
(title, meta_desc, slug, lang, now_iso, now_iso, seo_head, url_path, lang),
)
else:
await execute(

View File

@@ -7,7 +7,7 @@ from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from markupsafe import Markup
from quart import Blueprint, abort, render_template, request
from quart import Blueprint, abort, g, render_template, request
from ..core import capture_waitlist_email, csrf_protect, feature_gate, fetch_all, fetch_one
from ..i18n import get_translations
@@ -172,11 +172,14 @@ async def market_results():
async def _filter_articles(q: str, country: str, region: str) -> list[dict]:
"""Query published articles with optional FTS + country/region filters."""
"""Query published articles for the current language."""
lang = g.get("lang", "en")
if q:
# FTS query
wheres = ["articles_fts MATCH ?"]
params: list = [q]
wheres.append("a.language = ?")
params.append(lang)
if country:
wheres.append("a.country = ?")
params.append(country)
@@ -194,8 +197,8 @@ async def _filter_articles(q: str, country: str, region: str) -> list[dict]:
tuple(params),
)
else:
wheres = ["status = 'published'", "published_at <= datetime('now')"]
params = []
wheres = ["status = 'published'", "published_at <= datetime('now')", "language = ?"]
params = [lang]
if country:
wheres.append("country = ?")
params.append(country)
@@ -218,12 +221,13 @@ async def _filter_articles(q: str, country: str, region: str) -> list[dict]:
async def article_page(url_path: str):
"""Serve a published article by its url_path."""
clean_path = "/" + url_path.strip("/")
lang = g.get("lang", "en")
article = await fetch_one(
"""SELECT * FROM articles
WHERE url_path = ? AND status = 'published'
WHERE url_path = ? AND language = ? AND status = 'published'
AND published_at <= datetime('now')""",
(clean_path,),
(clean_path, lang),
)
if not article:
abort(404)

View File

@@ -756,6 +756,9 @@ async def run_worker(poll_interval: float = 1.0) -> None:
"""Main worker loop."""
print("[WORKER] Starting...")
await init_db()
from .analytics import open_analytics_db
open_analytics_db()
print("[WORKER] Analytics DB opened.")
while True:
try: