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 Start
Creating a new collection takes only two steps:
1. Create a bracketed folder
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 Frontmatter
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 Works
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:
- 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. - List Override (
list.tsx): If present, overrides the index listing page at/blog. Receives the list of all posts in the collection. - Post Override (
post.tsx): If present, overrides the wrapper layout for individual posts (e.g./blog/first-post). - 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 Views
To override the default look of your collections, you can drop standard React components directly into the collection folder.
Custom Post Component (post.tsx)
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">·</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>
)
}
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 Reference
Loader Data Structures
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:
CollectionPostLoaderData
Returned by React Router loaders for collection post routes. Use usePost() instead of accessing this directly.
| Property | Type | Default | Description |
|---|---|---|---|
route | ComponentRoute | — | Detailed route metadata extracted from the post's frontmatter. |
collection | string | — | The name of the collection this post belongs to (e.g. 'blog'). |
headings | Heading[] | [] | Extracted page headers for generating dynamic table of contents (TOC). |
CollectionListLoaderData
Returned by React Router loaders for collection list indexes (e.g. /blog). Use usePosts() instead of accessing this directly.
| Property | Type | Default | Description |
|---|---|---|---|
posts | CollectionPostItem[] | [] | Paginated array of all posts in the collection. |
totalPages | number | 1 | Total number of pages based on post count. |
currentPage | number | 1 | Current page number (1-based index). |
collection | string | — | The name of the collection identifier. |
Hooks
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 })
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()
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)
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)
// ...
}
ComponentRoute
The type mapping metadata extracted from the frontmatter of your files.
| Property | Type | Default | Description |
|---|---|---|---|
title | string | '' | Title of the post, pulled from frontmatter title. |
date | string | Date | undefined | Date of publication, pulled from frontmatter date. |
author | string | AuthorObject | undefined | Details 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. |
excerpt | string | '' | Short description or excerpt. |
lastUpdated | string | number | undefined | The timestamp of the last git commit (or manual override). |
frontmatter | Record<string, any> | {} | Extensible object with all custom frontmatter parameters. |
Advanced Customization
Custom List (list.tsx)
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:
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)
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:
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>
)
}
Pagination
By default, each page displays 10 posts, but you can configure this globally via collections.postsPerPage inside your boltdocs.config.ts:
export default defineConfig({
collections: {
postsPerPage: 12, // Customize items per page
},
})
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.