Skip to content

Latest commit

 

History

History
399 lines (306 loc) · 10.6 KB

create-pages.mdx

File metadata and controls

399 lines (306 loc) · 10.6 KB
slug title description
create-pages
createPages
The low-level routing API.

Routing (low-level 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;

Router Paths Type Safety

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.

PageProps

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

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.

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

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

Root component

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.

Note About <head>

If you only need to customize <head>, you can rely on React's Support for Document Metadata and do not need to use createRoot.

Client entry point

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.