Skip to content
Merged
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ server entry); those marked _explicit_ provide a `src/waku.server.tsx` that call
| [`tanstack-router`](fs-router/tanstack-router) | Replace the client router with TanStack Router _(explicit)_ |
| [`no-ssr`](fs-router/no-ssr) | Disable SSR for client-only rendering _(explicit)_ |
| [`cloudflare`](fs-router/cloudflare) | Deploy to Cloudflare Workers _(explicit)_ |
| [`nextjs-blog-starter`](fs-router/nextjs-blog-starter) | Next.js blog-starter, migrated: static generation, metadata, markdown |
| [`nextjs-dashboard`](fs-router/nextjs-dashboard) | Next.js Learn dashboard, migrated: auth, a database, server actions, search |
| [`nextjs-commerce`](fs-router/nextjs-commerce) | Next.js Commerce, migrated: cookies, optimistic UI, sitemap and OG images |
| [`nextjs-photo-modal`](fs-router/nextjs-photo-modal) | Nextgram, reimplemented: parallel and intercepting routes |

### create-pages

Expand Down Expand Up @@ -94,6 +98,7 @@ The same examples, grouped by what they teach.
| Integrations | [`create-pages/react-tweet`](create-pages/react-tweet), [`fs-router/tanstack-router`](fs-router/tanstack-router) |
| Deployment | [`fs-router/cloudflare`](fs-router/cloudflare) |
| Tooling & config | [`minimal-api/path-alias`](minimal-api/path-alias) |
| Migrating from Next.js | [`fs-router/nextjs-blog-starter`](fs-router/nextjs-blog-starter), [`fs-router/nextjs-dashboard`](fs-router/nextjs-dashboard), [`fs-router/nextjs-commerce`](fs-router/nextjs-commerce), [`fs-router/nextjs-photo-modal`](fs-router/nextjs-photo-modal) |

## Running an example

Expand Down
16 changes: 13 additions & 3 deletions e2e/examples-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* Smoke tests for all examples.
*
* Boots every example in both DEV and PRD modes and checks that the home page
* renders with a "Waku" title. Examples are discovered by scanning the API
* group directories at the repo root.
* renders with a "Waku" title, or the one listed in `expectedTitles`. Examples
* are discovered by scanning the API group directories at the repo root.
*/
import type { ChildProcess } from 'node:child_process';
import { exec } from 'node:child_process';
Expand Down Expand Up @@ -36,6 +36,14 @@ const examples = groups.flatMap((group) => {
}));
});

// The Next.js migrations keep the titles of the apps they were migrated from.
const expectedTitles: Record<string, RegExp> = {
'fs-router/nextjs-blog-starter': /^Next\.js Blog Example/,
'fs-router/nextjs-commerce': /^Acme Store/,
'fs-router/nextjs-dashboard': /^Acme Dashboard/,
'fs-router/nextjs-photo-modal': /^NextGram/,
};

const modes = ['DEV', 'PRD'] as const;

test.describe.configure({ mode: 'parallel' });
Expand Down Expand Up @@ -90,7 +98,9 @@ for (const { name, cwd } of examples) {
await page.goto(`http://localhost:${port}/`);
// title may not be ready yet
await page.waitForLoadState('load');
await expect.poll(() => page.title()).toMatch(/^Waku/);
await expect
.poll(() => page.title())
.toMatch(expectedTitles[name] ?? /^Waku/);
});
});
}
Expand Down
21 changes: 21 additions & 0 deletions fs-router/nextjs-blog-starter/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2025 Vercel, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
44 changes: 44 additions & 0 deletions fs-router/nextjs-blog-starter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Next.js `blog-starter` → Waku

A migration of [`vercel/next.js/examples/blog-starter`](https://github.com/vercel/next.js/tree/canary/examples/blog-starter)
to Waku. Markdown files in `_posts/` are read at build time and every page is
prerendered, which is what the original does too.

Code and images taken from the original are © Vercel, Inc., under the MIT
license in [`LICENSE`](LICENSE).

```sh
pnpm install
pnpm typegen # generates src/pages.gen.ts for typed routes
pnpm dev
pnpm build && pnpm start
```

## What changed

| Next.js | Waku |
| --- | --- |
| `app/layout.tsx` renders `<html>`/`<body>` | `src/pages/_root.tsx` renders the document shell, `src/pages/_layout.tsx` renders inside it |
| `export const metadata` | `<title>`/`<meta>`/`<link>` rendered in the component; Waku hoists them, and a page's override its layout's |
| `generateMetadata()` | same — render the tags with the page, using data you already fetched |
| `generateStaticParams()` | `staticPaths` in the page's `getConfig()` |
| (implicitly static) | `render: 'static'` in `getConfig()`, stated per page and layout |
| `params: Promise<{ slug }>` | `slug` arrives as a plain prop, typed by `PageProps<'/posts/[slug]'>` |
| `notFound()` from `next/navigation` | `unstable_notFound()` from `waku/router/server` |
| `next/link` `<Link href>` | `<Link to>` from `waku` |
| `next/image` | plain `<img>` — Waku has no image component |
| `next/font/google` | `@fontsource/inter`, imported from `globals.css` |
| `app/_components/` | `src/components/` |
| tsconfig `paths` resolve automatically | the same alias is declared in `waku.config.ts` for Vite |
| `postcss.config.js` with `module.exports` | ESM `export default` — Waku packages are `"type": "module"` |

Tailwind v3, PostCSS, CSS modules and `dangerouslySetInnerHTML` all work unchanged.

## Gotcha: `og:image` does not merge

The Next.js original sets a default `og:image` in `app/layout.tsx` and overrides it
in `app/posts/[slug]/page.tsx`. Waku merges a page's `title`, `description` and
single-value `og:*` tags over its layout's, but `og:image` can repeat, so it is
emitted as written and a layout default would come first on every post. The home
page and each post therefore declare their own `og:image`, and the layout declares
none.
19 changes: 19 additions & 0 deletions fs-router/nextjs-blog-starter/_posts/dynamic-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: "Dynamic Routing and Static Generation"
excerpt: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus."
coverImage: "/assets/blog/dynamic-routing/cover.jpg"
date: "2020-03-16T05:35:07.322Z"
author:
name: JJ Kasper
picture: "/assets/blog/authors/jj.jpeg"
ogImage:
url: "/assets/blog/dynamic-routing/cover.jpg"
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus. Praesent elementum facilisis leo vel fringilla. Congue mauris rhoncus aenean vel. Egestas sed tempus urna et pharetra pharetra massa massa ultricies.

Venenatis cras sed felis eget velit. Consectetur libero id faucibus nisl tincidunt. Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Volutpat consequat mauris nunc congue nisi vitae. Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Nibh cras pulvinar mattis nunc sed. Luctus accumsan tortor posuere ac ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod.

## Lorem Ipsum

Tristique senectus et netus et malesuada fames ac turpis. Ridiculous mus mauris vitae ultricies leo integer malesuada nunc vel. In mollis nunc sed id semper. Egestas tellus rutrum tellus pellentesque. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Quis blandit turpis cursus in hac habitasse platea dictumst quisque. Eros donec ac odio tempor orci dapibus ultrices. Aliquam sem et tortor consequat id porta nibh. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo nulla. Diam vulputate ut pharetra sit amet. Ut tellus elementum sagittis vitae et leo. Arcu non odio euismod lacinia at quis risus sed vulputate.
19 changes: 19 additions & 0 deletions fs-router/nextjs-blog-starter/_posts/hello-world.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: "Learn How to Pre-render Pages Using Static Generation with Next.js"
excerpt: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus."
coverImage: "/assets/blog/hello-world/cover.jpg"
date: "2020-03-16T05:35:07.322Z"
author:
name: Tim Neutkens
picture: "/assets/blog/authors/tim.jpeg"
ogImage:
url: "/assets/blog/hello-world/cover.jpg"
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus. Praesent elementum facilisis leo vel fringilla. Congue mauris rhoncus aenean vel. Egestas sed tempus urna et pharetra pharetra massa massa ultricies.

Venenatis cras sed felis eget velit. Consectetur libero id faucibus nisl tincidunt. Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Volutpat consequat mauris nunc congue nisi vitae. Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Nibh cras pulvinar mattis nunc sed. Luctus accumsan tortor posuere ac ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod.

## Lorem Ipsum

Tristique senectus et netus et malesuada fames ac turpis. Ridiculous mus mauris vitae ultricies leo integer malesuada nunc vel. In mollis nunc sed id semper. Egestas tellus rutrum tellus pellentesque. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Quis blandit turpis cursus in hac habitasse platea dictumst quisque. Eros donec ac odio tempor orci dapibus ultrices. Aliquam sem et tortor consequat id porta nibh. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo nulla. Diam vulputate ut pharetra sit amet. Ut tellus elementum sagittis vitae et leo. Arcu non odio euismod lacinia at quis risus sed vulputate.
19 changes: 19 additions & 0 deletions fs-router/nextjs-blog-starter/_posts/preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: "Preview Mode for Static Generation"
excerpt: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus."
coverImage: "/assets/blog/preview/cover.jpg"
date: "2020-03-16T05:35:07.322Z"
author:
name: Joe Haddad
picture: "/assets/blog/authors/joe.jpeg"
ogImage:
url: "/assets/blog/preview/cover.jpg"
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus. Praesent elementum facilisis leo vel fringilla. Congue mauris rhoncus aenean vel. Egestas sed tempus urna et pharetra pharetra massa massa ultricies.

Venenatis cras sed felis eget velit. Consectetur libero id faucibus nisl tincidunt. Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Volutpat consequat mauris nunc congue nisi vitae. Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Nibh cras pulvinar mattis nunc sed. Luctus accumsan tortor posuere ac ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod.

## Lorem Ipsum

Tristique senectus et netus et malesuada fames ac turpis. Ridiculous mus mauris vitae ultricies leo integer malesuada nunc vel. In mollis nunc sed id semper. Egestas tellus rutrum tellus pellentesque. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Quis blandit turpis cursus in hac habitasse platea dictumst quisque. Eros donec ac odio tempor orci dapibus ultrices. Aliquam sem et tortor consequat id porta nibh. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo nulla. Diam vulputate ut pharetra sit amet. Ut tellus elementum sagittis vitae et leo. Arcu non odio euismod lacinia at quis risus sed vulputate.
32 changes: 32 additions & 0 deletions fs-router/nextjs-blog-starter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "fs-router-nextjs-blog-starter",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "waku dev",
"build": "waku build",
"start": "waku start",
"typegen": "waku router typegen"
},
"dependencies": {
"@fontsource/inter": "^5.2.11",
"classnames": "^2.5.1",
"date-fns": "^3.6.0",
"gray-matter": "^4.0.3",
"react": "~19.3.0",
"react-dom": "~19.3.0",
"react-server-dom-webpack": "~19.3.0",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"waku": "1.0.0-rc.2"
},
"devDependencies": {
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "^7.0.2"
}
}
8 changes: 8 additions & 0 deletions fs-router/nextjs-blog-starter/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Waku projects are ESM ("type": "module" in package.json), so this file uses
// `export default` where the Next.js original used `module.exports`.
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions fs-router/nextjs-blog-starter/src/components/alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Container from "@/components/container";
import { EXAMPLE_PATH } from "@/lib/constants";
import cn from "classnames";

