From fbc9b58874cc5f488335edd4e8add893c7f21c3c Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Thu, 16 Jul 2026 23:00:59 +0300 Subject: [PATCH 1/2] feat(docs): add custom lint rule to match frontmatter title with page title --- package-lock.json | 1 - package.json | 2 + .../tooling-and-integration/typescript.md | 4 +- scripts/lint/frontmatter-title.mjs | 114 ++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 scripts/lint/frontmatter-title.mjs diff --git a/package-lock.json b/package-lock.json index ceac1875..ed93e53f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8772,7 +8772,6 @@ "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.7.0.tgz", "integrity": "sha512-Z4WR8fmLMRpdYqJ9i7vrlXSsSrxVJydwrkEXHapexfARbWfGb7vGcnvNQnIzN0cXciMVOlz/XLoiMCi9gUsy9Q==", "license": "MIT", - "peer": true, "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" } diff --git a/package.json b/package.json index fbaed220..b27810a5 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "lint": "npm-run-all \"lint:!(fix)\"", "lint:fix": "npm-run-all lint:*:fix", "lint:js": "eslint .", + "lint:md": "node scripts/lint/frontmatter-title.mjs", "lint:js:fix": "node --run lint:js -- --fix", + "lint:md:fix": "node --run lint:md -- --fix", "lint:css": "stylelint \"**/*.css\" --allow-empty-input", "lint:css:fix": "node --run lint:css -- --fix", "format": "prettier --write .", diff --git a/pages/guides/tooling-and-integration/typescript.md b/pages/guides/tooling-and-integration/typescript.md index 4659df3a..4e6553f9 100644 --- a/pages/guides/tooling-and-integration/typescript.md +++ b/pages/guides/tooling-and-integration/typescript.md @@ -1,6 +1,6 @@ --- -title: Typescript -authors: morsdyce,kkamali,mtrivera,byzyk,EugeneHlushko,chenxsan,snitin315,tusharthakur04,ThierryRakotomanana,valentina-buoro,avivkeller +title: TypeScript +authors: 'morsdyce,kkamali,mtrivera,byzyk,EugeneHlushko,chenxsan,snitin315,tusharthakur04,ThierryRakotomanana,valentina-buoro,avivkeller' --- # TypeScript diff --git a/scripts/lint/frontmatter-title.mjs b/scripts/lint/frontmatter-title.mjs new file mode 100644 index 00000000..636f2510 --- /dev/null +++ b/scripts/lint/frontmatter-title.mjs @@ -0,0 +1,114 @@ +import { readdir, readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import matter from 'gray-matter'; + +// Directories, that are auto-generated, should be ignored +const IGNORED_DIRS = [ + 'docs/api', + 'docs/loaders', + 'docs/plugins', + 'about/governance', +]; + +// Files that intentionally do not have an H1 heading (e.g. custom layouts) +const IGNORED_FILES = [ + '404.md', + 'index.md', + 'blog/index.md', + 'about/sponsors.md', +]; + +const isIgnored = relativePath => { + return ( + IGNORED_DIRS.some(dir => relativePath.startsWith(dir + '/')) || + IGNORED_FILES.some(file => relativePath === file) + ); +}; + +const run = async () => { + const args = process.argv.slice(2); + const isFixMode = args.includes('--fix'); + const pagesDir = join(import.meta.dirname, '..', '..', 'pages'); + let hasErrors = false; + + const files = await readdir(pagesDir, { recursive: true }); + + for (const file of files) { + if (!file.endsWith('.md') && !file.endsWith('.mdx')) { + continue; + } + + const relativePath = file.replace(/\\/g, '/'); + + if (isIgnored(relativePath)) { + continue; + } + + const fullPath = join(pagesDir, file); + + const content = await readFile(fullPath, 'utf8'); + const parsed = matter(content); + + const frontmatterTitle = parsed.data.title; + + const h1Match = parsed.content.match(/^#\s+(.+)$/m); + const h1Title = h1Match ? h1Match[1].trim() : null; + + if (!frontmatterTitle && !h1Title) { + continue; + } + + if (frontmatterTitle && h1Title && frontmatterTitle !== h1Title) { + if (isFixMode) { + parsed.data.title = h1Title; + await writeFile( + fullPath, + matter.stringify(parsed.content, parsed.data, { lineWidth: -1 }), + 'utf8' + ); + console.log( + `[lint] Fixed: Updated frontmatter title to "${h1Title}" in ${file}` + ); + } else { + console.error( + `[lint] Mismatch in ${file}:\nFrontmatter: "${frontmatterTitle}"\nH1 Heading : "${h1Title}"` + ); + hasErrors = true; + } + } else if (!frontmatterTitle && h1Title) { + if (isFixMode) { + parsed.data.title = h1Title; + await writeFile( + fullPath, + matter.stringify(parsed.content, parsed.data, { lineWidth: -1 }), + 'utf8' + ); + console.log( + `[lint] Fixed: Added frontmatter title "${h1Title}" in ${file}` + ); + } else { + console.error( + `[lint] Missing frontmatter title in ${file} (H1 is "${h1Title}")` + ); + hasErrors = true; + } + } else if (frontmatterTitle && !h1Title) { + console.error( + `[lint] Missing H1 heading in ${file} (Frontmatter is "${frontmatterTitle}")` + ); + hasErrors = true; + } + } + + if (hasErrors) { + throw new Error( + 'Lint failed: Frontmatter title and H1 heading must match.' + ); + } else { + console.log( + '[lint] All manually written pages have matching frontmatter and H1 titles.' + ); + } +}; + +await run(); From a5a61fa470652e461fda4efca4ff088ac0d12fbd Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Sat, 18 Jul 2026 16:03:25 +0300 Subject: [PATCH 2/2] feat(lint): migrate frontmatter title check to custom ESLint rule --- eslint-local-rules/frontmatter-title.mjs | 78 ++++++++++ eslint.config.mjs | 28 +++- package-lock.json | 188 +++++++++++++++++++++++ package.json | 3 +- scripts/lint/frontmatter-title.mjs | 114 -------------- 5 files changed, 294 insertions(+), 117 deletions(-) create mode 100644 eslint-local-rules/frontmatter-title.mjs delete mode 100644 scripts/lint/frontmatter-title.mjs diff --git a/eslint-local-rules/frontmatter-title.mjs b/eslint-local-rules/frontmatter-title.mjs new file mode 100644 index 00000000..f599ad3c --- /dev/null +++ b/eslint-local-rules/frontmatter-title.mjs @@ -0,0 +1,78 @@ +import matter from 'gray-matter'; + +export default { + meta: { + type: 'problem', + fixable: 'code', + messages: { + mismatch: 'Frontmatter title "{{fm}}" does not match H1 "{{h1}}".', + missingFrontmatterTitle: 'Missing frontmatter title.', + missingH1Title: 'Missing H1 title.', + }, + }, + + create(context) { + const { sourceCode } = context; + let h1Node = null; + + return { + heading(node) { + if (node.depth === 1 && !h1Node) h1Node = node; + }, + + 'root:exit'() { + const fmTitle = matter(sourceCode.text).data.title; + const h1Title = h1Node + ? sourceCode.getText(h1Node).replace(/^#\s+/, '').trim() + : null; + + if (!fmTitle && !h1Title) return; + + if (fmTitle && h1Title && fmTitle !== h1Title) { + context.report({ + node: h1Node, + messageId: 'mismatch', + data: { fm: fmTitle, h1: h1Title }, + fix(fixer) { + const match = /^title:.*$/m.exec(sourceCode.text); + if (match) { + return fixer.replaceTextRange( + [match.index, match.index + match[0].length], + `title: ${h1Title}` + ); + } + }, + }); + } else if (!fmTitle && h1Title) { + context.report({ + node: h1Node, + messageId: 'missingFrontmatterTitle', + fix(fixer) { + const hasFrontmatter = sourceCode.text.startsWith('---'); + if (hasFrontmatter) { + const match = /^---\r?\n/.exec(sourceCode.text); + if (match) { + return fixer.insertTextAfterRange( + [0, match[0].length], + `title: ${h1Title}\n` + ); + } + } else { + return fixer.insertTextBeforeRange( + [0, 0], + `---\ntitle: ${h1Title}\n---\n\n` + ); + } + }, + }); + } else if (!h1Title) { + // Missing H1, should put it manually. + context.report({ + loc: { line: 1, column: 0 }, + messageId: 'missingH1Title', + }); + } + }, + }; + }, +}; diff --git a/eslint.config.mjs b/eslint.config.mjs index 7aaf7509..1a5e1054 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,9 +1,15 @@ import js from '@eslint/js'; import globals from 'globals'; +import markdown from '@eslint/markdown'; +import frontmatterTitle from './eslint-local-rules/frontmatter-title.mjs'; export default [ - js.configs.recommended, { + ...js.configs.recommended, + files: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs'], + }, + { + files: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', @@ -28,6 +34,26 @@ export default [ 'pages/docs/api', 'pages/docs/loaders', 'pages/docs/plugins', + 'pages/about/governance', + 'pages/404.md', + 'pages/index.md', + 'pages/blog/index.md', + 'pages/about/sponsors.md', ], }, + { + files: ['pages/**/*.md'], + plugins: { + markdown, + local: { + rules: { + 'frontmatter-title': frontmatterTitle, + }, + }, + }, + language: 'markdown/commonmark', + rules: { + 'local/frontmatter-title': 'error', + }, + }, ]; diff --git a/package-lock.json b/package-lock.json index ed93e53f..7859bfb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@eslint/markdown": "^8.0.3", "eslint": "^10.4.0", "feed": "^6.0.0", "globals": "^17.6.0", @@ -447,6 +448,32 @@ } } }, + "node_modules/@eslint/markdown": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@eslint/markdown/-/markdown-8.0.3.tgz", + "integrity": "sha512-rBTSSShrq7e4O+PWfeE4azH4/CWPNrC+VGxBXiW00o3vYVJnznsZiDayj3KC9JztIMVRZxZHn2nrDIUau/4j7A==", + "dev": true, + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "github-slugger": "^2.0.0", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-gfm": "^3.1.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "micromark-extension-gfm": "^3.0.0", + "micromark-extension-math": "^3.1.0", + "micromark-util-normalize-identifier": "^2.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/object-schema": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", @@ -3001,6 +3028,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -4833,6 +4867,20 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4943,6 +4991,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6259,6 +6316,33 @@ "dev": true, "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6935,6 +7019,38 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-gfm": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", @@ -7036,6 +7152,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", @@ -7335,6 +7471,23 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", @@ -7456,6 +7609,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-mdx-expression": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", @@ -10871,6 +11044,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-5.1.0.tgz", diff --git a/package.json b/package.json index b27810a5..8d7fa6e9 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,7 @@ "lint": "npm-run-all \"lint:!(fix)\"", "lint:fix": "npm-run-all lint:*:fix", "lint:js": "eslint .", - "lint:md": "node scripts/lint/frontmatter-title.mjs", "lint:js:fix": "node --run lint:js -- --fix", - "lint:md:fix": "node --run lint:md -- --fix", "lint:css": "stylelint \"**/*.css\" --allow-empty-input", "lint:css:fix": "node --run lint:css -- --fix", "format": "prettier --write .", @@ -36,6 +34,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@eslint/markdown": "^8.0.3", "eslint": "^10.4.0", "feed": "^6.0.0", "globals": "^17.6.0", diff --git a/scripts/lint/frontmatter-title.mjs b/scripts/lint/frontmatter-title.mjs deleted file mode 100644 index 636f2510..00000000 --- a/scripts/lint/frontmatter-title.mjs +++ /dev/null @@ -1,114 +0,0 @@ -import { readdir, readFile, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import matter from 'gray-matter'; - -// Directories, that are auto-generated, should be ignored -const IGNORED_DIRS = [ - 'docs/api', - 'docs/loaders', - 'docs/plugins', - 'about/governance', -]; - -// Files that intentionally do not have an H1 heading (e.g. custom layouts) -const IGNORED_FILES = [ - '404.md', - 'index.md', - 'blog/index.md', - 'about/sponsors.md', -]; - -const isIgnored = relativePath => { - return ( - IGNORED_DIRS.some(dir => relativePath.startsWith(dir + '/')) || - IGNORED_FILES.some(file => relativePath === file) - ); -}; - -const run = async () => { - const args = process.argv.slice(2); - const isFixMode = args.includes('--fix'); - const pagesDir = join(import.meta.dirname, '..', '..', 'pages'); - let hasErrors = false; - - const files = await readdir(pagesDir, { recursive: true }); - - for (const file of files) { - if (!file.endsWith('.md') && !file.endsWith('.mdx')) { - continue; - } - - const relativePath = file.replace(/\\/g, '/'); - - if (isIgnored(relativePath)) { - continue; - } - - const fullPath = join(pagesDir, file); - - const content = await readFile(fullPath, 'utf8'); - const parsed = matter(content); - - const frontmatterTitle = parsed.data.title; - - const h1Match = parsed.content.match(/^#\s+(.+)$/m); - const h1Title = h1Match ? h1Match[1].trim() : null; - - if (!frontmatterTitle && !h1Title) { - continue; - } - - if (frontmatterTitle && h1Title && frontmatterTitle !== h1Title) { - if (isFixMode) { - parsed.data.title = h1Title; - await writeFile( - fullPath, - matter.stringify(parsed.content, parsed.data, { lineWidth: -1 }), - 'utf8' - ); - console.log( - `[lint] Fixed: Updated frontmatter title to "${h1Title}" in ${file}` - ); - } else { - console.error( - `[lint] Mismatch in ${file}:\nFrontmatter: "${frontmatterTitle}"\nH1 Heading : "${h1Title}"` - ); - hasErrors = true; - } - } else if (!frontmatterTitle && h1Title) { - if (isFixMode) { - parsed.data.title = h1Title; - await writeFile( - fullPath, - matter.stringify(parsed.content, parsed.data, { lineWidth: -1 }), - 'utf8' - ); - console.log( - `[lint] Fixed: Added frontmatter title "${h1Title}" in ${file}` - ); - } else { - console.error( - `[lint] Missing frontmatter title in ${file} (H1 is "${h1Title}")` - ); - hasErrors = true; - } - } else if (frontmatterTitle && !h1Title) { - console.error( - `[lint] Missing H1 heading in ${file} (Frontmatter is "${frontmatterTitle}")` - ); - hasErrors = true; - } - } - - if (hasErrors) { - throw new Error( - 'Lint failed: Frontmatter title and H1 heading must match.' - ); - } else { - console.log( - '[lint] All manually written pages have matching frontmatter and H1 titles.' - ); - } -}; - -await run();