feat(nx-dev): add conformance rule to verify blog post cover images
This commit is contained in:
parent
6610f3d632
commit
5651270b33
7
nx.json
7
nx.json
@ -263,6 +263,13 @@
|
|||||||
"mdGlobPattern": "{blog,shared}/**/!(sitemap).md"
|
"mdGlobPattern": "{blog,shared}/**/!(sitemap).md"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "@nx/workspace-plugin/conformance-rules/blog-cover-image",
|
||||||
|
"projects": ["docs"],
|
||||||
|
"options": {
|
||||||
|
"mdGlobPattern": "blog/**/!(sitemap).md"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "@nx/workspace-plugin/conformance-rules/project-package-json",
|
"rule": "@nx/workspace-plugin/conformance-rules/project-package-json",
|
||||||
"projects": [
|
"projects": [
|
||||||
|
|||||||
@ -0,0 +1,152 @@
|
|||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { join, dirname, basename, extname } from 'node:path';
|
||||||
|
import { load as yamlLoad } from 'js-yaml';
|
||||||
|
import { workspaceRoot } from '@nx/devkit';
|
||||||
|
import { sync as globSync } from 'glob';
|
||||||
|
import {
|
||||||
|
createConformanceRule,
|
||||||
|
type ProjectFilesViolation,
|
||||||
|
} from '@nx/powerpack-conformance';
|
||||||
|
|
||||||
|
export default createConformanceRule<{ mdGlobPattern: string }>({
|
||||||
|
name: 'blog-cover-image',
|
||||||
|
category: 'consistency',
|
||||||
|
description:
|
||||||
|
'Ensures that blog posts have a cover_image defined in avif or jpg format with appropriate fallbacks',
|
||||||
|
reporter: 'project-files-reporter',
|
||||||
|
implementation: async ({ projectGraph, ruleOptions }) => {
|
||||||
|
const violations: ProjectFilesViolation[] = [];
|
||||||
|
const webinarWarnings: ProjectFilesViolation[] = [];
|
||||||
|
const { mdGlobPattern } = ruleOptions;
|
||||||
|
|
||||||
|
// Look for the docs project
|
||||||
|
const docsProject = Object.values(projectGraph.nodes).find(
|
||||||
|
(project) => project.name === 'docs'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!docsProject) {
|
||||||
|
return {
|
||||||
|
severity: 'low',
|
||||||
|
details: {
|
||||||
|
violations: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const blogPattern = join(
|
||||||
|
workspaceRoot,
|
||||||
|
docsProject.data.root,
|
||||||
|
mdGlobPattern
|
||||||
|
);
|
||||||
|
|
||||||
|
// find markdown files
|
||||||
|
const files = globSync(blogPattern);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = readFileSync(file, 'utf-8');
|
||||||
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
|
||||||
|
if (!frontmatterMatch) {
|
||||||
|
//ignore missing frontmatter for now
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const frontmatter = yamlLoad(frontmatterMatch[1]) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Check for webinar tag as we ignore webinars for now (they're pulled in via the Notion API)
|
||||||
|
const isWebinar =
|
||||||
|
Array.isArray(frontmatter.tags) &&
|
||||||
|
frontmatter.tags.includes('webinar');
|
||||||
|
|
||||||
|
const coverImagePath = frontmatter.cover_image as string;
|
||||||
|
const fileExtension = extname(coverImagePath).toLowerCase();
|
||||||
|
|
||||||
|
if (fileExtension !== '.avif' && fileExtension !== '.jpg') {
|
||||||
|
const message = 'Blog post cover_image must be in avif or jpg format';
|
||||||
|
if (isWebinar) {
|
||||||
|
webinarWarnings.push({
|
||||||
|
message: `[Webinar] ${message}`,
|
||||||
|
sourceProject: docsProject.name,
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
violations.push({
|
||||||
|
message,
|
||||||
|
sourceProject: docsProject.name,
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the image path for proper resolution
|
||||||
|
// For paths starting with /blog/, we need to look in docs/blog/images/
|
||||||
|
let absoluteImagePath: string;
|
||||||
|
if (coverImagePath.startsWith('/blog/')) {
|
||||||
|
const adjustedPath = coverImagePath.replace(/^\/blog\//, '/');
|
||||||
|
absoluteImagePath = join(
|
||||||
|
workspaceRoot,
|
||||||
|
docsProject.data.root,
|
||||||
|
'blog',
|
||||||
|
adjustedPath
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// For any other paths, use the as-is path
|
||||||
|
absoluteImagePath = join(workspaceRoot, coverImagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the image file exists
|
||||||
|
if (!existsSync(absoluteImagePath)) {
|
||||||
|
const message = `Cover image file does not exist: ${coverImagePath} (resolved to ${absoluteImagePath})`;
|
||||||
|
if (isWebinar) {
|
||||||
|
webinarWarnings.push({
|
||||||
|
message: `[Webinar] ${message}`,
|
||||||
|
sourceProject: docsProject.name,
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
violations.push({
|
||||||
|
message,
|
||||||
|
sourceProject: docsProject.name,
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's an AVIF image, check if there's a JPG equivalent
|
||||||
|
if (fileExtension === '.avif' && !isWebinar) {
|
||||||
|
if (
|
||||||
|
!existsSync(absoluteImagePath.replace('.avif', '.jpg')) &&
|
||||||
|
!existsSync(absoluteImagePath.replace('.avif', '.png')) &&
|
||||||
|
!existsSync(absoluteImagePath.replace('.avif', '.webp'))
|
||||||
|
) {
|
||||||
|
violations.push({
|
||||||
|
message: `AVIF cover image must have a JPG equivalent to be accepted as a valid OG image: ${coverImagePath.replace(
|
||||||
|
'.avif',
|
||||||
|
'.jpg'
|
||||||
|
)}`,
|
||||||
|
sourceProject: docsProject.name,
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If YAML parsing fails, we skip the file
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return violations with appropriate severity level
|
||||||
|
return {
|
||||||
|
severity: violations.length > 0 ? 'high' : 'low',
|
||||||
|
details: {
|
||||||
|
violations: [...violations, ...webinarWarnings],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mdGlobPattern": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The glob pattern to use to find the markdown files to analyze"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user