首页
/ Supabase 文档站(apps/docs)架构解析:双管道内容管线、路由模型与 Markdown 导出机制

Supabase 文档站(apps/docs)架构解析:双管道内容管线、路由模型与 Markdown 导出机制

2026-09-06 13:59:41作者:鲍丁臣Ursa

本文以 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 的共享包(uicommonui-patterns 等)。next.config.mjstranspilePackages 字段明确列出了这些跨包依赖:

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>.mdAccept: 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 与架构地图文档中的列表已有出入:代码中多了 graphqlobservability,而 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.jsoncodegen: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 ?? [])])
)

几个关键机制:

  1. getGuidesMarkdown()GuidesMdx.utils.tsx)负责读取文件、校验 frontmatter(isValidGuideFrontmatter)、检查导航启用状态(checkGuidePageEnabled),最后返回供 GuideTemplate 使用的数据。它还包了一层 cache_fullProcess_withDevCacheBust 进程级缓存——注释解释:markdown 内容被「烘焙」进每次部署,不会变化,且只读公开的 MDX 文件,无敏感信息。
  2. slug 语义slug 只是 Next.js 的参数名;[[...slug]] 是可选 catch-all(可匹配零个或多个路径段),[...slug] 是必填。页面处理器先补上分区名,再把各段拼成磁盘上的内容路径。
  3. 安全边界getGuidesMarkdownInternal 在读取前做防御性检查——拒绝任何落在 GUIDES_DIRECTORY 之外或不属于 PUBLISHED_SECTIONS 白名单的路径,直接 notFound()。这是对路径穿越类问题的显式防护。
  4. Reference 侧Reference.utils.ts 中的 parseReferencePath(slug) 负责解释 javascriptv2auth-signin 这类分段,据此挑选 SDK、版本与章节。
  5. 静态参数生成genGuidesStaticParams 递归读取 content/guides/<section>/ 下所有 .mdx(过滤掉 _ 前缀的私有文件),并且同样会过滤掉导航配置中禁用的页面,输出静态生成参数列表。

五、双管道架构:同一份内容渲染两次

这是整个架构中「承重」的设计。同一份 MDX 内容必须在两条管道上产出:

  1. MDX 运行时——React 组件渲染 <MyComponent id="..." />,通过数据注册表(data registry)读取数据,输出 HTML;
  2. 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(如 storageGetStartedauthPricingrealtimeExamples),聚合进 ALL_GROUPS 并提供按 ID 查询的入口——正是文档所说的「ID 键值映射 + getById」模式。

新增一个带 markdown 表示的组件:四步流程

  1. apps/docs/components/ 下编写 React 组件;
  2. apps/docs/internals/markdown-schema/<同名>.ts 下添加处理器;
  3. generate-guides-markdown.tsSCHEMA 对象中注册该处理器;
  4. 如果组件是纯视觉的、应从 markdown 中丢弃,则不提供处理器——generate-guides-markdown.ts 会自动把未知 JSX 展开(unwrap)为其子节点。

第 3、4 步在源码中得到直接印证:SCHEMA 是一个 ComponentSchema 对象,列出了 AccordionItemAdmonitionAgentSetupContentListingsImagePrice 等处理器映射;applySchema() 的注释明确写道「Any component not listed is unwrapped (children are kept, wrapper is dropped)」。

六、Markdown 导出流水线:30 秒速览

generate-guides-markdown.ts 遍历 content/guides/**/*.mdx,完整步骤如下:

  1. 解析 MDX → mdast(使用 mdx + gfm 扩展——源码中即 fromMarkdown 配合 micromark-extension-mdxjsmicromark-extension-gfm);
  2. 递归内联 <$Partial path="..." />(partials 目录为 content/_partials,源码顶部有 PARTIALS_DIR 常量,并支持 parsePartialVariables 做变量替换);
  3. addBaseUrlPrefix(tree)——为内部链接加上 /docs/ 前缀(实现在 internals/internal-links.ts);
  4. applySchema(tree, SCHEMA)——自底向上:先序列化子节点,再逐个把 JSX 节点替换为其处理器的返回值(或展开未知节点);
  5. 把 mdast 序列化回 markdown(toMarkdown + gfm/mdx 序列化器);
  6. 在头部拼上 front-matter 派生的头信息(# title、subtitle、description);
  7. 写入 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/telemetrylib/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.jsonprebuild 钩子),一次完整构建的先后关系是:codegen:graphqlcodegen:referencescodegen:examples(从仓库根拷贝 examples/)→ build:federated-contentbuild:markdown(guides + reference 两套 markdown 导出)→ build:gz-archive

十、Providers:应用层的装配点

features/app.providers.tsx 为整个应用包裹以下 provider:

  • QueryClientProvider(React Query)
  • 来自 commonFeatureFlagProviderThemeProvider
  • 来自 uiTooltipProvider
  • 来自 dev-toolsDevToolbar
  • 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 同步」相互印证。

十二、已知集成与边界

  • Studioapps/studio)会链接到 docs URL——变更 URL 形态时要检查链接一致性;
  • wwwapps/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 和人提供「东西在哪、谁依赖谁」的快速索引,它自身也强调路径会漂移、动手前先核实。实践上建议:

  1. 找 guides 相关内容:先看 content/guides/,渲染问题查 features/docs/GuidesMdx.*,markdown 导出问题查 internals/generate-guides-markdown.tsinternals/markdown-schema/
  2. 新增 MDX 组件时严格按「组件 + 处理器 + SCHEMA 注册(或故意不注册)」三步走,保持双管道一致;
  3. 验证链路用本文第九节的命令表,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 面)。

登录后查看全文
热门项目推荐
相关项目推荐