chore: remove internal design docs from git tracking
Build & Deploy / build-and-deploy (push) Successful in 1m51s
Build & Deploy / build-and-deploy (push) Successful in 1m51s
This commit is contained in:
@@ -1,399 +0,0 @@
|
||||
# Design System — rmendes.net
|
||||
|
||||
## Palette
|
||||
|
||||
**Surfaces:** Warm stone — not yellow, not cold. The difference is felt, not seen.
|
||||
|
||||
| Token | Hex | Role |
|
||||
|-------|-----|------|
|
||||
| surface-50 | `#faf8f5` | Canvas (light) |
|
||||
| surface-100 | `#f4f2ee` | Cards, elevated surfaces (light) |
|
||||
| surface-200 | `#e8e5df` | Standard borders, dividers |
|
||||
| surface-300 | `#d5d0c8` | Strong borders, input borders |
|
||||
| surface-400 | `#a09a90` | Muted text, placeholders |
|
||||
| surface-500 | `#7a746a` | Secondary text |
|
||||
| surface-600 | `#5c5750` | Supporting text |
|
||||
| surface-700 | `#3f3b35` | Dark mode borders |
|
||||
| surface-800 | `#2a2722` | Cards, elevated surfaces (dark) |
|
||||
| surface-900 | `#1c1b19` | Canvas (dark) |
|
||||
| surface-950 | `#0f0e0d` | Deepest dark |
|
||||
|
||||
**Accent (Warm Amber):** Default interactive color — links, CTAs, focus rings.
|
||||
|
||||
| Token | Hex | Usage |
|
||||
|-------|-----|-------|
|
||||
| accent-400 | `#fbbf24` | Dark mode: links, hover |
|
||||
| accent-500 | `#f59e0b` | — |
|
||||
| accent-600 | `#d97706` | Light mode: links, buttons |
|
||||
| accent-700 | `#b45309` | Light mode: hover, pressed |
|
||||
|
||||
## Domain Colors
|
||||
|
||||
Every post type and section has its own unique color identity. On collection pages, sparklines, card borders, icons, labels, hover states, and permalink links all use the domain color. No two post types share the same color.
|
||||
|
||||
### Post Type Colors (each unique — no sharing)
|
||||
|
||||
| Post Type | Tailwind color | Light text | Dark text | Icon | Pages |
|
||||
|-----------|---------------|------------|-----------|------|-------|
|
||||
| **Articles** | indigo | indigo-600 | indigo-400 | document | `/articles/` |
|
||||
| **Notes** | teal | teal-600 | teal-400 | chat bubble | `/notes/` |
|
||||
| **Bookmarks** | amber | amber-600 | amber-400 | bookmark | `/bookmarks/` |
|
||||
| **Likes** | red | red-600 | red-400 | heart (filled) | `/likes/` |
|
||||
| **Replies** | sky | sky-600 | sky-400 | reply arrow | `/replies/` |
|
||||
| **Reposts** | green | green-600 | green-400 | refresh arrows | `/reposts/` |
|
||||
| **Photos** | purple | purple-600 | purple-400 | camera | `/photos/` |
|
||||
|
||||
Each color is applied consistently across: sparkline wrapper, card `border-l`, SVG icon, label text, title hover, and permalink link.
|
||||
|
||||
### Section Colors (non-post-type pages)
|
||||
|
||||
| Section | Tailwind color | Light text | Dark text | Pages |
|
||||
|---------|---------------|------------|-----------|-------|
|
||||
| **Blog** (mixed) | amber | amber-600 | amber-400 | `/blog/` (sparkline only — individual cards use their post-type color) |
|
||||
| **Code** | emerald | emerald-600 | emerald-400 | `/github/`, `/github/starred/` |
|
||||
| **Music** | purple | purple-600 | purple-400 | `/funkwhale/`, `/listening/` |
|
||||
| **Video** | red | red-600 | red-400 | `/youtube/` |
|
||||
| **Reading** | orange | orange-600 | orange-400 | `/blogroll/`, `/podroll/`, `/readlater/` |
|
||||
| **Neutral** | accent (amber) | — | — | `/` (home), `/about/`, `/cv/`, `/slashes/`, `/search/`, `/changelog/`, `/404` |
|
||||
|
||||
### Brand Colors (hardcoded hex — not domain colors)
|
||||
|
||||
These are third-party brand colors used in syndication badges and social widgets. Not part of the domain system.
|
||||
|
||||
| Brand | Hex | Where |
|
||||
|-------|-----|-------|
|
||||
| Mastodon | `#a730b8` | Syndication badges, social-activity widget |
|
||||
| Bluesky | `#0085ff` | Syndication badges, social-activity widget |
|
||||
| LinkedIn | `#0a66c2` | Syndication badges |
|
||||
| IndieNews | `#ff5c00` | Syndication badges |
|
||||
| Mastodon alt | `#6364ff` | Syndication badges |
|
||||
|
||||
### Domain Prominence (medium)
|
||||
|
||||
On a domain page, these elements adopt the domain color:
|
||||
- **Page title** — tinted text or accent underline
|
||||
- **Links** — `text-[domain]-600 dark:text-[domain]-400`
|
||||
- **Hover states** — `hover:text-[domain]-700 dark:hover:text-[domain]-300`
|
||||
- **Card borders on hover** — `hover:border-[domain]-400 dark:hover:border-[domain]-600`
|
||||
- **Post-type badges** — domain-colored background/text
|
||||
|
||||
These stay neutral (accent or surface) regardless of domain:
|
||||
- Header/navigation
|
||||
- Sidebar widget containers
|
||||
- Footer
|
||||
- Global UI (search, theme toggle)
|
||||
|
||||
## Depth
|
||||
|
||||
**Subtle shadows.** One consistent shadow level for elevated surfaces. Borders + shadow together.
|
||||
|
||||
| Element | Treatment |
|
||||
|---------|-----------|
|
||||
| Cards, widgets | `shadow-sm` + `border border-surface-200 dark:border-surface-700` |
|
||||
| Avatars, album art | `shadow-lg` (depth gives images presence against flat surfaces) |
|
||||
| Modals | `shadow-xl` (overlay needs clear elevation) |
|
||||
| Hover on cards | No shadow change — border color shift only |
|
||||
|
||||
**Gradients are allowed** for:
|
||||
- Now-playing cards (domain color tinted: `bg-gradient-to-br from-[color]-500/10`)
|
||||
- YouTube hero/live cards
|
||||
- Icon containers in reading pages (`from-[color]-400 to-[color]-600`)
|
||||
|
||||
Gradients are NOT used for:
|
||||
- Standard cards, widgets, or page backgrounds
|
||||
|
||||
## Typography
|
||||
|
||||
| Role | Style | Usage |
|
||||
|------|-------|-------|
|
||||
| Page titles | Inter, `text-2xl sm:text-3xl font-bold` | Main page headings |
|
||||
| Section headings | Inter, `text-xl sm:text-2xl font-bold` | Widget titles, section headers |
|
||||
| Subheadings | Inter, `text-lg font-semibold` | Card titles, list item titles |
|
||||
| Body | Inter, `text-sm` or `text-base` | Paragraphs, descriptions |
|
||||
| Labels | Inter, `font-medium` or `font-semibold` | Badges, nav items, metadata labels |
|
||||
| **Dates/timestamps** | **`font-mono text-sm`** | Every `<time>` element, stat numbers, version numbers |
|
||||
| Code | `font-mono` | Commit SHAs, code blocks, technical identifiers |
|
||||
| Small text | `text-xs` | Metadata, secondary info, captions |
|
||||
|
||||
### Date treatment rule
|
||||
|
||||
Every rendered date (via `dateDisplay` or `date()` filter) gets `font-mono`. This adds technical texture throughout the site — like timestamps in a log.
|
||||
|
||||
### Weight scale
|
||||
|
||||
| Weight | Class | Frequency | Usage |
|
||||
|--------|-------|-----------|-------|
|
||||
| 400 | (default) | Body text | Paragraphs, descriptions |
|
||||
| 500 | `font-medium` | 146x | Labels, metadata, nav items |
|
||||
| 600 | `font-semibold` | 100x | Subheadings, emphasis |
|
||||
| 700 | `font-bold` | 138x | Page titles, section headings |
|
||||
|
||||
## Spacing
|
||||
|
||||
Base: 4px (Tailwind default rem scale).
|
||||
|
||||
### Spacing scale (by frequency)
|
||||
|
||||
| Token | px | Frequency | Primary usage |
|
||||
|-------|-----|-----------|---------------|
|
||||
| `0.5` | 2px | 62x | Micro gaps (badge padding-y, icon margins) |
|
||||
| `1` | 4px | 150x | Tight internal spacing |
|
||||
| `1.5` | 6px | 45x | Button padding-y, small gaps |
|
||||
| `2` | 8px | 350x+ | Standard small spacing (px, py, gap, m) |
|
||||
| `3` | 12px | 180x | Standard medium spacing |
|
||||
| `4` | 16px | 200x+ | Card padding, section gaps |
|
||||
| `5` | 20px | 30x | Featured card padding |
|
||||
| `6` | 24px | 80x | Section margins |
|
||||
| `8` | 32px | 40x | Large section separation |
|
||||
| `10` | 40px | 8x | Page-level vertical rhythm |
|
||||
| `12` | 48px | 5x | Major section breaks |
|
||||
|
||||
### Common spacing patterns
|
||||
|
||||
| Pattern | Classes | Where |
|
||||
|---------|---------|-------|
|
||||
| Card padding | `p-4` | Standard cards, widgets |
|
||||
| Compact padding | `p-3` | List items, tight cards |
|
||||
| Featured padding | `p-5` | Hero cards, featured items |
|
||||
| Tight list gap | `gap-2` | Inline elements, tag lists |
|
||||
| Standard gap | `gap-3` | Card grids, form elements |
|
||||
| Spacious gap | `gap-4` | Section-level grids |
|
||||
| Section break | `mb-6` to `mb-8` | Between page sections |
|
||||
| Badge padding | `px-2 py-0.5` | Small badges, pills |
|
||||
| Pill padding | `px-3 py-1.5` | Larger pills, filter buttons |
|
||||
|
||||
## Border Radius
|
||||
|
||||
| Element | Radius | Frequency |
|
||||
|---------|--------|-----------|
|
||||
| Cards, inputs, buttons | `rounded-lg` | 154x (dominant) |
|
||||
| Avatars, status dots, badges | `rounded-full` | 134x |
|
||||
| Featured/hero cards | `rounded-xl` | 23x |
|
||||
| Now-playing sections | `rounded-xl sm:rounded-2xl` | 2x |
|
||||
| Audio players | `rounded-md` | — |
|
||||
|
||||
## Card Patterns
|
||||
|
||||
Five distinct card variants used across the site:
|
||||
|
||||
### Standard card (`.post-card`)
|
||||
```
|
||||
p-5 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm
|
||||
hover: border-[domain]-400 dark:border-[domain]-600
|
||||
```
|
||||
Used for: blog post listings, search results
|
||||
|
||||
### Widget card (`.widget`)
|
||||
```
|
||||
p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm
|
||||
```
|
||||
Used for: sidebar widgets, info panels
|
||||
|
||||
### Compact list card
|
||||
```
|
||||
p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm
|
||||
```
|
||||
Used for: list view items in news/podroll, compact listings
|
||||
|
||||
### Featured card
|
||||
```
|
||||
p-5 sm:p-6 bg-gradient-to-br from-[color]-500/10 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm
|
||||
```
|
||||
Used for: now-playing, YouTube hero, featured items
|
||||
|
||||
### Stat card
|
||||
```
|
||||
p-3 sm:p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm text-center
|
||||
```
|
||||
Used for: statistics grids (GitHub, Funkwhale, Last.fm)
|
||||
|
||||
## Button Patterns
|
||||
|
||||
Six button variants:
|
||||
|
||||
### Primary action
|
||||
```
|
||||
px-4 py-2 bg-accent-600 hover:bg-accent-700 text-white rounded-lg font-medium
|
||||
focus:ring-2 focus:ring-accent-500 transition-colors
|
||||
```
|
||||
Used for: form submits, main CTAs
|
||||
|
||||
### Secondary action
|
||||
```
|
||||
px-3 py-1.5 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700
|
||||
border border-surface-200 dark:border-surface-700 rounded-lg text-sm font-medium
|
||||
focus:ring-2 focus:ring-accent-500 transition-colors
|
||||
```
|
||||
Used for: filter toggles, view mode switches, secondary actions
|
||||
|
||||
### Icon button
|
||||
```
|
||||
p-2 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-700
|
||||
focus:ring-2 focus:ring-accent-500 transition-colors
|
||||
```
|
||||
Used for: theme toggle, menu toggle, refresh buttons
|
||||
|
||||
### Domain-colored button
|
||||
```
|
||||
px-3 py-1.5 bg-[domain]-600 hover:bg-[domain]-700 text-white rounded-lg text-sm font-medium
|
||||
focus:ring-2 focus:ring-[domain]-500 transition-colors
|
||||
```
|
||||
Used for: domain-specific actions (e.g., orange "Load More" on podroll)
|
||||
|
||||
### Link button
|
||||
```
|
||||
text-[domain]-600 dark:text-[domain]-400 hover:text-[domain]-700 dark:hover:text-[domain]-300
|
||||
hover:underline font-medium transition-colors
|
||||
```
|
||||
Used for: inline actions, "View all" links
|
||||
|
||||
### Pagination button
|
||||
```
|
||||
px-3 py-1 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700
|
||||
border border-surface-200 dark:border-surface-700 rounded-lg text-sm
|
||||
focus:ring-2 focus:ring-accent-500 transition-colors
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
```
|
||||
Used for: pagination controls
|
||||
|
||||
## Badge/Pill Patterns
|
||||
|
||||
Four badge variants:
|
||||
|
||||
### Post-type badge
|
||||
```
|
||||
px-2 py-0.5 text-xs font-medium rounded-full
|
||||
bg-[domain]-100 dark:bg-[domain]-900/30 text-[domain]-700 dark:text-[domain]-300
|
||||
```
|
||||
Used for: post type indicators on cards
|
||||
|
||||
### Category tag
|
||||
```
|
||||
px-2 py-0.5 text-xs bg-surface-100 dark:bg-surface-800
|
||||
text-surface-600 dark:text-surface-400 rounded-full
|
||||
hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors
|
||||
```
|
||||
Used for: category tags, hashtags
|
||||
|
||||
### Status badge
|
||||
```
|
||||
px-2 py-0.5 text-xs font-medium rounded-full
|
||||
bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300
|
||||
```
|
||||
Variants: emerald (active/success), amber (warning), red (error)
|
||||
Used for: status indicators, sync state
|
||||
|
||||
### Syndication badge
|
||||
```
|
||||
px-2 py-0.5 text-xs font-medium rounded-full text-white
|
||||
bg-[brand-hex]
|
||||
```
|
||||
Used for: Mastodon/Bluesky/LinkedIn syndication indicators
|
||||
|
||||
## Layout Patterns
|
||||
|
||||
### Page layouts
|
||||
|
||||
| Layout | Classes | Usage |
|
||||
|--------|---------|-------|
|
||||
| Full-width | `max-w-7xl mx-auto px-4 sm:px-6` | Page container |
|
||||
| With sidebar | `.layout-with-sidebar` = `grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6 lg:gap-8` | Blog, post pages |
|
||||
| Content area | `.main-content` = `min-w-0` (prevents overflow in grid) | Main column |
|
||||
| Sidebar | `.sidebar` = `space-y-4 lg:space-y-6` | Sidebar column |
|
||||
| Centered narrow | `max-w-3xl mx-auto` | About, CV pages |
|
||||
|
||||
### Grid patterns
|
||||
|
||||
| Pattern | Classes | Usage |
|
||||
|---------|---------|-------|
|
||||
| Stats grid | `grid grid-cols-2 sm:grid-cols-4 gap-3` | Statistics panels |
|
||||
| Card grid | `grid grid-cols-1 sm:grid-cols-2 gap-4` | Card view mode |
|
||||
| Post list | `space-y-4` or `space-y-3` | List view mode |
|
||||
| Widget stack | `space-y-4 lg:space-y-6` | Sidebar widgets |
|
||||
|
||||
### Responsive breakpoints
|
||||
|
||||
| Breakpoint | px | Frequency | Purpose |
|
||||
|------------|-----|-----------|---------|
|
||||
| `sm:` | 640px | 170x+ | Primary responsive step (dominant) |
|
||||
| `md:` | 768px | 19x | Tablet-specific adjustments |
|
||||
| `lg:` | 1024px | 6x | Sidebar layout switch |
|
||||
|
||||
**Mobile-first:** Base styles are mobile. `sm:` is the primary breakpoint for responsive changes. Most layouts switch from stacked to side-by-side at `sm:`.
|
||||
|
||||
## Interaction States
|
||||
|
||||
Every interactive element needs:
|
||||
- **hover:** color shift (`transition-colors` — dominant at 131x)
|
||||
- **focus:** visible ring (`focus:ring-2 focus:ring-accent-500` or domain equivalent)
|
||||
- **active:** not currently implemented — add where it matters (buttons)
|
||||
|
||||
### Hover patterns
|
||||
|
||||
| Element | Hover treatment |
|
||||
|---------|----------------|
|
||||
| Text links | `hover:underline` (163x — dominant link hover) |
|
||||
| Card borders | `hover:border-[domain]-400 dark:hover:border-[domain]-600` |
|
||||
| Buttons (filled) | Background darken (`hover:bg-[color]-700`) |
|
||||
| Buttons (ghost) | `hover:bg-surface-200 dark:hover:bg-surface-700` |
|
||||
| Nav items | `hover:text-surface-900 dark:hover:text-surface-100` |
|
||||
|
||||
### Focus pattern
|
||||
|
||||
All interactive elements: `focus:ring-2 focus:ring-[domain]-500 rounded` (or `focus:ring-accent-500` on neutral pages).
|
||||
|
||||
### Transitions
|
||||
|
||||
Default: `transition-colors` (131x). No duration override — uses Tailwind default (150ms).
|
||||
|
||||
Exceptions:
|
||||
- Collapsible widgets: `transition-all` for height animation
|
||||
- Mobile menu: `transition-transform` for slide-in
|
||||
|
||||
## Dark Mode
|
||||
|
||||
- Class-based: `darkMode: "class"` — toggled via button in header
|
||||
- Surfaces invert: light canvas (`surface-50`) -> dark canvas (`surface-900`)
|
||||
- Cards: `surface-100` -> `surface-800`
|
||||
- Domain colors shift to 400-weight (brighter) in dark mode
|
||||
- Borders: `surface-200` -> `surface-700`
|
||||
- Shadows remain `shadow-sm` (less visible but still present for subtle lift)
|
||||
|
||||
### Dark mode pairs (reference)
|
||||
|
||||
| Light | Dark |
|
||||
|-------|------|
|
||||
| `bg-surface-50` | `dark:bg-surface-900` (canvas) |
|
||||
| `bg-surface-100` | `dark:bg-surface-800` (cards) |
|
||||
| `bg-surface-200` | `dark:bg-surface-700` (hover bg) |
|
||||
| `border-surface-200` | `dark:border-surface-700` |
|
||||
| `text-surface-900` | `dark:text-surface-100` (primary text) |
|
||||
| `text-surface-600` | `dark:text-surface-400` (secondary text) |
|
||||
| `text-[domain]-600` | `dark:text-[domain]-400` (domain color) |
|
||||
| `bg-[domain]-100` | `dark:bg-[domain]-900/30` (badge bg) |
|
||||
|
||||
## CSS Component Classes
|
||||
|
||||
Reusable utility classes defined in `css/tailwind.css`:
|
||||
|
||||
| Class | Definition | Usage |
|
||||
|-------|------------|-------|
|
||||
| `.widget` | `p-4 bg-surface-50 rounded-lg border shadow-sm` + dark | Sidebar widgets |
|
||||
| `.widget-title` | `text-lg font-semibold` | Widget headings |
|
||||
| `.widget-header` | `flex items-center justify-between mb-3` | Widget header row |
|
||||
| `.widget-collapsible` | Alpine.js collapsible wrapper | Expandable widgets |
|
||||
| `.post-card` | `p-5 bg-surface-50 rounded-lg border shadow-sm` + dark | Post listing cards |
|
||||
| `.post-list` | `space-y-4` | Post list container |
|
||||
| `.layout-with-sidebar` | `grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6 lg:gap-8` | Two-column layout |
|
||||
| `.main-content` | `min-w-0` | Main column (prevents grid overflow) |
|
||||
| `.sidebar` | `space-y-4 lg:space-y-6` | Sidebar stack |
|
||||
| `.share-post-btn` | Blue share button | Post sharing |
|
||||
| `.save-later-btn` | Accent save button | Read-later action |
|
||||
|
||||
## What Needs Implementation
|
||||
|
||||
Audit findings — remaining gaps between this system and the current code:
|
||||
|
||||
1. ~~**Domain colors on section pages**~~ — ✅ Done: all 7 post-type collections + blog.njk mixed view + recent-posts widget use per-type colors
|
||||
2. **Active states** — add to buttons where appropriate
|
||||
3. **Consistent card hover** — some older templates use `hover:border-surface-400` instead of domain-colored border hover
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"audit_id": "indiekit-eleventy-theme_20260307_r2",
|
||||
"target": "entire indiekit-eleventy-theme codebase",
|
||||
"wcag_level": "AA",
|
||||
"focus_areas": ["all"],
|
||||
"status": "complete",
|
||||
"started_at": "2026-03-07T00:00:00Z",
|
||||
"completed_at": "2026-03-07T00:00:00Z",
|
||||
"files_audited": 95,
|
||||
"issues_found": {
|
||||
"critical": 0,
|
||||
"serious": 1,
|
||||
"moderate": 3,
|
||||
"minor": 1
|
||||
},
|
||||
"criteria_checked": 28,
|
||||
"criteria_passed": 26,
|
||||
"compliance_status": "substantially_compliant",
|
||||
"previous_audit": {
|
||||
"audit_id": "indiekit-eleventy-theme_20260307",
|
||||
"issues_found": {
|
||||
"critical": 8,
|
||||
"serious": 12,
|
||||
"moderate": 22,
|
||||
"minor": 8
|
||||
},
|
||||
"criteria_passed": 15,
|
||||
"compliance_status": "needs_improvement"
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
# Eleventy Build Performance Optimization — Design Spec
|
||||
|
||||
**Date:** 2026-03-19
|
||||
**Goal:** Reduce production build time (GitHub Actions deploy)
|
||||
**Approach:** Measure first, fix known low-hanging fruit in the same pass
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The blog runs Eleventy 3 with a monolithic `eleventy.config.js`. Two `eleventy.before` hooks run outside Eleventy's own timer:
|
||||
|
||||
1. **OG image generation** — spawns a subprocess loop using Satori + resvg; manifest-based so incremental, but the loop still initializes every build.
|
||||
2. **Unfurl prefetch** — walks all content files, parses frontmatter, calls `prefetchUrl()` for every interaction URL (likes, bookmarks, replies, reposts). Network responses are disk-cached by `eleventy-fetch`, but the walk + parse + cache-hit overhead runs unconditionally on every build.
|
||||
|
||||
One filter is also a known repeated-work issue:
|
||||
|
||||
- **`hash` filter** — reads a file from disk and computes MD5 on every call. The same 2–3 paths are passed on every page render (hundreds of pages), with no caching.
|
||||
|
||||
Inspired by [rmendes.net — Optimizing Eleventy Build Performance](https://rmendes.net/articles/2026/03/10/optimizing-eleventy-build-performance/).
|
||||
|
||||
---
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Build timing instrumentation
|
||||
|
||||
**Purpose:** Establish a baseline and validate that fixes in changes 2 and 3 have real impact.
|
||||
|
||||
**Implementation:**
|
||||
- Add `console.time("[og] image generation")` / `console.timeEnd(...)` around the OG `eleventy.before` hook body in `eleventy.config.js`.
|
||||
- Add `console.time("[unfurl] prefetch")` / `console.timeEnd(...)` around the unfurl `eleventy.before` hook body.
|
||||
- No logic is touched; labels are prefixed with `[og]` and `[unfurl]` to match existing log conventions.
|
||||
|
||||
**How to use:**
|
||||
```bash
|
||||
DEBUG=Eleventy:Benchmark* npm run build 2>&1 | grep -E "Benchmark|og|unfurl"
|
||||
```
|
||||
|
||||
This captures both Eleventy's internal timings (collections, transforms, filters, rendering) and the two before-hook timings in a single build run.
|
||||
|
||||
**Note on OG subprocess output:** The OG hook uses `execFileSync` with `stdio: "inherit"`, which means the subprocess writes directly to the parent's stdout/stderr — its output bypasses any pipe or grep on the parent process. The `console.time("[og] image generation")` and `console.timeEnd(...)` calls on the parent process measure the full wall time of the entire subprocess-spawn loop and will appear in filtered output. Individual per-batch log lines from the subprocess itself (`[og] Generating...`) will appear unfiltered inline.
|
||||
|
||||
---
|
||||
|
||||
### 2. `hash` filter memoization
|
||||
|
||||
**Location:** `eleventy.config.js` — `addFilter("hash", ...)` at ~line 744
|
||||
|
||||
**Problem:** `readFileSync()` + MD5 on every call. With 500+ pages each calling `| hash` for 2–3 asset paths, this is ~1000+ redundant disk reads per build.
|
||||
|
||||
**Fix:** Wrap with a `Map` cache, cleared on `eleventy.before`. Declare `const _hashCache = new Map()` inside the config export function body, immediately before the `addFilter("hash", ...)` call — matching the placement of `_dateDisplayCache` at line 550 (not at module scope):
|
||||
|
||||
```js
|
||||
const _hashCache = new Map();
|
||||
eleventyConfig.on("eleventy.before", () => { _hashCache.clear(); });
|
||||
eleventyConfig.addFilter("hash", (filePath) => {
|
||||
if (_hashCache.has(filePath)) return _hashCache.get(filePath);
|
||||
try {
|
||||
const fullPath = resolve(__dirname, filePath.startsWith("/") ? `.${filePath}` : filePath);
|
||||
const result = createHash("md5").update(readFileSync(fullPath)).digest("hex").slice(0, 8);
|
||||
_hashCache.set(filePath, result);
|
||||
return result;
|
||||
} catch {
|
||||
return Date.now().toString(36);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- Cache is per-build (cleared on `eleventy.before`), so a CSS change during `npm run dev` is always picked up on the next rebuild.
|
||||
- Pattern is identical to existing `_dateDisplayCache` and `_isoDateCache`.
|
||||
|
||||
---
|
||||
|
||||
### 3. Unfurl prefetch manifest skip
|
||||
|
||||
**Location:** `eleventy.config.js` — the `async eleventy.before` hook at ~line 1421
|
||||
|
||||
**Problem:** The hook walks all content files and calls `prefetchUrl()` for every interaction URL on every build, even when no new interaction posts have been added since the last build.
|
||||
|
||||
**Fix:** A manifest file at `.cache/unfurl-manifest.json` tracks the set of previously prefetched URLs. On each build, only URLs absent from the manifest are prefetched. If no new URLs exist, the hook exits immediately after the walk.
|
||||
|
||||
**Manifest lifecycle:**
|
||||
- **First build / cold cache:** no manifest → full prefetch → manifest written with all URLs.
|
||||
- **Subsequent builds, no new posts:** manifest loaded → `newUrls` is empty → early return, no network calls.
|
||||
- **New interaction post added:** manifest loaded → new URL detected → only that URL prefetched → manifest updated.
|
||||
- **Manifest deleted / corrupted:** falls back to full prefetch (same as first build).
|
||||
|
||||
**Implementation sketch:**
|
||||
|
||||
```js
|
||||
eleventyConfig.on("eleventy.before", async () => {
|
||||
const contentDir = resolve(__dirname, "content");
|
||||
if (!existsSync(contentDir)) return;
|
||||
|
||||
// --- existing walk to collect `urls` Set (unchanged) ---
|
||||
|
||||
if (urls.size === 0) return;
|
||||
|
||||
// Load manifest of previously seen URLs
|
||||
const manifestPath = resolve(__dirname, ".cache", "unfurl-manifest.json");
|
||||
let seen = new Set();
|
||||
try {
|
||||
seen = new Set(JSON.parse(readFileSync(manifestPath, "utf-8")));
|
||||
} catch { /* first build */ }
|
||||
|
||||
const newUrls = [...urls].filter(u => !seen.has(u));
|
||||
|
||||
if (newUrls.length === 0) {
|
||||
console.log("[unfurl] No new URLs — skipping prefetch");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefetch only new URLs using existing batch logic
|
||||
// (replace `urlArray` with `newUrls` in the batch loop)
|
||||
|
||||
// Update manifest
|
||||
mkdirSync(resolve(__dirname, ".cache"), { recursive: true });
|
||||
writeFileSync(manifestPath, JSON.stringify([...urls]));
|
||||
});
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- Manifest stored in `.cache/` — same gitignore and passthrough-copy rules as existing unfurl cache.
|
||||
- `writeFileSync` is used (not async) to guarantee the manifest is written even if the process exits unexpectedly mid-batch. On failure, next build falls back to full prefetch.
|
||||
- The content walk still runs on every build (cheap: local FS reads of small frontmatter). Only the prefetch calls are skipped.
|
||||
- Writing `[...urls]` (not `[...seen, ...newUrls]`) is intentional: it prunes URLs for soft-deleted posts on the next prefetch-triggering build, preventing unbounded manifest growth.
|
||||
|
||||
---
|
||||
|
||||
## Files changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `eleventy.config.js` | Add timing to 2 before-hooks; memoize `hash` filter; add manifest skip to unfurl hook |
|
||||
|
||||
No new files. No new dependencies.
|
||||
|
||||
---
|
||||
|
||||
## Success criteria
|
||||
|
||||
1. `console.time` output visible in production build logs.
|
||||
2. `DEBUG=Eleventy:Benchmark*` output available for baseline comparison.
|
||||
3. On a build with no new posts: `[unfurl] No new URLs — skipping prefetch` logged.
|
||||
4. Hash filter: modify a referenced asset (e.g. `css/style.css`) between two builds and confirm the `?v=` query string changes in rendered HTML. No log output expected — verify via view-source or build diff.
|
||||
5. No change to rendered output.
|
||||
Reference in New Issue
Block a user