/** * 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 the before/after split-compare demo block for a skill. * Returns '' when the skill has no demo data (e.g. /shape). */ function renderSkillDemo(skill) { if (!skill.demo) return ''; const { before, after, caption } = skill.demo; return `

Drag or hover to compare

${before}
${after || before}
Before After
${caption ? `

${escapeHtml(caption)}

` : ''}
`; } /** * 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 demoHtml = renderSkillDemo(skill); 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}
${demoHtml} ${editorialHtml ? `
\n${editorialHtml}\n
` : ''}
SKILL.md The canonical skill definition your AI harness loads.
${bodyHtml}
${referencesHtml}
`; } /** * Render the unified Docs sidebar used across /skills and /tutorials. * Shows every skill grouped by category, then tutorials as a final * group. Pass the current page identifier so we can mark it: * * { kind: 'skill', id: 'polish' } * { kind: 'tutorial', slug: 'getting-started' } * null (no current page) */ function renderDocsSidebar(skillsByCategory, tutorials, current = 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}
`; } /** * Group anti-pattern rules by skill section. * Rules without a skillSection fall into a 'General quality' bucket. */ function groupRulesBySection(rules) { const order = [ 'Visual Details', 'Typography', 'Color & Contrast', 'Layout & Space', 'Motion', 'General quality', ]; const bySection = {}; for (const name of order) bySection[name] = []; for (const rule of rules) { const section = rule.skillSection || 'General quality'; if (!bySection[section]) bySection[section] = []; bySection[section].push(rule); } // Sort each bucket: slop first (they're the named tells), then quality. for (const name of Object.keys(bySection)) { bySection[name].sort((a, b) => { if (a.category !== b.category) return a.category === 'slop' ? -1 : 1; return a.name.localeCompare(b.name); }); } return { order, bySection }; } /** * Render the anti-patterns sidebar: a table of contents of rule sections * with per-section rule counts. Every entry anchor-jumps to the section * in the main column. */ function renderAntiPatternsSidebar(grouped) { const entries = grouped.order .filter((section) => grouped.bySection[section]?.length > 0) .map((section) => { const slug = slugify(section); const count = grouped.bySection[section].length; return `
  • ${escapeHtml(section)}${count}
  • `; }) .join('\n'); return ` `; } /** * Render one rule card inside the anti-patterns main column. */ function renderRuleCard(rule) { const categoryLabel = rule.category === 'slop' ? 'AI slop' : 'Quality'; const skillLink = rule.skillSection ? `See in /impeccable` : ''; return `
    ${escapeHtml(rule.id)} ${categoryLabel}

    ${escapeHtml(rule.name)}

    ${escapeHtml(rule.description)}

    ${skillLink}
    `; } /** * Render the /tutorials index main content. */ function renderTutorialsIndexMain(tutorials) { const cards = tutorials .map( (t) => ` ${String(t.order).padStart(2, '0')}

    ${escapeHtml(t.title)}

    ${escapeHtml(t.tagline || t.description)}

    `, ) .join('\n'); return `

    ${tutorials.length} walk-throughs

    Tutorials

    Short, opinionated walk-throughs of the highest-leverage workflows. Each one takes around ten minutes and ends with something working in your project.

    ${cards}
    `; } /** * Render a tutorial detail page main content. */ function renderTutorialDetail(tutorial, knownSkillIds) { const bodyHtml = renderMarkdown(tutorial.body, { knownSkillIds }); return `

    Tutorials / ${String(tutorial.order).padStart(2, '0')}

    ${escapeHtml(tutorial.title)}

    ${tutorial.tagline ? `

    ${escapeHtml(tutorial.tagline)}

    ` : ''}
    ${bodyHtml}
    `; } /** * Render the /anti-patterns main column content. */ function renderAntiPatternsMain(grouped, totalRules) { let sectionsHtml = ''; for (const section of grouped.order) { const rules = grouped.bySection[section] || []; if (rules.length === 0) continue; const slug = slugify(section); sectionsHtml += `

    ${escapeHtml(section)}

    ${rules.length} ${rules.length === 1 ? 'rule' : 'rules'}

    ${rules.map(renderRuleCard).join('\n')}
    `; } return `

    ${totalRules} detection rules

    Anti-patterns

    These are the visible tells of AI-generated interfaces. Every rule in this catalog is implemented as a deterministic check in npx impeccable detect and in the browser extension. Run /critique on any page to see which ones it triggers.

    How to read this

    Rules are grouped by the section of the /impeccable skill that teaches the pattern to avoid. AI slop rules flag the specific visual tells (gradient text, purple palettes, side-tab borders, nested cards). Quality rules flag general design mistakes that are not AI-specific but still hurt the work.

    ${sectionsHtml}
    `; } /** * 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 = await 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 unified sidebar. { const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, 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: 'docs', 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 = renderDocsSidebar(data.skillsByCategory, data.tutorials, { kind: 'skill', id: 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: 'docs', 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); } // Anti-patterns index: single page, docs-browser shell with TOC sidebar. { const grouped = groupRulesBySection(data.rules); const sidebar = renderAntiPatternsSidebar(grouped); const main = renderAntiPatternsMain(grouped, data.rules.length); const html = renderPage({ title: 'Anti-patterns | Impeccable', description: `${data.rules.length} deterministic detection rules that flag the visible tells of AI-generated interfaces and common quality issues. Used by npx impeccable detect and the browser extension.`, bodyHtml: wrapInDocsLayout(sidebar, main), activeNav: 'anti-patterns', canonicalPath: '/anti-patterns', bodyClass: 'sub-page skills-layout-page anti-patterns-page', }); const out = path.join(outDirs.antiPatterns, 'index.html'); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } // Tutorials index (under the unified Docs umbrella). if (data.tutorials.length > 0) { const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, null); const main = renderTutorialsIndexMain(data.tutorials); const html = renderPage({ title: 'Tutorials | Impeccable', description: `${data.tutorials.length} short, opinionated walk-throughs of the highest-leverage Impeccable workflows.`, bodyHtml: wrapInDocsLayout(sidebar, main), activeNav: 'docs', canonicalPath: '/tutorials', bodyClass: 'sub-page skills-layout-page tutorials-page', }); const out = path.join(outDirs.tutorials, 'index.html'); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } // Tutorial detail pages. for (const tutorial of data.tutorials) { const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, { kind: 'tutorial', slug: tutorial.slug }); const main = renderTutorialDetail(tutorial, data.knownSkillIds); const html = renderPage({ title: `${tutorial.title} | Tutorials | Impeccable`, description: tutorial.description || tutorial.tagline || '', bodyHtml: wrapInDocsLayout(sidebar, main), activeNav: 'docs', canonicalPath: `/tutorials/${tutorial.slug}`, bodyClass: 'sub-page skills-layout-page tutorials-page', }); const out = path.join(outDirs.tutorials, `${tutorial.slug}.html`); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } return { files: generated }; }