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
This commit is contained in:
+112
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
Reference in New Issue
Block a user