Skip to Content

Writing Plugins

Add routes, UI and other features with your own plugin.

A plugin is an object with hooks. This one adds a link to the navbar:

press.config.tsx
createPress(content).plugins({
  name: "site:blog-link",
  layoutProps({ lang }) {
    return {
      links: [{ url: this.localizePath(lang, "/blog"), text: "Blog" }], 
    };
  },
});

layoutProps is a hook, and Fumapress calls it for each page it renders. this in a hook is the press context, the same object getPressContext() returns on custom pages.

Hooks

Sharing Plugins

In these samples, AppShape and Plugin come from @fumapress/astro/config.

Reusable Plugins

To use a plugin on more than one site, export a function that returns it. The type parameter C gives the hooks the types of the site that uses the plugin. This plugin adds a page, an API route and a response header:

type Types = {
  views: { "status.page": { ok: boolean } }; 
};

export function statusPlugin<C extends AppShape = AppShape>(): Plugin<C, Types> {
  return {
    name: "acme:status",
    routes({ createApi, createView }) {
      createView({
        path: "/status",
        render: "dynamic",
        view: () => ({
          type: "status.page", 
          props: { ok: true },
          node: <p>OK</p>,
        }),
      });
      createApi({ path: "/status.json", GET: () => Response.json({ ok: true }) });
    },
    async middleware(request, next) {
      const response = await next();
      const out = new Response(response.body, response);
      out.headers.set("x-powered-by", "acme");
      return out;
    },
  };
}

Plugin Types

The second type parameter of Plugin lists what your plugin adds, so that ui.renderPage and other plugins have its types. Use only the keys you need:

acme/index.tsx
type AcmeTypes = {
  // plugin pages: `props.type === "showcase.index"`, with `props.items`
  views: { "showcase.index": { items: ShowcaseItem[] } };
  // content pages it renders: `props.type === "reviews"`, with `props.stars`
  pages: { reviews: { stars: number } };
  // the names of its slot items: `props.slots["acme:share"]`
  slots: "acme:share" | "acme:rating";
};

Publishing a Package

To support every framework Fumapress runs on, build on the plugin API of Fumadocs, and make the plugin the default export of a /press entry. That API has the content hooks and createApi(), with no UI hooks.

acme/press.ts
import type { Plugin } from "fumadocs-core/framework/plugin";

export default function acmePlugin(): Plugin {
  return {
    name: "acme",
    routes({ createApi }) {
      createApi({ path: "/api/acme", GET: () => Response.json({ ok: true }) });
    },
  };
}

fumadocs-openapi/press is a complete example.

Sharing Data

To give custom pages and other plugins data of your plugin, export a function that takes the press context. Store the data by context, since one process can run more than one site:

acme/index.ts
const statuses = new WeakMap<Context, Promise<Status>>();

export function statusPlugin<C extends AppShape = AppShape>(): Plugin<C> {
  return {
    name: "acme:status",
    configure() {
      statuses.set(this, fetchStatus());
    },
  };
}

export function getStatus(ctx: Context): Promise<Status> | undefined {
  return statuses.get(ctx);
}

getBlogPosts() of @fumapress/blog works this way.

Custom Search Engine

A search engine is a plugin that returns its dialog from providerProps, and adds its own routes. A site using it sets search: false in recommendedPreset(). See Custom Search Dialog for the parts.

import { AcmeSearchDialog } from "./client";

export function acmeSearchPlugin<C extends AppShape = AppShape>(options: {
  index: string;
}): Plugin<C> {
  return {
    name: "acme:search",
    providerProps({ lang }) {
      const api = this.withBase("/api/acme-search");
      const options = { api, locale: lang };
      return { search: { SearchDialog: AcmeSearchDialog, options } };
    },
    routes({ createApi }) {
      createApi({
        path: "/api/acme-search",
        GET: (request) => {
          const { search } = new URL(request.url);
          return fetch(`https://search.acme.dev/${options.index}${search}`);
        },
      });
    },
  };
}

Last updated on

On this page