Skip to Content

UI

Add UI and head tags to pages from a plugin.

Slots

This plugin adds a share button below the title of every content page:

import { ShareButton } from "./share-button";

type Types = { slots: "acme:share" };

export function sharePlugin<C extends AppShape = AppShape>(): Plugin<C, Types> {
  return {
    name: "acme:share",
    slots({ page }) {
      if (!page) return; 
      return [
        {
          name: "acme:share", 
          region: "actions", 
          component: ShareButton,
          props: { url: this.absoluteUrl(page.url) },
        },
      ];
    },
  };
}

slots runs with page for the items of a content page, and without page for the items of every page. Each item has a name, unique on the site, written as <plugin>:<item>. List the names in the slots of your plugin types. Names starting with core: belong to Fumapress.

The built-in UI places an item in its region. A custom layout places it by name, with props.slots["acme:share"].

To translate the label, add your strings with the translations field. See the Fumadocs Translations API.

Regions

The regions marked for content pages take items from the call with page, the others from the call without. An item in a region of the other call is an error:

layout-wrapper

Around every page, inside the root provider. For providers.

banner

Above every page, custom pages and the 404 page included.

sidebar-banner, sidebar-footer

Above and below the sidebar.

actions

Content pages: the row of buttons below the title, with Copy Markdown.

body-wrapper

Content pages: around the content.

footer

Content pages: after the content.

toc-header, toc-footer

Content pages: above and below the table of contents.

ui.renderPage receives the items of every region but the wrappers and banner as props.slots. A layout without a region doesn't render its items, like the Home Layout of plugin pages, which has none of them.

Wrapper Slots

A wrapper receives what it wraps as children:

slots({ page }) {
  if (page) return [{ name: "acme:prose", region: "body-wrapper", component: Prose }];
  return [{ name: "acme:theme", region: "layout-wrapper", component: ThemeProvider, order: -10 }];
},

Two wrappers in one region are nested by order, with the lowest on the outside. Context providers use a negative order.

Layout Props

layoutProps adds to the layout props of every layout, like links, sidebar and tabs. It runs for each page, with page on content pages. Its links come after the links of the site, and for other props the values of the site take priority.

acme/index.tsx
layoutProps({ lang }) {
  return {
    links: [{ url: this.localizePath(lang, "/showcase"), text: "Showcase" }],
  };
},

providerProps does the same for the root provider.

Head Tags

head runs once for the whole site without page or route, and once for each page. A tag from the head option of the site replaces a plugin tag with the same key, like a meta tag of the same name.

acme/index.tsx
head({ page, route }) {
  if (page) return [{ tag: "meta", attrs: { name: "author", content: "acme" } }];
  if (route) return;
  return [{ tag: "meta", attrs: { name: "generator", content: "acme" } }];
},

Rendering Content Pages

To show the pages of one source differently, write getPageView. This plugin gives each page of the reviews source a star rating and its own UI:

type Types = {
  pages: { reviews: { stars: number } }; 
};

export function reviewsPlugin<C extends AppShape = AppShape>(): Plugin<C, Types> {
  return {
    name: "acme:reviews",
    getPageView(page, body) {
      if (page.type !== "reviews") return; 
      const stars = Number(Reflect.get(page.data, "stars") ?? 0);

      return {
        props: { stars }, 
        node: (
          <article>
            <p>{"★".repeat(stars)}</p>
            {body?.node}
          </article>
        ),
      };
    },
  };
}
  • pages maps a source key to the props that the plugin adds.
  • page.type is the source of the page. Return undefined for pages of other sources.
  • body.node is the content of the page.
  • props are passed to ui.renderPage as they are, and node becomes props.children.

Without a ui.renderPage, node shows in the Home Layout.

Page Rendering Order

Each step receives the result of the one above. Write the hook of the step you want to change.

  1. getPageBody(page){ node }

    The content. The content source provides it, so only write this hook for a custom source.

  2. body-wrapper slotsprops.body

    Wrap the content of every source.

  3. getPageView(page, body)props.children

    Adds props and UI to the pages of one source.

  4. ui.renderPage(props)the page

    Set by the site. It renders everything above in a layout.

Page Data

transformPage changes the page data of every page: content pages, plugin pages and custom pages. A layout reads it from props.pageData, like the title or the width, and <head> gets its title and description. Return only the fields you change. Two plugins changing the same field is an error.

acme/index.tsx
transformPage(data) {
  return { full: data.url.startsWith("/reference/") };
},

It receives page on content pages, and route with path on other pages. Read the page data of any page with this.getPageData().

Last updated on

On this page