feat: add OG images, favicon set, manifest and performance tuning

- 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>
This commit is contained in:
2026-10-16 02:36:56 +03:00
parent df9c40847d
commit 7d8fcce2e1
28 changed files with 914 additions and 41 deletions
+82
View File
@@ -0,0 +1,82 @@
// 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');