7d8fcce2e1
- Build-time Open Graph cards per post and a default card (Satori + Sharp) - Favicon/ICO/apple-touch/manifest icons generated from one SVG (npm run icons) - robots.txt and web manifest generated from site.json - Subset self-hosted fonts to Latin + Turkish (188 KB -> 66 KB) with preload - Inline the stylesheet to remove the render-blocking request - Post-build site audit (npm run check:site): h1, alt, SEO tags, links - Omit canonical on noindex pages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
3.0 KiB
JavaScript
83 lines
3.0 KiB
JavaScript
// Builds small self-hosted variable fonts (Latin + Turkish + symbols used in posts) into public/fonts/.
|
||
// Sources: @fontsource-variable/{rubik,inter} (SIL OFL 1.1). Usage: npm run fonts
|
||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||
import { createRequire } from 'node:module';
|
||
import subsetFont from 'subset-font';
|
||
|
||
const require = createRequire(import.meta.url);
|
||
|
||
const ranges = [
|
||
[0x20, 0x7e], // Basic Latin
|
||
[0xa0, 0xff], // Latin-1 (ç ö ü · ° × …)
|
||
[0x11e, 0x11f], // Ğ ğ
|
||
[0x130, 0x131], // İ ı
|
||
[0x15e, 0x15f], // Ş ş
|
||
[0x394, 0x394], // Δ
|
||
[0x3a9, 0x3a9], // Ω
|
||
[0x3bb, 0x3bc], // λ μ
|
||
[0x2013, 0x2014], // – —
|
||
[0x2018, 0x201e], // ‘ ’ ‚ “ ” „
|
||
[0x2022, 0x2022], // •
|
||
[0x2026, 0x2026], // …
|
||
[0x2032, 0x2033], // ′ ″
|
||
[0x20ba, 0x20ba], // ₺
|
||
[0x2122, 0x2122], // ™
|
||
[0x2190, 0x2193], // ← ↑ → ↓
|
||
[0x2212, 0x2212], // −
|
||
[0x2248, 0x2248], // ≈
|
||
[0x2264, 0x2265], // ≤ ≥
|
||
];
|
||
const text = ranges
|
||
.flatMap(([a, b]) => Array.from({ length: b - a + 1 }, (_, i) => String.fromCodePoint(a + i)))
|
||
.join('');
|
||
|
||
// Fontsource ships each family split by unicode-range (latin / latin-ext). Each part is subset
|
||
// on its own and keeps Fontsource's unicode-range, so browsers fetch only the parts a page needs.
|
||
const families = [
|
||
{ pkg: '@fontsource-variable/rubik', family: 'Rubik Variable', slug: 'rubik', wght: [400, 900] },
|
||
{ pkg: '@fontsource-variable/inter', family: 'Inter Variable', slug: 'inter', wght: [400, 700] },
|
||
];
|
||
const parts = ['latin', 'latin-ext'];
|
||
|
||
/** unicode-range of one Fontsource part, read from the package's own CSS. */
|
||
function unicodeRange(pkg, slug, part) {
|
||
const css = readFileSync(require.resolve(`${pkg}/index.css`), 'utf8');
|
||
const block = css
|
||
.split('@font-face')
|
||
.find((b) => b.includes(`${slug}-${part}-wght-normal.woff2`));
|
||
const m = block?.match(/unicode-range:\s*([^;]+);/);
|
||
if (!m) throw new Error(`unicode-range not found for ${slug} ${part}`);
|
||
return m[1].trim();
|
||
}
|
||
|
||
mkdirSync('public/fonts', { recursive: true });
|
||
const faces = [];
|
||
for (const f of families) {
|
||
for (const part of parts) {
|
||
const source = readFileSync(
|
||
require.resolve(`${f.pkg}/files/${f.slug}-${part}-wght-normal.woff2`),
|
||
);
|
||
const out = await subsetFont(source, text, {
|
||
targetFormat: 'woff2',
|
||
variationAxes: { wght: { min: f.wght[0], max: f.wght[1] } },
|
||
});
|
||
const file = `${f.slug}-${part}.woff2`;
|
||
writeFileSync(`public/fonts/${file}`, out);
|
||
console.log(`${file}: ${source.length} → ${out.length} bytes`);
|
||
faces.push(`@font-face {
|
||
font-family: '${f.family}';
|
||
font-style: normal;
|
||
font-display: swap;
|
||
font-weight: ${f.wght[0]} ${f.wght[1]};
|
||
src: url('/fonts/${file}') format('woff2-variations');
|
||
unicode-range: ${unicodeRange(f.pkg, f.slug, part)};
|
||
}`);
|
||
}
|
||
}
|
||
|
||
writeFileSync(
|
||
'src/styles/fonts.css',
|
||
`/* Generated by scripts/subset-fonts.mjs — do not edit. Fonts: SIL OFL 1.1 (Rubik, Inter). */\n${faces.join('\n')}\n`,
|
||
);
|
||
console.log('Wrote src/styles/fonts.css');
|