Skip to Content

Middleware and Lifecycle

Handle requests, and run code at startup and after the build.

Middleware

middleware runs first on every request your server receives. Call next() to continue, or return a Response to stop. This plugin protects a prefix with a token:

acme/index.tsx
async middleware(request, next) {
  const { pathname } = new URL(request.url);

  if (
    pathname.startsWith(this.withBase(options.prefix)) &&
    request.headers.get("authorization") !== `Bearer ${options.token}`
  )
    return new Response("Unauthorized", { status: 401 }); 

  return next(); 
},

Changing the Response

Wait for next(), then return a copy with your changes:

acme/index.tsx
async middleware(_request, next) {
  const response = await next();
  const out = new Response(response.body, response);
  out.headers.set("x-powered-by", "acme");
  return out;
},

Rewrites

next(path) renders another path instead, with the URL unchanged. The path doesn't include the base path.

acme/index.tsx
middleware(request, next) {
  const { pathname } = new URL(request.url);

  return pathname === this.withBase("/latest") ? next("/v2") : next();
},

Requests That Reach Middleware

On a Node.js server, middleware also runs for prerendered pages and the files of public/. Vercel, Netlify and Cloudflare serve those files without your server, so only the routes rendered on demand reach it there.

A plugin with middleware in .plugins() makes your site need a server. A build without one warns that the middleware never runs.

See Authentication for the routes that a check should cover.

Lifecycle Hooks

These hooks run in this order, once per site:

  1. init()

    Before the content loader exists. Read files your other hooks need.

  2. configureLoader()

    Adds options to the content loader, like its plugins.

  3. configure()

    After the loader is created. Start watchers and other long-running work.

  4. build(output)
    Runs after a build, not on dev or production servers.

Reading Files at Startup

The Mintlify plugin reads its docs.json like this, so that other hooks have it:

acme/index.ts
import { readFile } from "node:fs/promises";

export function acmePlugin<C extends AppShape = AppShape>(): Plugin<C> {
  let config: { title: string };

  return {
    name: "acme",
    async init() {
      config = JSON.parse(await readFile("acme.json", "utf8"));
    },
    layoutProps: () => ({ nav: { title: config.title } }),
  };
}

Loader Options

configureLoader adds to the Fumadocs loader options. Loader plugins from all your plugins are combined. Any other option can be set by one plugin only, and the options in createPress() take priority.

acme/index.tsx
import { lucideIconsPlugin } from "fumadocs-core/source/plugins/lucide-icons";

configureLoader() {
  return { plugins: [lucideIconsPlugin()] };
},

Watching Files

this.signal aborts when press.config.tsx reloads, and this.invalidateLoader(source) reloads a dynamic source. This plugin uses them to watch a folder:

acme/notes-plugin.ts
import { watch } from "node:fs";

export function notesPlugin<C extends AppShape = AppShape>(options: {
  dir: string;
  source: C["source"];
}): Plugin<C> {
  return {
    name: "acme:notes",
    configure() {
      if (!import.meta.env.DEV) return;

      watch(options.dir, { recursive: true, signal: this.signal }, () =>
        this.invalidateLoader(options.source),
      );
    },
  };
}

Post-Build Hook

build receives the links of prerendered pages, fetch to request a built page, and emitFile to write to the output. Throw to fail the build.

acme/index.tsx
async build({ links, emitFile }) {
  await emitFile("links.json", JSON.stringify(links));
},

Waku APIs

WakuPlugin adds hooks under waku. A plugin with these hooks only works on Waku sites, so only use them when the other hooks aren't enough.

acme/index.tsx
import type { WakuAppShape, WakuPlugin } from "fumapress";

export function acmePlugin<C extends WakuAppShape = WakuAppShape>(): WakuPlugin<C> {
  return {
    name: "acme",
    waku: {
      // the hooks below
    },
  };
}

Pages and Layouts

createPages receives Waku's functions, with lang. It runs once, so loop over the languages. Layouts must be in a route group, like /(showcase), and custom pages of the site take priority.

acme/index.tsx
createPages({ createLayout, createPage }) {
  for (const lang of this.languages) {
    createLayout({
      path: "/(showcase)",
      lang,
      render: "static",
      component: ({ children }: { children: ReactNode }) => <section>{children}</section>,
    });
    createPage({
      path: "/(showcase)/showcase/[item]",
      lang,
      render: "static",
      staticPaths: Object.keys(options.items),
      component: ({ item }: { item: string }) => <h1>{options.items[item]?.title}</h1>,
    });
  }
},

For API routes, use createApi.

Hono Context

honoMiddleware gives you c.env and c.executionCtx. It doesn't run for static files served by the adapter. For anything else, use middleware.

acme/index.tsx
honoMiddleware() {
  return async (c, next) => {
    await next();
    c.header("x-analytics-id", options.analyticsId);
  };
},

Server Entry

unstable_serverEntry changes the server entry of the adapter, like its buildOptions. Waku marks the entry type unstable.

acme/index.tsx
unstable_serverEntry(entry) {
  return {
    ...entry,
    buildOptions: {
      ...entry.buildOptions,
      ACME_ANALYTICS_ID: options.analyticsId,
    },
  };
},

Last updated on

On this page