Blog
Add a blog to your app.
Installation
npx @fumadocs/cli@latest add blogIt installs the components to components/blog, and the utilities of posts to lib/blog.ts.
Define a collection for your posts with blogPageSchema, and register your authors:
import { defineCollections } from 'fumadocs-mdx/macro';
import { loader } from 'fumadocs-core/source';
import { type BlogAuthor, blogPageSchema } from './blog';
export const blog = defineCollections({
type: 'doc',
dir: 'content/blog',
schema: blogPageSchema,
});
export const blogSource = loader({
baseUrl: '/blog',
source: blog.toFumadocsSource(),
});
export const blogAuthors: Record<string, BlogAuthor> = {
fuma: {
name: 'Fuma Nama',
title: 'Maintainer',
url: 'https://fuma-nama.dev',
image: 'https://avatars.githubusercontent.com/u/76240755',
},
};Write your posts under content/blog:
---
title: Hello World
description: My first post.
date: 2026-10-12
tags: [Announcement]
authors: [fuma]
image: /blog/hello-world.png
---| Field | Description |
|---|---|
date | Publish date, posts are sorted by it |
tags | Each tag has its page under /blog/tags |
authors | Keys of your authors, unknown keys are names |
image | Cover image URL, shown on the cards of posts |
Pages
getPosts() resolves the authors and tags of pages, newest first. Render them in routes under your Home Layout.
Posts
import type { Route } from './+types/blog';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { blogAuthors, blogSource } from '@/lib/source';
import { getPosts } from '@/lib/blog';
import { BlogCards } from '@/components/blog';
export function loader() {
return getPosts(blogSource.getPages(), blogAuthors);
}
export default function Page({ loaderData }: Route.ComponentProps) {
return (
<HomeLayout {...baseOptions()}>
<div className="mx-auto w-full max-w-(--fd-layout-width) px-4 py-12">
<BlogCards posts={loaderData} />
</div>
</HomeLayout>
);
}Post
findPost() finds a post along with the newer and older posts. With toc, a floating panel shows the table of contents and a share button.
import type { Route } from './+types/blog.post';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { blog, blogAuthors, blogSource } from '@/lib/source';
import { findPost, getPosts } from '@/lib/blog';
import { useMDXComponents } from '@/components/mdx';
import {
BlogAuthors,
BlogBackLink,
BlogBody,
BlogDate,
BlogDescription,
BlogHeader,
BlogPage,
BlogPostNav,
BlogTags,
BlogTitle,
} from '@/components/blog';
export function loader({ params }: Route.LoaderArgs) {
const page = blogSource.getPage([params.slug]);
const found = page && findPost(getPosts(blogSource.getPages(), blogAuthors), page.url);
if (!page || !found) throw new Response('Not Found', { status: 404 });
return { path: page.path, ...found };
}
export default function Page({ loaderData }: Route.ComponentProps) {
const { path, post, newer, older } = loaderData;
const page = blog.get(path)!;
const MDX = page.body;
return (
<HomeLayout {...baseOptions()}>
<BlogPage toc={page.toc}>
<BlogHeader>
<BlogBackLink href="/blog" />
<BlogTitle>{post.title}</BlogTitle>
<BlogDescription>{post.description}</BlogDescription>
<BlogAuthors authors={post.authors} />
<BlogTags tags={post.tags} />
{post.date && <BlogDate date={post.date} />}
</BlogHeader>
<BlogBody>
<MDX components={useMDXComponents()} />
</BlogBody>
<BlogPostNav newer={newer} older={older} />
</BlogPage>
</HomeLayout>
);
}Tags
List all tags with getTags():
import type { Route } from './+types/blog.tags';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { blogSource } from '@/lib/source';
import { getPosts, getTags } from '@/lib/blog';
import { BlogHeader, BlogTagCards, BlogTitle } from '@/components/blog';
export function loader() {
return getTags(getPosts(blogSource.getPages()));
}
export default function Page({ loaderData }: Route.ComponentProps) {
return (
<HomeLayout {...baseOptions()}>
<div className="mx-auto flex w-full max-w-(--fd-layout-width) flex-col gap-4 px-4 py-12">
<BlogHeader>
<BlogTitle>All Tags</BlogTitle>
</BlogHeader>
<BlogTagCards tags={loaderData} />
</div>
</HomeLayout>
);
}And the posts of a tag with getTagPosts(), which returns undefined for unknown tags:
import type { Route } from './+types/blog.tag';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { blogAuthors, blogSource } from '@/lib/source';
import { getPosts, getTagPosts } from '@/lib/blog';
import { BlogCards, BlogHeader, BlogTitle } from '@/components/blog';
export function loader({ params }: Route.LoaderArgs) {
const result = getTagPosts(getPosts(blogSource.getPages(), blogAuthors), params.tag);
if (!result) throw new Response('Not Found', { status: 404 });
return result;
}
export default function Page({ loaderData }: Route.ComponentProps) {
return (
<HomeLayout {...baseOptions()}>
<div className="mx-auto flex w-full max-w-(--fd-layout-width) flex-col gap-4 px-4 py-12">
<BlogHeader>
<BlogTitle>{loaderData.tag.name}</BlogTitle>
</BlogHeader>
<BlogCards posts={loaderData.posts} />
</div>
</HomeLayout>
);
}Internationalization
Pass the pages of a locale to getPosts(), like blogSource.getPages(lang).
The URLs of tag pages come from getBlogUrl in lib/blog.ts. Pass it your i18n config to match the URLs of your loader:
import { i18n } from '@/lib/i18n';
export const getBlogUrl = createGetUrl('/blog', i18n);To publish a feed of your posts, see RSS Feed.
How is this guide?
Last updated on
