diff --git a/frontend/package.json b/frontend/package.json index b406604..8e14a9a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite dev --host", "build": "vite build", - "postbuild": "node scripts/patch-play-head.mjs", + "postbuild": "node scripts/patch-static-heads.mjs", "preview": "vite preview", "test": "vitest run" }, diff --git a/frontend/scripts/patch-play-head.mjs b/frontend/scripts/patch-play-head.mjs deleted file mode 100644 index 6ac14e6..0000000 --- a/frontend/scripts/patch-play-head.mjs +++ /dev/null @@ -1,41 +0,0 @@ -// Post-build: give build/play.html its own social/canonical metadata. -// -// The app is a static SPA (ssr=false), so every prerendered shell ships app.html's -// HOMEPAGE — meaning a shared /play link previews as the news homepage. Client -// svelte:head can't fix that for non-JS social scrapers (Twitter/Slack/iMessage/etc.). -// So we rewrite the static head of play.html here, at build time. Deep-linked variants -// (/play?game=…) are served the same file, so they inherit this games-hub preview. -import { readFile, writeFile } from 'node:fs/promises'; - -const FILE = new URL('../build/play.html', import.meta.url); -const URL_PLAY = 'https://upbeatbytes.com/play'; -const TITLE = 'Play · Upbeat Bytes — calm daily games'; -const DESC = - 'A calm set of daily games — Daily Word, Word Search, Bloom, and Memory Match. ' + - 'A friendly little break from the doomscroll.'; - -const subs = [ - [/[\s\S]*?<\/title>/, `<title>${TITLE}`], - [//, ``], - [//, ``], - [//, ``], - [//, ``], - [//, ``], - [//, ``], - [//, ``], -]; - -let html = await readFile(FILE, 'utf8'); -const missed = []; -for (const [re, repl] of subs) { - 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 /play preview again. -if (missed.length) { - console.error('patch-play-head: these head tags were not found (app.html changed?):\n ' + missed.join('\n ')); - process.exit(1); -} -await writeFile(FILE, html); -console.log('patch-play-head: rewrote build/play.html head → /play metadata'); diff --git a/frontend/scripts/patch-static-heads.mjs b/frontend/scripts/patch-static-heads.mjs new file mode 100644 index 0000000..857d334 --- /dev/null +++ b/frontend/scripts/patch-static-heads.mjs @@ -0,0 +1,72 @@ +// 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 — 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 overrides. Keep titles/descriptions in sync with each page's intent. +const PAGES = [ + { + 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.', + }, +]; + +function subsFor(url, title, desc) { + return [ + [/[\s\S]*?<\/title>/, `<title>${title}`], + [//, ``], + [//, ``], + [//, ``], + [//, ``], + [//, ``], + [//, ``], + [//, ``], + ]; +} + +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}`); +} diff --git a/frontend/src/routes/home3/+page.svelte b/frontend/src/routes/home3/+page.svelte index 86d1ec7..a41bb74 100644 --- a/frontend/src/routes/home3/+page.svelte +++ b/frontend/src/routes/home3/+page.svelte @@ -26,6 +26,11 @@ // truncation handled by CSS (-webkit-line-clamp:2) — breaks on whole words, fills 2 full lines let headline = $derived(news?.title ?? 'What went right this week: the good news that actually matters'); + // Honest read-time from our own gist (~200 wpm, floor 1). We summarize, so this is + // usually "1 min read" — a feature, not a bug: the good news in about a minute. + const readMins = (t) => Math.max(1, Math.round((t || '').trim().split(/\s+/).filter(Boolean).length / 200)); + let readTime = $derived(`${readMins(news?.summary)} min read`); + // small-joys shelf: 3 cells shown two at a time, rotated by the reader (no auto-motion) const JOY_ACCENTS = ['#4f7da8', '#b06a86', '#b06a45']; let joyIdx = $state(0); @@ -125,7 +130,7 @@

{news?.summary || "We read the week so you don't have to doomscroll it. Five quietly hopeful stories, summarised to the gist."}

- 3 min read + {readTime}

Read more good news →