import { promises as fs } from 'node:fs'
import path from 'node:path'
import { generateDocsManifest, paths } from './generate-docs-manifest.mjs'
const distDir = path.join(paths.siteDir, 'dist')
const expectedCustomDomain = 'claudecode-haha.relakkesyang.org'
async function pathExists(targetPath) {
return fs.access(targetPath).then(() => true, () => false)
}
async function copyDirectory(source, destination) {
if (!await pathExists(source)) {
return
}
await fs.mkdir(path.dirname(destination), { recursive: true })
await fs.cp(source, destination, { recursive: true, force: true })
}
const markdownImagePattern = /!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g
const htmlImagePattern = /]*?\bsrc=["']([^"']+)["'][^>]*>/gi
const siteImagePattern = /["'(]\s*(\/[A-Za-z0-9_./-]+\.(?:avif|gif|jpe?g|png|svg|webp))/gi
function imageTargets(markdown) {
const prose = markdown.replace(/```[\s\S]*?```/g, '')
return [
...prose.matchAll(markdownImagePattern),
...prose.matchAll(htmlImagePattern),
].map((match) => match[1])
}
function isOutsideDocsPublic(relative) {
return relative.startsWith('..')
|| path.isAbsolute(relative)
|| relative === 'public'
|| relative.startsWith(`public${path.sep}`)
}
async function copySources(sources) {
for (const source of sources) {
if (!await pathExists(source)) continue
const destination = path.join(distDir, path.relative(paths.docsDir, source))
await fs.mkdir(path.dirname(destination), { recursive: true })
await fs.copyFile(source, destination)
}
}
async function copyReferencedDocImages(records) {
const sources = new Set()
for (const record of records) {
const sourceDirectory = path.dirname(path.join(paths.repoDir, record.sourcePath))
for (const target of imageTargets(record.content)) {
const pathname = target.split(/[?#]/, 1)[0]
if (!pathname || /^(?:[a-z]+:|\/\/|data:)/i.test(pathname)) continue
const source = pathname.startsWith('/')
? path.join(paths.docsDir, pathname.replace(/^\/+/, ''))
: path.resolve(sourceDirectory, pathname)
const relative = path.relative(paths.docsDir, source)
if (isOutsideDocsPublic(relative)) continue
sources.add(source)
}
}
await copySources(sources)
console.log(`Copied ${sources.size} referenced documentation images.`)
}
async function siteSourceFiles() {
const files = [path.join(paths.siteDir, 'index.html')]
const srcDir = path.join(paths.siteDir, 'src')
const entries = await fs.readdir(srcDir, { recursive: true, withFileTypes: true })
for (const entry of entries) {
if (!entry.isFile() || !/\.(?:jsx?|css)$/.test(entry.name)) continue
const directory = entry.parentPath || entry.path
// src/generated/ 里有一张「路径 → 图片尺寸」表,覆盖 docs/images 下的每张图。
// 那是给渲染器查尺寸用的,不代表页面引用了它们 —— 扫进来会把没人用的图
// (赞助、打赏之类)全拷进产物。
if (path.relative(srcDir, directory).split(path.sep)[0] === 'generated') continue
files.push(path.join(directory, entry.name))
}
return files
}
async function copySiteReferencedImages() {
const sources = new Set()
const missing = []
for (const file of await siteSourceFiles()) {
const content = await fs.readFile(file, 'utf8')
for (const match of content.matchAll(siteImagePattern)) {
const pathname = match[1]
const source = path.join(paths.docsDir, pathname.replace(/^\/+/, ''))
const relative = path.relative(paths.docsDir, source)
if (isOutsideDocsPublic(relative)) continue
if (await pathExists(source)) sources.add(source)
else missing.push(`${path.relative(paths.siteDir, file)}: ${pathname}`)
}
}
if (missing.length > 0) {
throw new Error(`Site-referenced images missing under docs/:\n- ${missing.join('\n- ')}`)
}
await copySources(sources)
console.log(`Copied ${sources.size} site-referenced images.`)
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
}
/**
* 每条路由的 HTML 骨架都写进自己的 title / description / canonical / hreflang。
* 不执行 JS 的爬虫看到的就不再是 78 个一模一样的页面。
*/
function shellForRoute(shell, meta) {
if (!meta) return shell
const origin = `https://${expectedCustomDomain}`
const isEnglish = meta.path === '/en' || meta.path.startsWith('/en/')
const canonical = `${origin}${meta.path}`
const alternate = meta.alternate ? `${origin}${meta.alternate}` : null
const head = [
``,
``,
alternate
? ``
: '',
``
].filter(Boolean).join('\n ')
return shell
.replace(/[\s\S]*?<\/title>/, `