diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index 89336cc..d20cc47 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -31,6 +31,42 @@ from ..core import ( slugify, ) +def _build_article_seo_head( + url_path: str, + title: str, + meta_desc: str, + language: str, + published_at: str, + *, + base_url: str = "https://padelnomics.io", +) -> str: + """Build SEO head block (canonical, OG, JSON-LD) for a manually created article.""" + def _esc(text: str) -> str: + return text.replace("&", "&").replace('"', """).replace("<", "<") + + full_url = f"{base_url}/{language}{url_path}" + jsonld = json.dumps({ + "@context": "https://schema.org", + "@type": "Article", + "headline": title[:110], + "description": meta_desc[:200], + "url": full_url, + "inLanguage": language, + "datePublished": published_at, + "dateModified": published_at, + "author": {"@type": "Organization", "name": "Padelnomics", "url": "https://padelnomics.io"}, + "publisher": {"@type": "Organization", "name": "Padelnomics", "url": "https://padelnomics.io"}, + }, ensure_ascii=False) + return "\n".join([ + f'', + f'', + f'', + f'', + '', + f'', + ]) + + # Blueprint with its own template folder bp = Blueprint( "admin", @@ -1716,6 +1752,7 @@ async def article_new(): country = form.get("country", "").strip() region = form.get("region", "").strip() body = form.get("body", "").strip() + language = form.get("language", "en").strip() or "en" status = form.get("status", "draft") published_at = form.get("published_at", "").strip() @@ -1731,9 +1768,9 @@ async def article_new(): body_html = mistune.html(body) body_html = await bake_scenario_cards(body_html) - BUILD_DIR.mkdir(parents=True, exist_ok=True) - build_path = BUILD_DIR / f"{article_slug}.html" - build_path.write_text(body_html) + build_dir = BUILD_DIR / language + build_dir.mkdir(parents=True, exist_ok=True) + (build_dir / f"{article_slug}.html").write_text(body_html) # Save markdown source md_dir = Path("data/content/articles") @@ -1741,14 +1778,15 @@ async def article_new(): (md_dir / f"{article_slug}.md").write_text(body) pub_dt = published_at or datetime.utcnow().isoformat() + seo_head = _build_article_seo_head(url_path, title, meta_description, language, pub_dt) await execute( """INSERT INTO articles (url_path, slug, title, meta_description, og_image_url, - country, region, status, published_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + country, region, language, status, published_at, seo_head) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (url_path, article_slug, title, meta_description, og_image_url, - country, region, status, pub_dt), + country, region, language, status, pub_dt, seo_head), ) from ..sitemap import invalidate_sitemap_cache invalidate_sitemap_cache() @@ -1780,6 +1818,7 @@ async def article_edit(article_id: int): country = form.get("country", "").strip() region = form.get("region", "").strip() body = form.get("body", "").strip() + language = form.get("language", article.get("language", "en")).strip() or "en" status = form.get("status", article["status"]) published_at = form.get("published_at", "").strip() @@ -1793,8 +1832,9 @@ async def article_edit(article_id: int): if body: body_html = mistune.html(body) body_html = await bake_scenario_cards(body_html) - BUILD_DIR.mkdir(parents=True, exist_ok=True) - (BUILD_DIR / f"{article['slug']}.html").write_text(body_html) + build_dir = BUILD_DIR / language + build_dir.mkdir(parents=True, exist_ok=True) + (build_dir / f"{article['slug']}.html").write_text(body_html) md_dir = Path("data/content/articles") md_dir.mkdir(parents=True, exist_ok=True) @@ -1802,14 +1842,16 @@ async def article_edit(article_id: int): now = datetime.utcnow().isoformat() pub_dt = published_at or article["published_at"] + seo_head = _build_article_seo_head(url_path, title, meta_description, language, pub_dt) await execute( """UPDATE articles SET title = ?, url_path = ?, meta_description = ?, og_image_url = ?, - country = ?, region = ?, status = ?, published_at = ?, updated_at = ? + country = ?, region = ?, language = ?, status = ?, published_at = ?, + seo_head = ?, updated_at = ? WHERE id = ?""", (title, url_path, meta_description, og_image_url, - country, region, status, pub_dt, now, article_id), + country, region, language, status, pub_dt, seo_head, now, article_id), ) await flash("Article updated.", "success") return redirect(url_for("admin.articles")) diff --git a/web/src/padelnomics/admin/templates/admin/article_form.html b/web/src/padelnomics/admin/templates/admin/article_form.html index aed810c..9e7df00 100644 --- a/web/src/padelnomics/admin/templates/admin/article_form.html +++ b/web/src/padelnomics/admin/templates/admin/article_form.html @@ -60,7 +60,14 @@
Use [scenario:slug] to embed scenario widgets. Sections: :capex, :operating, :cashflow, :returns, :full
-