2cfffdfd6a
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>
85 lines
3.8 KiB
JavaScript
85 lines
3.8 KiB
JavaScript
// Post-build: give specific prerendered shells their OWN social/canonical metadata.
|
||
//
|
||
// The app is a static SPA (ssr=false), so every prerendered shell ships app.html's
|
||
// HOMEPAGE <head> — meaning a shared /play or /word link previews as the news homepage,
|
||
// and its canonical points at "/". Client svelte:head can't fix that for non-JS scrapers
|
||
// (Twitter/Slack/iMessage) or for canonical dedup. So we rewrite each page's static head
|
||
// here, at build time. Deep-linked variants (e.g. /play?game=…) inherit the same file.
|
||
import { readFile, writeFile } from 'node:fs/promises';
|
||
|
||
const BASE = 'https://upbeatbytes.com';
|
||
|
||
// Per-page <head> overrides. Keep titles/descriptions in sync with each page's intent.
|
||
const PAGES = [
|
||
{
|
||
file: 'news.html', path: '/news',
|
||
title: 'News · upbeatBytes — calm, constructive news',
|
||
desc: 'Calm, constructive news, newest first — and a daily Highlights brief. ' +
|
||
'No ads, no paywalls, no doomscrolling.',
|
||
},
|
||
{
|
||
file: 'play.html', path: '/play',
|
||
title: 'Play · Upbeat Bytes — calm daily games',
|
||
desc: 'A calm set of daily games — Daily Word, Word Search, Bloom, and Memory Match. ' +
|
||
'A friendly little break from the doomscroll.',
|
||
},
|
||
{
|
||
file: 'word.html', path: '/word',
|
||
title: 'Word of the Day · upbeatBytes',
|
||
desc: 'A new uplifting word every day — its meaning in plain language, how to say it, and how to use it.',
|
||
},
|
||
{
|
||
file: 'quote.html', path: '/quote',
|
||
title: 'Quote of the Day · upbeatBytes',
|
||
desc: 'A hopeful, hand-picked quote each day, with a short note on what it means.',
|
||
},
|
||
{
|
||
file: 'onthisday.html', path: '/onthisday',
|
||
title: 'On This Day · upbeatBytes',
|
||
desc: 'One genuinely good thing that happened on this day in history.',
|
||
},
|
||
{
|
||
file: 'art.html', path: '/art',
|
||
title: 'Daily Art · upbeatBytes',
|
||
desc: "A masterwork a day from the world's open museum collections — beautifully framed, " +
|
||
'with a short note on what you’re looking at.',
|
||
},
|
||
];
|
||
|
||
function subsFor(url, title, desc) {
|
||
return [
|
||
[/<title>[\s\S]*?<\/title>/, `<title>${title}</title>`],
|
||
[/<meta name="description" content="[^"]*"\s*\/>/, `<meta name="description" content="${desc}" />`],
|
||
[/<link rel="canonical" href="https:\/\/upbeatbytes\.com\/"\s*\/>/, `<link rel="canonical" href="${url}" />`],
|
||
[/<meta property="og:title" content="[^"]*"\s*\/>/, `<meta property="og:title" content="${title}" />`],
|
||
[/<meta property="og:description" content="[^"]*"\s*\/>/, `<meta property="og:description" content="${desc}" />`],
|
||
[/<meta property="og:url" content="https:\/\/upbeatbytes\.com\/"\s*\/>/, `<meta property="og:url" content="${url}" />`],
|
||
[/<meta name="twitter:title" content="[^"]*"\s*\/>/, `<meta name="twitter:title" content="${title}" />`],
|
||
[/<meta name="twitter:description" content="[^"]*"\s*\/>/, `<meta name="twitter:description" content="${desc}" />`],
|
||
];
|
||
}
|
||
|
||
for (const { file, path, title, desc } of PAGES) {
|
||
const fileUrl = new URL(`../build/${file}`, import.meta.url);
|
||
let html;
|
||
try {
|
||
html = await readFile(fileUrl, 'utf8');
|
||
} catch {
|
||
console.error(`patch-static-heads: build/${file} is missing (route renamed/removed?)`);
|
||
process.exit(1);
|
||
}
|
||
const missed = [];
|
||
for (const [re, repl] of subsFor(BASE + path, title, desc)) {
|
||
if (!re.test(html)) { missed.push(re.source.slice(0, 40)); continue; }
|
||
html = html.replace(re, repl);
|
||
}
|
||
// Fail loudly if the homepage head drifted — better a broken build than silently
|
||
// shipping the wrong preview/canonical again.
|
||
if (missed.length) {
|
||
console.error(`patch-static-heads: ${file} — these head tags were not found (app.html changed?):\n ` + missed.join('\n '));
|
||
process.exit(1);
|
||
}
|
||
await writeFile(fileUrl, html);
|
||
console.log(`patch-static-heads: rewrote build/${file} head → ${path}`);
|
||
}
|