Skip to Content

Custom Source

Load pages from anywhere.

A custom source lists your pages, and a plugin renders them.

Setup

List Pages

A source is an object with files. The path of a file sets its URL, and its data becomes page.data. When files is a function, the source is dynamic: by default, pages are read on the server and render on demand, so the site needs a server.

press.config.tsx
const data = { title: "Home", load: async () => "Hello", plainText: () => "Hello" };

export default createPress({
  files: [{ type: "page", path: "index.mdx", data }],
}).plugins(acmePlugin());

Render Pages

A plugin renders the pages through content hooks, like getPageBody. Return undefined for pages it doesn't handle.

AppShape and Plugin come from fumapress.

acme/index.tsx
interface AcmePageData {
  load: () => Promise<string>;
  plainText: () => string;
}

function isAcmePage(data: object): data is AcmePageData {
  return "load" in data && "plainText" in data;
}

export default function acmePlugin<C extends AppShape = AppShape>(): Plugin<C> {
  return {
    name: "acme",
    async getPageBody(page) {
      if (!isAcmePage(page.data)) return;
      return { node: <div>{await page.data.load()}</div> };
    },
    getPageText(page) {
      if (isAcmePage(page.data)) return page.data.plainText();
    },
  };
}

To make your plugin work on every framework Fumapress runs on, see Publishing a Package.

Content Hooks

Only getPageBody is required. Add other hooks for the features you need:

HookNeeded for
getPageTocthe table of contents
getPageTextllms.txt, <page>.md, AI chat, MCP
getPageStructuredDatafull text search
getPageCreatedAtblog, RSS
getPageLastModifiedthe page, sitemap, RSS

Without structured data, search only matches the title of a page. To search its text, return the headings and the text under each:

acme/index.tsx
getPageStructuredData(page) {
  return {
    headings: [{ id: "intro", content: "Intro" }],
    // `heading` is the `id` of the heading above the text
    contents: [{ heading: "intro", content: "The text under the heading." }],
  };
},

Refreshing Content

The source is read once, so new content doesn't appear on its own. Choose when to read it again:

press.config.tsx
createPress(source, {
  alwaysRevalidate: true, 
});

Reading on every request works for low traffic. On a busier site, call the webhook when your content changes.

Last updated on

On this page