Zum Hauptinhalt springen
•
Windows Tipps & SecurityMicrosoft präsentiert neue Windows Suche(08.10.2026 um 08:34 Uhr)
•••••
Windows Tipps & SecurityAndroid-Smartphone: So prüfen Sie, wie gut Ihr Akku noch ist(08.10.2026 um 10:30 Uhr)
•••••
Windows Tipps & SecurityMicrosoft präsentiert neue Windows Suche(08.10.2026 um 08:34 Uhr)
•••••
Windows Tipps & SecurityAndroid-Smartphone: So prüfen Sie, wie gut Ihr Akku noch ist(08.10.2026 um 10:30 Uhr)
••••
Intelligence View
⚡ tsecurity.de Intelligence

Why We Ditched Our Custom Lint Script for a Native ESLint Markdown Rule in Webpack-doc-kit 👀

If you’ve ever worked on a large-scale documentation site, you know the silent killer of consistency: human error in Markdown files. In webpack-doc-kit (the c…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!

If you’ve ever worked on a large-scale documentation site, you know the silent killer of consistency: human error in Markdown files.



In webpack-doc-kit (the coming documentation engine for Webpack), every Markdown page has a YAML frontmatter title and a Markdown H1 heading (# Title). Ideally, they should always match 😅:




---
title: "TypeScript"
---

# TypeScript







But in an open-source project with a lot of pages and contributors, things quickly drift out of sync. You end up with pages where the frontmatter says title: Typescript (lowercase 's'), but the H1 says # TypeScript.



This mismatch breaks page titles, confuses search indexing, and ruins the overall consistency of the docs. We needed a way to enforce this automatically in our CI/CD pipeline and fix it effortlessly.



Here is the story of how I first solved this with a standalone Node.js script, why that approach wasn't enough, and how a single code review review pushed me to build a Custom ESLint Rule powered by Markdown ASTs.









Attempt 1: The "Brute Force" Node.js Script



When you need to validate files, I'm often writing a custom CLI script.



My first iteration (scripts/lint/frontmatter-title.mjs) was a Node.js tool. It recursively traversed the pages/ directory, read each .md file, and parsed it using gray-matter library:




// A simplified look at the first approach
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 && 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}`);
hasErrors = true;
}
}







I hooked this into package.json as npm run lint:md and npm run lint:md:fix.






Why wasn't this enough?



It worked! It caught mismatches and fixed them. But from an architectural and Developer Experience (DX) standpoint, it had major drawbacks:




  1. Regex Fragility: Using regular expressions (/^#\s+(.+)$/m) to find Markdown H1s can easily break on edge cases, I don't know one, but it's software engineering sense 👀


  2. No IDE Integration: Because it was an external CLI script, developers didn't see red error lines inside VS Code while typing. They only found out they made a mistake when they ran the command (or worse, when CI failed 🤦🏻‍♂️).










The POV: "Can we make this an ESLint plugin?"



When I opened the PR#189, Webpack maintainer Aviv Keller reviewed the approach and asked a very nice question:




"Can we make this an ESLint plugin?"




He shared a skeleton showing how we could hook into ESLint instead. That review changed the entire path of the task.



Instead of treating Markdown as "just text" to be scraped with Regex, why not treat it as an Abstract Syntax Tree (AST)? With modern ESLint (specifically ESLint v9+ Flat Config and @eslint/markdown), ESLint isn't just a JavaScript linter anymore. it can natively parse Markdown CommonMark AST nodes!



By migrating to an ESLint rule:




  • True AST Accuracy: We parse actual Markdown headings.


  • Instant IDE Feedback: Errors appear inline in the developer's editor instantly.










Building the Custom Rule (eslint-local-rules/frontmatter-title.mjs)



We deleted the standalone script and created a custom local ESLint rule. But to make it production-ready for Webpack's ecosystem, we had to handle edge cases that a basic implementation would miss.



Here is how the rule works under the hood:






1. Hooking into Markdown AST Nodes



We use the heading(node) visitor to locate the H1 node (depth === 1), and then evaluate the whole file when exiting the AST root ("root:exit"):




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;

// Validation logic and fixers go here...
}
};
}
};










2. Smart Fixers (Auto-Fixing Edge Cases)



A linter that only complains is annoying; a linter that fixes your code automatically is a superpower.



We designed the fix(fixer) method to handle two distinct scenarios:





  • Scenario A: Title Mismatch: If both titles exist but don't match, we replace the title: line in the YAML frontmatter to match the H1:




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}`
);
}
},
});
}









  • Scenario B: Missing Frontmatter Title Entirely: What if a contributor wrote # TypeScript but forgot to add title: in the frontmatter? Our fixer dynamically checks if the Markdown file already has a --- YAML block. If it does, it injects title: ... right after the opening dashes. If the file has no frontmatter at all, it prepends a brand-new YAML block to the top of the file:




} 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 {
// Prepend a completely new frontmatter block
return fixer.insertTextBeforeRange(
[0, 0],
`---\ntitle: ${h1Title}\n---\n\n`
);
}
},
});
}









  • Scenario C: Knowing When NOT to Auto-Fix: What if the title exists in the frontmatter, but the contributor completely forgot to write an H1 heading in the Markdown body? We can't safely auto-generate an H1 because we don't know where the author intended to place it. For this edge case, we explicitly report the error without attempting a destructive auto-fix, requiring manual action:




} else if (!h1Title) {
// Missing H1, should put it manually.
context.report({
loc: { line: 1, column: 0 },
messageId: 'missingH1Title',
});
}









  • Scenario D: There's no title at all: we do nothing for that scenario. The reason is simple: we can't guess 😂




