Glyphica is a digital type foundry concept built as a Next.js static export. The whole site is typography demonstrating itself: no images, no chart library, no stock anything — the product is the rendering engine, so the flourish is a live variable-font instrument you can actually pull on.
A type foundry site has an unfair advantage: its product needs no photography, because every paragraph is already a product demo. The risk is the opposite — that the site reads like a PDF specimen. So Glyphica’s centerpiece is an instrument, not a gallery: four real variable axes (weight, optical size, softness, wonk) wired to a giant specimen, with size, tracking, italic, editable text, presets with foundry names, and a readout that hands you the exact CSS you just made.
The fictional flagship, Cathedra, is played by Fraunces — chosen precisely because it exposes eccentric custom axes (SOFT, WONK) alongside the registered pair (opsz, wght). The voice follows the product: opinionated, precise, a little liturgical. “The alphabet is an argument. We publish ours.”
output: ‘export’. GitHub Pages serves static files, so the build emits plain HTML to out/. The playground is genuinely stateful — seven controls, presets, a clipboard readout and an idle animation loop — which is why this one is a React build rather than vanilla.basePath: ‘/glyphica’ + assetPrefix + trailingSlash: true in next.config.mjs, copy out/ to docs/, point Pages at main /docs, and keep a .nojekyll so Jekyll doesn’t eat _next/.next/font, self-hosted at build time. The critical line is the axes request — without it Google serves a static instance and every slider goes dead:const display = Fraunces({
subsets: ['latin'],
style: ['normal', 'italic'],
axes: ['SOFT', 'WONK', 'opsz'], // + wght, variable by default
variable: '--font-display',
});Everything reduces to one string. The playground keeps axis state and renders it into font-variation-settings on the specimen; the browser’s font engine does the rest. This is the actual render path from components/AxisPlayground.tsx:
const settings =
`"opsz" ${opsz}, "wght" ${wght}, "SOFT" ${soft}, "WONK" ${wonk ? 1 : 0}`;
<p
className="spec-word"
style={{
fontVariationSettings: settings,
fontSize: `min(${size}px, 16vw)`, // giant, but never overflowing
letterSpacing: `${(tracking / 100).toFixed(3)}em`,
fontStyle: italic ? 'italic' : 'normal',
}}
>
{text}
</p>Two details make it feel like an instrument instead of a form. First, the specimen carries transition: font-variation-settings 0.45s var(--ease) — the ink appears to settle into each new weight rather than snapping (axis lists must keep the same order for the interpolation to work). Second, until you touch a control, an auto-specimen loop breathes through weight and optical size, so the section demonstrates itself:
useEffect(() => {
if (reduced || interacted || !stageInView) return; // never under
let raf = 0, last = 0; // reduced motion
const t0 = performance.now();
const loop = (t: number) => {
if (interactedRef.current) return; // yours now
if (t - last >= 42) { // ~24fps is plenty
const s = (t - t0) / 1000;
setAxes((p) => ({
...p,
wght: Math.round(560 + 320 * Math.sin(s * 0.55)),
opsz: Math.round(80 + 64 * Math.sin(s * 0.31 + 1.4)),
}));
last = t;
}
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [reduced, interacted, stageInView]);The loop is throttled to ~24fps, pauses when the stage scrolls out of view (an IntersectionObserver flips stageInView), stops forever on first interaction, and under prefers-reduced-motion it simply never starts — the sliders still work, the changes just land without easing. The waterfall below the readout re-renders the same settings at four fixed sizes with opsz mapped to each — the argument for optical sizing, made visually instead of verbally.
execCommand fallback). A foundry site should hand designers something they can paste.input[type=range]; wonk and italic are real checkboxes styled as toggles. Arrow keys, focus rings and screen readers work for free, and the thumb is a compositor’s blade, not a circle.#f4f0e6) is the resting state and dark is the composing room after hours. Tokens flip on :root[data-theme]; an inline script reads localStorage.glyphica-theme before first paint so there’s no flash.html.js class added synchronously in the document head.cubic-bezier(.32, 0, .12, 1) — the ink trap: sharp entry, clean landing — used by reveals, buttons, toggles and the variation settle alike, mirrored in JS for the count-ups.The sign-in form is a demo — it validates and confirms in place but sends nothing and stores nothing. Wire it to your own auth (and your own cart) before selling fonts with it.
Four commands once the export is configured:
npm run build # next build → out/ (static export)
rm -rf docs && cp -r out docs # Pages serves /docs on main
touch docs/.nojekyll # keep _next/ out of Jekyll's mouth
gh api --method POST /repos/bswxyz/glyphica/pages \
-f 'source[branch]=main' -f 'source[path]=/docs'With trailingSlash: true this page exports as /guide/index.html, so the URL works with no server-side routing. Local dev is npm run dev — note the basePath means the site serves at localhost:3000/glyphica.