- Wrap next.config.ts with @serwist/next (webpack mode, disabled in dev) - Service worker: NetworkOnly for /api/*, offline fallback → /~offline - Web app manifest via Next.js metadata API (app/manifest.ts) - PNG icon set generated with sharp (192, 512, maskable-512, apple-180) - iOS meta tags: appleWebApp, themeColor viewport export - Middleware: pwaAssets early-return so /sw.js never gets a 302→login - Offline fallback page at /~offline (static, no auth dependency) - InstallPrompt component: beforeinstallprompt (Android) + iOS Share sheet instructions - Logout (menu/page.tsx): purge all SW caches on signout (shared-device safety) - Fix invite/[token]/page.tsx params type for Next.js 16 (use(params)) - Build script: next build --webpack (Serwist requires webpack, not Turbopack) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
47 lines
1.7 KiB
JavaScript
47 lines
1.7 KiB
JavaScript
// Generates PWA icon PNGs from an SVG template using sharp.
|
|
// Run: node scripts/generate-icons.mjs
|
|
import sharp from "sharp";
|
|
import { writeFileSync } from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const outDir = path.join(__dirname, "../public/icons");
|
|
|
|
// Brand colors from the app
|
|
const BG = "#fb7185"; // rose-400
|
|
|
|
// SVG template — rounded square with cherry blossom character.
|
|
// For maskable: full bleed background, content within safe zone (inner 80%).
|
|
function makeSvg(size, maskable = false) {
|
|
const radius = maskable ? 0 : Math.round(size * 0.18);
|
|
const fontSize = maskable ? Math.round(size * 0.48) : Math.round(size * 0.56);
|
|
const y = maskable ? Math.round(size * 0.68) : Math.round(size * 0.72);
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
<rect width="${size}" height="${size}" rx="${radius}" fill="${BG}"/>
|
|
<text
|
|
x="${size / 2}"
|
|
y="${y}"
|
|
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
|
|
font-weight="700"
|
|
font-size="${fontSize}"
|
|
text-anchor="middle"
|
|
fill="white"
|
|
dominant-baseline="auto"
|
|
>T</text>
|
|
</svg>`;
|
|
}
|
|
|
|
const icons = [
|
|
{ name: "192.png", size: 192, maskable: false },
|
|
{ name: "512.png", size: 512, maskable: false },
|
|
{ name: "maskable-512.png", size: 512, maskable: true },
|
|
{ name: "apple-180.png", size: 180, maskable: false },
|
|
];
|
|
|
|
for (const icon of icons) {
|
|
const svg = Buffer.from(makeSvg(icon.size, icon.maskable));
|
|
const outPath = path.join(outDir, icon.name);
|
|
await sharp(svg).png().toFile(outPath);
|
|
console.log(`✓ ${icon.name}`);
|
|
}
|