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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,88 @@ export const Component = () => {
};
```

### Redirects
Copy link
Owner

Choose a reason for hiding this comment

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

We carefully design and write our README (which will be on the website).
Can you write it down in docs instead for now?


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.

#### 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.
Expand Down
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