From 916f15778668cac909cb9690087988fedee547d0 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 2 Oct 2024 10:08:15 +0200 Subject: [PATCH 1/5] test: add redirects to static host --- e2e/fixtures/broken-links/public/serve.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 e2e/fixtures/broken-links/public/serve.json diff --git a/e2e/fixtures/broken-links/public/serve.json b/e2e/fixtures/broken-links/public/serve.json new file mode 100644 index 000000000..a85aa991d --- /dev/null +++ b/e2e/fixtures/broken-links/public/serve.json @@ -0,0 +1,6 @@ +{ + "redirects": [ + { "source": "/redirect", "destination": "/exists" }, + { "source": "/broken-redirect", "destination": "/broken" } + ] +} From 28dbcb908f840445b6e636f18f3e77232f887eab Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Wed, 2 Oct 2024 10:17:01 +0200 Subject: [PATCH 2/5] test: integration tests for client side redirect handling --- e2e/broken-link.spec.ts | 40 +++++++++++++++++++ e2e/fixtures/broken-links/src/pages/index.tsx | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/e2e/broken-link.spec.ts b/e2e/broken-link.spec.ts index f4869a2e8..5ad1286e0 100644 --- a/e2e/broken-link.spec.ts +++ b/e2e/broken-link.spec.ts @@ -170,4 +170,44 @@ test.describe('client side navigation', () => { await terminate(pid!); }); + + test('redirect', async ({ page }) => { + const [port, pid] = await start(true); + await page.goto(`http://localhost:${port}`); + + // Click on a link to a redirect + await page.getByRole('link', { name: 'Correct redirect' }).click(); + + // The page renders the target page + await expect(page.getByRole('heading')).toHaveText('Existing page'); + // The browsers URL is the one of the target page + expect(page.url()).toBe(`http://localhost:${port}/exists`); + + // Go back to the index page + await page.getByRole('link', { name: 'Back' }).click(); + await expect(page.getByRole('heading')).toHaveText('Index'); + + await terminate(pid!); + }); + + test('broken redirect', async ({ page }) => { + const [port, pid] = await start(true); + await page.goto(`http://localhost:${port}`); + + // Click on a link to a broken redirect + await page.getByRole('link', { name: 'Broken redirect' }).click(); + + // Navigate to a page that redirects to a non-existing page + await page.goto(`http://localhost:${port}/broken-redirect`); + // The page renders the custom 404.tsx + await expect(page.getByRole('heading')).toHaveText('Custom not found'); + // The browsers URL remains the one that was redirected to + expect(page.url()).toBe(`http://localhost:${port}/broken`); + + // Go back to the index page + await page.getByRole('link', { name: 'Back' }).click(); + await expect(page.getByRole('heading')).toHaveText('Index'); + + await terminate(pid!); + }); }); diff --git a/e2e/fixtures/broken-links/src/pages/index.tsx b/e2e/fixtures/broken-links/src/pages/index.tsx index d6b53c4b5..9c1a93842 100644 --- a/e2e/fixtures/broken-links/src/pages/index.tsx +++ b/e2e/fixtures/broken-links/src/pages/index.tsx @@ -11,7 +11,7 @@ export default function Index() { Broken link

- Correct Redirect + Correct redirect

Broken redirect From 5892e4ab2bbb1676710db68b1c2d1d20c71ab178 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Thu, 3 Oct 2024 04:45:02 +0200 Subject: [PATCH 3/5] test: handle client side redirects by redirecting RSC files --- e2e/broken-link.spec.ts | 9 +++++---- e2e/fixtures/broken-links/public/serve.json | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/e2e/broken-link.spec.ts b/e2e/broken-link.spec.ts index 5ad1286e0..fe6bd1d30 100644 --- a/e2e/broken-link.spec.ts +++ b/e2e/broken-link.spec.ts @@ -197,12 +197,13 @@ test.describe('client side navigation', () => { // Click on a link to a broken redirect await page.getByRole('link', { name: 'Broken redirect' }).click(); - // Navigate to a page that redirects to a non-existing page - await page.goto(`http://localhost:${port}/broken-redirect`); // The page renders the custom 404.tsx await expect(page.getByRole('heading')).toHaveText('Custom not found'); - // The browsers URL remains the one that was redirected to - expect(page.url()).toBe(`http://localhost:${port}/broken`); + // The browsers URL remains the link href + // NOTE: This is inconsistent with server side navigation, but + // there is no way to tell where the RSC request was redirected + // to before failing with 404. + expect(page.url()).toBe(`http://localhost:${port}/broken-redirect`); // Go back to the index page await page.getByRole('link', { name: 'Back' }).click(); diff --git a/e2e/fixtures/broken-links/public/serve.json b/e2e/fixtures/broken-links/public/serve.json index a85aa991d..2b5d974d4 100644 --- a/e2e/fixtures/broken-links/public/serve.json +++ b/e2e/fixtures/broken-links/public/serve.json @@ -1,6 +1,7 @@ { "redirects": [ { "source": "/redirect", "destination": "/exists" }, + { "source": "/RSC/redirect.txt", "destination": "/RSC/exists.txt" }, { "source": "/broken-redirect", "destination": "/broken" } ] } From 0a350e41db74809b8d5f1be12991d5901c875d33 Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Thu, 3 Oct 2024 06:06:00 +0200 Subject: [PATCH 4/5] docs: documentation on redirects --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/README.md b/README.md index 7e4aa951f..a809a2658 100644 --- a/README.md +++ b/README.md @@ -545,6 +545,88 @@ export const Component = () => { }; ``` +### Redirects + +Redirects are not handled by Waku directly. Instead, you can use either a custom middleware or the hosting environment to achieve that. The `` component does not deal with redirects either and will by default show the **404** page instead. To resolve this, you have to add an additional redirect for each redirected path, that points Waku to the correct `RSC` file. If there is a redirect from `/old` to `/new`, there also has to be one from `/RSC/old.txt`` to `/RSC/new.txt`to make the `` component`s smooth page transition work. + +> The `/RSC/` file naming convention is [subject to change](https://github.com/dai-shi/waku/discussions/929#discussioncomment-10825975) in future versions of Waku. + +#### Redirect via middleware + +Create a new middleware somewhere in your project and add it to the `waku.config.ts` file. + +```typescript +// ./src/middleware/redirect.ts +import type { Middleware } from 'waku/config'; + +const redirectsMiddleware: Middleware = () => async (ctx, next) => { + // Define the list of redirects. + const redirects = { + '/old': '/new', + // ... add more redirects here + }; + + // Create a corresponding /RSC/ entry for each redirect. + const withRSC = Object.fromEntries( + Object.entries(redirects).flatMap(([from, to]) => [ + [from, to], + [`/RSC${from}.txt`, `/RSC${to}.txt`], + ]), + ); + + if (withRSC[ctx.req.url.pathname]) { + ctx.res.status = 301; + ctx.res.headers = { + Location: redirects[ctx.req.url.pathname], + }; + } else { + return await next(); + } +}; + +export default redirectsMiddleware; +``` + +```typescript +// ./waku.config.ts +import type { Config } from 'waku/config'; + +export default { + middleware: () => [ + import('./src/middleware/redirects.js'), + import('waku/middleware/dev-server'), + import('waku/middleware/headers'), + import('waku/middleware/rsc'), + import('waku/middleware/ssr'), + ], +} satisfies Config; +``` + +#### Redirect via hosting environment + +This very much depends on the hosting environment you are using. For example, on [Vercel](https://vercel.com/docs/projects/project-configuration#redirects) you can use the `vercel.json` file to define redirects. + +```json +{ + "redirects": [ + { "source": "/old", "destination": "/new", "permanent": true }, + { + "source": "/RSC/old.txt", + "destination": "/RSC/new.txt", + "permanent": true + } + ] +} +``` + +[Netlify](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file) and [Cloudflare pages](https://developers.cloudflare.com/pages/configuration/redirects/) will respect a `_redirects` file that you can place in the `public` folder: +bbb + +``` +/old /new 301 +/RSC/old.txt /RSC/new.txt 301 +``` + ## Metadata Waku automatically hoists any title, meta, and link tags to the document head. That means adding meta tags is as simple as adding them to any of your layout or page components. From 6b898a6d9d887878a979bd729f4ed34a20e4ec6f Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Fri, 4 Oct 2024 05:36:37 +0200 Subject: [PATCH 5/5] docs: move redirects documentation into the docs folder --- README.md | 82 ------------------------------------------- docs/redirects.mdx | 87 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 docs/redirects.mdx diff --git a/README.md b/README.md index a809a2658..7e4aa951f 100644 --- a/README.md +++ b/README.md @@ -545,88 +545,6 @@ export const Component = () => { }; ``` -### Redirects - -Redirects are not handled by Waku directly. Instead, you can use either a custom middleware or the hosting environment to achieve that. The `` component does not deal with redirects either and will by default show the **404** page instead. To resolve this, you have to add an additional redirect for each redirected path, that points Waku to the correct `RSC` file. If there is a redirect from `/old` to `/new`, there also has to be one from `/RSC/old.txt`` to `/RSC/new.txt`to make the `` component`s smooth page transition work. - -> The `/RSC/` file naming convention is [subject to change](https://github.com/dai-shi/waku/discussions/929#discussioncomment-10825975) in future versions of Waku. - -#### Redirect via middleware - -Create a new middleware somewhere in your project and add it to the `waku.config.ts` file. - -```typescript -// ./src/middleware/redirect.ts -import type { Middleware } from 'waku/config'; - -const redirectsMiddleware: Middleware = () => async (ctx, next) => { - // Define the list of redirects. - const redirects = { - '/old': '/new', - // ... add more redirects here - }; - - // Create a corresponding /RSC/ entry for each redirect. - const withRSC = Object.fromEntries( - Object.entries(redirects).flatMap(([from, to]) => [ - [from, to], - [`/RSC${from}.txt`, `/RSC${to}.txt`], - ]), - ); - - if (withRSC[ctx.req.url.pathname]) { - ctx.res.status = 301; - ctx.res.headers = { - Location: redirects[ctx.req.url.pathname], - }; - } else { - return await next(); - } -}; - -export default redirectsMiddleware; -``` - -```typescript -// ./waku.config.ts -import type { Config } from 'waku/config'; - -export default { - middleware: () => [ - import('./src/middleware/redirects.js'), - import('waku/middleware/dev-server'), - import('waku/middleware/headers'), - import('waku/middleware/rsc'), - import('waku/middleware/ssr'), - ], -} satisfies Config; -``` - -#### Redirect via hosting environment - -This very much depends on the hosting environment you are using. For example, on [Vercel](https://vercel.com/docs/projects/project-configuration#redirects) you can use the `vercel.json` file to define redirects. - -```json -{ - "redirects": [ - { "source": "/old", "destination": "/new", "permanent": true }, - { - "source": "/RSC/old.txt", - "destination": "/RSC/new.txt", - "permanent": true - } - ] -} -``` - -[Netlify](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file) and [Cloudflare pages](https://developers.cloudflare.com/pages/configuration/redirects/) will respect a `_redirects` file that you can place in the `public` folder: -bbb - -``` -/old /new 301 -/RSC/old.txt /RSC/new.txt 301 -``` - ## Metadata Waku automatically hoists any title, meta, and link tags to the document head. That means adding meta tags is as simple as adding them to any of your layout or page components. diff --git a/docs/redirects.mdx b/docs/redirects.mdx new file mode 100644 index 000000000..89302acc8 --- /dev/null +++ b/docs/redirects.mdx @@ -0,0 +1,87 @@ +--- +slug: redirects +title: Redirects +description: How to add HTTP redirects to your Waku project. +--- + +### Redirects + +Redirects are not handled by Waku directly. Instead, you can use either a custom middleware or the hosting environment to achieve that. The `` component does not deal with redirects either and will by default show the **404** page instead. To resolve this, you have to add an additional redirect for each redirected path, that points Waku to the correct `RSC` file. If there is a redirect from `/old` to `/new`, there also has to be one from `/RSC/old.txt`` to `/RSC/new.txt`to make the `` component`s smooth page transition work. + +> The `/RSC/` file naming convention is [subject to change](https://github.com/dai-shi/waku/discussions/929#discussioncomment-10825975) in future versions of Waku. + +#### Redirect via middleware + +Create a new middleware somewhere in your project and add it to the `waku.config.ts` file. + +```typescript +// ./src/middleware/redirect.ts +import type { Middleware } from 'waku/config'; + +const redirectsMiddleware: Middleware = () => async (ctx, next) => { + // Define the list of redirects. + const redirects = { + '/old': '/new', + // ... add more redirects here + }; + + // Create a corresponding /RSC/ entry for each redirect. + const withRSC = Object.fromEntries( + Object.entries(redirects).flatMap(([from, to]) => [ + [from, to], + [`/RSC${from}.txt`, `/RSC${to}.txt`], + ]), + ); + + if (withRSC[ctx.req.url.pathname]) { + ctx.res.status = 301; + ctx.res.headers = { + Location: redirects[ctx.req.url.pathname], + }; + } else { + return await next(); + } +}; + +export default redirectsMiddleware; +``` + +```typescript +// ./waku.config.ts +import type { Config } from 'waku/config'; + +export default { + middleware: () => [ + import('./src/middleware/redirects.js'), + import('waku/middleware/dev-server'), + import('waku/middleware/headers'), + import('waku/middleware/rsc'), + import('waku/middleware/ssr'), + ], +} satisfies Config; +``` + +#### Redirect via hosting environment + +This very much depends on the hosting environment you are using. For example, on [Vercel](https://vercel.com/docs/projects/project-configuration#redirects) you can use the `vercel.json` file to define redirects. + +```json +{ + "redirects": [ + { "source": "/old", "destination": "/new", "permanent": true }, + { + "source": "/RSC/old.txt", + "destination": "/RSC/new.txt", + "permanent": true + } + ] +} +``` + +[Netlify](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file) and [Cloudflare pages](https://developers.cloudflare.com/pages/configuration/redirects/) will respect a `_redirects` file that you can place in the `public` folder: +bbb + +``` +/old /new 301 +/RSC/old.txt /RSC/new.txt 301 +```