Game share-loop: instrument funnel, deep-link shares, /play metadata
Sharpen the existing daily-game share loop into something measurable (per Codex's
"instrument what you have, then feed people into it" plan), ahead of a Show HN launch.
Analytics:
- Per-game funnel events <game>_{arrival,started,completed,shared} (article_id=0).
arrival = landed via a shared link (utm_source=game_share); started = first move
(guess/find/flip); completed = solved/cleared/Full Bloom; shared = on share success.
- trackVisit() moved into the global layout so direct /play landings count; the
server-rendered /a/ share page now creates a visitor token + sends a daily visit
beacon (first-time /a/-only visitors were previously dropped).
- Admin "Games funnel" panel: arrivals / engaged / completed / shared, per game.
Sharing:
- Memory Match gains a Share button (it was the only game without one).
- All shares deep-link to the exact game+variant with a full https:// URL +
utm_source=game_share (gameShareUrl helper), instead of a bare /play.
- "shared" is counted only after navigator.share()/clipboard.writeText() succeeds.
/play social metadata:
- /play served homepage canonical/OG (static SPA, ssr=false). postbuild script
patches build/play.html's head to /play canonical/title/description/OG; fails the
build if the homepage tags drift. Caddy try_files now serves {path}.html so /play
is served from the patched file (snapshot in deploy/caddy/).
Tests: backend 352, frontend 27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
// 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 <head> — 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 = [
|
||||
[/<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_PLAY}" />`],
|
||||
[/<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_PLAY}" />`],
|
||||
[/<meta name="twitter:title" content="[^"]*"\s*\/>/, `<meta name="twitter:title" content="${TITLE}" />`],
|
||||
[/<meta name="twitter:description" content="[^"]*"\s*\/>/, `<meta name="twitter:description" content="${DESC}" />`],
|
||||
];
|
||||
|
||||
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');
|
||||
Reference in New Issue
Block a user