59ff48ae90
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>
46 lines
1.6 KiB
Svelte
46 lines
1.6 KiB
Svelte
<script>
|
|
import '../app.css';
|
|
import { onMount } from 'svelte';
|
|
import FeedbackModal from '$lib/components/FeedbackModal.svelte';
|
|
import { fb, openFeedback, closeFeedback } from '$lib/feedback.svelte.js';
|
|
import { trackVisit } from '$lib/analytics.js';
|
|
let { children } = $props();
|
|
// Tell the boot-failure seatbelt (app.html) the app mounted — clears the
|
|
// recovery card + timeout as soon as the shell hydrates.
|
|
onMount(() => {
|
|
window.__ubBooted?.();
|
|
// Count the daily visit at the LAYOUT level so every landing page counts —
|
|
// direct /play and /a/ arrivals included, not just the news homepage.
|
|
trackVisit();
|
|
});
|
|
</script>
|
|
|
|
{@render children()}
|
|
|
|
{#if fb.open}<FeedbackModal onclose={closeFeedback} />{/if}
|
|
|
|
<footer class="site">
|
|
<div class="container">
|
|
<button class="fb" onclick={openFeedback}>Send feedback</button>
|
|
<span class="dot">·</span>
|
|
Upbeat Bytes · metadata & links only, no stored articles · <a href="/docs">API</a>
|
|
</div>
|
|
</footer>
|
|
|
|
<style>
|
|
footer.site {
|
|
text-align: center;
|
|
color: var(--muted);
|
|
font-size: 0.82rem;
|
|
padding: 26px 0 34px;
|
|
border-top: 1px solid var(--line);
|
|
}
|
|
footer.site a { color: var(--accent-deep); }
|
|
footer.site .fb { background: none; border: none; color: var(--accent-deep); font: inherit; font-size: 0.82rem; cursor: pointer; text-decoration: underline; padding: 0; }
|
|
footer.site .dot { margin: 0 4px; }
|
|
/* room for the mobile bottom tab bar */
|
|
@media (max-width: 720px) {
|
|
footer.site { padding-bottom: calc(34px + 64px + env(safe-area-inset-bottom)); }
|
|
}
|
|
</style>
|