iii 技术规格演示文稿托管指南:两棵树布局、frontmatter 注册与一键部署
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
本指南以 skills/presentation/reference/hosting.md 为核心骨架,结合仓库内
tech-specs/、website/roadmap/的真实实现与 CI 流水线源码,系统讲解 iii 仓库如何用"单一站点承载全部规格演示文稿(deck)"的托管模型:规格(spec)只允许 Markdown、deck 是可选的 React 内容层、注册完全靠 frontmatter、构建产物可整体搬移到任意 CDN。读完你将掌握该布局的配对契约、构建产物清单、日常开发/校验/发布命令,以及如何把旧的"每 deck 一个 Vite 工程"布局安全迁移到新模型。
为什么是"一棵树,而不是每个 deck 一棵树"
iii 仓库把每一份技术规格(tech spec)做成一份可公开浏览的演示文稿——既有纯 Markdown 的正文,也可能有交互式 React 演示层。托管该层的铁律是:整个仓库的规格演示文稿只属于一个 Vite 项目(即 "base"),不存在每个 deck 单独建工程、单独安装依赖、单独部署的做法。
这一决策直接消灭了一类历史问题:在旧模型下,每个 deck 各自持有package.json、lockfile、vite/tsconfig 与_gallery/构建脚本,任何一次构建工具升级或依赖变更都要重复发生在 N 个工程里。新模型让"加一个 spec"和"加一个 deck"都只触及自己的一小块目录,PR 之间天然互不冲突。
两棵树:spec 树与 base 树
<repo>/tech-specs/<slug>/*.md # spec —— 仅 Markdown # (README.md 的 frontmatter = 注册) <base>/<slug>/ # deck 的内容层(可选) <base>/src/ # 共享组件库 + 设计令牌在 iii 仓库中,<base>就是 website/roadmap/。指针文件 tech-specs/README.md 明确写出 base 目录的位置,任何仓库的构建都能据此找到 base——这正是构建的第一阶段(文档中称为 Phase 0)定位方式。
spec 树中每一个 spec 是一个以日期为前缀的目录,例如 tech-specs/2026-07-14-worker-compose/,目录内全部是.md文件(README.md 作为索引,其余按主题拆分成多个 md)。仓库目前已有 agentic、rbac-proxy-worker、codegen、worker-compose、injectable-ui 五个 spec 目录,与之配对的是 website/roadmap/ 下同名的五个 deck 内容层目录,以及_viewer/(纯 Markdown 阅读器)和src/(共享组件库)。
配对契约:slug 三处一致
slug永远等于 spec 目录的 basename(不含日期前缀),且必须在三处严格一致:
tech-specs/<slug>/—— spec 的 Markdown 所在;<base>/<slug>/—— deck 内容层所在;- URL
/roadmap/<slug>/—— 公开访问路径。
构建时若发现"孤儿 deck"(有 deck 目录、无对应 spec 目录)会直接失败。这条规则从机制上消灭了旧模型"manifest 里写的 slug ≠ 目录名 → 404"这一类漂移 bug:目录本身就是身份(the folder is the identity),根本不需要 manifest 来对账。
仓库中的契约检查实现在 website/scripts/validate-roadmap.ts:它遍历 base 下所有含src/App.tsx的目录,逐一确认tech-specs/<同名目录>存在,否则打印✗ orphan deck并使构建失败;成功时输出✓ roadmap contracts: N spec(s), M deck(s)。
注册 = frontmatter,没有中央 manifest
一个 spec 是否出现在 roadmap 上,完全由其README.md顶部的 YAML frontmatter 决定:
--- title: the developer experience overhaul # fallback:正文第一个 H1 tagline: one file, one command, zero zombies. # fallback:正文第一段 date: 2026-06-21 # YYYY-MM-DD;fallback:目录名 # 前缀。天级精度驱动 # roadmap 排序与标签 tags: [dx, cli] # ≤ 4 status: live # 或 draft —— 卡片置灰,不进入 # index.json 与 sitemap featured: false # 置顶到落地页 feed # (index.json);roadmap 本身 # 仍按时间倒序 ---字段规则与工程化保障:
slug永远不能是 frontmatter 字段——一旦出现,构建硬错误(hard-error)。解析器在 website/roadmap/scripts/manifest.mjs 中直接用throw new Error('frontmatter declares slug — the directory name IS the slug; remove the field')终止构建。- deck 存在性是派生的,从不声明:判定标准是
<base>/<slug>/src/App.tsx是否存在(manifest.mjs的hasDeck字段),而不是任何配置项。 - 没有 frontmatter 的 spec 依然会列出:
title回退到第一个 H1,tagline回退到第一段,date回退到目录名的YYYY-MM-DD前缀;每个派生的字段构建时都会给出警告。 - 新增 spec 只触碰自己的文件夹→ 两个 spec 的 PR 永远不会冲突。
该解析器刻意做成零依赖(不引 YAML 库),只支持六个已知键(title、tagline、date、tags、status、featured),未知键会以警告形式上报。日期同时被解析出month("2026 · june" 时间线分组)与dayLabel("jun 29" 卡片标注)两个派生标签,供 roadmap 画廊渲染时间轴。
构建产物:一个站点,五种输出
以 base 目录为工作区执行node build.mjs(--only=<slug>可只构建单个 spec 做快速局部验证),产出如下:
dist/index.html # roadmap 主页 —— 单列时间线,最新 spec 在前、 # 按月分组;数据来自 virtual:spec-manifest dist/index.json # 机器可读的 spec 列表(供 iii.dev 落地页时间线 # 消费;draft 排除) dist/<slug>/index.html # deck —— 若无 deck 则输出通用 md 阅读器 # (_viewer/,构建一次后复制) dist/<slug>/<file>.md # 原始 spec Markdown,可直接链接 dist/<slug>/spec.json # (仅阅读器页面)阅读器运行时拉取的文件清单在 iii 的集成形态下,"构建"由 Astro 完成而不是独立的 build.mjs:deck 页面在 website/src/pages/roadmap/ 下作为 Astro 路由生成(index.astro、[slug]/index.astro、[slug]/[file].ts、index.json.ts、[slug]/spec.json.ts),而dist/roadmap/就是这套产物的落点。index.json.ts的实现印证了 frontmatter 规则:status === 'draft'的 spec 会被过滤掉,不进入 feed。
可移植性保证:每个 deck 都以base: './'构建、资源带独立 hash,因此dist/<slug>/可以单独搬走——放到任意 CDN、任意路径前缀,甚至直接从本地磁盘打开都能工作。这正是"其他仓库无需任何部署配置"的根基。
spec-docs glob:全布局唯一一处脆弱耦合
deck 的src/spec-docs.ts在构建期把 spec 的 Markdown 打包进 bundle,供#/spec阅读模式使用。仓库真实实现(website/roadmap/2026-06-08-agentic/src/spec-docs.ts):
export const SPEC_DOCS = import.meta.glob('../../../../tech-specs/2026-06-08-agentic/*.md', { query: '?raw', import: 'default', eager: true, }) as Record<string, string>要点:
import.meta.glob是相对导入文件解析的,因此这个字面量编码了从<base>/<slug>/src/到 spec 树的固定深度(本仓库为四层../../../../);- skill 脚手架在生成 deck 时用
__SPEC_MD_GLOB__占位符替换该路径; - 如果 base 目录将来移动,所有 deck 的 glob 会一起失配——但
pnpm type-check/build会立刻报错,属于"会炸但炸得响亮"的可控耦合。
这也是文档与website/roadmap/README.md都反复强调的"两棵树之间唯一跨树耦合",日常新增 spec/deck 时它不需要任何改动。
开发、校验、发布
pnpm dev # 单服务器:/ 为画廊,每个 /<slug>/ 一个 deck pnpm type-check # 严格模式:共享库 + 画廊 + 阅读器 + 全部 deck node build.mjs --only=<slug> # 画廊 + 单个 spec(快速局部验证) node build.mjs --strict-registry # registry 对账升级为硬失败(CI 用) pnpm build && pnpm preview # 完整站点,预览端口 :4173iii 的集成形态:deck = Astro 路由中的 React 岛
与"每个仓库一个独立 Vite 工程"的通用形态不同,iii 仓库把 deck 直接做成iii-website包的 Astro 路由:
- website/src/pages/roadmap/[slug]/index.astro 在
getStaticPaths()中枚举全部 spec,然后通过<DeckHost client:only="react" slug={spec.slug} hasDeck={spec.hasDeck} />挂载 deck; - 共享宿主 website/roadmap/src/DeckHost.tsx 用一个
import.meta.glob('../*/src/App.tsx')懒加载 + 按 deck 代码分割:页面只下载自己那个 deck 的 chunk——这正是旧"每 deck 独立 Vite 构建"曾提供的隔离性;无 deck 的 spec 则懒加载_viewer/src/ViewerApp作为通用 Markdown 阅读器; - 契约检查集中在 website/scripts/validate-roadmap.ts(孤儿 deck、COMPONENTS.md registry 对账,
--strict模式把注册漂移升级为致命错误); - 产物落在
website/dist/roadmap/,与独立构建同构(带 hash 的资源统一放在站点级/_astro/下); - base 目录本身不持有每个 deck 的
index.html、build.mjs或自己的package.json——依赖全部在 website/package.json 工作区里。
Deploy(iii 仓库):合并即发布
合并到 main 会触发 .github/workflows/deploy-website.yml,其流水线可概括为"构建 → 双轨同步 S3 → 路由映射同步 → CloudFront 失效":
pnpm --filter iii-website build一次性产出整个站点(Astro 页面 +dist/roadmap/的 decks + 生成的 llms.txt、AGENTS.md、sitemap.xml);- 通过 GitHub OIDC 获取 AWS 凭据(
aws-actions/configure-aws-credentials); - 双轨 S3 同步:带 hash 的静态资源(
/_astro/、deck 资源、字体、图片)以public,max-age=31536000,immutable长缓存同步;而内容可变文件——HTML、XML、JSON(roadmap feed、viewer 的 spec.json)、Markdown(原始 spec、AGENTS.md)、txt(robots.txt、llms.txt)以及posthog-consent.js——以public,max-age=0,must-revalidate同步,保证新部署在 CloudFront 失效后立即可见; - 把"美观 URL → .html"的路由映射同步进 CloudFront KeyValueStore(
scripts/routes-kvs.ts计算 puts/deletes 增量,--if-matchETag 乐观并发控制),让新页面无需terraform apply即可生效; - 对
/*创建 CloudFront invalidation 并写入 Job summary。
值得注意的部署语义:CloudFront 的 viewer-request 函数会把/roadmap/…/目录型 URL 重写到…/index.html,并把无尾斜杠的形式 301 到带尾斜杠的规范形态——与/blog/用的是同一套机制。全程没有 Vercel、没有手动部署、没有每 deck 独立的流水线。
Deploy(其他仓库):静态产物,自带可移植性
对 iii 之外采用本托管模型的仓库,dist/是完全静态且资源路径全部相对的:无论仓库用什么样的 CI,只要把dist/发布到任意路径前缀下即可。skill 永远不会为仓库生成部署配置——部署方式完全交给仓库自己决定。
迁移:把旧布局移植到两棵树
旧模型(每个 deck 一个独立 Vite 工程 +tech-specs/build.mjs+_gallery/)可以一次迁移一个 deck,六步完成:
- 把 spec 的 md 挪进
tech-specs/<slug>/(只放 Markdown),并按旧_gallery/src/content/presentations.ts条目里的取值补上 frontmatter 块; - 把 deck 的内容层(
index.html→ 入口./src/main.tsx,src/{App,sections,pages,content})搬进<base>/<slug>/; - 删掉重复的机制文件:
package.json、lockfile、vite/tsconfig、src/{components,hooks,lib}、index.css、markdown 工具、SpecPage; - 重写 import(共享部分改为
@lib/…,deck 本地部分改为相对路径),从content/deck.ts贯通meta/nav/footer属性,新增src/spec-docs.ts与PAGES.spec; - 真正属于单个 spec 的示意图留在
<slug>/src/diagrams/,通用示意图按 skills/presentation/reference/component-standards.md 提升到共享库; - 跑
pnpm type-check && node build.mjs --only=<slug>并在/browse验证,然后删除旧工程;当最后一个 deck 迁移完成时,删除旧的_gallery/与根级胶水脚本。
组件提升(步骤 5)在 website/roadmap/README.md 中有明确的判定门槛:必须是纯 props 驱动且不含 spec 数据、对应反复出现的规格形态(生命周期、树、时间线、扇出……)、且通过设计系统检查清单(仅用设计令牌、reduced-motion 门槛、键盘可操作、aria-label、容器查询、横向溢出滚动、无阴影/渐变、文档化 props)。提升必须同一次变更内登记进 website/roadmap/COMPONENTS.md,否则validate-roadmap.ts报unregistered component,--strict模式直接失败。
常见故障速查
| 症状 | 根因与处置 |
|---|---|
| 卡片 404 | deck 目录名 ≠ spec 目录名(配对契约被破坏),统一两边 slug 即可 |
| frontmatter 警告 | 对照上文 schema 表检查字段;pnpm build会打印具体违规规则 |
unregistered component | 走了组件提升流程 (c),需要同时补 COMPONENTS.md 条目 |
orphan deck(validate-roadmap) | spec 目录被移动/改名,deck 失去了配对;把目录名改回一致 |
至此,你可以把这份托管模型套用到任意仓库:spec 只写 Markdown、deck 随需补充、注册交给 frontmatter、发布交给仓库自己的 CI,而 iii 仓库本身就是这套模型最完整的参考实现——从 website/roadmap/scripts/manifest.mjs 的零依赖解析器,到 website/scripts/validate-roadmap.ts 的契约检查,再到 .github/workflows/deploy-website.yml 的双轨同步与边缘路由,全部可以直接对照阅读。
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考