# 快速开始

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

## 前置条件

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

## 安装

```sh
pnpm add @minidocs/astro @astrojs/mdx
```

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

```js
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` 选项，表示页面放在哪个路径下：

```ts
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`：

```mdx
---
title: 你好
---

这是第一篇文档。
```

每个页面有一个 id，就是去掉扩展名的文件路径。这两个页面的 id 是 `index` 和 `hello`，地址是 `/docs/` 和 `/docs/hello/`。id 和地址的规则见[页面与地址](/docs/concepts/pages-and-urls.md)。

## 定义文档树

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

```ts
// 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 是类型错误。更多写法见[文档树](/docs/concepts/document-tree.md)。

## 渲染页面

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

```astro
---
// 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)` 把页面的标题整理成本页目录

```astro
---
// 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/`，就能看到一个完整的文档站：左边是侧边栏，中间是正文，右边是本页目录，底部是上一页和下一页，窄屏下侧边栏会变成抽屉。

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

- [页面布局](/docs/guides/layout.md)：每个布局组件做什么，不同屏幕宽度下的样子
- [侧边栏](/docs/guides/sidebar.md)：分组的几种样子
- [样式](/docs/guides/styling.md)：换颜色、只用一部分默认样式、配合 Tailwind
