docs: deployment guide + system architecture + code standards

Adds comprehensive documentation for v0.5 release: deployment guide
covering Vercel/Netlify/self-hosted options, system architecture overview,
and code standards for theme development.

Improves onboarding and contribution experience.
This commit is contained in:
2026-05-15 19:51:14 +07:00
parent f01767237b
commit 4c75ce93fd
3 changed files with 301 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
# Code standards
How code in this theme is written and why. The goal is predictability — both for humans reading the diff and for LLM-driven tooling navigating the repo.
## File naming
- **Kebab-case** for all filenames: `link-button.html`, `theme-toggle.js`, `schema-website.html`.
- Long descriptive names beat short cryptic ones — `analytics-loader.html` is better than `al.html`.
- Hugo partials living under `layouts/partials/` use the `.html` extension even when they emit JSON-LD or `<script>` blocks — Hugo doesn't care about extension, but consistency helps grep.
## Partial composition
Each partial reads its input via exactly one of:
1. **`site.Params` directly** — for page-wide singletons (`head.html`, `bio-card.html`, `schema-person.html`).
2. **A `dict` argument** — for reusable rendering blocks (`link-group.html`, `link-button.html`).
Never both. If a partial needs context, pass it explicitly.
## CSS conventions
- **BEM-ish naming**: `.block`, `.block__elem`, `.block--modifier`. Examples: `.bio`, `.bio__avatar`, `.bio__avatar--initials`, `.link`, `.link__icon`, `.link--featured`.
- **CSS custom properties** for every user-tunable value: colors per palette, layout sizes, fonts. Live in `:root` (layout) or `[data-bonsai-theme="…"]` blocks (color).
- **No `!important`** except the `noscript` rule hiding `.theme-toggle` when JS is disabled.
- **No deeply nested selectors** — keep specificity at ≤ (0,2,0). The single attribute selector `svg[stroke="currentColor"]` is the only exception.
- **No web fonts** — system font stacks only (`ui-sans-serif`, `system-ui`, `ui-serif`, `Georgia`).
- **Honor `prefers-reduced-motion`** — every transition lives behind a media query that disables it.
## Template patterns
- **Variable-first**: read params/inputs into named locals at the top of the partial. Branches use the locals.
- **`with` over nil-checks** where idiomatic: `{{ with $tagline }}…{{ end }}` is preferred over `{{ if $tagline }}…{{ end }}`.
- **`else if`, not `else with`** — `else with` had stricter parser rules in some Hugo versions; `else if` is universal.
- **`safeHTML` / `safeJS` / `safeCSS`** only when the value comes from a vetted source (an i18n bundle string or a Hugo `jsonify` payload). Never for user-controlled content rendered raw.
- **Warn loudly** for misconfigurations — `warnf` produces a build-time warning without failing the build.
## i18n keys
- Snake_case: `nav_links_label`, `skip_to_content`, `share_copied`.
- Every user-facing string the theme renders goes through `{{ i18n "key" }}`. Add a fallback default with `| default "…"` so missing keys don't leak raw identifiers into the DOM.
- Both `en.toml` and `vi.toml` must be updated together for any new key.
## Asset handling
- **All processable assets live under `assets/`** (`assets/css/`, `assets/js/`, `assets/icons/`, optional `assets/avatars/`, `assets/og/`, `assets/fonts/`).
- **`static/`** is for files Hugo should copy through verbatim — `_headers`, `vercel.json`, demo media that won't be processed.
- **Fingerprint + minify + SRI** every CSS and JS file shipped to production: `resources.Get | resources.Minify | resources.Fingerprint "sha384"` → emit with `integrity=` + `crossorigin="anonymous"`.
## Opt-in posture
Every new feature is **off by default**. A v0.4 site upgraded to v0.5 must build with byte-identical output unless the user explicitly opts in. This rule has zero exceptions.
When unsure: ship with a `param.foo = false` default and document the opt-in in README.
## Build invariants
- `hugo --gc --minify --themesDir ../..` from `exampleSite/` must produce a clean build with no `ERROR` lines.
- Lighthouse CI must pass thresholds ≥ 0.90 across Performance, Accessibility, Best-Practices, SEO.
- No new `console.error` / `console.warn` from theme JS at runtime.
- CSS production size budget: ≤ 5.5 KB gzipped (stretch ≤ 5 KB).
## Commit messages
Conventional commits: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`, `test:`, `style:`. Scope is optional but helpful: `feat(theme): add multi-section bio`.
No AI references in commit messages. Keep them describing the *change*, not the *process*.
+95
View File
@@ -0,0 +1,95 @@
# Deployment guide
Bonsai is a Hugo theme — the built `public/` directory is plain static files. You can host it anywhere. This guide covers four common targets and the security/cache headers that move Lighthouse Best-Practices and Performance scores into the ≥90 range.
## What Bonsai ships
Two example header files live under `exampleSite/`:
- [`exampleSite/static/_headers`](../exampleSite/static/_headers) — Netlify / Cloudflare Pages format. Copy to your site root.
- [`exampleSite/vercel.json`](../exampleSite/vercel.json) — Vercel format. Copy to your repo root.
Both apply identical policies; pick the one matching your host.
## Why these headers
| Header | Lighthouse audit | What it does |
|--------|-----------------|--------------|
| `X-Content-Type-Options: nosniff` | Best-Practices | Stops browsers MIME-sniffing responses. |
| `X-Frame-Options: SAMEORIGIN` | Best-Practices | Prevents clickjacking via iframe embedding. |
| `Referrer-Policy: strict-origin-when-cross-origin` | Best-Practices | Limits referer leakage on cross-origin requests. |
| `Permissions-Policy: camera=(), microphone=(), geolocation=()` | Best-Practices | Disables unused powerful APIs. |
| `Content-Security-Policy: …` | Best-Practices + XSS resilience | Restricts script/style/image sources. |
| `Cache-Control: public, max-age=31536000, immutable` (on `/css/*`, `/js/*`, `/icons/*`, `/images/*`) | Performance (repeat visits) | 1-year cache on fingerprinted assets. |
| `Cache-Control: public, max-age=3600, must-revalidate` (on `/`, `/index.html`) | Performance | 1-hour cache on HTML; lets you push content updates fast. |
## CSP and Bonsai's defaults
The shipped CSP is permissive enough to support every opt-in Bonsai feature:
- `script-src 'self' 'unsafe-inline' https://www.googletagmanager.com``'unsafe-inline'` is required by the theme-toggle FOUC script and the GA4 init snippet. `googletagmanager.com` lets the GA4 loader fetch `gtag.js`.
- `style-src 'self' 'unsafe-inline'` — required by inline `style=` attrs on the SVG initials avatar.
- `img-src 'self' data: https:` — covers QR-code data URLs and external image avatars.
- `connect-src 'self' https://www.google-analytics.com` — allows GA4 to POST events.
If you don't use the theme toggle and don't use GA4, you can tighten CSP to `script-src 'self'` and `connect-src 'self'`. Strict-CSP with nonces is out of scope for v0.5.
## Hosts
### Netlify
1. Copy `exampleSite/static/_headers` into your site's `static/` directory. Hugo will copy it to `public/_headers`, which Netlify reads on deploy.
2. Build command: `hugo --gc --minify`.
3. Publish directory: `public`.
4. Brotli + gzip applied automatically.
### Cloudflare Pages
Same `_headers` format as Netlify. Build settings same. Brotli applied automatically.
### Vercel
1. Copy `exampleSite/vercel.json` to your repo root.
2. Vercel auto-detects Hugo from `theme.toml` / `config.toml`. Build command: `hugo --gc --minify`. Output: `public`.
3. Brotli applied automatically.
### GitHub Pages
GitHub Pages does **not** support custom response headers. Two options:
- **Front with Cloudflare:** Point your domain to Cloudflare DNS, then origin to `username.github.io`. Apply security headers via a Cloudflare Worker — example below.
- **Switch to Cloudflare Pages or Netlify:** Free tier, custom domain, full header control. Easiest path.
Minimal Cloudflare Worker overlay:
```js
export default {
async fetch(request, env) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('X-Frame-Options', 'SAMEORIGIN');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
headers.set('Content-Security-Policy',
"default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'");
return new Response(response.body, { status: response.status, headers });
}
};
```
## Analytics & consent
Bonsai's optional GA4 integration sets cookies. In jurisdictions with strict privacy law you must obtain user consent **before** loading `gtag.js`:
- **GDPR (EU):** Explicit consent required for non-essential cookies.
- **UK PECR:** Same.
- **California CPRA:** Disclosure + opt-out required.
Bonsai does **not** ship a consent banner. Pair with a consent management platform (Klaro!, Cookiebot, OneTrust) or skip GA4 entirely.
If you don't enable `[params.analytics]`, zero analytics scripts ship — no consent banner needed.
## HTTPS
All recommended hosts (Netlify, Vercel, Cloudflare Pages, GitHub Pages) enforce HTTPS by default. Ensure your `baseURL` in `hugo.toml` starts with `https://`.
+140
View File
@@ -0,0 +1,140 @@
# System architecture
Bonsai is a [Hugo](https://gohugo.io) theme — a directory of templates, partials, CSS, JS, and data files that Hugo reads when building a user's site. There is no runtime: every page, JSON-LD block, OG meta tag, QR PNG, AVIF/WebP image variant, vCard, RSS feed, and Lighthouse-relevant output is produced at `hugo build` time.
## Directory layout
```
bonsai/
├── theme.toml # Hugo theme metadata (name, min_version, license)
├── data/icons.yaml # Public icon name → {family, slug} manifest
├── i18n/ # Localized UI strings (en, vi shipped; extensible)
├── assets/ # Hugo resource root (image/CSS/JS pipeline reads from here)
│ ├── css/ # Stylesheets (fingerprinted at build time)
│ ├── js/ # Optional scripts (theme-toggle, share)
│ └── icons/ # Vendored SVG icons (Simple Icons + Lucide)
├── layouts/
│ ├── _default/baseof.html # HTML5 skeleton
│ ├── index.html # Home page entry (calls bio-card partial)
│ ├── index.rss.xml # RSS 2.0 feed of links/sections
│ ├── _default/vcard.vcf # vCard output template (opt-in)
│ ├── _default/manifest.webmanifest # PWA manifest (opt-in)
│ └── partials/
│ ├── head.html # All <head> content
│ ├── bio-card.html # Main bio card with sections/links branching
│ ├── link-button.html # Individual link (with thumbnail / featured / scheduled / rel / note)
│ ├── link-group.html # Renders one <nav> of links (used by flat + sectioned modes)
│ ├── icon.html # Inline SVG icon lookup (data/icons.yaml → assets/icons/)
│ ├── avatar.html # Avatar: <picture> for local, <img> for URL, SVG initials fallback
│ ├── share-button.html # Opt-in Web Share button
│ ├── qr-block.html # Opt-in QR code block (images.QR)
│ ├── analytics-loader.html # Opt-in GA4 loader + click listener
│ ├── schema-person.html # JSON-LD ProfilePage > Person
│ ├── schema-website.html # Opt-in JSON-LD WebSite
│ ├── theme-toggle-button.html
│ └── footer.html
│ ├── themes/single.html # Demo: color-palette gallery
│ ├── variants/single.html # Demo: layout-variant gallery
│ └── icons/single.html # Demo: icon gallery
└── exampleSite/ # Self-contained demo + reference config
├── hugo.toml # Showcases every opt-in param
├── static/_headers # Netlify / Cloudflare Pages header file
├── vercel.json # Vercel header config
└── content/ # Demo content
```
## Build-time pipelines
### 1. CSS / JS pipeline
```
assets/css/bonsai.css
└─ resources.Get
└─ resources.Minify (strip whitespace, comments)
└─ resources.Fingerprint "sha384"
└─ /css/bonsai.min.<sha>.css + integrity="sha384-..."
```
Same shape for `gallery.css`, `theme-toggle.js`, `share.js`. The fingerprinted output lets hosts apply `Cache-Control: public, max-age=31536000, immutable` to `/css/*` and `/js/*` safely — any content change produces a new filename hash.
### 2. Image pipeline
When `params.avatar` resolves as a local asset:
```
assets/avatars/me.jpg
├─ .Process "resize 112x112 jpg q85" → JPEG 1x
├─ .Process "resize 224x224 jpg q85" → JPEG 2x
├─ .Process "resize 112x112 webp q75" → WebP 1x
├─ .Process "resize 224x224 webp q75" → WebP 2x
├─ .Process "resize 112x112 avif q60" → AVIF 1x
└─ .Process "resize 224x224 avif q60" → AVIF 2x
<picture>
<source type="image/avif" srcset="… 1x, … 2x">
<source type="image/webp" srcset="… 1x, … 2x">
<img src=jpeg-1x srcset=…>
</picture>
```
External-URL avatars (`https://…`) skip the pipeline and emit a plain `<img>` with Phase-1 attributes (width/height/fetchpriority/decoding).
### 3. QR pipeline
```
images.QR <Permalink> {level: medium, scale: 4}
└─ Build-time PNG at /qr_<hash>.png
<img class="bio__qr-img" src="…" width="…" height="…" alt="…">
```
### 4. Auto-OG (infra-only in v0.5)
```
$base := resources.Get params.ogAutoBase (1200×630 PNG)
$font := resources.Get params.ogAutoFont (TTF, latin subset recommended)
$base | images.Filter (images.Text $name {size:72, x:80, y:220, font:$font})
| images.Filter (images.Text $tagline {size:36, x:80, y:340, font:$font})
└─ Generated 1200×630 → og:image + twitter:card=summary_large_image
```
Theme ships no base PNG or font — user supplies via `params.ogAutoBase`/`ogAutoFont` (paths relative to `assets/`).
### 5. JSON-LD strategy
Always: `ProfilePage > Person` (suppressible via `params.schema = false`).
Opt-in: `WebSite` (via `params.schemaWebSite = true`) — emitted as a second `<script type="application/ld+json">` block. Google supports multiple JSON-LD blocks per page.
### 6. i18n flow
Every user-facing string the theme renders is sourced from `i18n/<lang>.toml`. Hugo's `i18n` function falls back from the active language to `en` on missing keys. User content (`name`, `tagline`, `bio`, link `title`s, `footerText`) is never auto-translated.
## Lighthouse-relevant signals (what the theme emits)
| Category | Signal | Source |
|----------|--------|--------|
| Performance | Avatar dims + `fetchpriority="high"` + `decoding="async"` | `partials/avatar.html` |
| Performance | Avatar preload (when local) | `partials/head.html` |
| Performance | Modern image formats (AVIF/WebP/JPEG `<picture>`) | `partials/avatar.html` |
| Performance | Fingerprinted CSS / JS with SRI | `partials/head.html` |
| Performance | Zero web fonts, system stack | `assets/css/bonsai.css` |
| A11y | Skip-link, `<nav aria-label>`, semantic landmarks | `baseof.html`, `bio-card.html` |
| A11y | Tap targets ≥ 48×48 on inline layout | `assets/css/bonsai.css` |
| A11y | `prefers-reduced-motion` honored | `assets/css/bonsai.css` |
| A11y | WCAG-AA accent colors | palette CSS |
| Best-Practices | Security headers (CSP, X-Frame, Referrer-Policy, Permissions-Policy) | `exampleSite/static/_headers` + `vercel.json` |
| Best-Practices | `rel="noopener noreferrer"` on external links | `link-button.html` |
| Best-Practices | SRI `integrity=` on assets | `partials/head.html` |
| SEO | `<link rel="canonical">` | `partials/head.html` |
| SEO | `<meta property="og:url">`, `og:type`, `og:title`, `og:description`, `og:image` | `partials/head.html` |
| SEO | Hreflang alternates (multi-lang sites) | `partials/head.html` |
| SEO | JSON-LD Person (always) + optional WebSite | `schema-person.html`, `schema-website.html` |
| SEO | `<meta name="robots">` configurable | `partials/head.html` |
| SEO | Tap-target sizing | `assets/css/bonsai.css` |
## Backward-compatibility contract
- Every new param added in v0.5 defaults to off OR has a safe default that preserves v0.4 behavior.
- `[[params.links]]` continues to work unchanged when `[[params.sections]]` is absent.
- Avatar URL paths from `static/` still work via the fallback branch in `partials/avatar.html`.
- The CSS/JS file move from `static/` to `assets/` is invisible to user sites — the fingerprinted output is served at `/css/*` and `/js/*`.