diff --git a/astro.config.ts b/astro.config.ts index 24d082ee04f3c63..9972119136db60a 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -11,6 +11,8 @@ import react from "@astrojs/react"; import { readdir } from "fs/promises"; import { fileURLToPath } from "url"; +import remarkValidateImages from "./src/plugins/remark/validate-images"; + import rehypeTitleFigure from "rehype-title-figure"; import rehypeMermaid from "./src/plugins/rehype/mermaid.ts"; import rehypeAutolinkHeadings from "./src/plugins/rehype/autolink-headings.ts"; @@ -61,6 +63,7 @@ export default defineConfig({ site: "https://developers.cloudflare.com", markdown: { smartypants: false, + remarkPlugins: [remarkValidateImages], rehypePlugins: [ rehypeMermaid, rehypeExternalLinks, diff --git a/src/plugins/remark/validate-images.ts b/src/plugins/remark/validate-images.ts new file mode 100644 index 000000000000000..07b434f937a480c --- /dev/null +++ b/src/plugins/remark/validate-images.ts @@ -0,0 +1,52 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { visit } from "unist-util-visit"; + +import type { Node } from "unist"; +import type { VFile } from "vfile"; + +interface ImageNode extends Node { + type: "image"; + url: string; + position?: { + start: { line: number; column: number }; + end: { line: number; column: number }; + }; +} + +export default function validateImages() { + const rootDir = process.cwd(); + + const assetsDir = join(rootDir, "src", "assets"); + const publicDir = join(rootDir, "public"); + + return (tree: Node, file: VFile) => { + visit(tree, "image", (node: ImageNode) => { + const { url } = node; + let fullPath: string; + + if (url.startsWith("~/assets/")) { + fullPath = join(assetsDir, url.slice(9)); + } else if (url.startsWith("/")) { + fullPath = join(publicDir, url); + } else { + // Remote image or unrecognised URL + return; + } + + if (!existsSync(fullPath)) { + const position = node.position + ? ` at line ${node.position.start.line}, column ${node.position.start.column}` + : ""; + + const error = new Error( + `Image not found: "${url}"${position} in ${file.path}\n` + + `Expected to find at: ${fullPath}`, + ) as Error & { file?: string }; + + error.file = file.path; + throw error; + } + }); + }; +}