// Docusaurus build health validation and deployment safety for Claude Skills showcase. Pre-commit MDX validation (Liquid syntax, angle brackets, prop mismatches), pre-build link checking, post-build health reports. Activate on "build errors", "commit hooks", "deployment safety", "site health", "MDX validation". NOT for general DevOps, Kubernetes, cloud infrastructure, or non-Docusaurus projects.
| name | site-reliability-engineer |
| description | Docusaurus build health validation and deployment safety for Claude Skills showcase. Pre-commit MDX validation (Liquid syntax, angle brackets, prop mismatches), pre-build link checking, post-build health reports. Activate on "build errors", "commit hooks", "deployment safety", "site health", "MDX validation". NOT for general DevOps, Kubernetes, cloud infrastructure, or non-Docusaurus projects. |
| allowed-tools | Read,Write,Edit,Bash,Grep,Glob |
Expert in Docusaurus build health, MDX validation, and deployment safety for the Claude Skills showcase website. Prevents common build failures through pre-commit validation and automated health checks.
โ Use for:
โ NOT for:
From recent debugging sessions, we've encountered these recurring anti-patterns:
Liquid Template Syntax in Code Examples
{{ variable }}} in Vue/Handlebars examples gets interpreted as Liquid{{{ variable }}}}Unescaped Angle Brackets in Markdown
<70 in headings/text parsed as incomplete HTML tag<70Component Prop Mismatches
fileName but receives skillIdMissing Critical Files
website/src/data/skills.ts (INVISIBLE on site!)Cache Corruption
.docusaurus, build, node_modules/.cacheNovice approach:
Expert approach:
# Run from project root
npm run install-hooks
# This creates:
# - .git/hooks/pre-commit (MDX validation)
# - package.json scripts: prebuild, postbuild
# Check all markdown for Liquid syntax issues
npm run validate:liquid
# Check for unescaped angle brackets
npm run validate:brackets
# Validate SkillHeader props across all docs
npm run validate:props
# Full pre-build validation
npm run validate:all
Liquid Template Syntax (Fast: <1s)
.md files for {{{ ... }}} outside code blocksAngle Bracket Escaping (Fast: <1s)
<\d (angle bracket + digit) in markdown text<70, >2 pages)SkillHeader Prop Validation (Fast: <2s)
<SkillHeader .../> component usesfileName exists, skillId doesn't, difficulty/category/tags removedRequired Files Check (Fast: <1s)
skills.ts requiredFile: .git/hooks/pre-commit (auto-generated)
#!/bin/bash
# Auto-generated by site-reliability-engineer skill
# DO NOT EDIT MANUALLY - regenerate with: npm run install-hooks
echo "๐ Running pre-commit validation..."
# Get list of staged .md files
STAGED_MD=$(git diff --cached --name-only --diff-filter=ACM | grep '\.md$' || true)
if [ -z "$STAGED_MD" ]; then
echo "โ
No markdown files to validate"
exit 0
fi
# Run validation scripts
node scripts/validate-liquid.js $STAGED_MD || exit 1
node scripts/validate-brackets.js $STAGED_MD || exit 1
node scripts/validate-skill-props.js $STAGED_MD || exit 1
echo "โ
Pre-commit validation passed"
exit 0
Runs before expensive Docusaurus build (npm run build).
Internal Link Validation (Medium: ~10s)
Skills Data Completeness (Fast: <3s)
skills.ts?skills.ts entry โ documentation file?TypeScript Compilation (Medium: ~15s)
tsc --noEmit to catch type errorsImage Optimization Check (Fast: <5s)
File: package.json (add these scripts)
{
"scripts": {
"prebuild": "node scripts/pre-build-validation.js",
"postbuild": "node scripts/post-build-health-check.js",
"validate:liquid": "node scripts/validate-liquid.js 'website/docs/**/*.md'",
"validate:brackets": "node scripts/validate-brackets.js 'website/docs/**/*.md'",
"validate:props": "node scripts/validate-skill-props.js 'website/docs/skills/*.md'",
"validate:links": "node scripts/validate-internal-links.js",
"validate:all": "npm run validate:liquid && npm run validate:brackets && npm run validate:props && npm run validate:links",
"install-hooks": "node scripts/install-git-hooks.js"
}
}
Runs after successful build, generates health report.
Build Size Analysis
Broken Link Detection
Performance Estimates
Health Report Output
File: website/.build-health.json
{
"timestamp": "2025-11-26T05:00:00Z",
"build": {
"success": true,
"duration_ms": 45328,
"bundle_size_mb": 8.2,
"warnings": 3,
"errors": 0
},
"broken_links": {
"count": 2,
"details": [
{
"file": "docs/skills/cv_creator.md",
"line": 393,
"target": "/planning/cv-creator-architecture.md",
"type": "internal"
}
]
},
"skills": {
"total": 40,
"with_hero_images": 40,
"with_zips": 40,
"in_skills_ts": 40
},
"recommendation": "Fix 2 broken internal links before deployment"
}
What it looks like:
# Checking for errors...
npm run build # Takes 2-3 minutes
Why it's wrong:
What to do instead:
# Fast pre-commit validation
npm run validate:all # <30 seconds
# Only build if validation passes
Detection: Pre-commit hook missing or disabled
What it looks like:
# Build failing, let me try clearing cache
rm -rf .docusaurus build node_modules/.cache
npm run build
Why it's wrong:
What to do instead:
# Auto-detect cache issues
npm run clean-build # Script that clears cache THEN builds
# Or: Post-build health check detects cache corruption signs
What it looks like:
[WARNING] Broken link: /docs/skills/nonexistent
[SUCCESS] Build completed
# "Success! Ship it!"
Why it's wrong:
What to do instead:
# Post-build validation fails on warnings
npm run postbuild # Exits non-zero if broken links > 0
What it looks like:
// Check for Liquid syntax
const liquidPattern = /\{\{.*?\}\}/g;
if (content.match(liquidPattern)) {
throw new Error("Liquid syntax detected!");
}
Why it's wrong:
What to do instead:
// Context-aware detection
const lines = content.split('\n');
let inCodeBlock = false;
for (const line of lines) {
if (line.trim().startsWith('```')) inCodeBlock = !inCodeBlock;
if (inCodeBlock) continue; // Skip code blocks
if (line.includes('{{') && !line.includes('{`{{')) {
// Found unescaped Liquid outside code block
}
}
File: scripts/validate-liquid.js
#!/usr/bin/env node
const fs = require('fs');
const glob = require('glob');
// Detect unescaped Liquid template syntax in MDX files
function validateLiquid(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const errors = [];
let inCodeBlock = false;
let inFrontmatter = false;
lines.forEach((line, idx) => {
// Track code blocks
if (line.trim().startsWith('```')) {
inCodeBlock = !inCodeBlock;
return;
}
// Track frontmatter
if (line.trim() === '---') {
inFrontmatter = !inFrontmatter;
return;
}
// Skip if in code block or frontmatter
if (inCodeBlock || inFrontmatter) return;
// Check for unescaped Liquid syntax
const liquidMatch = line.match(/\{\{[^`].*?\}\}/);
if (liquidMatch && !line.includes('{`{{')) {
errors.push({
line: idx + 1,
column: line.indexOf(liquidMatch[0]) + 1,
text: liquidMatch[0],
suggestion: `{\\`${liquidMatch[0]}\\`}`
});
}
});
return errors;
}
// Process files
const files = process.argv.slice(2);
let totalErrors = 0;
files.forEach(file => {
const errors = validateLiquid(file);
if (errors.length > 0) {
console.error(`โ ${file}:`);
errors.forEach(err => {
console.error(` Line ${err.line}:${err.column}: ${err.text}`);
console.error(` Fix: ${err.suggestion}`);
});
totalErrors += errors.length;
}
});
if (totalErrors > 0) {
console.error(`\nโ Found ${totalErrors} Liquid syntax error(s)`);
console.error(`\nRun: npm run fix:liquid (to auto-fix)`);
process.exit(1);
} else {
console.log('โ
No Liquid syntax errors found');
process.exit(0);
}
File: scripts/validate-brackets.js
#!/usr/bin/env node
const fs = require('fs');
function validateBrackets(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const errors = [];
let inCodeBlock = false;
lines.forEach((line, idx) => {
if (line.trim().startsWith('```')) {
inCodeBlock = !inCodeBlock;
return;
}
if (inCodeBlock) return;
// Check for unescaped < followed by digit or > followed by digit
const lessThanMatch = line.match(/<(\d+)/);
const greaterThanMatch = line.match(/>(\d+)/);
if (lessThanMatch && !line.includes('<')) {
errors.push({
line: idx + 1,
text: lessThanMatch[0],
fix: lessThanMatch[0].replace('<', '<')
});
}
if (greaterThanMatch && !line.includes('>')) {
errors.push({
line: idx + 1,
text: greaterThanMatch[0],
fix: greaterThanMatch[0].replace('>', '>')
});
}
});
return errors;
}
const files = process.argv.slice(2);
let totalErrors = 0;
files.forEach(file => {
const errors = validateBrackets(file);
if (errors.length > 0) {
console.error(`โ ${file}:`);
errors.forEach(err => {
console.error(` Line ${err.line}: "${err.text}" โ "${err.fix}"`);
});
totalErrors += errors.length;
}
});
if (totalErrors > 0) {
console.error(`\nโ Found ${totalErrors} unescaped angle bracket(s)`);
process.exit(1);
} else {
console.log('โ
No unescaped angle brackets found');
process.exit(0);
}
File: scripts/validate-skill-props.js
#!/usr/bin/env node
const fs = require('fs');
function validateSkillHeader(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const errors = [];
// Find SkillHeader component usage
const headerMatch = content.match(/<SkillHeader[\s\S]*?\/>/);
if (!headerMatch) return errors;
const headerText = headerMatch[0];
const lines = content.split('\n');
const lineNum = lines.findIndex(l => l.includes('<SkillHeader')) + 1;
// Check for correct prop: fileName (not skillId)
if (headerText.includes('skillId=')) {
errors.push({
line: lineNum,
issue: 'Uses "skillId" prop instead of "fileName"',
fix: 'Change skillId="..." to fileName="..."'
});
}
// Check for removed props (difficulty, category, tags)
const deprecatedProps = ['difficulty', 'category', 'tags'];
deprecatedProps.forEach(prop => {
if (headerText.includes(`${prop}=`)) {
errors.push({
line: lineNum,
issue: `Uses deprecated "${prop}" prop`,
fix: `Remove ${prop} prop (only use: skillName, fileName, description)`
});
}
});
// Check for required props
if (!headerText.includes('skillName=')) {
errors.push({
line: lineNum,
issue: 'Missing required "skillName" prop'
});
}
if (!headerText.includes('fileName=')) {
errors.push({
line: lineNum,
issue: 'Missing required "fileName" prop'
});
}
return errors;
}
const files = process.argv.slice(2);
let totalErrors = 0;
files.forEach(file => {
const errors = validateSkillHeader(file);
if (errors.length > 0) {
console.error(`โ ${file}:`);
errors.forEach(err => {
console.error(` Line ${err.line}: ${err.issue}`);
if (err.fix) console.error(` Fix: ${err.fix}`);
});
totalErrors += errors.length;
}
});
if (totalErrors > 0) {
console.error(`\nโ Found ${totalErrors} SkillHeader prop error(s)`);
process.exit(1);
} else {
console.log('โ
SkillHeader props validated successfully');
process.exit(0);
}
File: scripts/install-git-hooks.js
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const HOOK_SCRIPT = `#!/bin/bash
# Auto-generated by site-reliability-engineer skill
# Regenerate with: npm run install-hooks
echo "๐ Running pre-commit validation..."
# Get staged .md files
STAGED_MD=$(git diff --cached --name-only --diff-filter=ACM | grep '\\.md$' || true)
if [ -z "$STAGED_MD" ]; then
echo "โ
No markdown files to validate"
exit 0
fi
# Run validations
cd website
npm run validate:liquid $STAGED_MD || exit 1
npm run validate:brackets $STAGED_MD || exit 1
npm run validate:props $STAGED_MD || exit 1
echo "โ
Pre-commit validation passed"
exit 0
`;
const hookPath = path.join(process.cwd(), '.git/hooks/pre-commit');
// Write hook
fs.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
console.log('โ
Installed pre-commit hook at .git/hooks/pre-commit');
// Add npm scripts if not present
const packageJsonPath = path.join(process.cwd(), 'website/package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const scriptsToAdd = {
'validate:liquid': 'node scripts/validate-liquid.js',
'validate:brackets': 'node scripts/validate-brackets.js',
'validate:props': 'node scripts/validate-skill-props.js',
'validate:all': 'npm run validate:liquid && npm run validate:brackets && npm run validate:props',
'prebuild': 'npm run validate:all',
};
let added = false;
Object.entries(scriptsToAdd).forEach(([name, script]) => {
if (!packageJson.scripts[name]) {
packageJson.scripts[name] = script;
added = true;
console.log(`โ
Added npm script: ${name}`);
}
});
if (added) {
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log('โ
Updated package.json with validation scripts');
}
console.log('\n๐ Hook installation complete!');
console.log('\nTest with: npm run validate:all');
File: website/.site-reliability.config.json
{
"validation": {
"liquid": {
"enabled": true,
"autoFix": false,
"ignorePatterns": ["**/*.example.md"]
},
"brackets": {
"enabled": true,
"autoFix": true
},
"skillProps": {
"enabled": true,
"requiredProps": ["skillName", "fileName", "description"],
"deprecatedProps": ["difficulty", "category", "tags"]
}
},
"thresholds": {
"bundleSizeMB": 10,
"brokenLinksMax": 0,
"imageMaxSizeKB": 1024
},
"hooks": {
"preCommit": true,
"preBuild": true,
"postBuild": true
}
}
Symptom: Commit succeeds with invalid markdown
Diagnosis:
ls -la .git/hooks/pre-commit
# Should show executable permissions
Fix:
chmod +x .git/hooks/pre-commit
# Or reinstall: npm run install-hooks
Symptom: Validation fails on correct code
Diagnosis: Check if pattern is in code block
# Manually review file
# Verify code blocks are properly marked with ```
Fix: Add to ignore patterns in config, or fix detection logic
Symptom: Pre-commit takes >10 seconds
Diagnosis:
time npm run validate:all
# Identify slow script
Fix: Optimize glob patterns, add file count limits
name: Build Health Check
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: cd website && npm ci
- name: Run pre-build validation
run: cd website && npm run validate:all
- name: Build
run: cd website && npm run build
- name: Post-build health check
run: cd website && npm run postbuild
- name: Upload health report
uses: actions/upload-artifact@v3
with:
name: build-health
path: website/.build-health.json
2024 Early: Manual debugging of build errors, no validation
2024 Mid: Post-build error detection, manual cache clearing
2024 Late: Recognition of recurring anti-patterns (Liquid, brackets)
2025 Current: Pre-commit validation, automated health checks, this skill
Watch For: Docusaurus v4 changes to MDX parsing, new component patterns
After installing this skill's hooks:
This skill prevents: Liquid syntax errors | Angle bracket MDX failures | Component prop mismatches | Missing skill assets | Broken internal links
Use with: skill-documentarian (for skill sync) | docusaurus-expert (for advanced config)