type Props = {
preview?: boolean;
};

const Alert = ({ preview }: Props) => {
return (
<div
className={cn("border-b dark:bg-slate-800", {
"bg-neutral-800 border-neutral-800 text-white": preview,
"bg-neutral-50 border-neutral-200": !preview,
})}
>
<Container>
<div className="py-2 text-center text-sm">
{preview ? (
<>
This page is a preview.{" "}
<a
href="/api/exit-preview"
className="underline hover:text-teal-300 duration-200 transition-colors"
>
Click here
</a>{" "}
to exit preview mode.
</>
) : (
<>
The source code for this blog is{" "}
<a
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="underline hover:text-blue-600 duration-200 transition-colors"
>
available on GitHub
</a>
.
</>
)}
</div>
</Container>
</div>
);
};

export default Alert;
15 changes: 15 additions & 0 deletions fs-router/nextjs-blog-starter/src/components/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
type Props = {
name: string;
picture: string;
};

const Avatar = ({ name, picture }: Props) => {
return (
<div className="flex items-center">
<img src={picture} className="w-12 h-12 rounded-full mr-4" alt={name} />
<div className="text-xl font-bold">{name}</div>
</div>
);
};

export default Avatar;
9 changes: 9 additions & 0 deletions fs-router/nextjs-blog-starter/src/components/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type Props = {
children?: React.ReactNode;
};

