Skip to content

Commit d4c2b9b

Browse files
committed
Serve offline navigation fallback document
1 parent 4001039 commit d4c2b9b

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

packages/next/src/build/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4146,6 +4146,7 @@ export default async function build(
41464146
createOfflineNavigationServiceWorker({
41474147
buildId,
41484148
cacheNamespace: manifest.cacheNamespace,
4149+
fallbackDocumentHref: manifest.fallbackDocument.href,
41494150
manifestHref: manifest.manifest.href,
41504151
})
41514152
)

packages/next/src/build/offline-navigation-service-worker.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ export function getOfflineNavigationServiceWorkerFilePath(): string {
1212
export function createOfflineNavigationServiceWorker({
1313
buildId,
1414
cacheNamespace,
15+
fallbackDocumentHref,
1516
manifestHref,
1617
}: {
1718
buildId: string
1819
cacheNamespace: string
20+
fallbackDocumentHref: string
1921
manifestHref: string
2022
}): string {
2123
const metadata = JSON.stringify({
2224
buildId,
2325
cacheNamespace,
26+
fallbackDocumentHref,
2427
manifestHref,
2528
source: 'offline-navigation-service-worker',
2629
})
@@ -53,6 +56,26 @@ async function cacheOfflineNavigationResources(){
5356
const fallbackResponse=await fetchRequiredResource(manifest.fallbackDocument.href);
5457
await cache.put(manifest.fallbackDocument.href,fallbackResponse);
5558
}
59+
function isDocumentNavigationRequest(request){
60+
if(request.method!=='GET'||request.mode!=='navigate'||request.destination!=='document'){
61+
return false;
62+
}
63+
const url=new URL(request.url);
64+
return url.origin===self.location.origin;
65+
}
66+
async function fetchDocumentNavigation(request){
67+
try{
68+
return await fetch(request);
69+
}catch(err){
70+
const metadata=self.__NEXT_OFFLINE_NAVIGATION_SW;
71+
const cache=await caches.open(metadata.cacheNamespace);
72+
const fallbackResponse=await cache.match(metadata.fallbackDocumentHref);
73+
if(fallbackResponse){
74+
return fallbackResponse;
75+
}
76+
throw err;
77+
}
78+
}
5679
self.addEventListener('install',(event)=>{
5780
event.waitUntil((async()=>{
5881
await cacheOfflineNavigationResources();
@@ -71,5 +94,10 @@ self.addEventListener('activate',(event)=>{
7194
await self.clients.claim();
7295
})());
7396
});
97+
self.addEventListener('fetch',(event)=>{
98+
if(isDocumentNavigationRequest(event.request)){
99+
event.respondWith(fetchDocumentNavigation(event.request));
100+
}
101+
});
74102
`
75103
}

test/production/app-dir/offline-navigations/offline-navigations.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { existsSync } from 'fs'
22
import { join } from 'path'
33
import { nextTestSetup } from 'e2e-utils'
44
import { retry } from 'next-test-utils'
5+
import type * as Playwright from 'playwright'
56

67
describe('offlineNavigations build artifacts', () => {
78
const { next } = nextTestSetup({
@@ -67,14 +68,18 @@ describe('offlineNavigations build artifacts', () => {
6768
expect(serviceWorkerScript).toContain(
6869
`"cacheNamespace":"next-offline-navigation-v1:${buildId}:/docs"`
6970
)
71+
expect(serviceWorkerScript).toContain(
72+
`"fallbackDocumentHref":"/docs/_next/static/${buildId}/_offline-navigation-fallback.html"`
73+
)
7074
expect(serviceWorkerScript).toContain(
7175
`"manifestHref":"/docs/_next/static/${buildId}/_offline-navigation-manifest.json"`
7276
)
7377
expect(serviceWorkerScript).toContain('cacheOfflineNavigationResources')
7478
expect(serviceWorkerScript).toContain('caches.delete')
79+
expect(serviceWorkerScript).toContain('isDocumentNavigationRequest')
7580
expect(serviceWorkerScript).toContain('skipWaiting')
7681
expect(serviceWorkerScript).toContain('clients.claim')
77-
expect(serviceWorkerScript).not.toContain('respondWith')
82+
expect(serviceWorkerScript).toContain('respondWith')
7883

7984
expect(manifestJson).toEqual({
8085
version: 1,
@@ -107,6 +112,7 @@ describe('offlineNavigations build artifacts', () => {
107112
const { buildId } = await getOfflineNavigationArtifactPaths()
108113
await next.start({ skipBuild: true })
109114

115+
let page: Playwright.Page | undefined
110116
try {
111117
const swResponse = await next.fetch(
112118
`/docs/_next/static/_offline-navigation-service-worker.js${next.getDeploymentIdQuery()}`
@@ -117,7 +123,11 @@ describe('offlineNavigations build artifacts', () => {
117123
)
118124
expect(swResponse.headers.get('service-worker-allowed')).toBe('/docs/')
119125

120-
const browser = await next.browser('/docs')
126+
const browser = await next.browser('/docs', {
127+
beforePageLoad(p: Playwright.Page) {
128+
page = p
129+
},
130+
})
121131
await retry(async () => {
122132
const registration = await browser.eval(async () => {
123133
if (!('serviceWorker' in navigator)) {
@@ -144,6 +154,11 @@ describe('offlineNavigations build artifacts', () => {
144154
scriptURL: `${next.url}/docs/_next/static/_offline-navigation-service-worker.js${next.getDeploymentIdQuery()}`,
145155
})
146156
})
157+
await retry(async () => {
158+
expect(
159+
await browser.eval(() => Boolean(navigator.serviceWorker.controller))
160+
).toBe(true)
161+
})
147162

148163
const cacheState = await browser.eval(async () => {
149164
const cacheNames = (await caches.keys()).filter((cacheName) =>
@@ -181,6 +196,33 @@ describe('offlineNavigations build artifacts', () => {
181196
])
182197
)
183198

199+
await page!.context().setOffline(true)
200+
const nonNavigationResult = await browser.eval(async () => {
201+
try {
202+
await fetch('/docs?__next_offline_probe=1', {
203+
headers: { rsc: '1' },
204+
})
205+
return 'resolved'
206+
} catch {
207+
return 'rejected'
208+
}
209+
})
210+
expect(nonNavigationResult).toBe('rejected')
211+
212+
const offlineResponse = await page!.goto(
213+
`${next.url}/docs/offline-navigation-cache-miss`,
214+
{ waitUntil: 'domcontentloaded' }
215+
)
216+
expect(offlineResponse?.status()).toBe(200)
217+
expect(
218+
await browser.eval(() =>
219+
document.documentElement.hasAttribute(
220+
'data-next-offline-navigation-fallback'
221+
)
222+
)
223+
).toBe(true)
224+
await page!.context().setOffline(false)
225+
184226
await browser.eval(async () => {
185227
if (!('serviceWorker' in navigator)) {
186228
return
@@ -201,6 +243,9 @@ describe('offlineNavigations build artifacts', () => {
201243
)
202244
})
203245
} finally {
246+
if (page) {
247+
await page.context().setOffline(false)
248+
}
204249
await next.stop()
205250
}
206251
})

0 commit comments

Comments
 (0)