Automation
Automate documentation tasks—from content generation to deployment—to keep workflows efficient and reliable.
Built-in Scripts
Document Creation
Create new documents with the correct structure:
node scripts/create-document.js sample-docs en v2 guide "New Feature Overview"
node scripts/create-document.js sample-docs en v2 components "Sidebar Migration"
node scripts/create-document.js sample-docs en v2 advanced "Automation Recipes"
The script automatically:
- Creates the directory structure
- Generates frontmatter with metadata
- Sets up navigation
- Provides a content template
Version Management
Generate new documentation versions:
node scripts/create-version.js sample-docs v3
node scripts/create-version.js sample-docs v3 --interactive
node scripts/create-version.js sample-docs v3 --no-copy
Sidebar Generation
Create sidebar navigation:
pnpm build:sidebar
pnpm build:sidebar-selective --projects=sample-docs
Category Validation
Verify multi-language category keys:
node scripts/validate-category-structure.js
CI/CD Pipeline
GitHub Actions
.github/workflows/cloudflare-pages-deploy.yml coordinates quality checks and Cloudflare Pages deployments:
jobs:
quality-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
continue-on-error: true
- run: pnpm prettier --check .
continue-on-error: true
deploy:
needs: quality-check
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build:sidebar
- run: pnpm build
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: pages deploy dist --project-name libx
Quality Checks
Lint and Prettier currently continue on error to avoid blocking PRs. Add more validation (link checking, accessibility) gradually by extending the same workflow.
Custom Automation Scripts
Content Validation
// scripts/validate-content.js
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
class ContentValidator {
constructor(contentDir) {
this.contentDir = contentDir;
this.errors = [];
}
async validateAll() {
const files = await this.getAllMdxFiles();
for (const file of files) {
await this.validateFile(file);
}
return this.errors;
}
async validateFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const { data: frontmatter, content: body } = matter(content);
this.validateFrontmatter(filePath, frontmatter);
this.validateContent(filePath, body);
await this.validateLinks(filePath, body);
}
validateFrontmatter(filePath, frontmatter) {
for (const field of ['title', 'description']) {
if (!frontmatter[field]) {
this.errors.push({
file: filePath,
type: 'frontmatter',
message: `Missing required field: ${field}`
});
}
}
}
validateContent(filePath, content) {
const emptySections = content.match(/##\s+[^#\n]+\n\s*##/g);
if (emptySections) {
this.errors.push({
file: filePath,
type: 'content',
message: 'Empty section detected'
});
}
const todos = content.match(/TODO:|FIXME:|XXX:/gi);
if (todos) {
this.errors.push({
file: filePath,
type: 'content',
message: `Found ${todos.length} TODO items`
});
}
}
async validateLinks(filePath, content) {
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match;
while ((match = linkRegex.exec(content)) !== null) {
const url = match[2];
if (url.startsWith('/')) {
const exists = await this.checkInternalLink(url);
if (!exists) {
this.errors.push({
file: filePath,
type: 'link',
message: `Broken internal link: ${url}`
});
}
}
}
}
async getAllMdxFiles() {
const files = [];
function scanDirectory(dir) {
for (const item of fs.readdirSync(dir)) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
scanDirectory(fullPath);
} else if (item.endsWith('.mdx')) {
files.push(fullPath);
}
}
}
scanDirectory(this.contentDir);
return files;
}
}
const validator = new ContentValidator('apps/sample-docs/src/content/docs');
const errors = await validator.validateAll();
if (errors.length > 0) {
console.error('Validation errors found:');
errors.forEach(error => console.error(`${error.file}: ${error.message}`));
process.exit(1);
}
Image Optimization
// scripts/optimize-images.js
import sharp from 'sharp';
import fs from 'fs';
import path from 'path';
class ImageOptimizer {
constructor(publicDir) {
this.publicDir = publicDir;
this.sizes = [320, 640, 1024, 1920];
this.formats = ['webp', 'avif'];
}
async optimizeAll() {
const images = await this.findImages();
for (const image of images) {
await this.optimizeImage(image);
}
}
async optimizeImage(imagePath) {
const basename = path.basename(imagePath, path.extname(imagePath));
const dirname = path.dirname(imagePath);
const metadata = await sharp(imagePath).metadata();
for (const size of this.sizes) {
if (metadata.width && metadata.width > size) {
for (const format of this.formats) {
const outputPath = path.join(dirname, `${basename}-${size}w.${format}`);
await sharp(imagePath).resize(size).toFormat(format, { quality: 80 }).toFile(outputPath);
console.log(`Generated: ${outputPath}`);
}
}
}
}
async findImages() {
const images = [];
const supportedFormats = ['.jpg', '.jpeg', '.png', '.gif'];
function scanDirectory(dir) {
for (const item of fs.readdirSync(dir)) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
scanDirectory(fullPath);
} else if (supportedFormats.includes(path.extname(item).toLowerCase())) {
images.push(fullPath);
}
}
}
scanDirectory(this.publicDir);
return images;
}
}
SEO Automation
// scripts/generate-seo.js
import fs from 'fs';
import matter from 'gray-matter';
class SEOGenerator {
constructor(contentDir, baseUrl) {
this.contentDir = contentDir;
this.baseUrl = baseUrl;
}
async generateSitemap() {
const pages = await this.getAllPages();
const sitemap = this.createSitemap(pages);
fs.writeFileSync('public/sitemap.xml', sitemap);
}
async generateRobots() {
const robots = `User-agent: *
Allow: /
Sitemap: ${this.baseUrl}/sitemap.xml`;
fs.writeFileSync('public/robots.txt', robots);
}
createSitemap(pages) {
const urls = pages.map(page => `
<url>
<loc>${this.baseUrl}${page.url}</loc>
<lastmod>${page.lastmod}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>`).join('');
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`;
}
}
Monitoring and Alerts
Performance Monitoring
// scripts/performance-check.js
import lighthouse from 'lighthouse';
import chromeLauncher from 'chrome-launcher';
class PerformanceMonitor {
async checkSite(url) {
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
const options = {
logLevel: 'info',
output: 'json',
onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'],
port: chrome.port,
};
const runnerResult = await lighthouse(url, options);
await chrome.kill();
return this.processResults(runnerResult);
}
processResults(results) {
const scores = results.lhr.categories;
const report = {
performance: scores.performance.score * 100,
accessibility: scores.accessibility.score * 100,
bestPractices: scores['best-practices'].score * 100,
seo: scores.seo.score * 100
};
const thresholds = { performance: 90, accessibility: 95, bestPractices: 90, seo: 95 };
const alerts = [];
for (const [metric, score] of Object.entries(report)) {
if (score < thresholds[metric]) {
alerts.push(`${metric}: ${score} (threshold: ${thresholds[metric]})`);
}
}
return { scores: report, alerts };
}
}
Dead Link Checks
// scripts/check-links.js
import fetch from 'node-fetch';
import fs from 'fs';
class LinkChecker {
constructor() {
this.brokenLinks = [];
this.checkedUrls = new Set();
}
async checkAllLinks() {
const files = await this.getAllMdxFiles();
for (const file of files) {
await this.checkFileLinks(file);
}
return this.brokenLinks;
}
async checkFileLinks(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match;
while ((match = linkRegex.exec(content)) !== null) {
const url = match[2];
if (url.startsWith('http')) {
await this.checkExternalLink(url, filePath);
} else if (url.startsWith('/')) {
await this.checkInternalLink(url, filePath);
}
}
}
async checkExternalLink(url, filePath) {
if (this.checkedUrls.has(url)) return;
this.checkedUrls.add(url);
try {
const response = await fetch(url, { method: 'HEAD', timeout: 5000 });
if (!response.ok) {
this.brokenLinks.push({ file: filePath, url, status: response.status });
}
} catch (error) {
this.brokenLinks.push({ file: filePath, url, error: error.message });
}
}
}
Integrations and Workflow
Slack Notifications
// scripts/slack-notify.js
import { WebClient } from '@slack/web-api';
class SlackNotifier {
constructor(token, channel) {
this.slack = new WebClient(token);
this.channel = channel;
}
async notifyBuildSuccess(buildInfo) {
await this.slack.chat.postMessage({
channel: this.channel,
text: 'Documentation build succeeded!',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `✅ *Build Success*\n\n*Branch:* ${buildInfo.branch}\n*Commit:* ${buildInfo.commit}\n*Duration:* ${buildInfo.duration}s`
}
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View Docs' },
url: buildInfo.url
}
]
}
]
});
}
async notifyBuildFailure(error) {
await this.slack.chat.postMessage({
channel: this.channel,
text: 'Documentation build failed',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `❌ *Build Failed*\n\n*Error:* ${error.message}`
}
}
]
});
}
}
Automatic Translation
// scripts/auto-translate.js
import { translate } from '@google-cloud/translate';
class AutoTranslator {
constructor(credentials) {
this.translate = new translate.Translate(credentials);
}
async translateDocument(sourcePath, targetPath, targetLang) {
const content = fs.readFileSync(sourcePath, 'utf-8');
const { data: frontmatter, content: body } = matter(content);
const translatedFrontmatter = await this.translateFrontmatter(frontmatter, targetLang);
const translatedBody = await this.translateContent(body, targetLang);
const translatedContent = matter.stringify(translatedBody, translatedFrontmatter);
fs.writeFileSync(targetPath, translatedContent);
}
}
Next Steps
After understanding automation basics:
- Deployment: Learn deployment strategies
- Customization: Adapt automation scripts to your needs
- Reference: Review metadata details for scripting