const Container = ({ children }: Props) => {
return <div className="container mx-auto px-5">{children}</div>;
};

export default Container;
35 changes: 35 additions & 0 deletions fs-router/nextjs-blog-starter/src/components/cover-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import cn from "classnames";
import { Link } from "waku";

type Props = {
title: string;
src: string;
slug?: string;
};

const CoverImage = ({ title, src, slug }: Props) => {
const image = (
<img
src={src}
alt={`Cover Image for ${title}`}
className={cn("shadow-sm w-full", {
"hover:shadow-lg transition-shadow duration-200": slug,
})}
width={1300}
height={630}
/>
);
return (
<div className="sm:mx-0">
{slug ? (
<Link to={{ to: "/posts/[slug]", params: { slug } }} aria-label={title}>
{image}
</Link>
) : (
image
)}
</div>
);
};

export default CoverImage;
12 changes: 12 additions & 0 deletions fs-router/nextjs-blog-starter/src/components/date-formatter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { parseISO, format } from "date-fns";

type Props = {
dateString: string;
};

const DateFormatter = ({ dateString }: Props) => {
const date = parseISO(dateString);
return <time dateTime={dateString}>{format(date, "LLLL d, yyyy")}</time>;
};

export default DateFormatter;
32 changes: 32 additions & 0 deletions fs-router/nextjs-blog-starter/src/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Container from "@/components/container";
import { EXAMPLE_PATH } from "@/lib/constants";

export function Footer() {
return (
<footer className="bg-neutral-50 border-t border-neutral-200 dark:bg-slate-800">
<Container>
<div className="py-28 flex flex-col lg:flex-row items-center">
<h3 className="text-4xl lg:text-[2.5rem] font-bold tracking-tighter leading-tight text-center lg:text-left mb-10 lg:mb-0 lg:pr-4 lg:w-1/2">
Statically Generated with Next.js.
</h3>
<div className="flex flex-col lg:flex-row justify-center items-center lg:pl-4 lg:w-1/2">
<a
href="https://nextjs.org/docs/app/building-your-application/routing/layouts-and-templates"
className="mx-3 bg-black hover:bg-white hover:text-black border border-black text-white font-bold py-3 px-12 lg:px-8 duration-200 transition-colors mb-6 lg:mb-0"
>
Read Documentation
</a>
<a
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
className="mx-3 font-bold hover:underline"
>
View on GitHub
</a>
</div>
</div>
</Container>
</footer>
);
}

export default Footer;
14 changes: 14 additions & 0 deletions fs-router/nextjs-blog-starter/src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Link } from "waku";

const Header = () => {
return (
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight mb-20 mt-8 flex items-center">
<Link to="/" className="hover:underline">
Blog
</Link>
.
</h2>
);
};

export default Header;
Loading
Loading