SEO flywheel: /today digest, sitemap, robots, home OG tags

Make the summary pages discoverable so traffic compounds passively:
- /today: a server-rendered, shareable + indexable digest of today's brief —
  each item's title (→ /a summary), our summary, and a source link. OG/Twitter
  meta + self-canonical.
- /sitemap.xml: dynamic — home, /today, and every accepted non-duplicate /a page
  with lastmod. robots.txt allows all and points to it.
- Home (SPA shell) gains canonical + OG/Twitter tags for cleaner unfurls.
- Caddy routes /today + /sitemap.xml to the API. 133 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-05 19:37:05 +00:00
parent 427210ac3e
commit ea58039fb9
5 changed files with 176 additions and 0 deletions
+84
View File
@@ -187,6 +187,90 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None)
</html>"""
def render_digest(items: list[dict], base_url: str, brief_date: str | None) -> str:
"""A shareable, indexable 'today's good news, summarized' page."""
lead_img = next((i.get("image_url") for i in items if i.get("image_url")), None)
intro = "The day's calm, constructive news — summarized in plain English, so you can get the gist and go deeper only if you want."
page_url = f"{base_url}/today"
cards = ""
for it in items:
aid = it["id"]
title = escape((it.get("title") or "").strip())
source = escape((it.get("source_name") or "the source").strip())
src_url = escape(it.get("canonical_url") or base_url)
gist = escape((it.get("summary") or it.get("reason_text") or it.get("description") or "").strip())
cards += (
'<article class="item">'
f'<a class="t" href="/a/{aid}"><h2>{title}</h2></a>'
f'<div class="src">{source}</div>'
f'<p class="gist">{gist}</p>'
f'<div class="links"><a href="/a/{aid}">Read the summary</a>'
f'<a href="{src_url}" rel="noopener">Full story at {source} →</a></div>'
"</article>"
)
meta = "\n".join(filter(None, [
_tag("og:site_name", "Upbeat Bytes"),
_tag("og:type", "website"),
_tag("og:title", "Today's good news, summarized"),
_tag("og:description", intro),
_tag("og:url", page_url),
_tag("og:image", lead_img),
_tag("twitter:card", "summary_large_image" if lead_img else "summary", attr="name"),
_tag("twitter:title", "Today's good news, summarized", attr="name"),
_tag("twitter:description", intro, attr="name"),
_tag("twitter:image", lead_img, attr="name"),
]))
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Today's good news, summarized · Upbeat Bytes</title>
<meta name="description" content="{escape(intro)}">
<link rel="canonical" href="{page_url}">
<link rel="icon" href="/favicon.svg">
{meta}
<style>
:root {{ --accent:#0083ad; --accent-deep:#006b8e; --ink:#16263a; --muted:#5d6b78;
--bg:#f7f4ec; --surface:#fffdf8; --line:#e8e3d8; }}
* {{ box-sizing:border-box; }}
body {{ margin:0; background:var(--bg); color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; line-height:1.6; }}
.bar {{ background:var(--surface); border-bottom:1px solid var(--line); }}
.bar .inner {{ max-width:720px; margin:0 auto; padding:12px 20px; }}
.bar img {{ height:40px; display:block; }}
.wrap {{ max-width:720px; margin:0 auto; padding:26px 20px 60px; }}
h1 {{ font-family:"Iowan Old Style",Palatino,Georgia,serif; font-weight:600; font-size:2rem; margin:0 0 4px; }}
.lede {{ color:var(--muted); margin:0 0 26px; }}
.item {{ border-top:1px solid var(--line); padding:20px 0; }}
.item .t {{ text-decoration:none; color:var(--ink); }}
.item h2 {{ font-family:"Iowan Old Style",Palatino,Georgia,serif; font-weight:600; font-size:1.3rem; margin:0 0 4px; }}
.item .t:hover h2 {{ color:var(--accent-deep); }}
.item .src {{ color:var(--muted); font-size:.78rem; text-transform:uppercase; letter-spacing:.07em; margin-bottom:8px; }}
.item .gist {{ margin:0 0 10px; }}
.item .links {{ display:flex; flex-wrap:wrap; gap:16px; font-size:.9rem; }}
.item .links a {{ color:var(--accent-deep); text-decoration:none; }}
.item .links a:hover {{ text-decoration:underline; }}
.more {{ margin-top:28px; }}
.more a {{ display:inline-block; background:var(--accent); color:#fff; text-decoration:none;
padding:11px 20px; border-radius:999px; font-weight:600; }}
</style>
</head>
<body>
<div class="bar"><div class="inner"><a href="/"><img src="/logo.svg" alt="Upbeat Bytes"></a></div></div>
<main class="wrap">
<h1>Today's good news</h1>
<p class="lede">{escape(intro)}{f' · {escape(brief_date)}' if brief_date else ''}</p>
{cards}
<p class="more"><a href="/">Browse more on Upbeat Bytes →</a></p>
</main>
</body>
</html>"""
def render_not_found(base_url: str) -> str:
return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8">