Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/shuding/nextra/llms.txt

Use this file to discover all available pages before exploring further.

The Nextra Blog Theme (nextra-theme-blog) is a minimal, typography-focused layout for publishing markdown blog posts with Next.js App Router. It provides a post listing page with dates and reading times, a tags system for categorisation, RSS feed generation, a dark mode toggle, and a flexible navbar — all with very little configuration required.

What’s Included

Post Listing

Display all posts sorted by date, with titles, descriptions, and publication timestamps using the <PostCard> component.

Tags System

Filter posts by tag. Generate static tag pages with generateStaticParams using the getTags helper.

RSS Feed

Generate a standards-compliant rss.xml route from your page map data using a Next.js Route Handler.

Theme Switcher

Built-in <ThemeSwitch> component for light/dark/system theme selection via next-themes.

Navigation Bar

<Navbar> component that auto-generates top-level nav links from your page map, with slots for search and other children.

Reading Time

Enable readingTime: true in your Nextra config to automatically attach reading time metadata to every post.

Installation

1
Install the package
2
npm
npm install nextra nextra-theme-blog
pnpm
pnpm add nextra nextra-theme-blog
yarn
yarn add nextra nextra-theme-blog
3
Configure Nextra in next.config.js
4
Enable the readingTime option if you want per-post reading time automatically computed from your MDX content:
5
import nextra from 'nextra'

const withNextra = nextra({
  defaultShowCopyCode: true,
  readingTime: true
})

export default withNextra({
  reactStrictMode: true
})
6
Create the root layout
7
Import Layout, Navbar, Footer, and ThemeSwitch from nextra-theme-blog and compose them in your app/layout.jsx:
8
import { Footer, Layout, Navbar, ThemeSwitch } from 'nextra-theme-blog'
import { Banner, Head, Search } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
import 'nextra-theme-blog/style.css'

export const metadata = {
  title: 'My Blog'
}

export default async function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <Head />
      <body>
        <Layout>
          <Navbar pageMap={await getPageMap()}>
            <Search />
            <ThemeSwitch />
          </Navbar>

          {children}

          <Footer>
            CC BY-NC 4.0 {new Date().getFullYear()} © Your Name.
            <a href="/rss.xml" style={{ float: 'right' }}>
              RSS
            </a>
          </Footer>
        </Layout>
      </body>
    </html>
  )
}
9
Add MDX components
10
Create mdx-components.js at the project root:
11
export { useMDXComponents } from 'nextra-theme-blog'

Post Frontmatter

Each blog post is an MDX file inside your app/posts/ directory. Declare metadata in the frontmatter block at the top of the file. All fields are optional, but title and date are recommended for the post listing to display correctly.
---
title: Building Documentation Sites with Nextra
date: 2024-03-15
author: Jane Doe
tags: [nextra, tutorial, documentation]
description: Learn how to set up a fast, searchable docs site using Nextra and the blog theme.
---

Your post content begins here...

BlogMetadata Type

The BlogMetadata type describes the shape of the frontmatter object available on every processed post. All fields are optional.
type BlogMetadata = {
  author?: string        // Post author name
  date?: string          // ISO 8601 date string, e.g. "2024-03-15"
  description?: string   // Short description shown in post cards and SEO
  readingTime?: ReadingTime // Auto-populated when readingTime: true in nextra config
  tags?: []              // Tuple of tag strings for categorisation (e.g. ['nextra', 'tutorial'])
  title?: string         // Post title shown in listings and the browser tab
}
The readingTime field is populated automatically by Nextra when you set readingTime: true in next.config.js. It uses remark-reading-time under the hood and adds a ReadingTime object containing text (e.g. "3 min read") and minutes to the frontmatter of every MDX file.

Layout Component

The <Layout> component wraps your application in a ThemeProvider from next-themes and applies the blog’s base typography styles via a <article> container.
import { Layout } from 'nextra-theme-blog'

Props

children
ReactNode
required
The page content — passed automatically by Next.js as {children} in the root layout.
banner
ReactNode
A banner element displayed above the navbar. Use the <Banner> component from nextra/components.
nextThemes
object
Configuration for next-themes ThemeProvider. Accepts all ThemeProvider props except children. Useful for overriding attribute, defaultTheme, or storageKey.
import { Banner } from 'nextra/components'

