/** * Generate static HTML files for /skills, /anti-patterns, /tutorials. * * Called from both scripts/build.js (before buildStaticSite) and * server/index.js (at module load), so dev and prod share the same * code path and output shape. * * Output lives under public/skills/, public/anti-patterns/, * public/tutorials/, all gitignored. Bun's HTML loader picks them up * the same way it picks up the hand-authored pages. */ import fs from 'node:fs'; import path from 'node:path'; import { buildSubPageData, CATEGORY_ORDER, CATEGORY_LABELS, CATEGORY_DESCRIPTIONS, } from './lib/sub-pages-data.js'; import { renderMarkdown, slugify } from './lib/render-markdown.js'; import { renderPage } from './lib/render-page.js'; function escapeHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Render one skill detail page HTML body (without the site shell). */ function renderSkillDetail(skill, knownSkillIds) { const bodyHtml = renderMarkdown(skill.body, { knownSkillIds, currentSkillId: skill.id, }); const editorialHtml = skill.editorial ? renderMarkdown(skill.editorial.body, { knownSkillIds, currentSkillId: skill.id }) : ''; const tagline = skill.editorial?.frontmatter?.tagline || skill.description; const categoryLabel = CATEGORY_LABELS[skill.category] || skill.category; // Reference files as collapsible
blocks let referencesHtml = ''; if (skill.references && skill.references.length > 0) { const refs = skill.references .map((ref) => { const slug = slugify(ref.name); const refBody = renderMarkdown(ref.content, { knownSkillIds, currentSkillId: skill.id, }); const title = ref.name .split('-') .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(' '); return `
Reference${escapeHtml(title)}
${refBody}
`; }) .join('\n'); referencesHtml = `

Deeper reference

${refs}
`; } const metaStrip = `
${escapeHtml(categoryLabel)} User-invocable ${skill.argumentHint ? `${escapeHtml(skill.argumentHint)}` : ''}
`; return `

Skills / ${escapeHtml(categoryLabel)}

/${escapeHtml(skill.id)}

${escapeHtml(tagline)}

${metaStrip}
${editorialHtml ? `
\n${editorialHtml}\n
` : ''}
The skill itself
${bodyHtml}
${referencesHtml}
`; } /** * Render the /skills index page body. */ function renderSkillsIndex(skillsByCategory) { let html = `

22 commands

Skills

One skill (/impeccable) teaches your AI design. Twenty-one commands steer the result. Each one is a small, opinionated tool that knows how to fix one specific thing.

`; for (const category of CATEGORY_ORDER) { const list = skillsByCategory[category] || []; if (list.length === 0) continue; html += `

${escapeHtml(CATEGORY_LABELS[category])}

${escapeHtml(CATEGORY_DESCRIPTIONS[category])}

`; } html += `
`; return html; } /** * Entry point. Generates all sub-page HTML files. * * @param {string} rootDir * @returns {Promise<{ files: string[] }>} list of generated file paths (absolute) */ export async function generateSubPages(rootDir) { const data = buildSubPageData(rootDir); const outDirs = { skills: path.join(rootDir, 'public/skills'), antiPatterns: path.join(rootDir, 'public/anti-patterns'), tutorials: path.join(rootDir, 'public/tutorials'), }; // Fresh output dirs each time so stale files don't linger. for (const dir of Object.values(outDirs)) { if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); } const generated = []; // Skills index { const html = renderPage({ title: 'Skills — Impeccable', description: '22 commands that teach your AI harness how to design. Browse by category: create, evaluate, refine, simplify, harden, system.', bodyHtml: renderSkillsIndex(data.skillsByCategory), activeNav: 'skills', canonicalPath: '/skills', }); const out = path.join(outDirs.skills, 'index.html'); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } // Skills detail pages for (const skill of data.skills) { const bodyHtml = renderSkillDetail(skill, data.knownSkillIds); const title = `/${skill.id} — Impeccable skill`; const description = skill.editorial?.frontmatter?.tagline || skill.description; const html = renderPage({ title, description, bodyHtml, activeNav: 'skills', canonicalPath: `/skills/${skill.id}`, }); const out = path.join(outDirs.skills, `${skill.id}.html`); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } return { files: generated }; }