Skip to Content

Blog

Add a blog with post, index and tag pages.

Turn a content source into a blog. Each post gets a page, like /blog/hello-world, with an index at /blog and tag pages at /blog/tags.

Setup

Install

npm i @fumapress/blog

Add the Styles

Add this after the imports in your stylesheet:

src/app.css
@import "@fumapress/blog/css/preset.css";

Add the Collection

Add a collection for your posts, with the blog schema, and pass its key to the plugin:

import { defineConfig } from "astro/config";
import press from "@fumapress/astro";

export default defineConfig({
  integrations: [
    press({
      collections: {
        docs: { dir: "content/docs" },
        blog: { dir: "content/blog" },
      },
    }),
  ],
});

Writing Posts

content/blog/hello-world.mdx
---
title: My Blog Post
description: Hello World!!!
date: 2026-09-06
tags: [my-tag]
authors: [fuma]
image: /covers/hello.png
---

## Introduction

Welcome to my first blog post.

Posts are ordered by date. Each tag gets a page, and image shows on post cards.

Authors

The keys of authors are the IDs in the frontmatter. An ID that isn't registered shows as the plain ID.

press.config.tsx
blogPlugin({
  source: "blog",
  authors: {
    fuma: {
      name: "Fuma Nama",
      title: "Maintainer",
      url: "https://fuma-nama.dev",
      image: "https://github.com/fuma-nama.png",
    },
  },
});

Prop

Type

Customization

Other Content Sources

Posts can come from any content source, like Notion. The date comes from the getPageCreatedAt hook of its plugin. These options read the other fields when they aren't in the frontmatter:

press.config.tsx
blogPlugin({
  source: "blog",
  getTags: (page) => page.data.tags,
  getAuthors: (page) => page.data.authors,
  getImage: (page) => page.data.image,
});

To use multiple sources as posts, pass their keys:

press.config.tsx
blogPlugin({
  source: ["blog", "news"],
  getTags: (page) => page.data.tags ?? [page.type],
});

URLs

Set a path to move a page, or false to remove it:

press.config.tsx
blogPlugin({
  source: "blog",
  paths: {
    index: "/news",
    tags: false,
  },
});

Custom UI

In renderPage, a post has the type of its source key, like blog. The other pages are blog.index, blog.tags and blog.tag. This example replaces the index, and renders the built-in post in another layout:

press.config.tsx
import { BlogPostView } from "@fumapress/blog";
import { HomeLayout } from "fumadocs-ui/layouts/home";

renderPage(props) {
  const { renderDefault } = props;
  // the Home Layout takes these props, the others are for docs layouts
  const { nav, links, githubUrl, i18n, themeSwitch, searchToggle } = props.layoutProps;
  const home = { nav, links, githubUrl, i18n, themeSwitch, searchToggle };
  switch (props.type) {
    case "blog.index":
      return (
        <HomeLayout {...home}>
          <ul>
            {props.posts.map((post) => (
              <li key={post.page.url}>{post.page.data.title}</li>
            ))}
          </ul>
        </HomeLayout>
      );
    case "blog":
      return (
        <HomeLayout {...home}>
          <BlogPostView post={props.post} newer={props.newer} older={props.older}>
            {props.body}
          </BlogPostView>
        </HomeLayout>
      );
    default:
      return renderDefault();
  }
},

@fumapress/blog also exports BlogIndexView, BlogTagsView and BlogTagView.

Listing Posts

getBlogPosts() returns all posts, newest first:

src/pages/index.astro
---
import HomeLayout from "@fumapress/astro/components/HomeLayout.astro";
import { getPressContext } from "@fumapress/astro/runtime";
import { getBlogPosts } from "@fumapress/blog";
import type { app } from "../../press.config";

const ctx = getPressContext<typeof app>();
const posts = await getBlogPosts(ctx);
---

<HomeLayout title="Acme">
  <ul>
    {posts.slice(0, 3).map((post) => <li><a href={ctx.withBase(post.page.url)}>{post.page.data.title}</a></li>)}
  </ul>
</HomeLayout>

With internationalization, pass the language: getBlogPosts(ctx, { lang }).

Options

Prop

Type

Last updated on

On this page