diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c32e404 --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 87a2e94..e9d1458 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..e629272 --- /dev/null +++ b/docs/config.md @@ -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 , 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: "/" + 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. diff --git a/docs/customization.md b/docs/customization.md new file mode 100644 index 0000000..d0d7c86 --- /dev/null +++ b/docs/customization.md @@ -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/.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 + + + +``` + +## 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 + +{{ 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 +``` diff --git a/docs/data-schemas.md b/docs/data-schemas.md new file mode 100644 index 0000000..e88a4bb --- /dev/null +++ b/docs/data-schemas.md @@ -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/.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 `

` under name | +| avatar | string | no | resolved with `relURL`; show `` 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. diff --git a/docs/migrating-from-stack.md b/docs/migrating-from-stack.md new file mode 100644 index 0000000..90711f1 --- /dev/null +++ b/docs/migrating-from-stack.md @@ -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 5–10 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. diff --git a/exampleSite/data/projects.yaml b/exampleSite/data/projects.yaml index 839a6e9..75dd0ff 100644 --- a/exampleSite/data/projects.yaml +++ b/exampleSite/data/projects.yaml @@ -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]