refactor: consolidate TOC gate, inline single-call SEO partial, prune unused i18n

- Extract duplicated TOC predicate to _partials/toc-enabled.html
  (single.html + footer.html were both computing the same 6-line gate).
- Inline _partials/head/og-image.html into head/seo.html — single caller,
  the fallback chain is the contract; the partial wasn't earning its
  override surface.
- Drop 6 unused i18n keys: postedOn, tags, categories, archive, noResults,
  copyright. None referenced by any template (verified via grep). Also
  removed an accidental duplicate of the callout title block; vi.yml
  reorganized into thematic groups.

Behavior unchanged. Smoke (11 checks) + Hugo build clean. CSS bundle
unchanged at 3962 B gz.
This commit is contained in:
2026-05-09 09:45:57 +07:00
parent c260d2a9eb
commit 5d73a139ea
7 changed files with 86 additions and 94 deletions
+9
View File
@@ -4,6 +4,15 @@ All notable changes to tsuki will be documented here. Format follows [Keep a Cha
## [Unreleased]
### 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`.
+56 -74
View File
@@ -4,45 +4,54 @@
- id: wordCount
translation: "{{ .Count }} từ"
- id: calloutNote
translation: "Ghi chú"
- id: calloutTip
translation: "Mẹo"
- id: calloutImportant
translation: "Quan trọng"
- id: calloutWarning
translation: "Cảnh báo"
- id: calloutCaution
translation: "Thận trọng"
- id: postedOn
translation: "Đăng vào"
- id: updatedOn
translation: "Cập nhật"
- id: tags
translation: "Thẻ"
- id: categories
translation: "Chuyên mục"
- id: archive
translation: "Lưu trữ"
- id: search
translation: "Tìm kiếm"
- id: searchPlaceholder
translation: "Tìm bài viết..."
- id: searchNoScript
translation: "Tìm kiếm yêu cầu JavaScript."
- id: searchDisabled
translation: "Tìm kiếm hiện không khả dụng trên trang này."
- id: clearSearch
translation: "Xoá"
- id: loadMore
translation: "Xem thêm"
- id: filters
translation: "Bộ lọc"
- id: zeroResults
translation: "Không có kết quả cho [SEARCH_TERM]"
- id: oneResult
translation: "[COUNT] kết quả cho [SEARCH_TERM]"
- id: manyResults
translation: "[COUNT] kết quả cho [SEARCH_TERM]"
- id: searching
translation: "Đang tìm [SEARCH_TERM]..."
- id: searchSuggestion
translation: "Không có kết quả cho [SEARCH_TERM]. Thử các từ khoá: [DIFFERENT_TERMS]."
- id: altSearch
translation: "Không có kết quả cho [SEARCH_TERM]. Đang hiển thị kết quả cho [DIFFERENT_TERM]."
- id: recentPosts
translation: "Bài viết gần đây"
- id: relatedPosts
translation: "Bài viết liên quan"
- id: featuredProjects
translation: "Dự án nổi bật"
@@ -64,47 +73,20 @@
- id: archiveEmpty
translation: "Chưa có bài viết."
- id: toggleTheme
translation: "Đổi giao diện"
- id: poweredBy
translation: "Chạy bằng"
- id: noResults
translation: "Không có kết quả."
- id: searchNoScript
translation: "Tìm kiếm yêu cầu JavaScript."
- id: clearSearch
translation: "Xoá"
- id: loadMore
translation: "Xem thêm"
- id: filters
translation: "Bộ lọc"
- id: zeroResults
translation: "Không có kết quả cho [SEARCH_TERM]"
- id: manyResults
translation: "[COUNT] kết quả cho [SEARCH_TERM]"
- id: oneResult
translation: "[COUNT] kết quả cho [SEARCH_TERM]"
- id: searching
translation: "Đang tìm [SEARCH_TERM]..."
- id: copyright
translation: "© {{ .Year }} {{ .Author }}"
- id: month
translation: "Tháng {{ .Number }}"
- id: comments
translation: "Bình luận"
- id: month
translation: "Tháng {{ .Number }}"
- id: toggleTheme
translation: "Đổi giao diện"
- id: skipToContent
translation: "Đến nội dung chính"
- id: poweredBy
translation: "Chạy bằng"
- id: pageNotFound
translation: "Trang không tồn tại."
@@ -112,17 +94,17 @@
- id: backHome
translation: "Về trang chủ"
- id: searchSuggestion
translation: "Không có kết quả cho [SEARCH_TERM]. Thử các từ khoá: [DIFFERENT_TERMS]."
- id: calloutNote
translation: "Ghi chú"
- id: altSearch
translation: "Không có kết quả cho [SEARCH_TERM]. Đang hiển thị kết quả cho [DIFFERENT_TERM]."
- id: calloutTip
translation: "Mẹo"
- id: searchDisabled
translation: "Tìm kiếm hiện không khả dụng trên trang này."
- id: calloutImportant
translation: "Quan trọng"
- id: skipToContent
translation: "Đến nội dung chính"
- id: calloutWarning
translation: "Cảnh báo"
- id: relatedPosts
translation: "Bài viết liên quan"
- id: calloutCaution
translation: "Thận trọng"
+1 -4
View File
@@ -16,10 +16,7 @@
-}}
{{- $js := $jsFiles | resources.Concat "js/tsuki.bundle.js" | js.Build (dict "minify" true) | fingerprint -}}
<script type="module" src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}" crossorigin="anonymous"></script>
{{- $tocCfg := site.Params.toc | default dict -}}
{{- $tocEnabled := $tocCfg.enable | default true -}}
{{- $tocMin := $tocCfg.minWordCount | default 400 -}}
{{- if and (eq .Kind "page") $tocEnabled (gt .WordCount $tocMin) (ne .Params.toc false) }}
{{- if partial "toc-enabled.html" . }}
{{- $tocJs := resources.Get "js/toc-active.js" | js.Build (dict "minify" true) | fingerprint -}}
<script type="module" src="{{ $tocJs.RelPermalink }}" integrity="{{ $tocJs.Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
-11
View File
@@ -1,11 +0,0 @@
{{- /*
Resolves the OG/Twitter image URL for the current page.
Order: per-post `cover.image` → per-post `image` (legacy) → `params.og.fallbackImage` → `data/profile.yaml: avatar`.
Returns the bare site-relative path; caller applies `absURL`.
*/ -}}
{{- $img := "" -}}
{{- with .Params.cover -}}{{- $img = .image -}}{{- end -}}
{{- if not $img -}}{{- $img = .Params.image -}}{{- end -}}
{{- if not $img -}}{{- $img = site.Params.og.fallbackImage -}}{{- end -}}
{{- if not $img -}}{{- with site.Data.profile -}}{{- $img = .avatar -}}{{- end -}}{{- end -}}
{{- $img -}}
+6 -1
View File
@@ -7,7 +7,12 @@
{{- $rawDesc := .Description | default .Summary | default site.Params.description -}}
{{- $desc := "" -}}
{{- with $rawDesc -}}{{- $desc = . | plainify | truncate 200 -}}{{- end -}}
{{- $img := partial "head/og-image.html" . -}}
{{- /* OG/Twitter image: per-post `cover.image` → per-post `image` → site `og.fallbackImage` → `data/profile.yaml: avatar`. */ -}}
{{- $img := "" -}}
{{- with .Params.cover -}}{{- $img = .image -}}{{- end -}}
{{- if not $img -}}{{- $img = .Params.image -}}{{- end -}}
{{- if not $img -}}{{- $img = site.Params.og.fallbackImage -}}{{- end -}}
{{- if not $img -}}{{- with site.Data.profile -}}{{- $img = .avatar -}}{{- end -}}{{- end -}}
{{- /* OG locale: map common Hugo lang codes to OpenGraph IETF-with-region. */ -}}
{{- $ogLocale := site.Params.og.locale | default (index (dict "vi" "vi_VN" "en" "en_US" "ja" "ja_JP" "zh" "zh_CN" "ko" "ko_KR" "fr" "fr_FR" "de" "de_DE") site.Language.Lang) | default site.LanguageCode -}}
+13
View File
@@ -0,0 +1,13 @@
{{- /*
Single source of truth for whether to render the TOC for the current page.
Returns the literal string "true" when enabled, empty otherwise.
Callers: `single.html` (TOC partial), `footer.html` (toc-active.js loader).
Site config: `params.toc.enable` (default true), `params.toc.minWordCount` (default 400).
Per-page override: frontmatter `toc: false` always wins.
*/ -}}
{{- $cfg := site.Params.toc | default dict -}}
{{- $enabled := $cfg.enable | default true -}}
{{- $min := $cfg.minWordCount | default 400 -}}
{{- if and $enabled (eq .Kind "page") (gt .WordCount $min) (ne .Params.toc false) -}}
true
{{- end -}}
+1 -4
View File
@@ -25,10 +25,7 @@
</footer>
</article>
{{- $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) }}
{{- if partial "toc-enabled.html" . }}
{{ partial "toc.html" . }}
{{- end }}