Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions .github/scripts/check-seo-images.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env node

const fs = require("fs");
const path = require("path");

const sourceExtensions = new Set([".js", ".jsx", ".ts", ".tsx", ".mdx"]);

function walk(directory) {
if (!fs.existsSync(directory)) return [];

return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const absolutePath = path.join(directory, entry.name);
return entry.isDirectory() ? walk(absolutePath) : [absolutePath];
});
}

function lineNumberAt(source, index) {
return source.slice(0, index).split(/\r?\n/).length;
}

function findSeoOpeningTags(source) {
const tags = [];
const startPattern = /<SEO(?=[\s/>])/g;

for (const startMatch of source.matchAll(startPattern)) {
const start = startMatch.index || 0;
let quote = null;
let escaped = false;
let braceDepth = 0;

for (let index = start + startMatch[0].length; index < source.length; index++) {
const character = source[index];

if (quote) {
if (escaped) {
escaped = false;
} else if (character === "\\") {
escaped = true;
} else if (character === quote) {
quote = null;
}
continue;
}

if (character === '"' || character === "'" || character === "`") {
quote = character;
} else if (character === "{") {
braceDepth++;
} else if (character === "}" && braceDepth > 0) {
braceDepth--;
} else if (character === ">" && braceDepth === 0) {
tags.push({
index: start,
text: source.slice(start, index + 1),
});
break;
}
}
}

return tags;
}

function findLocalSeoImages(source) {
const references = [];
const imagePropPattern =
/\bimage\s*=\s*(?:"([^"]+)"|'([^']+)'|\{\s*["']([^"']+)["']\s*\})/;

for (const tag of findSeoOpeningTags(source)) {
const imageMatch = tag.text.match(imagePropPattern);
if (!imageMatch) continue;

const imagePath = imageMatch[1] || imageMatch[2] || imageMatch[3];
if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(imagePath)) continue;

references.push({
imagePath,
line: lineNumberAt(source, tag.index),
});
}

return references;
}

function validateSeoImages({ repoRoot, sourceRoot, assetRoots }) {
const failures = [];

for (const sourceFile of walk(sourceRoot)) {
if (!sourceExtensions.has(path.extname(sourceFile))) continue;

const source = fs.readFileSync(sourceFile, "utf8");
const relativeSource = path.relative(repoRoot, sourceFile);

for (const reference of findLocalSeoImages(source)) {
if (!reference.imagePath.startsWith("/")) {
failures.push(
`${relativeSource}:${reference.line} invalid local SEO image ${reference.imagePath} (use a root-relative path backed by static or public)`,
);
continue;
}

const pathname = reference.imagePath.split(/[?#]/, 1)[0];
const relativeAsset = path.posix
.normalize(pathname)
.replace(/^\/+/, "");

if (relativeAsset.startsWith("../") || path.isAbsolute(relativeAsset)) {
failures.push(
`${relativeSource}:${reference.line} invalid local SEO image path ${reference.imagePath}`,
);
continue;
}

const candidates = assetRoots.map((root) => path.join(root, relativeAsset));
if (!candidates.some((candidate) => fs.existsSync(candidate))) {
failures.push(
`${relativeSource}:${reference.line} missing local SEO image ${reference.imagePath} (expected ${path.relative(repoRoot, candidates[0])} or ${path.relative(repoRoot, candidates[1])})`,
);
}
}
}

return failures;
}

function main() {
const repoRoot = path.resolve(__dirname, "..", "..");
const sourceRoot = path.join(repoRoot, "src");
const assetRoots = [
path.join(repoRoot, "static"),
path.join(repoRoot, "public"),
];
const failures = validateSeoImages({ repoRoot, sourceRoot, assetRoots });

if (failures.length > 0) {
console.error("Local SEO image validation failed:");
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exitCode = 1;
return;
}

console.log("All local SEO image references resolve.");
}

if (require.main === module) {
main();
}

module.exports = {
findLocalSeoImages,
findSeoOpeningTags,
validateSeoImages,
};
88 changes: 88 additions & 0 deletions .github/scripts/check-seo-images.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const test = require("node:test");

const {
findLocalSeoImages,
validateSeoImages,
} = require("./check-seo-images.cjs");

function createFixture() {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "seo-images-"));
const sourceRoot = path.join(repoRoot, "src");
const staticRoot = path.join(repoRoot, "static");
const publicRoot = path.join(repoRoot, "public");
fs.mkdirSync(sourceRoot, { recursive: true });
fs.mkdirSync(staticRoot, { recursive: true });
fs.mkdirSync(publicRoot, { recursive: true });

return {
repoRoot,
sourceRoot,
assetRoots: [staticRoot, publicRoot],
};
}

