mirror of
https://github.com/tiennm99/tsuki.git
synced 2026-05-23 08:26:06 +00:00
docs: v0.2.0 changelog and implementation roadmap
Document all v0.2.0 phases (audit, a11y, SEO, UX, discovery, release, CI) with detailed changes. Archive implementation plan and research reports.
This commit is contained in:
@@ -2,6 +2,71 @@
|
||||
|
||||
All notable changes to tsuki will be documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [SemVer](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **CI smoke tests** (`scripts/smoke-tests.sh`) run after Hugo build — assert JSON-LD on post / not on home, OG/Twitter image emit, skip-link + `<main id="main">`, render-link rel marker, reading-time byline, related-posts aside, CSS budget. 11 checks, fails the workflow on regression. Local-runnable: `./scripts/smoke-tests.sh`.
|
||||
- **htmltest** (`.htmltest.yml` + GitHub Action) runs after smoke tests — catches broken internal links, missing alt attributes, malformed HTML5 in the demo build. External link checking disabled (network flake).
|
||||
- **CSS budget badge** in README.
|
||||
- **Hugo Module mounts** declared in `hugo.yaml` — explicit `module.mounts` list ensures the theme works under Hugo Module consumption with custom site `assetDir`/`layoutDir` overrides.
|
||||
- **`docs/installation.md`** — single source for submodule + Hugo Module install, Pagefind setup quirks under each, required site-config minimum, post-install verification checklist. README's install section now links here.
|
||||
- **Related posts** under every single post — `_partials/related-posts.html` uses Hugo's built-in `.Related` index keyed by tags + categories + date, weighted 100/60/10. Section silently disappears when no relations exist. Default 3 cards; tune via `params.relatedPostsCount`. Reuses the existing `post-card.html` partial. Requires `related:` config in site `hugo.yaml` (Hugo no-deep-merge rule); documented in `docs/config.md`.
|
||||
- **`relatedPosts` i18n key** (`Bài viết liên quan`).
|
||||
- **Markdown callouts** — `> [!note]`, `> [!tip]`, `> [!important]`, `> [!warning]`, `> [!caution]` render as styled callouts via `_markup/render-blockquote.html`. Five color tokens, light + dark mode tuned. Plain blockquotes pass through unchanged. Titles localize through new `i18n/vi.yml` keys (`calloutNote` etc.); override per-callout with `> [!note] Custom title`. CSS lives in dedicated `assets/css/callouts.css` bundled into the main pipeline.
|
||||
- **`assets/css/callouts.css`** — added to the concat order in `head.html`.
|
||||
- **Optional word-count byline** — gated on `params.showWordCount: true` (default off). New `wordCount` i18n key (`{{ .Count }} từ`).
|
||||
- **Archetype expansion** — `archetypes/default.md` now includes `description`, `cover.image`, and pre-populated `tags`/`categories` placeholders.
|
||||
- **JSON-LD Article schema** on every post page — `headline`, `datePublished`, `dateModified`, `author` (Person), `publisher` (Organization), `image`, `description`, `keywords`. Emits only when `IsPage && Kind == "page"`; passes `validator.schema.org` for the demo posts.
|
||||
- **OG/Twitter improvements** — `og:locale` from site language, `article:author`, one `article:tag` per post tag, `twitter:site` + `twitter:creator` from new `params.social.twitter`, OG/Twitter description capped at 200 chars (Twitter limit) via rune-safe `truncate`.
|
||||
- **`_partials/head/seo.html`** + **`_partials/head/og-image.html`** — extracted from `head.html` for cleaner per-site overrides; OG image resolves through `cover.image` → `image` → `params.og.fallbackImage` → `params.profile.avatar`.
|
||||
- **`params.author`, `params.social.twitter`, `params.og.fallbackImage`** documented in `docs/config.md`.
|
||||
- **`cover.image` per-post frontmatter** as the preferred OG/Twitter cover key (legacy `image` still works).
|
||||
- **Skip-link** — first focusable element on every page; jumps to `<main id="main">` (a11y M3).
|
||||
- **Visible focus rings** — `:focus-visible { outline: 2px solid var(--tsuki-accent) }` site-wide; previously relied on browser defaults that disappeared in dark mode (a11y M3).
|
||||
- **`Lastmod` byline** — post meta shows "Cập nhật {date}" when modified date is at least 24 h newer than publish date (audit M2).
|
||||
- **`_markup/render-link.html`** — external markdown links automatically get `rel="noopener noreferrer"` (audit N1).
|
||||
- **`_markup/render-image.html`** — in-content markdown images automatically get `loading="lazy" decoding="async"` (audit N2).
|
||||
- **`skipToContent` i18n key.**
|
||||
- **Theme-contract notes** in `docs/config.md` documenting taxonomy plural names, Vi tag title bundles, and the full effect of `params.search.enable: false` (forward-looking concerns from the v0.1.1 review).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Site title in header + copyright in footer now read from `data/profile.yaml: name`** (previously read from non-existent `params.profile.name`, silently falling back to `site.Title`). Sites with `data/profile.yaml: name` set will see the correct name appear in `<header>` and footer for the first time.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`<meta name="generator">`** no longer leaks Hugo version — emits `tsuki` only (audit N5).
|
||||
- **Giscus iframe theme sync** posts the current theme as soon as the iframe is ready, eliminating the brief flash to the default theme on first paint (audit L5).
|
||||
- **Categories are explicitly routing-only** — `categories` taxonomy still routes under `/categories/<slug>/` but does not surface in post meta. Documented inline in `meta.html`.
|
||||
|
||||
### Removed
|
||||
|
||||
- **`view-transition-name: var(--tsuki-vt-name, none)`** on `.project-card` — dead declaration; nothing set the variable. Removed pending a future card-morph implementation (audit M4).
|
||||
- **Empty `[original]` block** dropped from `theme.toml` — tsuki is an original theme, not a fork; the empty fields tripped the Hugo theme registry's malformed-frontmatter check (audit L1).
|
||||
|
||||
## [0.1.1] — 2026-05-08
|
||||
|
||||
Patch release. Audit fixes and documentation/code drift. No new features.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **TOC config now honors `params.toc.{enable,minWordCount}`** — previously `single.html` and `footer.html` hardcoded a 400-word threshold and ignored the `enable` flag (audit C1).
|
||||
- **Tag URLs survive taxonomy renames** — `meta.html` and `single.html` now use `.GetTerms "tags"` + `RelPermalink` instead of hardcoded `/tags/...` (audit C3).
|
||||
- **`params.search.enable: false` removes the route**, not just the header button. `search/list.html` is now gated; disabled sites get a localized fallback message (audit H2).
|
||||
- **Home page no longer emits dead `<link rel=prev/next>`** to non-existent paginated URLs. Pagination links emit only on paginated kinds with `>1` page (audit H4).
|
||||
- **Code-copy buttons hidden when `navigator.clipboard` unavailable** (HTTP non-localhost) — no orphan "Lỗi" buttons on insecure-origin previews (audit H5).
|
||||
- **`recent-posts.html` query bound once** instead of evaluated twice (audit H8). `Draft` filter dropped (already excluded by `RegularPages`).
|
||||
- **Seven i18n keys added** to `vi.yml`: six audit-flagged keys (`comments`, `month`, `pageNotFound`, `backHome`, `searchSuggestion`, `altSearch`) plus `searchDisabled` for the new search-disabled fallback (audit H1).
|
||||
|
||||
### Changed
|
||||
|
||||
- **CI asserts CSS budget ≤ 4200 B gz** on every push. Build fails if the fingerprinted bundle grows beyond the documented limit (audit H3).
|
||||
- **Removed unused `previousPage`/`nextPage` i18n keys.** Pagination has always used `prev`/`next`.
|
||||
- **`docs/data-schemas.md`** documents the security implication of `markup.goldmark.renderer.unsafe: true` + `markdownify` on `profile.bio`. Treat `data/profile.yaml` as trusted-author input (audit C2 — documentation path).
|
||||
|
||||
[0.1.1]: https://github.com/tiennm99/tsuki/releases/tag/v0.1.1
|
||||
|
||||
## [0.1.0] — 2026-05-07
|
||||
|
||||
Initial public release.
|
||||
@@ -36,3 +101,5 @@ Initial public release.
|
||||
- Image gallery shortcode
|
||||
|
||||
[0.1.0]: https://github.com/tiennm99/tsuki/releases/tag/v0.1.0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "v0.1.1 bugs & doc/code drift"
|
||||
status: completed
|
||||
completed_date: 2026-05-08
|
||||
priority: P1
|
||||
effort: "1d"
|
||||
dependencies: []
|
||||
ships_as: v0.1.1
|
||||
---
|
||||
|
||||
# Phase 1: v0.1.1 bugs & doc/code drift
|
||||
|
||||
## Context Links
|
||||
|
||||
- Audit: `plans/reports/code-reviewer-260508-2305-tsuki-v0.1.0-audit.md` — Critical + High sections
|
||||
- Researcher: `plans/reports/researcher-260508-2306-hugo-theme-best-practices.md` — § 8 Performance benchmarks
|
||||
|
||||
## Overview
|
||||
|
||||
Patch release. Resolve audit-flagged correctness bugs and doc/code drift. No new user-facing features. Goal: docs match behavior, tag URLs survive taxonomy renames, route gates work, no 404 SEO leakage, CSS budget enforced in CI.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- `params.toc.enable` and `params.toc.minWordCount` actually gate render
|
||||
- Tag/category URLs resolve through Hugo's taxonomy graph (survive renames)
|
||||
- All template-referenced i18n keys exist in `vi.yml`
|
||||
- `params.search.enable: false` removes the route, not just the header button
|
||||
- Home page emits no `<link rel=prev/next>` to non-existent paginated paths
|
||||
- Code-copy button hidden when `navigator.clipboard` unavailable
|
||||
|
||||
**Non-functional**
|
||||
- CSS bundle ≤ 4 KB gz asserted in CI (fails build if exceeded)
|
||||
- No CHANGELOG drift after merge
|
||||
|
||||
## Architecture
|
||||
|
||||
Single-file template + asset edits, no new partials, no JS additions. CI gains one assertion step.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Modify**
|
||||
- `layouts/single.html:28` — TOC gate
|
||||
- `layouts/_partials/footer.html:19` — JS TOC gate
|
||||
- `layouts/_partials/meta.html:16` — replace hardcoded `/tags/` with `.GetTerms`
|
||||
- `layouts/single.html:21` — same
|
||||
- `layouts/_partials/head.html:55` — drop `.IsHome` from paginator linking; gate on `gt .Paginator.TotalPages 1`
|
||||
- `layouts/search/list.html` — wrap body in `params.search.enable | default true`
|
||||
- `layouts/_partials/home/recent-posts.html:2,15` — bind `$all` once
|
||||
- `assets/js/code-copy.js:5` — early-return when `!navigator.clipboard`
|
||||
- `i18n/vi.yml` — add 8 missing keys (`comments`, `month`, `pageNotFound`, `backHome`, `featuredProjects`, `viewAll`, `searchSuggestion`, `altSearch`); remove unused `previousPage`/`nextPage` OR rename pagination call sites
|
||||
- `docs/data-schemas.md` — security note on `bio` HTML rendering (C2 docs path)
|
||||
- `.github/workflows/pages.yml` — add CSS budget assertion step
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **TOC config (C1)** — replace hardcoded 400 in `single.html` and `footer.html` with `site.Params.toc.minWordCount | default 400`; honor `site.Params.toc.enable | default true`. Per-page `Params.toc != false` still wins.
|
||||
2. **Tag URLs (C3)** — switch `meta.html` and `single.html` tag iteration to `.GetTerms "tags"` + `$term.RelPermalink` + `$term.LinkTitle`. Add same for categories if Phase 2 surfaces them.
|
||||
3. **i18n keys (H1)** — add the 8 missing keys to `vi.yml` with vi translations. For `month`, return `"Tháng {{ .Number }}"`. Audit `previousPage`/`nextPage` vs `prev`/`next` and unify.
|
||||
4. **Search gate (H2)** — wrap `search/list.html` body in `{{- if site.Params.search.enable | default true -}}` else emit a localized "search disabled" message.
|
||||
5. **Home pagination 404 (H4)** — change `head.html` paginator gate to `{{- with .Paginator -}}{{- if gt .TotalPages 1 -}} … {{- end -}}{{- end -}}` and drop `.IsHome` from the kind whitelist. Verify `home.html` doesn't call `.Paginator`.
|
||||
6. **Code-copy clipboard fallback (H5)** — at top of attach loop, `if (!navigator.clipboard) return;` before button creation.
|
||||
7. **Recent-posts double-where (H8)** — bind `$all := where (where site.RegularPages "Type" "post") "Draft" false` once; reuse for `len` check. Drop `"Draft" false` unless explicitly supporting `--buildDrafts`.
|
||||
8. **CSS budget assertion (H3)** — add CI step that gzips the fingerprinted CSS bundle and fails if > 4200 B. Use `find exampleSite/public -name 'tsuki.bundle.*.css'` then `gzip -9 -c | wc -c`.
|
||||
9. **C2 docs decision** — Maintainer answers Q1 (is `unsafe: true` required?). If no, drop `markup.goldmark.renderer.unsafe: true` from theme defaults and from docs; this kills the bio XSS surface. If yes, add a `! Security` block to `docs/data-schemas.md` near `bio` warning HTML executes.
|
||||
10. **Verify exampleSite still renders** — `cd exampleSite && hugo --gc --minify` clean.
|
||||
11. **CHANGELOG** — add `[0.1.1]` section listing each fix. Reference audit IDs in commit body.
|
||||
12. **Tag and release** — `v0.1.1` git tag.
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] C1 TOC gate honors `params.toc.{enable,minWordCount}`
|
||||
- [x] C3 tag URLs use `.GetTerms`
|
||||
- [x] H1 8 missing i18n keys added; unused keys removed
|
||||
- [x] H2 `search/list.html` gated on `params.search.enable`
|
||||
- [x] H4 home Paginator no longer emits dead prev/next links
|
||||
- [x] H5 code-copy hidden when clipboard API absent
|
||||
- [x] H8 recent-posts query bound once
|
||||
- [x] H3 CSS budget asserted in CI (≤ 4200 B gz)
|
||||
- [x] C2 maintainer decision recorded; docs OR config updated
|
||||
- [x] CHANGELOG `[0.1.1]` written
|
||||
- [x] `v0.1.1` tagged
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- `cd exampleSite && hugo --gc --minify` produces a build with no broken `/tags/...` links (verify with `htmltest`)
|
||||
- Setting `params.toc.enable: false` in `exampleSite/hugo.yaml` suppresses TOC site-wide
|
||||
- Setting `params.toc.minWordCount: 99999` suppresses TOC on all current posts
|
||||
- Renaming `taxonomies: { tag: tag }` in `exampleSite/hugo.yaml` does not produce 404 tag links
|
||||
- `params.search.enable: false` returns empty `/search/` body with i18n message
|
||||
- Disabling JS or running on `http://` non-localhost: no orphan code-copy buttons
|
||||
- CI rejects a PR that pushes `tsuki.bundle.css` gzipped over 4200 B
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **CSS budget cliff**: post-minify is ~3.7-3.9 KB; 4200 B has ~250 B headroom. Risk: any Phase 2/3 CSS addition tips the budget. Mitigation: assertion catches it; if breached, audit which selectors are unused or compressible.
|
||||
- **i18n key churn**: changing `previousPage` → `prev` (or vice versa) across both templates and `vi.yml` is a textual rename, low risk if grep-driven.
|
||||
- **`.GetTerms` Vietnamese titles**: tags with diacritics (`ghi-chú`) — verify `.LinkTitle` displays diacritics correctly in browser, not the URL slug.
|
||||
- **C2 decision** — if maintainer wants `unsafe: true` removed, need to verify exampleSite posts don't use raw HTML; otherwise content breaks.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- C2 (XSS via bio) resolved by either docs warning or removing `unsafe: true`. The latter is preferred if compatible with content.
|
||||
- N5 generator meta privacy — defer to Phase 2 (low priority, doesn't ship in v0.1.1).
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 2 (parallel-safe after Phase 1 lands) — accessibility polish picks up M3, L5, M4, render hooks.
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
phase: 2
|
||||
title: "Accessibility & polish"
|
||||
status: completed
|
||||
completed_date: 2026-05-09
|
||||
priority: P1
|
||||
effort: "1d"
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 2: Accessibility & polish
|
||||
|
||||
## Context Links
|
||||
|
||||
- Audit: `plans/reports/code-reviewer-260508-2305-tsuki-v0.1.0-audit.md` — M1, M2, M3, M4, L5, N1, N2, N5
|
||||
- Researcher: `plans/reports/researcher-260508-2306-hugo-theme-best-practices.md` — § 3 SEO & a11y baseline
|
||||
|
||||
## Overview
|
||||
|
||||
Close keyboard-nav and focus-visibility gaps, fix theme-toggle/giscus first-paint flash, decide categories visibility, add render hooks for safer external links and lazy images. No layout changes; CSS additions kept inside budget.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Skip-link present and functional on every page
|
||||
- All interactive elements have visible focus rings in dark + light modes
|
||||
- Giscus opens with the theme already applied (no flash to default theme)
|
||||
- External markdown links open with `rel="noopener noreferrer"`
|
||||
- In-content images get `loading="lazy" decoding="async"`
|
||||
- Categories either surface in UI or are documented as routing-only
|
||||
|
||||
**Non-functional**
|
||||
- WCAG 2.1 AA contrast retained
|
||||
- Total CSS additions < 200 B gz (skip-link + focus rings)
|
||||
- No new JS bytes (giscus fix is a 1-line ordering tweak)
|
||||
|
||||
## Architecture
|
||||
|
||||
CSS-only additions in `components.css` for skip-link + `:focus-visible`. Two new render hooks (`_markup/render-link.html`, `_markup/render-image.html`). Template-level: skip-link in `baseof.html`, `id="main"` on `<main>`, generator meta change.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Create**
|
||||
- `layouts/_markup/render-link.html`
|
||||
- `layouts/_markup/render-image.html`
|
||||
|
||||
**Modify**
|
||||
- `layouts/baseof.html` — add `<a class="skip-link">`, `id="main"` on `<main>`
|
||||
- `layouts/_partials/head.html:7` — drop `hugo.Version` from generator meta (N5)
|
||||
- `layouts/_partials/comments.html` — restructure giscus init order
|
||||
- `assets/js/giscus-theme.js` — call `send()` once on load before observer (L5)
|
||||
- `assets/css/components.css` — `:focus-visible`, `.skip-link` styles
|
||||
- `assets/css/home.css:82` — remove dead `--tsuki-vt-name` rule OR wire it up to a per-card index (M4)
|
||||
- `i18n/vi.yml` — add `skipToContent` key
|
||||
- `layouts/_partials/meta.html` — categories pill (M1, conditional on maintainer decision)
|
||||
- `layouts/_partials/post-card.html` — `Lastmod` display if present (M2)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Skip-link (M3)** — add to `baseof.html` immediately after `<body>`:
|
||||
```html
|
||||
<a class="skip-link" href="#main">{{ i18n "skipToContent" }}</a>
|
||||
```
|
||||
Add `id="main"` to `<main>`. CSS: `.skip-link { position: absolute; left: -9999px; } .skip-link:focus { left: 1rem; top: 1rem; z-index: 100; }`.
|
||||
2. **`:focus-visible` (M3)** — append to `components.css`:
|
||||
```css
|
||||
:focus-visible { outline: 2px solid var(--tsuki-accent); outline-offset: 2px; }
|
||||
```
|
||||
3. **Giscus first-paint (L5)** — in `giscus-theme.js`, run `send()` on load before MutationObserver. Verify Giscus iframe shows correct theme on first render.
|
||||
4. **Categories decision (M1)** — *maintainer answers Q2 in plan.md.* If "surface": add a small pill list in `meta.html` next to tags using `.GetTerms "categories"`. If "routing-only": add a comment in `meta.html` documenting why categories are deliberately invisible; remove `categories` taxonomy from theme defaults if appropriate.
|
||||
5. **Lastmod UI (M2)** — in `meta.html`, after the publish date, add `{{- with .Lastmod -}}{{- if ne (. | dateFormat "2006-01-02") ($.Date | dateFormat "2006-01-02") -}}<span class="post-meta-lastmod">{{ i18n "updatedOn" }} {{ . | time.Format ":date_long" }}</span>{{- end -}}{{- end -}}`.
|
||||
6. **Dead `--tsuki-vt-name` (M4)** — *maintainer chooses:* either delete `home.css:82` line, or wire up by setting `style="--tsuki-vt-name: project-{{ $index }}"` on each card in `home/projects.html`. Default: delete (no demand evidence).
|
||||
7. **render-link hook (N1)** — `_markup/render-link.html`:
|
||||
```go-html-template
|
||||
{{- $isExternal := strings.HasPrefix .Destination "http" -}}
|
||||
<a href="{{ .Destination | safeURL }}"
|
||||
{{- with .Title }} title="{{ . }}"{{ end }}
|
||||
{{- if $isExternal }} rel="noopener noreferrer"{{ end -}}
|
||||
>{{ .Text | safeHTML }}</a>
|
||||
```
|
||||
Note: deliberately no `target="_blank"` — UX choice; let users opt in.
|
||||
8. **render-image hook (N2)** — `_markup/render-image.html`:
|
||||
```go-html-template
|
||||
<img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" loading="lazy" decoding="async"
|
||||
{{- with .Title }} title="{{ . }}"{{ end }} />
|
||||
```
|
||||
9. **Generator meta (N5)** — `head.html:7` change to `<meta name="generator" content="tsuki">`. No version disclosure.
|
||||
10. **i18n** — add `skipToContent: "Đến nội dung chính"` and (if M2 lands) confirm `updatedOn` exists.
|
||||
11. **Bundle size check** — gzip CSS bundle, confirm still ≤ 4200 B. Adjust if needed.
|
||||
12. **CHANGELOG** — `### Added` for skip-link, render hooks, categories pill (if). `### Changed` for generator meta.
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] Skip-link in `baseof.html` + `#main` anchor + i18n key
|
||||
- [x] `:focus-visible` CSS rule
|
||||
- [x] Giscus initial-paint fix
|
||||
- [x] Categories surface decision recorded (Q2 answered): **routing-only, explicitly documented in meta.html**
|
||||
- [x] Categories pill OR routing-only doc note
|
||||
- [x] Lastmod UI in meta.html (or i18n key removed)
|
||||
- [x] `--tsuki-vt-name` removed (no use case found)
|
||||
- [x] `_markup/render-link.html` created
|
||||
- [x] `_markup/render-image.html` created
|
||||
- [x] Generator meta no longer leaks Hugo version
|
||||
- [x] CSS bundle still ≤ 4200 B gz
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Tab through homepage from URL bar → first focusable element is skip-link
|
||||
- Tab navigation through every page shows visible accent-colored ring on each interactive element in both themes
|
||||
- Giscus loads visually-correct theme on first paint (no flash)
|
||||
- Manually viewing source: no `Hugo X.Y.Z` in `<meta name="generator">`
|
||||
- All in-content images in `exampleSite/` posts have `loading="lazy"`
|
||||
- All external links in posts have `rel="noopener noreferrer"`
|
||||
- Pa11y or axe-core run on demo site shows zero new violations vs v0.1.0
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Focus ring on dark mode** — `var(--tsuki-accent)` must contrast against dark backgrounds. Verify with WCAG contrast checker; bump accent saturation if needed.
|
||||
- **render-link hook breaking same-doc anchor links** — `strings.HasPrefix .Destination "http"` correctly excludes `#anchor` and relative links. Test on a TOC-heavy post.
|
||||
- **Giscus race** — `send()` before observer + before iframe ready may post to nothing. Verify Giscus emits a load event we can listen for, or post on `DOMContentLoaded` + on `data-theme` mutations.
|
||||
- **Categories M1 churn** — if "surface" is chosen, post-card.html and meta.html grow; verify CSS budget.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- N5 mitigates Hugo-version-CVE-fingerprinting.
|
||||
- render-link's `safeURL` preserves XSS protection on destination.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 3 (parallel) — SEO baseline lands JSON-LD + OG + Twitter on top of the cleaned `head.html`.
|
||||
→ Phase 5 — categories surfacing decision flows here for related-posts UX shape.
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
phase: 3
|
||||
title: "SEO baseline (JSON-LD + OG + Twitter)"
|
||||
status: completed
|
||||
completed_date: 2026-05-09
|
||||
priority: P1
|
||||
effort: "0.5d"
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 3: SEO baseline
|
||||
|
||||
## Context Links
|
||||
|
||||
- Researcher: `plans/reports/researcher-260508-2306-hugo-theme-best-practices.md` — § 1.2 Gap 1, § 3.1 SEO checklist, § 9 Tier 1.1
|
||||
- Audit: `plans/reports/code-reviewer-260508-2305-tsuki-v0.1.0-audit.md` — § Documentation vs Code Match (OG image fallback already present)
|
||||
|
||||
## Overview
|
||||
|
||||
Add the SEO metadata that 2025 themes ship: JSON-LD Article schema, OpenGraph tags, Twitter Cards, structured author. tsuki currently emits basic `<title>` + `<meta description>` only. Quick win, no JS, ~600 B HTML per page.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Every post emits valid `application/ld+json` Article schema (validates against schema.org)
|
||||
- Every page emits OpenGraph tags (`og:title`, `og:type`, `og:image`, `og:url`, `og:site_name`)
|
||||
- Every page emits Twitter Card meta (`twitter:card="summary_large_image"`, `twitter:image`, `twitter:title`, `twitter:description`)
|
||||
- `og:image` resolves to `cover.image` (per-post) → `params.profile.avatar` → site logo (configurable order)
|
||||
- Author surfaces as `schema.Person` in JSON-LD
|
||||
- Canonical URL emitted on every page
|
||||
|
||||
**Non-functional**
|
||||
- Lighthouse SEO ≥ 95 on demo (currently ~90)
|
||||
- HTML increase per page < 800 B (well within budget; this is content not bundle)
|
||||
- No new JS
|
||||
|
||||
## Architecture
|
||||
|
||||
New partial `layouts/_partials/head/seo.html` consumed by `head.html`. Conditional emission based on `.IsPage` / `.Kind`. JSON-LD for `single` only; OG/Twitter on every kind.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Create**
|
||||
- `layouts/_partials/head/seo.html` — JSON-LD + OG + Twitter
|
||||
- `layouts/_partials/head/og-image.html` — image resolution helper
|
||||
|
||||
**Modify**
|
||||
- `layouts/_partials/head.html` — call `partial "head/seo.html" .`
|
||||
- `data/profile.yaml` (exampleSite) — show `twitter` + `linkedin` handle examples
|
||||
- `docs/config.md` — document new params (`params.social.twitter`, `params.og.fallbackImage`)
|
||||
- `docs/data-schemas.md` — document `cover.image` convention for OG fallback
|
||||
- `archetypes/default.md` — add `cover: { image: "" }` field (Phase 4 cross-ref)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **OG image resolution (Q4 maintainer answer)** — *maintainer chooses:* default order is per-post `cover.image` → `params.profile.avatar` → `params.og.fallbackImage` → none. Document the resolved order.
|
||||
2. **`head/og-image.html` partial** — emit absolute URL for resolved image. Use `.Page.Resources.GetMatch` for leaf-bundle image discovery.
|
||||
3. **`head/seo.html` partial — OG block**:
|
||||
```go-html-template
|
||||
<meta property="og:title" content="{{ .Title | default site.Title }}">
|
||||
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
|
||||
<meta property="og:url" content="{{ .Permalink }}">
|
||||
<meta property="og:site_name" content="{{ site.Title }}">
|
||||
{{- with .Description | default .Summary | default site.Params.description -}}
|
||||
<meta property="og:description" content="{{ . | plainify | truncate 200 }}">
|
||||
{{- end }}
|
||||
{{- $img := partial "head/og-image.html" . -}}
|
||||
{{- with $img }}<meta property="og:image" content="{{ . | absURL }}">{{ end }}
|
||||
```
|
||||
4. **`head/seo.html` partial — Twitter block**:
|
||||
```go-html-template
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ .Title | default site.Title }}">
|
||||
{{- with site.Params.social.twitter }}<meta name="twitter:site" content="@{{ . }}">{{ end }}
|
||||
{{- with .Description | default .Summary -}}<meta name="twitter:description" content="{{ . | plainify | truncate 200 }}">{{- end }}
|
||||
{{- with $img }}<meta name="twitter:image" content="{{ . | absURL }}">{{ end }}
|
||||
```
|
||||
5. **`head/seo.html` partial — JSON-LD Article (only if `.IsPage` and `.Kind == "page"`)**:
|
||||
```go-html-template
|
||||
{{- if and .IsPage (eq .Kind "page") -}}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": {{ .Title | jsonify }},
|
||||
"datePublished": {{ .Date.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
|
||||
"dateModified": {{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": {{ site.Params.author | default site.Title | jsonify }}{{- with site.Params.profile.url }},
|
||||
"url": {{ . | jsonify }}{{- end }}
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": {{ site.Title | jsonify }}
|
||||
}{{- with $img }},
|
||||
"image": {{ . | absURL | jsonify }}{{- end }}{{- with .Description | default .Summary }},
|
||||
"description": {{ . | plainify | truncate 250 | jsonify }}{{- end }}
|
||||
}
|
||||
</script>
|
||||
{{- end -}}
|
||||
```
|
||||
6. **Canonical URL** — already implicit via `<link rel="canonical">`? Verify in `head.html`; if missing, add `<link rel="canonical" href="{{ .Permalink }}">`.
|
||||
7. **Wire it** — `head.html` calls `{{ partial "head/seo.html" . }}` near other meta.
|
||||
8. **exampleSite update** — set `params.social.twitter`, `params.author`, ensure 2-3 demo posts have `cover.image`.
|
||||
9. **Validate** — run JSON-LD through https://validator.schema.org/, OG through https://www.opengraph.xyz/, Twitter via card validator. Fix any warnings.
|
||||
10. **Docs** — `docs/config.md` adds `params.social.*`, `params.og.fallbackImage` reference. `docs/data-schemas.md` documents `cover.image` for OG.
|
||||
11. **CHANGELOG** — `### Added` SEO metadata section.
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] OG image resolution order decided: `cover.image` → `image` → `params.og.fallbackImage` → `params.profile.avatar`
|
||||
- [x] `head/og-image.html` partial
|
||||
- [x] `head/seo.html` emits OG, Twitter, JSON-LD
|
||||
- [x] Canonical URL verified emitted (implicit via permalink)
|
||||
- [x] exampleSite demo posts have cover images
|
||||
- [x] schema.org validator: 0 errors on demo posts
|
||||
- [x] OpenGraph debugger: rich preview renders
|
||||
- [x] Twitter Card validator: summary_large_image displays
|
||||
- [x] Lighthouse SEO ≥ 95 on demo
|
||||
- [x] Docs updated (`docs/config.md`)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- View-source on `exampleSite/post/.../`: JSON-LD Article block present and valid
|
||||
- View-source on homepage: OG + Twitter tags present, JSON-LD absent (it's a website not article)
|
||||
- Pasting any demo post URL into LinkedIn/Twitter/Discord: rich preview with image + title + description
|
||||
- Lighthouse audit on demo: SEO score ≥ 95
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Page bloat** — JSON-LD is ~400-500 B per page; over a 1000-page site that's negligible (still static), but worth noting in CHANGELOG.
|
||||
- **`og:image` absolute URL** — Hugo's `absURL` requires correct `baseURL`. Common deployment trap. Document explicitly.
|
||||
- **JSON-LD escaping** — `jsonify` handles escaping; do NOT hand-build JSON.
|
||||
- **Schema validity** — JSON-LD Article requires `headline ≤ 110 chars`. Long Vietnamese titles may exceed; consider truncation or skip the field.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All user input flows through `jsonify` (escapes), `plainify` (strips tags), `truncate` (rune-safe). No XSS surface added.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 4 — Author UX (reading time, callouts) lands on top of cleaner head.
|
||||
→ Phase 6 — distribution prep references the new SEO posture in theme.toml description.
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
phase: 4
|
||||
title: "Author UX (reading time, callouts, archetype)"
|
||||
status: completed
|
||||
completed_date: 2026-05-09
|
||||
priority: P2
|
||||
effort: "0.5d"
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 4: Author UX
|
||||
|
||||
## Context Links
|
||||
|
||||
- Researcher: `plans/reports/researcher-260508-2306-hugo-theme-best-practices.md` — § 1.2 Gap 2, Gap 4; § 2.1 Blockquote Render Hook; § 9 Tier 1.2, 1.3; § 4.2 archetype
|
||||
- Audit: `plans/reports/code-reviewer-260508-2305-tsuki-v0.1.0-audit.md` — L2 archetype description
|
||||
|
||||
## Overview
|
||||
|
||||
Three quick wins for authors: reading time + word count byline (Hugo provides out-of-box; just template + i18n), native Markdown callouts via blockquote render hook (`> [!note]`, `> [!warning]`, `> [!caution]` per Hugo 0.150+), and a richer archetype.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Post pages show "X phút đọc" reading time and optional word count
|
||||
- `> [!note]`, `> [!warning]`, `> [!caution]`, `> [!tip]`, `> [!important]` render as styled callouts
|
||||
- Plain `>` blockquotes render unchanged
|
||||
- Archetype includes `description`, `tags`, `categories`, `cover.image` placeholder
|
||||
|
||||
**Non-functional**
|
||||
- Reading time uses `i18n` so en-translation is trivial
|
||||
- Callouts add ≤ 400 B gz CSS
|
||||
- No JS
|
||||
|
||||
## Architecture
|
||||
|
||||
Reading time = template tweak in `meta.html`. Callouts = single render hook `_markup/render-blockquote.html` matching Hugo 0.150+ alert-syntax convention. CSS for callout styles slots into `components.css` (or a new `callouts.css` if budget tight). Archetype is a single file.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Create**
|
||||
- `layouts/_markup/render-blockquote.html`
|
||||
- `assets/css/callouts.css` (optional; could live in `components.css`)
|
||||
|
||||
**Modify**
|
||||
- `layouts/_partials/meta.html` — add reading time + word count
|
||||
- `i18n/vi.yml` — add `wordCount`, ensure `readingTime` plural-aware (L4)
|
||||
- `archetypes/default.md` — add `description: ""`, `cover: { image: "" }`
|
||||
- `assets/css/components.css` (or new `callouts.css`) — callout styles
|
||||
- `assets/css/tokens.css` — add `--tsuki-callout-{note,warning,caution,tip,important}` color tokens
|
||||
- `layouts/baseof.html` or asset pipeline — include `callouts.css` in concat list
|
||||
- `docs/customization.md` — document callout syntax for content authors
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Reading time byline** — in `meta.html`, after date:
|
||||
```go-html-template
|
||||
{{- if gt .ReadingTime 0 -}}
|
||||
<span class="post-meta-reading">{{ i18n "readingTime" (dict "Count" .ReadingTime) }}</span>
|
||||
{{- end -}}
|
||||
{{- with site.Params.showWordCount }}{{ if . }}
|
||||
<span class="post-meta-words">{{ i18n "wordCount" (dict "Count" $.WordCount) }}</span>
|
||||
{{ end }}{{- end }}
|
||||
```
|
||||
2. **i18n keys** — convert `readingTime` to plural-aware form (vi has no plurals but go-i18n shape is consistent for future en):
|
||||
```yaml
|
||||
- id: readingTime
|
||||
translation: "{{ .Count }} phút đọc"
|
||||
- id: wordCount
|
||||
translation: "{{ .Count }} từ"
|
||||
```
|
||||
3. **Blockquote render hook** — create `_markup/render-blockquote.html` matching Hugo's documented alert pattern:
|
||||
```go-html-template
|
||||
{{- $type := .AttributeOrDefault "alertType" "" -}}
|
||||
{{- if $type -}}
|
||||
<blockquote class="callout callout-{{ $type | lower }}">
|
||||
<p class="callout-title">
|
||||
{{ partial "icon.html" $type }}
|
||||
{{ i18n (printf "callout%s" ($type | title)) | default ($type | title) }}
|
||||
</p>
|
||||
{{ .Text }}
|
||||
</blockquote>
|
||||
{{- else -}}
|
||||
<blockquote{{ with .Attributes }}{{ range $k, $v := . }}{{ if ne $k "alertType" }} {{ $k }}="{{ $v }}"{{ end }}{{ end }}{{ end }}>
|
||||
{{ .Text }}
|
||||
</blockquote>
|
||||
{{- end -}}
|
||||
```
|
||||
*Verify Hugo 0.146 supports `.AttributeOrDefault` on blockquote render hooks; if not, require Hugo 0.150+ (bump `min_version` in `theme.toml`).*
|
||||
4. **Callout CSS** — minimal: bordered box, accent stripe, title color via custom property. ≈ 350 B gz.
|
||||
```css
|
||||
.callout { border-left: 3px solid var(--tsuki-callout); padding: .75rem 1rem; margin: 1rem 0; background: var(--tsuki-callout-bg); border-radius: .25rem; }
|
||||
.callout-title { font-weight: 600; margin: 0 0 .5rem; display: flex; gap: .375rem; }
|
||||
.callout-note { --tsuki-callout: #2563eb; --tsuki-callout-bg: #2563eb12; }
|
||||
.callout-warning { --tsuki-callout: #d97706; --tsuki-callout-bg: #d9770612; }
|
||||
.callout-caution { --tsuki-callout: #dc2626; --tsuki-callout-bg: #dc262612; }
|
||||
.callout-tip { --tsuki-callout: #16a34a; --tsuki-callout-bg: #16a34a12; }
|
||||
.callout-important { --tsuki-callout: #7c3aed; --tsuki-callout-bg: #7c3aed12; }
|
||||
```
|
||||
5. **i18n callout titles** — `vi.yml`:
|
||||
```yaml
|
||||
- id: calloutNote
|
||||
translation: "Ghi chú"
|
||||
- id: calloutWarning
|
||||
translation: "Cảnh báo"
|
||||
- id: calloutCaution
|
||||
translation: "Thận trọng"
|
||||
- id: calloutTip
|
||||
translation: "Mẹo"
|
||||
- id: calloutImportant
|
||||
translation: "Quan trọng"
|
||||
```
|
||||
6. **Archetype** — `archetypes/default.md`:
|
||||
```md
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
description: ""
|
||||
tags: []
|
||||
categories: []
|
||||
cover:
|
||||
image: ""
|
||||
---
|
||||
```
|
||||
7. **exampleSite demo** — add a post showcasing all 5 callout types so users can copy-paste.
|
||||
8. **Docs** — `docs/customization.md` adds a "Callouts" section with the `> [!note]` syntax.
|
||||
9. **Bundle size check** — gzip CSS bundle. Should land around ~4.0-4.1 KB; acceptable. If over 4200 B, split callouts into a separate optional bundle loaded only when `> [!...]` is used (Hugo doesn't expose this gate cheaply; usually accept the 200 B).
|
||||
10. **CHANGELOG** — `### Added` reading time, native callouts.
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] Reading time + word count byline emitted
|
||||
- [x] `readingTime`, `wordCount` i18n keys plural-aware
|
||||
- [x] `_markup/render-blockquote.html` created
|
||||
- [x] Hugo version compatibility verified: `min_version: 0.146.0`
|
||||
- [x] Callout CSS added (5 types) in `assets/css/callouts.css`
|
||||
- [x] Callout title i18n keys (5) in `i18n/vi.yml`
|
||||
- [x] Archetype expanded with `description`, `cover.image`
|
||||
- [x] Demo post with all 5 callouts in exampleSite
|
||||
- [x] CSS bundle ≤ 4200 B gz
|
||||
- [x] `docs/customization.md` callout section documented
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- A post in exampleSite with `> [!note]` content renders as a styled note callout, not a plain blockquote
|
||||
- Reading time displays on every post with content
|
||||
- `hugo new post/foo.md` produces an archetype with all expected frontmatter fields
|
||||
- Callout demo post renders correctly in light + dark modes with sufficient contrast
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Hugo version compat** — blockquote render hook with `alertType` attribute requires Hugo 0.140+. Audit confirms `min_version: 0.146.0` already, ✓.
|
||||
- **CSS budget cliff** — ~400 B addition. May force splitting `callouts.css` out or removing dead bytes elsewhere. Run gzip test after merge.
|
||||
- **Reading time accuracy** — Hugo uses 213 wpm by default; configurable via `wordsPerMinute` in `hugo.yaml`. Document in `docs/config.md` if Vietnamese reading speed differs (research suggests vi readers ~250 wpm).
|
||||
- **Callout colors in dark mode** — colors above are picked for light mode; verify contrast in dark mode and add `[data-theme="dark"] .callout-{type} { … }` overrides if needed.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Render hooks emit user content via `.Text` which Hugo treats as already-parsed safe HTML. Goldmark parses callout body the same as any blockquote — no new XSS surface.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 5 — related posts (the reading-time byline groundwork helps related-post cards display consistently).
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
phase: 5
|
||||
title: "Discovery features (related posts + categories)"
|
||||
status: completed
|
||||
completed_date: 2026-05-09
|
||||
priority: P2
|
||||
effort: "0.5d"
|
||||
dependencies: [2, 4]
|
||||
---
|
||||
|
||||
# Phase 5: Discovery features
|
||||
|
||||
## Context Links
|
||||
|
||||
- Researcher: `plans/reports/researcher-260508-2306-hugo-theme-best-practices.md` — § 1.2 Gap 5, § 9 Tier 2.4
|
||||
- Audit: `plans/reports/code-reviewer-260508-2305-tsuki-v0.1.0-audit.md` — M1 categories visibility
|
||||
|
||||
## Overview
|
||||
|
||||
Add related-posts section to single post layout using Hugo's built-in `.Site.RegularPages.Related`. Follow through on Phase 2's categories visibility decision: if "surface", finalize the categories pill UX; if "routing-only", document and remove from theme defaults.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Single post page shows up to 5 related posts based on shared tags + categories
|
||||
- Related-posts section hidden when no relations found
|
||||
- Related-posts use `post-card.html` partial (shared with home recent-posts) — no new card variant
|
||||
- Categories visibility decision from Phase 2 fully landed (UI exists OR docs explain absence)
|
||||
|
||||
**Non-functional**
|
||||
- Hugo `.Related` config tuned for vi-language post titles (no language-specific issues, but confirm)
|
||||
- No JS
|
||||
- Related-posts CSS reuses existing post-card styles; budget impact ~50 B for layout adjustments
|
||||
|
||||
## Architecture
|
||||
|
||||
Hugo built-in: configure `related` in `hugo.yaml` (theme defaults ship a sensible weight matrix). Single new partial `_partials/related-posts.html` consumed by `single.html`. Reuses `post-card.html`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Create**
|
||||
- `layouts/_partials/related-posts.html`
|
||||
|
||||
**Modify**
|
||||
- `layouts/single.html` — call `partial "related-posts.html" .` after content + comments
|
||||
- `hugo.yaml` (theme defaults) — `related: { ... }` section
|
||||
- `i18n/vi.yml` — add `relatedPosts` key
|
||||
- `assets/css/components.css` — minor adjustments if needed
|
||||
- `docs/config.md` — document `related` config + how to override
|
||||
- `layouts/_partials/meta.html` — finalize categories pill (carry from Phase 2)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Hugo `related` config** — add to `hugo.yaml` (theme defaults, mirror in `exampleSite/hugo.yaml`):
|
||||
```yaml
|
||||
related:
|
||||
threshold: 80
|
||||
includeNewer: true
|
||||
toLower: true
|
||||
indices:
|
||||
- name: tags
|
||||
weight: 100
|
||||
- name: categories
|
||||
weight: 60
|
||||
- name: date
|
||||
weight: 10
|
||||
```
|
||||
*Note Hugo's no-deep-merge rule applies — document that consumers must replicate this in their `hugo.yaml`.*
|
||||
2. **`_partials/related-posts.html`**:
|
||||
```go-html-template
|
||||
{{- $related := first 5 (site.RegularPages.Related .) -}}
|
||||
{{- with $related -}}
|
||||
<section class="related-posts" aria-labelledby="related-heading">
|
||||
<h2 id="related-heading">{{ i18n "relatedPosts" }}</h2>
|
||||
<div class="related-grid">
|
||||
{{- range . -}}
|
||||
{{ partial "post-card.html" . }}
|
||||
{{- end -}}
|
||||
</div>
|
||||
</section>
|
||||
{{- end -}}
|
||||
```
|
||||
3. **single.html** — insert call after main content, before comments:
|
||||
```go-html-template
|
||||
{{ partial "related-posts.html" . }}
|
||||
```
|
||||
4. **i18n** — add `relatedPosts: "Bài viết liên quan"` in `vi.yml`.
|
||||
5. **CSS** — append minimal grid layout to `components.css`:
|
||||
```css
|
||||
.related-posts { margin: 3rem 0 1rem; padding-top: 1.5rem; border-top: 1px solid var(--tsuki-border); }
|
||||
.related-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
|
||||
```
|
||||
6. **exampleSite verification** — ensure 5 demo posts share tags/categories so Related actually surfaces results.
|
||||
7. **Categories follow-through (M1 from Phase 2)** — *if Q2 was "surface":* confirm `meta.html` shows categories pill and Phase 5 Related uses both indices. *If "routing-only":* drop `categories` from `related.indices` (only `tags` remains), remove the unused taxonomy from theme defaults.
|
||||
8. **Docs** — `docs/config.md` adds the `related:` block as required consumer config (alongside `taxonomies`, `permalinks`).
|
||||
9. **CSS budget check** — gzip CSS bundle. Likely now ~4.1-4.2 KB. May need to drop dead bytes elsewhere or split.
|
||||
10. **CHANGELOG** — `### Added` related posts.
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] `related` config in theme defaults + exampleSite (tags/categories/date weights)
|
||||
- [x] `_partials/related-posts.html` created
|
||||
- [x] `single.html` calls related partial
|
||||
- [x] `relatedPosts` i18n key (`"Bài viết liên quan"`)
|
||||
- [x] Related-grid CSS responsive (auto-fit, minmax)
|
||||
- [x] Categories M1 follow-through: routing-only (per Phase 2 decision)
|
||||
- [x] exampleSite demo posts share tags for relation display
|
||||
- [x] CSS bundle ≤ 4200 B gz
|
||||
- [x] `docs/config.md` documents `related` config + threshold tuning
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Visiting a demo post shows 2-5 related posts at the bottom, before comments
|
||||
- A post with no shared tags/categories shows no Related section (no empty heading orphan)
|
||||
- Related-grid wraps responsively on mobile (1 col), tablet (2 col), desktop (3+)
|
||||
- Lighthouse perf score doesn't drop more than 1 point vs Phase 4 baseline
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Hugo `Related` performance** — O(n²) over `.Pages` for large sites. Hugo caches; for 1000+ posts a build could slow. Document threshold tuning in `docs/config.md`.
|
||||
- **Empty relations on small sites** — if exampleSite has 5 posts and threshold is 80, may produce zero relations. Tune for demo or seed posts with shared `keywords` frontmatter.
|
||||
- **CSS budget** — heading + grid adds ~150 B gz. Combined with Phase 4's callouts, likely the budget cliff arrives here. Be ready to drop the lowest-value rule (e.g. unused `--tsuki-vt-name` from M4 if not removed earlier).
|
||||
- **Related includes drafts** — verify Hugo's `Related` respects `Draft = false` in production builds. (It does, since `RegularPages` excludes drafts.)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- No new attack surface; all data flows through Hugo's already-validated taxonomy graph.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 6 — distribution prep finalizes theme.toml + module mounts now that feature surface is stable.
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
phase: 6
|
||||
title: "Distribution prep (theme.toml + gallery)"
|
||||
status: completed
|
||||
completed_date: 2026-05-09
|
||||
priority: P2
|
||||
effort: "0.5d"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 6: Distribution prep
|
||||
|
||||
## Context Links
|
||||
|
||||
- Researcher: `plans/reports/researcher-260508-2306-hugo-theme-best-practices.md` — § 5 Theme Distribution Standards
|
||||
- Audit: `plans/reports/code-reviewer-260508-2305-tsuki-v0.1.0-audit.md` — L1 theme.toml original block, L8 screenshot dims, L9 Pagefind/npm note, N3 module mounts
|
||||
|
||||
## Overview
|
||||
|
||||
Make the theme installable from the Hugo theme gallery and from Hugo Modules cleanly. Verify image asset dims, clean theme.toml, declare module mounts, document Pagefind behavior under Hugo Modules.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- `theme.toml` validates against gohugoio/hugoThemes registry schema
|
||||
- `images/screenshot.png` = 1500×1000, `images/tn.png` = 900×600 (verify, not just trust commit message)
|
||||
- `[module]` mounts declared so theme works under Hugo Module consumption with custom `assetDir`/`layoutDir` overrides
|
||||
- Documentation explains Pagefind doesn't run automatically under Hugo Module consumption (npm not part of module flow)
|
||||
|
||||
**Non-functional**
|
||||
- Theme passes the "Hugo Basic Example" test (clean install, hugo server, no errors)
|
||||
- Submission PR to `gohugoio/hugoThemes` is ready (branch + tag)
|
||||
|
||||
## Architecture
|
||||
|
||||
Pure-config phase. No layout or asset changes.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Modify**
|
||||
- `theme.toml` — drop empty `[original]`, verify all required fields, finalize tags + features list
|
||||
- `hugo.yaml` (theme defaults) — add `module:` mounts block
|
||||
- `images/screenshot.png` — verify or regenerate at 1500×1000
|
||||
- `images/tn.png` — verify or regenerate at 900×600
|
||||
- `docs/installation.md` (new) — split installation guide from README; document submodule + Hugo Module + Pagefind quirks
|
||||
- `README.md` — link to new installation guide
|
||||
- `package.json` — clarify "Pagefind built in CI; consumer sites need their own pagefind step or accept search-disabled"
|
||||
|
||||
**Create**
|
||||
- `docs/installation.md`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **theme.toml audit** — current state has empty `[original]` block (L1). For an original theme, drop it entirely. Final shape:
|
||||
```toml
|
||||
name = "tsuki"
|
||||
license = "Apache-2.0"
|
||||
licenselink = "https://github.com/tiennm99/tsuki/blob/main/LICENSE"
|
||||
description = "Vietnamese-first Hugo blog + portfolio. Dark mode, Pagefind search, Giscus comments, View Transitions. Zero build step."
|
||||
homepage = "https://github.com/tiennm99/tsuki"
|
||||
demosite = "https://tiennm99.github.io/tsuki"
|
||||
tags = ["blog", "portfolio", "dark-mode", "search", "vietnamese", "minimal", "view-transitions", "responsive"]
|
||||
features = ["responsive", "dark-mode", "search", "comments", "i18n", "pagination"]
|
||||
min_version = "0.146.0"
|
||||
|
||||
[author]
|
||||
name = "tiennm99"
|
||||
homepage = "https://github.com/tiennm99"
|
||||
```
|
||||
2. **Image dims verification** — `identify -format "%wx%h" images/screenshot.png` and confirm. Same for `tn.png`. Regenerate from a Phase 3-deployed demo if dims wrong.
|
||||
3. **Module mounts (N3)** — add to theme `hugo.yaml`:
|
||||
```yaml
|
||||
module:
|
||||
mounts:
|
||||
- source: layouts
|
||||
target: layouts
|
||||
- source: assets
|
||||
target: assets
|
||||
- source: i18n
|
||||
target: i18n
|
||||
- source: data
|
||||
target: data
|
||||
- source: archetypes
|
||||
target: archetypes
|
||||
- source: static
|
||||
target: static
|
||||
```
|
||||
4. **`docs/installation.md`** — covers:
|
||||
- Submodule install
|
||||
- Hugo Module install
|
||||
- **Pagefind under Hugo Module:** consumers must `npm install pagefind` and add `npx pagefind --site public` to their build, OR accept search-disabled. tsuki's `package.json` covers it for submodule users only.
|
||||
- GitHub Pages workflow snippet (lift from `.github/workflows/pages.yml`)
|
||||
- Required site `hugo.yaml` keys (already in README; reference)
|
||||
5. **README slim-down** — remove duplicated install snippets; link to `docs/installation.md`.
|
||||
6. **Hugo Basic Example test** — clone `gohugoio/hugoBasicExample`, install tsuki as module + as submodule, run `hugo server`, verify clean.
|
||||
7. **Submission preparation** — fork `gohugoio/hugoThemes`, add `tsuki` submodule pointing at the repo, follow CONTRIBUTING.md exactly. Submit PR after `v0.2.0` tag.
|
||||
8. **CHANGELOG** — `### Added` module mounts. `### Changed` theme.toml cleanup.
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] `theme.toml` `[original]` removed
|
||||
- [x] `theme.toml` `tags`, `features`, `min_version` finalized
|
||||
- [x] `images/screenshot.png` 1500×1000 verified + updated
|
||||
- [x] `images/tn.png` 900×600 verified + updated
|
||||
- [x] `module.mounts` declared in theme `hugo.yaml`
|
||||
- [x] `docs/installation.md` written (submodule + Hugo Module + Pagefind)
|
||||
- [x] README links to installation guide; install duplication removed
|
||||
- [x] Hugo Basic Example smoke-tested with submodule install
|
||||
- [x] Hugo Basic Example smoke-tested with Hugo Module install
|
||||
- [x] `gohugoio/hugoThemes` PR ready (waiting for v0.2.0 tag)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- `theme.toml` parses cleanly with no Hugo warnings
|
||||
- Both screenshot images match registry size requirements
|
||||
- `cd /tmp && git clone hugoBasicExample && hugo mod init x.com/test && echo "module: { imports: [{ path: 'github.com/tiennm99/tsuki' }] }" >> hugo.yaml && hugo server` produces a clean build
|
||||
- gohugoio/hugoThemes registry submission PR opens green CI
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Image regen mismatch** — current commit says 1500×1000 + 900×600 added, but verify with `identify`; commit messages aren't ground truth.
|
||||
- **Module mounts overriding consumer mounts** — Hugo merges; consumer mounts win. Low risk.
|
||||
- **Pagefind behavior under Module** — biggest gotcha. Misleading if not documented; users will report "search broken." Mitigate with prominent install-doc note.
|
||||
- **`min_version: 0.146.0` vs Phase 4 callouts requiring 0.150+** — if Phase 4 lands, bump min_version to 0.150.0. Cross-ref Phase 4 step 3.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- None new.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 7 — CI hardening can land in parallel and improves the "tested against hugoBasicExample" claim.
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
phase: 7
|
||||
title: "CI hardening (htmltest + Lighthouse + budget)"
|
||||
status: completed
|
||||
completed_date: 2026-05-09
|
||||
priority: P3
|
||||
effort: "0.5d"
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 7: CI hardening
|
||||
|
||||
## Context Links
|
||||
|
||||
- Researcher: `plans/reports/researcher-260508-2306-hugo-theme-best-practices.md` — § 7 CI/Testing best practices
|
||||
- Audit: `plans/reports/code-reviewer-260508-2305-tsuki-v0.1.0-audit.md` — H3 CSS budget assertion (cross-ref Phase 1)
|
||||
|
||||
## Overview
|
||||
|
||||
Add automated checks to `.github/workflows/pages.yml`: htmltest for broken-link/HTML-validity catch, optional Lighthouse for perf/SEO regression alerting, CSS bundle gz size assertion (carries from Phase 1). Catches doc/code drift before users do.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- CI fails on broken internal links in exampleSite
|
||||
- CI fails if CSS bundle > 4200 B gz (already done in Phase 1; ensure consistent)
|
||||
- CI emits Lighthouse SEO + perf scores (informational; failing thresholds optional)
|
||||
|
||||
**Non-functional**
|
||||
- CI build time stays under 3 minutes total
|
||||
- No external paid services required
|
||||
|
||||
## Architecture
|
||||
|
||||
GitHub Actions job additions; no source changes. htmltest runs against built `exampleSite/public/`. Lighthouse runs against the deployed Pages URL post-deploy (or against a local `hugo server` for PR-time check).
|
||||
|
||||
## Related Code Files
|
||||
|
||||
**Modify**
|
||||
- `.github/workflows/pages.yml` — add jobs/steps for htmltest, css-budget, optional lighthouse
|
||||
|
||||
**Create (optional)**
|
||||
- `.htmltest.yml` — htmltest config
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **htmltest setup** — add to `pages.yml` after Hugo + Pagefind build:
|
||||
```yaml
|
||||
- name: htmltest
|
||||
uses: wjdp/htmltest-action@master
|
||||
with:
|
||||
config: .htmltest.yml
|
||||
path: exampleSite/public
|
||||
```
|
||||
`.htmltest.yml`:
|
||||
```yaml
|
||||
DirectoryPath: exampleSite/public
|
||||
IgnoreURLs:
|
||||
- "^https://giscus.app"
|
||||
- "^https://github.com/tiennm99"
|
||||
CheckExternal: false
|
||||
IgnoreInternalEmptyHash: true
|
||||
```
|
||||
2. **CSS budget assertion** — already added in Phase 1; verify still present and threshold matches (`4200`).
|
||||
3. **Lighthouse CI (optional)** — add an experimental job that runs on `push` to main only (not PRs, to avoid flake):
|
||||
```yaml
|
||||
lighthouse:
|
||||
needs: deploy
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: treosh/lighthouse-ci-action@v11
|
||||
with:
|
||||
urls: |
|
||||
https://tiennm99.github.io/tsuki/
|
||||
https://tiennm99.github.io/tsuki/post/example-1/
|
||||
uploadArtifacts: true
|
||||
```
|
||||
Thresholds soft (informational only); fail-on-threshold disabled until baselines stabilize.
|
||||
4. **Status badge** — add `htmltest` badge to README alongside existing `build` and `license`.
|
||||
5. **Docs** — add a `docs/ci.md` (or section in existing CI doc) explaining each check and how to run locally:
|
||||
- `npx htmltest -c .htmltest.yml exampleSite/public`
|
||||
- `du -b exampleSite/public/css/tsuki.bundle.*.css | xargs -I{} gzip -9 -c {} | wc -c`
|
||||
6. **CHANGELOG** — `### Added` CI checks (this is dev-facing; small note).
|
||||
|
||||
## Todo
|
||||
|
||||
- [x] htmltest step added; `.htmltest.yml` configured (internal links, no external)
|
||||
- [x] CSS budget assertion confirmed in CI (Phase 1 carries forward)
|
||||
- [x] Lighthouse CI job added (optional; `continue-on-error: true`)
|
||||
- [x] htmltest badge in README
|
||||
- [x] `docs/ci.md` section added (check instructions)
|
||||
- [x] First CI run is green (htmltest passes, no false positives)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- A PR that breaks an internal link in exampleSite/content fails CI before merge
|
||||
- A PR that grows CSS bundle past 4200 B gz fails CI before merge
|
||||
- README shows three green badges: build, htmltest, license
|
||||
- Lighthouse SEO + perf scores tracked on main pushes
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **htmltest false positives** — anchor links to `#section-id` resolve only if `id` exists at build time. If render hooks change anchor scheme, htmltest breaks. Mitigate by `IgnoreInternalEmptyHash: true` and explicit ignore patterns.
|
||||
- **Lighthouse flake** — first-byte time, 3rd-party (Giscus, Pagefind UI) affect scores. Soft-fail (`continue-on-error: true`) avoids gating merges on environmental noise.
|
||||
- **CI minutes** — htmltest fast (~5s); Lighthouse adds ~30s. Total well within free-tier.
|
||||
- **htmltest external link checking** — disabled (`CheckExternal: false`) to avoid network flake. Internal link graph is the value.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- None new. CI runs on stock GitHub-hosted runner; no secrets touched.
|
||||
|
||||
## Next Steps
|
||||
|
||||
→ Phase 7 closes v0.2.0 dev cycle. After landing: tag `v0.2.0`, write CHANGELOG release notes, submit theme to gohugoio/hugoThemes from Phase 6.
|
||||
→ Future (v0.3.0+): consider deferred items (image lightbox, multi-author) only if user demand emerges.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: tsuki v0.2.0 roadmap — audit fixes, SEO, UX polish, distribution
|
||||
status: completed
|
||||
created: 2026-05-08
|
||||
completed_date: 2026-05-09
|
||||
target: v0.2.0 (with v0.1.1 patch milestone in Phase 1)
|
||||
source_reports:
|
||||
- plans/reports/code-reviewer-260508-2305-tsuki-v0.1.0-audit.md
|
||||
- plans/reports/researcher-260508-2306-hugo-theme-best-practices.md
|
||||
---
|
||||
|
||||
# tsuki v0.2.0 Roadmap
|
||||
|
||||
Improve the v0.1.0 theme along two axes: close audit gaps (TOC config dead, missing i18n, /tags hardcode, search route gate, home pagination 404s, a11y polish) and adopt the lightweight subset of 2025 best practices (JSON-LD + OG, reading time, native blockquote callouts, related posts). Stay inside the existing budget (CSS ≤ 4 KB gz, JS ≤ 1 KB gz, zero build step).
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Priority | Status | Notes |
|
||||
|---|---|---|---|---|
|
||||
| 1 | [v0.1.1 bugs & doc drift](phase-01-v0.1.1-bugs-and-doc-drift.md) | P1 | completed | Ship as `v0.1.1` patch — fixes only, no new features |
|
||||
| 2 | [Accessibility & polish](phase-02-accessibility-and-polish.md) | P1 | completed | Skip-link, focus rings, render-link/image hooks |
|
||||
| 3 | [SEO baseline](phase-03-seo-baseline.md) | P1 | completed | JSON-LD Article + OG + Twitter |
|
||||
| 4 | [Author UX](phase-04-author-ux.md) | P2 | completed | Reading time, native callouts, archetype |
|
||||
| 5 | [Discovery features](phase-05-discovery-features.md) | P2 | completed | Related posts; categories follow-through from Phase 2 |
|
||||
| 6 | [Distribution](phase-06-distribution-prep.md) | P2 | completed | theme.toml, module mounts, gallery submission |
|
||||
| 7 | [CI hardening](phase-07-ci-hardening.md) | P3 | completed | htmltest, optional Lighthouse, budget assert (Phase 1 cross-ref) |
|
||||
|
||||
## Sequencing
|
||||
|
||||
- Phase 1 ships independently as `v0.1.1`.
|
||||
- Phases 2–4 can run in parallel after Phase 1 (no file conflicts).
|
||||
- Phase 5 depends on Phase 2's categories visibility decision.
|
||||
- Phase 6 + 7 are independent; can land any time.
|
||||
- Target `v0.2.0` = Phases 1–6 complete. Phase 7 may slip to `v0.2.1`.
|
||||
|
||||
## Deferred (out of scope, do not add to core)
|
||||
|
||||
Per CHANGELOG and researcher Tier 3:
|
||||
- KaTeX math, Mermaid diagrams, image lightbox/gallery
|
||||
- Multi-author support, multilingual (en) i18n
|
||||
- Self-hosted woff2, tag cloud widget
|
||||
|
||||
These remain opt-in extensions; consumers add them per-site if needed.
|
||||
|
||||
## Outcome
|
||||
|
||||
**v0.2.0 delivered**: All 7 phases complete. v0.1.1 (Phases 1–2 subset) shipped as a patch release fixing TOC config, tag URLs, search route gating, home pagination, clipboard fallback, i18n keys, CSS budget CI assertion, and generator meta. Phases 2–7 layered accessibility (skip-link, focus rings, render hooks), SEO (JSON-LD Article, OG/Twitter metadata), author UX (reading time, native callouts, expanded archetype), discovery (related posts), distribution (theme.toml, module mounts, Pagefind docs), and CI hardening (htmltest, Lighthouse, CSS/budget checks). Theme ready for Hugo theme gallery submission. See CHANGELOG [Unreleased] for the full feature list.
|
||||
|
||||
## Maintainer decisions to surface (block specific phase steps)
|
||||
|
||||
1. **`unsafe: true` Goldmark** — required for footnote/details/raw HTML in posts, or vestigial? If vestigial, drop it (kills C2 XSS surface entirely). Affects Phase 1.
|
||||
2. **Categories visibility** — surface as pills in `meta.html` or stay routing-only? Affects Phase 2 + Phase 5.
|
||||
3. **Audience** — solo bloggers vs teams. If teams ever a target, multi-author moves out of "deferred." Affects long-term roadmap, not v0.2.0.
|
||||
4. **OG image strategy** — auto from `cover.image` (requires per-post cover convention) vs static site logo fallback. Affects Phase 3.
|
||||
|
||||
## Success criteria (v0.2.0)
|
||||
|
||||
- All audit Critical + High items resolved or explicitly waived with rationale
|
||||
- Lighthouse SEO ≥ 95 on demo site (currently ~90)
|
||||
- CSS bundle ≤ 4 KB gz asserted in CI
|
||||
- Theme accepted to themes.gohugo.io gallery
|
||||
- CHANGELOG entries for every behavior change
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
1. Is the homepage ever paginated, or always portfolio-shaped? Drives Phase 1 H4 fix shape.
|
||||
2. Does `params.toc.enable: false` need to also strip `toc.css` from the bundle, or is dead-byte (~600 B gz) acceptable?
|
||||
3. Should Pagefind UI CSS be added to SRI or remain third-party uncontrolled? (M6)
|
||||
4. What's the lowest-supported browser baseline? README says "modern evergreen" — pin a version (Chrome 100? Safari 16?) so audit decisions land consistently.
|
||||
5. Does tsuki need an `i18n/en.yml` skeleton even though defaults are vi-first, to make consumer sites trivially translatable?
|
||||
@@ -0,0 +1,506 @@
|
||||
# tsuki v0.1.0 Production-Readiness Audit
|
||||
|
||||
Date: 2026-05-08
|
||||
Scope: full theme — `layouts/`, `assets/`, `i18n/`, `data/`, `exampleSite/`, `hugo.yaml`, `theme.toml`, `package.json`, `.github/`, `docs/`
|
||||
Branch: main @ d88f18d
|
||||
|
||||
---
|
||||
|
||||
## Overall Assessment
|
||||
|
||||
Solid v0.1.0. Code is small, idiomatic Hugo 0.146+ (`_partials`, `_markup`), minimal JS, sane CSS tokens. Asset pipeline (`Concat | minify | fingerprint` with SRI) is well-formed. Vietnamese-first claims mostly hold. A handful of bugs and ergonomic gaps exist; none are show-stoppers but several are user-visible regressions vs documented behavior.
|
||||
|
||||
**Top concerns**
|
||||
- **Documentation drift**: docs claim `params.toc.minWordCount` and `params.toc.enable` gate TOC. Code hardcodes 400 and ignores `enable`.
|
||||
- **`unsafe: true` Goldmark** is documented as required. With `markdownify` of `profile.bio`, raw HTML in `data/profile.yaml` executes unescaped.
|
||||
- **Tag URLs hardcoded** to `/tags/...` — breaks if site renames the taxonomy.
|
||||
- **Several i18n keys referenced but missing** from `vi.yml` (`comments`, `month`, `pageNotFound`, `backHome`, `next/prev page` referenced but only `prev`/`next` exist; the key is also referenced as `previousPage`/`nextPage` in `vi.yml`).
|
||||
- **CSS budget** claim "≤ 4 KB gz" — concatenated source is 4296 B gzipped before minify, so post-minify will likely fit, but tight; needs CI assertion.
|
||||
|
||||
---
|
||||
|
||||
## Critical
|
||||
|
||||
### C1 — Theme TOC defaults config is dead code
|
||||
**File:** `hugo.yaml:34-36`, `layouts/single.html:28`, `layouts/_partials/footer.html:19`, `docs/config.md:46-48`
|
||||
|
||||
Theme defaults `params.toc.enable: true` and `params.toc.minWordCount: 400`. Docs say the latter "gates render".
|
||||
Code uses `(gt .WordCount 400) (ne .Params.toc false)` — literal 400, no read of `site.Params.toc.minWordCount`, and `enable` is not consulted at all.
|
||||
|
||||
**Impact:** Setting `params.toc.enable: false` site-wide does nothing. Setting `params.toc.minWordCount: 800` does nothing. Hugo's "no deep-merge" rule for theme nested config is unrelated — these are read in templates at runtime.
|
||||
|
||||
**Fix:**
|
||||
```go-html-template
|
||||
{{- $tocCfg := site.Params.toc | default dict -}}
|
||||
{{- $tocEnabled := $tocCfg.enable | default true -}}
|
||||
{{- $tocMin := $tocCfg.minWordCount | default 400 -}}
|
||||
{{- if and $tocEnabled (gt .WordCount $tocMin) (ne .Params.toc false) }}
|
||||
{{ partial "toc.html" . }}
|
||||
{{- end }}
|
||||
```
|
||||
Apply same change in `layouts/_partials/footer.html:19` for the JS gate.
|
||||
|
||||
---
|
||||
|
||||
### C2 — `unsafe: true` + `markdownify` of profile.bio = stored XSS surface
|
||||
**File:** `layouts/_partials/home/hero.html:15`
|
||||
|
||||
```go-html-template
|
||||
<div class="home-hero-bio">{{ . | markdownify }}</div>
|
||||
```
|
||||
|
||||
With `markup.goldmark.renderer.unsafe: true` (required by README/docs), any HTML in `data/profile.yaml: bio` renders verbatim — including `<script>`. For a single-author personal theme this is "I'm hurting myself" territory, but tsuki ships as a theme others adopt. A user who copies a tagline with `<img src=x onerror=...>` from a Stack-themed site (where `markdownify` was sandboxed differently) gets a script execution.
|
||||
|
||||
**Impact:** If any consumer accepts profile data from a less-trusted source (CMS, multi-author, generated), this is stored XSS.
|
||||
|
||||
**Recommendations** (pick one):
|
||||
- Document explicitly that `data/profile.yaml: bio` is trusted-author input, never user-submitted, and HTML executes.
|
||||
- Or sanitize: render bio with `markdownify` then `safeHTML` only after passing through a strict policy — Hugo has no built-in HTML sanitizer, so the practical mitigation is tight docs.
|
||||
- Or hard-strip with a regex pre-pass before `markdownify` (loses code blocks).
|
||||
|
||||
At minimum: add a line to `docs/data-schemas.md` next to `bio` warning that HTML is rendered.
|
||||
|
||||
---
|
||||
|
||||
### C3 — Hardcoded `/tags/...` URLs assume taxonomy name
|
||||
**File:** `layouts/_partials/meta.html:16`, `layouts/single.html:21`
|
||||
|
||||
```go-html-template
|
||||
<a href="{{ printf "/tags/%s/" (urlize $tag) | relURL }}">#{{ $tag }}</a>
|
||||
```
|
||||
|
||||
Theme defaults declare `taxonomies: { category: categories, tag: tags }`. Hugo does not deep-merge from theme. If consumer omits or renames (e.g., `tag: tag`), this template emits broken `/tags/...` links pointing to 404s.
|
||||
|
||||
**Fix:** Use `Page.GetTerms` so URLs resolve through Hugo's taxonomy graph:
|
||||
```go-html-template
|
||||
{{- with .GetTerms "tags" }}
|
||||
<span class="post-tags">
|
||||
{{- range $i, $term := . -}}
|
||||
{{- if $i }}, {{ end -}}
|
||||
<a href="{{ $term.RelPermalink }}">#{{ $term.LinkTitle }}</a>
|
||||
{{- end -}}
|
||||
</span>
|
||||
{{- end }}
|
||||
```
|
||||
This also handles vi-titled tags correctly.
|
||||
|
||||
---
|
||||
|
||||
## High
|
||||
|
||||
### H1 — Missing i18n keys referenced in templates
|
||||
**Files:** `i18n/vi.yml`, multiple consumers
|
||||
|
||||
Referenced but absent from `vi.yml`:
|
||||
| key | referenced in |
|
||||
|---|---|
|
||||
| `comments` | `_partials/comments.html:5` |
|
||||
| `month` (with `Number` arg) | `_partials/archive-group.html:6` |
|
||||
| `pageNotFound` | `404.html:5` |
|
||||
| `backHome` | `404.html:6` |
|
||||
| `featuredProjects` | `_partials/home/projects.html:4` |
|
||||
| `viewAll` | `_partials/home/recent-posts.html:17` |
|
||||
| `searchSuggestion` | `search/list.html:38` |
|
||||
| `altSearch` | `search/list.html:37` |
|
||||
|
||||
All have `| default "..."` fallbacks so site builds. But CHANGELOG claims "all UI strings" in `i18n/vi.yml`.
|
||||
|
||||
**Fix:** add the 8 missing keys. For `month`, return a localized name from a numeric arg (`{{ .Number }}` → `Tháng 8`). Current call `{{ i18n "month" (dict "Number" .Key) }}` passes a string `.Key` (Hugo `GroupByDate "1"` returns "1".."12"), so the template just needs:
|
||||
```yaml
|
||||
- id: month
|
||||
translation: "Tháng {{ .Number }}"
|
||||
```
|
||||
|
||||
Also: `vi.yml` defines `previousPage`/`nextPage` that no template uses (`pagination.html` uses `prev`/`next`). Either delete the unused keys or switch pagination to the more descriptive ones.
|
||||
|
||||
---
|
||||
|
||||
### H2 — `params.search.enable: false` only hides header button, route still builds
|
||||
**Files:** `layouts/_partials/header.html:7`, `layouts/search/list.html` (no gate), `docs/customization.md:111`
|
||||
|
||||
`docs/customization.md` says `params.search.enable: false` "removes search button + /search/ route." The header gate exists (good); the route gate does not. `search/list.html` always renders if `content/search/_index.md` exists. CI also unconditionally runs `npx pagefind --site exampleSite/public`.
|
||||
|
||||
**Fix:** wrap `search/list.html` body in `{{- if site.Params.search.enable | default true -}}` and emit a `<p>{{ i18n "searchDisabled" }}</p>` fallback, or document that disabling `search.enable` only affects the header button and consumers should also remove `content/search/_index.md`.
|
||||
|
||||
---
|
||||
|
||||
### H3 — Concatenated CSS budget is on the edge
|
||||
**Files:** `assets/css/*.css`
|
||||
|
||||
Raw concat → gzip = 4296 B (over the 4 KB claim). Post-`minify` will save ~10–15% and bring it under, but `ResourceMinifier` minify is not lossless to gzip ratios; result could be 3.5–3.9 KB gz. Without a CI assertion this can silently regress.
|
||||
|
||||
**Recommendation:** Add a CI step that fails if `du -b public/css/*.bundle.*.css | head -1 | awk '{...}' && gzip -c | wc -c` exceeds 4096:
|
||||
```yaml
|
||||
- name: CSS budget assertion
|
||||
run: |
|
||||
css=$(find exampleSite/public -name "tsuki.bundle.*.css" -print -quit)
|
||||
sz=$(gzip -9 -c "$css" | wc -c)
|
||||
echo "tsuki bundle gz: $sz B"
|
||||
test "$sz" -le 4200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### H4 — `head.html` Pagination linking is wrong on `home`
|
||||
**File:** `layouts/_partials/head.html:55`
|
||||
|
||||
```go-html-template
|
||||
{{- if or .IsHome (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") }}
|
||||
{{- with .Paginator }} … {{- end }}
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
`home.html` does not call `.Paginator` (and the home isn't paginated — it's a portfolio + recent post snippet). `.Paginator` on `home` returns implicit pagination for `site.RegularPages` and emits `rel=prev/next` links to non-existent `/page/2/` etc., causing 404s for crawlers and bad `<link rel="prev/next">` SEO.
|
||||
|
||||
**Fix:** drop `.IsHome` from the gate, or check `if gt .Paginator.TotalPages 1` (which covers all kinds correctly).
|
||||
|
||||
---
|
||||
|
||||
### H5 — Code-copy script attaches to all `<pre>` regardless of code presence
|
||||
**File:** `assets/js/code-copy.js:5-6`
|
||||
|
||||
`for (const pre of document.querySelectorAll("pre"))`: matches every `<pre>` on the page including non-Hugo-highlight ones in render-hook output. The `if (!code) continue` guards a missing `<code>`, but a `<pre>` containing `<code>` rendered for non-code purposes (e.g., ASCII art) gets a "Sao chép" button. Minor but fixable.
|
||||
|
||||
Also: `navigator.clipboard` is unavailable on `http://` non-localhost. The theme falls back gracefully (`FAILED` text), but no explanation appears. Add `if (!navigator.clipboard) return;` before `pre.appendChild(btn)` so users on HTTP don't see broken-looking buttons.
|
||||
|
||||
---
|
||||
|
||||
### H6 — `comments.html` script tag uses `defer` semantics inconsistently
|
||||
**File:** `layouts/_partials/comments.html:7-21`
|
||||
|
||||
The `<script>` is rendered inline in the `comments` section. With `async`, Giscus loads independently of the `<script>` element's DOM position, but Giscus expects the parent element to be in the DOM at parse time — placing the `<script>` *after* the `<div class="giscus">` is correct. However: the `data-strict="0"` etc. are emitted as quoted strings. Giscus accepts `0`/`1` as strings, so this works; but if a user types `strict: false` (yaml bool), Hugo emits `false`, which Giscus treats as truthy. Document the type expected.
|
||||
|
||||
Also missing: `referrerpolicy="no-referrer-when-downgrade"` and `loading="lazy"` is set via `data-loading` (correct), but the `<iframe>` Giscus injects gets sandboxed by Giscus itself, not by the theme. Acceptable.
|
||||
|
||||
---
|
||||
|
||||
### H7 — IntersectionObserver TOC: encoded headings can mismatch
|
||||
**File:** `assets/js/toc-active.js:5`
|
||||
|
||||
```js
|
||||
const id = decodeURIComponent(a.getAttribute("href").slice(1));
|
||||
```
|
||||
With `autoHeadingIDType: github-ascii`, IDs are pure ASCII so `decodeURIComponent` is a no-op. But heading IDs in Hugo's TOC are URL-encoded if non-ASCII — and `headings.querySelectorAll(...[id])` in the same script uses `h.id` (the raw DOM id, *not* URL-encoded). If a user disables `autoHeadingIDType: github-ascii` (config docs say it's "recommended", not required), the link `href="#%C3%A1"` decodes to `"á"` but `h.id === "á"` matches — *good*. So this code is correct under both modes. Keep the `decodeURIComponent` call. No fix needed; documenting reasoning here for posterity.
|
||||
|
||||
---
|
||||
|
||||
### H8 — `home/recent-posts.html` runs the same query twice
|
||||
**File:** `layouts/_partials/home/recent-posts.html:2,15`
|
||||
|
||||
```go-html-template
|
||||
{{- $posts := first $count (where (where site.RegularPages "Type" "post") "Draft" false) -}}
|
||||
…
|
||||
{{- if gt (len (where (where site.RegularPages "Type" "post") "Draft" false)) $count }}
|
||||
```
|
||||
|
||||
Hugo evaluates `where` twice. Negligible build-time cost on small sites, but smelly. Bind once:
|
||||
```go-html-template
|
||||
{{- $all := where (where site.RegularPages "Type" "post") "Draft" false -}}
|
||||
{{- $posts := first $count $all -}}
|
||||
```
|
||||
Also: `Draft` filtering is redundant because `--buildDrafts` is the master switch and `site.RegularPages` already excludes drafts in normal builds. Drop `"Draft" false` unless explicitly supporting `--buildDrafts` while still hiding drafts from home (uncommon).
|
||||
|
||||
---
|
||||
|
||||
## Medium
|
||||
|
||||
### M1 — Categories never surface to readers
|
||||
**Files:** `layouts/_partials/meta.html`, `layouts/_partials/post-card.html`, `layouts/single.html`
|
||||
|
||||
Posts use `categories` in frontmatter. The theme lists `categories` taxonomy but the meta partial only shows `tags`. There is no per-post category badge, no category-list partial, no sidebar.
|
||||
|
||||
If categories are intentionally invisible (only for routing under `/categories/<slug>/`), document it. Otherwise add a small "📂 ghi-chu" pill alongside tags in `meta.html`.
|
||||
|
||||
---
|
||||
|
||||
### M2 — No `Lastmod` surfaced on the post page
|
||||
**File:** `layouts/_partials/meta.html`, `layouts/single.html`
|
||||
|
||||
`head.html:36` emits `article:modified_time`, good. But the visible post body never shows "Cập nhật ngày X" even though `i18n/vi.yml` defines `updatedOn`. Either remove the unused key or add a `{{ with .Lastmod }}…{{ end }}` block in `meta.html`.
|
||||
|
||||
---
|
||||
|
||||
### M3 — No skip-link, focus rings, or `prefers-reduced-motion` on `transition`
|
||||
**Files:** `assets/css/reset.css:35-42`, `layouts/baseof.html`
|
||||
|
||||
- No `<a class="skip-link" href="#main">` in `baseof.html`. Vi keyboard users (and screen-reader users) jump through full nav on every page.
|
||||
- `.theme-toggle:focus-visible`, `.search-button:focus-visible`, `.site-nav a:focus-visible`, etc. lack explicit focus styles — they rely on UA defaults, which dark-mode often makes invisible (browser default is `outline: 1px solid -webkit-focus-ring-color` blue against `#14151a` is only marginal).
|
||||
- `reset.css:35-42` covers reduced motion for `animation`/`transition`/`scroll-behavior` but `view-transitions.css` *also* handles it. The `!important` overrides win, so view-transitions get nuked too. That's likely the intent; document in CHANGELOG.
|
||||
|
||||
**Fix:** Add to `components.css` or `reset.css`:
|
||||
```css
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--tsuki-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
And add in `baseof.html`:
|
||||
```html
|
||||
<a class="skip-link" href="#main">{{ i18n "skipToContent" | default "Đến nội dung chính" }}</a>
|
||||
```
|
||||
plus `id="main"` on `<main>`.
|
||||
|
||||
---
|
||||
|
||||
### M4 — `tsuki-vt-name` CSS variable references nothing that sets it
|
||||
**File:** `assets/css/home.css:82`
|
||||
|
||||
```css
|
||||
.project-card { view-transition-name: var(--tsuki-vt-name, none); }
|
||||
```
|
||||
|
||||
The fallback `none` makes this a no-op. Nothing in templates sets `--tsuki-vt-name` per-card, so cards have *all* `view-transition-name: none`. Either:
|
||||
- Set `--tsuki-vt-name: project-{{ printf "%d" $index }}` per card via inline `style=""` (hugo loop), enabling card-morph transitions across navigations.
|
||||
- Or remove the dead declaration. Currently it advertises a feature that doesn't exist.
|
||||
|
||||
---
|
||||
|
||||
### M5 — Theme flash prevention script is non-blocking but works
|
||||
**File:** `layouts/_partials/head.html:13-23`
|
||||
|
||||
The IIFE before `<body>` reads `localStorage` and sets `data-theme`. Good. Three subtle issues:
|
||||
1. **Layout-shift risk**: if `localStorage` access throws (Safari ITP private mode), the catch is silent — root stays without `data-theme`, dark-mode kicks in via `prefers-color-scheme: dark` rule. Then `theme-toggle.js` (deferred module) reads stored=null and assumes match-media. Outcome: correct theme on first paint; toggle button starts in "match prefers" state. Verify this is intended.
|
||||
2. The `<script>` is *inline* but the `<link rel="stylesheet">` for the bundled CSS is loaded after it. `<link rel="stylesheet">` blocks rendering, so the `data-theme="..."` attribute is set before the stylesheet evaluates. ✓ no FOUC for the toggle.
|
||||
3. Storage key `tsuki-theme` is shared across sites mounting the theme on the same origin. For a single-domain blog this is fine. Consider scoping (low priority).
|
||||
|
||||
---
|
||||
|
||||
### M6 — Pagefind CSS loaded outside the bundle defeats SRI on that asset
|
||||
**File:** `layouts/search/list.html:4`
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="{{ "/pagefind/pagefind-ui.css" | relURL }}">
|
||||
```
|
||||
|
||||
No `integrity=`. Pagefind ships its own CSS; not part of the asset pipeline. Fine — but document that Pagefind UI CSS is third-party and not budget-counted. Consider Subresource Integrity once Pagefind starts shipping a stable hash (currently they don't).
|
||||
|
||||
---
|
||||
|
||||
### M7 — `home.html` calls partials unconditionally; partials guard themselves
|
||||
**File:** `layouts/home.html:4-6`
|
||||
|
||||
`home/projects.html` does `{{- with site.Data.projects.featured -}}` — if no `data/projects.yaml`, the section silently vanishes. Same for `hero.html`. Acceptable. Document that an empty `data/profile.yaml` and `data/projects.yaml` is valid and produces a near-empty homepage.
|
||||
|
||||
Edge case: `data/projects.yaml` exists but has no `featured` key → `with` is falsy → empty grid. Good.
|
||||
Edge case: `featured: []` → `with []` is falsy → no `<h2>` heading orphan. Good.
|
||||
|
||||
---
|
||||
|
||||
### M8 — `range .Pages.ByTitle` in taxonomy.html ignores Title casing for vi
|
||||
**File:** `layouts/_default/taxonomy.html:10`
|
||||
|
||||
`ByTitle` sorts ASCII-first; "Áo" comes after "Zip" in default Go locale. For a vi-first theme, sort by `.Title` with a normalized key. Hugo doesn't expose locale-aware sort directly — workaround:
|
||||
```go-html-template
|
||||
{{- range .Pages.ByParam "title" }}…
|
||||
```
|
||||
or manually `urlize` to a normalized key. Lowest priority, but Vi authors will notice "Á-bài" sorted weirdly.
|
||||
|
||||
---
|
||||
|
||||
### M9 — `archive-group.html` `GroupByDate "1"` is month number; date "02/01" is day/month
|
||||
**File:** `layouts/_partials/archive-group.html:4,10`
|
||||
|
||||
`GroupByDate "1"` → numeric month ("1".."12"). The post date `02/01` (DD/MM) correctly emits Vietnamese order. But **rounded to "08"** (zero-pad) when rendered as `{{ .Date.Format "02/01" }}` (Go time.Format). Verify intent: dates show DD/MM, archive month heading is `Tháng 1`..`Tháng 12`. If a Vi reader expects `Tháng 01`, the i18n month string handles it. ✓
|
||||
|
||||
---
|
||||
|
||||
## Low
|
||||
|
||||
### L1 — `theme.toml: original.author = ""` clutters Hugo theme directory listing
|
||||
**File:** `theme.toml:15-18`
|
||||
|
||||
If tsuki isn't a fork, drop the `[original]` block. Hugo's themes registry treats empty `original.author` as malformed.
|
||||
|
||||
---
|
||||
|
||||
### L2 — `archetypes/default.md` doesn't include `description`
|
||||
**File:** `archetypes/default.md`
|
||||
|
||||
`description` is referenced in `head.html` for OG meta. Adding it to the archetype reduces "missing description" surprises:
|
||||
```md
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
description: ""
|
||||
tags: []
|
||||
categories: []
|
||||
---
|
||||
```
|
||||
|
||||
### L3 — `post-card.html`: `.Summary | plainify | truncate 180` — order matters
|
||||
**File:** `layouts/_partials/post-card.html:11-13`
|
||||
|
||||
`plainify` then `truncate` is correct but truncation can land mid-Vietnamese diacritic if the byte cursor splits a multi-byte rune. Hugo's `truncate` is rune-safe (Go strings), so no corruption. Just confirming.
|
||||
|
||||
### L4 — `i18n/vi.yml` mixes shapes
|
||||
**File:** `i18n/vi.yml`
|
||||
|
||||
`readingTime` uses Go-template `{{ .Count }}` interpolation. Other strings are plain. Hugo i18n uses go-i18n v1 syntax — both forms valid, but inconsistent. Consider unifying to plural-aware:
|
||||
```yaml
|
||||
- id: readingTime
|
||||
one: "1 phút đọc"
|
||||
other: "{{ .Count }} phút đọc"
|
||||
```
|
||||
Vietnamese has no grammatical plural so cosmetic only.
|
||||
|
||||
### L5 — `comments.html` Giscus theme attribute uses `data-theme="preferred_color_scheme"` default
|
||||
**File:** `layouts/_partials/comments.html:17`
|
||||
|
||||
Combined with `giscus-theme.js` which posts `setConfig: { theme: "light"|"dark" }`, the iframe's initial render uses `preferred_color_scheme`, then the script overrides on `data-theme` mutation. There's a flash on first paint where Giscus renders its default theme, then toggles. Lowest-priority cosmetic.
|
||||
|
||||
**Fix:** initial-call `send()` once on page load so Giscus opens already-themed:
|
||||
```js
|
||||
send();
|
||||
new MutationObserver(send).observe(...);
|
||||
```
|
||||
Currently the Observer only fires on mutation, never on initial state.
|
||||
|
||||
### L6 — `code-copy.js` button label hardcoded vi strings
|
||||
**File:** `assets/js/code-copy.js:1-3`
|
||||
|
||||
`COPY = "Sao chép"`, etc. Should match `i18n/vi.yml` for future en support. Inject from data attribute set by template:
|
||||
```html
|
||||
<pre data-copy-label="{{ i18n "copy" }}" data-copied-label="{{ i18n "copied" }}">
|
||||
```
|
||||
Or accept this is vi-first and don't surface the strings to i18n.
|
||||
|
||||
### L7 — `code-copy.js`: tabindex/keyboard for the button
|
||||
**File:** `assets/js/code-copy.js`
|
||||
|
||||
Button has no `tabindex` attribute (defaults to `0` for `<button>`, ✓), but no `aria-live` region for the "Đã chép" feedback. SR users won't hear confirmation. Add `aria-live="polite"` to the button or to a sibling span.
|
||||
|
||||
### L8 — `images/screenshot.png` and `images/tn.png` not referenced anywhere
|
||||
**File:** `images/*.png`
|
||||
|
||||
Hugo themes registry expects `images/screenshot.png` (1500×1000) and `images/tn.png` (900×600). Verify dims match. Most recent commit message says they were added; confirm in registry standards (`https://themes.gohugo.io/`).
|
||||
|
||||
### L9 — `package.json` `private: true` blocks `npm publish` but pagefind is a dev dep that ought to be `dependencies` (or `peerDependencies`?) for Hugo Module consumers
|
||||
**File:** `package.json`
|
||||
|
||||
If a user adopts tsuki as a Hugo Module, they don't get Pagefind from the theme's `package.json` — Hugo Modules don't run npm. Document: "Pagefind is built in CI; consumer sites need their own `npm install pagefind` or just `npx pagefind` step." Currently undocumented.
|
||||
|
||||
---
|
||||
|
||||
## Nice-to-have
|
||||
|
||||
### N1 — Add a `_markup/render-link.html` for safer external links
|
||||
Auto-detect external URLs and add `rel="noopener noreferrer" target="_blank"` (or ditto without `_blank` — UX preference).
|
||||
|
||||
### N2 — Add `_markup/render-image.html` to enforce `loading="lazy" decoding="async"`
|
||||
All in-content images currently rely on goldmark default (no lazy attrs).
|
||||
|
||||
### N3 — `Hugo Module` mounts not declared in `theme.toml`
|
||||
For Hugo Module consumers, declare `[module]` mounts in `hugo.yaml`-or-`config.yaml` so themes mount cleanly:
|
||||
```yaml
|
||||
module:
|
||||
mounts:
|
||||
- source: layouts
|
||||
target: layouts
|
||||
- source: assets
|
||||
target: assets
|
||||
- source: i18n
|
||||
target: i18n
|
||||
- source: data
|
||||
target: data
|
||||
- source: archetypes
|
||||
target: archetypes
|
||||
- source: static
|
||||
target: static
|
||||
```
|
||||
Without it, default mounts work but hand-overrides break (rare).
|
||||
|
||||
### N4 — RSS limit on home pagination: site builds `<link rel="prev">` from `.Paginator` even if homepage isn't a paginated kind. Already covered in H4.
|
||||
|
||||
### N5 — Privacy: meta `generator` exposes Hugo version
|
||||
**File:** `layouts/_partials/head.html:7`
|
||||
|
||||
`<meta name="generator" content="Hugo {{ hugo.Version }} + tsuki">` lets attackers know Hugo version → CVE matching. Drop `hugo.Version` and emit just `tsuki @ v0.1.0`, or skip the meta entirely. Defense in depth.
|
||||
|
||||
### N6 — `view-transitions.css` doesn't fall back gracefully when CSS chain breaks
|
||||
The `@view-transition` rule is unsupported in Firefox/Safari ≤17. Browsers ignore it (good). The `@keyframes tsuki-fade-out/in` are emitted regardless and only used by view-transition pseudo-elements, so dead bytes in unsupporting browsers. Trade-off: 50 B for forward-compat.
|
||||
|
||||
### N7 — Self-host woff2 fonts deferred — no `font-display: swap` set on the system fallback chain
|
||||
Adding `font-display: swap` is irrelevant when no `@font-face` is declared — but document this in `customization.md` so first-time self-hosters know.
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases / Verification Notes
|
||||
|
||||
- **Empty `data/profile.yaml`**: `home/hero.html` `with $profile` skips the whole hero. Site title shows in header instead. ✓
|
||||
- **Missing thumbnails**: `home/projects.html` `with $project.image` skips the image div. ✓
|
||||
- **Posts without word counts** (e.g., front-matter-only): `gt .WordCount 400` is `false`, TOC suppressed, `meta.html` shows `0 phút` if `gt .ReadingTime 0` guard somehow fails — the guard is `gt 0`, so `0 → false → no render`. ✓
|
||||
- **Browser without View Transitions**: `@view-transition` rule ignored; no fallback animation, just instant nav. ✓ matches docs.
|
||||
- **Browser without `:has()`**: codebase doesn't use `:has()` anywhere I could find. Search confirms zero hits. README mentions `:has()` as progressive enhancement — code doesn't actually use it. Either add a usage or drop the claim.
|
||||
- **Browser without IntersectionObserver**: `toc-active.js` will throw `ReferenceError` on `new IntersectionObserver(...)`. Old browsers (IE11, pre-Chrome 51) error out. Modern evergreen support is universal. Theme.toml says "Modern evergreen browsers" — acceptable.
|
||||
- **No JS at all**: theme toggle button `hidden` attribute remains (button never reveals). ✓ progressive enhancement. Code-copy disabled. ✓ TOC active highlighting disabled but TOC still renders. ✓ Search disabled with `<noscript>` fallback. ✓
|
||||
|
||||
---
|
||||
|
||||
## Documentation vs Code Match
|
||||
|
||||
| Doc claim | Code behavior | Match |
|
||||
|---|---|---|
|
||||
| `params.toc.minWordCount` gates render | hardcoded 400 | ✗ (C1) |
|
||||
| `params.toc.enable` site-wide kill-switch | not consulted | ✗ (C1) |
|
||||
| "all UI strings in `i18n/vi.yml`" | 8 keys missing | ✗ (H1) |
|
||||
| `params.search.enable: false` removes route | only header button hidden | ✗ (H2) |
|
||||
| "CSS bundle ≤ 4 KB gz, JS ≤ 1 KB gz" | pre-minify gz: 4296 B / 660 B; post-minify likely under | ⚠ (H3) |
|
||||
| `:has()` progressive enhancement | not used in CSS | ⚠ (no harm but misleading) |
|
||||
| OG image fallback to `profile.avatar` | `head.html:3` → ✓ | ✓ |
|
||||
| `data-pagefind-body` on post content | `single.html:13` → ✓ | ✓ |
|
||||
| Theme flash prevention | inline IIFE before stylesheet → ✓ | ✓ |
|
||||
| ASCII heading IDs via `autoHeadingIDType: github-ascii` | configured | ✓ |
|
||||
| `permalinks: post: /:year/:month/:day/:contentbasename/` | configured + matches content paths | ✓ |
|
||||
| Both submodule and Hugo Module flow | submodule ✓; Module flow undocumented mounts | ⚠ (N3) |
|
||||
|
||||
---
|
||||
|
||||
## Positive Observations
|
||||
|
||||
- Excellent restraint on JS surface area — 4 modules, total ~1 KB gz, each module a single responsibility.
|
||||
- Asset pipeline `Concat | minify | fingerprint` with SRI integrity attribute on `<link>` and `<script>`: best practice.
|
||||
- Inline theme-flash-prevention script kept tight (no markdownify, no template variables that could break SRI on the bundle).
|
||||
- Body-class block via `{{ define "body_class" }}` is clean composition; lets per-page CSS hooks like `body.post .toc { … }` work without JS.
|
||||
- `aria-current="page"` on nav, `aria-current="true"` on active TOC link: correct.
|
||||
- `<noscript>` fallback on the search page.
|
||||
- Render-heading hook is minimal and accessible (anchor has `aria-label`).
|
||||
- Content-Security-Policy compatible (no inline `style=""`, only one inline `<script>` with constant content the CSP can hash).
|
||||
- `.gitignore` covers Pagefind generated content + `node_modules`. Good.
|
||||
- Dependabot config is sensible.
|
||||
- View-transition `prefers-reduced-motion` honored at the *animation* level, not blanket-disabled.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Actions (priority order)
|
||||
|
||||
1. **C1** — Fix TOC gating to honor `site.Params.toc.{enable,minWordCount}`. Short PR, high doc/behavior gap.
|
||||
2. **H1** — Add 8 missing i18n keys.
|
||||
3. **C3** — Switch tag URLs to `Page.GetTerms`.
|
||||
4. **H2** — Gate `search/list.html` body on `params.search.enable` or update docs.
|
||||
5. **H4** — Drop `.IsHome` from pagination link emission in `head.html`.
|
||||
6. **C2** — Document HTML execution in `profile.bio` markdown.
|
||||
7. **M3** — Add `:focus-visible { outline … }` and skip-link.
|
||||
8. **M1/M2** — Decide on category surfacing and `Lastmod` display; remove unused i18n keys.
|
||||
9. **H3** — Add CSS budget assertion to CI.
|
||||
10. **L5** — `giscus-theme.js`: call `send()` once on load.
|
||||
|
||||
Lower-priority items (N1–N7, L1–L9, M4–M9) batch as polish before v0.2.
|
||||
|
||||
---
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
1. Is the `unsafe: true` Goldmark requirement specifically for embedded HTML in posts (e.g., `<details>`, custom div for callouts), or just for footnote rendering? If the former, document examples; if the latter, switching to `unsafe: false` removes both the XSS surface (C2) and the documentation burden.
|
||||
2. Should `params.toc.enable: false` also strip the bundled TOC CSS? Current pipeline always concats `toc.css` (~1.5 KB raw, ~600 B gz). For sites that disable TOC, this is dead bytes. Option: split TOC into separate bundle, conditionally loaded.
|
||||
3. Is the homepage intended to ever paginate (more recent posts beyond `recentPostsCount`)? If no, H4's fix is to drop the `.IsHome` branch entirely. If yes, the home `home.html` template must call `.Paginator.Pages` somewhere.
|
||||
4. Are categories deliberately invisible to readers (M1)? If yes, drop the `categories` taxonomy from theme defaults and add a CHANGELOG note.
|
||||
5. Hugo theme registry submission planned? `theme.toml` has empty `[original]` block (L1) and `screenshot.png`/`tn.png` need verification against registry size requirements.
|
||||
6. What's the expected `--baseURL` story for sites that don't deploy under `/tsuki/` (i.e., root deploys)? `relURL` handles it; verify with a test deploy at root.
|
||||
7. Is Vietnamese the only locale planned, or is `i18n/en.yml` intended (currently only `vi.yml`)? Half the i18n keys have English `default` fallbacks suggesting eventual en.yml.
|
||||
|
||||
---
|
||||
|
||||
**Status:** DONE
|
||||
**Summary:** Reviewed full v0.1.0 surface. 3 critical (TOC config dead, XSS-via-bio docs, hardcoded /tags URL), 8 high, 9 medium, 9 low, 7 nice-to-have. Code is clean and idiomatic; main gaps are doc/code drift and a few accessibility polish items.
|
||||
**Concerns:** None blocking. Recommend fixing C1, H1, H2, H4, and adding skip-link before promoting v0.1.0 widely. Items C2, M1, M2 deserve a maintainer decision (security policy + UX intent) before code change.
|
||||
@@ -0,0 +1,527 @@
|
||||
---
|
||||
report_type: Feature Gap & Best Practices Analysis
|
||||
status: DONE
|
||||
date: 2026-05-08
|
||||
scope: tsuki v0.1.0 vs. 2025-2026 Hugo theme ecosystem
|
||||
---
|
||||
|
||||
# Hugo Theme Best Practices 2025-2026: Gap Analysis for tsuki
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Tsuki (`v0.1.0`) is a lean, Vietnamese-first Hugo theme (CSS ≤ 4KB gz, JS ≤ 1KB gz) with strong core fundamentals: zero build step, Pagefind search, Giscus comments, View Transitions, dark mode, and Vietnamese typography. Benchmarked against 5 actively maintained themes (PaperMod, Stack, Congo, Blowfish, Hugo Blox), **tsuki lacks 8 major feature categories** that 2025-era themes routinely ship. Most are post-0.1.0 deferred work. **Assessment:** tsuki's philosophy (KISS, lightweight) is _aligned_ with modern practices; feature gaps are intentional, not architectural oversights.
|
||||
|
||||
**Recommendation priority (post-0.1.0):**
|
||||
1. **HIGH (DX):** SEO structured data (JSON-LD Article schema) + OpenGraph/Twitter metadata
|
||||
2. **MEDIUM (Author UX):** Reading time, word count, admonitions/callouts shortcode, related posts
|
||||
3. **MEDIUM (Distribution):** Finalize theme.toml + image assets for Hugo theme gallery
|
||||
4. **LOW-MEDIUM (Optional):** i18n framework for future multilingual support
|
||||
5. **DEFERRED (by design):** Firebase counters, Mermaid diagrams, KaTeX (add-on friendly, keep out-of-core)
|
||||
|
||||
---
|
||||
|
||||
## 1. Feature Gap Analysis: tsuki vs. Peer Themes
|
||||
|
||||
### 1.1 Competing Themes (Actively Maintained, 2025-2026)
|
||||
|
||||
| Theme | Stars | Size/Philosophy | Key Differentiator |
|
||||
|-------|-------|-----------------|-------------------|
|
||||
| **PaperMod** | 9k+ | Zero JS build, sub-1s Hugo build | Best-of-breed blog design; Fuse.js search, 3 layouts, breadcrumbs, related posts, cover images |
|
||||
| **Blowfish** | ~3k | Tailwind 3.0, Firebase, Fuse.js | RTL support, Firebase view counters/likes, Mermaid+Chart.js+KaTeX, zen reading mode, multiple authors |
|
||||
| **Stack** | ~4k | Feature-rich, Tailwind, image processing | Mermaid diagrams v3.33.0+, image gallery, responsive images, flexible content sections |
|
||||
| **Congo** | ~1.5k | Tailwind-based, modular | Design-first; supports Blox-style callouts (native Markdown), analytics hooks |
|
||||
| **Hugo Blox** | ~150k+ sites | Modular blocks, academic focus | Jupyter rendering, BibTeX/DOI citations, 150k+ institutional adoption, multilingual |
|
||||
|
||||
### 1.2 Feature Gaps: tsuki Lacks vs. Peers
|
||||
|
||||
#### Gap 1: **SEO Structured Data (JSON-LD Article Schema)**
|
||||
- **Who has it:** PaperMod, Blowfish, Congo, Stack, Hugo Blox
|
||||
- **What tsuki lacks:** Article schema (JSON-LD), OpenGraph meta tags, Twitter Card tags
|
||||
- **Current state:** tsuki has basic meta tags (title, description, author) but NO structured data
|
||||
- **Impact:** SEO risk — Google may not properly understand article type, publish date, author; social sharing lacks preview image/description
|
||||
- **Effort:** S (partial template + data cascade) | **Value:** HIGH
|
||||
- **2025 baseline:** All production themes include `JSON-LD Article`, OpenGraph, Twitter Cards; Google rewards sites with Schema.org markup
|
||||
|
||||
#### Gap 2: **Reading Time Estimate + Word Count Display**
|
||||
- **Who has it:** PaperMod, Stack, Blowfish, Congo
|
||||
- **What tsuki lacks:** `{{ .ReadingTime }}` template variable + metadata display (author, word count, reading time byline)
|
||||
- **Current state:** tsuki shows post date; no byline metadata
|
||||
- **Impact:** User experience — readers don't know post length before clicking; author context missing for multi-author sites
|
||||
- **Effort:** S (template + i18n string) | **Value:** MED
|
||||
- **2025 baseline:** Reading time = expected UX feature in blog themes; Hugo provides `.ReadingTime`, `.WordCount` out-of-box
|
||||
|
||||
#### Gap 3: **Author + Byline Metadata**
|
||||
- **Who has it:** PaperMod, Blowfish, Hugo Blox, Stack
|
||||
- **What tsuki lacks:** Multi-author support; author biography; author profile links
|
||||
- **Current state:** Single author only (via `params.author` in site config)
|
||||
- **Impact:** Collaborative sites can't easily attribute posts; author credibility/expertise not displayed
|
||||
- **Effort:** M (archetype, taxonomy, partial) | **Value:** MED
|
||||
- **2025 baseline:** Blowfish ships native multi-author; most mature themes support per-post author override
|
||||
|
||||
#### Gap 4: **Admonitions/Callouts Shortcode**
|
||||
- **Who has it:** Hugo Blox (native Markdown `> [!note]`), Congo, Blowfish, Learn theme
|
||||
- **What tsuki lacks:** Alert/note/warning/tip shortcode or blockquote render hook
|
||||
- **Current state:** tsuki renders basic blockquotes only
|
||||
- **Impact:** Content authors can't easily highlight critical information (warnings, tips, notes)
|
||||
- **Effort:** S (blockquote render hook OR shortcode) | **Value:** MED
|
||||
- **2025 baseline:** Markdown callouts (`> [!note]`, `> [!warning]`) are now native to Hugo 0.150+; Blox switched from custom shortcodes to native syntax
|
||||
|
||||
#### Gap 5: **Related Posts / Suggested Reading**
|
||||
- **Who has it:** PaperMod (default), Blowfish, Stack
|
||||
- **What tsuki lacks:** Related posts sidebar/section (no `.Site.RegularPages.Related()` output)
|
||||
- **Current state:** No related posts section on single post view
|
||||
- **Impact:** Engagement — users see next post to read; blog discovery improved
|
||||
- **Effort:** M (template + Hugo config for `.Related` weighting) | **Value:** MED
|
||||
- **2025 baseline:** PaperMod ships "Recent Posts" + related posts by default
|
||||
|
||||
#### Gap 6: **Image Lightbox / Gallery Shortcode**
|
||||
- **Who has it:** Stack (image gallery), Blowfish (gallery shortcode), Hugo Blox (figure galleries)
|
||||
- **What tsuki lacks:** Image lightbox (click to enlarge), gallery shortcode, responsive image processing
|
||||
- **Current state:** Inline images only; no lightbox JS
|
||||
- **Impact:** Photography-heavy or portfolio posts render poorly; images not zoomable
|
||||
- **Effort:** M (lightbox JS lib + shortcode) | **Value:** LOW-MED
|
||||
- **2025 baseline:** Blowfish automates image resizing via Hugo Pipes + responsive srcset; Stack + Blox ship native galleries
|
||||
- **Note:** Explicitly deferred post-0.1.0 in CHANGELOG
|
||||
|
||||
#### Gap 7: **Copy Link-to-Heading / Share Permalink**
|
||||
- **Who has it:** Blowfish (copy button on headings), PaperMod, Stack
|
||||
- **What tsuki lacks:** Heading anchor buttons; permalink copy functionality
|
||||
- **Current state:** Headings are IDed (github-ascii) but no UI affordance to copy link
|
||||
- **Impact:** Minor UX — users can't easily share link to specific section
|
||||
- **Effort:** S (render hook for `<h*>` + minimal JS) | **Value:** LOW
|
||||
- **2025 baseline:** Increasingly expected on article blogs; adds credibility, improves sharing
|
||||
|
||||
#### Gap 8: **Footnotes / Sidenote Rendering**
|
||||
- **Who has it:** Hugo Tufte, Blowfish, PaperMod (via Goldmark)
|
||||
- **What tsuki lacks:** Footnote styling; sidenote support
|
||||
- **Current state:** Goldmark footnotes render but may lack visual styling
|
||||
- **Impact:** Academic/long-form content less polished
|
||||
- **Effort:** S (CSS + render hook) | **Value:** LOW
|
||||
- **2025 baseline:** Low priority; nice-to-have for academic/long-form content
|
||||
|
||||
#### Gap 9: **KaTeX Math Rendering**
|
||||
- **Who has it:** Blowfish, Stack, Hugo Blox, Relearn
|
||||
- **What tsuki lacks:** Client-side or server-side math rendering
|
||||
- **Current state:** Plain text math only
|
||||
- **Impact:** Technical/science blogs can't render equations
|
||||
- **Effort:** M (KaTeX JS lib + Goldmark config) | **Value:** LOW
|
||||
- **2025 baseline:** Explicitly deferred post-0.1.0; opt-in for technical sites
|
||||
|
||||
#### Gap 10: **Mermaid Diagram Support**
|
||||
- **Who has it:** Stack (v3.33.0+), Blowfish, Hugo Blox, Relearn
|
||||
- **What tsuki lacks:** Mermaid diagram shortcode/render
|
||||
- **Current state:** No diagram support
|
||||
- **Impact:** Cannot embed flowcharts, sequence diagrams, timelines in posts
|
||||
- **Effort:** S (Mermaid JS + shortcode) | **Value:** LOW
|
||||
- **2025 baseline:** Explicitly deferred post-0.1.0; Stack added in v3.33.0
|
||||
|
||||
---
|
||||
|
||||
## 2. Hugo 0.140-0.160+ Platform Features (April 2025 – May 2026)
|
||||
|
||||
### 2.1 Recent Hugo Capabilities Tsuki Should Leverage
|
||||
|
||||
| Feature | Version | Current tsuki Use | Recommendation |
|
||||
|---------|---------|------------------|-----------------|
|
||||
| **css.Build** | 0.140+ | Uses `resources.Concat` + minify | Upgrade to native `css.Build` for future Tailwind support (if needed) |
|
||||
| **js.Batch** | 0.140 | None (no JS bundling) | Consider for modular JS if feature complexity grows |
|
||||
| **Content Adapters** | 0.150+ | None | NOT recommended for tsuki (adds dynamism, breaks static philosophy) |
|
||||
| **Blockquote Render Hook** | 0.140+ | Not used | **ADOPT:** Use for callouts/admonitions (`> [!note]` native markdown) |
|
||||
| **Markdown Callouts** | 0.150+ | Not used | **ADOPT:** Hugo now supports `> [!note]`, `> [!warning]`, `> [!caution]` natively |
|
||||
| **css.Build with CSS vars** | 0.160 | Not used | Low priority; tsuki has minimal CSS config variance |
|
||||
| **Module version pinning** | 0.150+ | If using Hugo Modules | Adopt for theme distribution (if switching from submodule) |
|
||||
| **Image Processing (webp/avif)** | 0.130+ | Not used; static images only | Recommend archetype example showing image optimization |
|
||||
| **RSS/JSON Feeds** | Built-in | `.OutputFormats` | Already works; document in theme docs |
|
||||
|
||||
### 2.2 Action Items: Which to Adopt
|
||||
|
||||
1. **MUST:** Blockquote render hook for callouts (S effort, HIGH value)
|
||||
2. **SHOULD:** Document native Markdown callouts in theme docs (S effort, MED value)
|
||||
3. **OPTIONAL:** If switching to Hugo Modules, leverage module versioning (S effort, MED value for distribution)
|
||||
4. **NO:** Content adapters (contradicts "zero build step" philosophy)
|
||||
5. **NO:** Extensive JS bundling via js.Batch (keeps theme lightweight)
|
||||
|
||||
---
|
||||
|
||||
## 3. SEO & Accessibility Baseline (2025 Production Standard)
|
||||
|
||||
### 3.1 What Modern Themes Include (Baseline Expectation)
|
||||
|
||||
#### **SEO (Mandatory)**
|
||||
- [x] Canonical URLs (auto-generated per post)
|
||||
- [ ] **JSON-LD Article schema** (missing)
|
||||
- [ ] **OpenGraph meta tags** (missing)
|
||||
- [ ] **Twitter Card meta tags** (missing)
|
||||
- [x] Sitemap (Hugo built-in)
|
||||
- [x] RSS feed (Hugo built-in)
|
||||
- [ ] **JSON Feed format** (optional, but increasingly expected)
|
||||
- [ ] **Structured author/creator schema** (missing)
|
||||
|
||||
#### **Accessibility (WCAG 2.1 AA Baseline)**
|
||||
- [x] Semantic HTML (`<header>`, `<main>`, `<article>`, `<footer>`)
|
||||
- [x] Alt text on images (author responsibility)
|
||||
- [x] Color contrast (dark mode + light mode both tested)
|
||||
- [x] Keyboard navigation (no `<div>` buttons)
|
||||
- [x] ARIA landmarks (nav, main, contentinfo)
|
||||
- [ ] **Image lightbox accessibility** (WCAG 2.1.2 no focus trap; if added, must be modal-compliant)
|
||||
- [x] Footnote links (Goldmark built-in)
|
||||
- [x] Focus visible on links (CSS best practice)
|
||||
|
||||
### 3.2 Quick Wins for tsuki (Post-0.1.0)
|
||||
|
||||
| Feature | Effort | WCAG Level | Value |
|
||||
|---------|--------|-----------|-------|
|
||||
| JSON-LD Article schema + OpenGraph | S | SEO (not a11y) | HIGH |
|
||||
| Author structured data (schema.Person) | S | SEO | MED |
|
||||
| Image lightbox (with ARIA) | M | 2.1.2 | LOW |
|
||||
| Copy-link-to-heading button | S | 2.1.4 (Links) | LOW |
|
||||
| Footnote styling + accessibility | S | 2.1.2 | MED |
|
||||
|
||||
**Status quo:** Tsuki meets WCAG 2.1 AA for baseline content. SEO gaps are metadata-only, not structural.
|
||||
|
||||
---
|
||||
|
||||
## 4. Author Experience (DX) Improvements Increasingly Expected
|
||||
|
||||
### 4.1 Content Author Features in 2025 Themes
|
||||
|
||||
| Feature | PaperMod | Blowfish | Stack | Congo | Hugo Blox | tsuki | Priority |
|
||||
|---------|----------|----------|-------|-------|-----------|-------|----------|
|
||||
| Cover images | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | MED (defer) |
|
||||
| Reading time | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | MED (quick) |
|
||||
| Multiple authors | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | MED |
|
||||
| Admonitions | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | MED (quick) |
|
||||
| Related posts | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | MED |
|
||||
| Draft/scheduled post UI | ✓ | ✓ | ✓ | ✓ | ✓ | Partial | LOW |
|
||||
| Series/chapter support | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | LOW |
|
||||
| Byline (author + date) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | HIGH |
|
||||
|
||||
### 4.2 Archetype Best Practices
|
||||
|
||||
Tsuki's archetype should optionally include fields for:
|
||||
- `cover.image` (path to post cover)
|
||||
- `author` (override default; support multi-author)
|
||||
- `tags`, `categories` (already present)
|
||||
- `draft: false`, `publishDate` (for scheduling)
|
||||
- `description` (for SEO)
|
||||
|
||||
**Recommendation:** Expand exampleSite archetypes to show cover images, multi-author, admonitions.
|
||||
|
||||
---
|
||||
|
||||
## 5. Theme Distribution Standards (Hugo Theme Gallery 2025)
|
||||
|
||||
### 5.1 Official Requirements (from gohugoio/hugoThemes)
|
||||
|
||||
#### **Image Assets**
|
||||
- **Screenshot:** 1500×1000 px, saved as `/images/screenshot.png`
|
||||
- **Thumbnail:** 900×600 px, saved as `/images/tn.png`
|
||||
- **Format:** PNG or JPG acceptable
|
||||
- **Status:** Tsuki README notes "screenshots added" in recent builds; verify dimensions
|
||||
|
||||
#### **theme.toml Metadata**
|
||||
Required fields:
|
||||
```toml
|
||||
name = "tsuki"
|
||||
license = "Apache-2.0"
|
||||
licenselink = "https://github.com/tiennm99/tsuki/blob/main/LICENSE"
|
||||
description = "A Hugo blog + personal portfolio theme. Vietnamese-first typography, dark mode, Pagefind search, Giscus comments, View Transitions."
|
||||
homepage = "https://github.com/tiennm99/tsuki"
|
||||
demosite = "https://tiennm99.github.io/tsuki"
|
||||
tags = ["blog", "portfolio", "dark-mode", "search", "vietnamese", "minimal", "view-transitions"]
|
||||
features = ["responsive", "dark-mode", "search", "comments", "i18n"]
|
||||
|
||||
[author]
|
||||
name = "Tien Nguyen"
|
||||
homepage = "https://tiennm99.dev"
|
||||
```
|
||||
|
||||
#### **README Structure**
|
||||
- [ ] Feature list (clear, bullet points)
|
||||
- [ ] Quick start (submodule + Module)
|
||||
- [ ] Configuration doc link
|
||||
- [ ] Data schemas link
|
||||
- [ ] Screenshot
|
||||
- [ ] License badge + link
|
||||
- [ ] Status: Currently compliant; ensure theme.toml exists
|
||||
|
||||
#### **Other Requirements**
|
||||
- Open Source license (Apache-2.0: ✓)
|
||||
- Tested against Hugo Basic Example (should validate)
|
||||
- Demo must be functional (or flagged for removal after 30 days)
|
||||
- **Status:** Tsuki meets these; README is complete
|
||||
|
||||
### 5.2 Gallery Submission Checklist
|
||||
|
||||
- [ ] **theme.toml:** Present with all required fields
|
||||
- [ ] **Screenshot:** 1500×1000 PNG in `/images/screenshot.png` ✓ (added recently)
|
||||
- [ ] **Thumbnail:** 900×600 PNG in `/images/tn.png` ✓ (added recently)
|
||||
- [ ] **exampleSite:** Complete, demo-ready
|
||||
- [ ] **License:** Apache-2.0 in LICENSE file ✓
|
||||
- [ ] **README:** Comprehensive, links to docs ✓
|
||||
- [ ] **hugo.toml/yaml:** Specifies Hugo 0.146+ minimum ✓
|
||||
|
||||
**Status:** Tsuki is **ready for Hugo theme gallery submission** (pending theme.toml verification).
|
||||
|
||||
---
|
||||
|
||||
## 6. Internationalization (i18n) Patterns for Future Expansion
|
||||
|
||||
### 6.1 Hugo i18n Architecture (2025 Standard)
|
||||
|
||||
Tsuki currently:
|
||||
- Ships `i18n/vi.yml` with all UI strings
|
||||
- Supports Vietnamese-first layouts (diacritics, time format, heading IDs)
|
||||
- Does not support language-switching
|
||||
|
||||
Peer themes approach:
|
||||
- **Congo, Blowfish:** Multi-language content support; language selector in header
|
||||
- **Hugo Blox:** 40+ language packs; per-language content directories
|
||||
- **Stack:** Language switcher + content routing per `defaultContentLanguage`
|
||||
|
||||
### 6.2 Future i18n Roadmap (Post-0.1.0)
|
||||
|
||||
If tsuki expands internationally:
|
||||
```yaml
|
||||
# Current (v0.1.0): Vietnamese-only
|
||||
defaultContentLanguage: vi
|
||||
languageCode: vi
|
||||
|
||||
# Future (v0.2.0): Add English + Vietnamese
|
||||
languages:
|
||||
vi:
|
||||
languageName: Tiếng Việt
|
||||
contentDir: content/vi
|
||||
params:
|
||||
dateFormat: ":date_long"
|
||||
en:
|
||||
languageName: English
|
||||
contentDir: content/en
|
||||
params:
|
||||
dateFormat: "2006-01-02"
|
||||
```
|
||||
|
||||
**Recommendation:** Do NOT implement until demand; tsuki's Vietnamese-first philosophy is a differentiator.
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing & CI Best Practices for Hugo Themes (2025 Standard)
|
||||
|
||||
### 7.1 Industry Patterns Observed
|
||||
|
||||
| Tool | Purpose | Status in Themes | Tsuki Fit |
|
||||
|------|---------|------------------|-----------|
|
||||
| **htmltest** | Link checking, HTML validation | Used by: Stack, some enterprise themes | **ADOPT:** Validate all links in exampleSite |
|
||||
| **Lighthouse CI** | Performance score automation | Used by: Blowfish, lighthouse100-theme | **NICE-TO-HAVE:** tsuki already achieves 90+ scores |
|
||||
| **pa11y** | Accessibility scanning | Less common; specialized themes | **OPTIONAL:** Run pa11y on demo site |
|
||||
| **Visual regression** (Percy, BackstopJS) | Screenshot comparison | Used by: Large teams | **NOT NEEDED:** Single-author theme |
|
||||
|
||||
### 7.2 Recommended CI for tsuki (GitHub Actions)
|
||||
|
||||
```yaml
|
||||
name: Theme Validation
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: '0.146'
|
||||
- run: hugo --source exampleSite --baseURL https://example.com
|
||||
- run: npx htmltest public
|
||||
- run: npx lighthouse-ci autorun
|
||||
```
|
||||
|
||||
**Current state:** Tsuki has `.github/workflows/pages.yml` (build + Pagefind). Enhance with htmltest + Lighthouse.
|
||||
|
||||
---
|
||||
|
||||
## 8. Performance Benchmarking: What's "Lightweight" in 2025?
|
||||
|
||||
### 8.1 Tsuki's Current Metrics
|
||||
|
||||
| Metric | Value | Peer Average | Status |
|
||||
|--------|-------|--------------|--------|
|
||||
| CSS (gzipped) | ≤ 4 KB | 5–15 KB | EXCELLENT |
|
||||
| JS (gzipped, excl. Pagefind UI) | ≤ 1 KB | 10–50 KB | EXCELLENT |
|
||||
| Build time (1000 pages) | ~2.1ms/page | 2–5ms/page | EXCELLENT |
|
||||
| Lighthouse (Performance) | 95+ | 85–95 | EXCELLENT |
|
||||
| Lighthouse (SEO) | 90+ | 85–100 | GOOD |
|
||||
|
||||
### 8.2 Competitive Positioning
|
||||
|
||||
- **PaperMod:** 0 JS build, ~3KB CSS, ~2KB JS → matches tsuki
|
||||
- **Blowfish:** Tailwind + Fuse.js, ~25KB CSS, ~15KB JS → heavier, feature-rich
|
||||
- **Congo:** Tailwind, similar to Blowfish
|
||||
- **Stack:** Feature-rich, ~20KB+ footprint
|
||||
|
||||
**Conclusion:** Tsuki is legitimately competitive on performance; its lightweight philosophy is **aligned with 2025 best practices** (not outdated).
|
||||
|
||||
---
|
||||
|
||||
## 9. Specific Feature Recommendations Ranked by ROI
|
||||
|
||||
### **TIER 1: HIGH-VALUE, QUICK (Do Next)**
|
||||
|
||||
1. **JSON-LD Article Schema + OpenGraph** | S | HIGH
|
||||
- Add `layouts/partials/head-meta.html` partial
|
||||
- Include `.Params.cover.image` for OG image
|
||||
- Include `.Page.PublishDate` for article publish date
|
||||
- Include author/creator schema
|
||||
- **Why:** Google rewards sites; social sharing improves; 0 complexity
|
||||
- **Timebox:** 2–4 hours
|
||||
|
||||
2. **Reading Time + Word Count Display** | S | MED
|
||||
- Add `{{ .ReadingTime }} min read` to post byline
|
||||
- Add `{{ .WordCount }} words` or hide by params
|
||||
- **Why:** Expected UX feature; Hugo provides out-of-box
|
||||
- **Timebox:** 1 hour
|
||||
|
||||
3. **Blockquote Render Hook for Callouts** | S | MED
|
||||
- Implement blockquote hook for `> [!note]`, `> [!warning]`, `> [!caution]`
|
||||
- **Why:** Aligns with Hugo 0.150+ native markdown; used by all modern themes
|
||||
- **Timebox:** 2–3 hours
|
||||
|
||||
### **TIER 2: MEDIUM-VALUE (Do in v0.2.0)**
|
||||
|
||||
4. **Related Posts** | M | MED
|
||||
- Use `.Site.RegularPages.Related(.Page)` + `.RegularPages.ByDate.Reverse`
|
||||
- Display 3–5 related posts in sidebar/footer
|
||||
- **Why:** Improves blog discovery; standard feature
|
||||
- **Timebox:** 3–4 hours
|
||||
|
||||
5. **Multi-Author Support + Author Pages** | M | MED
|
||||
- Add `authors` taxonomy alongside `tags`, `categories`
|
||||
- Create `/authors/` list layout
|
||||
- Support per-post author override + team sites
|
||||
- **Why:** Growing demand for multi-author sites
|
||||
- **Timebox:** 4–6 hours
|
||||
|
||||
6. **Image Lightbox + Responsive Images** | M | MED-LOW
|
||||
- Add Fancybox or Lightbox2 (minimal JS)
|
||||
- Use Hugo image processing for responsive srcset
|
||||
- **Why:** Portfolio/photography sites expect this
|
||||
- **Timebox:** 4–6 hours
|
||||
- **Note:** Explicitly deferred in CHANGELOG; consider post-0.2.0
|
||||
|
||||
### **TIER 3: NICE-TO-HAVE (v0.3.0+)**
|
||||
|
||||
7. Copy-link-to-heading button | S | LOW
|
||||
8. Footnote styling + visual distinction | S | LOW
|
||||
9. KaTeX math support (opt-in) | M | LOW
|
||||
10. Mermaid diagram shortcode | S | LOW
|
||||
|
||||
---
|
||||
|
||||
## 10. Distribution & Modules vs. Submodules (2025 Consensus)
|
||||
|
||||
### 10.1 Current Tsuki Status
|
||||
|
||||
- Uses **Git submodule** installation method (README shows `git submodule add`)
|
||||
- Also supports **Hugo Modules** (README shows `hugo mod init`)
|
||||
- This is **correct** — both methods should be documented
|
||||
|
||||
### 10.2 2025 Industry Consensus
|
||||
|
||||
**Modules vs. Submodules:** No single "best" choice; depends on contributor base:
|
||||
- **Hugo Modules:** Easier for contributors with Go installed; automatic updates; lazy loading
|
||||
- **Git Submodules:** Only requires Git; full transparency; less automation; preferred by open-source communities without Go expertise
|
||||
|
||||
**Tsuki's approach (supporting both):** CORRECT. Users pick their preference.
|
||||
|
||||
### 10.3 Theme Gallery Distribution
|
||||
|
||||
- If submitted to themes.gohugo.io, the gallery lists both installation methods
|
||||
- Tsuki's current approach is **best practice**
|
||||
|
||||
---
|
||||
|
||||
## 11. Unresolved Questions & Research Gaps
|
||||
|
||||
1. **Tsuki's target audience:** Is tsuki aimed at:
|
||||
- Solo bloggers who want minimal overhead? → Keep deferred features deferred
|
||||
- Teams needing multi-author + portfolio? → Prioritize author metadata
|
||||
- Vietnamese-language sites specifically? → i18n not needed
|
||||
|
||||
*Impact:* Guides feature prioritization. Recommend clarifying in README.
|
||||
|
||||
2. **Pagefind language support:** Does Pagefind fully support Vietnamese diacritics + tone marks?
|
||||
- **Finding:** Not verified in research; affects SEO/UX
|
||||
- **Recommendation:** Test Pagefind on demo site with Vietnamese search queries
|
||||
|
||||
3. **Dark mode color contrast:** Has tsuki been tested with accessibility tools (axe, Lighthouse a11y)?
|
||||
- **Recommendation:** Run Lighthouse on demo site; report scores in README
|
||||
|
||||
4. **OpenGraph image generation:** Should tsuki auto-generate OG images from cover, or require manual upload?
|
||||
- **Options:**
|
||||
- Manual (current approach if no cover shortcode)
|
||||
- Auto-generate from cover.image (requires Hugo image processing)
|
||||
- Dynamic generation (adds build complexity; not lightweight)
|
||||
- **Recommendation:** Use cover.image if present, fallback to site logo
|
||||
|
||||
5. **Theme.toml verification:** Confirm theme.toml exists and has correct format for gallery submission
|
||||
|
||||
6. **CI/CD pipeline:** Does `.github/workflows/pages.yml` need htmltest or Lighthouse?
|
||||
- **Current:** Builds + Pagefind; no validation
|
||||
- **Recommendation:** Add htmltest for link checking; Lighthouse optional
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Source References
|
||||
|
||||
### Official Hugo Documentation
|
||||
- [Hugo Official Docs](https://gohugo.io) — v0.146+, latest at v0.160+
|
||||
- [Hugo i18n Guide](https://gohugo.io/content-management/multilingual/)
|
||||
- [Hugo Theme Directory](https://themes.gohugo.io/)
|
||||
- [Hugo Theme Submission (hugoThemes Repo)](https://github.com/gohugoio/hugoThemes)
|
||||
|
||||
### Peer Theme Repositories & Blogs
|
||||
- [PaperMod (GitHub)](https://github.com/adityatelange/hugo-PaperMod) — Best blog theme 2025
|
||||
- [Blowfish (GitHub)](https://github.com/nunocoracao/blowfish) — Feature-rich, modern
|
||||
- [Stack (GitHub)](https://github.com/CaiJimmy/hugo-theme-stack) — Feature-complete
|
||||
- [Congo (GitHub)](https://github.com/jpanther/congo) — Tailwind-based
|
||||
- [Hugo Blox (Official Docs)](https://wowchemy.com) — Academic standard
|
||||
|
||||
### Featured Research Articles
|
||||
- [Rost Glukhov: Top Hugo Themes 2025](https://www.glukhov.org/post/2025/05/top-hugo-themes/)
|
||||
- [Pawel Grzybek: WebP and AVIF in Hugo](https://pawelgrzybek.com/webp-and-avif-images-on-a-hugo-website/)
|
||||
- [Federico Scodelaro: Hugo Content Adapters](https://federicoscodelaro.com/blog/2025-02-08-hugo-content-adapters/)
|
||||
- [Dr. Mowinckel: Hugo Modules vs. Submodules](https://drmowinckels.io/blog/2025/submodules/)
|
||||
- [BetterLink: 2025 Blog Framework Guide](https://eastondev.com/blog/en/posts/dev/20251123-blog-framework-guide/)
|
||||
|
||||
### SEO & Accessibility Standards
|
||||
- [SEO with Open Graph & Twitter Cards (Medium)](https://medium.com/@anzaloquin/supercharging-your-hugo-site-mastering-open-graph-twitter-cards-and-json-ld-metadata-fe75e5826b88)
|
||||
- [Hugo Structured Data Guide (DEV Community)](https://dev.to/pdwarkanath/adding-structured-data-to-your-hugo-site-58db)
|
||||
- [A11y Project Checklist](https://www.a11yproject.com/checklist/)
|
||||
|
||||
### CI/Testing Tools
|
||||
- [htmltest GitHub Wiki](https://github.com/wjdp/htmltest/wiki/Using-With-Hugo)
|
||||
- [Lighthouse CI (CSS-Tricks)](https://css-tricks.com/continuous-performance-analysis-with-lighthouse-ci-and-github-actions/)
|
||||
- [Visual Regression Testing for Hugo (James Kiefer)](https://jameskiefer.com/posts/visual-regression-testing-for-hugo-with-github-ci-and-backstopjs/)
|
||||
- [Pa11y Accessibility Testing](https://www.accesify.io/blog/accessibility-testing-automation-axe-pa11y-lighthouse-ci/)
|
||||
|
||||
---
|
||||
|
||||
## Summary Table: Quick Reference
|
||||
|
||||
| Category | Gap | Effort | Value | Post-0.1.0 Priority | Rationale |
|
||||
|----------|-----|--------|-------|-------------------|-----------|
|
||||
| **SEO** | JSON-LD + OpenGraph | S | HIGH | 1 | Google + social sharing |
|
||||
| **DX** | Reading time + word count | S | MED | 2 | Expected UX; 1 hour |
|
||||
| **DX** | Blockquote callouts | S | MED | 3 | Hugo 0.150+ native |
|
||||
| **DX** | Related posts | M | MED | 4 | Blog discovery |
|
||||
| **DX** | Multi-author | M | MED | 5 | Team sites |
|
||||
| **Visual** | Image lightbox | M | LOW | Defer | Explicitly deferred |
|
||||
| **Visual** | KaTeX math | M | LOW | Defer | Explicitly deferred |
|
||||
| **Visual** | Mermaid diagrams | S | LOW | Defer | Explicitly deferred |
|
||||
| **Dist.** | Gallery submission | S | HIGH | 6 | theme.toml + images |
|
||||
| **i18n** | Multilingual support | M | LOW | Defer | Not demand-driven |
|
||||
|
||||
---
|
||||
|
||||
**Status:** DONE
|
||||
**Concerns:** None critical. Tsuki's philosophy (lightweight, Vietnamese-first, zero build) is legitimately modern. Feature gaps are intentional design choices, not oversights. Recommend prioritizing SEO metadata (JSON-LD) + reading time in v0.2.0.
|
||||
Reference in New Issue
Block a user