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:
@@ -0,0 +1,76 @@
|
||||
// Regenerates the favicon set in public/ from the Tabler antenna-tower icon.
|
||||
// Usage: npm run icons
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { icons } = require('@iconify-json/tabler/icons.json');
|
||||
|
||||
const RED = '#c95748';
|
||||
const INK = '#111012';
|
||||
const tower = (stroke) =>
|
||||
icons['building-broadcast-tower'].body
|
||||
.replaceAll('currentColor', INK)
|
||||
.replaceAll('stroke-width="2"', `stroke-width="${stroke}"`);
|
||||
|
||||
/**
|
||||
* Square icon: red background, ink tower.
|
||||
* `pad` = icon inset (0..1), `radius` in viewBox units, `stroke` = Tabler stroke width.
|
||||
*/
|
||||
const svg = ({ pad = 0.14, radius = 6, stroke = 2.4 } = {}) => {
|
||||
const size = 32;
|
||||
const inner = size * (1 - pad * 2);
|
||||
const offset = size * pad;
|
||||
const scale = inner / 24;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" rx="${radius}" fill="${RED}"/><g transform="translate(${offset} ${offset}) scale(${scale})">${tower(stroke)}</g></svg>`;
|
||||
};
|
||||
|
||||
const png = (source, px) =>
|
||||
sharp(Buffer.from(source), { density: 72 * (px / 32) * 4 })
|
||||
.resize(px, px)
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
/** ICO container with embedded PNG images. */
|
||||
function ico(images) {
|
||||
const header = Buffer.alloc(6 + images.length * 16);
|
||||
header.writeUInt16LE(0, 0);
|
||||
header.writeUInt16LE(1, 2);
|
||||
header.writeUInt16LE(images.length, 4);
|
||||
let offset = header.length;
|
||||
images.forEach(({ size, data }, i) => {
|
||||
const e = 6 + i * 16;
|
||||
header.writeUInt8(size >= 256 ? 0 : size, e);
|
||||
header.writeUInt8(size >= 256 ? 0 : size, e + 1);
|
||||
header.writeUInt16LE(1, e + 4); // colour planes
|
||||
header.writeUInt16LE(32, e + 6); // bits per pixel
|
||||
header.writeUInt32LE(data.length, e + 8);
|
||||
header.writeUInt32LE(offset, e + 12);
|
||||
offset += data.length;
|
||||
});
|
||||
return Buffer.concat([header, ...images.map((i) => i.data)]);
|
||||
}
|
||||
|
||||
const rounded = svg();
|
||||
const tiny = svg({ pad: 0.06, radius: 4, stroke: 3.2 }); // 16 px tab icon: bigger, bolder
|
||||
const square = svg({ radius: 0 }); // platforms apply their own mask
|
||||
const maskable = svg({ pad: 0.24, radius: 0 }); // keep the icon inside the 80% safe zone
|
||||
|
||||
writeFileSync('public/favicon.svg', rounded);
|
||||
writeFileSync(
|
||||
'public/favicon.ico',
|
||||
ico(
|
||||
await Promise.all(
|
||||
[16, 32, 48].map(async (size) => ({
|
||||
size,
|
||||
data: await png(size === 16 ? tiny : rounded, size),
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
writeFileSync('public/apple-touch-icon.png', await png(svg({ pad: 0.18, radius: 0 }), 180));
|
||||
writeFileSync('public/icon-192.png', await png(square, 192));
|
||||
writeFileSync('public/icon-512.png', await png(square, 512));
|
||||
writeFileSync('public/icon-maskable-512.png', await png(maskable, 512));
|
||||
console.log('Icons written to public/');
|
||||
Reference in New Issue
Block a user