minidocs

Getting Started

This page starts from an Astro project and builds a docs site with a sidebar, a table of contents and previous and next links.

Prerequisites

  • Node.js 22.22 or later
  • An Astro 7 project

Install

pnpm add @minidocs/astro @astrojs/mdx

Add both integrations to astro.config.mjs. @astrojs/mdx lets you write docs in MDX, and minidocs handles links and callouts in your content:

import mdx from '@astrojs/mdx';
import { defineConfig } from 'astro/config';
import minidocs from '@minidocs/astro';

export default defineConfig({
  integrations: [mdx(), minidocs()],
});

Define a collection

Define the collection in src/content.config.ts, importing glob from @minidocs/astro/loaders. It is Astro’s glob with one more option, prefix, the path the pages live under:

import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from '@minidocs/astro/loaders';

const docs = defineCollection({
  loader: glob({ base: './src/content/docs', pattern: '**/*.{md,mdx}', prefix: 'docs' }),
  schema: z.object({
    title: z.string(),
  }),
});

export const collections = { docs };

minidocs only needs a title in the schema, used as the label in the sidebar. Every other field is up to you.

Write your first page

Create index.mdx and hello.mdx in src/content/docs/:

---
title: Hello
---

This is the first page.

Every page has an id, which is its file path without the extension. These two pages are index and hello, at /docs/ and /docs/hello/. See Pages and URLs for how ids and URLs are made.

Define the tree

Pages in a collection have no order. defineTree arranges them into a tree, which the sidebar and the previous and next links follow:

// src/lib/docs.ts
import { getCollection } from 'astro:content';
import { defineTree, listPages } from '@minidocs/astro/runtime';

export const entries = await getCollection('docs');

export const tree = await defineTree(entries, ({ page, dir }) => [page('index'), ...dir('')]);

export const pages = listPages(entries);

dir('') lists the pages in the root directory except index. listPages lists every page to generate, which the route in the next step uses. Arguments of page() and dir() are autocompleted, and a wrong id is a type error. See The Document Tree for more.

Render pages

Put one route in the folder prefix names to render every page of the collection. Each page in pages carries its route parameters as page.params, ready for getStaticPaths. The headings from render() are the headings of the page, for the table of contents:

---
// src/pages/docs/[...slug].astro
import { render } from 'astro:content';
import Docs from '../../layouts/Docs.astro';
import { pages } from '../../lib/docs';

export async function getStaticPaths() {
  return pages.map((page) => ({ params: page.params, props: page }));
}

const { Content, headings } = await render(Astro.props.entry);
---

<Docs {...Astro.props} headings={headings}>
  <Content />
</Docs>

Build the page

Last comes the page layout. Layout, Sidebar, Toc, Pager and Prose are minidocs components, and importing @minidocs/astro/styles.css gives them the full default look. The top of the layout computes the data the components need:

  • navState(tree, id) records where the current page sits in the tree, and nav.of(node) gives each item its link and whether it is the current page
  • neighbors(tree, id) finds the previous and next pages
  • tocTree(headings) arranges the headings of the page into a table of contents
---
// src/layouts/Docs.astro
import { Layout, Pager, Prose, Sidebar, Toc } from '@minidocs/astro/components';
import { navState, neighbors, tocTree, toHref } from '@minidocs/astro/runtime';
import '@minidocs/astro/styles.css';
import { tree } from '../lib/docs';

const { id, entry, headings } = Astro.props;
const nav = navState(tree, id);
const toc = tocTree(headings);
const { prev, next } = neighbors(tree, id);
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{entry.data.title}</title>
  </head>
  <body>
    <Layout.Root>
      <Layout.Header>
        <Layout.Title href={toHref('docs', 'index')}>My docs</Layout.Title>
      </Layout.Header>

      <Sidebar.Trigger target="sidebar" aria-label="Menu">
        <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
          <path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
        </svg>
      </Sidebar.Trigger>

      <Layout.Sidebar>
        <Sidebar.Root id="sidebar" label="Docs">
          <Sidebar.List>
            {tree.map(function item(node) {
              return (
                <Sidebar.Item>
                  {node.children.length > 0 ? (
                    <Sidebar.Group {...nav.of(node)}>
                      <Fragment slot="label">{node.label}</Fragment>
                      <Sidebar.List>{node.children.map(item)}</Sidebar.List>
                    </Sidebar.Group>
                  ) : (
                    <Sidebar.Link {...nav.of(node)}>{node.label}</Sidebar.Link>
                  )}
                </Sidebar.Item>
              );
            })}
          </Sidebar.List>
        </Sidebar.Root>
      </Layout.Sidebar>

      <Layout.Main>
        <Prose>
          <h1>{entry.data.title}</h1>
          <slot />
        </Prose>
        {(prev || next) && (
          <Pager.Root label="Pagination">
            {prev && (
              <Pager.Link rel="prev" href={nav.of(prev).href}>
                <Pager.Label>Previous</Pager.Label>
                <Pager.Title>{prev.label}</Pager.Title>
              </Pager.Link>
            )}
            {next && (
              <Pager.Link rel="next" href={nav.of(next).href}>
                <Pager.Label>Next</Pager.Label>
                <Pager.Title>{next.label}</Pager.Title>
              </Pager.Link>
            )}
          </Pager.Root>
        )}
      </Layout.Main>

      {toc.length > 0 && (
        <Layout.Aside title="On this page">
          <Toc.Root label="On this page">
            <Toc.List>
              {toc.map(function item(heading) {
                return (
                  <Toc.Item>
                    <Toc.Link slug={heading.slug}>{heading.text}</Toc.Link>
                    {heading.children.length > 0 && <Toc.List>{heading.children.map(item)}</Toc.List>}
                  </Toc.Item>
                );
              })}
            </Toc.List>
          </Toc.Root>
        </Layout.Aside>
      )}
    </Layout.Root>
  </body>
</html>

Run pnpm dev and open /docs/ to see a complete docs site: the sidebar on the left, the content in the middle, the table of contents on the right, previous and next links at the bottom, and a drawer for the sidebar on narrow screens.

This layout file is your code, so its structure, order and looks are all yours to change. Next:

  • Page Layout: what each layout component does, and how the page looks at different widths
  • Sidebar: the kinds of groups
  • Styling: colors, using part of the default styles, and Tailwind
On this page
Search in
Type to start searching