Collections

Collections let you group related documents under dynamic directories with automatic route generation. By defining folder-based conventions, you can decouple standard documentation pages from custom listings, post pages, and custom layouts.

Quick StartLink

Creating a new collection only takes two steps:

1. Create a bracket folderLink

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

bash
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.

md

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 automatically generated at /blog.


How It WorksLink

Boltdocs interprets bracket directory names (e.g. [blog], [releases]) as collections.

text
docs/
└── 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 bracketed folder name (e.g. [blog]) maps to the collection id (blog) and serves as the base URL path /blog.
  2. List Override (list.tsx): If present, overrides the index listing page at /blog. It receives the list of all posts in the collection.
  3. Post Override (post.tsx): If present, overrides the container layout for individual posts (e.g. /blog/first-post).
  4. Layout Override (layout.tsx): If present, acts as the root route container layout (with its own sidebar or navigation context) for all routes within the collection.

Custom View LayoutsLink

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

Custom Post Component (post.tsx)Link

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

tsx
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>
  )
}
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 writing custom list.tsx or post.tsx files, Boltdocs provides hooks (usePosts, usePost) that handle data access internally. The data structures below are what the 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 it directly.

PropertyTypeDefaultDescription
routeComponentRoute—Detailed route metadata extracted from the post's frontmatter.
collectionstring—The name of the collection this post belongs to (e.g. 'blog').
headingsHeading[][]Extracted page headings for generating a dynamic table of contents (TOC).

CollectionListLoaderDataLink

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

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

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 none 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 listings, sidebars, or any component needing collection posts.

tsx
import { usePosts } from 'boltdocs/client'

// Defaults to the "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:

tsx
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:

tsx
// 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.

tsx
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 fetch a specific post from anywhere in your app:

tsx
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 from a collection. Defaults to collection "blog" and count of 5.

tsx
import { useRecentPosts } from 'boltdocs/client'

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

ComponentRouteLink

The metadata mapping type extracted from your files' frontmatter.

PropertyTypeDefaultDescription
titlestring''Post title, sourced from the title frontmatter.
datestring | DateundefinedPublication date, sourced from the date frontmatter.
authorstring | AuthorObjectundefinedAuthor details, supports simple strings or objects with name and avatar.
excerptstring''Short description or excerpt.
lastUpdatedstring | numberundefinedThe last git commit timestamp (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 shows 10 posts, but you can configure this globally via collections.postsPerPage in your boltdocs.config.ts:

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

All parameters written in your post's frontmatter are preserved in the frontmatter property of the route record. 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 October 18, 2026

Was this page helpful?