Supabase 文档站(apps/docs)架构解析:双管道内容管线、路由模型与 Markdown 导出机制
本文以 Supabase monorepo 中 apps/docs 的架构地图文档(app-map.md)为核心,系统讲解这套文档站的目录组织、路由模型、MDX 渲染与 Markdown 导出「双管道」架构。读完后你将掌握:如何在 apps/docs 中定位任意功能的源码位置、如何理解 guides 页面的 slug 路由与静态生成逻辑,以及新增一个同时支持 Web 渲染和 Markdown 导出的 MDX 组件需要走的完整链路。
一、高层架构:一个站点,多种内容来源
apps/docs 是一个 Next.js 15 App Router 站点,部署在 /docs 路径下(由 next.config.mjs 中的 basePath 配置控制:basePath: process.env.NEXT_PUBLIC_BASE_PATH || '/docs')。它把四类内容统一渲染在一个站点里:
- 手写的 MDX 指南(guides)
- 从
spec/机器生成的 API/SDK 参考文档(reference) - 故障排查内容(troubleshooting,部分从 GitHub issues 同步而来)
- 构建时从外部仓库联邦拉取的内容(federated content)
同时它复用了 monorepo 的共享包(ui、common、ui-patterns 等)。next.config.mjs 中 transpilePackages 字段明确列出了这些跨包依赖:
transpilePackages: [
'ui', 'ui-patterns', 'common', 'dayjs',
'shared-data', 'api-types', 'icons', 'next-mdx-remote',
],
架构地图文档给出的整体依赖关系(路由层 → 内容源 → 渲染层)如下:
flowchart TB
subgraph routes ["app/ — Next.js routes"]
guides["/guides/*"]
reference["/reference/*"]
api["/api/*"]
end
subgraph content ["Content sources"]
mdx["content/guides/*.mdx"]
trouble["content/troubleshooting/*.mdx"]
spec["spec/*.yml, *.json"]
generated["features/docs/generated/**"]
refmdx["docs/ref/*.mdx"]
fed["External repos<br/>(federated)"]
end
subgraph render ["Rendering layer"]
features["features/docs/"]
components["components/"]
layouts["layouts/"]
end
guides --> mdx
guides --> trouble
guides --> fed
reference --> spec
reference --> generated
reference --> refmdx
guides --> features
reference --> features
features --> components
features --> layouts
这张图的核心信息是:app/ 路由层保持极薄,真正的渲染逻辑全部下沉到 features/docs/。
二、顶层目录布局:什么东西放在哪里
架构地图文档给出了如下目录职责表,结合当前仓库实际结构逐一验证(均已确认存在):
| 路径 | 职责 | 说明 |
|---|---|---|
apps/docs/app/ |
Next.js App Router 路由文件 | 薄路由,按分区使用 slug 风格的 catch-all,如 guides/auth/[[...slug]]/page.tsx |
apps/docs/content/guides/ |
/docs/guides/... 页面的 MDX 源文件 |
一页一文件;content/_partials/ 存放共享片段 |
apps/docs/content/troubleshooting/ |
故障排查文章 | 部分由 Troubleshooting.script.mjs 从 GitHub issues 同步 |
apps/docs/components/ |
MDX 内部使用的 React 组件 | 每个组件或组件家族一个文件夹 |
apps/docs/data/ |
组件消费的带类型数据模块 | 数据文件以 .data.ts 为后缀;查询辅助函数放在 .utils.ts 中,而不是这里 |
apps/docs/lib/ |
各管道共享的纯库代码 | zod schema、.utils.ts 辅助函数、测试 |
apps/docs/features/docs/ |
页面骨架与 MDX 渲染器(MdxBase) |
共享的 <Heading> 位于 MdxBase.shared.tsx |
apps/docs/features/ |
领域逻辑:文档渲染、auth、搜索/命令菜单、遥测、应用 providers | app.providers.tsx 装配 React Query、主题、dev toolbar、命令菜单 |
apps/docs/spec/ |
参考文档生成的 source-of-truth | OpenAPI、SDK YAML、CLI 配置 |
apps/docs/generator/ |
参考文档的 codegen 模板 | |
apps/docs/resources/ |
GraphQL 端点(/api/graphql):按查询划分 schema、model、resolver |
详见 graphql-endpoint.md |
apps/docs/internals/ |
构建时 markdown 生成 | 禁止在运行时/客户端代码中 import |
apps/docs/internals/markdown-schema/ |
逐组件处理器:JSX → markdown 字符串 | 文件名与组件名一致 |
apps/docs/public/markdown/guides/ |
生成的 .md 输出,经 /docs/guides/<path>.md 或 Accept: text/markdown 提供 |
由 generate-guides-markdown.ts 构建;构建产物目录,见下文 |
apps/docs/public/markdown/reference/ |
生成的参考 .md 文件 |
由 generate-reference-markdown.ts 构建 |
apps/docs/middleware.ts |
guides 的内容协商;reference 深链的 bot 重写 | 复用 markdown-negotiation.ts |
apps/docs/app/api/guides-md/ |
向 Agent 提供预生成的 guide markdown | 从 /docs/guides/<path>.md rewrite 而来 |
apps/docs/examples/ |
构建时从仓库根目录 examples/ 拷贝 |
codegen:examples 脚本完成 |
apps/docs/scripts/ |
构建时脚本(sitemap、markdown 导出、embeddings) |
需要说明的一点:从源码结构看,public/markdown/ 目录在当前仓库中仅提交了 manifest.json(gitignore 了生成产物),实际的 guides/、reference/ 下的 .md 文件是构建时由 build:guides-markdown / build:reference-markdown 生成的。generate-guides-markdown.ts 头部注释也确认了这一点:manifest「位于 gitignored 的 public/markdown/ 输出目录之下,由 build:guides-markdown 重新生成,turbo 在 build/typecheck/lint 之前执行它」。
已发布的 guides 分区(每区有自己的 layout.tsx 用于侧边栏导航),以 GuidesMdx.utils.tsx 中的 PUBLISHED_SECTIONS 常量为准:
const PUBLISHED_SECTIONS = [
'ai', 'api', 'auth', 'cron', 'database', 'deployment', 'functions',
'getting-started', 'graphql', 'integrations', 'local-development',
'observability', 'platform', 'queues', 'realtime', 'resources',
'security', 'self-hosting', 'storage',
] as const
注意文档开头特意警告「动手前先对照实时目录树核实——路径会漂移(Verify against the live tree before acting — paths drift)」。实际上当前仓库的 PUBLISHED_SECTIONS 与架构地图文档中的列表已有出入:代码中多了 graphql、observability,而 telemetry 不再出现在该白名单里。这正是文档所提醒的路径漂移实例——以 PUBLISHED_SECTIONS 为准即可。
三、内容类型:四种内容的分工
架构地图文档将站点内容分为四类:
| 类型 | 位置 | 说明 |
|---|---|---|
| Guides / 教程 | content/guides/ |
手写 MDX,面向目标场景 |
| Troubleshooting | content/troubleshooting/ |
部分从 GitHub issues 同步 |
| Reference | 由 spec/ 生成到 features/docs/generated/** |
Spec 驱动(OpenAPI、SDKSpec、ConfigSpec、CLISpec)。参考页不走标准 MDX 路径 |
| Federated | 构建时从外部仓库拉取 | 经 GitHub App 拉取,见 federated-docs.md |
Reference 类型不走 MDX 管道是有意的架构决策,动机在 docs-app-direction.md 中有专门论述;Management API 的 OpenAPI 路径则见 management-api-reference.md。生成入口在 package.json 的 codegen:references 系列脚本中(legacy + new 两套,new 管线还包含 spec 目录的 make download.tsdoc.v2 等 SDK 规范下载步骤)。
四、路由模型:薄路由 + features 渲染
核心原则:路由保持薄,渲染逻辑住在 features/docs/ 里。以 getting-started 分区为例,实际路由文件 page.tsx 与文档给出的代码一致:
// app/guides/getting-started/[[...slug]]/page.tsx
const slug = ['getting-started', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
配套的导出项同样值得注意:
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('getting-started') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['getting-started', ...(params.slug ?? [])])
)
几个关键机制:
getGuidesMarkdown()(GuidesMdx.utils.tsx)负责读取文件、校验 frontmatter(isValidGuideFrontmatter)、检查导航启用状态(checkGuidePageEnabled),最后返回供GuideTemplate使用的数据。它还包了一层cache_fullProcess_withDevCacheBust进程级缓存——注释解释:markdown 内容被「烘焙」进每次部署,不会变化,且只读公开的 MDX 文件,无敏感信息。- slug 语义:
slug只是 Next.js 的参数名;[[...slug]]是可选 catch-all(可匹配零个或多个路径段),[...slug]是必填。页面处理器先补上分区名,再把各段拼成磁盘上的内容路径。 - 安全边界:
getGuidesMarkdownInternal在读取前做防御性检查——拒绝任何落在GUIDES_DIRECTORY之外或不属于PUBLISHED_SECTIONS白名单的路径,直接notFound()。这是对路径穿越类问题的显式防护。 - Reference 侧:Reference.utils.ts 中的
parseReferencePath(slug)负责解释javascript、v2、auth-signin这类分段,据此挑选 SDK、版本与章节。 - 静态参数生成:
genGuidesStaticParams递归读取content/guides/<section>/下所有.mdx(过滤掉_前缀的私有文件),并且同样会过滤掉导航配置中禁用的页面,输出静态生成参数列表。
五、双管道架构:同一份内容渲染两次
这是整个架构中「承重」的设计。同一份 MDX 内容必须在两条管道上产出:
- MDX 运行时——React 组件渲染
<MyComponent id="..." />,通过数据注册表(data registry)读取数据,输出 HTML; - Markdown 导出——
internals/markdown-schema/<MyComponent>.ts处理器把同一份 JSX 转成纯 markdown,写入public/markdown/guides/。
承重规则:两条管道必须解引用同一个数据注册表(通常是 apps/docs/data/<topic>/index.ts 导出的 ID 键值映射和 getById 查询函数)。组件读它,处理器也读它,JSX 的 prop 只是一个 id。这样两个输出天然保持同步,无需维护平行的数据结构。
文档给出生产环境中的参考实现 ContentListings,当前仓库中四层文件全部存在:
| 层 | 路径 |
|---|---|
| 数据注册表 | data/content-listings/(每主题一个 .data.ts + index.ts) |
| 运行时组件 | components/ContentListings/ |
| Markdown 处理器 | internals/markdown-schema/ContentListings.ts |
| MDX 注册 | features/docs/MdxBase.shared.tsx |
MDX 中的用法是 <ContentListings id="storage-get-started" />。数据侧的实际形态(index.ts)是:各主题的 .data.ts 导出若干 ContentListingGroup(如 storageGetStarted、authPricing、realtimeExamples),聚合进 ALL_GROUPS 并提供按 ID 查询的入口——正是文档所说的「ID 键值映射 + getById」模式。
新增一个带 markdown 表示的组件:四步流程
- 在
apps/docs/components/下编写 React 组件; - 在
apps/docs/internals/markdown-schema/<同名>.ts下添加处理器; - 在 generate-guides-markdown.ts 的
SCHEMA对象中注册该处理器; - 如果组件是纯视觉的、应从 markdown 中丢弃,则不提供处理器——
generate-guides-markdown.ts会自动把未知 JSX 展开(unwrap)为其子节点。
第 3、4 步在源码中得到直接印证:SCHEMA 是一个 ComponentSchema 对象,列出了 AccordionItem、Admonition、AgentSetup、ContentListings、Image、Price 等处理器映射;applySchema() 的注释明确写道「Any component not listed is unwrapped (children are kept, wrapper is dropped)」。
六、Markdown 导出流水线:30 秒速览
generate-guides-markdown.ts 遍历 content/guides/**/*.mdx,完整步骤如下:
- 解析 MDX → mdast(使用 mdx + gfm 扩展——源码中即
fromMarkdown配合micromark-extension-mdxjs与micromark-extension-gfm); - 递归内联
<$Partial path="..." />(partials 目录为content/_partials,源码顶部有PARTIALS_DIR常量,并支持parsePartialVariables做变量替换); addBaseUrlPrefix(tree)——为内部链接加上/docs/前缀(实现在 internals/internal-links.ts);applySchema(tree, SCHEMA)——自底向上:先序列化子节点,再逐个把 JSX 节点替换为其处理器的返回值(或展开未知节点);- 把 mdast 序列化回 markdown(
toMarkdown+ gfm/mdx 序列化器); - 在头部拼上 front-matter 派生的头信息(
# title、subtitle、description); - 写入
public/markdown/guides/<同路径>.md。
applySchema 的实现细节也值得注意(这决定了为什么处理器返回的字符串会原样落入最终输出):
function applySchema(parent: Parent, schema: ComponentSchema): void {
for (const child of parent.children as Content[]) {
if ('children' in child) applySchema(child as Parent, schema)
}
const next: Content[] = []
for (const child of parent.children as Content[]) {
// 跳过 mdxFlowExpression / mdxTextExpression / mdxjsEsm
if (isJsx(child)) {
const handler = schema[child.name ?? ''] ?? defaultHandler
const children = serializeMdx({ type: 'root', children: child.children as Root['children'] }).trim()
const value = handler({ props: propsFrom(child), children, node: child })
next.push({ type: 'html', value }) // html 节点被 to-markdown 原样透传
continue
}
next.push(child)
}
parent.children = next as Parent['children']
}
每个 SCHEMA 条目接收 { props, children, node },返回用于替换的 markdown 字符串。以 markdown-schema/ContentListings.ts 为例:它把内容列表组序列化为「可选标题(按 headingLevel 映射 h2/h3/h4)+ 描述 + 逐项链接(外部链接原样、内部链接经 withDocsBasePath 加前缀)」。
七、<Heading> 与散文排版契约
- MdxBase.shared.tsx 中的
<Heading>是 MDX 的规范标题组件,负责 level→标签的映射与锚点 ID。 - MDX wrapper 对
.prose内的一切应用排版样式(guide 页默认);在.not-prose块内排版是「选入」的。 - 新组件含标题时的模式:把
<Heading>渲染在not-prose之外,把结构化布局渲染在not-prose之内。标题继承 prose 样式;布局组件自管类名。
八、遥测(Telemetry)约定
- 事件名集中定义在 packages/common/telemetry-constants.ts(snake_case,docs 事件使用
docs_前缀); - 组件通过
~/lib/telemetry(lib/telemetry.ts)中的useSendTelemetryEvent()发事件; - 属性(properties)偏好扁平的、带 ID 前缀的键;除非属性在 schema 中确实可选,否则避免可选展开技巧。
九、Lint 与验证入口点
完整 CI 面见 ci-and-lint.md。本地命令(均已对照 package.json 确认):
| 工具 | 执行位置 | 捕获什么 |
|---|---|---|
pnpm test:local:unwatch <path>(即 vitest --run) |
apps/docs 内 |
lib/ 与 data/ schema 的 Vitest 套件;需先本地起 Supabase + 重置 DB,见 apps/docs/AGENTS.md |
pnpm format |
仓库根 | Prettier——开 PR 前运行 |
pnpm lint --filter=docs |
仓库根 | ESLint 覆盖 apps/docs |
pnpm typecheck |
仓库根 | 跨包 TS 类型检查 |
pnpm build --filter=docs |
仓库根 | 包含 markdown 生成;此处失败会阻塞发布 |
pnpm lint:mdx |
apps/docs 内 |
MDX 内容 lint(覆盖整个 content/ 树),实为 supa-mdx-lint content --config ../../supa-mdx-lint.config.toml |
Typos 检查(.github/workflows/avoid-typos.yml) |
仅 CI | runner / misspell 任务,error 级别——无本地命令,合并前修掉被标记的词 |
文档同时提醒:添加自定义 lint 任务之前,先检查现有任务能否吸收这个检查(见 adding-features.md 的「Reuse pipelines」一节)。
从构建顺序看(package.json 的 prebuild 钩子),一次完整构建的先后关系是:codegen:graphql → codegen:references → codegen:examples(从仓库根拷贝 examples/)→ build:federated-content → build:markdown(guides + reference 两套 markdown 导出)→ build:gz-archive。
十、Providers:应用层的装配点
features/app.providers.tsx 为整个应用包裹以下 provider:
QueryClientProvider(React Query)- 来自
common的FeatureFlagProvider、ThemeProvider - 来自
ui的TooltipProvider - 来自
dev-tools的DevToolbar DocsCommandProvider/DocsCommandMenu- 来自
layouts/的SiteLayout
十一、Troubleshooting 页面子树
- Troubleshooting.page.tsx——入口级页面渲染器;
- Troubleshooting.utils.ts——TS 工具函数;
- Troubleshooting.utils.common.mjs——之所以是
.mjs,是因为它同时被 Next.js 构建和一个 Node 同步脚本消费,后者的 import 解析行为有特殊性。未确认同步脚本兼容之前不要改成.ts; TroubleshootingSchema中的topics字段(Topics enum)是产品标签值的 source of truth。
当前仓库中该子树实际文件比文档列出更多:另有 Troubleshooting.script.mjs(issues 同步脚本)、Troubleshooting.ui.tsx / Troubleshooting.ui.client.tsx(UI 层)和 TroubleshootingSection.page.tsx(分区页),与文档描述的「部分从 GitHub issues 同步」相互印证。
十二、已知集成与边界
- Studio(
apps/studio)会链接到 docs URL——变更 URL 形态时要检查链接一致性; - www(
apps/www)有时会嵌入 docs 分区内容。此外 next.config.mjs 注释明确:正式环境的 doc 重写与跳转由apps/www的 Next 配置(apps/www/lib/redirects.js)统一处理,apps/docs只放 dev/preview 专用的跳转; - PostHog 接收
docs_*事件用于分析; docs-agent-skills独立仓库承载批量审计/转换 skills,驱动多 PR 的文档迁移;- 联邦上游仓库——完整列表(pg_graphql、vecs、wrappers、terraform-provider、setup-cli、splinter、agent-skills)见 federated-docs.md。
十三、结语:把架构地图当索引用
这份架构地图的定位是给 Agent 和人提供「东西在哪、谁依赖谁」的快速索引,它自身也强调路径会漂移、动手前先核实。实践上建议:
- 找 guides 相关内容:先看
content/guides/,渲染问题查features/docs/GuidesMdx.*,markdown 导出问题查internals/generate-guides-markdown.ts与internals/markdown-schema/; - 新增 MDX 组件时严格按「组件 + 处理器 + SCHEMA 注册(或故意不注册)」三步走,保持双管道一致;
- 验证链路用本文第九节的命令表,
pnpm build --filter=docs是包含 markdown 生成的最终防线。
配套深度文档(与本架构地图同目录):llm-agent-surface.md(Agent/LLM 如何消费导出的 markdown:协商、批量下载、llms.txt)、build-pipeline.md 相关的构建流程(Turbo + pnpm lifecycle)、graphql-endpoint.md(/api/graphql 的 schema/model/resolver 划分)与 ci-and-lint.md(完整 CI 面)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00