Commit 4fa6d331 by PLN (Algolia)

blog: dev-only body-face switcher (serif default, sans candidate stubbed in prod)

parent 0870c645
'use client';
import { useEffect, useState } from 'react';
import styles from './blog.module.css';
/**
* DEV-ONLY article body-face switcher — the body-face decision belongs to
* the owner, made at the mockup. `?fonts=0` renders the editorial serif
* body (default), `?fonts=1` the humanist sans candidate; the face is
* applied via a data attribute on <html> and styled from blog.module.css.
*
* Inert in production twice over: this component renders nothing outside
* development, and the dev-fonts module it depends on is replaced by a stub
* at build time (next.config.js), so the alternative pair never loads.
*/
export default function FontSwitcher() {
const [face, setFace] = useState(null);
useEffect(() => {
if (process.env.NODE_ENV === 'production') return undefined;
const param = new URLSearchParams(window.location.search).get('fonts');
if (param !== '0' && param !== '1') return undefined;
const value = param === '1' ? 'sans' : 'serif';
document.documentElement.dataset.articleBody = value;
setFace(value);
return () => {
delete document.documentElement.dataset.articleBody;
};
}, []);
if (process.env.NODE_ENV === 'production' || !face) return null;
return (
<div className={styles.fontBadge} aria-hidden="true">
dev preview — body face: {face} · ?fonts={face === 'sans' ? '1' : '0'}
</div>
);
}
import { Source_Serif_4, Syne } from 'next/font/google';
/**
* Article-page faces (production-safe).
*
* - Syne — the brand display face, article title only. It never enters the
* prose: display faces at reading size are noise.
* - Source Serif 4 — the default article body face (the `?fonts=0` side of
* the dev switcher). Chosen deliberately, not by reflex: a sturdy Adobe
* workhorse with true italics and 400/600/700, book-like without borrowing
* this season's fashion serif (Newsreader / Fraunces / Lora are the
* monoculture the brand register bans). The alternative sans candidate is
* dev-only and lives in dev-fonts.js, stubbed out of prod by next.config.js.
*
* `display: swap` + next/font's metric-adjusted fallbacks keep the swap from
* reflowing the page. Self-hosted at build — the reader's browser never
* phones out.
*/
export const syne = Syne({
subsets: ['latin'],
weight: ['700', '800'],
display: 'swap',
});
export const serifBody = Source_Serif_4({
subsets: ['latin'],
weight: ['400', '600', '700'],
style: ['normal', 'italic'],
display: 'swap',
});
import { Atkinson_Hyperlegible } from 'next/font/google';
/**
* DEV-ONLY body-face candidate for the article mockup — `?fonts=1`.
*
* Atkinson Hyperlegible (Braille Institute): a humanist sans designed for
* reading endurance — plain-spoken, high glyph differentiation, and already
* part of this site's type family (the clarte apps ship it from /fonts).
* Runners-up considered and set aside: Work Sans, Hanken Grotesk.
*
* The face ships 400/700 + italics only — there is no 600. CSS font
* matching sends 600 requests to the real 700, so semibold headings render
* as true bold, not synthesis.
*
* next.config.js replaces this module with dev-fonts.stub.js in production
* builds, so the sans pair is never emitted into the shipped bundle.
*/
export const sansBody = Atkinson_Hyperlegible({
subsets: ['latin'],
weight: ['400', '700'],
style: ['normal', 'italic'],
display: 'swap',
});
/**
* Production stand-in for dev-fonts.js — swapped in by next.config.js
* (webpack alias, non-dev builds only). Same module shape, no font loading:
* the alternative body face must never reach the shipped bundle.
*/
export const sansBody = null;
...@@ -25,8 +25,17 @@ module.exports = { ...@@ -25,8 +25,17 @@ module.exports = {
// Playwright route-smoke net remain the real gates. // Playwright route-smoke net remain the real gates.
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
webpack: (config) => { webpack: (config, { dev }) => {
config.resolve.alias['@'] = path.resolve(__dirname); config.resolve.alias['@'] = path.resolve(__dirname);
// The article body-face switcher (?fonts=) is a dev-only mockup surface.
// In production builds the alternative font module is swapped for a stub
// so no second body pair is ever emitted into the shipped bundle.
if (!dev) {
config.resolve.alias['@/app/blog/dev-fonts$'] = path.resolve(
__dirname,
'app/blog/dev-fonts.stub.js',
);
}
return config; return config;
}, },
}; };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment