fix(scripts): let the documentation site carry its own images

An image the manifest does not publish was rewritten to a raw.githubusercontent
URL, which serves a private repository to nobody: raw answers 404 unauthenticated,
and a github.com session does not authenticate it. Every image on the site was
broken for every reader, and no gate could say so — link checking asks whether
the file exists in the repository, not whether a site reader can fetch it.

The projector now copies a referenced image into the generated tree beside the
page and points the reference at it, so Vite bundles it like any other site
asset and repository visibility cannot reach the published page. The copy lands
beside the page so one relative URL is right from both locale trees and a page's
assets leave with the page; two sources projecting onto one path throw rather
than letting the last copy win.

Canonical Markdown keeps writing ordinary repository-relative paths, so the same
file still renders when read in the repository.

The guide this was found through is renamed to its subject: 配置模型.
This commit is contained in:
Yichen Jiang
2026-08-06 20:21:17 +08:00
parent a231b56eba
commit 2426be893a
12 changed files with 163 additions and 15 deletions
+36 -5
View File
@@ -5,8 +5,8 @@
* tier, while this adapter rewrites cross-source links for the public site.
*/
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, extname, posix, relative, resolve, sep } from 'node:path'
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { basename, dirname, extname, posix, relative, resolve, sep } from 'node:path'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmFromMarkdown } from 'mdast-util-gfm'
import { gfm } from 'micromark-extension-gfm'
@@ -38,6 +38,15 @@ export interface RewriteMarkdownOptions {
pages: DocsPage[]
repoRoot: string
repositoryRef: string
/**
* Place one referenced image beside the projected page and return the URL to
* reach it from that page. A GitHub raw URL cannot serve this repository —
* `raw.githubusercontent.com` answers 404 for a private one, and no reader of
* the site is authenticated to it — so an image travels into the generated
* tree and Vite bundles it like any other site asset. Omitted by callers that
* only rewrite text, which then leave images pointing at the repository.
*/
placeImage?: (absPath: string) => string
}
function repoPath(absPath: string, repoRoot: string): string {
@@ -222,9 +231,11 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
? options.locale === 'root' ? 'en' : 'root'
: options.locale
const page = published.get(targetPath)?.get(targetLocale)
const nextUrl = page === undefined
? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
: routeTarget(options.route, page.route, suffix)
const nextUrl = page !== undefined
? routeTarget(options.route, page.route, suffix)
: node.type === 'image' && options.placeImage !== undefined
? options.placeImage(absPath)
: githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
const start = node.position?.start.offset
const end = node.position?.end.offset
@@ -299,6 +310,8 @@ export function docsSourceFiles(): string[] {
/** Rebuild the disposable VitePress source tree from the publication manifest. */
export function projectDocs(): void {
const routes = new Set<string>()
/** Projected asset path to the source it came from, for collision detection. */
const assets = new Map<string, string>()
const repositoryRef = process.env.GITHUB_SHA ?? 'master'
rmSync(generatedRoot, { recursive: true, force: true })
@@ -319,6 +332,24 @@ export function projectDocs(): void {
pages: docsPages,
repoRoot: root,
repositoryRef,
placeImage: (absPath) => {
// Beside the page that references it, under its own basename: each
// locale's route tree gets its own copy, so one relative URL is correct
// from both. Two sources that would land on one name are a collision
// rather than a silent overwrite of whichever copied last.
const name = basename(absPath)
const target = resolve(dirname(output), name)
const claimed = assets.get(target)
if (claimed !== undefined && claimed !== absPath) {
throw new Error(
`project-doc-site: ${repoPath(absPath, root)} and ${repoPath(claimed, root)}`
+ ` both project to ${relative(generatedRoot, target).split(sep).join('/')}.`,
)
}
assets.set(target, absPath)
copyFileSync(absPath, target)
return `./${name}`
},
})
writeFileSync(output, addProjectionFrontmatter(projectedPageContent(projected, page), page))
}