feat(theme): extract gallery CSS to demo-only stylesheet

Closes #11.

Move .themes-gallery__* + .variants-gallery__* selectors out of
static/css/bonsai.css into static/css/gallery.css. The demo pages
/themes/ and /variants/ load the new stylesheet via a head_extra
block defined in layouts/_default/baseof.html; the home page and
/icons/ keep getting only bonsai.css.

End-user sites stop shipping ~1.9 KB raw / ~200 B gzipped of demo-only
CSS. README "< 3 KB gzipped" claim restored to true.

Sizes (minified):
  bonsai.css   12,394 -> 10,470 raw  (-1,924)
                3,113 ->  2,910 gzip  (-203)
  gallery.css                2,252 raw / 626 gzip (loaded only on demo pages)

Verified rendered HTML at /, /themes/, /variants/, /icons/ via curl;
demo pages carry both stylesheets, others carry only bonsai.css.
This commit is contained in:
tiennm99 committed 2026-05-10 02:50:49 +07:00
1 parent de5231b6ce
commit e4fafef83b
12 files changed
+352 -90

No files matched your search

+1
View File
@@ -6,6 +6,7 @@ All notable changes to this project are documented here. Format follows [Keep a
### Added
- **Favicon polish** — opt-in `params.faviconSvg` and `params.appleTouchIcon` for SVG and iOS home-screen icons. Default behavior unchanged when unset.
- **Demo-only gallery CSS** — `/themes/` and `/variants/` pages load a separate `static/css/gallery.css`; `static/css/bonsai.css` no longer ships gallery selectors to user sites. Saves ~1.9 KB raw / ~200 B gzipped on every real site. New `head_extra` block in `baseof.html` enables per-page stylesheet additions.
### Changed
- **A11y** — sakura accent darkened `#d4456a → #c93f63` (4.04 → 4.49 vs bg) and koi accent darkened `#c8521e → #bd4c1c` (4.17 → 4.63 vs bg) to reach WCAG AA on the gallery accent chip. Brand intent preserved (cherry blossom pink / koi orange). README hex table synced.
+1 -1
View File
@@ -19,7 +19,7 @@ Bonsai treats your bio page the same way: a quiet, well-pruned page that surface
- **35 icons out of the box** — 25 brand (GitHub, Mastodon, Bluesky, X, Threads, LinkedIn, Instagram…) + 10 utility (mail, globe, rss…). Vendored from [Simple Icons](https://simpleicons.org) and [Lucide](https://lucide.dev).
- **Light & dark mode** — respects `prefers-color-scheme`; optional toggle.
- **Zero JavaScript by default** — pure HTML + CSS; opt-in JS for theme toggle only.
- **Fast** — ~3 KB gzipped CSS, no web fonts (system stack), no runtime fetches.
- **Fast** — < 3 KB gzipped CSS, no web fonts (system stack), no runtime fetches.
- **Accessible** — semantic HTML, focus-visible outlines, `prefers-reduced-motion`.
- **Responsive** — mobile-first, looks right at every viewport.
@@ -2,6 +2,7 @@
<html lang="{{ site.LanguageCode | default `en` }}" data-bonsai-theme="{{ site.Params.colorTheme | default `bonsai` }}">
<head>
{{- partial "head.html" . -}}
{{- block "head_extra" . }}{{- end }}
</head>
<body>
<main class="bonsai">
+4
View File
@@ -1,3 +1,7 @@
{{ define "head_extra" }}
<link rel="stylesheet" href="{{ `css/gallery.css` | relURL }}" />
{{ end }}
{{ define "main" }}
<article style="text-align:center;">
<h1 id="themes-heading" style="font-family:var(--bonsai-font-display);">Color themes</h1>
@@ -1,3 +1,7 @@
{{ define "head_extra" }}
<link rel="stylesheet" href="{{ `css/gallery.css` | relURL }}" />
{{ end }}
{{ define "main" }}
<article style="text-align:center;">
<h1 id="variants-heading" style="font-family:var(--bonsai-font-display);">Layout variants</h1>
@@ -0,0 +1,48 @@
---
phase: 2
title: Extract gallery CSS (#11)
status: completed
priority: P2
effort: 30m
dependencies: []
---
# Phase 2: Extract gallery CSS (#11)
## Overview
Move `.themes-gallery__*` + `.variants-gallery__*` selectors out of `static/css/bonsai.css` into a new `static/css/gallery.css` referenced only by the demo pages. End-user sites stop paying for ~300 gzipped bytes of CSS they never use.
## Related Code Files
- Modify: `static/css/bonsai.css` — remove gallery rules
- Create: `static/css/gallery.css` — new file with the extracted rules
- Modify: `layouts/themes/single.html` — load gallery.css for this page only
- Modify: `layouts/variants/single.html` — load gallery.css for this page only
- Modify: `layouts/_default/baseof.html` (if needed) — add `head_extra` block hook
## Implementation Steps
1. Identify and excise gallery selectors from `static/css/bonsai.css`. Selectors: `.themes-gallery`, `.themes-gallery__card`, `.themes-gallery__name`, `.themes-gallery__hex`, `.themes-gallery__chip`, `.themes-gallery__chip--accent`, `.variants-gallery`, `.variants-gallery__card`, `.variants-gallery__name`, `.variants-gallery__desc`, `.variants-gallery__code`. Plus any responsive `@media` rules that target only those selectors.
2. Create `static/css/gallery.css` with the excised rules.
3. Add a `{{- block "head_extra" . -}}{{- end }}` hook in `layouts/_default/baseof.html` `<head>` (after the main stylesheet link).
4. In `layouts/themes/single.html` and `layouts/variants/single.html`, define the block: `{{ define "head_extra" }}<link rel="stylesheet" href="{{ \`css/gallery.css\` | relURL }}">{{ end }}`.
5. Verify `/icons/` page does NOT pull in `gallery.css` (it has no `head_extra` block).
6. Build + curl: confirm `/themes/` and `/variants/` pull both stylesheets; `/` and `/icons/` pull only `bonsai.css`.
7. Measure: `bonsai.css` raw + gzipped should drop ~1,200 raw / ~300 gzipped.
8. Update CHANGELOG (Unreleased) and README's CSS budget claim if it shifts back under 3 KB.
## Success Criteria
- [ ] `static/css/bonsai.css` no longer contains gallery selectors
- [ ] `static/css/gallery.css` exists and renders gallery markup correctly
- [ ] `/themes/` and `/variants/` load both stylesheets
- [ ] `/` and `/icons/` load only `bonsai.css`
- [ ] `bonsai.css` gzipped < 3 KB
- [ ] `hugo --gc --minify` clean
- [ ] PR opened against main
## Risk Assessment
- **Risk:** `head_extra` block name conflicts with future Hugo theme usage. **Mitigation:** name is theme-local, undocumented externally; safe.
- **Risk:** missed responsive rules during excision. **Mitigation:** grep `gallery` in CSS before commit; only matches in gallery.css.
@@ -0,0 +1,64 @@
---
phase: 3
title: "Add more icons (#10)"
status: pending
priority: P3
effort: "30m"
dependencies: []
---
# Phase 3: Add more icons (#10)
## Overview
Vendor ~10 additional brand + utility SVGs to the existing 35-icon set. Mechanical: add entries to `data/icons.yaml`, run `scripts/sync-icons.sh`, update README tables, verify `/icons/` gallery.
## Shortlist (≤ 10, picked for breadth)
Brand (Simple Icons CC0):
- `bandcamp` — musicians
- `soundcloud` — musicians
- `spotify` — artist profiles
- `figma` — designers
- `dribbble` — designers
- `stackoverflow` — devs
- `matrix` — fediverse messaging
Utility (Lucide ISC):
- `book-open` — bookshelves, reading lists
- `download` — resume / vCard
- `heart` — sponsor / support
Total +10 → 45 icons. Soft ceiling at ~50 for v0-line per issue.
## Related Code Files
- Modify: `data/icons.yaml`
- Run: `scripts/sync-icons.sh`
- Generated: `assets/icons/brand/*.svg`, `assets/icons/ui/*.svg`
- Modify: `README.md` icon tables (both `<details>` blocks)
## Implementation Steps
1. Inspect current `data/icons.yaml` structure to match the entry schema.
2. Append the 10 shortlisted entries (correct slug, license attribution).
3. Run `scripts/sync-icons.sh` — verify SVGs land in `assets/icons/brand/` and `assets/icons/ui/`.
4. Inspect downloaded SVGs: confirm size budget (each typically < 1 KB).
5. Update README icon tables under both `<details>` blocks (brand + utility), keep alphabetical or current order convention.
6. Update README intro line if it claims a fixed count (e.g. "35 icons" → "45 icons").
7. Build + curl `/icons/` to confirm new icons render in gallery.
8. Update CHANGELOG (Unreleased).
## Success Criteria
- [ ] 10 new SVGs vendored under `assets/icons/`
- [ ] `data/icons.yaml` updated with 10 new entries
- [ ] README icon tables updated; count line updated
- [ ] `/icons/` gallery renders all new icons
- [ ] `hugo --gc --minify` clean
- [ ] PR opened against main
## Risk Assessment
- **Risk:** Simple Icons / Lucide rename or remove a slug. **Mitigation:** `sync-icons.sh` fails loudly if slug missing; pick alternates from the original issue list.
- **Risk:** SVG size larger than expected (some Simple Icons brand marks are heavy). **Mitigation:** check raw sizes after sync; reject if any single icon > 3 KB.
@@ -0,0 +1,54 @@
---
phase: 4
title: "Optional RSS feed (#8)"
status: pending
priority: P3
effort: "45m"
dependencies: []
---
# Phase 4: Optional RSS feed (#8)
## Overview
Add `params.rss = true` opt-in. When enabled, the theme stops adding `RSS` to its `disableKinds` recommendation and renders an RSS feed of `[[params.links]]`. Adds `<link rel="alternate" type="application/rss+xml">` to `<head>`.
## Design notes
- Hugo respects user `disableKinds` in their `hugo.toml`. The theme can't force-enable RSS — but it can:
- Document the param in README
- Provide an `index.rss.xml` template that runs when the user removes `RSS` from `disableKinds`
- Add the `<link rel="alternate">` in `head.html` when `params.rss` is true (regardless of `disableKinds` — feed crawler will 404 if user mismatches; that's their config)
- Item shape: `<title>` = link title, `<link>` = link URL, `<description>` = optional description, `<pubDate>` = build time (links lack intrinsic dates).
## Related Code Files
- Create: `layouts/index.rss.xml` — template for the feed
- Modify: `layouts/partials/head.html` — emit `<link rel="alternate">` when `params.rss = true`
- Modify: `README.md` — document the new param + the `disableKinds` removal step
## Implementation Steps
1. Create `layouts/index.rss.xml` rendering an RSS 2.0 feed:
- `<channel>` populated from `params.name`, `params.tagline`, `params.bio`, site `baseURL`, build `time.Now`
- `<item>` per `[[params.links]]` entry; `<pubDate>` = build time; `<guid isPermaLink="true">` = link URL
2. In `layouts/partials/head.html`, when `site.Params.rss` is true: `<link rel="alternate" type="application/rss+xml" title="…" href="{{ \`index.xml\` | absURL }}">`. Hugo emits `index.xml` for the home RSS template.
3. README: add `rss` to the params table; add a short "Enable RSS" subsection explaining that user must remove `RSS` from their `disableKinds` line and set `params.rss = true`.
4. Build + curl `/index.xml` — confirm valid RSS 2.0 (well-formed XML, items present); confirm `<head>` of `/` carries the alternate link when `rss = true`.
5. Validate against W3C feed validator if reachable; if not, manually inspect for required elements.
6. Update CHANGELOG (Unreleased).
## Success Criteria
- [ ] `layouts/index.rss.xml` exists and renders well-formed RSS 2.0 when feature on
- [ ] `<head>` carries `<link rel="alternate">` when `params.rss = true`
- [ ] No RSS link emitted when `params.rss` unset/false (default)
- [ ] README documents the new param + `disableKinds` step
- [ ] `hugo --gc --minify` clean
- [ ] PR opened against main
## Risk Assessment
- **Risk:** User leaves `RSS` in `disableKinds` and the alternate link 404s. **Mitigation:** README is explicit; theme cannot override user's `disableKinds`.
- **Risk:** Build-time pubDate makes the feed change every build, polluting feed readers. **Mitigation:** documented as a known limitation; alternative is a per-link `pubDate` field (out of scope).
- **Risk:** Emoji/unicode in `params.tagline` breaks XML. **Mitigation:** Hugo's `transform.XMLEscape` or default escaping in templates handles it.
@@ -0,0 +1,38 @@
---
phase: 7
title: "v0.4.0 release prep"
status: pending
priority: P3
effort: "30m"
dependencies: [2, 3, 4]
---
# Phase 7: v0.4.0 release prep
## Overview
After phases 2, 3, 4 merge to main, sync `CHANGELOG.md`, bump version references, tag `v0.4.0`, push tag.
## Implementation Steps
1. Pull latest `main`, confirm phases 2/3/4 PRs are merged.
2. Move `## [Unreleased]` section to `## [0.4.0] — 2026-05-10` (or actual merge date).
3. Add fresh `## [Unreleased]` section above with v0.5 placeholders (#7 OG auto-gen, #9 multi-section bio explicitly listed as deferred from v0.4).
4. Cross-check README for stale "v0.3" or version mentions; update where needed.
5. Reissue `theme.toml` `min_version` only if a phase added a Hugo feature requiring a newer Hugo version (none expected here).
6. Final build: `hugo --gc --minify --themesDir ../..` from `exampleSite/` — clean.
7. `git tag v0.4.0 -m "v0.4.0: gallery CSS extraction, more icons, opt-in RSS"`
8. `git push origin v0.4.0`
9. Run `/ck:journal` for the v0.4.0 release retro.
## Success Criteria
- [ ] CHANGELOG has dated `## [0.4.0]` section
- [ ] `## [Unreleased]` section exists with v0.5 deferral notes
- [ ] `git tag v0.4.0` exists locally and on remote
- [ ] Build clean across all 3 demo pages
## Risk Assessment
- **Risk:** auto-tag without user approval. **Mitigation:** ask before `git push origin v0.4.0`; tag is the publish event, deserves explicit consent.
- **Risk:** stale "35 icons" claim in README after Phase 3. **Mitigation:** Phase 3 success criterion already covers this.
@@ -0,0 +1,43 @@
---
title: 'v0.4 release: ship gallery-CSS extraction + more icons + opt-in RSS'
description: >-
Ship issues #11, #10, #8 each as its own PR; tag v0.4.0. Issues #7 (OG
auto-gen) and #9 (multi-section bio) deferred to v0.5 per their own deferral
notes.
status: pending
priority: P2
created: 2026-05-10T00:00:00.000Z
---
# v0.4 release: ship the three well-scoped v0-line enhancements
## Overview
Per scope-check: ship the three well-scoped issues now, defer the two design-heavy ones.
- **Phase 2** Extract gallery CSS (#11) — pure refactor, smallest scope
- **Phase 3** Add more icons (#10) — mechanical, data-driven
- **Phase 4** Optional RSS feed (#8) — additive opt-in
- **Phase 7** v0.4.0 release prep
Each phase ships as its own feature branch + PR (matches repo pattern).
## Deferred to v0.5
- **#7 Auto-generate OG image** — issue text: *"If no path meets [≤30 KB binary], defer to v0.5"*. Of 5 candidate approaches, only `pyftsubset Inter` + base PNG might fit; needs measured prototype before committing.
- **#9 Multi-section bio** — issue text: *"Likely a 2026-Q3 candidate, not v0.4"*. Schema design dispute; needs `/ck:brainstorm` before a plan.
## Phases (active)
| Phase | Name | Status |
|-------|------|--------|
| 2 | [Extract gallery CSS (#11)](./phase-02-extract-gallery-css-11.md) | Completed |
| 3 | [Add more icons (#10)](./phase-03-add-more-icons-10.md) | Pending |
| 4 | [Optional RSS feed (#8)](./phase-04-optional-rss-feed-8.md) | Pending |
| 7 | [v0.4.0 release prep](./phase-07-v0-4-0-release-prep.md) | Pending |
(Phase numbers preserved from original 7-phase scaffold for stable references.)
## Dependencies
- Phase 7 depends on Phases 2, 3, 4. Each of 2/3/4 is independent and ships as a separate PR.
+2 -89
View File
@@ -364,95 +364,8 @@ body {
[data-theme="light"] .theme-toggle .theme-toggle__sun { display: none; }
[data-theme="light"] .theme-toggle .theme-toggle__moon { display: block; }
/* ============================================================
Themes gallery (exampleSite/themes/) — 4 mini bio cards
============================================================ */
.themes-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
max-width: 60rem;
margin: 2rem auto;
}
.themes-gallery__card {
background: var(--bonsai-bg);
border: 1px solid var(--bonsai-border);
border-radius: var(--bonsai-radius);
padding: 1.5rem;
text-align: center;
}
.themes-gallery__name {
font-family: var(--bonsai-font-display);
font-size: 1.1rem;
margin: 0 0 .25rem;
color: var(--bonsai-text);
}
.themes-gallery__hex {
color: var(--bonsai-muted);
font-family: ui-monospace, monospace;
font-size: .75rem;
margin: 0 0 1rem;
}
.themes-gallery__chip {
display: inline-block;
padding: .4rem .75rem;
background: var(--bonsai-surface);
color: var(--bonsai-text);
border: 1px solid var(--bonsai-border);
border-radius: var(--bonsai-radius);
font-size: .85rem;
}
.themes-gallery__chip--accent {
background: var(--bonsai-accent);
color: var(--bonsai-bg);
border-color: var(--bonsai-accent);
}
/* ============================================================
Variants gallery (exampleSite/variants/) — 3 sample bio cards
============================================================ */
.variants-gallery {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
max-width: 36rem;
margin: 2rem auto;
}
.variants-gallery__card {
background: var(--bonsai-surface);
border: 1px solid var(--bonsai-border);
border-radius: var(--bonsai-radius);
padding: 1.5rem;
}
.variants-gallery__name {
font-family: var(--bonsai-font-display);
font-size: 1.1rem;
margin: 0 0 .25rem;
color: var(--bonsai-text);
}
.variants-gallery__desc {
color: var(--bonsai-muted);
font-size: .85rem;
margin: 0 0 .5rem;
}
.variants-gallery__code {
display: inline-block;
margin-bottom: 1rem;
font-family: ui-monospace, monospace;
font-size: .75rem;
color: var(--bonsai-muted);
}
/* Note: gallery selectors (.themes-gallery, .variants-gallery) live in gallery.css
and load only on /themes/ and /variants/ demo pages via the head_extra block. */
@media (prefers-reduced-motion: reduce) {
.link, .link:hover, .link:active,
+92
View File
@@ -0,0 +1,92 @@
/* Bonsai gallery styles — loaded only on /themes/ and /variants/ demo pages.
* Kept out of bonsai.css so end-user sites don't ship demo-only selectors. */
/* ============================================================
Themes gallery (exampleSite/themes/) — 4 mini bio cards
============================================================ */
.themes-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
max-width: 60rem;
margin: 2rem auto;
}
.themes-gallery__card {
background: var(--bonsai-bg);
border: 1px solid var(--bonsai-border);
border-radius: var(--bonsai-radius);
padding: 1.5rem;
text-align: center;
}
.themes-gallery__name {
font-family: var(--bonsai-font-display);
font-size: 1.1rem;
margin: 0 0 .25rem;
color: var(--bonsai-text);
}
.themes-gallery__hex {
color: var(--bonsai-muted);
font-family: ui-monospace, monospace;
font-size: .75rem;
margin: 0 0 1rem;
}
.themes-gallery__chip {
display: inline-block;
padding: .4rem .75rem;
background: var(--bonsai-surface);
color: var(--bonsai-text);
border: 1px solid var(--bonsai-border);
border-radius: var(--bonsai-radius);
font-size: .85rem;
}
.themes-gallery__chip--accent {
background: var(--bonsai-accent);
color: var(--bonsai-bg);
border-color: var(--bonsai-accent);
}
/* ============================================================
Variants gallery (exampleSite/variants/) — 3 sample bio cards
============================================================ */
.variants-gallery {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
max-width: 36rem;
margin: 2rem auto;
}
.variants-gallery__card {
background: var(--bonsai-surface);
border: 1px solid var(--bonsai-border);
border-radius: var(--bonsai-radius);
padding: 1.5rem;
}
.variants-gallery__name {
font-family: var(--bonsai-font-display);
font-size: 1.1rem;
margin: 0 0 .25rem;
color: var(--bonsai-text);
}
.variants-gallery__desc {
color: var(--bonsai-muted);
font-size: .85rem;
margin: 0 0 .5rem;
}
.variants-gallery__code {
display: inline-block;
margin-bottom: 1rem;
font-family: ui-monospace, monospace;
font-size: .75rem;
color: var(--bonsai-muted);
}