Skip to content

Commit 4fe7918

Browse files
committed
fix(html): follow the host's redirect to clean URLs
Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6433551 commit 4fe7918

2 files changed

Lines changed: 35 additions & 8 deletions

File tree

‎e2e/client-side-navigation.spec.js‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@ import { expect, test } from '@playwright/test';
33
const REMOTE_CONFIG_URL = 'https://nodejs.org/site.json';
44

55
/**
6-
* Navigates the way following a link does, and waits for the navigation to
7-
* finish.
6+
* Navigates the way following a link does, and waits for the page to show.
7+
* On hosts with clean URLs, the navigation that shows it follows the redirect.
88
*/
99
const navigate = (page, url) =>
10-
page.evaluate(url => navigation.navigate(url).finished.then(() => {}), url);
10+
page.evaluate(
11+
url =>
12+
new Promise(resolve => {
13+
navigation.addEventListener('navigatesuccess', () => resolve(), {
14+
once: true,
15+
});
16+
17+
navigation.navigate(url);
18+
}),
19+
url
20+
);
1121

1222
test.describe('Client-side navigation', () => {
1323
test.beforeEach(async ({ page }) => {
@@ -42,7 +52,8 @@ test.describe('Client-side navigation', () => {
4252

4353
await navigate(page, 'all.html');
4454

45-
await expect(page).toHaveURL(/\/all\.html$/);
55+
// `serve` redirects `all.html` to `all`, as hosts with clean URLs do
56+
await expect(page).toHaveURL(/\/all$/);
4657
await expect(page).toHaveTitle(/^All \|/);
4758
await expect(page.locator('meta[property="og:title"]')).toHaveAttribute(
4859
'content',

‎packages/react/src/html/ui/router.mjs‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,26 @@ export const startRouter = ({ unmount, islands }) => {
113113
}
114114

115115
const page = fetchPage(url);
116+
const entry = { page, expires: Date.now() + ROUTER_PAGE_LIFETIME };
116117

117118
pages.delete(url);
118-
pages.set(url, { page, expires: Date.now() + ROUTER_PAGE_LIFETIME });
119+
pages.set(url, entry);
119120

120-
if (pages.size > ROUTER_MAX_PAGES) {
121+
// Redirected pages take two entries (below)
122+
while (pages.size > ROUTER_MAX_PAGES) {
121123
pages.delete(pages.keys().next().value);
122124
}
123125

124-
// A failure is not kept, so the next attempt fetches again
125126
page.then(result => {
127+
// A failure is not kept, so the next attempt fetches again
126128
if (!result && pages.get(url)?.page === page) {
127129
pages.delete(url);
128130
}
131+
132+
// Following the redirect (see the handler) then needs no second fetch
133+
if (result && result.url !== url) {
134+
pages.set(result.url, entry);
135+
}
129136
});
130137

131138
return page;
@@ -167,12 +174,21 @@ export const startRouter = ({ unmount, islands }) => {
167174
* Swaps in the page the navigation leads to.
168175
*/
169176
async handler() {
170-
const page = await loadPage(withoutFragment(url.href));
177+
const href = withoutFragment(url.href);
178+
const page = await loadPage(href);
171179

172180
if (event.signal.aborted) {
173181
return;
174182
}
175183

184+
// Hosts with clean URLs redirect `fs.html` to `fs`: follow the redirect
185+
// as a full load would, replacing this navigation's history entry
186+
if (page && page.url !== href) {
187+
navigation.navigate(page.url + url.hash, { history: 'replace' });
188+
189+
return;
190+
}
191+
176192
const doc = page && parsePage(page, assets);
177193

178194
if (!doc) {

0 commit comments

Comments
 (0)