Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: client side redirects #928

Merged
merged 6 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/redirects.mdx
Original file line number Diff line number Diff line change
@@ -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 `<Link />` 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 `<Link />` 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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to change it in the next patch version. 😝


#### 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
```
41 changes: 41 additions & 0 deletions e2e/broken-link.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,45 @@ 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();

// The page renders the custom 404.tsx
await expect(page.getByRole('heading')).toHaveText('Custom not found');
// 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();
await expect(page.getByRole('heading')).toHaveText('Index');

await terminate(pid!);
});
});
7 changes: 7 additions & 0 deletions e2e/fixtures/broken-links/public/serve.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"redirects": [
{ "source": "/redirect", "destination": "/exists" },
{ "source": "/RSC/redirect.txt", "destination": "/RSC/exists.txt" },
{ "source": "/broken-redirect", "destination": "/broken" }
]
}
2 changes: 1 addition & 1 deletion e2e/fixtures/broken-links/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Index() {
<Link to="/broken">Broken link</Link>
</p>
<p>
<Link to="/redirect">Correct Redirect</Link>
<Link to="/redirect">Correct redirect</Link>
</p>
<p>
<Link to="/broken-redirect">Broken redirect</Link>
Expand Down
Loading