<Layout
  banner={
    <Banner storageKey="v4-release">
      🎉 Nextra 4.0 is out!{' '}
      <a href="/posts/v4-release">Read the announcement →</a>
    </Banner>
  }
  nextThemes={{ defaultTheme: 'dark' }}
>
  {children}
</Layout>

The <Navbar> component reads your page map to automatically render top-level navigation links, then slots in any children (such as a search box or theme switch) on the right side.
import { Navbar } from 'nextra-theme-blog'

Props

pageMap
PageMapItem[]
required
The result of getPageMap() from nextra/page-map. Top-level items in the page map become nav links.
children
ReactNode
Additional elements rendered after the nav links — typically <Search /> and <ThemeSwitch />.
import { Search } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
import { Navbar, ThemeSwitch } from 'nextra-theme-blog'

<Navbar pageMap={await getPageMap()}>
  <Search />
  <ThemeSwitch />
</Navbar>

The <Footer> component renders a small footer below the article content. Pass children for copyright text, license info, or an RSS link.
import { Footer } from 'nextra-theme-blog'
<Footer>
  CC BY-NC 4.0 {new Date().getFullYear()} © Jane Doe.
  <a href="/rss.xml" style={{ float: 'right' }}>
    RSS
  </a>
</Footer>

Getting Posts and Tags

The blog theme does not provide a built-in getPosts helper — you create a small utility that calls getPageMap() and processes the results. This runs at build time as a Server Component or inside Route Handlers.

posts/get-posts.js

Create a shared helper module that both the posts listing page and the tag pages can import:
import { normalizePages } from 'nextra/normalize-pages'
import { getPageMap } from 'nextra/page-map'

export async function getPosts() {
  const { directories } = normalizePages({
    list: await getPageMap('/posts'),
    route: '/posts'
  })
  return directories
    .filter(post => post.name !== 'index')
    .sort((a, b) => new Date(b.frontMatter.date) - new Date(a.frontMatter.date))
}

export async function getTags() {
  const posts = await getPosts()
  const tags = posts.flatMap(post => post.frontMatter.tags)
  return tags
}
normalizePages is a low-level utility from nextra/normalize-pages that converts the raw PageMapItem[] tree into a flat list of directories (folders/pages) with resolved frontmatter attached. Always filter out the index entry to exclude the listing page itself from the post array.

Posts Listing Page

The posts page fetches all posts and their tags, then renders them using <PostCard>:
import Link from 'next/link'
import { PostCard } from 'nextra-theme-blog'
import { getPosts, getTags } from './get-posts'

export const metadata = { title: 'Posts' }

export default async function PostsPage() {
  const tags = await getTags()
  const posts = await getPosts()
  const allTags = Object.create(null)

  for (const tag of tags) {
    allTags[tag] ??= 0
    allTags[tag] += 1
  }

  return (
    <div data-pagefind-ignore="all">
      <h1>{metadata.title}</h1>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
        {Object.entries(allTags).map(([tag, count]) => (
          <Link key={tag} href={`/tags/${tag}`} className="nextra-tag">
            {tag} ({count})
          </Link>
        ))}
      </div>
      {posts.map(post => (
        <PostCard key={post.route} post={post} />
      ))}
    </div>
  )
}

PostCard Component

The <PostCard> component renders a single post preview with title, description, date, and a “Read More” link.
import { PostCard } from 'nextra-theme-blog'
post
{ route: string, frontMatter: BlogMetadata }
required
The post object from getPosts(). Must have a route string and a frontMatter object following the BlogMetadata shape.
readMore
string
default:"'Read More →'"
The text of the “read more” link appended to the description. Set to an empty string "" or leave it undefined to hide the link.

Tags Page

Use generateStaticParams to pre-render a page for each unique tag at build time:
import { PostCard } from 'nextra-theme-blog'
import { getPosts, getTags } from '../../posts/get-posts'

export async function generateMetadata(props) {
  const params = await props.params
  return {
    title: `Posts Tagged with "${decodeURIComponent(params.tag)}"`
  }
}

