NEWS RELAUNCH CUTOVER: promote the hub to /, feed to /news, go public

The big flip. /home3 (hub) becomes /; the feed lives at /news; both indexable.
- PROMOTE: routes/+page.svelte is now the hub (was the interim NewsFeed wrapper);
  noindex removed; "Read more good news" → /news. routes/home3 + home2 deleted.
- routes/+page.js: redirects legacy root-query links (/?view=latest, /?tag, /?source,
  /?q, /?view=today→highlights) to /news before the hub renders (no flash).
- /news: noindex dropped (route meta + Caddy @newsHidden removed); now public.
- LINKS: HubBar brand/Home → /, News default → /news; HubShell/art/play back → /;
  account Following + share.py Explore/Browse/source → /news.
- FOOTER: one shared Footer.svelte (motto + Send feedback + slot) across Hub/News/
  Play/Art/HubShell/Account/Zen; global layout footer removed (FeedbackModal stays).
- SITEMAP: + /news /art /play /word /quote /onthisday; cap 5k→50k; gated on
  has-summary; paywalled excluded; HEAD now 200 (api_route GET+HEAD).
- Head-patcher: /news entry. PWA + shell description broadened to the hub.
- Caddy: @newsHidden dropped; @hidden now admin-only (word/quote/onthisday public);
  /home2,/home3 → / 301. Mirrored to deploy/caddy snapshot.

425 backend + 36 frontend tests green; build clean; Caddy valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-28 19:16:43 -04:00
parent 1c1ecefde8
commit 2cfffdfd6a
21 changed files with 682 additions and 793 deletions
+12 -2
View File
@@ -1695,22 +1695,32 @@ def create_app() -> FastAPI:
return HTMLResponse(share.render_not_found(PUBLIC_BASE_URL), status_code=404)
return HTMLResponse(share.render_digest(items, PUBLIC_BASE_URL, b.get("brief_date")))
@app.get("/sitemap.xml")
@app.api_route("/sitemap.xml", methods=["GET", "HEAD"])
def sitemap() -> Response:
with get_conn() as conn:
pwx = queries.paywalled_source_ids(conn)
pw_clause = f" AND a.source_id NOT IN ({','.join('?' * len(pwx))})" if pwx else ""
# Only articles with a real summary (the page has substance), paywalled excluded.
# Cap near the 50k protocol ceiling so older canonical pages aren't dropped.
rows = conn.execute(
"SELECT a.id, COALESCE(a.published_at, a.discovered_at) AS lm "
"FROM articles a JOIN article_scores s ON s.article_id = a.id "
"WHERE s.accepted = 1 AND a.duplicate_of IS NULL" + pw_clause + " "
"ORDER BY lm DESC LIMIT 5000",
"AND EXISTS (SELECT 1 FROM article_summaries asum WHERE asum.article_id = a.id) "
"ORDER BY lm DESC LIMIT 50000",
pwx,
).fetchall()
base = PUBLIC_BASE_URL
# Hub + the public sections, then every summarized article page.
urls = [
f"<url><loc>{base}/</loc><changefreq>hourly</changefreq><priority>1.0</priority></url>",
f"<url><loc>{base}/news</loc><changefreq>hourly</changefreq><priority>0.9</priority></url>",
f"<url><loc>{base}/today</loc><changefreq>daily</changefreq><priority>0.9</priority></url>",
f"<url><loc>{base}/art</loc><changefreq>daily</changefreq><priority>0.6</priority></url>",
f"<url><loc>{base}/play</loc><changefreq>weekly</changefreq><priority>0.5</priority></url>",
f"<url><loc>{base}/word</loc><changefreq>daily</changefreq><priority>0.5</priority></url>",
f"<url><loc>{base}/quote</loc><changefreq>daily</changefreq><priority>0.5</priority></url>",
f"<url><loc>{base}/onthisday</loc><changefreq>daily</changefreq><priority>0.5</priority></url>",
]
for r in rows:
lm = (r["lm"] or "")[:10]
+3 -3
View File
@@ -27,7 +27,7 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None,
source_id = article.get("source_id")
# Link the source name into the app's publication feed for that source.
source_html = (
f'<a class="src srclink" href="/?source={source_id}">{escape(source)}</a>'
f'<a class="src srclink" href="/news?source={source_id}">{escape(source)}</a>'
if source_id else f'<div class="src">{escape(source)}</div>'
)
src_url = article.get("canonical_url") or base_url
@@ -207,7 +207,7 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None,
<div class="actions">
<a class="primary" href="{escape(src_url)}" target="_blank" rel="noopener" data-src-click>Read the full story at {escape(source)}</a>
<button class="secondary" type="button" data-share>Copy link</button>
<a class="secondary" href="/">Explore Upbeat Bytes →</a>
<a class="secondary" href="/news">Explore Upbeat Bytes →</a>
</div>
<p class="note">Upbeat Bytes summarizes in its own words and links to the original publisher — it doesn't host the article.</p>
</div>
@@ -329,7 +329,7 @@ def render_digest(items: list[dict], base_url: str, brief_date: str | None) -> s
<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>
<p class="more"><a href="/news">Browse more on Upbeat Bytes →</a></p>
</main>
</body>
</html>"""