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:
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:
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.
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:
init()Before the content loader exists. Read files your other hooks need.
configureLoader()Adds options to the content loader, like its plugins.
configure()After the loader is created. Start watchers and other long-running work.
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:
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.
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:
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.
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.
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.
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.
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.
unstable_serverEntry(entry) {
return {
...entry,
buildOptions: {
...entry.buildOptions,
ACME_ANALYTICS_ID: options.analyticsId,
},
};
},Last updated on