1. Home
  2. ChevronRightFeatures
  3. ChevronRightCollections

Collections

Collections allow you to group related documents under dynamic directories with automated route generation. By defining folder-based conventions, you can decouple standard documentation pages from customized lists, post pages, and custom layouts.

Quick StartLink

Creating a new collection takes only two steps:

1. Create a bracketed folderLink

Add a directory starting with [ and ending with ] inside your docs/ directory, and drop your MDX/Markdown files inside it.

mkdir -p docs/docs/\[blog\]
touch docs/docs/\[blog\]/first-post.mdx

2. Add FrontmatterLink

Add metadata at the top of your markdown files. Boltdocs will automatically register these files as collection posts.

---
title: "Introducing Boltdocs Collections"
date: 2026-05-28
author: Jesús Alcalá
excerpt: "A look at the new flexible collection routing."
---

Welcome to the future of dynamic collections in Boltdocs!

That’s it! Your post will be accessible at /blog/first-post and a collection list index will be auto-generated at /blog.


How It WorksLink

Boltdocs parses bracketed directory names (e.g. [blog], [releases]) as collections.

website/
└── docs/
    └── [blog]/                  → Dynamic Collection Namespace
        ├── list.tsx             → Custom Collection Index Layout (Optional)
        ├── post.tsx             → Custom Post View Layout (Optional)
        ├── layout.tsx           → Custom Root Layout for this Collection (Optional)
        ├── first-post.mdx       → Post page at `/blog/first-post`
        └── second-post.mdx      → Post page at `/blog/second-post`

Folder Convention Rules:Link

  1. Dynamic Namespace: The name of the bracketed directory (e.g., [blog]) maps to the collection ID (blog) and serves as the URL base route /blog.
  2. List Override (list.tsx): If present, overrides the index listing page at /blog. Receives the list of all posts in the collection.
  3. Post Override (post.tsx): If present, overrides the wrapper layout for individual posts (e.g. /blog/first-post).
  4. Layout Override (layout.tsx): If present, acts as the root route layout wrapper (with its own sidebar or navigation context) for all routes inside the collection.

Designing Custom ViewsLink

To override the default look of your collections, you can drop standard React components directly into the collection folder.

Custom Post Component (post.tsx)Link

A custom post component allows you to design how individual entries (like a blog article) render. Use usePost() to access the current post's data — no useLoaderData required:

import { usePost, useMergedComponents } from 'boltdocs/client'

export default function BlogPost({ MDXComponent, mdxComponents }: any) {
  const post = usePost()
  if (!post) return null

  const { title, date, author, excerpt, lastUpdated, coverImage } = post

  const allComponents = useMergedComponents(mdxComponents)
  const { LastUpdated } = allComponents

  return (
    <article className="max-w-3xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
      <header className="mb-10 pb-8 border-b border-gray-200 dark:border-gray-800">
        {title && (
          <h1 className="text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white mb-4">
            {title}
          </h1>
        )}

        <div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400 mt-6">
          {date && (
            <time dateTime={new Date(date).toISOString()}>
              {new Date(date).toLocaleDateString(undefined, {
                year: 'numeric',
                month: 'long',
                day: 'numeric',
              })}
            </time>
          )}

          {author && (
            <>
              <span aria-hidden="true">&middot;</span>
              <img
                src={typeof author === 'string' ? author : author.avatar}
                alt={typeof author === 'string' ? author : author.name}
                className="w-8 h-8 rounded-full"
              />
              <span>{typeof author === 'string' ? author : author.name}</span>
            </>
          )}
        </div>

        {coverImage && (
          <div className="relative aspect-video w-full overflow-hidden rounded-xl border border-gray-200 dark:border-gray-800 bg-neutral-100 dark:bg-neutral-900 mt-8 mb-6">
            <img
              src={coverImage}
              alt={title || 'Cover image'}
              className="object-cover w-full h-full"
            />
          </div>
        )}

        {excerpt && (
          <p className="mt-6 text-xl text-gray-600 dark:text-gray-300">
            {excerpt}
          </p>
        )}
      </header>

      <div className="prose prose-blue dark:prose-invert max-w-none">
        <MDXComponent components={allComponents} />
      </div>

      {lastUpdated && LastUpdated && (
        <div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-800">
          <LastUpdated date={lastUpdated} />
        </div>
      )}
    </article>
  )
}
Info
Note

usePost() called without parameters returns the current post's data when used inside a post.tsx component. It reads from an internal context — no useLoaderData or React Router imports needed.


API ReferenceLink

Loader Data StructuresLink

When you write custom list.tsx or post.tsx files, Boltdocs provides hooks (usePosts, usePost) that handle data access internally. The loader data structures below are what React Router loaders return, but you should use the hooks instead:

CollectionPostLoaderDataLink

Returned by React Router loaders for collection post routes. Use usePost() instead of accessing this directly.

PropertyTypeDefaultDescription
routeComponentRouteDetailed route metadata extracted from the post's frontmatter.
collectionstringThe name of the collection this post belongs to (e.g. 'blog').
headingsHeading[][]Extracted page headers for generating dynamic table of contents (TOC).

CollectionListLoaderDataLink

Returned by React Router loaders for collection list indexes (e.g. /blog). Use usePosts() instead of accessing this directly.

PropertyTypeDefaultDescription
postsCollectionPostItem[][]Paginated array of all posts in the collection.
totalPagesnumber1Total number of pages based on post count.
currentPagenumber1Current page number (1-based index).
collectionstringThe name of the collection identifier.

HooksLink

Boltdocs provides several React hooks under boltdocs/client to fetch your collections from anywhere in your app (like sidebars, footers, or custom home pages).

All hooks default to the "blog" collection when no collection is specified.

usePosts(collection?: string, options?: { includeDrafts?: boolean })Link

Returns an array of all posts in a collection, filtered by the current locale and version. Defaults to "blog". Use this for lists, sidebars, or any component that needs collection posts.

import { usePosts } from 'boltdocs/client'

// Defaults to "blog" collection
function BlogSidebar() {
  const posts = usePosts()
  return (
    <ul>
      {posts.map(post => <li key={post.path}>{post.title}</li>)}
    </ul>
  )
}

// Explicit collection
function ChangelogList() {
  const posts = usePosts('changelog')
  // ...
}

The returned array includes all filtered posts — implement your own pagination or infinite scroll by slicing the array:

function PaginatedBlog() {
  const allPosts = usePosts()
  const [page, setPage] = useState(1)
  const perPage = 10
  const posts = allPosts.slice((page - 1) * perPage, page * perPage)
  // ...
}

When drafts are visible (via drafts.visible: true or BOLTDOCS_DRAFTS=true), draft posts are included in the results. To explicitly include or exclude drafts:

// Include draft posts (useful for admin panels)
function AdminBlogList() {
  const posts = usePosts('blog', { includeDrafts: true })
  return posts.map(post => (
    <div key={post.path}>
      {post.title}
      {post.draft && <span className="badge">Draft</span>}
    </div>
  ))
}

// Explicitly exclude drafts (default behavior)
function PublicBlogList() {
  const posts = usePosts('blog', { includeDrafts: false })
  // ...
}

usePost()Link

Returns the current post's data when called inside a post.tsx component. No parameters needed — the hook reads from an internal context provided by the framework.

import { usePost } from 'boltdocs/client'

export default function BlogPost({ MDXComponent, mdxComponents }) {
  const { title, date, author, headings, lastUpdated } = usePost()
  // ...
}

You can also call usePost(collection, slug) with explicit parameters to look up a specific post from anywhere in your app:

import { usePost } from 'boltdocs/client'

function FeaturedPost() {
  const post = usePost('blog', 'boltdocs-2.9.0')
  return <div>{post?.title}</div>
}

useRecentPosts(collection?: string, count?: number)Link

Returns the most recent posts of a collection. Defaults to "blog" collection and count of 5.

