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>
77 lines
2.8 KiB
JavaScript
77 lines
2.8 KiB
JavaScript
// 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/');
|