Fumadocs

Blog & Changelog

Add a blog or changelog to your site.

Blog

The Blog components render your posts with cards, tags and a floating table of contents. Install them with Fumadocs CLI:

npx @fumadocs/cli@latest add blog

Setup

Add a blog collection with blogPageSchema and create a source:

fumadocs({
  collections: {
    docs: { dir: 'content/docs' },
    blog: { dir: 'content/blog' }, 
  },
});

See Blog for the frontmatter fields and utilities like getPosts().

Pages

The blog pages are custom pages with HomeLayout.astro. BlogPage hydrates with client:load for the floating panel; its children stay static. <Body /> renders the content as an island with typography styles, so you don't need BlogBody.

---
import HomeLayout from '@fumadocs/astro/components/HomeLayout.astro';
import { blogAuthors, blogSource } from '@/lib/source';
import { getPosts } from '@/lib/blog';
import { BlogCards } from '@/components/blog';
---

<HomeLayout title="Blog">
  <div class="mx-auto w-full max-w-(--fd-layout-width) px-4 py-12">
    <BlogCards posts={getPosts(blogSource.getPages(), blogAuthors)} />
  </div>
</HomeLayout>

Internationalization

Pass a locale's pages to getPosts(), like blogSource.getPages(lang). Create the pages under src/pages/[...lang]/blog with a path for each language, see Custom Pages.

Changelog

The Changelog components render a timeline of your releases. Install them with Fumadocs CLI:

npx @fumadocs/cli@latest add changelog

The pages are custom pages, generated at build time. The timeline renders with ReactContent.astro so copy buttons in release notes hydrate:

src/pages/changelog/[...page].astro
---
import type { GetStaticPaths, InferGetStaticPropsType } from 'astro';
import { createElement } from 'react';
import { githubReleases } from '@fumadocs/changelog/github';
import HomeLayout from '@fumadocs/astro/components/HomeLayout.astro';
import ReactContent from '@fumadocs/astro/components/ReactContent.astro';
import { ChangelogPagination, ChangelogTimeline } from '@/components/changelog';

export const getStaticPaths = (async () => {
  const paths = [];
  for (let page: number | undefined = 1; page; ) {
    const releases = await githubReleases({
      owner: 'fuma-nama',
      repo: 'fumadocs',
      token: import.meta.env.GITHUB_TOKEN,
      page,
    });
    if (releases.entries.length === 0) break;
    paths.push({
      params: { page: page > 1 ? String(page) : undefined },
      props: { page, ...releases },
    });
    page = releases.nextPage;
  }
  return paths;
}) satisfies GetStaticPaths;

type Props = InferGetStaticPropsType<typeof getStaticPaths>;

const { page, entries, nextPage } = Astro.props;
const href = (page: number) => (page > 1 ? `/changelog/${page}` : '/changelog');
---

<HomeLayout title="Changelog">
  <div class="mx-auto flex w-full max-w-[900px] flex-col gap-10 px-4 py-12">
    <ReactContent node={createElement(ChangelogTimeline, { entries })} />
    <ChangelogPagination
      newer={page > 1 ? href(page - 1) : undefined}
      older={nextPage ? href(nextPage) : undefined}
    />
  </div>
</HomeLayout>

See Changelog for the sources and components, like githubReleases(). With custom entries, body takes React nodes, not Astro components. Pass the Markdown of pages as content instead, like await page.data.getText('raw').

How is this guide?

Last updated on