mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
feat(site): merge /anti-patterns and /visual-mode into /slop
Top nav was at six items and growing. The anti-patterns catalog and the visual-mode overlay demo were always two views of the same subject (the rule set and seeing it caught in place), so collapsing them into one page reduces nav weight and puts the catalog + live demo next to each other. Four sections in one scroll: See it (iframe demo), Try it live (specimen gallery), The catalog (full rule list), Run it yourself (invocation methods). Sidebar TOC nests the catalog sections under "The catalog" for deep linking. 301s from the old URLs preserve external links. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c160ffc38d
commit
5316809139
@@ -63,6 +63,7 @@ public/docs/
|
||||
public/anti-patterns/
|
||||
public/tutorials/
|
||||
public/visual-mode/
|
||||
public/slop/
|
||||
|
||||
# Note: harness skill directories (.claude/skills/, .cursor/skills/, etc.)
|
||||
# are intentionally tracked. npx skills reads them from this repo at install
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
<a href="/" data-nav="home">Home</a>
|
||||
<a href="/designing" data-nav="designing">Designing</a>
|
||||
<a href="/docs" data-nav="docs">Docs</a>
|
||||
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
|
||||
<a href="/slop" data-nav="slop">Slop</a>
|
||||
<a href="/live-mode" data-nav="live">Live</a>
|
||||
<a href="/visual-mode" data-nav="visual-mode">Overlay</a>
|
||||
</nav>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
|
||||
+132
-64
@@ -904,27 +904,135 @@ main#main {
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Catalog subsections sit under the "The catalog" h2 on /slop, so they
|
||||
must read clearly as h3 — smaller, uppercase mono label instead of
|
||||
display italic, to avoid competing with the parent section heading. */
|
||||
.anti-patterns-section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.75rem, 3vw, 2.25rem);
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-ink);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.anti-patterns-section-count {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-ash);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SLOP PAGE (/slop)
|
||||
Merger of the former /anti-patterns + /visual-mode.
|
||||
Reuses .anti-patterns-* for the catalog and .visual-mode-*
|
||||
for the demo / gallery / methods blocks; the rules below wire
|
||||
the four numbered top-level sections and the nested sidebar.
|
||||
============================================ */
|
||||
|
||||
.slop-content { width: 100%; }
|
||||
|
||||
.slop-header {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.slop-section {
|
||||
margin-top: clamp(3rem, 6vw, 4.5rem);
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
.slop-section-heading {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(2rem, 3.5vw, 2.75rem);
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
color: var(--color-ink);
|
||||
letter-spacing: -0.015em;
|
||||
line-height: 1.15;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.slop-section-num {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-style: normal;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-ash);
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.slop-catalog-header {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.slop-catalog-lede {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.7;
|
||||
color: var(--color-charcoal);
|
||||
max-width: 72ch;
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.slop-catalog-lede strong {
|
||||
color: var(--color-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.slop-catalog-lede a {
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.slop-catalog-lede a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Sidebar: nested catalog-section list under "The catalog". */
|
||||
.slop-sidebar-sublist {
|
||||
list-style: none;
|
||||
margin: 6px 0 4px 0;
|
||||
padding: 0 0 0 var(--spacing-md);
|
||||
border-left: 1px solid var(--color-mist);
|
||||
}
|
||||
|
||||
.slop-sidebar-sublist li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.slop-sidebar-sublist a {
|
||||
display: flex !important;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-ash);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.slop-sidebar-sublist a:hover {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.slop-sidebar-sublist .anti-patterns-sidebar-count {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.rule-card-grid {
|
||||
@@ -1275,24 +1383,6 @@ main#main {
|
||||
VISUAL MODE PAGE
|
||||
============================================ */
|
||||
|
||||
.visual-mode-page-body main#main {
|
||||
/* Reset the docs-layout override; this page uses its own max-width. */
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.visual-mode-page {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: clamp(2rem, 4vw, 3.5rem) clamp(var(--spacing-md), 4vw, var(--spacing-2xl)) clamp(4rem, 8vw, 6rem);
|
||||
}
|
||||
|
||||
.visual-mode-page-header {
|
||||
max-width: 720px;
|
||||
margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
|
||||
}
|
||||
|
||||
.visual-mode-demo-wrap {
|
||||
margin-bottom: clamp(3rem, 6vw, 4.5rem);
|
||||
}
|
||||
@@ -1305,12 +1395,11 @@ main#main {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Mac-window chrome around the iframe. Scoped to .visual-mode-page so
|
||||
the sub-page's preview styles don't leak onto the homepage, which
|
||||
carries its own .visual-mode-preview rules in main.css. The auto
|
||||
margins below would disable grid-item stretch on the homepage and
|
||||
collapse the preview to the iframe's 300px intrinsic width. */
|
||||
.visual-mode-page .visual-mode-preview {
|
||||
/* Mac-window chrome around the iframe. Lives inside sub-pages.css, which
|
||||
is not loaded on the homepage — the homepage has its own copy in
|
||||
main.css. The max-width + auto margins would disable grid stretch on
|
||||
the homepage and collapse the preview to the iframe's intrinsic width. */
|
||||
.visual-mode-preview {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-mist);
|
||||
@@ -1319,7 +1408,7 @@ main#main {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.visual-mode-page .visual-mode-preview-header {
|
||||
.visual-mode-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@@ -1328,18 +1417,18 @@ main#main {
|
||||
border-bottom: 1px solid var(--color-mist);
|
||||
}
|
||||
|
||||
.visual-mode-page .visual-mode-preview-dot {
|
||||
.visual-mode-preview-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.visual-mode-page .visual-mode-preview-dot.red { background: #ff5f56; }
|
||||
.visual-mode-page .visual-mode-preview-dot.yellow { background: #ffbd2e; }
|
||||
.visual-mode-page .visual-mode-preview-dot.green { background: #27c93f; }
|
||||
.visual-mode-preview-dot.red { background: #ff5f56; }
|
||||
.visual-mode-preview-dot.yellow { background: #ffbd2e; }
|
||||
.visual-mode-preview-dot.green { background: #27c93f; }
|
||||
|
||||
.visual-mode-page .visual-mode-preview-title {
|
||||
.visual-mode-preview-title {
|
||||
margin-left: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
@@ -1354,21 +1443,8 @@ main#main {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Three invocation methods. */
|
||||
.visual-mode-methods {
|
||||
margin-bottom: clamp(3rem, 6vw, 4.5rem);
|
||||
}
|
||||
|
||||
.visual-mode-methods-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.75rem, 3vw, 2.25rem);
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
color: var(--color-ink);
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* Three invocation methods grid. Section wrapper margins come from
|
||||
.slop-section; this block only controls the grid of cards. */
|
||||
.visual-mode-methods-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
@@ -1453,26 +1529,18 @@ main#main {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Gallery (specimens) section */
|
||||
/* Gallery (specimens) section header. Title comes from .slop-section-heading;
|
||||
this block just scopes the lede + spacing under the heading. */
|
||||
.visual-mode-gallery-header {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.visual-mode-gallery-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.75rem, 3vw, 2.25rem);
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
color: var(--color-ink);
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.visual-mode-gallery-lede {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-charcoal);
|
||||
max-width: 60ch;
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
||||
+3
-5
@@ -60,9 +60,8 @@
|
||||
<a href="/" data-nav="home" aria-current="page">Home</a>
|
||||
<a href="/designing" data-nav="designing">Designing</a>
|
||||
<a href="/docs" data-nav="docs">Docs</a>
|
||||
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
|
||||
<a href="/slop" data-nav="slop">Slop</a>
|
||||
<a href="/live-mode" data-nav="live">Live</a>
|
||||
<a href="/visual-mode" data-nav="visual-mode">Overlay</a>
|
||||
</nav>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
@@ -338,7 +337,7 @@
|
||||
<span class="gallery-stack-label">Bad Contrast</span>
|
||||
</a>
|
||||
</div>
|
||||
<a class="gallery-stack-link" href="/anti-patterns">Browse the full catalog →</a>
|
||||
<a class="gallery-stack-link" href="/slop">Browse the full catalog →</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1155,9 +1154,8 @@
|
||||
<nav class="footer-links" aria-label="Footer">
|
||||
<a href="/designing">Designing</a>
|
||||
<a href="/docs">Docs</a>
|
||||
<a href="/anti-patterns">Anti-Patterns</a>
|
||||
<a href="/slop">Slop</a>
|
||||
<a href="/live-mode">Live Mode</a>
|
||||
<a href="/visual-mode">Overlay</a>
|
||||
<a href="#language">Commands</a>
|
||||
<a href="/privacy">Privacy</a>
|
||||
<a href="https://github.com/pbakaus/impeccable">GitHub</a>
|
||||
|
||||
@@ -32,9 +32,8 @@
|
||||
<a href="/" data-nav="home">Home</a>
|
||||
<a href="/designing" data-nav="designing">Designing</a>
|
||||
<a href="/docs" data-nav="docs">Docs</a>
|
||||
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
|
||||
<a href="/slop" data-nav="slop">Slop</a>
|
||||
<a href="/live-mode" data-nav="live" aria-current="page">Live</a>
|
||||
<a href="/visual-mode" data-nav="visual-mode">Overlay</a>
|
||||
</nav>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
|
||||
+1
-2
@@ -33,8 +33,7 @@
|
||||
<nav class="site-header-nav" aria-label="Primary">
|
||||
<a href="/" data-nav="home">Home</a>
|
||||
<a href="/docs" data-nav="docs">Docs</a>
|
||||
<a href="/anti-patterns" data-nav="anti-patterns">Anti-Patterns</a>
|
||||
<a href="/visual-mode" data-nav="visual-mode">Visual Mode</a>
|
||||
<a href="/slop" data-nav="slop">Slop</a>
|
||||
</nav>
|
||||
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 21k stars">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
||||
|
||||
+2
-8
@@ -22,17 +22,11 @@
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://impeccable.style/anti-patterns</loc>
|
||||
<lastmod>2026-04-10</lastmod>
|
||||
<loc>https://impeccable.style/slop</loc>
|
||||
<lastmod>2026-04-23</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://impeccable.style/visual-mode</loc>
|
||||
<lastmod>2026-04-10</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://impeccable.style/tutorials</loc>
|
||||
<lastmod>2026-04-10</lastmod>
|
||||
|
||||
+156
-130
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* Generate static HTML files for /skills, /anti-patterns, /tutorials.
|
||||
* 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/anti-patterns/,
|
||||
* public/tutorials/, all gitignored. Bun's HTML loader picks them up
|
||||
* the same way it picks up the hand-authored pages.
|
||||
* 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';
|
||||
@@ -425,31 +426,42 @@ function groupRulesBySection(rules) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 renderAntiPatternsSidebar(grouped) {
|
||||
const entries = grouped.order
|
||||
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 ` <li><a href="#section-${slug}"><span>${escapeHtml(section)}</span><span class="anti-patterns-sidebar-count">${count}</span></a></li>`;
|
||||
return ` <li><a href="#section-${slug}"><span>${escapeHtml(section)}</span><span class="anti-patterns-sidebar-count">${count}</span></a></li>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const catalogTotal = grouped.order
|
||||
.reduce((sum, s) => sum + (grouped.bySection[s]?.length || 0), 0);
|
||||
|
||||
return `
|
||||
<aside class="skills-sidebar anti-patterns-sidebar" aria-label="Anti-pattern sections">
|
||||
<button class="skills-sidebar-toggle" type="button" aria-expanded="false" aria-controls="anti-patterns-sidebar-inner">
|
||||
<span class="skills-sidebar-toggle-label">Sections</span>
|
||||
<aside class="skills-sidebar slop-sidebar" aria-label="Slop page sections">
|
||||
<button class="skills-sidebar-toggle" type="button" aria-expanded="false" aria-controls="slop-sidebar-inner">
|
||||
<span class="skills-sidebar-toggle-label">On this page</span>
|
||||
<svg class="skills-sidebar-toggle-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div class="skills-sidebar-inner" id="anti-patterns-sidebar-inner">
|
||||
<p class="skills-sidebar-label">Sections</p>
|
||||
<div class="skills-sidebar-inner" id="slop-sidebar-inner">
|
||||
<p class="skills-sidebar-label">On this page</p>
|
||||
<div class="skills-sidebar-group">
|
||||
<ul class="skills-sidebar-list anti-patterns-sidebar-list">
|
||||
${entries}
|
||||
<li><a href="#see-it"><span>See it</span></a></li>
|
||||
<li><a href="#try-it-live"><span>Try it live</span><span class="anti-patterns-sidebar-count">${gallerySize}</span></a></li>
|
||||
<li>
|
||||
<a href="#catalog"><span>The catalog</span><span class="anti-patterns-sidebar-count">${catalogTotal}</span></a>
|
||||
<ul class="slop-sidebar-sublist">
|
||||
${catalogEntries}
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#run-it"><span>Run it yourself</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -522,14 +534,38 @@ ${cards}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the /visual-mode page main content.
|
||||
* Render the /slop 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.
|
||||
* 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 renderVisualModeMain() {
|
||||
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 += `
|
||||
<section class="anti-patterns-section" id="section-${slug}">
|
||||
<header class="anti-patterns-section-header">
|
||||
<h3 class="anti-patterns-section-title">${escapeHtml(section)}</h3>
|
||||
<p class="anti-patterns-section-count">${rules.length} ${rules.length === 1 ? 'rule' : 'rules'}</p>
|
||||
</header>
|
||||
<div class="rule-card-grid">
|
||||
${rules.map(renderRuleCard).join('\n')}
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
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) => `
|
||||
<a class="gallery-card" href="/antipattern-examples/${item.id}.html">
|
||||
@@ -544,14 +580,15 @@ function renderVisualModeMain() {
|
||||
).join('\n');
|
||||
|
||||
return `
|
||||
<div class="visual-mode-page">
|
||||
<header class="visual-mode-page-header">
|
||||
<p class="sub-page-eyebrow">Live detection overlay</p>
|
||||
<h1 class="sub-page-title">Visual Mode</h1>
|
||||
<p class="sub-page-lede">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.</p>
|
||||
<div class="anti-patterns-content slop-content">
|
||||
<header class="anti-patterns-header slop-header">
|
||||
<p class="sub-page-eyebrow">The visible tells of AI design</p>
|
||||
<h1 class="sub-page-title">Slop</h1>
|
||||
<p class="sub-page-lede">${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 (<code>npx impeccable detect</code> or the browser extension); ${llmCount} need <a href="/docs/critique">/impeccable critique</a>'s LLM review pass.</p>
|
||||
</header>
|
||||
|
||||
<section class="visual-mode-demo-wrap" aria-label="Visual Mode demo">
|
||||
<section class="slop-section visual-mode-demo-wrap" id="see-it" aria-label="Detection overlay demo">
|
||||
<h2 class="slop-section-heading"><span class="slop-section-num">01</span> See it</h2>
|
||||
<div class="visual-mode-preview">
|
||||
<div class="visual-mode-preview-header">
|
||||
<span class="visual-mode-preview-dot red"></span>
|
||||
@@ -564,13 +601,49 @@ function renderVisualModeMain() {
|
||||
<p class="visual-mode-demo-caption">Hover or tap any outlined element to see which rule fired.</p>
|
||||
</section>
|
||||
|
||||
<section class="visual-mode-methods" aria-label="Where to run Visual Mode">
|
||||
<h2 class="visual-mode-methods-title">Three ways to run it</h2>
|
||||
<section class="slop-section visual-mode-gallery" id="try-it-live" aria-label="Try the overlay on synthetic specimens">
|
||||
<header class="visual-mode-gallery-header">
|
||||
<h2 class="slop-section-heading"><span class="slop-section-num">02</span> Try it live</h2>
|
||||
<p class="visual-mode-gallery-lede">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 hover the outlined elements.</p>
|
||||
</header>
|
||||
<div class="gallery-grid">
|
||||
${specimenCards}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slop-section slop-catalog" id="catalog" aria-label="Rule catalog">
|
||||
<header class="slop-catalog-header">
|
||||
<h2 class="slop-section-heading"><span class="slop-section-num">03</span> The catalog</h2>
|
||||
<p class="slop-catalog-lede">Every pattern <a href="/docs/impeccable">/impeccable</a> teaches against. <strong>AI slop</strong> rules flag the tells of AI-generated UIs; <strong>Quality</strong> rules flag general design mistakes that hurt regardless of who wrote them.</p>
|
||||
</header>
|
||||
|
||||
<details class="anti-patterns-legend">
|
||||
<summary class="anti-patterns-legend-summary">
|
||||
<span class="anti-patterns-legend-title">How to read this</span>
|
||||
<svg class="anti-patterns-legend-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</summary>
|
||||
<div class="anti-patterns-legend-body">
|
||||
<p>Each rule shows how it is detected:</p>
|
||||
<dl class="anti-patterns-legend-layers">
|
||||
<div><dt><span class="rule-card-layer" data-layer="cli">CLI</span></dt><dd>Deterministic. Runs from <code>npx impeccable detect</code> on files, no browser required.</dd></div>
|
||||
<div><dt><span class="rule-card-layer" data-layer="browser">Browser</span></dt><dd>Deterministic, but needs real browser layout. Runs via the browser extension or Puppeteer, not the plain CLI.</dd></div>
|
||||
<div><dt><span class="rule-card-layer" data-layer="llm">LLM only</span></dt><dd>No deterministic detector. Caught by <a href="/docs/critique">/impeccable critique</a> during its LLM design review.</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="anti-patterns-sections">
|
||||
${catalogSectionsHtml}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="slop-section visual-mode-methods" id="run-it" aria-label="Where to run the overlay">
|
||||
<h2 class="slop-section-heading"><span class="slop-section-num">04</span> Run it yourself</h2>
|
||||
<div class="visual-mode-methods-grid">
|
||||
<article class="visual-mode-method">
|
||||
<p class="visual-mode-method-label">Inside /impeccable critique</p>
|
||||
<h3 class="visual-mode-method-name"><a href="/docs/critique">/impeccable critique</a></h3>
|
||||
<p class="visual-mode-method-desc">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.</p>
|
||||
<p class="visual-mode-method-desc">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.</p>
|
||||
</article>
|
||||
<article class="visual-mode-method">
|
||||
<p class="visual-mode-method-label">Standalone CLI</p>
|
||||
@@ -584,16 +657,6 @@ function renderVisualModeMain() {
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="visual-mode-gallery" id="try-it-live" aria-label="Try it on synthetic specimens">
|
||||
<header class="visual-mode-gallery-header">
|
||||
<h2 class="visual-mode-gallery-title">Try it live</h2>
|
||||
<p class="visual-mode-gallery-lede">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.</p>
|
||||
</header>
|
||||
<div class="gallery-grid">
|
||||
${specimenCards}
|
||||
</div>
|
||||
</section>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -915,8 +978,8 @@ function renderDesigningMain() {
|
||||
<p class="designing-phase-sub">Run <code>/impeccable teach</code> once per project to establish PRODUCT.md and DESIGN.md. Then reach for <code>/impeccable craft</code> and describe what you want to build. Shape, build, and iterate happen inside one invocation.</p>
|
||||
<div class="designing-phase-commands">
|
||||
<a class="designing-phase-cmd" href="/docs/teach">/impeccable teach</a>
|
||||
<a class="designing-phase-cmd" href="/docs/craft">/impeccable craft</a>
|
||||
<a class="designing-phase-cmd" href="/docs/shape">/impeccable shape</a>
|
||||
<a class="designing-phase-cmd" href="/docs/craft">/impeccable craft</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -952,9 +1015,36 @@ function renderDesigningMain() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="designing-codex">
|
||||
<span class="designing-codex-eyebrow">A note on image-gen</span>
|
||||
<p class="designing-codex-body">Harnesses that can generate images take this further. On Codex and Gemini, craft generates reference visuals first, then implements from them. The mood board is built into the flow, not a separate step you have to remember.</p>
|
||||
<aside class="designing-visualize" aria-label="Visualize-first workflow with image generation">
|
||||
<header class="designing-visualize-head">
|
||||
<span class="designing-visualize-eyebrow">The new start</span>
|
||||
<h3 class="designing-visualize-title">Shape in pictures. <em>Craft</em> from them.</h3>
|
||||
<p class="designing-visualize-lede">Image generation is finally good enough to serve as a design brief. <code>/impeccable shape</code> drafts a brand toolkit you can review at a glance. <code>/impeccable craft</code> codes toward a hi-fi mock instead of a paragraph. The mood board lives inside the flow, on any harness that can generate images.</p>
|
||||
</header>
|
||||
|
||||
<div class="designing-visualize-spread">
|
||||
<figure class="designing-visualize-plate designing-visualize-plate--brand">
|
||||
<div class="designing-visualize-plate-frame">
|
||||
<img src="../assets/openai_image_2_brand.jpg" alt="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." loading="lazy" width="1280" height="854" />
|
||||
</div>
|
||||
<figcaption class="designing-visualize-plate-cap">
|
||||
<span class="designing-visualize-plate-kind">Shape</span>
|
||||
<p class="designing-visualize-plate-note">Brand toolkit. Identity, palette, type, icon language, applications, social tiles, UI direction. One plate, reviewable at a glance. Approved decisions get written into <code>DESIGN.md</code>.</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="designing-visualize-plate designing-visualize-plate--hifi">
|
||||
<div class="designing-visualize-plate-frame">
|
||||
<img src="../assets/openai_image_2_hifi.jpg" alt="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." loading="lazy" width="960" height="1280" />
|
||||
</div>
|
||||
<figcaption class="designing-visualize-plate-cap">
|
||||
<span class="designing-visualize-plate-kind">Craft</span>
|
||||
<p class="designing-visualize-plate-note">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.</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<p class="designing-visualize-foot">Plates above generated by <strong>OpenAI GPT Image 2</strong> in a single pass. <strong>Gemini Nano Banana Pro</strong>, <strong>Imagen 4 Ultra</strong>, and <strong>Grok Imagen</strong> work the same way, via Codex, Gemini CLI, and compatible harnesses.</p>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1244,61 +1334,6 @@ ${bodyHtml}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 += `
|
||||
<section class="anti-patterns-section" id="section-${slug}">
|
||||
<header class="anti-patterns-section-header">
|
||||
<h2 class="anti-patterns-section-title">${escapeHtml(section)}</h2>
|
||||
<p class="anti-patterns-section-count">${rules.length} ${rules.length === 1 ? 'rule' : 'rules'}</p>
|
||||
</header>
|
||||
<div class="rule-card-grid">
|
||||
${rules.map(renderRuleCard).join('\n')}
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
const detectedCount = grouped.order
|
||||
.flatMap((s) => grouped.bySection[s] || [])
|
||||
.filter((r) => r.layer !== 'llm').length;
|
||||
const llmCount = totalRules - detectedCount;
|
||||
|
||||
return `
|
||||
<div class="anti-patterns-content">
|
||||
<header class="anti-patterns-header">
|
||||
<p class="sub-page-eyebrow">${totalRules} rules</p>
|
||||
<h1 class="sub-page-title">Anti-patterns</h1>
|
||||
<p class="sub-page-lede">The full catalog of patterns <a href="/docs/impeccable">/impeccable</a> teaches against. ${detectedCount} are caught by a deterministic detector (<code>npx impeccable detect</code> or the browser extension). ${llmCount} can only be flagged by <a href="/docs/critique">/impeccable critique</a>'s LLM review pass. Want to see them live on real pages? Try <a href="/visual-mode">Visual Mode</a>, or iterate past them on your own dev server with <a href="/live-mode">Live Mode</a>.</p>
|
||||
</header>
|
||||
|
||||
<details class="anti-patterns-legend">
|
||||
<summary class="anti-patterns-legend-summary">
|
||||
<span class="anti-patterns-legend-title">How to read this</span>
|
||||
<svg class="anti-patterns-legend-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
||||
</summary>
|
||||
<div class="anti-patterns-legend-body">
|
||||
<p><strong>AI slop</strong> rules flag the visible tells of AI-generated UIs. <strong>Quality</strong> rules flag general design mistakes that are not AI-specific but still hurt the work. Each rule also shows how it is detected:</p>
|
||||
<dl class="anti-patterns-legend-layers">
|
||||
<div><dt><span class="rule-card-layer" data-layer="cli">CLI</span></dt><dd>Deterministic. Runs from <code>npx impeccable detect</code> on files, no browser required.</dd></div>
|
||||
<div><dt><span class="rule-card-layer" data-layer="browser">Browser</span></dt><dd>Deterministic, but needs real browser layout. Runs via the browser extension or Puppeteer, not the plain CLI.</dd></div>
|
||||
<div><dt><span class="rule-card-layer" data-layer="llm">LLM only</span></dt><dd>No deterministic detector. Caught by <a href="/docs/critique">/impeccable critique</a> during its LLM design review.</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="anti-patterns-sections">
|
||||
${sectionsHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point. Generates all sub-page HTML files.
|
||||
*
|
||||
@@ -1309,12 +1344,18 @@ 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'),
|
||||
slop: path.join(rootDir, 'public/slop'),
|
||||
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'),
|
||||
};
|
||||
// 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)) {
|
||||
@@ -1363,20 +1404,21 @@ export async function generateSubPages(rootDir) {
|
||||
generated.push(out);
|
||||
}
|
||||
|
||||
// Anti-patterns index: single page, docs-browser shell with TOC sidebar.
|
||||
// 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 = renderAntiPatternsSidebar(grouped);
|
||||
const main = renderAntiPatternsMain(grouped, data.rules.length);
|
||||
const sidebar = renderSlopSidebar(grouped, GALLERY_ITEMS.length);
|
||||
const main = renderSlopMain(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.`,
|
||||
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: 'anti-patterns',
|
||||
canonicalPath: '/anti-patterns',
|
||||
bodyClass: 'sub-page skills-layout-page anti-patterns-page',
|
||||
activeNav: 'slop',
|
||||
canonicalPath: '/slop',
|
||||
bodyClass: 'sub-page skills-layout-page slop-page',
|
||||
});
|
||||
const out = path.join(outDirs.antiPatterns, 'index.html');
|
||||
const out = path.join(outDirs.slop, 'index.html');
|
||||
fs.writeFileSync(out, html, 'utf-8');
|
||||
generated.push(out);
|
||||
}
|
||||
@@ -1398,25 +1440,9 @@ export async function generateSubPages(rootDir) {
|
||||
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.
|
||||
// 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 = `
|
||||
<link rel="stylesheet" href="../css/live-mode.css">
|
||||
|
||||
+6
-4
@@ -259,7 +259,7 @@ async function buildStaticSite(extraEntrypoints = []) {
|
||||
// Older Bun versions (e.g. the one Cloudflare Pages ships) don't dedupe
|
||||
// shared CSS/JS chunks across HTML entrypoints — every entry tries to
|
||||
// emit its own copy, and three different sub-pages all named index.html
|
||||
// (under skills/, tutorials/, anti-patterns/) collide on the same
|
||||
// (under docs/, tutorials/, slop/) collide on the same
|
||||
// chunk filename. Including [dir] in the chunk template scopes each
|
||||
// chunk to its entry's directory so the names stay unique even when
|
||||
// dedupe is off. Local Bun still emits a single shared chunk; CF Bun
|
||||
@@ -527,15 +527,17 @@ function generateCFConfig(buildDir) {
|
||||
fs.writeFileSync(path.join(buildDir, '_headers'), headers);
|
||||
|
||||
// _redirects: rewrite JSON API routes to static files (200 = rewrite, not redirect).
|
||||
// Also permanent redirects for legacy URLs: /skills -> /docs, /cheatsheet -> /docs.
|
||||
// Plus permanent redirects for legacy URLs.
|
||||
const redirects = `/api/skills /_data/api/skills.json 200
|
||||
/api/commands /_data/api/commands.json 200
|
||||
/api/patterns /_data/api/patterns.json 200
|
||||
/api/command-source/:id /_data/api/command-source/:id.json 200
|
||||
/gallery /visual-mode#try-it-live 301
|
||||
/gallery /slop#try-it-live 301
|
||||
/cheatsheet /docs 301
|
||||
/skills /docs 301
|
||||
/skills/:id /docs/:id 301
|
||||
/anti-patterns /slop#catalog 301
|
||||
/visual-mode /slop#see-it 301
|
||||
`;
|
||||
fs.writeFileSync(path.join(buildDir, '_redirects'), redirects);
|
||||
|
||||
@@ -557,7 +559,7 @@ function generateCFConfig(buildDir) {
|
||||
async function build() {
|
||||
console.log('🔨 Building cross-provider design skills...\n');
|
||||
|
||||
// Pre-generate sub-pages (skills, anti-patterns, tutorials) from source
|
||||
// Pre-generate sub-pages (docs, slop, tutorials, live-mode, designing) from source
|
||||
console.log('📝 Generating sub-pages...');
|
||||
const { files: subPageFiles } = await generateSubPages(ROOT_DIR);
|
||||
console.log(`✓ Generated ${subPageFiles.length} sub-page(s)\n`);
|
||||
|
||||
@@ -33,7 +33,7 @@ export function readHeaderPartial() {
|
||||
* the default nav href state. Matches on `data-nav="{activeNav}"`.
|
||||
*
|
||||
* @param {string} headerHtml
|
||||
* @param {string} activeNav - one of: home, skills, anti-patterns, tutorials, gallery, github
|
||||
* @param {string} activeNav - one of: home, designing, docs, slop, live, github
|
||||
* @returns {string}
|
||||
*/
|
||||
export function applyActiveNav(headerHtml, activeNav) {
|
||||
|
||||
@@ -272,8 +272,7 @@ export function writeFile(filePath, content) {
|
||||
*/
|
||||
// Curated short-list for the homepage Antidote section. Intentionally
|
||||
// hand-written (not auto-extracted) so the copy stays tight and
|
||||
// editorial. The long-form catalog lives on /anti-patterns — this is
|
||||
// the teaser.
|
||||
// editorial. The long-form catalog lives on /slop — this is the teaser.
|
||||
const CURATED_CATEGORIES = [
|
||||
{
|
||||
name: 'Typography',
|
||||
@@ -356,7 +355,7 @@ const CURATED_CATEGORIES = [
|
||||
|
||||
export function readPatterns(_rootDir, _relativePath) {
|
||||
// Hand-curated list — see CURATED_CATEGORIES above. The homepage
|
||||
// Antidote teaser uses this; the full catalog lives on /anti-patterns.
|
||||
// Antidote teaser uses this; the full catalog lives on /slop.
|
||||
return {
|
||||
patterns: CURATED_CATEGORIES.map((c) => ({ name: c.name, items: c.do })),
|
||||
antipatterns: CURATED_CATEGORIES.map((c) => ({ name: c.name, items: c.dont })),
|
||||
|
||||
+4
-3
@@ -44,9 +44,11 @@ const server = serve({
|
||||
|
||||
// Legacy URL redirects (kept stable for external links and existing users).
|
||||
"/cheatsheet": Response.redirect("/docs", 301),
|
||||
"/gallery": Response.redirect("/visual-mode#try-it-live", 301),
|
||||
"/gallery": Response.redirect("/slop#try-it-live", 301),
|
||||
"/skills": Response.redirect("/docs", 301),
|
||||
"/skills/:id": (req) => Response.redirect(`/docs/${req.params.id}`, 301),
|
||||
"/anti-patterns": Response.redirect("/slop#catalog", 301),
|
||||
"/visual-mode": Response.redirect("/slop#see-it", 301),
|
||||
|
||||
// Generated sub-pages — served directly from the pre-generated files
|
||||
"/docs": () => serveGenerated(path.join(ROOT_DIR, "public/docs/index.html")),
|
||||
@@ -54,8 +56,7 @@ const server = serve({
|
||||
const id = req.params.id.replace(/[^a-z0-9-]/gi, "");
|
||||
return serveGenerated(path.join(ROOT_DIR, `public/docs/${id}.html`));
|
||||
},
|
||||
"/anti-patterns": () => serveGenerated(path.join(ROOT_DIR, "public/anti-patterns/index.html")),
|
||||
"/visual-mode": () => serveGenerated(path.join(ROOT_DIR, "public/visual-mode/index.html")),
|
||||
"/slop": () => serveGenerated(path.join(ROOT_DIR, "public/slop/index.html")),
|
||||
"/live-mode": () => serveGenerated(path.join(ROOT_DIR, "public/live-mode/index.html")),
|
||||
"/designing": () => serveGenerated(path.join(ROOT_DIR, "public/designing/index.html")),
|
||||
"/tutorials": () => serveGenerated(path.join(ROOT_DIR, "public/tutorials/index.html")),
|
||||
|
||||
Reference in New Issue
Block a user