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.
Both createLayout
and createPage
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';
const pages = createPages(async ({ createPage, createLayout }) => [
// 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.
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>
);
};
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.