Skip to Content

Routes

Add pages and API routes from a plugin.

Pages

createView() adds a page. view returns its content, and type tells ui.renderPage which page it is. data is the page data, like its title in <head>:

type Types = {
  views: { "showcase.index": { items: string[] } }; 
};

export function showcasePlugin<C extends AppShape = AppShape>(options: {
  items: string[];
}): Plugin<C, Types> {
  return {
    name: "acme:showcase",
    routes({ createView }) {
      createView({
        path: "/showcase",
        data: { title: "Showcase" },
        view: () => ({
          type: "showcase.index", 
          props: { items: options.items }, 
          node: <p>{options.items.join(", ")}</p>, 
        }),
      });
    },
  };
}

A custom page at the same path takes priority over a plugin page.

Translated Pages

view is called once for each language, and receives its lang. The language prefix is added to the path for you.

acme/index.tsx
createView({
  path: "/showcase",
  view: (_params, { lang }) => ({
    type: "showcase.index",
    props: { items: options.items },
    node: <h1>{lang === "zh" ? "展示" : "Showcase"}</h1>, 
  }),
});

When the path differs per language, set lang to create the page in that language only.

Dynamic Routes

Add [name] to the path, and list each page in staticPaths. view receives the params.

acme/index.tsx
createView({
  path: "/showcase/[slug]", 
  staticPaths: options.items, 
  view: ({ slug }) => ({
    type: "showcase.item",
    props: { slug: slug as string },
    node: <h1>{slug}</h1>,
  }),
});

On-Demand Rendering

With render: "dynamic", view also receives the request. Return undefined for a 404:

acme/index.tsx
createView({
  path: "/account",
  render: "dynamic", 
  data: { title: "Account", noindex: true },
  view(_params, { request }) {
    const user = /session=([^;]+)/.exec(request.headers.get("cookie") ?? "")?.[1];
    if (!user) return;

    return {
      type: "acme.account",
      props: { user },
      node: <p>Signed in as {user}</p>,
    };
  },
});

API Routes

createApi() takes a handler per HTTP method, or ALL for all of them. Add render: "static" to generate a file from the GET handler at build time:

acme/index.tsx
routes({ createApi }) {
  createApi({
    path: "/api/ping",
    POST: async (request) => Response.json(await request.json()),
  });
},

A param must be a whole path segment: use /data/[id]/info.json, not /data/[id].json.

A route at the path of a page in a static source fails the build. A dynamic source can add pages after the build, so the route takes priority over its page.

Content Render Mode

A route that lists your pages depends on them. With a dynamic source, like Notion, that list changes after the build, so the route must render on demand too. this.getContentRender() returns the render mode of your content, which follows the mode of the site:

acme/index.tsx
routes({ createApi }) {
  createApi({
    path: "/pages.json",
    render: this.getContentRender(), 
    GET: async () => {
      const urls: string[] = [];
      for (const page of (await this.getLoader()).getPages()) urls.push(page.url);
      return Response.json(urls);
    },
  });
},

Pass a source key to get the render mode of that source only, like this.getContentRender("blog").

A single route rendered on demand means the site needs a server. Add one only when your plugin needs it.

Derived Routes

deriveRoutes runs once per language, after the other routes are created. It receives the same functions as the routes hook, so use it for routes generated from your pages. This plugin adds a print version of each content page:

acme/index.tsx
async deriveRoutes({ createView }, { lang }) { 
  const loader = await this.getLoader();

  createView({
    path: "/print/[...slugs]",
    render: this.getContentRender(),
    data: { noindex: true },
    staticPaths: loader.getPages(lang).map((page) => page.slugs),
    view: async ({ slugs }) => {
      const page = loader.getPage(slugs as string[], lang);
      if (!page) return;

      const body = await this.getPageBody(page); 
      return {
        type: "print",
        props: { page },
        node: body?.node,
      };
    },
  });
},

path has no language prefix. To type the props, use the page type of the site:

Plugin<C, { views: { print: { page: C["page"] } } }>;

Reading Existing Routes

The routes argument has the custom pages and plugin pages of that language. This plugin adds a JSON file with the params of each page, at /meta<path>. For a prerendered route, it creates one file per entry in route.pages. For a route rendered on demand, it creates one API route:

acme/index.tsx
deriveRoutes({ createApi }, { routes }) {
  for (const route of routes) {
    if (route.render === "dynamic") {
      createApi({
        path: `/meta${route.path}`,
        GET: (_request, { params }) => Response.json(params),
      });
      continue;
    }

    for (const { path, params } of route.pages)
      createApi({
        path: `/meta${path}`,
        render: "static",
        GET: () => Response.json(params),
      });
  }
},

Last updated on

On this page