Blog
Build a blog system with Fumapress.
Installation
This plugin is included in fumapress.
Fumadocs MDX
If you use Fumadocs MDX, create a new collection for blog and register it as a content source:
import { defineConfig } from "fumapress";
import { blogMetaSchema, blogPageSchema } from "fumapress/adapters/mdx/schema";
import { blogPlugin } from "fumapress/plugins/blog";
import { defineDocs } from "fumadocs-mdx/macro";
const docs = defineDocs({
dir: "content/docs",
docs: { async: true },
});
const blog = defineDocs({
dir: "content/blog",
docs: {
async: true,
schema: blogPageSchema,
lastModified: true,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: blogMetaSchema,
},
});
export default defineConfig({
content: {
docs: docs.toFumadocsSource(),
blog: blog.toFumadocsSource({
baseDir: "blog",
}),
},
})
.plugins(blogPlugin());The plugin overrides the renderer for blog pages, by default, all pages from the blog content source are rendered as blog posts.
You can also define a isBlog() option to determine whether the page is a blog post
import { defineConfig } from "fumapress";
import { blogPlugin } from "fumapress/plugins/blog";
export default defineConfig({
// ...
}).plugins(
blogPlugin({
isBlog: (page) => page.type === "blog",
}),
);The integration is now configured, you can write blog posts under the content/blog directory (as specified in defineDocs()):
---
title: My Blog Post
description: Hello World!!!
date: 2026-09-06
tags: [my-tag]
---
## Introduction
Welcome to my first blog post.blogPageSchema supports the following frontmatter fields:
| Field | Description |
|---|---|
title | The post title (required) |
description | Shown below the title and on post cards |
date | Publish date, posts are sorted by it |
tags | Post tags, each tag gets a page under /blog/tags |
authors | Author ids, see the authors option |
image | Cover image URL, shown on post cards |
Cover images and author avatars render through the Image component. Paths under your own site work as they are, remote URLs need their hostname allowed by the image plugin:
import { imagePlugin } from "fumapress/plugins/image/self-hosted";
export default defineConfig({
// ...
}).plugins(imagePlugin({ allowedHosts: ["avatars.githubusercontent.com"] }));Watch for redirects
A redirect that leaves the origin is checked again, so allow the final host too.
github.com/<user>.png, for example, redirects to avatars.githubusercontent.com.
Other Content Sources
If you don't use Fumadocs MDX, you can keep the your content config, but make sure to define the isBlog() option like:
import { defineConfig } from "fumapress";
import { blogPlugin } from "fumapress/plugins/blog";
import { myCms } from "./my-cms";
export default defineConfig({
content: myCms.toStaticSource(),
}).plugins(
blogPlugin({
// since it is not using multi-source, `page.type` will be `undefined`.
// you need to detect it from other properties, like page path.
isBlog: (page) => page.path.startsWith("blog/"),
}),
);Post data is resolved through adapter hooks: core:get-creation-date for the
date, blog:get-tags, blog:get-authors and blog:get-image for the other fields. The Fumadocs
MDX adapter reads them from frontmatter, for other sources, implement the hooks in your adapter.
Options
paths
The plugin will create additional pages, you can override their pathnames.
import { defineConfig } from "fumapress";
import { blogPlugin } from "fumapress/plugins/blog";
export default defineConfig({
// ...
}).plugins(
blogPlugin({
paths: {
index: "/blog",
tags: "/blog/tags",
},
}),
);You can set to false to disable a certain page.
authors
Register authors once, keyed by the ids used in the authors frontmatter of posts.
import { defineConfig } from "fumapress";
import { blogPlugin } from "fumapress/plugins/blog";
export default defineConfig({
// ...
}).plugins(
blogPlugin({
authors: {
fuma: {
name: "Fuma Nama",
title: "Maintainer",
url: "https://fuma-nama.dev",
image: "https://github.com/fuma-nama.png",
},
},
}),
);---
title: My Blog Post
authors: [fuma]
---Prop
Type
Ids missing from the registry are shown by name only.
layouts
Override the UI.
import { defineConfig } from "fumapress";
import { blogPlugin } from "fumapress/plugins/blog";
// blog layouts
import { createHomeLayout } from "fumapress/layouts/home";
import { createBlogLayoutPage } from "fumapress/layouts/blog";
import { createBlogIndexPage } from "fumapress/layouts/blog.index";
import { createBlogTagPage, createBlogTagsPage } from "fumapress/layouts/blog.tags";
const HomeLayout = createHomeLayout<typeof config.$context>();
const BlogPage = createBlogLayoutPage<typeof config.$context>();
const BlogIndex = createBlogIndexPage<typeof config.$context>();
const BlogTags = createBlogTagsPage<typeof config.$context>();
const BlogTag = createBlogTagPage<typeof config.$context>();
const config = defineConfig({
// ...
}).plugins(
blogPlugin({
layouts: {
// shared layout for blog
layout: (props) => <HomeLayout {...props} />,
// renderer for blog pages
page: (props) => <BlogPage {...props} />,
// renderer for blog's index page
index: (props) => <BlogIndex {...props} />,
// renderer for blog's tags list
tags: (props) => <BlogTags {...props} />,
// renderer for blog's tag info page
tag: (props) => <BlogTag {...props} />,
},
}),
);
export default config;Custom Layouts
When writing your own layouts, fumapress/plugins/blog exports helpers for blog data, so you don't read the loader and adapters yourself:
getBlogPosts(ctx, lang?): posts of a locale as{ page, date }, newest first.getBlogAuthors(ctx, page): authors of a post, resolved with theauthorsoption.tagSlug(tag): URL segment of a tag, for links to tag pages.
import { getPressContext } from "fumapress";
import { getBlogAuthors, getBlogPosts, type BlogLayoutPage } from "fumapress/plugins/blog";
const BlogPage: BlogLayoutPage<typeof config.$context> = async ({ page }) => {
const ctx = getPressContext<typeof config.$context>();
const authors = await getBlogAuthors(ctx, page);
const posts = await getBlogPosts(ctx, page.locale);
const index = posts.findIndex((post) => post.page.url === page.url);
const older = posts[index + 1];
return (
<article>
<h1>{page.data.title}</h1>
<p>{authors.map((author) => author.name).join(", ")}</p>
{(await ctx.getPageBody(page))?.node}
{older && <a href={older.page.url}>{older.page.data.title}</a>}
</article>
);
};Tag pages receive the slug as tag, link to them with tagSlug():
import { tagSlug } from "fumapress/plugins/blog";
<a href={`/blog/tags/${tagSlug(tag)}`}>{tag}</a>;Last updated on