function writeFile(root, relativePath, contents = "") {
const filePath = path.join(root, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, contents);
}

test("extracts literal image props from multiline SEO tags", () => {
const source = `
export const Head = () => (
<SEO
title="Platform Engineering"
image={"/images/platform.webp"}
/>
);`;

assert.deepEqual(findLocalSeoImages(source), [
{ imagePath: "/images/platform.webp", line: 3 },
]);
});

test("ignores external and dynamic SEO image props", () => {
const source = `
<SEO image="https://example.com/image.webp" />
<SEO image={dynamicImage} />`;

assert.deepEqual(findLocalSeoImages(source), []);
});

test("accepts an existing root-relative asset", (context) => {
const fixture = createFixture();
context.after(() => fs.rmSync(fixture.repoRoot, { recursive: true, force: true }));
writeFile(fixture.sourceRoot, "page.js", '<SEO image="/images/card.webp" />');
writeFile(fixture.assetRoots[0], "images/card.webp", "image");

assert.deepEqual(validateSeoImages(fixture), []);
});

test("reports a missing local asset with its source line", (context) => {
const fixture = createFixture();
context.after(() => fs.rmSync(fixture.repoRoot, { recursive: true, force: true }));
writeFile(
fixture.sourceRoot,
"pages/missing.js",
'const Head = () => (\n <SEO image="/images/missing.webp" />\n);',
);

const failures = validateSeoImages(fixture);
assert.equal(failures.length, 1);
assert.match(failures[0], /src[\\/]pages[\\/]missing\.js:2/);
assert.match(failures[0], /missing local SEO image \/images\/missing\.webp/);
});

test("rejects a relative local image path", (context) => {
const fixture = createFixture();
context.after(() => fs.rmSync(fixture.repoRoot, { recursive: true, force: true }));
writeFile(fixture.sourceRoot, "page.jsx", '<SEO image="images/card.webp" />');

const failures = validateSeoImages(fixture);
assert.equal(failures.length, 1);
assert.match(failures[0], /invalid local SEO image images\/card\.webp/);
});
6 changes: 6 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ jobs:
- name: Check CONTRIBUTING versions
run: npm run check:contributing-versions

- name: Check local SEO image references
run: npm run check:seo-images

- name: Test local SEO image validation
run: npm run test:seo-images

- name: Build
run: npm run build

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"lint": "eslint --fix .",
"checklint": "eslint .",
"check:contributing-versions": "node .github/scripts/check-contributing-versions.cjs",
"check:seo-images": "node .github/scripts/check-seo-images.cjs",
"test:seo-images": "node --test .github/scripts/check-seo-images.test.cjs",
"pretest": "eslint --ignore-path .gitignore .",
"preload-fonts": "gatsby-preload-fonts",
"deploy": "gatsby build && gh-pages -d public -b master",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ const Adventures = () => {
};
export default Adventures;
export const Head = () => {
return <SEO title="Adventures of Five and Friends" description="Meet Five, our intergalatic Cloud Native Hero" image="./mascot-five-adventure-github-stars.webp" />;
return <SEO title="Adventures of Five and Friends" description="Meet Five, our intergalatic Cloud Native Hero" image="/images/mascot-five-adventure-github-stars.webp" />;
};
2 changes: 1 addition & 1 deletion src/pages/solutions/platform-engineering/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export default PlatformEngineeringSolutions;
export const Head = () => {
return <SEO title="Platform Engineering"
description="Empower Your Teams with Platform Engineering. Streamline development, enhance collaboration, and accelerate innovation in cloud-native environments."
image="/images/solutions-devrel.webp"
image="/images/solutions-platform-engineering.webp"
/>;
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/meshery-operator.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/solutions-collaborate.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/solutions-devrel.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/solutions-diagram.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/solutions-gitops.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/solutions-infrastructure.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/solutions-operation.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading