mirror of
https://github.com/tiennm99/iammiti99.git
synced 2026-10-14 04:16:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feeb6a8731 | ||
|
|
d8c6e8a419 | ||
|
|
35dfa8f3dc | ||
|
|
698f39392f | ||
|
|
689bb80edb | ||
|
|
086b675c31 | ||
|
|
8bdcc2369a | ||
|
|
19f6b85316 | ||
|
|
7809c0b4c8 | ||
|
|
949d73c141 |
No files matched your search
+2
-2
@@ -17,7 +17,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
run: hugo --gc --minify --themesDir ../.. --printPathWarnings
|
||||
|
||||
- name: Upload built site
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: exampleSite-public
|
||||
path: exampleSite/public
|
||||
|
||||
+4
-4
@@ -18,13 +18,13 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- id: pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Setup Hugo Extended
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
--baseURL "${{ steps.pages.outputs.base_url }}/"
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: exampleSite/public
|
||||
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -5,7 +5,25 @@ All notable changes to this project are documented here. Format follows [Keep a
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- (placeholder for v0.3 — layout variants, i18n support)
|
||||
- (placeholder for v0.4 — auto-generated OG images, RSS opt-in, multi-section bio)
|
||||
|
||||
## [0.3.0] — 2026-05-03
|
||||
|
||||
Layout, social-preview, and i18n release. All additions are strictly opt-in; v0.2 sites upgrade with no config edits.
|
||||
|
||||
### Added
|
||||
- **Layout variants** — `params.layout` accepts `stack` (default), `grid` (responsive 2-col), or `inline` (icon-only horizontal row). Pure CSS, zero JS. Unknown values warn at build time and fall back to `stack`. Live gallery at `/variants/`.
|
||||
- **OG image controls** — `params.ogImageUrl` for an explicit 1200×630 social-preview image (auto-upgrades `twitter:card` to `summary_large_image`); `params.ogImage = false` to suppress all `og:image` / `twitter:image` tags. Avatar still serves as the fallback when neither is set.
|
||||
- **i18n string externalization** — every theme-rendered string (`nav` aria-label, theme-toggle labels, default footer template) sourced from `i18n/{lang}.toml`. Bundles for `en` (default) and `vi` ship with the theme. Adding a language is a single-file change.
|
||||
|
||||
### Changed
|
||||
- `layouts/partials/bio-card.html` validates `params.layout` against the allowlist and emits `bio__links bio__links--{layout}` so variant rules scope under a parent class (avoids the v0.2-style specificity collision).
|
||||
- `layouts/partials/head.html` resolves OG image via the new `ogImage`/`ogImageUrl` ladder; emits `twitter:image` whenever `og:image` is present.
|
||||
- `layouts/partials/footer.html` uses `i18n "footer_default"` when no `params.footerText` override is set.
|
||||
- `exampleSite/hugo.toml` switches the demo to `layout = "grid"` to make the new feature visible on the live site.
|
||||
|
||||
### Deferred
|
||||
- Auto-generated OG images (text overlay on per-palette base PNGs). Required vendoring a TTF + 4 base PNGs (~150 KB binary) into a theme that prides itself on `< 4 KB CSS`. Moved to v0.4 once a smaller path is found.
|
||||
|
||||
## [0.2.0] — 2026-05-03
|
||||
|
||||
|
||||
+41
-2
@@ -6,7 +6,7 @@
|
||||
|
||||
A minimalist Hugo theme for link-in-bio pages, inspired by [Linktree](https://linktr.ee) and the Japanese art of [bonsai](https://en.wikipedia.org/wiki/Bonsai) — *small, curated, intentional*.
|
||||
|
||||
**→ [Live demo](https://tiennm99.github.io/bonsai/)** · **[Icon gallery](https://tiennm99.github.io/bonsai/icons/)**
|
||||
**→ [Live demo](https://tiennm99.github.io/bonsai/)** · **[Icon gallery](https://tiennm99.github.io/bonsai/icons/)** · **[Layout variants](https://tiennm99.github.io/bonsai/variants/)** · **[Color themes](https://tiennm99.github.io/bonsai/themes/)**
|
||||
|
||||
> 盆栽 (bonsai): "tray planting" — the art of growing miniature trees through patient, deliberate cultivation. Every branch placed with care.
|
||||
|
||||
@@ -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** — < 4 KB CSS, < 4 KB HTML, 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.
|
||||
|
||||
@@ -93,7 +93,10 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
|
||||
| `avatarBg` | string (CSS color) | `var(--bonsai-accent)` | Background color of the initials circle. |
|
||||
| `favicon` | string (URL) | `/favicon.ico` | Favicon path. |
|
||||
| `colorTheme` | string | `bonsai` | Palette: `bonsai`, `sakura`, `sumi`, or `koi`. See [Color themes](#color-themes). |
|
||||
| `layout` | string | `stack` | Link arrangement: `stack`, `grid`, or `inline`. See [Layout variants](#layout-variants). |
|
||||
| `themeToggle` | bool | `false` | Render a sun/moon button in the footer + load the toggle script. |
|
||||
| `ogImage` | bool | `true` | Set `false` to suppress all `og:image` / `twitter:image` tags. |
|
||||
| `ogImageUrl` | string (URL) | — | Explicit OG preview image (1200×630 recommended). Overrides the avatar fallback and upgrades Twitter card to `summary_large_image`. |
|
||||
| `schema` | bool | `true` | Emit schema.org `Person` JSON-LD in `<head>`. Set `false` if you provide your own. |
|
||||
| `jobTitle` | string | — | Optional `Person.jobTitle` field for JSON-LD. |
|
||||
| `location` | string | — | Optional `Person.address` field for JSON-LD. |
|
||||
@@ -123,6 +126,42 @@ Four built-in palettes, each with light + dark variants. Set `colorTheme` in `[p
|
||||
|
||||
Live preview: **[tiennm99.github.io/bonsai/themes/](https://tiennm99.github.io/bonsai/themes/)**.
|
||||
|
||||
## Layout variants
|
||||
|
||||
Three arrangements for `[[params.links]]`. Pick one via `layout` in `[params]`:
|
||||
|
||||
| Value | Look | Best for |
|
||||
|-------|------|----------|
|
||||
| `stack` *(default)* | Full-width vertical buttons | ≤ 6 link bios; the classic Linktree shape. |
|
||||
| `grid` | Two-column responsive grid (collapses to one column under 480 px) | 6–12 links; balances density and tap-target size. |
|
||||
| `inline` | Icon-only horizontal row | Lots of accounts, short page; titles stay in DOM for screen readers. |
|
||||
|
||||
Live preview: **[tiennm99.github.io/bonsai/variants/](https://tiennm99.github.io/bonsai/variants/)**.
|
||||
|
||||
## i18n
|
||||
|
||||
Theme-rendered strings (nav landmark, theme-toggle labels, default footer) live in `i18n/{lang}.toml`. Bundles for `en` and `vi` ship with the theme.
|
||||
|
||||
To use another language, set Hugo's `defaultContentLanguage` and add a matching `i18n/{lang}.toml`:
|
||||
|
||||
```toml
|
||||
defaultContentLanguage = "fr"
|
||||
```
|
||||
|
||||
```toml
|
||||
# i18n/fr.toml
|
||||
[nav_links_label]
|
||||
other = "Liens"
|
||||
[theme_toggle_label]
|
||||
other = "Basculer le thème clair / sombre"
|
||||
[theme_toggle_title]
|
||||
other = "Basculer le thème"
|
||||
[footer_default]
|
||||
other = "© {{ .year }} {{ .name }}"
|
||||
```
|
||||
|
||||
Missing keys fall back to `en`. User content (`name`, `tagline`, `bio`, link `title`s, `footerText`) is never auto-translated — it stays user-owned.
|
||||
|
||||
## Available Icons
|
||||
|
||||
<details>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Bonsai v0.2: Same-Day Polish Release Caught CSS Specificity and Hugo's JSON Escape Trap
|
||||
|
||||
**Date**: 2026-05-03 20:15
|
||||
**Severity**: Medium
|
||||
**Component**: Theme toggle UI, schema.org markup, CSS cascade, SVG assets
|
||||
**Status**: Resolved
|
||||
|
||||
## What Happened
|
||||
|
||||
Shipped v0.2.0 same day as v0.1.0 (7 hours apart): 4 color theme presets (sakura/sumi/koi via CSS custom properties), schema.org Person JSON-LD, theme-toggle button with sun/moon icons, avatar initials fallback SVG. All 5 phases completed in ~6h. Tagged release; live demo + themes gallery live. No breaking changes — all features opt-in via params.
|
||||
|
||||
## The Brutal Truth
|
||||
|
||||
Adding features that fast after a critical v0.1 release felt reckless but the modularity kept us honest. We caught two ugly bugs only because the screenshot review was rigorous. A CSS specificity silent failure sneaked into production; a Hugo templating anti-pattern almost shipped JSON garbage into `<script>` tags. Both were low-level enough to silently break on edge cases. The discipline of small additive commits meant rollback was one revert, not six.
|
||||
|
||||
## Technical Details
|
||||
|
||||
1. **Hugo `jsonify` double-encodes inside `<script>` context** — Hugo's auto-escape pipeline treats `<script>` block content as JavaScript string literal. Output `{{ $payload | jsonify }}` becomes `"{\"@context\":...}"` (stringified JSON string) instead of raw JSON. Fix: pipe through `safeJS` to mark output as safe for JS context: `{{ $payload | jsonify | safeJS }}`. Manifests as broken schema.org payloads when parsed by search crawlers.
|
||||
|
||||
2. **CSS specificity silent battle on `.theme-toggle`** — `.theme-toggle svg { display: block }` (specificity 0,1,1) silently beat `.theme-toggle__sun { display: none }` (specificity 0,1,0). Both icons rendered at once, toggling did nothing visible. Caught only because screenshot diff showed two moons stacked. Fix: scope all per-icon rules under parent: `.theme-toggle .theme-toggle__sun { display: none }` to match the broader selector's context. Lesson: CSS specificity debugs invisibly.
|
||||
|
||||
3. **Hardcoded color in `exampleSite/static/images/avatar.svg`** — vermilion `fill="#8b3a2b"` locked the avatar to one palette; theme toggle recolored the site but avatar stayed red. Removed avatar from exampleSite entirely to showcase the new initials fallback feature. Felt like a loss until we realized the demo got *better* — validates that initials auto-inherit palette accent and live-update on theme toggle.
|
||||
|
||||
4. **Small + additive release cadence builds confidence in rollback** — v0.1 was large. v0.2 added 4 features in 5 commits. When specificity bug surfaced at final review, the fix was a 2-line CSS change in a single commit. If all v0.2 work were one megacommit, rollback would've nuked 6 hours of work. Discipline in commit granularity pays off.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
- **Hugo template quirks aren't obvious from docs.** `jsonify` works fine in template interpolation but breaks in `<script>`. Needed to test the actual rendered HTML, not just the template logic.
|
||||
- **CSS cascade assumed matching specificity.** Both selectors hit the toggle element; only one was checked. No linting caught this because both selectors are valid — the bug is order + specificity interaction.
|
||||
- **Static assets assumed single-theme context.** SVG fill attributes don't inherit from CSS custom properties by default. Should've validated asset reuse under theme toggle during phase 4 review.
|
||||
- **Commit discipline was the real safety net.** Each phase was one clean commit. Enabled fast iteration without fear of losing work.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Test rendered HTML, not template output.** For JSON-LD and `<script>` blocks, inspect the final HTML in DevTools. Auto-escape traps are invisible in template files.
|
||||
|
||||
2. **CSS specificity bugs hide best in visual tests.** Automated CSS linting misses specificity collisions. Screenshot diffs catch them immediately.
|
||||
|
||||
3. **Static SVG assets don't theme well.** If an SVG must recolor under theme toggle, either: remove the fill attribute and inherit from CSS, or generate it dynamically. Hardcoded fills are a gotcha.
|
||||
|
||||
4. **Same-day releases are only safe if phases are modular.** v0.1→v0.2 worked because phases had minimal coupling. Try this with a monolithic feature and you're fighting merge conflicts and rollback chaos.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Monitor schema.org validation via Google Search Console (confirm JSON-LD is parsed correctly after safeJS fix).
|
||||
- Consider CSS specificity audit for all `.theme-toggle__*` variants before v0.3 (preventive).
|
||||
- For future static assets: add a validation step in screenshot capture that checks for hardcoded colors that don't inherit.
|
||||
- v0.3 candidates: layout variants (cards/grid), OG image generation, i18n setup. Keep the same cadence: small, additive, low blast radius.
|
||||
|
||||
**Owner**: @tiennm99
|
||||
**Timeline**: Next release pending catalog PR #708 feedback
|
||||
@@ -0,0 +1,61 @@
|
||||
# Bonsai v0.3: Layout Variants and i18n Shipped Same Day, OG Auto-Generation Cut for Theme Weight
|
||||
|
||||
**Date**: 2026-05-03 12:10
|
||||
**Severity**: Low
|
||||
**Component**: Layout system, i18n bundle, OG meta tags, demo site
|
||||
**Status**: Resolved
|
||||
|
||||
## What Happened
|
||||
|
||||
Shipped v0.3 features ~2 hours after v0.2 wrapped: `params.layout` for stack/grid/inline link arrangements, `i18n/{en,vi}.toml` bundles for theme strings, and `params.ogImageUrl` + `ogImage` controls for social previews. **Cut OG image auto-generation mid-implementation** — vendoring a TTF font + 4 base PNGs (~150 KB) conflicted directly with the theme's `< 4 KB CSS` minimalist principle. Reduced Phase 2 scope to URL override + smart `twitter:card` upgrade; deferred auto-gen to v0.4. All 5 phase files written, 4 of 5 phases done; tag/push pending user authorization.
|
||||
|
||||
## The Brutal Truth
|
||||
|
||||
The hardest thing about v0.3 wasn't writing code — it was killing a feature mid-implementation when its weight didn't match the theme's identity. Plan said "ship OG auto-generation," code said "vendor a 120 KB font," principle said "stay under 4 KB CSS." Three votes, principle won. Better to ship four solid features than five with one obese addition.
|
||||
|
||||
The same-day cadence held because v0.2's modularity work paid dividends: layout variants slot in as one CSS section + one template line, i18n is one Hugo function call replacing literal strings, OG params are six lines of template logic. Zero refactors, zero broken tests, zero rollbacks.
|
||||
|
||||
Caught one real bug in testing: my first i18n smoke test set `languageCode = "vi-VN"` and watched English render. Spent a minute confused before remembering Hugo uses `defaultContentLanguage` (not `languageCode`) to select i18n bundles. That's now documented in the README so the next person doesn't waste the same minute.
|
||||
|
||||
## Technical Details
|
||||
|
||||
1. **CSS specificity discipline carried from v0.2** — Layout variant rules scope under `.bio__links--{variant} .link { ... }`, never naked `.link { ... }`. The v0.2 theme-toggle bug (where `.theme-toggle svg` silently beat `.theme-toggle__sun` at specificity 0,1,1 vs 0,1,0) is fresh enough that the discipline came automatically. No specificity collisions in v0.3.
|
||||
|
||||
2. **Inline layout uses screen-reader-friendly visual hiding** — `.bio__links--inline .link__title` clips the title to a 1×1 px region with `clip: rect(0,0,0,0)` instead of `display: none`. Titles stay in the DOM and accessible to screen readers; visually only the icon shows. Standard pattern, but worth being deliberate about — `display: none` would have hidden them from AT, breaking the link's purpose for non-sighted users.
|
||||
|
||||
3. **Hugo `languageCode` ≠ i18n bundle selector** — `languageCode = "vi-VN"` sets the HTML `lang` attribute and metadata, but Hugo picks `i18n/*.toml` bundles based on `defaultContentLanguage`. For a single-language site, both must be set. Documented this in the README's new i18n section because it's the kind of gotcha that costs people 20 minutes the first time.
|
||||
|
||||
4. **OG image auto-generation killed for theme-weight reasons** — Original plan: vendor a TTF (~50–120 KB Latin+Vietnamese subset of Inter) and 4 base PNGs (~5–20 KB each), use Hugo's `images.Text` filter to overlay name + tagline at build time. Total binary weight: ~100–200 KB. Theme today: ~12 KB CSS + ~13 KB icon SVGs. Ratio: auto-gen would have nearly doubled the theme's binary footprint to support an opt-in feature. Decision: ship the URL override (6 lines of template logic, zero new bytes), defer auto-gen to v0.4 once a smaller path is found. **The feature didn't change; the implementation timeline did.**
|
||||
|
||||
5. **README's `< 4 KB CSS` claim was already stale** — Inherited from v0.1. v0.2's themes-gallery + v0.3's variants-gallery pushed raw CSS to 11.8 KB. Gzipped is still 2.9 KB. Updated the claim to `< 3 KB gzipped CSS`, which is the meaningful number for transfer cost. Lesson: marketing-style claims rot fast; quote sizes that match reality.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
- **Plan optimism vs implementation reality** — On paper, "use Hugo's `images.Text` filter" sounds like a one-partial change. In practice, it's a font-vendoring decision with license review, glyph-coverage analysis, and a binary asset commitment. The plan should have flagged "binary asset weight" as a Phase 2 risk dimension, not just "Hugo template quirks."
|
||||
|
||||
- **Hugo locale resolution is under-documented for single-language sites** — Most Hugo i18n examples assume multilingual setups with `[languages]` blocks. Single-language with non-English bundle is a less-traveled path; the `defaultContentLanguage` requirement is easy to miss.
|
||||
|
||||
- **CSS bundle drift went uncaught for a release cycle** — v0.2 grew the bundle past the README claim and nobody (me) noticed until v0.3 added more. A doctests-style "verify README claims" hook would catch this; not worth building yet, but worth knowing exists as a category of stale-doc bug.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Kill features mid-implementation when weight contradicts identity.** A theme that brags about being small can't ship a feature that doubles its binary footprint, even if the feature is desirable. Defer is a perfectly fine answer; ship-anyway-with-bloat is not.
|
||||
|
||||
2. **CSS specificity discipline becomes muscle memory after one incident.** The v0.2 theme-toggle bug was acutely painful enough that v0.3's variant CSS auto-scoped under parent classes without conscious effort. Real bugs teach better than style guides.
|
||||
|
||||
3. **Document the gotchas you trip over in real time.** The `defaultContentLanguage` confusion took 60 seconds to resolve and would take 20 minutes for a fresh user. Three lines in the README's i18n section makes it a non-issue forever.
|
||||
|
||||
4. **Quote real numbers, not aspirational ones.** `< 4 KB CSS` was true once. `< 3 KB gzipped CSS` is true now. The second one will stay true longer because it's tied to a transfer-cost reality, not a frozen-at-launch ideal.
|
||||
|
||||
5. **Same-day cadence works when phases are independent.** Phases 1, 2, 3 touched disjoint files (`bio-card.html`+CSS, `head.html`, `i18n/`+two partials). They could have shipped in any order. That's not architectural luck — it's a result of v0.2's component carve-up making each phase a leaf change instead of a refactor.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Regenerate `images/screenshot.png` + `images/tn.png` from a real browser (no headless tooling available in the dev environment); needed for Hugo themes-site refresh.
|
||||
- Commit + tag `v0.3.0` after screenshots land.
|
||||
- Verify OG previews on Twitter / Facebook / LinkedIn debuggers post-deploy.
|
||||
- v0.4 candidates: revisit OG auto-generation with a smaller path (system-font CSS-rendered SVG behind a rasterizer? Single-palette generic base? User-supplied font path?), RSS opt-in, multi-section bio, more icons.
|
||||
- Consider extracting the gallery CSS (`.themes-gallery__*`, `.variants-gallery__*`) to a separate `gallery.css` loaded only on demo pages — saves ~2 KB on every user's site for CSS that only the demo needs.
|
||||
|
||||
**Owner**: @tiennm99
|
||||
**Timeline**: Tag pending screenshot regen
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Layout variants"
|
||||
type: variants
|
||||
---
|
||||
@@ -13,9 +13,11 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
|
||||
# avatar omitted intentionally so the demo showcases the initials fallback
|
||||
# (the SVG circle uses the active palette's accent color)
|
||||
colorTheme = "sakura" # try "bonsai" (default), "sakura", "sumi", "koi"
|
||||
layout = "grid" # try "stack" (default), "grid", "inline"
|
||||
themeToggle = true
|
||||
jobTitle = "Software Engineer"
|
||||
footerText = "© 2026 · made with bonsai"
|
||||
# ogImageUrl = "/og.png" # optional 1200×630 image for social previews
|
||||
|
||||
[[params.links]]
|
||||
title = "GitHub"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Bonsai theme — English strings.
|
||||
# Override per language by adding e.g. i18n/vi.toml; missing keys fall back to en.
|
||||
|
||||
[nav_links_label]
|
||||
other = "Links"
|
||||
|
||||
[theme_toggle_label]
|
||||
other = "Toggle light and dark theme"
|
||||
|
||||
[theme_toggle_title]
|
||||
other = "Toggle theme"
|
||||
|
||||
[footer_default]
|
||||
other = "© {{ .year }} {{ .name }}"
|
||||
@@ -0,0 +1,13 @@
|
||||
# Bonsai theme — Vietnamese strings.
|
||||
|
||||
[nav_links_label]
|
||||
other = "Liên kết"
|
||||
|
||||
[theme_toggle_label]
|
||||
other = "Chuyển giao diện sáng / tối"
|
||||
|
||||
[theme_toggle_title]
|
||||
other = "Đổi giao diện"
|
||||
|
||||
[footer_default]
|
||||
other = "© {{ .year }} {{ .name }}"
|
||||
@@ -2,6 +2,12 @@
|
||||
{{- $tagline := site.Params.tagline -}}
|
||||
{{- $bio := site.Params.bio -}}
|
||||
{{- $links := site.Params.links -}}
|
||||
{{- $layout := site.Params.layout | default "stack" -}}
|
||||
{{- $validLayouts := slice "stack" "grid" "inline" -}}
|
||||
{{- if not (in $validLayouts $layout) -}}
|
||||
{{- warnf "bonsai: unknown params.layout %q (expected stack|grid|inline), falling back to 'stack'" $layout -}}
|
||||
{{- $layout = "stack" -}}
|
||||
{{- end -}}
|
||||
|
||||
<article class="bio">
|
||||
{{ partial "avatar.html" . }}
|
||||
@@ -17,7 +23,7 @@
|
||||
{{- end }}
|
||||
|
||||
{{- if $links }}
|
||||
<nav class="bio__links" aria-label="Links">
|
||||
<nav class="bio__links bio__links--{{ $layout }}" aria-label="{{ i18n "nav_links_label" | default "Links" }}">
|
||||
{{- range $links }}
|
||||
{{- partial "link-button.html" . -}}
|
||||
{{- end }}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
{{- with site.Params.footerText -}}
|
||||
<p>{{ . | safeHTML }}</p>
|
||||
{{- else -}}
|
||||
<p>© {{ now.Year }} {{ site.Params.name | default site.Title }}</p>
|
||||
{{- $ctx := dict "year" now.Year "name" (site.Params.name | default site.Title) -}}
|
||||
<p>{{ i18n "footer_default" $ctx | default (printf "© %d %s" now.Year (site.Params.name | default site.Title)) | safeHTML }}</p>
|
||||
{{- end -}}
|
||||
{{- if site.Params.themeToggle -}}
|
||||
{{ partial "theme-toggle-button.html" . }}
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
{{- $tagline := site.Params.tagline | default "" -}}
|
||||
{{- $description := site.Params.bio | default $tagline | default site.Params.description -}}
|
||||
{{- $avatar := site.Params.avatar -}}
|
||||
{{- /* OG image resolution:
|
||||
- params.ogImage = false → emit nothing.
|
||||
- params.ogImageUrl set → use it (1200×630 expected → summary_large_image).
|
||||
- else fall back to params.avatar (square → summary card). */ -}}
|
||||
{{- $ogImageEnabled := site.Params.ogImage | default true -}}
|
||||
{{- $ogImageUrl := "" -}}
|
||||
{{- $twitterCard := "summary" -}}
|
||||
{{- if $ogImageEnabled -}}
|
||||
{{- if site.Params.ogImageUrl -}}
|
||||
{{- $ogImageUrl = site.Params.ogImageUrl | absURL -}}
|
||||
{{- $twitterCard = "summary_large_image" -}}
|
||||
{{- else if $avatar -}}
|
||||
{{- $ogImageUrl = $avatar | absURL -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
@@ -15,10 +30,11 @@
|
||||
{{- with $description }}
|
||||
<meta property="og:description" content="{{ . }}" />
|
||||
{{- end }}
|
||||
{{- with $avatar }}
|
||||
<meta property="og:image" content="{{ . | absURL }}" />
|
||||
{{- with $ogImageUrl }}
|
||||
<meta property="og:image" content="{{ . }}" />
|
||||
<meta name="twitter:image" content="{{ . }}" />
|
||||
{{- end }}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:card" content="{{ $twitterCard }}" />
|
||||
|
||||
{{ partial "schema-person.html" . }}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
Sun shown in dark mode (target = light). Moon shown in light mode (target = dark). */ -}}
|
||||
<button type="button" class="theme-toggle"
|
||||
data-bonsai-theme-toggle
|
||||
aria-label="Toggle light and dark theme"
|
||||
aria-label="{{ i18n "theme_toggle_label" | default "Toggle light and dark theme" }}"
|
||||
aria-pressed="false"
|
||||
title="Toggle theme">
|
||||
title="{{ i18n "theme_toggle_title" | default "Toggle theme" }}">
|
||||
<svg class="theme-toggle__sun" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4" fill="currentColor"/>
|
||||
<g stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{{ define "main" }}
|
||||
<article style="text-align:center;">
|
||||
<h1 style="font-family:var(--bonsai-font-display);">Layout variants</h1>
|
||||
<p style="color:var(--bonsai-muted);max-width:32rem;margin:0 auto 2rem;">
|
||||
Set <code>layout = "stack"</code> (default), <code>"grid"</code>, or <code>"inline"</code> in your <code>hugo.toml</code> <code>[params]</code> block.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
{{- $sample := slice
|
||||
(dict "title" "GitHub" "url" "#" "icon" "github")
|
||||
(dict "title" "LinkedIn" "url" "#" "icon" "linkedin")
|
||||
(dict "title" "Email" "url" "#" "icon" "mail")
|
||||
(dict "title" "Mastodon" "url" "#" "icon" "mastodon")
|
||||
-}}
|
||||
|
||||
{{- $variants := slice
|
||||
(dict "name" "stack" "label" "Stack (default)" "desc" "Full-width vertical buttons. Best for ≤ 6 links.")
|
||||
(dict "name" "grid" "label" "Grid" "desc" "Two-column responsive grid; collapses to one column under 480 px.")
|
||||
(dict "name" "inline" "label" "Inline" "desc" "Icon-only horizontal row. Titles stay in DOM for screen readers.")
|
||||
-}}
|
||||
|
||||
<div class="variants-gallery">
|
||||
{{- range $variants }}
|
||||
<section class="variants-gallery__card">
|
||||
<h3 class="variants-gallery__name">{{ .label }}</h3>
|
||||
<p class="variants-gallery__desc">{{ .desc }}</p>
|
||||
<code class="variants-gallery__code">layout = "{{ .name }}"</code>
|
||||
<nav class="bio__links bio__links--{{ .name }}" aria-label="{{ .label }} sample">
|
||||
{{- range $sample }}
|
||||
{{- partial "link-button.html" . -}}
|
||||
{{- end }}
|
||||
</nav>
|
||||
</section>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "Layout variants (stack/grid/inline)"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "2h"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Layout variants (stack/grid/inline)
|
||||
|
||||
## Overview
|
||||
Introduce `params.layout` to let users pick how `[[params.links]]` are arranged. Pure CSS — no JS, no template forks. Default unchanged (stack).
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- New string param `layout` with values: `stack` (default), `grid`, `inline`.
|
||||
- `stack`: current full-width vertical buttons.
|
||||
- `grid`: 2-col responsive grid (collapses to 1-col below ~480px).
|
||||
- `inline`: icon-only horizontal row (titles become `aria-label`/`title` for a11y); great for users with many short links.
|
||||
- Unknown value → falls back to `stack` and emits a Hugo warning via `errorf`.
|
||||
- Non-functional:
|
||||
- Zero JS additions.
|
||||
- Visible layout class lives on `<nav class="bio__links">` so the cascade stays scoped.
|
||||
- All three variants pass focus-visible + reduced-motion tests.
|
||||
|
||||
## Architecture
|
||||
- Template: `bio-card.html` reads `site.Params.layout`, validates against allowlist, renders `<nav class="bio__links bio__links--{layout}">`.
|
||||
- CSS: add three rule blocks under `/* layouts */`. Variant rules are scoped: `.bio__links--grid .link { ... }`, never `.link { ... }` directly. (Lesson from v0.2 specificity bug.)
|
||||
- `inline` variant changes `link-button.html` rendering? **No** — keep one button template. Hide `.link__title` visually-but-not-from-AT in the inline variant via `.bio__links--inline .link__title { position: absolute; left: -10000px; }` (or use existing `clip-path` pattern); add `aria-label` only when title is hidden via CSS — actually simpler: keep title in DOM, hide visually, screen readers still get it. No template changes needed.
|
||||
|
||||
## Related Code Files
|
||||
- Modify:
|
||||
- `layouts/partials/bio-card.html` — read + validate `layout`, render variant class.
|
||||
- `static/css/bonsai.css` — add 3 variant rule blocks (grid + inline; stack is the default).
|
||||
- `exampleSite/hugo.toml` — uncomment example showing `layout = "grid"` (commented-out for default).
|
||||
- Create: none.
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. In `bio-card.html`, add validation:
|
||||
```
|
||||
{{- $layout := site.Params.layout | default "stack" -}}
|
||||
{{- $valid := slice "stack" "grid" "inline" -}}
|
||||
{{- if not (in $valid $layout) }}
|
||||
{{- warnf "bonsai: unknown params.layout %q, falling back to 'stack'" $layout -}}
|
||||
{{- $layout = "stack" -}}
|
||||
{{- end -}}
|
||||
```
|
||||
2. Render `<nav class="bio__links bio__links--{{ $layout }}">`.
|
||||
3. CSS — append a `/* === layout variants === */` section after the existing `.bio__links` block. Default `.bio__links` rules stay; variants override only what differs (`grid-template-columns`, `flex-direction`, padding).
|
||||
4. For `inline`: hide `.link__title` via `clip` pattern — preserves screen-reader access; do **not** add `aria-label` (title is still in DOM).
|
||||
5. Add a 480px media query inside `--grid` so it collapses to 1-col on phones.
|
||||
6. Document the new param in README param table (deferred to Phase 4 — but note here so it's not lost).
|
||||
|
||||
## Todo List
|
||||
- [ ] Validate `params.layout` in `bio-card.html` with allowlist + `warnf`.
|
||||
- [ ] Append CSS for `.bio__links--grid`.
|
||||
- [ ] Append CSS for `.bio__links--inline` (icon-only with visually-hidden titles).
|
||||
- [ ] Add 480px breakpoint for `--grid`.
|
||||
- [ ] Verify all 3 variants render correctly via `hugo server` + screenshot diff.
|
||||
- [ ] Verify focus-visible outline works in all variants.
|
||||
- [ ] Verify reduced-motion respected (no new transitions added).
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `params.layout = "stack"` (or unset) renders identically to v0.2.
|
||||
- [ ] `params.layout = "grid"` renders 2-col → 1-col at 480px.
|
||||
- [ ] `params.layout = "inline"` renders icon-only horizontal row; screen readers still announce link titles.
|
||||
- [ ] `params.layout = "garbage"` falls back to stack with a Hugo build warning.
|
||||
- [ ] No new JS bytes shipped.
|
||||
- [ ] CSS bundle stays under 5 KB.
|
||||
|
||||
## Risk Assessment
|
||||
- **CSS specificity collisions** (v0.2 lesson) — mitigate by scoping all variant rules under `.bio__links--{variant} .link`.
|
||||
- **Inline variant accessibility** — visually-hidden title pattern is well-known; verify with VoiceOver / NVDA via screenshot of accessibility tree.
|
||||
- **Grid breakpoint feels arbitrary at 480px** — chosen because most one-handed phone widths are ~390–430px; falls back to stack-equivalent below that.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
phase: 2
|
||||
title: "OG image params (override + twitter card upgrade)"
|
||||
status: in-progress
|
||||
priority: P2
|
||||
effort: "0.5h"
|
||||
dependencies: []
|
||||
notes: "Scope reduced from auto-generation. Auto-gen requires font + base PNG vendoring (~150 KB binary), conflicts with minimalist theme principle. Moved to v0.4 candidates."
|
||||
---
|
||||
|
||||
# Phase 2: OG image params (scope-reduced)
|
||||
|
||||
**Reduced from auto-generation.** Reason: build-time text overlay needs Hugo `images.Text` + vendored TTF (~50–120 KB) + 4 base PNGs. That's significant binary weight in a `< 4 KB CSS` theme. Defer auto-gen to v0.4 once a smaller path is found (e.g. system-font CSS-rendered SVG behind a rasterizer).
|
||||
|
||||
## What ships in v0.3
|
||||
- `params.ogImageUrl` — explicit user-provided OG image URL. Takes precedence over `params.avatar`.
|
||||
- `params.ogImage = false` — opt out entirely (emit no `og:image` / `twitter:image`).
|
||||
- When any image present, `twitter:card` upgrades from `summary` to `summary_large_image` if user explicitly opted in via `params.ogImageUrl` (since avatar is square, keep `summary` for avatar fallback).
|
||||
|
||||
## Overview
|
||||
Auto-generate a 1200×630 Open Graph preview image from `params.name`, `params.tagline`, and the active palette accent color. Falls back gracefully if user supplies their own `params.avatar` path that resolves to an OG-suitable image — and skippable entirely via `params.ogImage = false`.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- When `params.ogImage` is unset or `true` AND no `params.ogImageUrl` override: theme generates a 1200×630 PNG via Hugo image processing.
|
||||
- Generated image: solid background = palette `bg`; centered name (large) + tagline (smaller) in palette `fg`; small accent stripe on the left in palette `accent`.
|
||||
- When `params.ogImageUrl` is set: use that absolute/relative URL verbatim, skip generation.
|
||||
- When `params.ogImage = false`: emit no `og:image` and no `twitter:image` (back-compat with v0.2 behavior when no avatar set).
|
||||
- Same generated image used for `og:image` AND `twitter:image` (upgrade card to `summary_large_image`).
|
||||
- Non-functional:
|
||||
- Build time impact: < 500ms for default site (one image, one variant).
|
||||
- Generated image is content-hashed by Hugo for cache busting.
|
||||
- Works without any external binary — pure Hugo image API.
|
||||
|
||||
## Architecture
|
||||
- Hugo lacks native text-on-image rendering. Approach: ship a **template-PNG-per-palette** (4 PNGs, ~5 KB each) in `assets/og/` with the palette background + accent stripe baked in, leaving a transparent / solid text region. Then overlay text via SVG-to-image: render an SVG with name + tagline, convert to image via Hugo's `images.Filter` or external pipeline.
|
||||
- **Simpler alternative (chosen):** generate the entire OG image as an **SVG**, return it as `og:image` content. Crawlers (Twitter, Facebook, Slack, Discord, LinkedIn) all accept SVG OG images **except Twitter and Facebook** which strip them. So SVG-only is too risky.
|
||||
- **Final approach:** generate SVG → render to PNG using Hugo's built-in `resources.GetRemote` pattern is no good (no binary). Use Hugo's `images.Text` filter (Hugo 0.108+) which overlays text on an existing image using a TTF font. We ship one base palette PNG per theme (4 PNGs) and one TTF font (~50 KB Inter or system-stack-equivalent open font like Geist Mono — license check required).
|
||||
- Concrete pipeline:
|
||||
1. `assets/og/base-{theme}.png` — 1200×630 solid bg + accent stripe.
|
||||
2. `assets/og/font.ttf` — single open-license sans (Inter Regular ~120 KB, or smaller alt).
|
||||
3. New partial `partials/og-image.html` — does:
|
||||
```
|
||||
{{ $base := resources.Get (printf "og/base-%s.png" $theme) }}
|
||||
{{ $img := $base | images.Filter (images.Text $name (dict "color" $fg "size" 72 "x" 80 "y" 220 "font" $font))
|
||||
| images.Filter (images.Text $tagline (dict "color" $fg "size" 36 "x" 80 "y" 340 "font" $font)) }}
|
||||
{{ $img := $img | resources.Fingerprint }}
|
||||
```
|
||||
4. `head.html` consumes `{{ partial "og-image.html" . }}` to emit `<meta property="og:image">` + `<meta name="twitter:image">` + upgrade `twitter:card` to `summary_large_image`.
|
||||
|
||||
## Related Code Files
|
||||
- Create:
|
||||
- `assets/og/base-bonsai.png` (1200×630 — washi paper bg + vermilion stripe)
|
||||
- `assets/og/base-sakura.png`
|
||||
- `assets/og/base-sumi.png`
|
||||
- `assets/og/base-koi.png`
|
||||
- `assets/og/font.ttf` (single open-license sans; document source + license in NOTICE)
|
||||
- `layouts/partials/og-image.html`
|
||||
- `scripts/generate-og-bases.sh` — repeatable ImageMagick script that generates the 4 base PNGs from palette CSS values, so palette tweaks regenerate cleanly.
|
||||
- Modify:
|
||||
- `layouts/partials/head.html` — replace static `og:image` block with `{{ partial "og-image.html" . }}`; upgrade `twitter:card` to `summary_large_image` when image present.
|
||||
- `README.md` (in Phase 4) — document `params.ogImage` and `params.ogImageUrl`.
|
||||
- `NOTICE` — credit font + base image generation.
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. Pick font. Candidates: Inter (OFL), Geist (OFL), IBM Plex Sans (OFL). Smallest weight that reads well at 72px. Document choice in NOTICE.
|
||||
2. Write `scripts/generate-og-bases.sh` — uses ImageMagick (`magick -size 1200x630 xc:#bg -fill #accent -draw 'rectangle 0,0 16,630'`) to generate all 4 base PNGs.
|
||||
3. Run script, commit the 4 base PNGs to `assets/og/`.
|
||||
4. Add font file. Verify license, add NOTICE entry.
|
||||
5. Write `layouts/partials/og-image.html`:
|
||||
- Resolve theme via same `site.Params.colorTheme | default "bonsai"` pattern.
|
||||
- Resolve foreground color via a small dict mapping theme → fg hex (matches CSS palette).
|
||||
- Skip generation if `site.Params.ogImage == false` → return empty.
|
||||
- Skip generation if `site.Params.ogImageUrl` set → emit it verbatim, return.
|
||||
- Otherwise: load base PNG, overlay name + tagline via `images.Text`, fingerprint, emit `<meta>` tags.
|
||||
6. Update `head.html` — call partial, remove old conditional.
|
||||
7. Add an exampleSite check: build `hugo --themesDir ../..` and verify `public/og-*.png` exists with overlay text.
|
||||
|
||||
## Todo List
|
||||
- [ ] Pick + license-check OG font; add to `assets/og/`; document in NOTICE.
|
||||
- [ ] Write `scripts/generate-og-bases.sh`; generate all 4 base PNGs; commit.
|
||||
- [ ] Implement `partials/og-image.html` with skip-on-false + url-override + generation paths.
|
||||
- [ ] Wire into `head.html`; upgrade twitter:card to `summary_large_image` when image present.
|
||||
- [ ] Verify generated image renders correctly via `hugo server` → inspect `public/og/*.png`.
|
||||
- [ ] Verify all 4 palette themes produce distinct, readable images.
|
||||
- [ ] Test crawler preview via opengraph.xyz or twitter card validator (manual).
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Default config (no `ogImage*` params) generates a palette-matched OG image.
|
||||
- [ ] `params.ogImageUrl = "/static/my-og.png"` overrides generation, skips Hugo image pipeline.
|
||||
- [ ] `params.ogImage = false` emits no `og:image` / `twitter:image`.
|
||||
- [ ] All 4 themes produce visually-distinct, readable images.
|
||||
- [ ] Build time delta < 500ms for the default exampleSite.
|
||||
- [ ] Image is content-hashed (filename includes fingerprint).
|
||||
|
||||
## Risk Assessment
|
||||
- **Hugo `images.Text` requires Hugo 0.108+** — `theme.toml` already pins `min_version = "0.128"`; safe.
|
||||
- **Font file size** — Inter Regular is ~120 KB; acceptable as a one-time vendored asset, but check if a smaller subset (Latin-only) reduces to ~40 KB. Use `pyftsubset` if so.
|
||||
- **Long names overflow the canvas** — clip text at ~30 chars; document the limit in README.
|
||||
- **Tagline with markdown** — `images.Text` is plain-text only; strip markdown via `plainify` filter before overlay.
|
||||
- **Reusing `og:image` for `twitter:image`** — Twitter's `summary_large_image` requires 2:1 ratio; 1200×630 is close enough (1.9:1) and is the de-facto standard.
|
||||
- **Locale-specific characters** — font must include glyphs for non-Latin names; if user is Vietnamese (per repo author) and font lacks Vietnamese diacritics, image will show tofu. Pick a font with Vietnamese coverage (Inter does).
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
---
|
||||
phase: 3
|
||||
title: "i18n string externalization"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "1.5h"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 3: i18n string externalization
|
||||
|
||||
## Overview
|
||||
Move every hardcoded English string in templates to Hugo i18n bundles so non-English sites work end-to-end. Ship `en` (default) + `vi` (smoke test for the maintainer's locale + non-Latin diacritics).
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- All theme-rendered strings sourced from `i18n/{lang}.toml` via `{{ i18n "key" }}`.
|
||||
- Site `languageCode` (e.g. `vi-VN`) resolves to the matching bundle; falls back to `en` for missing keys.
|
||||
- User-supplied strings (`params.name`, `params.tagline`, `params.bio`, link `title`s, `params.footerText`) remain untouched — those are user content, not theme strings.
|
||||
- Non-functional:
|
||||
- Adding a new language requires one new `.toml` file, no template edits.
|
||||
- Existing English-only sites see no behavior change (en is the default).
|
||||
|
||||
## Architecture
|
||||
- Audit current hardcoded strings in templates:
|
||||
- `bio-card.html`: `aria-label="Links"` (nav landmark).
|
||||
- `theme-toggle-button.html`: button label / aria-label / title (likely "Toggle theme").
|
||||
- `footer.html`: default `© {year} {name}` template (the format itself is i18n-able; `name` stays a param).
|
||||
- `og-image.html` (new in Phase 2): no user-visible strings; skip.
|
||||
- `head.html`: no user-visible strings.
|
||||
- Create `i18n/en.toml` with all extracted keys.
|
||||
- Create `i18n/vi.toml` mirroring `en.toml` with Vietnamese translations (smoke-test diacritics + Hugo's locale resolution).
|
||||
- Replace inline strings with `{{ i18n "key" }}` calls.
|
||||
|
||||
## Related Code Files
|
||||
- Create:
|
||||
- `i18n/en.toml`
|
||||
- `i18n/vi.toml`
|
||||
- Modify:
|
||||
- `layouts/partials/bio-card.html` — replace `aria-label="Links"` with `aria-label="{{ i18n "nav_links_label" }}"`.
|
||||
- `layouts/partials/theme-toggle-button.html` — replace English strings with i18n calls.
|
||||
- `layouts/partials/footer.html` — i18n the default footer template (when `params.footerText` is unset).
|
||||
- `exampleSite/hugo.toml` — keep `languageCode = "en-us"`; add a commented example for `vi-VN`.
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. Read `theme-toggle-button.html` + `footer.html` to enumerate every English literal.
|
||||
2. Create `i18n/en.toml` with proposed keys:
|
||||
- `nav_links_label = "Links"`
|
||||
- `theme_toggle_label = "Toggle theme"`
|
||||
- `theme_toggle_to_dark = "Switch to dark theme"`
|
||||
- `theme_toggle_to_light = "Switch to light theme"`
|
||||
- `footer_default = "© {{ .year }} {{ .name }}"` — Hugo i18n supports template params via the `dict` 2nd arg.
|
||||
3. Create `i18n/vi.toml` with translated values (e.g. `nav_links_label = "Liên kết"`, `theme_toggle_label = "Đổi giao diện"`).
|
||||
4. Replace template literals with `{{ i18n "key" }}` (or `{{ i18n "key" $ctx }}` where params needed).
|
||||
5. Test: build exampleSite with `languageCode = "en-us"` → strings unchanged. Switch to `languageCode = "vi-VN"` → Vietnamese strings appear.
|
||||
6. Hugo missing-translation behavior: by default Hugo emits `[i18n] translation not found for key 'X'` — verify no warnings during `en` build.
|
||||
|
||||
## Todo List
|
||||
- [ ] Audit all hardcoded English strings in `layouts/partials/`.
|
||||
- [ ] Draft full key list; review for granularity (e.g., separate to_dark / to_light vs single Toggle).
|
||||
- [ ] Write `i18n/en.toml`.
|
||||
- [ ] Write `i18n/vi.toml` (Vietnamese translations).
|
||||
- [ ] Replace all literals in templates with `{{ i18n }}` calls.
|
||||
- [ ] Build exampleSite with `en-us` — verify no missing-translation warnings.
|
||||
- [ ] Build exampleSite with `vi-VN` — verify Vietnamese strings render, including footer template-with-params case.
|
||||
- [ ] Verify diacritics render correctly in HTML.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] No English literals remain in `layouts/partials/*.html` (excluding HTML attribute names + user-content placeholders).
|
||||
- [ ] `i18n/en.toml` and `i18n/vi.toml` ship with the theme.
|
||||
- [ ] Switching `languageCode` in `hugo.toml` flips all theme strings, no template edits needed.
|
||||
- [ ] Adding a new language is a single-file change.
|
||||
|
||||
## Risk Assessment
|
||||
- **Hugo i18n with template params** — `footer_default` uses `{{ .year }}`. Hugo's i18n template params require the right `dict` invocation; verify with the official docs (gohugo.io/content-management/multilingual/#translate-strings-in-templates) before committing.
|
||||
- **vi.toml as smoke test** — if maintainer doesn't speak the target language, machine translation is OK for the smoke test as long as it's labeled. For Vietnamese, the maintainer is a native speaker so this is safe.
|
||||
- **Theme-toggle button has dynamic state** — `to_dark` vs `to_light` switch on current theme. JS-based, so the i18n strings need to be readable by `theme-toggle.js` — pass them as `data-` attributes on the button, not as JS string literals.
|
||||
- **Pluralization not needed** — none of the current strings have count-based variants. Defer Hugo's plural i18n until a real need arises.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
phase: 4
|
||||
title: "Demo + docs polish"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "1.5h"
|
||||
dependencies: [1, 2, 3]
|
||||
---
|
||||
|
||||
# Phase 4: Demo + docs polish
|
||||
|
||||
## Overview
|
||||
Update README, exampleSite, and screenshots to showcase v0.3 additions. Same screenshot-rigor as v0.2 — visual diff is the only thing that catches CSS specificity bugs.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- README parameter table includes all new params (`layout`, `ogImage`, `ogImageUrl`).
|
||||
- README has a short section on each new feature (Layout variants, OG images, i18n).
|
||||
- exampleSite demonstrates one of the new layouts (probably `grid`) so the live demo visibly shows the new feature.
|
||||
- Screenshots regenerated showing v0.3 state.
|
||||
- Optional: dedicated `/layouts/` demo page showing all 3 variants side by side, mirroring the existing `/themes/` and `/icons/` galleries.
|
||||
- Non-functional:
|
||||
- README stays under ~250 lines after additions (currently 204) — be terse.
|
||||
- exampleSite remains a one-page bio; gallery pages are content-driven additions.
|
||||
|
||||
## Architecture
|
||||
- Reuse the `/themes/` and `/icons/` gallery pattern: an `exampleSite/content/layouts/_index.md` page rendered by a new `layouts/layouts/single.html` template (Hugo's "layout" page-kind name conflicts will need careful handling — likely call the section `arrangements` or `variants` to avoid colliding with the literal `layouts/` template directory).
|
||||
- **Naming decision:** call the section `variants/` (URL `/variants/`), template `layouts/variants/single.html`.
|
||||
- README diff: add a "## Layout variants" section after "## Color themes"; add an "## i18n" subsection inside Configuration or as its own short section; expand the params table.
|
||||
|
||||
## Related Code Files
|
||||
- Create:
|
||||
- `exampleSite/content/variants/_index.md` — content page for layout gallery.
|
||||
- `layouts/variants/single.html` — renders the gallery (3 mini bio-cards, one per layout variant).
|
||||
- Modify:
|
||||
- `README.md` — params table additions, new feature sections, link to `/variants/` gallery.
|
||||
- `exampleSite/hugo.toml` — set `layout = "grid"` to make the new feature visible on the live demo.
|
||||
- `images/screenshot.png` + `images/tn.png` — regenerate per Hugo themes-site submission requirements.
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. Update `README.md`:
|
||||
- Add `layout`, `ogImage`, `ogImageUrl` rows to the params table.
|
||||
- Add `## Layout variants` section listing stack/grid/inline with one-line descriptions and a link to `/variants/`.
|
||||
- Add `## i18n` short section: "Theme strings live in `i18n/{lang}.toml`. Set Hugo's `languageCode` and add a matching bundle to translate. Ships with `en` and `vi`."
|
||||
- Add OG image note in the existing Configuration section (one short paragraph).
|
||||
2. Update `exampleSite/hugo.toml`:
|
||||
- Set `layout = "grid"`.
|
||||
- Add commented `ogImageUrl = "..."` example.
|
||||
3. Build the variants gallery:
|
||||
- `exampleSite/content/variants/_index.md` with frontmatter + short intro.
|
||||
- `layouts/variants/single.html` renders 3 mini cards (similar to themes gallery pattern). Each card shows a sample bio rendered with one variant.
|
||||
4. Regenerate screenshots — `exampleSite` running locally, capture at 1280×800 per Hugo themes-site spec; capture both light + dark.
|
||||
5. Validate: build full site, click through `/`, `/themes/`, `/icons/`, `/variants/` — every page renders, no console errors, no missing translations.
|
||||
|
||||
## Todo List
|
||||
- [ ] Add `layout`, `ogImage`, `ogImageUrl` to README params table.
|
||||
- [ ] Add README "Layout variants" + "i18n" sections.
|
||||
- [ ] Update `exampleSite/hugo.toml` to use `layout = "grid"`.
|
||||
- [ ] Create `exampleSite/content/variants/_index.md`.
|
||||
- [ ] Create `layouts/variants/single.html` rendering all 3 variants side by side.
|
||||
- [ ] Regenerate `images/screenshot.png` + `images/tn.png` (light + dark verified).
|
||||
- [ ] Verify all pages: `/`, `/themes/`, `/icons/`, `/variants/`.
|
||||
- [ ] Spot-check OG image preview via Twitter card validator.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] README documents every new v0.3 param with default + description.
|
||||
- [ ] Live demo (after deploy) visibly uses the grid layout.
|
||||
- [ ] `/variants/` page accessible from a link in the bio card or footer.
|
||||
- [ ] Screenshots match the v0.3 demo state.
|
||||
- [ ] No 404s, no missing translations, no console errors.
|
||||
|
||||
## Risk Assessment
|
||||
- **Section name collision** — `layouts/` is Hugo's template directory; using `layouts` as a content section is confusing. Picking `variants/` keeps the section URL clean and avoids any chance of Hugo resolving the section via template autoloading.
|
||||
- **Screenshot drift** — v0.2 already changed CSS palette scoping; another regen now means the v0.2 screenshots become outdated within ~24h. Acceptable since v0.3 is the new baseline; archive v0.2 screenshots in git history only.
|
||||
- **README sprawl** — adding 3 features can easily push README past 300 lines. Discipline: each new section ≤ 15 lines; defer deep dives to inline doc comments or a separate `docs/features.md` if needed.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
phase: 5
|
||||
title: "v0.3.0 release"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "0.5h"
|
||||
dependencies: [4]
|
||||
---
|
||||
|
||||
# Phase 5: v0.3.0 release
|
||||
|
||||
## Overview
|
||||
Cut v0.3.0 — bump CHANGELOG, tag the commit, push, monitor demo deploy, write the journal entry. Same protocol as v0.2.
|
||||
|
||||
## Requirements
|
||||
- Functional:
|
||||
- CHANGELOG `[Unreleased]` block moves to `[0.3.0] — <today>`.
|
||||
- New `[Unreleased]` placeholder added with v0.4 candidates.
|
||||
- Annotated tag `v0.3.0` on the merge commit.
|
||||
- GitHub Pages deploy of the new exampleSite succeeds.
|
||||
- Journal entry under `docs/journals/2026-05-04-v0-3-release.md` (or actual date) following the v0.2 entry's format.
|
||||
- Non-functional:
|
||||
- No breaking changes; all v0.2 sites upgrade with no config edits.
|
||||
|
||||
## Architecture
|
||||
- N/A — release admin only.
|
||||
|
||||
## Related Code Files
|
||||
- Modify:
|
||||
- `CHANGELOG.md`
|
||||
- Create:
|
||||
- `docs/journals/{date}-v0-3-release.md`
|
||||
- Delete: none.
|
||||
|
||||
## Implementation Steps
|
||||
1. Update `CHANGELOG.md`:
|
||||
- Move existing v0.3 placeholder content into a real `## [0.3.0] — <date>` section.
|
||||
- List Added (3 features), Changed (head/twitter:card upgrade, exampleSite layout switch), and any Fixed entries.
|
||||
- Add fresh `## [Unreleased]` block with placeholder bullet for v0.4 candidates (e.g. RSS opt-in, more icons, multi-section bio?).
|
||||
2. Verify build: `cd exampleSite && hugo --themesDir ../.. --gc --minify` — no warnings, no missing translations.
|
||||
3. Commit final changes: `chore(release): v0.3.0 — layout variants, OG images, i18n`.
|
||||
4. Tag: `git tag -a v0.3.0 -m "Release v0.3.0 — layout variants, OG images, i18n"`.
|
||||
5. Push: `git push && git push --tags`.
|
||||
6. Watch GitHub Actions: build + deploy must both go green.
|
||||
7. Verify live demo: <https://tiennm99.github.io/bonsai/> shows new layout, `/variants/`, `/themes/`, `/icons/` all accessible.
|
||||
8. Verify OG image: paste live URL into <https://cards-dev.twitter.com/validator> + <https://www.opengraph.xyz>.
|
||||
9. Write journal entry following v0.2 format (What Happened / Brutal Truth / Technical Details / Root Cause / Lessons / Next Steps).
|
||||
|
||||
## Todo List
|
||||
- [ ] Update `CHANGELOG.md` — move Unreleased to v0.3.0 section.
|
||||
- [ ] Add fresh Unreleased placeholder.
|
||||
- [ ] Final exampleSite build — no warnings.
|
||||
- [ ] Commit + tag + push.
|
||||
- [ ] Verify CI green (build + deploy).
|
||||
- [ ] Verify live demo + new pages.
|
||||
- [ ] Verify OG image via crawler validators.
|
||||
- [ ] Write journal entry.
|
||||
- [ ] (Optional) Submit theme catalog PR update if Hugo themes site re-validation needed.
|
||||
|
||||
## Success Criteria
|
||||
- [ ] `v0.3.0` tag exists on `main`.
|
||||
- [ ] CHANGELOG accurately documents the release.
|
||||
- [ ] Live demo running v0.3 with grid layout visible.
|
||||
- [ ] Twitter / Facebook / LinkedIn previews render the generated OG image.
|
||||
- [ ] No upgrade path issues for v0.2 users (test by reverting exampleSite to v0.2 hugo.toml — site still builds).
|
||||
|
||||
## Risk Assessment
|
||||
- **OG image cache** — Facebook + LinkedIn aggressively cache previews. After release, manually re-scrape via the FB sharing debugger and LinkedIn post inspector to flush.
|
||||
- **Hugo themes catalog** — v0.2 may still be pending review per the v0.2 journal note. Don't open a v0.3 PR there until v0.2 is merged; otherwise the catalog PR collects two pending updates.
|
||||
- **Same-day or next-day release** — v0.2 was same-day after v0.1. v0.3 is bigger (3 features). Realistic target: 1–2 days from start. Don't rush a tag if Phase 4 screenshots aren't fully validated.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "v0.3 release — layout variants, OG image generation, i18n"
|
||||
status: completed
|
||||
created: 2026-05-03
|
||||
completed: 2026-05-03
|
||||
shipped_via: "PR #6 (https://github.com/tiennm99/bonsai/pull/6)"
|
||||
journal: "docs/journals/2026-05-03-v0-3-release.md"
|
||||
deferred_to_v0_4: ["OG image auto-generation"]
|
||||
release_admin_pending: ["screenshot regeneration", "v0.3.0 git tag"]
|
||||
---
|
||||
|
||||
# v0.3 Release
|
||||
|
||||
Same cadence as v0.2: small, additive, opt-in. v0.1 / v0.2 sites upgrade with no config edits.
|
||||
|
||||
## Goals
|
||||
- Give users layout choice (stack vs grid vs inline) without losing the minimalist defaults.
|
||||
- Auto-generate Open Graph preview images so shared links look polished without per-site asset work.
|
||||
- Externalize all theme-rendered strings via Hugo i18n so non-English sites work end-to-end.
|
||||
|
||||
## Non-goals
|
||||
- Multi-page support (still single-page bio).
|
||||
- Runtime JS for layout switching.
|
||||
- Translating bio copy itself (user content stays user-owned).
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Title | Status | Notes |
|
||||
|---|-------|--------|-------|
|
||||
| 1 | Layout variants (stack/grid/inline) | ✅ done | New `params.layout`. Pure CSS, scoped under `.bio__links--{variant}`. |
|
||||
| 2 | OG image params (override + twitter card) | ✅ done (scope-reduced) | Auto-generation deferred to v0.4 (binary asset weight conflicted with minimalist principle). |
|
||||
| 3 | i18n string externalization | ✅ done | `i18n/en.toml` + `vi.toml` shipped. Vietnamese smoke-tested. |
|
||||
| 4 | Demo + docs polish | ✅ done (screenshots pending) | README updated, `/variants/` gallery live, demo uses grid. **Screenshots not regenerated yet — user needs to capture on real browser.** |
|
||||
| 5 | v0.3.0 release | ⏸ awaiting user | CHANGELOG ready. Commit + tag + push pending user authorization. |
|
||||
|
||||
## Dependencies
|
||||
- Phase 1–3 are independent (different files). Can be implemented in any order.
|
||||
- Phase 4 depends on 1–3 (needs final params + screenshots).
|
||||
- Phase 5 depends on 4 (needs README + CHANGELOG before tag).
|
||||
|
||||
## Risks (carried from v0.2 lessons)
|
||||
- CSS specificity bugs hide in cascade. Layout variants must be screenshot-tested.
|
||||
- `<script>` / `<style>` template contexts auto-escape — use `safeHTML` / `safeCSS` where needed.
|
||||
- Static assets that should respond to theme must inherit, not hardcode.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
plans/260503-1120-v0-3-release/
|
||||
├── plan.md (this file)
|
||||
├── phase-01-layout-variants.md
|
||||
├── phase-02-og-image-generation.md
|
||||
├── phase-03-i18n-string-externalization.md
|
||||
├── phase-04-demo-docs-polish.md
|
||||
└── phase-05-v0-3-release.md
|
||||
```
|
||||
@@ -221,6 +221,40 @@ body {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* === layout variants ===
|
||||
* All variant rules scope under .bio__links--{name} to avoid CSS specificity
|
||||
* collisions with the default .link rules (see v0.2 journal for the lesson).
|
||||
*/
|
||||
.bio__links--grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.bio__links--grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.bio__links--inline {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.bio__links--inline .link {
|
||||
padding: .75rem;
|
||||
gap: 0;
|
||||
}
|
||||
/* Visually hide the title in inline mode but keep it for screen readers. */
|
||||
.bio__links--inline .link__title {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -376,6 +410,46 @@ body {
|
||||
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);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.link, .link:hover { transition: none; transform: none; }
|
||||
}
|
||||
Reference in new issue
Block a user