Back to Home

Fumapress v2

Type-safety, Astro support, and smarter rendering mode.

release

Fumapress v2 is out. This release brings:

  • Astro support: use the same plugins, layouts and search on Astro.
  • Smarter render mode: the render mode of a route now matches the content it references.
  • Type-safety: your config is typed from your content sources and plugins.

This is a major release, so existing sites need to rewrite their config. See Upgrading.

Astro

Until now, Fumapress only supported Waku.js. @fumapress/astro adds support for Astro:

npm create fumapress -- --framework astro

Astro still handles routing, builds and deployment. Your pages are stored in content collections, and Fumapress uses your output, adapter, site and base settings:

import { defineConfig } from "astro/config";
import press from "@fumapress/astro";

export default defineConfig({
  integrations: [press({ collections: { docs: { dir: "content" } } })],
});

Custom pages are .astro files with React components as islands, and a custom layout is an .astro file that wraps a React island. @fumapress/astro needs Astro 7.4 and Node.js 22.23.4 or later.

Smarter Render Mode

v1 had a mode option that set the render mode of the whole site. With a Notion source, mode: "dynamic" also rendered your Markdown pages on demand, even though they only change when you deploy.

In v2, the render mode of a route matches the content it references by default. Pages of a dynamic source render on demand, pages of a static source are prerendered, and one site can have both:

press.config.tsx
export default createPress({
  docs: docs.toFumadocsSource({ baseDir: "docs" }),
  guides: notion.dynamicSource({ baseDir: "guides" }),
}).plugins(notionPlugin({ notion }));

mode: "dynamic"

  • /docs/installon demand
  • /docs/install.mdon demand
  • /guides/setupon demand
  • /guides/setup.mdon demand
  • /api/searchon demand

1/3In v1, one option set the render mode of the whole site. With a Notion source, your Markdown pages also rendered on demand.

The mode option of createPress() can still force a render mode: force-dynamic renders your content on demand, and force-static prerenders a dynamic source at build time.

Type-Safety

createPress()

defineConfig() is replaced by createPress(). You pass your content first, then plugins, then options:

press.config.tsx
import { createPress } from "fumapress";
import { blogPlugin } from "@fumapress/blog";
import { defineDocs } from "fumadocs-mdx/macro";

const docs = defineDocs({ dir: "content/docs" });
const blog = defineDocs({ dir: "content/blog" });

export const app = createPress({
  docs: docs.toFumadocsSource(),
  blog: blog.toFumadocsSource(),
}).plugins(blogPlugin({ source: "blog" }));

export default app.config({
  site: { name: "My Docs" },
});

Each stage is typed from the ones before it, so your options have types for your source keys and for everything your plugins add. In other files, typeof app gives you the same types. .config() is optional when the defaults are enough.

JSX Layouts

The layout factories are removed. A layout is now a Fumadocs UI component that you render in ui.renderPage. This function is called for every page, and props.type tells you the kind of page:

press.config.tsx
import { DocsLayout } from "fumadocs-ui/layouts/notebook";
import { DocsBody, DocsPage, DocsTitle } from "fumadocs-ui/layouts/notebook/page";

export default app.config({
  ui: {
    renderPage(props) {
      const { renderDefault } = props;
      if (props.type !== "docs") return renderDefault();

      return (
        <DocsLayout {...props.layoutProps}>
          <DocsPage toc={props.toc}>
            <DocsTitle>{props.pageData.title}</DocsTitle>
            <DocsBody>{props.body}</DocsBody>
          </DocsPage>
        </DocsLayout>
      );
    },
  },
});

The options of each factory are now props of these components. To change the source of a component, copy it into your project with the Fumadocs CLI:

npx @fumadocs/cli add banner

On a Fumapress site, add only installs UI components. It rejects everything else and prints the reason.

Plugins

Plugins now create routes with createApi() and createView() in the routes hook, and ui.renderPage receives their pages with typed props:

import type { AppShape, Plugin } from "fumapress";

type Types = {
  views: { status: { ok: boolean } };
};

export function statusPlugin<C extends AppShape = AppShape>(): Plugin<C, Types> {
  return {
    name: "acme:status",
    routes({ createApi, createView }) {
      createApi({
        path: "/api/status",
        GET: () => Response.json({ ok: true }),
      });
      createView({
        path: "/status",
        view: () => ({ type: "status", props: { ok: true }, node: <p>All good.</p> }),
      });
    },
  };
}

Fumapress checks the routes of all plugins. When two plugins use the same path, it throws an error with both names:

[Fumapress] "acme:status" and "acme:health" both take the path "/status": a path has one route.

A route at the path of a page from a static source also fails the build. A dynamic source can add pages at any time, so the route takes priority over its pages.

Integrations moved out of fumapress. The OpenAPI, AsyncAPI, GraphQL, Notion, Sanity and Obsidian plugins are now exported by the Fumadocs packages, like fumadocs-openapi/press. The blog is now a separate package, @fumapress/blog.

Client Navigation

On Waku.js, every page used to include the whole page tree. The page tree is now a separate file that the browser fetches once per language. A site with 2000 pages used to crash the build. It now builds, and a client navigation on that site transfers 3.5 KB.

Client state no longer carries over to the next page. Before, a selected code tab could leave a tab group on the next page with no content.

Security Fixes

  • URLs like //docs and /%64ocs now redirect to /docs, so a prefix check in middleware also covers them.
  • The self-hosted image optimizer now resolves relative images from site.baseUrl instead of the Host header, and limits its memory use.
  • /api/ai now rejects system messages and message parts that the UI never sends.

Upgrading

v2 is fumapress 2.0.0. It needs fumadocs-core and the Base UI build of fumadocs-ui 16.18.2 or later, and fumadocs-mdx 15.5.1 or later:

npm i fumapress@latest
npm i fumadocs-core@latest fumadocs-mdx@latest
npm i fumadocs-ui@npm:@fumadocs/base-ui@latest

Then follow the upgrade guide. If you didn't customize your site, rewriting the config is enough. Otherwise, the guide has a section for each feature, so you only need to read the ones your site uses.

New to Fumapress? Each framework has its own docs: