| `CLAUDE.md` | Engineering directive for AI-assisted development |
Install
npx skillscat add koolamusic/wpmigrate-skills Install via the SkillsCat registry.
WordPress-to-Jekyll Migration Playbook
A reusable guide for migrating a WordPress site (via static HTML clone) to Jekyll 4.4 + Tailwind CSS 3.4, deployed on Netlify. Based on the migration of andrewmiracle.com — 429 static HTML pages spanning 2012–2025, converted to a fully themed Jekyll site with dark mode, digital garden features, password-protected projects, and RSS feeds.
Source: 429 WordPress pages scraped to static HTML
Target: Jekyll 4.4.1 + Tailwind 3.4.17 + PostCSS
Deployment: Netlify (Ruby 3.2, Node 18)
Output: 178 blog posts, 50 lab projects, 10 talks, 3 notes, 8 drafts, 6 standalone pages
Prerequisites
System Dependencies
| Tool | Version | Purpose |
|---|---|---|
| Ruby | 3.2+ | Jekyll runtime |
| Bundler | latest | Ruby dependency management |
| Node.js | 18+ | Tailwind CSS compilation |
| pnpm | latest | Fast Node package manager |
| Python 3 | 3.8+ | Content extraction and cleanup scripts |
| BeautifulSoup4 | latest | HTML parsing in Python scripts |
Ruby Gems (Gemfile)
gem 'jekyll' # Static site generator
gem 'webrick' # Dev server (Ruby 3.x dropped it from stdlib)
gem 'jekyll-postcss-v2' # PostCSS/Tailwind integration
gem 'jekyll-feed' # RSS/Atom feed generation
gem 'jekyll-sitemap' # XML sitemap generation
gem 'jekyll-seo-tag' # Meta tags and structured data
gem 'logger' # Ruby 3.x stdlib extraction
gem 'csv' # Ruby 3.x stdlib extraction
gem 'base64' # Ruby 3.x stdlib extractionnpm Packages (package.json)
{
"devDependencies": {
"tailwindcss": "^3.4.17",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.7",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1"
}
}Bootstrap Commands
bundle install && pnpm install # Install all dependencies
bundle exec jekyll serve # Dev server at localhost:4000
bundle exec jekyll build # Production build to _site/
bundle exec jekyll build --drafts # Include _drafts/ contentPhase 1 — Source Acquisition
Scrape the live WordPress site to a local static HTML clone using HTTrack or wget. The goal is a complete mirror preserving the original URL structure.
Approach
# HTTrack example — mirror entire site
httrack "https://andrewmiracle.com" -O ./andrewmiracle.com \
--mirror --robots=0 --depth=10
# Alternative with wget
wget --mirror --convert-links --adjust-extension \
--page-requisites --no-parent https://andrewmiracle.comWhat You Get
andrewmiracle.com/
├── index.html # Homepage
├── 2025/02/26/post-slug/index.html # Blog posts (YYYY/MM/DD/slug/)
├── lab/project-name/index.html # Portfolio projects
├── talks/talk-name/index.html # Speaking engagements
├── whoami/index.html # Static pages
├── wp-content/uploads/ # All media files
└── ... # 429 HTML files totalKey Details
- WordPress uses
YYYY/MM/DD/slug/permalink structure — preserve this exactly - Images land in
wp-content/uploads/YYYY/MM/— these get reorganized later - Some projects may be password-protected (WordPress server-side auth) — the scraper captures only the public-facing page, not protected content
- Check for duplicate pages (e.g.,
/embed/variants of posts) that can be skipped
Phase 2 — Content Extraction
A Python script parses each scraped HTML file, classifies it by URL pattern, extracts frontmatter from meta tags, pulls body content from WordPress DOM structure, and writes Jekyll-compatible files.
Script: scripts/extract.py
Classification logic — route files to the correct Jekyll collection based on URL path:
| URL Pattern | Output | Collection |
|---|---|---|
/YYYY/MM/DD/slug/ |
_posts/YYYY-MM-DD-slug.html |
Blog posts |
/lab/slug/ |
_lab/slug.html |
Portfolio projects |
/talks/slug/ |
_talks/slug.html |
Speaking engagements |
/whoami/, /now/, etc. |
pages/slug.html |
Standalone pages |
| Everything else | Skip or manual review | — |
Frontmatter extraction — derive YAML frontmatter from HTML <meta> tags:
# OpenGraph tags → frontmatter fields
<meta property="og:title"> → title
<meta property="og:description"> → description
<meta property="og:image"> → image
<meta property="og:url"> → permalink
# Article tags → frontmatter fields
<meta property="article:published_time"> → date
<meta property="article:modified_time"> → last_modified_at
<meta property="article:tag"> → tags (multiple)
<meta property="article:section"> → categoriesContent extraction — isolate the article body from WordPress page chrome:
# Target: div.post-content between header.entry-header and footer.article-tags
# This strips navigation, sidebars, related posts, comments, and footer
content = soup.find("div", class_="post-content")Image handling:
- Prefer
data-srcoversrc(WordPress lazy-loading stores real URL indata-src) - Strip query params:
?resize=800,600&ssl=1→ clean URL - Rewrite paths:
wp-content/uploads/2024/01/photo.jpg→/assets/images/uploads/2024/01/photo.jpg - Download all images locally to
assets/images/uploads/YYYY/MM/
Output Summary
| Collection | Count | Format | Naming Convention |
|---|---|---|---|
_posts/ |
178 | HTML | YYYY-MM-DD-slug.html |
_lab/ |
50 | HTML | slug.html (no date prefix) |
_talks/ |
10 | HTML | slug.html |
_drafts/ |
8 | HTML | YYYY-MM-DD-slug.html |
pages/ |
6 | HTML | slug.html |
Phase 3 — Content Cleanup
A second Python script (scripts/clean_lab.py) tackles WordPress markup remnants that survive extraction. This is the most labor-intensive phase — WordPress themes (especially Visual Composer / WPBakery) inject deeply nested wrapper divs, custom classes, and inline styles.
Script: scripts/clean_lab.py
Run modes:
python3 scripts/clean_lab.py # Dry-run: prints what would change
python3 scripts/clean_lab.py --apply # Apply changes in-place
python3 scripts/clean_lab.py --backup # Backup to _lab_backup/ then apply
python3 scripts/clean_lab.py --file x.html # Process single fileCleanup Pipeline (19 steps)
The script uses BeautifulSoup and operates in strict order:
- Strip Gutenberg comments — Remove
<!-- wp:paragraph -->,<!-- /wp:image -->,<!-- wp:jetpack/slideshow {...} /-->etc. via regex - Strip malformed attributes — Remove orphaned
thb_animated_color="..."text from Visual Composer - Embed bare media URLs — Convert plain YouTube/Loom URLs on their own line to responsive
<iframe>embeds wrapped in<div class="aspect-video"> - Handle inline media URLs — Convert YouTube URLs mixed with other content
- Parse with BeautifulSoup — Switch from regex to DOM manipulation
- Detect Twitter embeds — Flag
blockquote.twitter-tweetfor widget script injection - Clean WordPress figures — Unwrap
figure.wp-block-imageto plain<img>, preserve<figcaption> - Remove decorative elements — Delete
a.btn-text,div.arrow,div.thb-divider-container,span.vc_sep_holder, etc. - Unwrap Visual Composer containers — Multiple passes to peel nested wrappers:
div.wpb_row,div.row-fluid,div.vc_inner,figure.wp-block-embed,figure.wp-block-gallery, etc. - Strip remaining VC/WP wrapper divs — Regex match against comprehensive class pattern list (handles
vc_custom_*,thb-*,wp-block-*, etc.) - Unwrap lightbox anchors —
<a>tags that only wrap an<img>pointing to the same image - Clean all images — Strip
data-src,srcset,sizes,width,height,decoding,fetchpriority,title; remove WP classes (wp-image-*,aligncenter,size-*, etc.); addloading="lazy"; remove inline styles - Unwrap style-only spans —
<span style="color:...">that only wrap text - Strip all inline styles — Remove
styleattribute from every element - Fix asterisk headings —
<h2><strong>*Title*</strong></h2>→<h2>Title</h2> - Remove duplicate
<h1>— If<h1>text matches frontmatter title, remove it (layout renders it) - Remove empty elements — Iteratively delete
<p>,<div>,<span>,<h1>–<h6>with no text and no meaningful children - Strip WP/VC IDs and classes — Final pass to remove
thb-*IDs andwp-element-*classes from all elements - Append Twitter widget script — If Twitter embeds were detected, add
<script async src="https://platform.twitter.com/widgets.js">at the end
Final Whitespace Pass
After DOM cleanup, a text-level pass collapses multiple blank lines, trims trailing whitespace, and ensures files end with a single newline.
Phase 4 — Jekyll Architecture
Directory Structure
wordkyll/
├── _config.yml # Site config, collections, defaults, plugins
├── _posts/ # 178 blog posts (HTML + Markdown)
├── _lab/ # 50 portfolio projects
├── _talks/ # 10 speaking engagements
├── _notes/ # 3 short-form notes (Markdown only)
├── _drafts/ # 8 unpublished posts
├── _layouts/ # 7 templates
│ ├── default.html # Base: <html>, <head>, fonts, Prism.js, SEO
│ ├── home.html # Homepage: hero, feature boxes, posts, talks
│ ├── post.html # Blog: breadcrumbs, growth stage, prose, related
│ ├── portfolio.html # Lab + Talks: password gate, external links
│ ├── note.html # Notes: tags, references grid
│ ├── garden.html # Garden posts: minimal, custom bg
│ └── page.html # Generic: title + prose content
├── _includes/ # 7 reusable components
│ ├── header.html # Nav, mobile menu, dark mode toggle
│ ├── footer.html # Newsletter form, social links, theme toggle
│ ├── post-card.html # Blog post card (used in grids)
│ ├── post-placeholder.html # Emoji-based fallback when no image
│ ├── portfolio-card.html # Project card for lab/talks
│ ├── breadcrumbs.html # Section breadcrumb navigation
│ └── longform-sidebar.html # Homepage desktop sidebar
├── _plugins/
│ └── sha256_filter.rb # Custom Liquid filter for password hashing
├── pages/ # 6 standalone pages
├── assets/
│ ├── css/main.css # Tailwind directives + custom CSS
│ └── images/uploads/ # All media, organized by YYYY/MM/
├── garden/index.html # Garden page with search JS
├── notes/index.html # Notes page with tag filter JS
├── scripts/ # Python migration/cleanup scripts
├── Gemfile # Ruby dependencies
├── package.json # Node dependencies
├── tailwind.config.js # Tailwind theme configuration
├── postcss.config.js # PostCSS pipeline
└── netlify.toml # Netlify deployment configCollection Configuration (_config.yml)
permalink: /:year/:month/:day/:title/ # Preserves WordPress URL structure
collections:
lab:
output: true
permalink: /lab/:title/
talks:
output: true
permalink: /talks/:title/
notes:
output: true
permalink: /notes/:title/
defaults:
- scope: { path: "", type: "posts" }
values: { layout: "post" }
- scope: { path: "", type: "lab" }
values: { layout: "portfolio" }
- scope: { path: "", type: "talks" }
values: { layout: "portfolio" } # Talks share portfolio layout
- scope: { path: "", type: "notes" }
values: { layout: "note" }
- scope: { path: "pages" }
values: { layout: "page" }Plugin Ecosystem
| Plugin | Purpose |
|---|---|
jekyll-postcss-v2 |
PostCSS/Tailwind integration during Jekyll build |
jekyll-feed |
RSS/Atom feeds (blog, lab, notes — 3 separate feeds) |
jekyll-sitemap |
Auto-generated sitemap.xml and robots.txt |
jekyll-seo-tag |
<meta> tags, Open Graph, Twitter Cards, JSON-LD |
sha256_filter.rb |
Custom Liquid filter: {{ password | sha256 }} for client-side password gating |
Layout Hierarchy
default.html ← Base: <html>, <head>, fonts, Prism.js, dark mode init, SEO
├── home.html ← Homepage: hero section, feature boxes, recent posts, talks
├── post.html ← Blog: breadcrumbs, growth stage badge, prose, references, related posts
├── portfolio.html ← Lab + Talks: password protection, external links, Schema.org
├── note.html ← Notes: tag badges, references grid
├── garden.html ← Garden posts: minimal layout, custom background
└── page.html ← Generic: title + prose content blockPhase 5 — Design System
Tailwind Configuration
The design system lives in tailwind.config.js with custom tokens for colors, fonts, and typography.
Font Families:
| Token | Family | Usage |
|---|---|---|
font-serif |
STIX Two Text | Headings, prose body, article content |
font-sans |
Noto Sans | Default body text, UI elements |
font-nav |
Shantell Sans | Nav labels, section headers, buttons |
font-mono |
Intel One Mono | Code blocks, dates, technical labels |
All loaded via Google Fonts CDN with <link rel="preconnect"> for performance.
Color System:
Accents: #6a9a7b (links, CTAs) → #5e8e6f (hover) → #dd8940 (alt/orange)
Surfaces: #ebedea (light bg) → #2d2d2b (dark bg) → #323230 (dark cards)
Text: #3a3f3b (body) → #3a4a40 (headers)
Links: #5e9a74 (default) → #b85f2c (hover)
Semantic: green/yellow/emerald scales (100–900) for growth stage badges
Gray: Warm green-tinted gray scale (50–900)Typography Plugin:
Custom prose-green variant configures link colors, code styling (background, border, border-radius, font), and pre block appearance. The prose-invert variant overrides for dark mode.
Applied to all article bodies as:
<div class="prose prose-lg prose-green dark:prose-invert max-w-none font-serif
prose-headings:font-serif prose-headings:font-normal
prose-a:text-accent prose-a:no-underline hover:prose-a:text-accent-hover
prose-code:font-mono prose-code:text-sm
prose-img:ring-1 prose-img:ring-black/10 dark:prose-img:ring-white/10">Dark Mode
- Strategy:
darkMode: 'class'in Tailwind config —.darkclass on<html> - No-flash init: Inline
<script>in<head>checkslocalStoragethenprefers-color-schemebefore first paint - Toggle:
toggleTheme()function inheader.htmlbound to 3 buttons (desktop, mobile menu, footer) - Storage:
localStorage.setItem('theme', 'dark' | 'light') - Pattern: Every element uses
class="light-value dark:dark-value"
// Inline in <head> — runs before body render
(function() {
var saved = localStorage.getItem('theme');
if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();Custom CSS (assets/css/main.css)
Beyond Tailwind utilities, custom CSS handles:
- Prism.js code blocks: Overrides for
pre[class*="language-"]with#2d2d2dbackground (light) and#1a1a1a(dark), plus zebra-stripe line backgrounds viarepeating-linear-gradient - Post placeholders:
linear-gradientbackgrounds using a CSS custom property--ph-huederived from title length, with separate gradients for light/dark and growth stages - Now page timeline: Vertical line with
::beforepseudo-element, dot markers onh2::before, responsive padding - Scrollbar hiding:
.scrollbar-hideutility for horizontal scroll containers
Responsive Strategy
Mobile-first using Tailwind default breakpoints:
sm: 640px — Tablets
md: 768px — Medium tablets
lg: 1024px — Laptops
xl: 1280px — DesktopsCommon patterns:
<!-- Container -->
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<!-- Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Type scale -->
<h1 class="text-3xl sm:text-4xl lg:text-5xl">
<!-- Nav visibility -->
<nav class="hidden sm:flex"> <!-- Desktop -->
<div class="sm:hidden"> <!-- Mobile -->Phase 6 — Feature Layer
Digital Garden (Growth Stages)
Posts can declare a growth_stage in frontmatter: seedling, blossoming, or flourishing. The post layout renders a colored badge with emoji:
| Stage | Emoji | Badge Colors |
|---|---|---|
| seedling | 🌱 | Green background (bg-green-100 text-green-800) |
| blossoming | 🌼 | Yellow background (bg-yellow-100 text-yellow-800) |
| flourishing | 🌳 | Emerald background (bg-emerald-100 text-emerald-800) |
The garden index page (garden/index.html) provides client-side search filtering by title, description, and content snippets, with load-more pagination (25 posts per page).
Password Protection
16 lab projects use protected: true in frontmatter. The protection mechanism:
_config.ymlstores the plaintext password (portfolio_password)- At build time,
_plugins/sha256_filter.rbhashes it:{{ site.portfolio_password | sha256 }} - The hash is embedded in the page's
<script>block - Content lives inside a
<template id="protected-content">(not rendered by default) - On form submit, the entered password is hashed client-side via
crypto.subtle.digest('SHA-256', ...) - If hashes match, content is cloned from the
<template>into the DOM sessionStoragepersists the unlock for the browser session
Limitation: The hash is visible in page source. This is adequate for casual gating, not real security.
Post Placeholders
When a post has no featured image, an emoji-based placeholder is generated:
- A hue value is derived from the post title length:
--ph-hue: {{ title.size | modulo: 360 }} - CSS
linear-gradientuses this hue for a subtle, unique background per post - Different gradients for light mode, dark mode, and blossoming stage
- Defined in
_includes/post-placeholder.htmlandassets/css/main.css
References System
Posts and notes can declare structured references in frontmatter:
references:
- title: "Resource Name"
url: "https://example.com"
description: "Why it's relevant"Rendered as a card grid in post.html and note.html with bookmark icons, hover states, and external link targets.
Intended Audience
Posts can declare intended_audience in frontmatter, rendered as a callout component above the article body:
intended_audience: "Engineers and product managers working on AI-powered tools"Notes Tag Filtering
The notes index page (notes/index.html) supports tag-based filtering with URL hash sync — clicking a tag updates the hash (#typography), and loading the page with a hash pre-selects that filter.
Phase 7 — Build & Deployment
Build Pipeline
Source files → Jekyll 4.4 → PostCSS (Tailwind + Autoprefixer) → _site/PostCSS config (postcss.config.js):
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
]
}Important: cssnano is intentionally excluded from the pipeline due to a csso/css-tree incompatibility. Do not re-add it.
Netlify Configuration (netlify.toml)
[build]
command = "bundle exec jekyll build"
publish = "_site"
[build.environment]
JEKYLL_ENV = "production"
RUBY_VERSION = "3.2.0"
NODE_VERSION = "18"Redirects
Preserve WordPress URL compatibility and handle renamed pages:
/connect-with-andrew/* → /connect/ (301)
/whoami/now/ → /now/ (301)
/feed/ → /feed.xml (301)
/* → /404.html (404)RSS Feeds
Three separate feeds via jekyll-feed:
| Feed | Path | Content |
|---|---|---|
| Blog | /feed.xml |
Latest 20 posts |
| Lab | /lab/feed.xml |
Portfolio projects |
| Notes | /notes/feed.xml |
Notes |
Generated Outputs
| File | Generator | Purpose |
|---|---|---|
/sitemap.xml |
jekyll-sitemap | XML sitemap for search engines |
/robots.txt |
jekyll-sitemap | Crawl directives, references sitemap |
/feed.xml |
jekyll-feed | Blog RSS (20 items) |
/lab/feed.xml |
jekyll-feed | Portfolio RSS |
/notes/feed.xml |
jekyll-feed | Notes RSS |
Phase 8 — Refinement
Responsiveness Audit
- Viewport overflow:
min-w-fullon fixed/absolute elements causes horizontal scroll on mobile — replaced withw-full - Mobile menu: Added
overflow-hiddenon<body>when open,divide-ynavigation, compressed spacing - Touch targets: Ensured all interactive elements meet 44px minimum touch target size
- Image containment:
prose-img:w-full prose-img:max-w-full prose-img:h-autoprevents images from overflowing containers
Dark Mode Code Blocks
Prism.js doesn't wrap individual lines in elements, making per-line styling impossible via selectors. Solution: repeating-linear-gradient on pre background creates zebra-stripe effect:
.prose pre {
background-image: repeating-linear-gradient(
180deg,
transparent, transparent 1.4rem,
rgba(0, 0, 0, 0.04) 1.4rem, rgba(0, 0, 0, 0.04) 2.8rem
) !important;
background-position: 0 1.25em;
background-size: 100% 2.8rem;
}Dark mode uses rgba(255, 255, 255, 0.03) and a darker base (#1a1a1a) with an inset box-shadow border.
SEO
- Meta descriptions on all pages via
{% seo %}plugin +descriptionfrontmatter - Open Graph and Twitter Card tags auto-generated
- Fallback OG image when no
page.imageis set article:tagandarticle:sectionmeta tags from frontmatter- Keywords meta tag from post tags
- Schema.org JSON-LD on portfolio pages
Content-to-Markdown Conversion
Select posts converted from legacy HTML to Markdown for easier editing. During conversion:
- Inline references moved to
referencesfrontmatter array intended_audiencemoved from content to frontmatter field- WordPress Gutenberg block comments stripped
Lessons Learned & Pitfalls
Build & Tooling
cssnano breaks with csso/css-tree — Installing cssnano pulls in csso which has an incompatibility with certain css-tree versions. CSS minification had to be dropped from the PostCSS pipeline. The site ships unminified CSS.
Tailwind arbitrary
calc()values fail with spaces —w-[calc(100%-2rem)]works butw-[calc(100% - 2rem)]does not compile. Remove spaces insidecalc()in arbitrary Tailwind values.jekyll-postcss-v2requires empty frontmatter — The CSS file (assets/css/main.css) must start with empty YAML frontmatter delimiters (---\n---) for Jekyll to process it through PostCSS. Without this, Tailwind directives pass through unprocessed.
WordPress Content
Visual Composer nesting is extreme — A single image can be wrapped in 5+ layers of
div.wpb_row > div.row-fluid > div.vc_inner > div.thb-image-inner > figure > a > img. The cleanup script needs multiple unwrapping passes (up to 5 iterations).WordPress artifacts persist in unexpected places — Social share buttons, portfolio navigation markup, Gutenberg comments, and VC decorative elements (
div.arrow,div.thb-divider-container) survive initial extraction and must be explicitly targeted.data-srcvssrcfor lazy-loaded images — WordPress lazy-loading plugins store the real image URL indata-srcand put a placeholder insrc. Always checkdata-srcfirst during extraction.Image query parameters must be stripped — WordPress appends
?resize=800,600&ssl=1to image URLs. These break when served statically and must be removed during extraction.Gutenberg block comments have varied syntax — Some are self-closing (
<!-- wp:jetpack/slideshow {...} /-->), some have content between open/close tags. The regex must handle both:<!--\s*/?wp:.*?-->.
Design & CSS
Inline
styleattributes override Tailwind dark mode — WordPress content often has inlinestyle="color: #333"on elements. These overridedark:text-creamclasses because inline styles have higher specificity. Must strip all inline styles during cleanup.Prism.js line styling requires background gradients — Prism doesn't wrap lines in elements, so you can't use
:nth-childfor zebra striping. Userepeating-linear-gradienton thepreelement's background-image, carefully calibrated to line-height.min-w-fullon positioned elements causes mobile overflow — Fixed or absolute elements withmin-w-fullextend beyond the viewport. Usew-fullinstead, and addoverflow-x-hiddenon<body>as a safety net.
Architecture
Client-side password protection is transparent — The SHA-256 hash is visible in page source. Anyone can extract it and compute the password (or just read the
<template>content). This is by design — adequate for "please don't casually browse this" gating, not real access control.Multiple collections can share a layout —
_lab/and_talks/both useportfolio.htmlvia_config.ymldefaults. This works well when the content structure is similar, avoiding layout duplication.Preserve WordPress permalink structure — Setting
permalink: /:year/:month/:day/:title/in_config.ymlensures all existing URLs continue working. Combined with Netlify redirects for renamed pages, this prevents 404s from external links.
File Reference
Quick-reference for all project files and when you'd touch them.
Configuration
| File | Purpose | When to Touch |
|---|---|---|
_config.yml |
Site config, collections, defaults, plugins, feed settings | Adding collections, changing permalinks, updating plugins |
Gemfile |
Ruby dependencies | Adding Jekyll plugins |
package.json |
Node dependencies, build scripts | Adding PostCSS plugins, updating Tailwind |
tailwind.config.js |
Colors, fonts, dark mode, typography plugin | Changing design tokens, adding content paths |
postcss.config.js |
PostCSS plugin pipeline | Adding/removing PostCSS plugins (do NOT add cssnano) |
netlify.toml |
Build command, env vars, redirects | Adding redirects, changing build settings |
Layouts
| File | Extends | Used By |
|---|---|---|
_layouts/default.html |
— | All other layouts |
_layouts/home.html |
default | Homepage (index.html) |
_layouts/post.html |
default | _posts/ |
_layouts/portfolio.html |
default | _lab/, _talks/ |
_layouts/note.html |
default | _notes/ |
_layouts/garden.html |
default | Garden-tagged posts |
_layouts/page.html |
default | pages/ |
Includes
| File | Used In | Parameters |
|---|---|---|
_includes/header.html |
default.html |
— |
_includes/footer.html |
default.html |
— |
_includes/post-card.html |
home.html, post.html |
post (object) |
_includes/post-placeholder.html |
post-card.html, layouts |
post, class, emoji_size |
_includes/portfolio-card.html |
home.html, experiments.html |
project (object) |
_includes/breadcrumbs.html |
post.html, portfolio.html, note.html |
section, section_url |
_includes/longform-sidebar.html |
home.html |
— |
Content Collections
| Directory | Count | Format | Layout |
|---|---|---|---|
_posts/ |
178 | HTML + Markdown | post |
_lab/ |
50 | HTML + Markdown | portfolio |
_talks/ |
10 | HTML | portfolio |
_notes/ |
3 | Markdown | note |
_drafts/ |
8 | HTML | post |
pages/ |
6 | HTML + Markdown | page/default |
Scripts
| File | Purpose |
|---|---|
scripts/clean_lab.py |
WordPress markup cleanup (BeautifulSoup-based, 19-step pipeline) |
Other
| File | Purpose |
|---|---|
_plugins/sha256_filter.rb |
Custom Liquid filter for password hashing |
assets/css/main.css |
Tailwind directives + custom CSS (Prism, placeholders, timeline) |
garden/index.html |
Garden page with client-side search and pagination |
notes/index.html |
Notes page with tag filtering and URL hash sync |
CLAUDE.md |
Engineering directive for AI-assisted development |