/**
* 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/docs/, 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,
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 `
`;
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 `
${editorialHtml ? `\n${editorialHtml}\n` : ''}
${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 isBeta = skill.id === 'live';
let metaHtml = '';
if (rel.pairs) {
metaHtml = `
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:
`;
}
/**
* 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 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 `
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 /visual-mode page main content.
*
* Single-column layout, no sidebar. Editorial header, live iframe embed
* of the detector running on a synthetic slop page, three-card section
* explaining the invocation methods, then a grid of real specimens the
* user can click into to see the overlay on a different page.
*/
function renderVisualModeMain() {
const specimenCards = GALLERY_ITEMS.map(
(item) => `
See every anti-pattern flagged directly on the page. No screenshots, no JSON to map back to line numbers. The overlay draws an outline and a label on every element the detector catches, so you fix them in place.
Live on a synthetic slop page
Hover or tap any outlined element to see which rule fired.
The design review command opens the overlay automatically during its browser assessment pass. You get the 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.
These ${GALLERY_ITEMS.length} synthetic slop pages ship with the detector script baked in. Click any to see the overlay running on a real page, then scroll around and hover the outlined elements.
${specimenCards}
`;
}
/**
* 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.
more playful
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 Beta
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.
$/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.
`;
}
/**
/**
* 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}
↘↙↖↗
designingimpeccable
01 · Start
From a blank file to a designed feature.
Run /impeccable teach once per project to establish PRODUCT.md and DESIGN.md. Then reach for /impeccable craft and describe what you want to build. Shape, build, and iterate happen inside one invocation.
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.
From that point on, every command reads both files before generating. Craft, polish, critique, live, all of them.
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.
$/impeccable polish pricing
$/impeccable bolder hero
$/impeccable typeset checkout
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.
Scans tokens, components, and rendered output. Writes a spec-compliant DESIGN.md that every other command reads.
01Overview
02Colors
03Typography
04Elevation
05Components
06Do's and Don'ts
Before any of this
Pick a register.
Brand and product surfaces have different defaults. Impeccable tracks this in PRODUCT.md as a single field, so commands like typeset, animate, and colorize adapt their vocabulary to match.
No. 04 · DispatchLetters, occasionally.
NewsletterSubscribe to updates
The same element, rendered in brand register (editorial masthead) and product register (utility). Register is answered once during teach and applies to every downstream command.
DESIGN.md follows the format Google Stitch publishes. Not a lock-in. When you outgrow Impeccable or want a second opinion from another tool, the file comes with you.
Writes itImpeccable
DESIGN.md
Reads it
Google StitchOther tools
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 `
The full catalog of patterns /impeccable teaches against. ${detectedCount} are caught by a deterministic detector (npx impeccable detect or the browser extension). ${llmCount} can only be flagged by /impeccable critique's LLM review pass. Want to see them live on real pages? Try Visual Mode, or iterate past them on your own dev server with Live Mode.
How to read this
AI slop rules flag the visible tells of AI-generated UIs. Quality rules flag general design mistakes that are not AI-specific but still hurt the work. Each rule also 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.
${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 = {
docs: path.join(rootDir, 'public/docs'),
antiPatterns: path.join(rootDir, 'public/anti-patterns'),
tutorials: path.join(rootDir, 'public/tutorials'),
visualMode: path.join(rootDir, 'public/visual-mode'),
liveMode: path.join(rootDir, 'public/live-mode'),
designing: path.join(rootDir, 'public/designing'),
};
// 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);
}
// 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);
}
// Visual Mode: single standalone page, no sidebar, single-column layout.
{
const html = renderPage({
title: 'Visual Mode | Impeccable',
description:
'See every anti-pattern flagged directly on the page. Live detection overlay from Impeccable, available via /impeccable critique, npx impeccable live, or the upcoming Chrome extension.',
bodyHtml: renderVisualModeMain(),
activeNav: 'visual-mode',
canonicalPath: '/visual-mode',
bodyClass: 'sub-page visual-mode-page-body',
});
const out = path.join(outDirs.visualMode, 'index.html');
fs.writeFileSync(out, html, 'utf-8');
generated.push(out);
}
// Live Mode: marketing landing mirroring /visual-mode. 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 };
}