feat(web): PWA — manifest, icone, meta iOS
- src/app/manifest.ts (Next 14 file convention) → /manifest.webmanifest - public/icons/icon.svg + icon-maskable.svg (vettoriali, palette editoriale) - scripts/generate-icons.mjs (sharp): genera icon-192/512.png, apple-touch-icon-180.png, icon-maskable-512.png, badge-72.png - layout.tsx: viewport con themeColor light/dark, manifest link, appleWebApp meta (capable + title), icons (svg + png + apple touch) - .eslintrc: ignora scripts Node out-of-project; lint-staged: .mjs format-only Display standalone, lang it, theme #221d18 (ink) / bg #f6f0df (carta). Sblocca "Installa app" su Chrome/Edge/Android e "Aggiungi alla Home" su iOS Safari — quest'ultimo è pre-requisito per ricevere le push su iOS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No files matched your search
@@ -5,5 +5,7 @@ module.exports = {
|
||||
ignorePatterns: [
|
||||
// Service worker servito al browser: out-of-project per il TS parser.
|
||||
'public/**/*.js',
|
||||
// Script Node ESM: out-of-project per il TS parser.
|
||||
'scripts/**/*.mjs',
|
||||
],
|
||||
};
|
||||
@@ -35,13 +35,14 @@
|
||||
"devDependencies": {
|
||||
"@ketopath/eslint-config": "workspace:*",
|
||||
"@ketopath/tsconfig": "workspace:*",
|
||||
"@playwright/test": "^1.45.3",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint-config-next": "^14.2.5",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"@playwright/test": "^1.45.3"
|
||||
"sharp": "^0.34.5",
|
||||
"tailwindcss": "^3.4.4"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="KetoPath">
|
||||
<!-- Maskable: full-bleed background, contenuto entro safe area centrale (≈ 80%). -->
|
||||
<rect width="512" height="512" fill="#f6f0df"/>
|
||||
<!-- Wordmark centrato; nessun filetto perché può essere mascherato. -->
|
||||
<text x="256" y="320" font-family="'Fraunces', 'Cormorant Garamond', 'Times New Roman', serif" font-size="240" font-weight="500" fill="#221d18" text-anchor="middle" letter-spacing="-10">
|
||||
KP<tspan fill="#9a3f29">.</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 557 B |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="KetoPath">
|
||||
<!-- Sfondo cream coerente con la palette del masthead. -->
|
||||
<rect width="512" height="512" fill="#f6f0df"/>
|
||||
<!-- Filetto editoriale superiore. -->
|
||||
<rect x="56" y="80" width="400" height="2" fill="#221d18"/>
|
||||
<!-- Filetto editoriale inferiore. -->
|
||||
<rect x="56" y="430" width="400" height="2" fill="#221d18"/>
|
||||
<!-- Eyebrow monospaziato. -->
|
||||
<text x="256" y="118" font-family="ui-monospace, 'JetBrains Mono', monospace" font-size="22" letter-spacing="6" fill="#221d18" text-anchor="middle" opacity="0.65">KETOPATH</text>
|
||||
<!-- Wordmark KP — Fraunces-like serif fallback. -->
|
||||
<text x="256" y="320" font-family="'Fraunces', 'Cormorant Garamond', 'Times New Roman', serif" font-size="280" font-weight="500" fill="#221d18" text-anchor="middle" letter-spacing="-12">
|
||||
KP<tspan fill="#9a3f29">.</tspan>
|
||||
</text>
|
||||
<!-- Dicitura editoriale al piedi. -->
|
||||
<text x="256" y="410" font-family="'Fraunces', serif" font-style="italic" font-size="28" fill="#3d3530" text-anchor="middle" opacity="0.7">una keto italiana</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,28 @@
|
||||
// Genera i PNG dell'icona PWA a partire dalle SVG sorgente.
|
||||
// Eseguilo dopo aver modificato icon.svg / icon-maskable.svg.
|
||||
//
|
||||
// pnpm --filter @ketopath/web exec node scripts/generate-icons.mjs
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PUBLIC_ICONS = path.join(here, '..', 'public', 'icons');
|
||||
|
||||
const targets = [
|
||||
{ src: 'icon.svg', out: 'icon-192.png', size: 192 },
|
||||
{ src: 'icon.svg', out: 'icon-512.png', size: 512 },
|
||||
{ src: 'icon.svg', out: 'apple-touch-icon.png', size: 180 },
|
||||
{ src: 'icon-maskable.svg', out: 'icon-maskable-512.png', size: 512 },
|
||||
{ src: 'icon-maskable.svg', out: 'badge-72.png', size: 72 },
|
||||
];
|
||||
|
||||
for (const t of targets) {
|
||||
const svg = await readFile(path.join(PUBLIC_ICONS, t.src));
|
||||
const png = await sharp(svg, { density: 384 }).resize(t.size, t.size).png().toBuffer();
|
||||
await writeFile(path.join(PUBLIC_ICONS, t.out), png);
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(`✓ ${t.out} (${t.size}×${t.size})`);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from 'next';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Fraunces, Inter_Tight, JetBrains_Mono } from 'next/font/google';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
@@ -35,6 +35,16 @@ export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
{ media: '(prefers-color-scheme: light)', color: '#f6f0df' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: '#221d18' },
|
||||
],
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
// Su iOS standalone mode mantiene il colore della status bar coerente.
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { locale },
|
||||
}: {
|
||||
@@ -44,6 +54,21 @@ export async function generateMetadata({
|
||||
return {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
manifest: '/manifest.webmanifest',
|
||||
applicationName: 'KetoPath',
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
title: 'KetoPath',
|
||||
statusBarStyle: 'default',
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/icons/icon.svg', type: 'image/svg+xml' },
|
||||
{ url: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||
{ url: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||||
],
|
||||
apple: [{ url: '/icons/apple-touch-icon.png', sizes: '180x180', type: 'image/png' }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Web App Manifest (Next.js 14 file convention).
|
||||
// Servito a /manifest.webmanifest. PRD §5.6 — installabile su iOS/Android per
|
||||
// abilitare le notifiche push sui dispositivi mobili (vedi ADR 0003).
|
||||
|
||||
import type { MetadataRoute } from 'next';
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: 'KetoPath — Una keto italiana',
|
||||
short_name: 'KetoPath',
|
||||
description:
|
||||
'Chetogenica e digiuno intermittente personalizzati: piani settimanali, lista spesa, notifiche.',
|
||||
lang: 'it',
|
||||
start_url: '/',
|
||||
scope: '/',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
background_color: '#f6f0df',
|
||||
theme_color: '#221d18',
|
||||
categories: ['health', 'lifestyle', 'food'],
|
||||
icons: [
|
||||
{
|
||||
src: '/icons/icon.svg',
|
||||
sizes: 'any',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any',
|
||||
},
|
||||
{
|
||||
src: '/icons/icon-192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
{
|
||||
src: '/icons/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
{
|
||||
src: '/icons/icon-maskable-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -47,11 +47,11 @@
|
||||
"@vitest/coverage-v8": "^1.6.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,jsx,cjs,mjs}": [
|
||||
"*.{ts,tsx,jsx,cjs}": [
|
||||
"eslint --fix --max-warnings=0",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.js": [
|
||||
"*.{js,mjs}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{json,md,yml,yaml,css}": [
|
||||
|
||||