Skip to content

Commit 10c0883

Browse files
authored
feat: add build-time validation for route meta images (#302)
Add a Vite plugin that validates all og:image and twitter:image references in route files during build. The plugin scans route files for image references and ensures corresponding files exist in the public directory. If images are missing, the build fails with a clear error message showing which images are missing and which routes reference them. Also removes image references from forks and entities routes that were pointing to non-existent files.
1 parent 661ed57 commit 10c0883

File tree

7 files changed

+96
-22
lines changed

7 files changed

+96
-22
lines changed

src/routes/ethereum/entities.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,13 @@ export const Route = createFileRoute('/ethereum/entities')({
2222
content:
2323
'Explore Ethereum validator entities. View entity attestation performance, block proposals, and activity.',
2424
},
25-
{ property: 'og:image', content: '/images/ethereum/entities.png' },
2625
{ name: 'twitter:url', content: `${import.meta.env.VITE_BASE_URL}/ethereum/entities` },
2726
{ name: 'twitter:title', content: `Entities | ${import.meta.env.VITE_BASE_TITLE}` },
2827
{
2928
name: 'twitter:description',
3029
content:
3130
'Explore Ethereum validator entities. View entity attestation performance, block proposals, and activity.',
3231
},
33-
{ name: 'twitter:image', content: '/images/ethereum/entities.png' },
3432
],
3533
}),
3634
});

src/routes/ethereum/entities/$entity.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,13 @@ export const Route = createFileRoute('/ethereum/entities/$entity')({
2929
content:
3030
'Detailed analysis of a validator entity including attestation performance, block proposals, and historical activity trends.',
3131
},
32-
{ property: 'og:image', content: '/images/ethereum/entities.png' },
3332
{ name: 'twitter:url', content: `${import.meta.env.VITE_BASE_URL}/ethereum/entities/${ctx.params.entity}` },
3433
{ name: 'twitter:title', content: `${ctx.params.entity} | ${import.meta.env.VITE_BASE_TITLE}` },
3534
{
3635
name: 'twitter:description',
3736
content:
3837
'Detailed analysis of a validator entity including attestation performance, block proposals, and historical activity trends.',
3938
},
40-
{ name: 'twitter:image', content: '/images/ethereum/entities.png' },
4139
],
4240
}),
4341
});

src/routes/ethereum/entities/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,13 @@ export const Route = createFileRoute('/ethereum/entities/')({
2323
content:
2424
'Explore Ethereum validator entities. View entity attestation performance, block proposals, and activity.',
2525
},
26-
{ property: 'og:image', content: '/images/ethereum/entities.png' },
2726
{ name: 'twitter:url', content: `${import.meta.env.VITE_BASE_URL}/ethereum/entities` },
2827
{ name: 'twitter:title', content: `Entities | ${import.meta.env.VITE_BASE_TITLE}` },
2928
{
3029
name: 'twitter:description',
3130
content:
3231
'Explore Ethereum validator entities. View entity attestation performance, block proposals, and activity.',
3332
},
34-
{ name: 'twitter:image', content: '/images/ethereum/entities.png' },
3533
],
3634
}),
3735
});

src/routes/ethereum/forks.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,12 @@ export const Route = createFileRoute('/ethereum/forks')({
2424
property: 'og:description',
2525
content: 'View Ethereum consensus layer fork history and upcoming network upgrades',
2626
},
27-
{
28-
property: 'og:image',
29-
content: '/images/ethereum/forks.png',
30-
},
3127
{ name: 'twitter:url', content: `${import.meta.env.VITE_BASE_URL}/ethereum/forks` },
3228
{ name: 'twitter:title', content: `Forks | ${import.meta.env.VITE_BASE_TITLE}` },
3329
{
3430
name: 'twitter:description',
3531
content: 'View Ethereum consensus layer fork history and upcoming network upgrades',
3632
},
37-
{
38-
name: 'twitter:image',
39-
content: '/images/ethereum/forks.png',
40-
},
4133
],
4234
}),
4335
});

