DSH / Atlas
2026-08-06implementedprocess

The documentation site carries its own images

文档站点自带图片

`scripts/project-doc-site.ts` rewrote every repository-relative target that the publication manifest does not publish into a GitHub URL, and for an image that meant `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`. Nothing in the site build copies files: `srcDir` is the disposable `.generated` tree, VitePress sets no `publicDir` (its default, `<srcDir>/public`, is inside the tree the projector deletes

English

Problem

scripts/project-doc-site.ts rewrote every repository-relative target that the publication manifest does not publish into a GitHub URL, and for an image that meant https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>. Nothing in the site build copies files: srcDir is the disposable .generated tree, VitePress sets no publicDir (its default, <srcDir>/public, is inside the tree the projector deletes on every run), and only Markdown is written there.

That works only for a public repository. This one is private, and raw.githubusercontent.com answers 404 to an unauthenticated request — a browser session on github.com does not authenticate it either, since GitHub's own UI serves private blobs through separately signed URLs. Every image on the site was therefore broken for every reader, and no gate said so: verify-md-links and the projector check that the target file exists in the repository, which is a different question from whether a site reader can fetch it.

Decision

rewriteMarkdown takes an optional placeImage(absPath): string. When a page references an image the manifest does not publish as a page, the projector copies that file into the generated tree beside the page and rewrites the reference to ./<basename>; Vite then bundles it like any other site asset. Nothing about repository visibility can reach the published page.

The copy lands beside the page rather than in a shared asset directory. Each locale's route tree gets its own copy, so one relative URL is correct from both guide/ and en/guide/ without computing per-locale prefixes, and a page's assets are removed with the page when the manifest drops it. One map claims every projected path — pages and images alike — so a second source for one path throws, in the same spirit as the existing duplicate-route check, rather than letting whichever wrote last win.

Only a regular file whose real path stays inside the repository is copied; anything else fails the projection naming the page and the target. Link rewriting needs to know a target exists, but publication copies its bytes onto the site, so a reference escaping the repository — through ../.. or a symlink out of the tree — would put a build-machine file on a published page. The reference's ?query or #fragment rides along to the placed URL exactly as the GitHub branch has always carried it, and the file name is percent-encoded because the destination is a Markdown inline target.

docsSourceFiles() reports the placed images alongside the Markdown, so the dev server's watcher re-projects when a screenshot is replaced instead of serving the previous copy until something touches the page.

placeImage is optional because rewriteMarkdown is also called directly by its spec, where no generated tree exists. Without it the GitHub-raw fallback points at the public source home, which keeps that seam honest for a consumer that only rewrites text.

Canonical Markdown keeps writing ordinary repository-relative image paths, so the same file renders on GitHub and on the site. No document carries a site-absolute URL to satisfy VitePress.

Alternatives considered

Set publicDir outside .generated and reference site-absolute URLs. Fewer moving parts in the projector, but every image reference would then be broken when the same Markdown is read in the repository, and canonical docs are read both ways.

Host images on the assets branch, as demo GIFs already are. That branch exists to keep large binaries out of the main history, and its raw URLs have exactly the same visibility problem. It remains the right home for recordings; it does not solve this.

Wait for the repository to become public. It would fix the symptom without making the site self-contained, and the site would silently depend on GitHub's availability and rate limits for every image.

Consequences

Images in published documentation now work regardless of who is reading or whether the repository is public, and the site build has no runtime dependency on GitHub for them. The generated tree grows by one copy of each referenced image per locale — the four screenshots in the model-provider guide add roughly 270 KB per locale.

Images referenced from unpublished documents are untouched. A text-only projection resolves them against the public source home; a document that is not on the site has no site build to carry its assets.

Testing

scripts/project-doc-site.spec.ts covers the placer receiving the resolved absolute path and the returned URL landing in the Markdown, a placed reference keeping its fragment, a published page link still resolving to its route when a placer is present, and the unchanged GitHub-raw fallback when no placer is supplied. publishableImage is covered directly: a regular file inside the repository resolves, while a symlink whose target escapes it, a path outside it, and a directory are all refused. pnpm docs:check builds the site with the model-provider guide's screenshots and fails on a missing source; the copied files and their ./<basename> references were verified in website/.generated and in a running docs:dev (naturalWidth > 0 in both locales).

中文

问题

scripts/project-doc-site.ts 会把发布 manifest(元数据清单)未收录的仓库相对目标一律改写成 GitHub 地址,对图片而言就是 https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>。站点构建不拷贝任何文件:srcDir 是用完即弃的 .generated 树,VitePress 没有设置 publicDir(其默认值 <srcDir>/public 恰好位于投影每次运行时删除的那棵树里),而写进去的只有 Markdown。

这只对公开仓库成立。本仓库是私有的,而 raw.githubusercontent.com 对未认证请求一律回 404——github.com 上的登录会话也不能认证它,因为 GitHub 自家界面是用另一套单独签名的地址提供私有 blob 的。于是站点上的每一张图片对每一位读者都是坏的,却没有任何门禁能说出来:verify-md-links 与投影校验的是目标文件在仓库里是否存在,那与站点读者能否取到它是两个问题。

决策

rewriteMarkdown 新增可选的 placeImage(absPath): string。当页面引用了一张 manifest 未作为页面发布的图片时,投影把该文件复制进生成树中该页面的旁边,并把引用改写为 ./<basename>;随后 Vite 会像处理其他站点资源一样打包它。仓库可见性再也影响不到已发布页面。

副本落在页面旁边,而不是某个共享资源目录。每个 locale 的路由树各持一份副本,因此同一个相对 URL 在 guide/en/guide/ 下都正确,无需按 locale 计算前缀;manifest 撤下某页时,它的资源也随之消失。一张表登记所有被投影的路径——页面与图片一视同仁——同一路径出现第二个来源就抛错,与既有的重复路由检查同一个立场,而不是让最后写入的那个静默胜出。

只有真实路径位于仓库内的普通文件才会被拷贝,其余一律让投影失败并点名页面与目标。链接改写只需要知道目标存在,但发布是把它的字节拷上站点,因此一个逃出仓库的引用——经由 ../.. 或指向树外的符号链接——会把构建机上的文件放到已发布页面上。引用自带的 ?query#fragment 会随安置后的 URL 一同保留,与 GitHub 分支一贯的做法一致;文件名做百分号编码,因为目标位于 Markdown 内联目标的位置。

docsSourceFiles() 会连同被安置的图片一起上报,于是替换截图时开发服务器的 watcher 会重新投影,而不是一直服务旧副本直到有人碰一下页面。

placeImage 之所以可选,是因为 rewriteMarkdown 也被它自己的 spec 直接调用,而那里并不存在生成树。不传它时,GitHub raw 回退会指向公开源主页;这让该 seam 对只改写文本的消费方保持诚实。

正本 Markdown 照旧写普通的仓库相对图片路径,因此同一份文件在 GitHub 上和站点上都能正常显示。没有任何文档为了迁就 VitePress 而写站内绝对 URL。

考虑过的替代方案

publicDir 设到 .generated 之外,并使用站内绝对 URL。 投影这边的活动部件更少,但同一份 Markdown 在仓库中阅读时,每一处图片引用都会是坏的,而正本文档是两种方式都要读的。

把图片放到 assets 分支,就像演示 GIF 那样。 那个分支的存在是为了让大体积二进制不进主线历史,而它的 raw 地址有着完全相同的可见性问题。它仍然是录屏的正确归宿;但它解决不了这件事。

等仓库转为公开。 那只是消除症状,不会让站点自给自足,而且每一张图片都会让站点隐式依赖 GitHub 的可用性与限流。

后果

已发布文档中的图片,现在无论谁在阅读、无论仓库是否公开都能显示,站点构建也不再为图片依赖 GitHub 的运行时可达性。生成树会为每个 locale 各增加一份被引用图片的副本——模型提供方指南里的四张截图,每个 locale 约 270 KB。

未发布文档引用的图片不受影响。纯文本投影会相对于公开源主页解析它们;不在站点上的文档没有站点构建可以承载其资源。

测试

scripts/project-doc-site.spec.ts 覆盖:placer 收到解析后的绝对路径且其返回的 URL 落进 Markdown、被安置的引用保留其 fragment、存在 placer 时已发布页面的链接仍解析到自己的路由、以及不传 placer 时不变的 GitHub raw 回退。publishableImage 另有直接覆盖:仓库内的普通文件被接受,而目标逃出仓库的符号链接、仓库外的路径与目录一律拒绝。pnpm docs:check 会带着模型提供方指南的截图构建站点,并在来源缺失时失败;被拷贝的文件及其 ./<basename> 引用已在 website/.generated 与运行中的 docs:dev 里核实(两个 locale 均 naturalWidth > 0)。