- 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).
19 KiB
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.htmlnow assembles separate bundles per page kind.core.css(tokens + reset + typography + layout + components + view-transitions) loads everywhere;home.cssonly on home;single.css(toc + callouts + comments + single-extras) only on posts;archive.cssonly under/archives/;search.cssonly on/search/. Frees ~1 KB gz from non-post pages and unlocks Phase 3 feature growth without breaching budget. code-copy.jsgated to post pages — separate<script type=module>emit oneq .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 viaparams.breadcrumbs.enable. Renders Home › Section › Page trail above the post header and emits matchingBreadcrumbListJSON-LD for SEO. Off by default; demo enables it. - Prev/next post navigation (
_partials/prev-next.html) — opt-in viaparams.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 whenhugo.IsMultilingualis true; renders nothing on single-language sites. Marks active language witharia-current="page". hreflangalternate links —<link rel="alternate" hreflang="...">per.AllTranslations+x-default, gated onhugo.IsMultilingual.i18n/en.yml— full English starter (~50 keys) mirroringvi.yml. Theme builds withdefaultContentLanguage: enwithout missing-key fallbacks.linkToSectioni18n key +breadcrumb,breadcrumbHome,prevPost,nextPost,copyCode,copiedCodekeys 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-pressedSSR on theme-toggle — rendered in HTML before paint so axe/Lighthouse never see a missing-state toggle button.<details>styling insingle-extras.css— markdown collapse blocks now render with border + padding + dark-mode awareness./llm.txtoutput 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 overridelayouts/index.llmtxt.txt.- Speculation Rules opt-in —
<script type="speculationrules">emitted whenparams.prefetch.enable: true. Default rules prefetch internal links withmoderateeagerness, excluding/search/*. Override withparams.prefetch.rulesraw 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.ymlbuilds + 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.shcovering: 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-subtlelight-mode darkened from#888to#6b6b6bfor WCAG AA contrast (#888was 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#777unchanged (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 theopacity: 0.5compound that double-dimmed text below contrast. - Tap targets enlarged — header theme-toggle + search-button bumped from
2rem(32px) to2.5rem(40px); pagination links usemin-width: 2.75rem; min-height: 2.75rem(44px); footer links getpadding-block. Closer to Lighthouse's 48×48 audit target. Header buttons may still trip the strict 48px check; documented indocs/accessibility.md. <html lang>fallback chain — was hard-coded"vi"; nowsite.Language.LanguageCode | default site.Language.Lang | default "en". English-default sites no longer paintlang="vi".render-heading.htmlaria-labelmoved from hard-coded vi toi18n "linkToSection"(defaults to "Link to section" when key missing).code-copy.jsreadsdata-copy-code/data-copied-codefrom<html>(set bybaseof.htmlviai18nresolution). Addsdata-state="copied"for CSS styling polish.scripts/smoke-tests.shasserts 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.ymlnow strips the/tsuki/baseURL prefix so internal-link checks resolve againstexampleSite/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) — seedocs/customization.mdfor 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.htmldropped| safeHTMLon.Text. With Goldmarkunsafe: 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 vsrender-image.html. - Project links sanitize URL + add
noreferrer—layouts/_partials/home/projects.htmlpipesrepo/demoURLs throughsafeURLand emitsrel="noopener noreferrer" target="_blank". Closes inconsistency with therender-linkpolicy and prevents referer leakage to project destinations. - Comments gate tightened —
layouts/_partials/comments.htmlnow requiresrepo,repoId, ANDcategoryIdin addition toenable. Prevents broken Giscus iframes when partially configured. - htmltest GitHub Action pinned to commit SHA —
.github/workflows/pages.ymlno longer trackswjdp/htmltest-action@master; pinned to31be84afor supply-chain hygiene.
Fixed
seo.html$authorURLchain hardened — refactored fragilesite.Params.profile.url | default ...chain into nil-safewithblocks. Site builds no longer risknil.urltemplate error when consumers follow the documenteddata/profile.yaml-only configuration.- Header navigation respects sub-path deploys —
layouts/_partials/nav.htmlpipesMenu.URLthroughrelURL. Sites deploying under a sub-path (e.g., GitHub Pages/repo/) withurl:-form menu entries now route correctly. - CI Pagefind step simplified —
.github/workflows/pages.ymldropped the deadif 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 theparams.toc.{enable,minWordCount}+ per-pagetoc: falsepredicate, called fromsingle.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.htmlinlined intohead/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 ofseo.html. Override surface unchanged: replacehead/seo.htmlto customize.
Removed
- Six unused i18n keys —
postedOn,tags,categories,archive,noResults,copyrightwere defined invi.ymlbut never referenced by any template. Dropped along with one accidental duplicate of the callout title block.vi.ymlreorganized 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— explicitmodule.mountslist ensures the theme works under Hugo Module consumption with custom siteassetDir/layoutDiroverrides. 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.htmluses Hugo's built-in.Relatedindex keyed by tags + categories + date, weighted 100/60/10. Section silently disappears when no relations exist. Default 3 cards; tune viaparams.relatedPostsCount. Reuses the existingpost-card.htmlpartial. Requiresrelated:config in sitehugo.yaml(Hugo no-deep-merge rule); documented indocs/config.md. relatedPostsi18n 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 newi18n/vi.ymlkeys (calloutNoteetc.); override per-callout with> [!note] Custom title. CSS lives in dedicatedassets/css/callouts.cssbundled into the main pipeline. assets/css/callouts.css— added to the concat order inhead.html.- Optional word-count byline — gated on
params.showWordCount: true(default off). NewwordCounti18n key ({{ .Count }} từ). - Archetype expansion —
archetypes/default.mdnow includesdescription,cover.image, and pre-populatedtags/categoriesplaceholders. - JSON-LD Article schema on every post page —
headline,datePublished,dateModified,author(Person),publisher(Organization),image,description,keywords. Emits only whenIsPage && Kind == "page"; passesvalidator.schema.orgfor the demo posts. - OG/Twitter improvements —
og:localefrom site language,article:author, onearticle:tagper post tag,twitter:site+twitter:creatorfrom newparams.social.twitter, OG/Twitter description capped at 200 chars (Twitter limit) via rune-safetruncate. _partials/head/seo.html+_partials/head/og-image.html— extracted fromhead.htmlfor cleaner per-site overrides; OG image resolves throughcover.image→image→params.og.fallbackImage→params.profile.avatar.params.author,params.social.twitter,params.og.fallbackImagedocumented indocs/config.md.cover.imageper-post frontmatter as the preferred OG/Twitter cover key (legacyimagestill 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). Lastmodbyline — 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 getrel="noopener noreferrer"(audit N1)._markup/render-image.html— in-content markdown images automatically getloading="lazy" decoding="async"(audit N2).skipToContenti18n key.- Theme-contract notes in
docs/config.mddocumenting taxonomy plural names, Vi tag title bundles, and the full effect ofparams.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-existentparams.profile.name, silently falling back tosite.Title). Sites withdata/profile.yaml: nameset will see the correct name appear in<header>and footer for the first time.
Changed
<meta name="generator">no longer leaks Hugo version — emitstsukionly (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 —
categoriestaxonomy still routes under/categories/<slug>/but does not surface in post meta. Documented inline inmeta.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 fromtheme.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}— previouslysingle.htmlandfooter.htmlhardcoded a 400-word threshold and ignored theenableflag (audit C1). - Tag URLs survive taxonomy renames —
meta.htmlandsingle.htmlnow use.GetTerms "tags"+RelPermalinkinstead of hardcoded/tags/...(audit C3). params.search.enable: falseremoves the route, not just the header button.search/list.htmlis 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>1page (audit H4). - Code-copy buttons hidden when
navigator.clipboardunavailable (HTTP non-localhost) — no orphan "Lỗi" buttons on insecure-origin previews (audit H5). recent-posts.htmlquery bound once instead of evaluated twice (audit H8).Draftfilter dropped (already excluded byRegularPages).- Seven i18n keys added to
vi.yml: six audit-flagged keys (comments,month,pageNotFound,backHome,searchSuggestion,altSearch) plussearchDisabledfor 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/nextPagei18n keys. Pagination has always usedprev/next. docs/data-schemas.mddocuments the security implication ofmarkup.goldmark.renderer.unsafe: true+markdownifyonprofile.bio. Treatdata/profile.yamlas 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.htmlfor 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-asciifor clean URL fragments, line-height tuned for diacritic clearance - Dark mode — CSS custom properties,
prefers-color-schemedefault, persistent toggle viadata-theme+localStorage, no flash on first paint - View Transitions API —
@view-transition: navigation autoon 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 > 400andParams.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 viaMutationObserverondata-theme - i18n —
i18n/vi.ymlwith all UI strings (search, pagination, archive, comments, theme toggle) - CI —
.github/workflows/pages.ymlruns Hugo + Pagefind + uploads to GitHub Pages;npm ciresolves Pagefind frompackage.jsonlockfile (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,_shortcodesconvention). - 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 ownhugo.yaml. Seedocs/config.md. - Permalink token
:contentbasenamerecommended 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