How the freeCodeCamp Challenge-Linter Validates Curriculum Markdown Files
The freeCodeCamp challenge-linter validates curriculum markdown files by combining the markdownlint engine with three custom rules that check PrismJS language support, YAML syntax, and fenced code block requirements.
The challenge-linter is a Node.js utility located in the packages/challenge-linter/ directory that enforces consistent style and technical correctness across the freeCodeCamp curriculum. It processes every markdown file in curriculum/challenges/ to catch formatting errors and invalid code blocks before they reach the live site.
Core Linting Engine and Configuration
The challenge-linter leverages the widely-adopted markdownlint package as its foundation. It reads the repository-wide configuration file .markdownlint.yaml located at curriculum/challenges/.markdownlint.yaml to determine which rules to enforce.
This configuration enables most default markdownlint rules while disabling or tweaking specific ones that do not apply to the curriculum format. For example, the linter disables line-length restrictions and top-level heading checks that would otherwise conflict with freeCodeCamp's content structure.
Custom Validation Rules
Beyond standard Markdown linting, the challenge-linter injects three custom rules via the customRules option in packages/challenge-linter/src/linter/index.js. These rules enforce freeCodeCamp-specific quality standards.
PrismJS Language Validation
The markdown-prism.js rule verifies that every language declared in a fenced code block is supported by PrismJS. If a markdown file contains a code block with an unrecognized language identifier, the linter reports an error:
'foobar' is not recognised.
This ensures that all syntax highlighting on the freeCodeCamp platform renders correctly using PrismJS's lexer.
YAML Syntax Verification
The markdown-yaml.js rule parses fenced blocks marked with yaml or yml using the js-yaml library. When invalid YAML is detected, the linter throws an error that includes the parser's specific message and the offending line number. This prevents broken configuration examples from being published in the curriculum.
Fenced Code Block Requirements
The fenced-code-block.js rule enforces MD040 (fenced-code-language), requiring every fenced code block to include a language identifier. This rule complements the standard markdownlint configuration by ensuring no code blocks are left without syntax highlighting specifications.
Execution Flow and Build Pipeline Integration
The exported linter(rules) function in packages/challenge-linter/src/linter/index.js constructs a markdownlint options object that wires together the configuration and custom rules:
const options = {
files: [file.path], // the markdown file being processed
config: rules, // contents of .markdownlint.yaml
customRules: [lintYAML, lintPrism, fencedCodeBlock]
};
markdownlint(options, callback);
The callback function handles the results by printing any linting failures to the console. If errors are found, it sets process.exitCode = 1 to ensure continuous integration pipelines fail appropriately. The function then calls the optional next callback to continue processing additional files in the pipeline.
The linter integrates with the curriculum build system through curriculum/src/lint-localized.ts and related task scripts. These entry points gather all markdown files across language directories, feed them sequentially to the linter, and halt the build immediately if validation errors are detected.
Usage Examples
Programmatic Usage
You can invoke the linter programmatically by importing the linter function and passing the markdownlint configuration:
import { linter } from 'freecodecamp/packages/challenge-linter/src/linter/index.js';
import fs from 'fs';
import path from 'path';
// Load the repo-wide markdownlint configuration
const cfgPath = path.resolve(
__dirname,
'../../curriculum/challenges/.markdownlint.yaml'
);
const rules = fs.readFileSync(cfgPath, 'utf-8');
// Create a lint function for a single file
const lintFile = linter(rules);
// Example file to lint
const markdownFile = {
path: path.resolve(
__dirname,
'../../curriculum/challenges/english/blocks/lab-markdown-to-html-converter/66f55eac933ff64ce654ca74.md'
)
};
lintFile(markdownFile, (err, file) => {
if (err) console.error('Lint failed:', err);
else console.log('✅', file.path, 'passes all checks');
});
Command-Line Execution
For local development or CI environments, run the built-in script to validate all curriculum files:
# From the repo root
node tools/challenge-helper-scripts/lint-localized.ts
This script recursively walks curriculum/challenges/, applies the linter to each markdown file, and exits with status 1 if any issues are found.
Sample Error Output
When validation fails, the linter provides specific file paths and rule violations:
/curriculum/challenges/english/blocks/.../example.md:12 MD040/fenced-code-language
Code block language is missing
/curriculum/challenges/english/blocks/.../example.md:27 MD040/prism-langs
'foo' is not recognised.
Summary
- The challenge-linter uses
markdownlintwith a custom configuration atcurriculum/challenges/.markdownlint.yamlto enforce Markdown style rules. - Three custom rules—
markdown-prism.js,markdown-yaml.js, andfenced-code-block.js—validate PrismJS language support, YAML syntax, and mandatory language identifiers. - The
linter(rules)function inpackages/challenge-linter/src/linter/index.jsorchestrates the validation and setsprocess.exitCode = 1on failure. - Integration with
curriculum/src/lint-localized.tsensures all curriculum markdown files are validated during the build process before deployment.
Frequently Asked Questions
What configuration file controls the challenge-linter rules?
The challenge-linter reads curriculum/challenges/.markdownlint.yaml to determine which standard markdownlint rules to enable or disable. This file disables rules like line-length limits while enforcing others such as MD040 for fenced code block languages.
How does the linter validate code block languages?
The custom rule in packages/challenge-linter/src/linter/markdown-prism.js checks every fenced code block's language identifier against the PrismJS library. If the language is not recognized by PrismJS, the linter reports an error indicating the invalid language string.
Can I run the challenge-linter on a single file instead of the entire curriculum?
Yes. While the standard CI pipeline uses curriculum/src/lint-localized.ts to process all files, you can import the linter function from packages/challenge-linter/src/linter/index.js programmatically, load the rules from .markdownlint.yaml, and pass a specific file path to lint individual markdown files during development.
What happens if the linter finds errors during the build process?
When the linter detects validation errors, it prints the specific rule violations and file paths to the console, sets process.exitCode = 1, and propagates the error through the callback chain. This causes the CI pipeline to fail immediately, preventing invalid curriculum content from being deployed to the production site.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →