Files
tiennm99 12da47e9b2 docs: v0.3.0 sync — CHANGELOG, accessibility statement, plan close-out
- README: add a11y badge linking to docs/accessibility.md.
- CHANGELOG: full v0.3.0 entry under Unreleased (Added/Changed/Fixed/
  Deferred-to-v0.3.1). Documents per-kind CSS bundles, Tier A
  features, Lighthouse-relevant polish, contrast-token visual diff,
  tap-target sizing decisions, and Hugo CI matrix.
- docs/accessibility.md: new — WCAG 2.2 AA conformance summary,
  Lighthouse measurement instructions, projected baseline (TBD entries
  to fill after first production deploy), known limitations.
- docs/customization.md: cover-image override snippet (v0.4.0 native
  pipeline promise); document breadcrumbs/prev-next/lang-switcher opt-ins.
- plans/260510-0144-tsuki-v0.3.0/: mark all phases completed; note
  narrow-TOC <details> deferral to v0.3.1; record audit-pass scope
  additions integrated into Phases 2–6.
- plans/reports/: add researcher-260515-tsuki-vs-stack-papermod-feature
  -gap.md (verdict: feature parity on must-haves; Tier A gaps shipped
  this round) + code-reviewer-260515-lighthouse-80-baseline-audit.md
  (projected ≥80 on all 4 categories; ≥95 a11y achievable with
  shipped fixes).
2026-05-15 18:56:12 +07:00

19 KiB
Raw Permalink Blame History

Changelog

All notable changes to tsuki will be documented here. Format follows Keep a Changelog, versioning follows SemVer.

[Unreleased] — v0.3.0

Feature-parity round + Lighthouse ≥80 baseline. Six phases per plans/260510-0144-tsuki-v0.3.0/. Net: tsuki now ships breadcrumbs, prev/next post navigation, language switcher UI, llm.txt, English i18n bundle, and per-page-kind CSS bundles, while maintaining the ≤4 KB gz / page-kind / ≤1 KB gz JS hard caps.

Added

  • Per-page-kind CSS bundles — head.html now assembles separate bundles per page kind. core.css (tokens + reset + typography + layout + components + view-transitions) loads everywhere; home.css only on home; single.css (toc + callouts + comments + single-extras) only on posts; archive.css only under /archives/; search.css only on /search/. Frees ~1 KB gz from non-post pages and unlocks Phase 3 feature growth without breaching budget.
  • code-copy.js gated to post pages — separate <script type=module> emit on eq .Kind "page"; home/list/search no longer load the dead 0.5 KB gz.
  • Pagefind UI CSS preload-swap — /search/ now <link rel="preload" as="style" ... onload> the third-party stylesheet with <noscript> fallback. No longer render-blocking.
  • Giscus preconnect — <link rel="preconnect" href="https://giscus.app"> emitted only on post pages with comments fully configured. Shaves third-party DNS+TLS handshake.
  • Breadcrumbs partial (_partials/breadcrumbs.html) — opt-in via params.breadcrumbs.enable. Renders Home › Section › Page trail above the post header and emits matching BreadcrumbList JSON-LD for SEO. Off by default; demo enables it.
  • Prev/next post navigation (_partials/prev-next.html) — opt-in via params.prevNextNav.enable (default true). Two-card layout below the post, rel="prev" / rel="next" for SEO. Renders single-cell when only one neighbour exists.
  • Language switcher UI (_partials/lang-switcher.html) — auto-emits when hugo.IsMultilingual is true; renders nothing on single-language sites. Marks active language with aria-current="page".
  • hreflang alternate links — <link rel="alternate" hreflang="..."> per .AllTranslations + x-default, gated on hugo.IsMultilingual.
  • i18n/en.yml — full English starter (~50 keys) mirroring vi.yml. Theme builds with defaultContentLanguage: en without missing-key fallbacks.
  • linkToSection i18n key + breadcrumb, breadcrumbHome, prevPost, nextPost, copyCode, copiedCode keys added to both bundles.
  • <meta name="theme-color"> — two variants for light/dark (prefers-color-scheme). Mobile browsers theme their chrome to match the site.
  • aria-pressed SSR on theme-toggle — rendered in HTML before paint so axe/Lighthouse never see a missing-state toggle button.
  • <details> styling in single-extras.css — markdown collapse blocks now render with border + padding + dark-mode awareness.
  • /llm.txt output format — custom Hugo output format on the home; emits a plain-text summary of site + bio + recent posts + projects per llmstxt.org. Builds automatically; consumers can override layouts/index.llmtxt.txt.
  • Speculation Rules opt-in — <script type="speculationrules"> emitted when params.prefetch.enable: true. Default rules prefetch internal links with moderate eagerness, excluding /search/*. Override with params.prefetch.rules raw JSON.
  • docs/accessibility.md — WCAG 2.2 AA conformance statement, known limitations, Lighthouse measurement instructions, baseline table (TBD entries to fill on next production deploy).
  • README a11y badge linking to docs/accessibility.md.
  • Hugo CI matrix — pages.yml builds + smoke-tests on Hugo 0.146 (theme.toml floor) and 0.154 (pinned current). Deploy uploads only the current version.
  • 15+ new smoke-test assertions in scripts/smoke-tests.sh covering: theme-color meta, aria-pressed SSR, breadcrumbs + BreadcrumbList JSON-LD, prev/next nav with rel attrs, heading-anchor aria-label i18n, llm.txt presence, Speculation Rules absence by default, Pagefind preload-swap, Giscus-preconnect gating, per-page-kind CSS bundle routing, code-copy.js gating. Total checks: 32 (was 11).

Changed

  • --tsuki-fg-subtle light-mode darkened from #888 to #6b6b6b for WCAG AA contrast (#888 was 3.54:1 against #fbfaf7; fails 4.5:1 body-text threshold). Affects post-card date, pagination disabled state, heading anchor — they now render slightly darker. Dark-mode #777 unchanged (passes at 4.7:1). Visual diff is subtle; consumers theming the token are unaffected.
  • Pagination disabled state uses --tsuki-fg-muted + cursor: not-allowed. Dropped the opacity: 0.5 compound that double-dimmed text below contrast.
  • Tap targets enlarged — header theme-toggle + search-button bumped from 2rem (32px) to 2.5rem (40px); pagination links use min-width: 2.75rem; min-height: 2.75rem (44px); footer links get padding-block. Closer to Lighthouse's 48×48 audit target. Header buttons may still trip the strict 48px check; documented in docs/accessibility.md.
  • <html lang> fallback chain — was hard-coded "vi"; now site.Language.LanguageCode | default site.Language.Lang | default "en". English-default sites no longer paint lang="vi".
  • render-heading.html aria-label moved from hard-coded vi to i18n "linkToSection" (defaults to "Link to section" when key missing).
  • code-copy.js reads data-copy-code / data-copied-code from <html> (set by baseof.html via i18n resolution). Adds data-state="copied" for CSS styling polish.
  • scripts/smoke-tests.sh asserts per-kind CSS budget (each page kind ≤ 4200 B gz) instead of aggregate. CI workflow drops the now-redundant standalone budget step.

Fixed

  • CI htmltest URLSwap — .htmltest.yml now strips the /tsuki/ baseURL prefix so internal-link checks resolve against exampleSite/public/. Was failing on every CI run since v0.2.0; deploys did not propagate until this fix. No theme-side change.

Deferred to v0.3.1

  • Narrow-viewport TOC <details> collapse (UX ambiguity on wide-viewport summary toggle + CSS budget pressure)
  • Synthetic test posts for branch coverage (lastmod-test.md, no-tags-test.md)
  • Optional Lighthouse-CI workflow (manual measurement for now via docs/accessibility.md)
  • Default cover-image renderer pipeline (images.Resize + srcset/AVIF) — see docs/customization.md for override snippet; built-in support targeted for v0.4.0

[0.2.1] — 2026-05-10

Patch release. Closes 5 P1 correctness/security findings from the post-v0.2.0 review plus 2 CI hygiene items. No new features.

Security

  • Render-link no longer trusts inline HTML in link text — layouts/_markup/render-link.html dropped | safeHTML on .Text. With Goldmark unsafe: true, the prior pipeline allowed [<img onerror=...>](url) inner-text to execute. Side effect: italic/emphasis-in-link ([*x*](url)) now renders as plain text rather than <em>. Restore by overriding the render hook if needed. Self-XSS in single-author themes; consistency fix vs render-image.html.
  • Project links sanitize URL + add noreferrer — layouts/_partials/home/projects.html pipes repo/demo URLs through safeURL and emits rel="noopener noreferrer" target="_blank". Closes inconsistency with the render-link policy and prevents referer leakage to project destinations.
  • Comments gate tightened — layouts/_partials/comments.html now requires repo, repoId, AND categoryId in addition to enable. Prevents broken Giscus iframes when partially configured.
  • htmltest GitHub Action pinned to commit SHA — .github/workflows/pages.yml no longer tracks wjdp/htmltest-action@master; pinned to 31be84a for supply-chain hygiene.

Fixed

  • seo.html $authorURL chain hardened — refactored fragile site.Params.profile.url | default ... chain into nil-safe with blocks. Site builds no longer risk nil.url template error when consumers follow the documented data/profile.yaml-only configuration.
  • Header navigation respects sub-path deploys — layouts/_partials/nav.html pipes Menu.URL through relURL. Sites deploying under a sub-path (e.g., GitHub Pages /repo/) with url:-form menu entries now route correctly.
  • CI Pagefind step simplified — .github/workflows/pages.yml dropped the dead if find ... | head -1 | grep -q . guard. Layouts exist; the else branch never triggered.

[0.2.0] — 2026-05-09

SEO baseline, accessibility polish, author UX, discovery, distribution prep, CI hardening. See v0.2.0 tag for highlights.

Changed

  • TOC gate consolidated to _partials/toc-enabled.html — single source for the params.toc.{enable,minWordCount} + per-page toc: false predicate, called from single.html (TOC partial) and _partials/footer.html (toc-active.js loader). Was duplicated 6-line logic in two sites; now one partial. No behavior change.
  • _partials/head/og-image.html inlined into head/seo.html — single-call partial removed; the OG image fallback chain (cover.image → image → params.og.fallbackImage → data/profile.yaml: avatar) is now expressed once at the top of seo.html. Override surface unchanged: replace head/seo.html to customize.

Removed

  • Six unused i18n keys — postedOn, tags, categories, archive, noResults, copyright were defined in vi.yml but never referenced by any template. Dropped along with one accidental duplicate of the callout title block. vi.yml reorganized into thematic groups (search/feeds/UI/callouts).

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.0 — 2026-05-07

Initial public release.

Added

  • Layouts — baseof, home (hero + projects + recent), single, list, archives, taxonomy/term, _default/taxonomy, post/list, search/list, 404
  • Partials — head, header, footer, nav, meta, post-card, pagination, archive-group, toc, comments, search-button, icon, home/{hero,projects,recent-posts}
  • Render hooks — _markup/render-heading.html for cosmetic anchor links
  • Asset pipeline — resources.Concat | minify | fingerprint. No SCSS, no PostCSS, no Node bundler. CSS bundle ≤ 4 KB gz, JS ≤ 1 KB gz
  • Vietnamese-first — time.Format ":date_long" localized dates, autoHeadingIDType: github-ascii for clean URL fragments, line-height tuned for diacritic clearance
  • Dark mode — CSS custom properties, prefers-color-scheme default, persistent toggle via data-theme + localStorage, no flash on first paint
  • View Transitions API — @view-transition: navigation auto on supported browsers, motion-safe @keyframes, falls back to instant nav
  • Table of contents — sticky on wide viewports, framed block on narrow, IntersectionObserver-driven active heading; gated by WordCount > 400 and Params.toc != false
  • Search — Pagefind, indexed at build time in CI, UI mounted at /search/ with full Vi i18n
  • Comments — Giscus, opt-in via params.comments.giscus.enable, theme-sync via MutationObserver on data-theme
  • i18n — i18n/vi.yml with all UI strings (search, pagination, archive, comments, theme toggle)
  • CI — .github/workflows/pages.yml runs Hugo + Pagefind + uploads to GitHub Pages; npm ci resolves Pagefind from package.json lockfile (Dependabot-friendly)
  • ExampleSite — 5 vi-language demo posts, 4 featured projects, profile data, archive, search, all routes verified

Configuration notes

  • Hugo 0.146+ required (uses _partials, _markup, _shortcodes convention).
  • Theme defaults are documentation only — Hugo does not deep-merge nested config (pagination, permalinks, taxonomies, markup) from themes. Consumer sites must set these in their own hugo.yaml. See docs/config.md.
  • Permalink token :contentbasename recommended for clean ASCII URLs from leaf bundles.

Deferred to post-0.1.0

  • Self-hosted Be Vietnam Pro / Inter woff2 files (system fonts work fine; user adds woff2 via static/fonts/ if desired)
  • KaTeX math support
  • Tag cloud widget
  • Image gallery shortcode