Skip to content

Commit ebd4a28

Browse files
committed
[Docs Site] Add remark plugin to validate images exist
1 parent 5ac8f5e commit ebd4a28

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

astro.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import react from "@astrojs/react";
1111
import { readdir } from "fs/promises";
1212
import { fileURLToPath } from "url";
1313

14+
import remarkValidateImages from "./src/plugins/remark/validate-images";
15+
1416
import rehypeTitleFigure from "rehype-title-figure";
1517
import rehypeMermaid from "./src/plugins/rehype/mermaid.ts";
1618
import rehypeAutolinkHeadings from "./src/plugins/rehype/autolink-headings.ts";
@@ -61,6 +63,7 @@ export default defineConfig({
6163
site: "https://developers.cloudflare.com",
6264
markdown: {
6365
smartypants: false,
66+
remarkPlugins: [remarkValidateImages],
6467
rehypePlugins: [
6568
rehypeMermaid,
6669
rehypeExternalLinks,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { existsSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { visit } from "unist-util-visit";
4+
5+
import type { Node } from "unist";
6+
import type { VFile } from "vfile";
7+
8+
interface ImageNode extends Node {
9+
type: "image";
10+
url: string;
11+
position?: {
12+
start: { line: number; column: number };
13+
end: { line: number; column: number };
14+
};
15+
}
16+
17+
export default function validateImages() {
18+
const rootDir = process.cwd();
19+
20+
const assetsDir = join(rootDir, "src", "assets");
21+
const publicDir = join(rootDir, "public");
22+
23+
return (tree: Node, file: VFile) => {
24+
visit(tree, "image", (node: ImageNode) => {
25+
const { url } = node;
26+
let fullPath: string | null;
27+
28+
// Skip remote images
29+
if (url.startsWith("http:") || url.startsWith("https:")) {
30+
return;
31+
}
32+
33+
// Handle relative paths to assets
34+
if (url.startsWith("../") || url.startsWith("~/assets/")) {
35+
// Remove the ~/assets/ or ../ prefix and join with the assets directory
36+
const relativePath = url.startsWith("~/") ? url.slice(9) : url;
37+
fullPath = join(assetsDir, relativePath);
38+
} else if (url.startsWith("/")) {
39+
// Handle absolute paths (public directory)
40+
fullPath = join(publicDir, url);
41+
} else {
42+
// Skip if we don't know how to handle this URL
43+
return;
44+
}
45+
46+
// Check if the file exists
47+
if (!existsSync(fullPath)) {
48+
const position = node.position
49+
? ` at line ${node.position.start.line}, column ${node.position.start.column}`
50+
: "";
51+
52+
const error = new Error(
53+
`Image not found: "${url}"${position} in ${file.path}\n` +
54+
`Expected to find at: ${fullPath}`,
55+
) as Error & { file?: string };
56+
57+
error.file = file.path;
58+
throw error;
59+
}
60+
});
61+
};
62+
}

0 commit comments

Comments
 (0)