export async function generateStaticParams() {
  const allTags = await getTags()
  return [...new Set(allTags)].map(tag => ({ tag }))
}

export default async function TagPage(props) {
  const params = await props.params
  const { title } = await generateMetadata({ params })
  const posts = await getPosts()

  return (
    <>
      <h1>{title}</h1>
      {posts
        .filter(post =>
          post.frontMatter.tags.includes(decodeURIComponent(params.tag))
        )
        .map(post => (
          <PostCard key={post.route} post={post} />
        ))}
    </>
  )
}

RSS Feed

Generate a standard RSS 2.0 feed by creating a Next.js Route Handler at app/rss.xml/route.js. This file runs server-side and returns a valid XML response:
import { getPosts } from '../posts/get-posts.js'

const CONFIG = {
  title: 'My Blog',
  siteUrl: 'https://your-domain.com',
  description: 'Latest posts from my blog',
  lang: 'en-us'
}

export async function GET() {
  const allPosts = await getPosts()

  const items = allPosts
    .map(
      post => `    <item>
        <title>${post.title}</title>
        <description>${post.frontMatter.description}</description>
        <link>${CONFIG.siteUrl}${post.route}</link>
        <pubDate>${new Date(post.frontMatter.date).toUTCString()}</pubDate>
    </item>`
    )
    .join('\n')

  const xml = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
  <channel>
    <title>${CONFIG.title}</title>
    <link>${CONFIG.siteUrl}</link>
    <description>${CONFIG.description}</description>
    <language>${CONFIG.lang}</language>
${items}
  </channel>
</rss>`

  return new Response(xml, {
    headers: { 'Content-Type': 'application/rss+xml' }
  })
}
The Route Handler approach means the RSS feed is generated at request time (or cached at build time with export const dynamic = 'force-static'). Link to it from your footer with <a href="/rss.xml">RSS</a> and add a <link> element in your <Head> for feed autodiscovery.

Comments Integration

The blog theme includes a <Comments> component backed by Cusdis, a lightweight, privacy-friendly comment system. Wrap individual post layouts with it to enable per-post comments:
import { Comments } from 'nextra-theme-blog'

export default function CommentsLayout({ children }) {
  return (
    <>
      {children}
      <Comments lang="en" appId="your-cusdis-app-id" />
    </>
  )
}
Use a Next.js Route Group (the (with-comments) folder) to apply the comments layout only to specific posts, leaving others without the comment widget.

ThemeSwitch Component

The standalone <ThemeSwitch> component renders a dark/light/system mode toggle. It is typically placed inside the <Navbar> but can be used anywhere in the tree.
import { ThemeSwitch } from 'nextra-theme-blog'

<ThemeSwitch />

All Named Exports

The following are all named exports available from nextra-theme-blog:
ExportTypeDescription
LayoutComponentRoot layout — wraps content in ThemeProvider and article container
NavbarComponentTop navigation bar with auto-generated links from pageMap
FooterComponentPage footer with optional children
ThemeSwitchComponentDark/light/system mode toggle
PostCardComponentIndividual post preview card (title, description, date)
CommentsComponentCusdis-powered comment section
useMDXComponentsHookMDX component map — re-export from mdx-components.js
useThemeHookRe-exported from next-themes — read the active theme

Complete Example Structure

A typical blog project using nextra-theme-blog has this file layout:
app/
├── layout.jsx              ← Root layout with Layout, Navbar, Footer
├── page.mdx                ← Homepage / about page
├── mdx-components.js       ← Re-exports useMDXComponents
├── posts/
│   ├── get-posts.js        ← getPosts() and getTags() helpers
│   ├── page.jsx            ← /posts listing page
│   ├── my-first-post.mdx   ← Individual post
│   └── (with-comments)/
│       ├── layout.jsx      ← Adds <Comments> to matching posts
│       └── another-post.mdx
├── tags/
│   └── [tag]/
│       └── page.jsx        ← /tags/:tag filtered listing
└── rss.xml/
    └── route.js            ← RSS 2.0 feed generator
Remember to add data-pagefind-ignore="all" to page-listing containers (like the posts page) so that Pagefind’s full-text indexing does not index the listing UI — only the individual post pages should be indexed.

Build docs developers (and LLMs) love