/** * Generate static HTML files for /docs, /slop, /tutorials, /live-mode, * /designing. * * 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/docs/, public/slop/, 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, COMMAND_RELATIONSHIPS, LAYER_LABELS, LAYER_DESCRIPTIONS, GALLERY_ITEMS, } 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 ${caption ? `

${escapeHtml(caption)}

` : ''} After
`; } function renderCraftCaseCallout() { return ` `; } /** * 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 caseCalloutHtml = skill.id === 'craft' ? renderCraftCaseCallout() : ''; 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)}` : ''}
`; const hasDemo = demoHtml.trim().length > 0; // Sub-commands are accessed via /impeccable . Show "/impeccable" as // a smaller namespace label above the command name, matching the magazine // spread treatment so the command name stays at full display size. const titleHtml = skill.isSubCommand ? `/impeccable${escapeHtml(skill.id)}` : `/${escapeHtml(skill.id)}`; return `

Docs / ${escapeHtml(categoryLabel)}

${titleHtml}

${escapeHtml(tagline)}

${metaStrip}
${demoHtml}
${editorialHtml ? `
\n${editorialHtml}\n
` : ''} ${caseCalloutHtml}
${skill.isSubCommand ? 'reference/' + escapeHtml(skill.id) + '.md' : 'SKILL.md'} ${skill.isSubCommand ? 'Loaded when the impeccable skill routes to this command.' : '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) { // Label the toggle button with the current page so mobile users know // where they are at a glance, then open the menu to switch. let currentLabel = 'Docs menu'; if (current?.kind === 'skill') { currentLabel = `/${current.id}`; } else if (current?.kind === 'tutorial') { const t = tutorials.find((x) => x.slug === current.slug); if (t) currentLabel = t.title; } 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, allSkills) { // Build a lookup by id so we can pull taglines/descriptions for the cards. const skillsById = Object.fromEntries(allSkills.map((s) => [s.id, s])); const commandCount = allSkills.filter((s) => s.id !== 'impeccable').length; // Short, clean tagline for the home command card. Fall back to the editorial // tagline if set, otherwise use a default. const impeccable = skillsById['impeccable']; const homeTagline = impeccable?.editorial?.frontmatter?.tagline || 'The design intelligence behind every command.'; // Render a single command as a compact row: name on the left, description // and relationship on the right. Matches the original cheatsheet density. const renderCommandRow = (skill) => { const tagline = skill.editorial?.frontmatter?.tagline || skill.description; const shortTagline = tagline.length > 140 ? tagline.slice(0, 137) + '...' : tagline; const rel = COMMAND_RELATIONSHIPS[skill.id] || {}; const isAlpha = skill.id === 'live'; let metaHtml = ''; if (rel.pairs) { metaHtml = `
Pairs with ${rel.pairs}
`; } else if (rel.leadsTo?.length) { const links = rel.leadsTo.map((c) => `${c}`).join(', '); metaHtml = `
Leads to ${links}
`; } else if (rel.combinesWith?.length) { const links = rel.combinesWith.map((c) => `${c}`).join(', '); metaHtml = `
Combines with ${links}
`; } // Row is a
(not an ) so the inner relationship links are valid. // The name on the left is the primary link target. return `
/impeccable ${escapeHtml(skill.id)}${isAlpha ? ' ALPHA' : ''}

${escapeHtml(shortTagline)}

${metaHtml}
`; }; // Render category sections, skipping the Create category's /impeccable root // (shown as a hero card above) but keeping everything else in place. let categoriesHtml = ''; for (const category of CATEGORY_ORDER) { const list = (skillsByCategory[category] || []).filter((s) => s.id !== 'impeccable'); if (list.length === 0) continue; const rowsHtml = list.map(renderCommandRow).join(''); categoriesHtml += `

${escapeHtml(CATEGORY_LABELS[category])}

${escapeHtml(CATEGORY_DESCRIPTIONS[category])}

${list.length} ${list.length === 1 ? 'command' : 'commands'}
${rowsHtml}
`; } return `

1 skill · ${commandCount} commands

Commands

Impeccable gives you a shared design vocabulary with your AI. ${commandCount} commands that each encode a specific design discipline, so you can steer with precision. Pick one for the job or let the skill route you automatically.

The home command

/impeccable

${escapeHtml(homeTagline)}

Call /impeccable directly for freeform design work with the full guidebook loaded. Or reach for one of its specialized modes:

${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) { // Canonical ordering. Additional sections referenced by rules (e.g. // 'Interaction', 'Responsive' from LLM-only entries) are appended to // the end, before 'General quality', so every rule renders. const primaryOrder = [ 'Visual Details', 'Typography', 'Color & Contrast', 'Layout & Space', 'Motion', 'Interaction', 'Responsive', ]; const bySection = {}; for (const name of primaryOrder) bySection[name] = []; bySection['General quality'] = []; 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); }); } // Final render order: primary sections first, then any extras that // rules introduced, then General quality last. const order = [...primaryOrder]; for (const name of Object.keys(bySection)) { if (!order.includes(name) && name !== 'General quality') { order.push(name); } } order.push('General quality'); return { order, bySection }; } /** * Render the /slop sidebar: a table of contents for the four top-level * sections (See it / Try it live / The catalog / Run it yourself), with * the catalog's per-section anchors nested under "The catalog". */ function renderSlopSidebar(grouped, gallerySize) { const catalogEntries = 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'); const catalogTotal = grouped.order .reduce((sum, s) => sum + (grouped.bySection[s]?.length || 0), 0); return ` `; } /** * Render one rule card inside the anti-patterns main column. */ function renderRuleCard(rule) { const categoryLabel = rule.category === 'slop' ? 'AI slop' : 'Quality'; const layer = rule.layer || 'cli'; const layerLabel = LAYER_LABELS[layer] || layer; const layerTitle = LAYER_DESCRIPTIONS[layer] || ''; const skillLink = rule.skillSection ? `See in /impeccable` : ''; const visual = rule.visual ? `` : ''; return `
    ${visual}
    ${categoryLabel} ${escapeHtml(layerLabel)}

    ${escapeHtml(rule.name)}

    ${escapeHtml(rule.description)}

    ${skillLink}
    `; } function escapeAttr(str) { return String(str || '').replace(/"/g, '"'); } /** * 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 the /slop page main content. * * Four numbered sections in one scroll: See it (iframe overlay demo), * Try it live (specimen gallery), The catalog (the full rule list), and * Run it yourself (three invocation methods). Lives inside the docs * layout shell so the slop sidebar (renderSlopSidebar) navigates both * the top-level anchors and the per-catalog-section anchors. */ function renderSlopMain(grouped, totalRules) { // Catalog: rule-card sections, grouped by skillSection. let catalogSectionsHtml = ''; for (const section of grouped.order) { const rules = grouped.bySection[section] || []; if (rules.length === 0) continue; const slug = slugify(section); catalogSectionsHtml += `

    ${escapeHtml(section)}

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

    ${rules.map(renderRuleCard).join('\n')}
    `; } const detectedCount = grouped.order .flatMap((s) => grouped.bySection[s] || []) .filter((r) => r.layer !== 'llm').length; const llmCount = totalRules - detectedCount; const specimenCards = GALLERY_ITEMS.map( (item) => ` `, ).join('\n'); return `

    The visible tells of AI design

    Slop

    ${totalRules} patterns that mark an interface as AI-generated, and the detection overlay that catches them in place. Watch it flag them live, try it on ${GALLERY_ITEMS.length} synthetic specimens, or browse the full catalog. ${detectedCount} rules run deterministically (npx impeccable detect or the browser extension); ${llmCount} need /impeccable critique's LLM review pass.

    01 See it

    Live on a synthetic slop page

    Hover or tap any outlined element to see which rule fired.

    03 The catalog

    Every pattern /impeccable teaches against. AI slop rules flag the tells of AI-generated UIs; Quality rules flag general design mistakes that hurt regardless of who wrote them.

    How to read this

    Each rule shows how it is detected:

    CLI
    Deterministic. Runs from npx impeccable detect on files, no browser required.
    Browser
    Deterministic, but needs real browser layout. Runs via the browser extension or Puppeteer, not the plain CLI.
    LLM only
    No deterministic detector. Caught by /impeccable critique during its LLM design review.
    ${catalogSectionsHtml}

    04 Run it yourself

    Inside /impeccable critique

    /impeccable critique

    The design review command opens the overlay automatically during its browser assessment pass. Deterministic findings highlighted in place while the LLM runs its separate heuristic review.

    Standalone CLI

    npx impeccable live

    Starts a local server that serves the detector script. Inject it into any page via a <script> tag to see the overlay. Works on your own dev server, a staging URL, or anyone's live page.

    `; } /** * Render the animated Live Mode demo block. HTML structure matches the * homepage #live-demo exactly so public/js/components/live-demo.js can * drive it without modification. The CSS (.live-demo-*) lives in * public/css/live-mode.css, imported by main.css and also loaded on this * page via extraHead. */ function renderLiveModeDemo() { return `
    localhost:3000
    Newsletter

    Subscribe for updates

    Monthly-ish design notes.

    No. 04

    Letters, occasionally.

    A postcard from the editor, about once a month. No tracking pixels, no "just checking in."

    Dispatch

    Design notes,
    every other
    Thursday.

    Field Notes

    A monthly letter, for people who still read email for pleasure.

    Generating variants…
    1 / 3
    Variant 3 written to source
    /
    `; } /** * Render the /live-mode page main content. * * Marketing-style single-column layout mirroring /visual-mode's structure. * Surfaces the animated homepage live-demo, a three-stage narrative, * pathway cards (tutorial / reference / install), and the supported * framework strip. */ function renderLiveModeMain() { return `

    New in v3.0 Alpha

    Live Mode

    Pick any element in the browser. Drop a comment or a stroke. Three production-quality variants swap in via your framework's HMR. Accept the one you want and it writes back to source.

    Why alpha: Live Mode works end-to-end and is ready to try, but it still needs more testing against real-world repos and framework configs. Expect rough edges on uncommon setups, and please report what breaks.

    $ /impeccable live
    ${renderLiveModeDemo()}

    Click the frame or scroll it into view to start the loop. Respects prefers-reduced-motion.

    What happens, in three moves

    01 · Pick

    Point at what bugs you

    Click any element on your running dev server. Add a comment pin where the issue lives. Draw a stroke through the bit you want to change. Or just type "more playful".

    Newsletter card 1
    02 · Generate

    Three genuinely different takes

    Variants anchor to different archetypes, not three riffs on color. Each one explores a different primary axis: hierarchy, typography, density, layout, or palette strategy.

    No.04
    Dispatch
    Field
    03 · Accept

    Lands in real source

    The accepted variant replaces the picked element in your source file. CSS consolidates into your real stylesheet, not inline. Discard all three and the original stays.

    Variant 2 written to source

    Where next

    Supported dev servers
    • Vite
    • Next.js (incl. monorepos)
    • SvelteKit
    • Astro
    • Nuxt
    • Bun
    • Plain static HTML
    `; } /** /** * Render the /designing page. * * Editorial orientation: four-phase loop (start → iterate → polish → * maintain) as the spine, plus three appendix sections (register, * interop, avoid) and a CTA climax. Cards are rare: most sections * rely on typography, hairline rules, and whitespace for structure. */ function renderDesigningMain() { const loopNodes = [ { id: 'start', num: '01', name: 'Start', hint: 'From a blank file, through a brief, to a designed feature.' }, { id: 'iterate', num: '02', name: 'Iterate', hint: 'Refine in place. Command line or in the browser.' }, { id: 'polish', num: '03', name: 'Polish', hint: 'The pre-ship gauntlet. Audit, clarify, harden.' }, { id: 'maintain', num: '04', name: 'Maintain', hint: 'Pay down design debt before it solidifies.' }, ]; const nodeHtml = loopNodes.map((n) => ` ${n.num} ${n.name} ${escapeHtml(n.hint)} `).join(''); return `
    The core loop

    Designing with Impeccable

    Shipping a real interface is a loop. Four phases, each with one place to begin.

    The core loop
    ${nodeHtml}
    01 · Start

    From a blank file to a designed feature.

    Three commands, one arc. /impeccable teach writes the brief, once per project. /impeccable shape drafts a reference you can look at. /impeccable craft codes toward what you can see. Words, then pictures, then code.

    teach · in words
    PRODUCT.md Written by teach
    Register Product. Design serves the task.
    Users SREs on call, reading fast, often in the dark.
    Voice Calm, clinical, no hype.
    Anti-references Purple gradients. Glassmorphism. Hype.

    Teach runs a short discovery interview about audience, register, voice, and anti-references. It writes PRODUCT.md and, if there's code to scan, a DESIGN.md. Every later command reads both files before generating.

    shape + craft · in pictures

    Since image generation crossed the reference-quality threshold, shape can draft a brand toolkit you review at a glance, and craft can code toward a hi-fi mock instead of a paragraph. Neo Mirai is the full loop: generated direction, implemented page, browser iteration.

    Auto-generated brand toolkit plate: identity lockups, colour palette, type specimens, icon system, and application mocks for a fictional AI design conference, rendered in warm earth tones.
    Shape

    Brand toolkit. Identity, palette, type, icon language, applications, social tiles, UI direction. One plate, reviewable at a glance. Approved decisions get written into DESIGN.md.

    Auto-generated hi-fi landing-page mock: a long vertical editorial comp for a fictional Tokyo AI design conference, in warm earth tones with committed serif display type.
    Visualize

    Hi-fi reference. The destination, before the first line of CSS. Craft codes toward a concrete image, not an abstract brief. That is the step change.

    Full-page screenshot of the implemented Neo Mirai website.
    Ship

    Live build. The mock became semantic markup, regenerated assets, responsive fixes, nav state, speaker carousel behavior, and browser-verified polish. Open the live site.

    The first two plates were generated by OpenAI GPT Image 2. The third is the implemented Neo Mirai page. Gemini Nano Banana Pro, Imagen 4 Ultra, and Grok Imagen work the same way, via Codex, Gemini CLI, and compatible harnesses.

    02 · Iterate

    Refine what's there.

    Once something exists, you're iterating. There are two paths: specific commands for named dimensions, or Live Mode for visual exploration.

    Command line

    When the edit has a name.

    Type a command and let the skill encode a specific discipline. Best when you know the word: typography, layout, color, motion.

    Live Mode

    When the edit is easier to point at.

    Pick any element in the browser, draw, type, hit Go. Three production-quality variants. Accept one and it writes to source.

    When to reach for which
    Fix something "off" that you can't name /impeccable live
    Apply a specific discipline: type, layout, color, motion /typeset · /layout · /colorize · /animate
    Explore three directions side by side /impeccable live
    Ask "is this any good?" /impeccable critique
    Bring a safe design to life, or tone a shouting one down /bolder · /quieter
    03 · Polish

    The pre-ship gauntlet.

    Three commands in sequence before anything ships. They don't redesign; they find what still needs to change.

    Pre-ship 03 · 04

    Score it.

    Five dimensions scored 0 to 4: accessibility, performance, theming, responsive, anti-patterns. Findings tagged P0 to P3.

    Rewrite the copy.

    Labels, error messages, empty-state prose, microcopy. Tuned to the audience from PRODUCT.md.

    Stress-test reality.

    60-character names, German product titles, prices in the billions, 500s, offline. Production data is messy.

    04 · Maintain

    Design debt is real. Pay it down.

    Features ship, drift happens. Two commands close the gap before it solidifies.

    /impeccable extract

    Consolidate drift.

    Find patterns used three or more times with the same intent. Propose tokens and primitives.

    /impeccable document

    Re-capture the system.

    Scans tokens, components, and rendered output. Writes a spec-compliant DESIGN.md.

    Two lanes

    Brand, or product.

    Two defaults with different vocabularies. Impeccable picks the lane from your task cue and PRODUCT.md before every command, so typeset, animate, colorize, and friends adjust their output to match. You rarely need to set it by hand.

    Brand

    Design IS the product. Marketing, landing, editorial, long-form, portfolio.

    Product

    Design serves the task. App UI, admin, dashboards, tools.

    Read the brand-vs-product tutorial →
    Common mistakes

    What to avoid.

    An anti-patterns list, for using the anti-patterns tool.

    • Running both Impeccable and Anthropic's frontend-design skill

      Anthropic still promotes their skill in Claude Code, but it's been unmaintained and is now behind on recommended patterns. Run both and they collide on vocabulary, cancelling each other out. Pick one.

    • Pinning every command

      Pinning brings back /audit, /polish, /critique as shortcuts. Pin everything and you've re-exploded the / menu the v3.0 consolidation cleaned up. Pin the two or three you reach for daily.

    • Skipping teach

      Commands still run without PRODUCT.md and DESIGN.md. They default to generic SaaS patterns. The floor is meaningfully higher with context. Run teach once; every later command benefits.

    • Treating it like a linter

      Impeccable is an opinionated design partner, not a validator. It has a point of view. Push back with a reason and it'll work with you. Ignore the opinion without a reason and output gets worse, not better.

    `; } /** * 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}
    `; } /** * 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 = { docs: path.join(rootDir, 'public/docs'), slop: path.join(rootDir, 'public/slop'), tutorials: path.join(rootDir, 'public/tutorials'), liveMode: path.join(rootDir, 'public/live-mode'), designing: path.join(rootDir, 'public/designing'), }; // Clean up legacy output dirs from /anti-patterns and /visual-mode, // which have been merged into /slop. A stray file in either would // otherwise keep getting served by Bun's static handler. for (const legacy of ['public/anti-patterns', 'public/visual-mode']) { const dir = path.join(rootDir, legacy); if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true }); } // 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 = []; // Docs index: the full command reference with rich cards. { const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, null); const main = renderSkillsOverviewMain(data.skillsByCategory, data.skills); const html = renderPage({ title: 'Docs | Impeccable', description: '22 commands that teach your AI harness how to design. Browse by category: create, evaluate, refine, simplify, harden.', bodyHtml: wrapInDocsLayout(sidebar, main), activeNav: 'docs', canonicalPath: '/docs', bodyClass: 'sub-page skills-layout-page', }); const out = path.join(outDirs.docs, 'index.html'); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } // Per-command 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.isSubCommand ? `/impeccable ${skill.id} | Impeccable` : `/${skill.id} | Impeccable`; const description = skill.editorial?.frontmatter?.tagline || skill.description; const html = renderPage({ title, description, bodyHtml: wrapInDocsLayout(sidebar, main), activeNav: 'docs', canonicalPath: `/docs/${skill.id}`, bodyClass: 'sub-page skills-layout-page', }); const out = path.join(outDirs.docs, `${skill.id}.html`); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } // Slop: merged anti-patterns catalog + visual-mode overlay demo + gallery. // Single page, docs-browser shell with a nested TOC sidebar. { const grouped = groupRulesBySection(data.rules); const sidebar = renderSlopSidebar(grouped, GALLERY_ITEMS.length); const main = renderSlopMain(grouped, data.rules.length); const html = renderPage({ title: 'Slop | Impeccable', description: `${data.rules.length} patterns that mark an interface as AI-generated, plus the live detection overlay that catches them in place. The rule catalog behind npx impeccable detect, the browser extension, and /impeccable critique.`, bodyHtml: wrapInDocsLayout(sidebar, main), activeNav: 'slop', canonicalPath: '/slop', bodyClass: 'sub-page skills-layout-page slop-page', }); const out = path.join(outDirs.slop, '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); } // Live Mode: marketing landing mirroring the other single-column pages. // Needs live-mode.css (not imported by sub-pages.css to keep the base // bundle small) and the live-demo JS module to animate the demo. { const extraHead = ` `; const html = renderPage({ title: 'Live Mode | Impeccable', description: 'Iterate on UI in the browser. Pick an element, drop a comment, get three production-quality variants, accept one, and it writes back to source. /impeccable live.', bodyHtml: renderLiveModeMain(), activeNav: 'live', canonicalPath: '/live-mode', bodyClass: 'sub-page live-mode-page-body', extraHead, }); const out = path.join(outDirs.liveMode, 'index.html'); fs.writeFileSync(out, html, 'utf-8'); generated.push(out); } // Designing: orientation page about the core loop. { const html = renderPage({ title: 'Designing with Impeccable', description: 'The core loop: start, iterate, polish, maintain. How to use Impeccable end-to-end, from a blank file to shipped feature to paid-down design debt.', bodyHtml: renderDesigningMain(), activeNav: 'designing', canonicalPath: '/designing', bodyClass: 'sub-page designing-page-body', }); const out = path.join(outDirs.designing, '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 }; }