Files
PIG-Demo/src/lib/seo.ts
T
karti-ai 69607fbfe9 Pin seed->word across both languages with a shared hash
engine.ts used mulberry32 and engine.py used random.Random(seed). Same seed,
different word — so every ?seed= permalink on the site would have shown a
different puzzle than the recorded run it claimed to be replaying, and nobody
would have noticed until someone checked one by hand.

Both now derive the index from FNV-1a 32-bit over the decimal seed. A hash
rather than a PRNG because there is no honest one-line JavaScript equivalent of
Mersenne Twister, and this way there is nothing to keep in step: both sides
compute the same integer from the same string. Math.imul on the JS side is
load-bearing — a plain multiply overflows into a double and diverges after the
first few bytes.

Twelve seeds are pinned as a vector in both test suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
2026-08-28 15:42:17 -07:00

113 lines
3.8 KiB
TypeScript

/**
* Per-route document head.
*
* `scripts/prerender.mjs` bakes these tags into the static HTML at build time,
* which is what crawlers and link unfurlers actually read. This hook exists for
* the other half: a visitor who lands on `/` and clicks through to a demo never
* fetches a new document, so without it the tab title and the canonical link
* would still say "home" three pages later.
*
* Deliberately no cleanup. Restoring the previous head on unmount would mean
* every navigation flickers back to the old title before the next route sets
* its own; the next route always sets one, so the last writer simply wins.
*/
import { useEffect } from 'react';
export const SITE_ORIGIN = 'https://demo.primeintellectgrowth.com';
export const SITE_NAME = 'PIG Demo';
/** `Wordle-five — PIG Demo`. One place, so every tab reads the same shape. */
export function pageTitle(name?: string): string {
return name && name.trim() !== '' ? `${name}${SITE_NAME}` : SITE_NAME;
}
/**
* Absolutise a site-root path. Open Graph consumers do not resolve relative
* URLs — a relative `og:image` is simply no image, silently, and you only find
* out when someone pastes the link into Slack.
*/
export function absoluteUrl(pathOrUrl: string): string {
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
return `${SITE_ORIGIN}${pathOrUrl.startsWith('/') ? '' : '/'}${pathOrUrl}`;
}
export interface SeoInput {
/** Used verbatim as `document.title`. Wrap with `pageTitle()` if you want the suffix. */
title: string;
description?: string;
/** Site-root path or absolute URL. */
canonical?: string;
/** Site-root path or absolute URL. */
ogImage?: string;
}
type MetaKey = { name: string } | { property: string };
function upsertMeta(key: MetaKey, content: string): void {
const selector =
'name' in key ? `meta[name="${key.name}"]` : `meta[property="${key.property}"]`;
let tag = document.head.querySelector<HTMLMetaElement>(selector);
if (!tag) {
tag = document.createElement('meta');
if ('name' in key) tag.setAttribute('name', key.name);
else tag.setAttribute('property', key.property);
document.head.appendChild(tag);
}
tag.setAttribute('content', content);
}
function upsertCanonical(href: string): void {
let link = document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]');
if (!link) {
link = document.createElement('link');
link.setAttribute('rel', 'canonical');
document.head.appendChild(link);
}
link.setAttribute('href', href);
}
export function applySeo(input: SeoInput): void {
if (typeof document === 'undefined') return;
document.title = input.title;
upsertMeta({ property: 'og:title' }, input.title);
upsertMeta({ name: 'twitter:title' }, input.title);
if (input.description) {
upsertMeta({ name: 'description' }, input.description);
upsertMeta({ property: 'og:description' }, input.description);
upsertMeta({ name: 'twitter:description' }, input.description);
}
if (input.canonical) {
const href = absoluteUrl(input.canonical);
upsertCanonical(href);
upsertMeta({ property: 'og:url' }, href);
}
if (input.ogImage) {
const href = absoluteUrl(input.ogImage);
upsertMeta({ property: 'og:image' }, href);
upsertMeta({ name: 'twitter:image' }, href);
}
}
/**
* Set the head for this route.
*
* Deps are the individual strings rather than the object, so a caller can pass
* an inline literal without re-running this on every render.
*/
export function useSeo(input: SeoInput): void {
const { title, description, canonical, ogImage } = input;
useEffect(() => {
applySeo({
title,
...(description === undefined ? {} : { description }),
...(canonical === undefined ? {} : { canonical }),
...(ogImage === undefined ? {} : { ogImage }),
});
}, [title, description, canonical, ogImage]);
}