/** * 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 left sidebar used across the /skills section. * Shows every skill grouped by category. Pass the current skill id to * mark it with aria-current="page". */ function renderSkillsSidebar(skillsByCategory, currentSkillId = null) { let html = ` `; return html; } /** * Render the /skills overview main column content (not the sidebar). * This is the orientation piece: what skills are, how to pick one, * the six categories explained with inline cross-links to detail pages. */ function renderSkillsOverviewMain(skillsByCategory) { const totalSkills = Object.values(skillsByCategory).reduce( (sum, list) => sum + list.length, 0, ); let categoriesHtml = ''; for (const category of CATEGORY_ORDER) { const list = skillsByCategory[category] || []; if (list.length === 0) continue; const skillChips = list .map( (s) => `/${escapeHtml(s.id)}`, ) .join(''); categoriesHtml += `

${escapeHtml(CATEGORY_LABELS[category])}

${list.length} ${list.length === 1 ? 'skill' : 'skills'}

${escapeHtml(CATEGORY_DESCRIPTIONS[category])}

${skillChips}
`; } return `

${totalSkills} commands

Skills

One skill, /impeccable, teaches your AI design. Twenty commands steer the result. Each command does one job with an opinion about what good looks like.

How to pick one

Skills are named after the intent you bring to them. Reviewing something? /critique or /audit. Fixing type? /typeset. Last-mile pass before shipping? /polish. The categories below group skills by the job.

${categoriesHtml}
`; } /** * Wrap sidebar + main content in the docs-browser layout shell. */ function wrapInDocsLayout(sidebarHtml, mainHtml) { return `
${sidebarHtml}
${mainHtml}
`; } /** * 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: docs-browser layout with sticky sidebar. { const sidebar = renderSkillsSidebar(data.skillsByCategory, null); const main = renderSkillsOverviewMain(data.skillsByCategory); const html = renderPage({ title: 'Skills | Impeccable', description: '21 commands that teach your AI harness how to design. Browse by category: create, evaluate, refine, simplify, harden, system.', bodyHtml: wrapInDocsLayout(sidebar, main), activeNav: 'skills', canonicalPath: '/skills', bodyClass: 'sub-page skills-layout-page', }); const out = path.join(outDirs.skills, 'index.html'); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } // Skills detail pages: same docs-browser shell as the overview. for (const skill of data.skills) { const sidebar = renderSkillsSidebar(data.skillsByCategory, skill.id); const main = renderSkillDetail(skill, data.knownSkillIds); const title = `/${skill.id} | Impeccable`; const description = skill.editorial?.frontmatter?.tagline || skill.description; const html = renderPage({ title, description, bodyHtml: wrapInDocsLayout(sidebar, main), activeNav: 'skills', canonicalPath: `/skills/${skill.id}`, bodyClass: 'sub-page skills-layout-page', }); const out = path.join(outDirs.skills, `${skill.id}.html`); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } return { files: generated }; }