if (!fmTitle && !h1Title) return;












Tying It All Together in eslint.config.mjs



With ESLint’s Flat Config, integrating @eslint/markdown and our custom rule took just a few lines.



We also made sure to exclude auto-generated documentation directories (docs/api, docs/loaders) and layout-only pages (like 404.md or index.md) that intentionally don't follow the standard H1 structure:




import markdown from '@eslint/markdown';
import frontmatterTitle from './eslint-local-rules/frontmatter-title.mjs';

export default [
// ... JS configs ...
{
files: ['pages/**/*.md'],
ignores: [
'pages/docs/api',
'pages/docs/loaders',
'pages/docs/plugins',
'pages/404.md',
'pages/index.md',
],
plugins: {
markdown,
local: {
rules: {
'frontmatter-title': frontmatterTitle,
},
},
},
language: 'markdown/commonmark',
rules: {
'local/frontmatter-title': 'error',
},
},
];













Why This mattered



Checking if two strings match seems like a simple task. But in large-scale open-source engineering, how you integrate a solution is just as important as the solution itself.



By moving from a standalone script to an ESLint Markdown rule:




  1. We eliminated an extra CI step and deleted over 100 lines of manual file-traversal code.

  2. We gave contributors immediate visual feedback inside their IDEs.

  3. We took advantage of modern AST parsing instead of fragile regular expressions.



A massive thank you to Aviv Keller for pointing me toward the ESLint AST approach during the PR review. It was a great reminder that open-source code reviews aren't just about catching bugs; they're about leveling up your architectural thinking 🤍



Have you ever built custom ESLint rules for non-JS files like Markdown or JSON? Let me know in the comments below 👀

🔍 CTI & Forensik

Cyber Threat Intelligence & Forensik

ATT&CK-Navigator · IoC-Radar · Exploit-Belege
MITRE ATT&CK Matrix Navigator
Enterprise-Matrix · nur belegte Techniken
14 Taktiken
1 belegte Technik
T1110TA0006 · Credential Access
Brute Force
Mitigation: M1036 Account Use Policies
Quelle: Kontext-Klassifikation des Artikeltextes
Reconnaissance
Resource Development
Initial Access
Execution
Persistence
Privilege Escalation
Defense Evasion
Credential Access
Discovery
Lateral Movement
Collection
Command and Control
Exfiltration
Impact
CTI Threat Relationship Graph
Akteure · Techniken · Beziehungen
3 Knoten · 2 Relationen
CVE / Incident Threat Actor Software MITRE ATT&CK CWE Weakness IoC
Intelligence Digest — kostenlos Täglich die wichtigsten Security-News · jederzeit abbestellbar
Community Rating & Social Proof
⭐ Leser-Wertung
–
Ø / 5.0
Noch keine Leser-Bewertungen
War diese Seite hilfreich?
0
Jetzt Bewertung abgeben
🟢 Niedrig (1-3.9) 🟡 Mittel (4.0-6.9) 🔴 Hoch (7.0-8.9) Kritisch (9.0-10.0)

Verwandte Story-Cluster & Quellen (Vektor-KI)

Zum Aktualisieren ziehen
Nächster Beitrag