Repository navigation
Expand file tree
/
Copy pathnext.config.ts
More file actions
178 lines (168 loc) · 6.78 KB
/
Copy pathnext.config.ts
File metadata and controls
178 lines (168 loc) · 6.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import type { NextConfig } from "next";
import withPWA, { runtimeCaching as defaultRuntimeCaching } from "@ducanh2912/next-pwa";
import fs from "fs";
import path from "path";
import { createHash } from "crypto";
const nextConfig: NextConfig = {
output: "export",
trailingSlash: true,
experimental: { viewTransition: true },
};
// ---------------------------------------------------------------------------
// Offline precache entries (built at config load, i.e. during `next build`).
// Data bundles are read from public/data (generated by scripts/generate-data.ts,
// which runs first in the build script); images are globbed from public/images.
// Each entry's revision is a content hash so updates bust the cache cleanly.
// ---------------------------------------------------------------------------
interface PrecacheEntry {
url: string;
revision: string;
}
function hashFile(p: string): string {
return createHash("md5").update(fs.readFileSync(p)).digest("hex").slice(0, 16);
}
function collectPrecacheEntries(): PrecacheEntry[] {
const entries: PrecacheEntry[] = [];
const publicDir = path.join(process.cwd(), "public");
// All data bundles (+ dictionaries + manifest) from public/data.
const dataDir = path.join(publicDir, "data");
if (fs.existsSync(dataDir)) {
for (const f of fs.readdirSync(dataDir)) {
if (!f.endsWith(".json")) continue;
entries.push({ url: `/data/${f}`, revision: hashFile(path.join(dataDir, f)) });
}
}
// Search + calculator payloads (kept for those features).
for (const extra of ["search-data.json", "weapon-data.json"]) {
const p = path.join(publicDir, extra);
if (fs.existsSync(p)) entries.push({ url: `/${extra}`, revision: hashFile(p) });
}
// All images (pixel-art sprites / icons).
const imagesDir = path.join(publicDir, "images");
const walk = (dir: string) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (/\.(png|jpg|jpeg|webp|gif)$/.test(entry.name)) {
entries.push({
url: `/images/${path.relative(imagesDir, full).split(path.sep).join("/")}`,
revision: hashFile(full),
});
}
}
};
if (fs.existsSync(imagesDir)) walk(imagesDir);
return entries;
}
const additionalManifestEntries = collectPrecacheEntries();
// Version/cacheId from the data manifest hash so data updates invalidate cleanly.
let cacheId = "mhfu-blacksmith";
try {
const manifest = JSON.parse(
fs.readFileSync(path.join(process.cwd(), "public", "data", "data-manifest.json"), "utf-8")
) as { version?: string };
if (manifest.version) cacheId = `mhfu-${manifest.version}`;
} catch {
// manifest not generated yet (e.g. plain `next build` without the pipeline)
}
export default withPWA({
dest: "public",
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true,
reloadOnOnline: true,
disable: process.env.NODE_ENV === "development",
// When a navigation (full page load, or the hard-navigation Next.js falls
// back to when a client-side RSC fetch fails offline) misses every cache,
// serve this precached shell instead of a network error. It reads the
// *actual* requested URL client-side and renders the right weapon/armor/
// monster/decoration detail view from the already-cached data bundles —
// see src/app/offline. This is what lets any of the ~3800 detail pages
// work offline without precaching every single one of them.
fallbacks: {
document: "/offline/",
},
workboxOptions: {
disableDevLogs: true,
cacheId,
// Precache all data bundles, images, and the app shell at SW install.
additionalManifestEntries,
runtimeCaching: [
{
// Data bundles: precached by workbox, but also serve the manual
// offline cache the "Download for offline" button populates.
urlPattern: /\/data\/.+\.json$/,
handler: "CacheFirst",
options: {
cacheName: "mhfu-offline-v1",
},
},
{
// Cache data JSON files
urlPattern: /\/(search-data|weapon-data)\.json$/,
handler: "CacheFirst",
options: {
cacheName: "mhfu-offline-v1",
},
},
{
// Cache images (pixel art sprites)
urlPattern: /\/images\/.+\.(png|jpg|webp|gif)$/,
handler: "CacheFirst",
options: {
cacheName: "images",
expiration: { maxEntries: 1000, maxAgeSeconds: 30 * 24 * 60 * 60 },
},
},
// Every weapon/armor/monster/decoration is its own statically-exported
// route (~3800 of them) — App Router client navigation needs that
// specific route's RSC payload (and the "pages" cache for a plain
// document load/refresh) to exist offline, even though the visible
// content is rendered from the already-cached compact JSON bundles
// (src/lib/client-data.ts). We don't proactively download all ~3800
// pages (see src/lib/offline-cache.ts) — instead, raise next-pwa's
// default 32-entry cap on these three so pages you've actually
// browsed (or that were auto-prefetched by <Link>) stay cached instead
// of getting evicted almost immediately, which previously made
// offline navigation to anything but the last few visited pages 404.
{
urlPattern: ({ request, url: { pathname }, sameOrigin }) =>
request.headers.get("RSC") === "1" &&
request.headers.get("Next-Router-Prefetch") === "1" &&
sameOrigin &&
!pathname.startsWith("/api/"),
handler: "NetworkFirst",
options: {
cacheName: "pages-rsc-prefetch",
expiration: { maxEntries: 4000, maxAgeSeconds: 86400 },
},
},
{
urlPattern: ({ request, url: { pathname }, sameOrigin }) =>
request.headers.get("RSC") === "1" &&
sameOrigin &&
!pathname.startsWith("/api/"),
handler: "NetworkFirst",
options: {
cacheName: "pages-rsc",
expiration: { maxEntries: 4000, maxAgeSeconds: 86400 },
},
},
{
urlPattern: ({ url: { pathname }, sameOrigin }) =>
sameOrigin && !pathname.startsWith("/api/"),
handler: "NetworkFirst",
options: {
cacheName: "pages",
expiration: { maxEntries: 4000, maxAgeSeconds: 86400 },
},
},
// Supplying a custom `runtimeCaching` array replaces next-pwa's default
// one entirely (it doesn't merge). Append the defaults after our own
// rules above (data/image caches, and the higher-capacity page caches)
// so those take precedence and only truly uncovered request types
// (cross-origin assets, fonts, etc.) fall through to next-pwa's stock
// behavior.
...defaultRuntimeCaching,
],
},
})(nextConfig);