minidocs

快速开始

这一页从一个 Astro 项目开始,搭出一个带侧边栏、本页目录和翻页的文档站。

前置条件

  • Node.js 22.22 或更高版本
  • 一个 Astro 7 项目

安装

pnpm add @minidocs/astro @astrojs/mdx

在 astro.config.mjs 里加上两个 integration。@astrojs/mdx 让你可以用 MDX 写文档,minidocs 处理正文里的链接和提示框:

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

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

定义内容集合

在 src/content.config.ts 里定义集合,glob 从 @minidocs/astro/loaders 导入。它就是 Astro 的 glob,多了一个 prefix 选项,表示页面放在哪个路径下:

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 只要求 schema 里有 title,用作侧边栏里的标题。其他字段随你定义。

写第一篇文档

在 src/content/docs/ 下新建 index.mdx 和 hello.mdx:

---
title: 你好
---

这是第一篇文档。

每个页面有一个 id,就是去掉扩展名的文件路径。这两个页面的 id 是 index 和 hello,地址是 /docs/ 和 /docs/hello/。id 和地址的规则见页面与地址。

定义文档树

集合里的页面没有顺序。用 defineTree 把它们组织成一棵树,侧边栏和翻页都按这棵树来:

// 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('') 列出根目录下除 index 以外的页面。listPages 列出所有要生成的页面,下一步的路由要用到它。page() 和 dir() 的参数都有补全,写错 id 是类型错误。更多写法见文档树。

渲染页面

在 prefix 对应的文件夹里放一个路由,渲染这个集合的所有页面。pages 里每个页面都带着路由参数 page.params,直接交给 getStaticPaths。render() 返回的 headings 是页面里的标题,留给本页目录用:

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

搭出页面

最后写页面的布局。Layout、Sidebar、Toc、Pager 和 Prose 都是 minidocs 的组件,引入 @minidocs/astro/styles.css 就有完整的默认外观。布局开头算出组件要用的数据:

  • navState(tree, id) 记下当前页面在文档树里的位置,nav.of(node) 给出每一项的链接和是否是当前页面
  • neighbors(tree, id) 找出上一页和下一页
  • tocTree(headings) 把页面的标题整理成本页目录
---
// 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="zh">
  <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')}>我的文档</Layout.Title>
      </Layout.Header>

      <Sidebar.Trigger target="sidebar" aria-label="菜单">
        <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="文档">
          <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="翻页">
            {prev && (
              <Pager.Link rel="prev" href={nav.of(prev).href}>
                <Pager.Label>上一页</Pager.Label>
                <Pager.Title>{prev.label}</Pager.Title>
              </Pager.Link>
            )}
            {next && (
              <Pager.Link rel="next" href={nav.of(next).href}>
                <Pager.Label>下一页</Pager.Label>
                <Pager.Title>{next.label}</Pager.Title>
              </Pager.Link>
            )}
          </Pager.Root>
        )}
      </Layout.Main>

      {toc.length > 0 && (
        <Layout.Aside title="本页目录">
          <Toc.Root label="本页目录">
            <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>

运行 pnpm dev,打开 /docs/,就能看到一个完整的文档站:左边是侧边栏,中间是正文,右边是本页目录,底部是上一页和下一页,窄屏下侧边栏会变成抽屉。

这个布局文件是你的代码,结构、顺序、每一部分的样子都可以改。接下来可以看:

  • 页面布局:每个布局组件做什么,不同屏幕宽度下的样子
  • 侧边栏:分组的几种样子
  • 样式:换颜色、只用一部分默认样式、配合 Tailwind
本页目录
搜索范围
输入关键词开始搜索