slug | title | description |
---|---|---|
create-pages |
createPages |
The low-level routing API. |
The entry point for programmatic routing in Waku projects is ./src/entries.tsx
. Export the createPages
function to create your layouts and pages.
createLayout
, createPage
, and createRoot
accept a configuration object to specify the route path, React component, and render method. Waku currently supports two options: 'static'
for static prerendering (SSG) or 'dynamic'
for server-side rendering (SSR).
For example, you can statically prerender a global header and footer in the root layout at build time, but dynamically render the rest of a home page at request time for personalized user experiences.
// ./src/entries.tsx
import { createPages } from 'waku';
import { RootLayout } from './templates/root-layout';
import { HomePage } from './templates/home-page';
import { Root } from './components/root';
const pages = createPages(async ({ createPage, createLayout, createRoot }) => [
// Create root component
// not required, but supported for customizing `<html>`, `<head>`, and `<body>` tags
createRoot({
render: 'static',
component: Root,
}),
// Create root layout
createLayout({
render: 'static',
path: '/',
component: RootLayout,
}),
// Create home page
createPage({
render: 'dynamic',
path: '/',
component: HomePage,
}),
]);
export default pages;
Waku provides inference for the router paths when created pages are returned from the callback passed into createPages
. The following example shows how a minimal example for how to setup the router paths type safety.
// ./src/entries.tsx
import { createPages } from 'waku';
import type { PathsForPages } from 'waku/router';
import { HomePage } from './templates/home-page';
const pages = createPages(async ({ createPage, createLayout }) => [
// Create root layout
createLayout({
render: 'static',
path: '/',
component: RootLayout,
}),
// Create home page
createPage({
render: 'dynamic',
path: '/',
component: HomePage,
}),
]);
declare module 'waku/router' {
interface RouteConfig {
paths: PathsForPages<typeof pages>;
}
interface CreatePagesConfig {
pages: typeof pages;
}
}
export default pages;
Once this is done, any <Link />
component or hook from waku/router
that uses paths in your app will use this type. In this case, the one valid use would be <Link to="/" />
, but as you add more pages to the router, this type will grow to include them.
Note: The file-based router paths will be supported in the future with some form of code-generation to get the types from your local page files.
The above snippet will also provide types for the PageProps
type. This will give type safety for the slugs in your pages.
// ./src/pages/about.tsx
import type { PageProps } from 'waku/router';
// PageProps<'/about/[foo]'> => { path: `/about/${string}`; foo: string; query: string; }
export default function AboutPage({ path }: PageProps<'/about/[foo]'>) {
return <>{/* ...*/}</>;
}
Pages can be rendered as a single route (e.g., /about
).
// ./src/entries.tsx
import { createPages } from 'waku';
import { AboutPage } from './templates/about-page';
import { BlogIndexPage } from './templates/blog-index-page';
export default createPages(async ({ createPage }) => [
// Create about page
createPage({
render: 'static',
path: '/about',
component: AboutPage,
}),
// Create blog index page
createPage({
render: 'static',
path: '/blog',
component: BlogIndexPage,
}),
]);
Pages can also render a segment route (e.g., /blog/[slug]
). The rendered React component automatically receives a prop named by the segment (e.g, slug
) with the value of the rendered segment (e.g., 'introducing-waku'
). If statically prerendering a segment route at build time, a staticPaths
array must also be provided.
Note: Slugs will be sanitized to remove .
and replace spaces with -
.
// ./src/entries.tsx
import { createPages } from 'waku';
import { BlogArticlePage } from './templates/blog-article-page';
import { ProductCategoryPage } from './templates/product-category-page';
export default createPages(async ({ createPage }) => [
// Create blog article pages
// `<BlogArticlePage>` receives `slug` prop
createPage({
render: 'static',
path: '/blog/[slug]',
staticPaths: ['introducing-waku', 'introducing-create-pages'],
component: BlogArticlePage,
}),
// Create product category pages
// `<ProductCategoryPage>` receives `category` prop
createPage({
render: 'dynamic',
path: '/shop/[category]',
component: ProductCategoryPage,
}),
]);
Static paths (or other values) could also be generated programmatically.
// ./src/entries.tsx
import { createPages } from 'waku';
import { getBlogPaths } from './lib/get-blog-paths';
import { BlogArticlePage } from './templates/blog-article-page';
export default createPages(async ({ createPage }) => {
const blogPaths = await getBlogPaths();
return [
createPage({
render: 'static',
path: '/blog/[slug]',
staticPaths: blogPaths,
component: BlogArticlePage,
}),
];
});
Routes can contain multiple segments (e.g., /shop/[category]/[product]
).
// ./src/entries.tsx
import { createPages } from 'waku';
import { ProductDetailPage } from './templates/product-detail-page';
export default createPages(async ({ createPage }) => [
// Create product detail pages
// `<ProductDetailPage>` receives `category` and `product` props
createPage({
render: 'dynamic',
path: '/shop/[category]/[product]',
component: ProductDetailPage,
}),
]);
For static prerendering of nested segment routes, the staticPaths
array is instead composed of ordered arrays.
// ./src/entries.tsx
import { createPages } from 'waku';
import { ProductDetailPage } from './templates/product-detail-page';
export default createPages(async ({ createPage }) => [
// Create product detail pages
// `<ProductDetailPage>` receives `category` and `product` props
createPage({
render: 'static',
path: '/shop/[category]/[product]',
staticPaths: [
['some-category', 'some-product'],
['some-category', 'another-product'],
],
component: ProductDetailPage,
}),
]);
Catch-all or "wildcard" routes (e.g., /app/[...catchAll]
) have indefinite segments. Wildcard routes receive a prop with segment values as an ordered array.
For example, the /app/profile/settings
route would receive a catchAll
prop with the value ['profile', 'settings']
. These values can then be used to determine what to render in the component.
// ./src/entries.tsx
import { createPages } from 'waku';
import { DashboardPage } from './templates/dashboard-page';
export default createPages(async ({ createPage }) => [
// Create account dashboard
// `<DashboardPage>` receives `catchAll` prop (string[])
createPage({
render: 'dynamic',
path: '/app/[...catchAll]',
component: DashboardPage,
});
]);
Layouts wrap an entire route and its descendents. They must accept a children
prop of type ReactNode
. While not required, you will typically want at least a root layout.
The root layout rendered at path: '/'
is especially useful. It can be used for setting global styles, global metadata, global providers, global data, and global components, such as a header and footer.
// ./src/entries.tsx
import { createPages } from 'waku';
import { RootLayout } from './templates/root-layout';
export default createPages(async ({ createLayout }) => [
// Add a global header and footer
createLayout({
render: 'static',
path: '/',
component: RootLayout,
}),
]);
// ./src/templates/root-layout.tsx
import '../styles.css';
import { Providers } from '../components/providers';
import { Header } from '../components/header';
import { Footer } from '../components/footer';
export const RootLayout = async ({ children }) => {
return (
<Providers>
<link rel="icon" type="image/png" href="/images/favicon.png" />
<meta property="og:image" content="/images/opengraph.png" />
<Header />
<main>{children}</main>
<Footer />
</Providers>
);
};
// ./src/components/providers.tsx
'use client';
import { createStore, Provider } from 'jotai';
const store = createStore();
export const Providers = ({ children }) => {
return <Provider store={store}>{children}</Provider>;
};
Layouts are also helpful further down the tree. For example, you could add a layout at path: '/blog'
to add a sidebar to both the blog index and all blog article pages.
// ./src/entries.tsx
import { createPages } from 'waku';
import { BlogLayout } from './templates/blog-layout';
export default createPages(async ({ createLayout }) => [
// Add a sidebar to the blog index and blog article pages
createLayout({
render: 'static',
path: '/blog',
component: BlogLayout,
}),
]);
// ./src/templates/blog-layout.tsx
import { Sidebar } from '../components/sidebar';
export const BlogLayout = async ({ children }) => {
return (
<div className="flex">
<div>{children}</div>
<Sidebar />
</div>
);
};
Root component is a special component that is rendered at the root of html document. It is useful for customizing <html>
, <head>
, and <body>
tags.
// ./src/components/root.tsx
export const Root = ({ children }) => {
return (
<html lang="en">
<head></head>
<body>{children}</body>
</html>
);
};
Add this with createRoot
function inside createPages
.
If you only need to customize <head>
, you can rely on React's Support for Document Metadata and do not need to use createRoot
.
The file ./src/entries.tsx
is the entry point for the server.
For the client, the entry point file is ./src/main.tsx
.
The default client entry file content is the following.
import { Component, StrictMode } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import { Router } from 'waku/router/client';
const rootElement = (
<StrictMode>
<Router />
</StrictMode>
);
if (globalThis.__WAKU_HYDRATE__) {
hydrateRoot(document, rootElement);
} else {
createRoot(document).render(rootElement);
}
You can omit ./src/main.tsx
unless you need to modify it.