mirror of
https://github.com/tiennm99/bonsai.git
synced 2026-09-03 00:17:43 +00:00
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:
@@ -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*.
|
||||
@@ -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://`.
|
||||
@@ -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/*`.
|
||||
Reference in New Issue
Block a user