v0.19
January 16, 2024

Introducing createPages

Learn about the new API for creating layouts and pages programmatically.

by Sophia Andren
technical director of candycode

Until now, Waku has been helpful as a reference implementation for library authors curious about React server components. Today’s v0.19 release marks the shift towards building Waku into a production-ready React framework. It’s designed for startups and agencies seeking a lightweight alternative for small to medium-sized React projects. We want to make React development fun!

The first building block is the new createPages function, a low-level routing API which allows Waku developers to create layouts and pages programmatically. Both static prerendering (SSG) and server-side rendering (SSR) options are available and are selected at the layout and page level.

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.

Let’s explore the details of the new Waku API.

Low-level routing API

The entry point for routing in Waku projects is ./src/entries.tsx. Export the createPages function to create your layouts and pages programmatically.

Both createLayout and createPage accept a configuration object to specify the route path, React component, and render method ('static' for SSG or 'dynamic' for SSR). Layout components must accept a children prop.

// ./src/entries.tsx
import { createPages } from 'waku';

import { RootLayout } from './templates/root-layout';
import { HomePage } from './templates/home-page';

export default createPages(async ({ createPage, createLayout }) => {
  // Create root layout
  createLayout({
    render: 'static',
    path: '/',
    component: RootLayout,
  });

  // Create home page
  createPage({
    render: 'dynamic',
    path: '/',
    component: HomePage,
  });
});

Pages

Single routes

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,
  });
});

Segment routes

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.

// ./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();

  createPage({
    render: 'static',
    path: '/blog/[slug]',
    staticPaths: blogPaths,
    component: BlogArticlePage,
  });
});

Nested segment routes

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 routes

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

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.

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>;
};

Other layouts

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>
  );
};

Looking forward

Now that a low-level API for creating layouts and pages is complete, we’re building a lightweight file-based “pages router” to further accelerate the work of Waku developers.

Stay tuned for this and other features in the upcoming v0.20 release! In the meantime, please give Waku a try on non-production projects and join our friendly GitHub discussions or Discord server to participate in the Waku community.