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

middleware runs for the routes rendered on demand. Prerendered pages and the files of public/ never reach it.

A build without an adapter warns that the middleware of your plugins never runs.

next(path) can render pages from dynamic sources, plugin views and API routes. For prerendered files and your .astro pages, it responds with an empty 404.

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

The build hook only receives the pages that Astro prerenders. See Post-Build Plugins.

Last updated on

On this page