- Add web app manifest, maskable/apple-touch icons, and a production-only service worker that caches the app shell for offline start - Improve mobile UX: responsive breakpoints from ~320px, 44px touch targets, safe-area insets, dvh heights, 16px inputs to avoid iOS zoom - Serve .webmanifest with correct content type in the Go static handler - Document PWA behavior and mobile features in the README
99 lines
2.8 KiB
TypeScript
99 lines
2.8 KiB
TypeScript
import { defineConfig, type Plugin } from 'vite'
|
||
import vue from '@vitejs/plugin-vue'
|
||
|
||
// Service Worker als Build-Artefakt: Die Cache-Version ist der Build-Zeitstempel,
|
||
// damit Browser nach jedem Deploy den SW aktualisieren und alte Caches räumen.
|
||
const SW_SOURCE = `
|
||
const VERSION = 'wp-__SW_VERSION__';
|
||
const CACHE_NAME = 'wannpassts-' + VERSION;
|
||
const PRECACHE = [
|
||
'/',
|
||
'/manifest.webmanifest',
|
||
'/favicon.svg',
|
||
'/icons/icon-192.png',
|
||
'/icons/icon-512.png',
|
||
'/icons/icon-maskable-512.png',
|
||
'/icons/apple-touch-icon.png',
|
||
];
|
||
|
||
self.addEventListener('install', (event) => {
|
||
event.waitUntil((async () => {
|
||
const cache = await caches.open(CACHE_NAME);
|
||
await cache.addAll(PRECACHE).catch(() => {});
|
||
await self.skipWaiting();
|
||
})());
|
||
});
|
||
|
||
self.addEventListener('activate', (event) => {
|
||
event.waitUntil((async () => {
|
||
const keys = await caches.keys();
|
||
await Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)));
|
||
await self.clients.claim();
|
||
})());
|
||
});
|
||
|
||
self.addEventListener('fetch', (event) => {
|
||
const req = event.request;
|
||
if (req.method !== 'GET') return;
|
||
const url = new URL(req.url);
|
||
if (url.origin !== self.location.origin) return;
|
||
// API-Aufrufe immer live – niemals aus dem Cache bedienen.
|
||
if (url.pathname.startsWith('/api/')) return;
|
||
|
||
// Navigation: Netzwerk zuerst, offline App-Shell aus dem Cache.
|
||
if (req.mode === 'navigate') {
|
||
event.respondWith((async () => {
|
||
try {
|
||
const res = await fetch(req);
|
||
const cache = await caches.open(CACHE_NAME);
|
||
cache.put('/', res.clone());
|
||
return res;
|
||
} catch (err) {
|
||
const cached = (await caches.match(req)) || (await caches.match('/'));
|
||
if (cached) return cached;
|
||
return new Response('Offline – bitte Internetverbindung prüfen.', {
|
||
status: 503,
|
||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||
});
|
||
}
|
||
})());
|
||
return;
|
||
}
|
||
|
||
// Statische Assets: Cache zuerst (gehashte Namen sind unveränderlich).
|
||
event.respondWith((async () => {
|
||
const cached = await caches.match(req, { ignoreSearch: true });
|
||
if (cached) return cached;
|
||
const res = await fetch(req);
|
||
if (res.ok) {
|
||
const cache = await caches.open(CACHE_NAME);
|
||
cache.put(req, res.clone());
|
||
}
|
||
return res;
|
||
})());
|
||
});
|
||
`
|
||
|
||
function serviceWorkerPlugin(): Plugin {
|
||
return {
|
||
name: 'wannpassts-sw',
|
||
apply: 'build',
|
||
generateBundle() {
|
||
this.emitFile({
|
||
type: 'asset',
|
||
fileName: 'sw.js',
|
||
source: SW_SOURCE.replace('__SW_VERSION__', new Date().toISOString()),
|
||
})
|
||
},
|
||
}
|
||
}
|
||
|
||
export default defineConfig({
|
||
plugins: [vue(), serviceWorkerPlugin()],
|
||
server: {
|
||
port: 5173,
|
||
proxy: {
|
||
'/api': { target: 'http://localhost:8080', changeOrigin: true },
|
||
},
|
||
},
|
||
})
|