import { useRecentPosts } from 'boltdocs/client'

// Defaults to "blog", returns 3 most recent
function RecentUpdates() {
  const recent = useRecentPosts('blog', 3)
  // ...
}

ComponentRouteLink

The type mapping metadata extracted from the frontmatter of your files.

PropertyTypeDefaultDescription
titlestring''Title of the post, pulled from frontmatter title.
datestring | DateundefinedDate of publication, pulled from frontmatter date.
authorstring | AuthorObjectundefinedDetails of the author, supporting plain strings or objects with name, avatar, url, and image. When serialized into the route metadata (route.author), the framework coerces the object shape to its .name so consumers always see a plain string.
excerptstring''Short description or excerpt.
lastUpdatedstring | numberundefinedThe timestamp of the last git commit (or manual override).
frontmatterRecord<string, any>{}Extensible object with all custom frontmatter parameters.

Advanced CustomizationLink

Custom List (list.tsx)Link

Create a list.tsx file inside your collection folder to override the default listing page. Use usePosts() to get all filtered posts and handle pagination:

docs/docs/[blog]/list.tsx
import { useState } from 'react'
import { usePosts } from 'boltdocs/client'

export default function BlogList() {
  const allPosts = usePosts()
  const [page, setPage] = useState(1)
  const perPage = 10
  const posts = allPosts.slice((page - 1) * perPage, page * perPage)
  const totalPages = Math.ceil(allPosts.length / perPage)

  return (
    <div className="py-8 max-w-2xl mx-auto px-4">
      <h1 className="text-3xl font-bold mb-6">Blog</h1>

      <div className="space-y-8">
        {posts.map(post => (
          <article key={post.path} className="border-b border-subtle pb-6">
            <h2 className="text-xl font-semibold mb-2">
              <a href={post.path} className="text-primary-600 hover:underline">
                {post.title}
              </a>
            </h2>
            {post.date && (
              <time className="text-xs text-muted block mb-2">
                {new Date(post.date).toLocaleDateString()}
              </time>
            )}
            {post.excerpt && <p className="text-sm text-body">{post.excerpt}</p>}
          </article>
        ))}
      </div>

      {totalPages > 1 && (
        <div className="mt-8 flex gap-4 text-sm">
          {page > 1 && (
            <button onClick={() => setPage(page - 1)} className="text-primary-600 hover:underline">
              Previous
            </button>
          )}
          <span>
            Page {page} of {totalPages}
          </span>
          {page < totalPages && (
            <button onClick={() => setPage(page + 1)} className="text-primary-600 hover:underline">
              Next
            </button>
          )}
        </div>
      )}
    </div>
  )
}

Custom Layout (layout.tsx)Link

Create a layout.tsx file inside your collection folder to wrap all routes (list and posts) with a custom layout. This is useful for adding collection-specific navigation, sidebars, or headers:

docs/docs/[blog]/layout.tsx
import { usePosts } from 'boltdocs/client'

export default function BlogLayout({ children }: { children: React.ReactNode }) {
  const posts = usePosts()

  return (
    <div className="flex">
      <aside className="w-64 border-r border-subtle p-4">
        <h2 className="font-bold mb-4">Blog</h2>
        <nav>
          {posts.map(post => (
            <a
              key={post.path}
              href={post.path}
              className="block py-1 text-sm hover:text-primary-600"
            >
              {post.title}
            </a>
          ))}
        </nav>
      </aside>
      <main className="flex-1">{children}</main>
    </div>
  )
}

PaginationLink

By default, each page displays 10 posts, but you can configure this globally via collections.postsPerPage inside your boltdocs.config.ts:

boltdocs.config.ts
export default defineConfig({
  collections: {
    postsPerPage: 12, // Customize items per page
  },
})
Info
Metadata Access

All parameters written in your post frontmatter are preserved in the frontmatter record property inside route. This means you can add custom flags (like featured: true or readingTime: '5 min') and consume them safely in your custom components.

Last updated on July 27, 2026

Was this page helpful?