feat: docs, changelog, expanded examples for v0.1.0 (Phase 8)

- docs/config.md: full params reference (required + optional + per-post
  frontmatter); explains why theme defaults don't deep-merge into
  consumer sites
- docs/data-schemas.md: profile.yaml + projects.yaml field tables
- docs/customization.md: layout overrides, design token override via
  custom CSS, font self-hosting, per-page color overrides, kill switches
- docs/migrating-from-stack.md: submodule swap, drop list, Stack-to-tsuki
  param mapping, frontmatter compat, gotchas (heading id changes break
  inbound anchor links)
- CHANGELOG.md: complete 0.1.0 entry covering all phases
- README.md: drop "under construction"; add full params snippet, search
  + comments + browser support sections, links to all docs
- exampleSite/data/projects.yaml: +tsuki, +penny-pincher (6 total)

Screenshots, miti99 migration, and v0.1.0 tag are user-driven actions
recorded in phase-08 plan.
This commit is contained in:
2026-05-07 22:20:44 +07:00
parent c96cea81c6
commit 25f36aec12
7 changed files with 519 additions and 7 deletions
+38
View File
@@ -0,0 +1,38 @@
# Changelog
All notable changes to tsuki will be documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [SemVer](https://semver.org/).
## [0.1.0] — 2026-05-07
Initial public release.
### Added
- **Layouts** — `baseof`, `home` (hero + projects + recent), `single`, `list`, `archives`, `taxonomy/term`, `_default/taxonomy`, `post/list`, `search/list`, `404`
- **Partials** — `head`, `header`, `footer`, `nav`, `meta`, `post-card`, `pagination`, `archive-group`, `toc`, `comments`, `search-button`, `icon`, `home/{hero,projects,recent-posts}`
- **Render hooks** — `_markup/render-heading.html` for cosmetic anchor links
- **Asset pipeline** — `resources.Concat | minify | fingerprint`. No SCSS, no PostCSS, no Node bundler. CSS bundle ≤ 4 KB gz, JS ≤ 1 KB gz
- **Vietnamese-first** — `time.Format ":date_long"` localized dates, `autoHeadingIDType: github-ascii` for clean URL fragments, line-height tuned for diacritic clearance
- **Dark mode** — CSS custom properties, `prefers-color-scheme` default, persistent toggle via `data-theme` + `localStorage`, no flash on first paint
- **View Transitions API** — `@view-transition: navigation auto` on supported browsers, motion-safe `@keyframes`, falls back to instant nav
- **Table of contents** — sticky on wide viewports, framed block on narrow, IntersectionObserver-driven active heading; gated by `WordCount > 400` and `Params.toc != false`
- **Search** — Pagefind, indexed at build time in CI, UI mounted at `/search/` with full Vi i18n
- **Comments** — Giscus, opt-in via `params.comments.giscus.enable`, theme-sync via `MutationObserver` on `data-theme`
- **i18n** — `i18n/vi.yml` with all UI strings (search, pagination, archive, comments, theme toggle)
- **CI** — `.github/workflows/pages.yml` runs Hugo + Pagefind + uploads to GitHub Pages; `npm ci` resolves Pagefind from `package.json` lockfile (Dependabot-friendly)
- **ExampleSite** — 5 vi-language demo posts, 4 featured projects, profile data, archive, search, all routes verified
### Configuration notes
- Hugo 0.146+ required (uses `_partials`, `_markup`, `_shortcodes` convention).
- Theme defaults are documentation only — Hugo does not deep-merge nested config (`pagination`, `permalinks`, `taxonomies`, `markup`) from themes. Consumer sites must set these in their own `hugo.yaml`. See [`docs/config.md`](docs/config.md).
- Permalink token `:contentbasename` recommended for clean ASCII URLs from leaf bundles.
### Deferred to post-0.1.0
- Self-hosted Be Vietnam Pro / Inter woff2 files (system fonts work fine; user adds woff2 via `static/fonts/` if desired)
- KaTeX math support
- Tag cloud widget
- Image gallery shortcode
[0.1.0]: https://github.com/tiennm99/tsuki/releases/tag/v0.1.0
+56 -7
View File
@@ -8,23 +8,24 @@ A Hugo blog + personal portfolio theme. The homepage *is* the portfolio — bio,
> 月 (*tsuki*): the moon. Quiet, observed, returned to. Companion to [`bonsai`](https://github.com/tiennm99/bonsai) in the same naming family.
**→ [Live demo](https://tiennm99.github.io/tsuki/)** (after first deploy)
**→ [Live demo](https://tiennm99.github.io/tsuki/)**
## Status
🚧 **Under construction** — initial scaffold. See [the implementation plan](https://github.com/tiennm99/miti99/tree/main/plans) for what's coming.
`v0.1.0` — initial release. See [CHANGELOG.md](CHANGELOG.md).
## Features
- **Blog** — posts, tags, categories, year-grouped archive
- **Blog** — posts, tags, categories, year-grouped archive, paginated post list
- **Personal portfolio on the homepage** — driven by `data/profile.yaml` + `data/projects.yaml`, no separate `/portfolio` section
- **Search** — [Pagefind](https://pagefind.app), zero-runtime, indexed at build time
- **Comments** — [Giscus](https://giscus.app) (GitHub Discussions)
- **Vietnamese-first** — diacritic-safe typography, native vi date formats, Be Vietnam Pro fallback
- **Vietnamese-first** — diacritic-safe typography, native vi date formats, ASCII heading IDs
- **Dark mode** — `prefers-color-scheme` + persistent toggle, no flash of wrong theme
- **View Transitions API** — smooth same-document navigation in supporting browsers
- **No build step** — pure Hugo + browser ES modules. No SCSS, no TypeScript, no bundler in the theme.
- **Light** — target ≤ 15 KB CSS gzipped, ≤ 8 KB JS gzipped (excluding Pagefind UI)
- **Table of contents** — auto-mounted on long posts, sticky on wide viewports, IntersectionObserver active highlight
- **No build step** — pure Hugo + browser ES modules. No SCSS, no TypeScript, no bundler in the theme
- **Light** — CSS ≤ 4 KB gz, JS ≤ 1 KB gz (excluding Pagefind UI)
## Quick start
@@ -57,7 +58,55 @@ module:
## Configuration
Documented in [`docs/config.md`](docs/config.md) (coming soon). For now, see `exampleSite/hugo.yaml` and `exampleSite/data/`.
Add the required keys to your site's `hugo.yaml`:
```yaml
theme: tsuki
languageCode: vi
defaultContentLanguage: vi
pagination:
pagerSize: 10
path: page
taxonomies:
category: categories
tag: tags
permalinks:
post: /:year/:month/:day/:contentbasename/
markup:
goldmark:
renderer:
unsafe: true
parser:
autoHeadingIDType: github-ascii
tableOfContents:
startLevel: 2
endLevel: 4
```
Then drop `data/profile.yaml` and `data/projects.yaml` into your site (see [`docs/data-schemas.md`](docs/data-schemas.md)).
Hugo doesn't deep-merge nested config from themes — settings above belong in your *site* `hugo.yaml`. The `exampleSite/hugo.yaml` is a complete working example.
## Documentation
- [`docs/config.md`](docs/config.md) — full params reference
- [`docs/data-schemas.md`](docs/data-schemas.md) — `profile.yaml` + `projects.yaml`
- [`docs/customization.md`](docs/customization.md) — override layouts, tokens, fonts
- [`docs/migrating-from-stack.md`](docs/migrating-from-stack.md) — for users coming from `hugo-theme-stack`
## Search and comments
Search uses Pagefind, built in CI via `npx pagefind --site public` after Hugo. Pinned in `package.json`; bumps via Dependabot. No runtime dependency.
Comments use Giscus. Generate config at [giscus.app](https://giscus.app) and add to `params.comments.giscus.*` to enable. Defaults to off.
## Browser support
Modern evergreen browsers. View Transitions and `:has()` are progressive enhancements; the theme remains functional without them.
## License
+108
View File
@@ -0,0 +1,108 @@
# Configuration
Reference for `hugo.yaml` keys that tsuki reads. **All settings below are set in your *site's* `hugo.yaml`, not in the theme.** The theme ships defaults for documentation only — Hugo does not deep-merge nested config from themes.
## Required
```yaml
languageCode: vi
defaultContentLanguage: vi
theme: tsuki
pagination:
pagerSize: 10
path: page
taxonomies:
category: categories
tag: tags
permalinks:
post: /:year/:month/:day/:contentbasename/
markup:
goldmark:
renderer:
unsafe: true
parser:
autoHeadingIDType: github-ascii # ASCII slugs for vi diacritics
tableOfContents:
startLevel: 2
endLevel: 4
```
## Theme params
```yaml
params:
description: "Site description used in <meta>, falls back to site.Title."
search:
enable: true # mounts /search/ + header search button
home:
recentPostsCount: 5 # how many post-cards on the homepage
toc:
enable: true # gate at template level (currently informational)
minWordCount: 400 # post must exceed this to render TOC
comments:
giscus:
enable: false # opt-in
repo: "<owner>/<repo>"
repoId: "R_kgDO..."
category: "Announcements"
categoryId: "DIC_kwDO..."
mapping: "pathname" # pathname | url | title | og:title
strict: "0"
reactionsEnabled: "1"
inputPosition: "bottom" # top | bottom
theme: "preferred_color_scheme"
lang: "vi"
```
Generate Giscus values at [giscus.app](https://giscus.app). Only `enable: true` activates the partial; missing IDs are ignored.
## Profile and projects
The homepage portfolio reads from data files, **not** `params`:
- `data/profile.yaml` — bio, avatar, links. See [data-schemas.md](data-schemas.md).
- `data/projects.yaml` — featured projects grid. See [data-schemas.md](data-schemas.md).
## Per-post frontmatter
```yaml
---
title: "Tiêu đề bài viết"
date: 2026-05-07T19:00:00+07:00
draft: false
description: "Tóm tắt 1-2 câu (dùng cho meta + post-card)."
tags: ["hugo", "viet"]
categories: ["ghi-chu"]
toc: false # opt out of TOC for this post (default: render if WordCount > 400)
comments: false # opt out of comments for this post (default: enabled if giscus.enable)
image: "img/cover.jpg" # OG image override
---
```
## Optional
```yaml
menus:
main:
- name: "Bài viết"
url: /post/
weight: 10
- name: "Lưu trữ"
url: /archives/
weight: 20
- name: "Thẻ"
url: /tags/
weight: 30
```
## Why theme defaults don't merge
Hugo's config-merging strategy for nested keys (markup, permalinks, pagination) is "none" by default. The theme ships a complete `hugo.yaml` for reference, but consumer sites must duplicate the keys above. See `exampleSite/hugo.yaml` for a working example.
+122
View File
@@ -0,0 +1,122 @@
# Customization
Hugo's lookup order means **anything in your site overrides the theme**. No fork required.
## Override layouts and partials
Drop a file with the same path under your site's `layouts/` to replace the theme version.
```
your-site/
└── layouts/
├── _partials/
│ └── footer.html # overrides themes/tsuki/layouts/_partials/footer.html
└── single.html # overrides themes/tsuki/layouts/single.html
```
To extend rather than replace, copy the theme partial into your site, then modify. Common overrides:
- `_partials/footer.html` — add license, build info, web ring links
- `_partials/home/hero.html` — restructure the homepage hero
- `_partials/comments.html` — swap Giscus for a different provider
## Override design tokens
Tokens are CSS custom properties on `:root` and `:where([data-theme="dark"])`. Override them in a custom stylesheet without editing the theme.
```css
/* your-site/assets/css/custom.css */
:root {
--tsuki-accent: #cc7a3b; /* warmer accent */
--tsuki-font-sans: "Public Sans", system-ui, sans-serif;
--tsuki-content-width: 48rem; /* wider posts */
}
:where([data-theme="dark"]) {
--tsuki-accent: #f0a070;
}
```
Append it to the bundle by overriding `_partials/head.html`. Copy the theme version, add your file:
```go-html-template
{{- $cssFiles := slice
(resources.Get "css/tokens.css")
(resources.Get "css/reset.css")
...
(resources.Get "css/view-transitions.css")
(resources.Get "css/custom.css") {{/* your override, last in cascade */}}
-}}
```
## Add custom icons
Drop SVGs into `assets/icons/<name>.svg`, then reference by name:
```yaml
# data/profile.yaml
links:
- icon: bluesky
label: Bluesky
url: https://bsky.app/profile/...
```
Use `currentColor` for fill/stroke so icons inherit text color in light and dark modes:
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="..."/>
</svg>
```
## Self-host fonts
The theme stack is `"Inter", "Be Vietnam Pro", system-ui, ...`. The first two fall back to system fonts when missing. To self-host:
1. Subset Be Vietnam Pro + Inter to `vietnamese,latin` ranges.
2. Drop the woff2 files into `static/fonts/`.
3. Add `@font-face` declarations in `assets/css/custom.css` (see "Override design tokens" above).
```css
@font-face {
font-family: "Inter";
src: url("/fonts/Inter-VariableFont_slnt,wght.woff2") format("woff2-variations");
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: "Be Vietnam Pro";
src: url("/fonts/BeVietnamPro-Regular.woff2") format("woff2");
font-weight: 400;
font-display: swap;
}
```
## Override colors per-page
Set CSS variables inline via a frontmatter `style` you wire up yourself, or use the `body_class` block from `baseof.html`:
```html
<!-- layouts/section/work.html -->
{{ define "body_class" }}list section-work{{ end }}
```
Then style `body.section-work { --tsuki-accent: #...; }` in your custom CSS.
## Disable features
Every interactive feature has a kill switch:
```yaml
params:
search:
enable: false # removes search button + /search/ route
comments:
giscus:
enable: false # comments partial becomes a no-op
```
Per-post:
```yaml
toc: false
comments: false
```
+73
View File
@@ -0,0 +1,73 @@
# Data schemas
The homepage portfolio is data-driven. Two YAML files in `data/`.
## `data/profile.yaml`
Powers the homepage hero and the OG image fallback.
```yaml
name: "Tien Nguyen" # required: display name
handle: "tiennm99" # optional: short handle (currently unused; reserved)
tagline: "Building tools, breaking things, writing it down." # optional: under name
avatar: "img/avatar.svg" # optional: site-relative path under static/
bio: | # optional: markdown, rendered with markdownify
Software engineer based in Ho Chi Minh City. I build for the web,
experiment with AI tooling, and write down what I learn.
links: # optional: array of {icon, label, url}
- icon: github # icon name resolves to assets/icons/<name>.svg
label: GitHub
url: https://github.com/tiennm99
- icon: mail
label: Email
url: mailto:hi@example.com
- icon: rss
label: RSS
url: /index.xml
```
Field reference:
| field | type | required | notes |
|---------|--------|----------|-------------------------------------------|
| name | string | yes | falls back to `site.Title` if absent |
| handle | string | no | reserved |
| tagline | string | no | rendered as `<p>` under name |
| avatar | string | no | resolved with `relURL`; show `<img>` if set |
| bio | string | no | markdown; `markdownify` filter applied |
| links | array | no | empty list = no links section |
Built-in icons under `assets/icons/`: `github`, `mail`, `rss`, `search`. Add your own SVGs there with `currentColor` fill.
## `data/projects.yaml`
Powers the featured projects grid on the homepage.
```yaml
featured:
- title: "bonsai"
tagline: "Minimalist Hugo theme for link-in-bio."
repo: https://github.com/tiennm99/bonsai
demo: https://tiennm99.github.io/bonsai/
image: "img/projects/bonsai.svg"
tags: [hugo, theme, minimal]
- title: "vngeoguessr"
tagline: "GeoGuessr clone for Vietnamese locations."
repo: https://github.com/tiennm99/vngeoguessr
image: "img/projects/vngeoguessr.svg"
tags: [geo, mapillary, redis]
```
Field reference (per project):
| field | type | required | notes |
|---------|----------|----------|--------------------------------------------------|
| title | string | yes | card heading |
| tagline | string | no | one-line description under title |
| image | string | no | site-relative; rendered 16:9 with `object-fit: cover` |
| repo | URL | no | "repo →" link if present |
| demo | URL | no | "demo →" link if present |
| tags | string[] | no | small chips below tagline |
The grid uses `auto-fit, minmax(16rem, 1fr)`; cards reflow on narrow viewports automatically.
+111
View File
@@ -0,0 +1,111 @@
# Migrating from hugo-theme-stack
tsuki extracts Stack's information architecture (posts, tags, categories, archive, search, comments) and rewrites the visual layer + asset pipeline. Most posts will render unchanged. This guide covers what to delete, what to add, and where Stack-isms hide.
## What carries over
- Post frontmatter: `title`, `date`, `description`, `tags`, `categories`, `image`, `draft`
- Content path convention `content/post/YYYY/MM/DD/slug/index.md`
- Permalink shape `/:year/:month/:day/:slug/` (set `:contentbasename` on tsuki for clean ASCII)
- Tags and categories taxonomies
- Archive page (`content/archives/_index.md`)
- Giscus comment provider
## What changes
### Replace theme submodule
```bash
git submodule deinit themes/hugo-theme-stack
git rm themes/hugo-theme-stack
git submodule add https://github.com/tiennm99/tsuki themes/tsuki
```
### Update `theme:`
```yaml
theme: tsuki # was: hugo-theme-stack
```
### Drop these (Stack-only)
- `params.sidebar.*` — tsuki has no sidebar
- `params.widgets.*` — no widget system; portfolio is in homepage hero/projects
- `params.article.headingAnchor` — anchors render-hooked into headings unconditionally (cosmetic, opt-out is via removing the partial)
- `params.colorScheme.toggleIcon` — fixed icon
- `params.imageProcessing.*` — tsuki uses Hugo's default image processing
- `params.comments.provider` — only Giscus is supported; no Disqus / Utterances / Waline
- TypeScript build pipeline (`assets/ts/`, `tsconfig.json`) — tsuki ships ES modules directly
- SCSS pipeline (`assets/scss/`) — tsuki uses plain CSS with custom properties
### Param mapping
| Stack | tsuki | Notes |
|---|---|---|
| `params.description` | `params.description` | unchanged |
| `params.sidebar.avatar.src` | `data/profile.yaml: avatar` | now data-driven |
| `params.sidebar.subtitle` | `data/profile.yaml: tagline` | |
| `params.sidebar.emoji` | — | dropped |
| `params.menu.social[].icon` | `data/profile.yaml: links[].icon` | same icon names |
| `params.article.toc` | `params.toc.enable` + per-post `toc:` | |
| `params.search.fuse` | — | replaced by Pagefind (different config; see [config.md](config.md)) |
| `params.colorScheme.default` | system: respects `prefers-color-scheme` | toggle persists in localStorage |
| `params.dateFormat.published` | — | uses `time.Format ":date_long"` with site language |
| `params.imageProcessing.cover.enabled` | — | dropped; cover image renders as-is |
### Content frontmatter
Stack-specific fields ignored without error:
- `image` — Stack used this for sidebar cover; tsuki uses it for OG image only
- `categories[0]` weight quirks — tsuki uses Hugo defaults
- `menu`, `weight` — still respected
- `slug` — still respected
No content rewrite required for typical posts.
## Required additions
Add these to your `hugo.yaml` (tsuki defaults don't deep-merge from theme):
```yaml
pagination:
pagerSize: 10
path: page
permalinks:
post: /:year/:month/:day/:contentbasename/
markup:
goldmark:
renderer:
unsafe: true
parser:
autoHeadingIDType: github-ascii
tableOfContents:
startLevel: 2
endLevel: 4
```
If your existing permalink scheme differs, **keep it** — the theme works with any permalink shape.
## Build and verify
```bash
hugo --gc --minify
```
Spot-check 510 posts in the rendered output. Common surprises:
- **TOC appears unexpectedly**: gated by `WordCount > 400` and `Params.toc != false`. Add `toc: false` to opt out.
- **Heading IDs changed**: switching to `autoHeadingIDType: github-ascii` produces different anchor slugs. Old `#lập-trình` becomes `#lap-trinh`. Inbound links from external sites break. Mitigate with redirects, or skip the `autoHeadingIDType` change and accept Vi diacritics in fragment URLs.
- **Search results paths**: rebuild Pagefind index (`npx pagefind --site public`); the old Fuse.js JSON index becomes dead weight.
## Dropped features (consider before migrating)
- KaTeX math (Phase 9 follow-up; available in Stack)
- Tag cloud widget (deferred)
- Image gallery shortcode (deferred)
- Any Stack-specific shortcode (`gallery`, `keyword`, `bilibili`, etc.)
If you rely on these heavily, defer the migration or wait for the follow-up release.
+11
View File
@@ -23,3 +23,14 @@ featured:
repo: https://github.com/tiennm99/vngeoguessr
image: "img/projects/vngeoguessr.svg"
tags: [geo, mapillary, redis]
- title: "tsuki"
tagline: "Hugo blog + personal portfolio theme. Vietnamese-first, no build step, View Transitions API."
repo: https://github.com/tiennm99/tsuki
demo: https://tiennm99.github.io/tsuki/
tags: [hugo, theme, vi]
- title: "penny-pincher"
tagline: "Personal expense tracker on Cloudflare D1 + Workers. Telegram-bot-first input, web read-only view."
repo: https://github.com/tiennm99/penny-pincher-provider
tags: [cloudflare, d1, telegram]