src/routes/ethereum/forks/index.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,12 @@ export const Route = createFileRoute('/ethereum/forks/')({
2222
property: 'og:description',
2323
content: 'View Ethereum consensus layer fork history and upcoming network upgrades',
2424
},
25-
{
26-
property: 'og:image',
27-
content: '/images/ethereum/forks.png',
28-
},
2925
{ name: 'twitter:url', content: `${import.meta.env.VITE_BASE_URL}/ethereum/forks` },
3026
{ name: 'twitter:title', content: `Forks | ${import.meta.env.VITE_BASE_TITLE}` },
3127
{
3228
name: 'twitter:description',
3329
content: 'View Ethereum consensus layer fork history and upcoming network upgrades',
3430
},
35-
{
36-
name: 'twitter:image',
37-
content: '/images/ethereum/forks.png',
38-
},
3931
],
4032
}),
4133
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { promises as fs } from 'node:fs';
2+
import { join } from 'node:path';
3+
import type { Plugin } from 'vite';
4+
5+
/**
6+
* Recursively find all .tsx files in a directory
7+
*/
8+
async function findTsxFiles(dir: string): Promise<string[]> {
9+
const files: string[] = [];
10+
const entries = await fs.readdir(dir, { withFileTypes: true });
11+
12+
for (const entry of entries) {
13+
const fullPath = join(dir, entry.name);
14+
if (entry.isDirectory()) {
15+
files.push(...(await findTsxFiles(fullPath)));
16+
} else if (entry.isFile() && entry.name.endsWith('.tsx')) {
17+
files.push(fullPath);
18+
}
19+
}
20+
21+
return files;
22+
}
23+
24+
/**
25+
* Vite plugin to validate that image files referenced in route meta tags exist.
26+
*
27+
* This plugin scans all route files for og:image and twitter:image meta tags,
28+
* extracts the image paths, and verifies that the corresponding files exist
29+
* in the public directory. If any images are missing, the build fails with
30+
* a clear error message.
31+
*/
32+
export function validateRouteImages(): Plugin {
33+
return {
34+
name: 'validate-route-images',
35+
async buildStart() {
36+
const publicDir = join(process.cwd(), 'public');
37+
const routesDir = join(process.cwd(), 'src', 'routes');
38+
39+
// Find all route files
40+
const routeFiles = await findTsxFiles(routesDir);
41+
42+
const missingImagesByPath = new Map<string, Set<string>>();
43+
const imageRegex =
44+
/(?:property|name):\s*['"](?:og:image|twitter:image)['"]\s*,\s*content:\s*['"](\/images\/[^'"]+)['"]/g;
45+
46+
for (const routeFile of routeFiles) {
47+
const content = await fs.readFile(routeFile, 'utf-8');
48+
49+
// Find all image references in this file
50+
const imagesInFile = new Set<string>();
51+
let match;
52+
while ((match = imageRegex.exec(content)) !== null) {
53+
const imagePath = match[1]; // e.g., '/images/ethereum/forks.png'
54+
imagesInFile.add(imagePath);
55+
}
56+
57+
// Check each unique image in this file
58+
for (const imagePath of imagesInFile) {
59+
const fullImagePath = join(publicDir, imagePath);
60+
61+
try {
62+
await fs.access(fullImagePath);
63+
} catch {
64+
// Image doesn't exist
65+
const relativeRoutePath = routeFile.replace(process.cwd() + '/', '');
66+
if (!missingImagesByPath.has(imagePath)) {
67+
missingImagesByPath.set(imagePath, new Set());
68+
}
69+
missingImagesByPath.get(imagePath)!.add(relativeRoutePath);
70+
}
71+
}
72+
}
73+
74+
if (missingImagesByPath.size > 0) {
75+
const errorLines = ['\n❌ Route image validation failed!\n'];
76+
errorLines.push('The following images are referenced but do not exist:\n');
77+
78+
for (const [imagePath, files] of missingImagesByPath) {
79+
errorLines.push(`\n Missing: ${imagePath}`);
80+
errorLines.push(' Referenced in:');
81+
for (const file of files) {
82+
errorLines.push(` • ${file}`);
83+
}
84+
}
85+
86+
errorLines.push('\nPlease create the missing images in the public directory or remove the references.\n');
87+
88+
throw new Error(errorLines.join('\n'));
89+
}
90+
91+
console.log('✅ All route images validated successfully');
92+
},
93+
};
94+
}

vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite';
55
import { visualizer } from 'rollup-plugin-visualizer';
66
import path from 'path';
77
import { generateHeadPlugin } from './vite-plugins/generate-head-plugin';
8+
import { validateRouteImages } from './vite-plugin-validate-route-images';
89

910
// https://vite.dev/config/
1011
export default defineConfig({
@@ -14,6 +15,7 @@ export default defineConfig({
1415
'import.meta.env.VITE_BASE_URL': JSON.stringify('https://lab.ethpandaops.io'),
1516
},
1617
plugins: [
18+
validateRouteImages(),
1719
tanstackRouter({
1820
routesDirectory: './src/routes',
1921
generatedRouteTree: './src/routeTree.gen.ts',

0 commit comments

Comments
 (0)