feat: add PWA support for offline cold load
- Add vite-plugin-pwa with Workbox caching strategy - Cache all static assets (JS, CSS, HTML, fonts, WASM) - Enable service worker in dev mode for testing - Add PWA manifest with app name and icons - Add SVG icons for PWA (192x192 and 512x512) - Increase cache limit to 10MB for large chunks (Board ~8MB) - Add runtime caching for API responses and Google Fonts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
03dde7cb16
commit
46cc0485a2
|
|
@ -5,6 +5,7 @@
|
||||||
<title>Jeff Emmett</title>
|
<title>Jeff Emmett</title>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🍄</text></svg>" />
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🍄</text></svg>" />
|
||||||
|
<link rel="apple-touch-icon" href="/pwa-192x192.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta http-equiv="Permissions-Policy" content="midi=*, microphone=*, camera=*, autoplay=*">
|
<meta http-equiv="Permissions-Policy" content="midi=*, microphone=*, camera=*, autoplay=*">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -115,6 +115,7 @@
|
||||||
"playwright": "^1.57.0",
|
"playwright": "^1.57.0",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^6.0.3",
|
"vite": "^6.0.3",
|
||||||
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vite-plugin-top-level-await": "^1.6.0",
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
"vite-plugin-wasm": "^3.5.0",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192" width="192" height="192">
|
||||||
|
<rect width="192" height="192" fill="#1a1a2e" rx="24"/>
|
||||||
|
<text x="96" y="130" font-size="120" text-anchor="middle" font-family="system-ui">🍄</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 249 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||||
|
<rect width="512" height="512" fill="#1a1a2e" rx="64"/>
|
||||||
|
<text x="256" y="350" font-size="320" text-anchor="middle" font-family="system-ui">🍄</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 250 B |
|
|
@ -2,6 +2,7 @@ import { defineConfig, loadEnv } from "vite"
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
import wasm from "vite-plugin-wasm"
|
import wasm from "vite-plugin-wasm"
|
||||||
import topLevelAwait from "vite-plugin-top-level-await"
|
import topLevelAwait from "vite-plugin-top-level-await"
|
||||||
|
import { VitePWA } from "vite-plugin-pwa"
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
// Load env file based on `mode` in the current working directory.
|
// Load env file based on `mode` in the current working directory.
|
||||||
|
|
@ -25,17 +26,103 @@ export default defineConfig(({ mode }) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
envPrefix: ["VITE_"],
|
envPrefix: ["VITE_"],
|
||||||
plugins: [react(), wasm(), topLevelAwait()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
wasm(),
|
||||||
|
topLevelAwait(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
injectRegister: 'auto',
|
||||||
|
workbox: {
|
||||||
|
// Cache all static assets
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2,wasm}'],
|
||||||
|
// Increase the limit for large chunks (Board is ~8MB with tldraw, automerge, etc.)
|
||||||
|
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
|
||||||
|
// Runtime caching for dynamic requests
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
// Cache API responses with network-first strategy
|
||||||
|
urlPattern: /^https?:\/\/.*\/api\/.*/i,
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'api-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 100,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24, // 24 hours
|
||||||
|
},
|
||||||
|
networkTimeoutSeconds: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Cache fonts
|
||||||
|
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
||||||
|
handler: 'CacheFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'google-fonts-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
||||||
|
handler: 'CacheFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'gstatic-fonts-cache',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
manifest: {
|
||||||
|
name: 'Jeff Emmett Canvas',
|
||||||
|
short_name: 'Canvas',
|
||||||
|
description: 'Collaborative canvas for research and creativity',
|
||||||
|
theme_color: '#1a1a2e',
|
||||||
|
background_color: '#1a1a2e',
|
||||||
|
display: 'standalone',
|
||||||
|
start_url: '/',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/pwa-192x192.svg',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/svg+xml',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/pwa-512x512.svg',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/svg+xml',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/pwa-512x512.svg',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/svg+xml',
|
||||||
|
purpose: 'maskable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
// Enable SW in development for testing
|
||||||
|
enabled: true,
|
||||||
|
type: 'module',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
// Force IPv4 to ensure compatibility with WSL2 and remote devices
|
// Force IPv4 to ensure compatibility with WSL2 and remote devices
|
||||||
listen: "0.0.0.0",
|
listen: "0.0.0.0",
|
||||||
// Configure HMR to use the correct hostname for WebSocket connections
|
// Configure HMR to use the client's hostname for WebSocket connections
|
||||||
|
// This allows HMR to work from any network (localhost, LAN, Tailscale)
|
||||||
hmr: {
|
hmr: {
|
||||||
host: wslIp,
|
// Use 'clientPort' to let client determine the correct host
|
||||||
port: 5173,
|
clientPort: 5173,
|
||||||
},
|
},
|
||||||
// Proxy API requests to the worker server
|
// Proxy API requests to the worker server
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue