From b2834749c9a8f3df0a7f018bf3e1382d0f9fe84f Mon Sep 17 00:00:00 2001
From: tiennm99
Date: Fri, 15 May 2026 18:55:47 +0700
Subject: [PATCH] =?UTF-8?q?feat:=20v0.3.0=20=E2=80=94=20Tier=20A=20parity,?=
=?UTF-8?q?=20per-kind=20CSS,=20llm.txt,=20a11y=20baseline?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Headline outcomes:
- Feature parity with Stack/PaperMod on Tier A polish: breadcrumbs
(+BreadcrumbList JSON-LD), prev/next post navigation (rel=prev/next),
language switcher UI (gated on hugo.IsMultilingual), code-copy button
polish, styling.
- Per-page-kind CSS bundles: core loads everywhere; home/single/archive/
search bundles load only where needed. Frees ~1 KB gz from non-post
pages. code-copy.js gated to post pages.
- Lighthouse-relevant network polish: Pagefind UI CSS preload-swap,
conditional preconnect to giscus.app on comment-enabled posts,
hreflang alternates on multilingual sites,
light/dark variants, aria-pressed SSR-rendered on theme-toggle.
- WCAG AA contrast: --tsuki-fg-subtle darkened to #6b6b6b (light); was
3.54:1 on bg, now 5:1. Pagination disabled uses --tsuki-fg-muted
without opacity compound. Header tap targets bumped to 40×40; pagination
to 44×44.
- i18n: full i18n/en.yml mirror (~50 keys); render-heading aria-label
i18n-driven via linkToSection key; new keys for breadcrumb*, prevPost,
nextPost, copyCode, copiedCode.
- Discovery: /llm.txt output format (llmstxt.org) on home; Speculation
Rules opt-in via params.prefetch.enable.
- fallback: site.Language.LanguageCode → Lang → "en"
(was hard-coded "vi").
Per-kind bundle gz sizes on demo: home 3673 / post 4167 / list 2897 /
archives 3363 / search 3179 B (all under 4200 B cap).
Plan: plans/260510-0144-tsuki-v0.3.0/
---
themes/tsuki/assets/css/components.css | 14 +-
themes/tsuki/assets/css/layout.css | 48 ++++++-
themes/tsuki/assets/css/search.css | 28 +---
themes/tsuki/assets/css/single-extras.css | 47 +++++++
themes/tsuki/assets/css/tokens.css | 2 +-
themes/tsuki/assets/js/code-copy.js | 14 +-
themes/tsuki/exampleSite/hugo.yaml | 15 ++
themes/tsuki/i18n/en.yml | 131 ++++++++++++++++++
themes/tsuki/i18n/vi.yml | 21 +++
.../tsuki/layouts/_markup/render-heading.html | 2 +-
.../tsuki/layouts/_partials/breadcrumbs.html | 34 +++++
themes/tsuki/layouts/_partials/footer.html | 12 +-
themes/tsuki/layouts/_partials/head.html | 61 ++++++--
themes/tsuki/layouts/_partials/header.html | 3 +-
.../layouts/_partials/lang-switcher.html | 30 ++++
themes/tsuki/layouts/_partials/prev-next.html | 27 ++++
themes/tsuki/layouts/baseof.html | 4 +-
themes/tsuki/layouts/index.llmtxt.txt | 30 ++++
themes/tsuki/layouts/search/list.html | 6 +-
themes/tsuki/layouts/single.html | 4 +
20 files changed, 472 insertions(+), 61 deletions(-)
create mode 100644 themes/tsuki/assets/css/single-extras.css
create mode 100644 themes/tsuki/i18n/en.yml
create mode 100644 themes/tsuki/layouts/_partials/breadcrumbs.html
create mode 100644 themes/tsuki/layouts/_partials/lang-switcher.html
create mode 100644 themes/tsuki/layouts/_partials/prev-next.html
create mode 100644 themes/tsuki/layouts/index.llmtxt.txt
diff --git a/themes/tsuki/assets/css/components.css b/themes/tsuki/assets/css/components.css
index dda9d49..650412a 100644
--- a/themes/tsuki/assets/css/components.css
+++ b/themes/tsuki/assets/css/components.css
@@ -155,10 +155,12 @@
}
.pagination a,
.pagination .pagination-current {
- display: inline-block;
- min-width: 2rem;
- padding: var(--tsuki-space-1) var(--tsuki-space-3);
- text-align: center;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 2.75rem;
+ min-height: 2.75rem;
+ padding-inline: var(--tsuki-space-3);
border-radius: var(--tsuki-radius);
}
.pagination .pagination-current {
@@ -167,8 +169,8 @@
font-weight: 600;
}
.pagination .disabled {
- color: var(--tsuki-fg-subtle);
- opacity: 0.5;
+ color: var(--tsuki-fg-muted);
+ cursor: not-allowed;
}
/* code copy button */
diff --git a/themes/tsuki/assets/css/layout.css b/themes/tsuki/assets/css/layout.css
index 97eea77..b8658ed 100644
--- a/themes/tsuki/assets/css/layout.css
+++ b/themes/tsuki/assets/css/layout.css
@@ -42,12 +42,55 @@ main,
.site-nav a:hover { color: var(--tsuki-fg); }
.site-nav a[aria-current="page"] { color: var(--tsuki-fg); font-weight: 550; }
+.lang-switcher {
+ display: flex;
+ gap: var(--tsuki-space-2);
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ font-size: var(--tsuki-fs-sm);
+}
+.lang-switcher a {
+ color: var(--tsuki-fg-muted);
+ padding: var(--tsuki-space-1) var(--tsuki-space-2);
+}
+.lang-switcher a:hover { color: var(--tsuki-fg); }
+.lang-switcher a[aria-current="page"] {
+ color: var(--tsuki-fg);
+ font-weight: 550;
+ text-decoration: none;
+}
+
+.site-header-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--tsuki-space-2);
+ margin-inline-start: var(--tsuki-space-3);
+}
+
+.search-button {
+ display: inline-grid;
+ place-items: center;
+ min-width: 2.5rem;
+ min-height: 2.5rem;
+ border: 1px solid var(--tsuki-border);
+ border-radius: 999px;
+ color: var(--tsuki-fg-muted);
+ transition: var(--tsuki-transition);
+}
+.search-button:hover {
+ color: var(--tsuki-fg);
+ border-color: var(--tsuki-fg-muted);
+ text-decoration: none;
+}
+.search-button svg { width: 1rem; height: 1rem; }
+
.theme-toggle {
background: transparent;
border: 1px solid var(--tsuki-border);
border-radius: 999px;
- width: 2rem;
- height: 2rem;
+ min-width: 2.5rem;
+ min-height: 2.5rem;
display: inline-grid;
place-items: center;
color: var(--tsuki-fg-muted);
@@ -79,6 +122,7 @@ body.list main { max-width: var(--tsuki-wide-width); }
gap: var(--tsuki-space-3);
}
.site-footer p { margin: 0; }
+.site-footer a { display: inline-block; padding-block: var(--tsuki-space-1); }
.list-header {
margin-block-end: var(--tsuki-space-8);
diff --git a/themes/tsuki/assets/css/search.css b/themes/tsuki/assets/css/search.css
index 1d01939..edcd2f6 100644
--- a/themes/tsuki/assets/css/search.css
+++ b/themes/tsuki/assets/css/search.css
@@ -1,30 +1,4 @@
-/* tsuki: search page — Pagefind UI overrides */
-
-.site-header-actions {
- display: flex;
- align-items: center;
- gap: var(--tsuki-space-2);
- margin-inline-start: var(--tsuki-space-3);
-}
-
-.search-button {
- display: inline-grid;
- place-items: center;
- width: 2rem;
- height: 2rem;
- border: 1px solid var(--tsuki-border);
- border-radius: 999px;
- color: var(--tsuki-fg-muted);
- transition: var(--tsuki-transition);
-}
-.search-button:hover {
- color: var(--tsuki-fg);
- border-color: var(--tsuki-fg-muted);
- text-decoration: none;
-}
-.search-button svg { width: 1rem; height: 1rem; }
-
-.site-nav { margin-inline-start: auto; }
+/* tsuki: search page — Pagefind UI container + token mapping */
.search-container { margin-block: var(--tsuki-space-6); min-block-size: 6rem; }
diff --git a/themes/tsuki/assets/css/single-extras.css b/themes/tsuki/assets/css/single-extras.css
new file mode 100644
index 0000000..406b125
--- /dev/null
+++ b/themes/tsuki/assets/css/single-extras.css
@@ -0,0 +1,47 @@
+/* tsuki: per-post extras — breadcrumbs, prev/next, details, code-copy polish, TOC narrow */
+
+.breadcrumbs {
+ margin-block-end: var(--tsuki-space-4);
+ font-size: var(--tsuki-fs-sm);
+ color: var(--tsuki-fg-muted);
+}
+.breadcrumbs ol {
+ list-style: none; padding: 0; margin: 0;
+ display: flex; flex-wrap: wrap; gap: var(--tsuki-space-2);
+}
+.breadcrumbs li + li::before { content: "›"; margin-inline-end: var(--tsuki-space-2); color: var(--tsuki-fg-subtle); }
+.breadcrumbs a { color: var(--tsuki-fg-muted); }
+.breadcrumbs a:hover { color: var(--tsuki-fg); }
+.breadcrumbs [aria-current="page"] { color: var(--tsuki-fg); }
+
+.prev-next {
+ display: grid; grid-template-columns: 1fr 1fr; gap: var(--tsuki-space-4);
+ margin-block-start: var(--tsuki-space-12);
+ padding-block-start: var(--tsuki-space-6);
+ border-block-start: 1px solid var(--tsuki-border);
+}
+.prev-next a {
+ display: flex; flex-direction: column; gap: var(--tsuki-space-1);
+ padding: var(--tsuki-space-3);
+ border: 1px solid var(--tsuki-border); border-radius: var(--tsuki-radius);
+ color: var(--tsuki-fg); text-decoration: none; min-block-size: 3rem;
+}
+.prev-next a:hover { border-color: var(--tsuki-fg-muted); }
+.prev-next-next { text-align: end; }
+.prev-next-label { font-size: var(--tsuki-fs-xs); color: var(--tsuki-fg-muted); }
+.prev-next-title { font-weight: 550; }
+@media (max-width: 32rem) {
+ .prev-next { grid-template-columns: 1fr; }
+ .prev-next-next { text-align: start; }
+}
+
+details {
+ border: 1px solid var(--tsuki-border); border-radius: var(--tsuki-radius);
+ padding: var(--tsuki-space-3) var(--tsuki-space-4);
+ margin-block: var(--tsuki-space-4);
+}
+details > summary { cursor: pointer; font-weight: 550; }
+details[open] > summary { margin-block-end: var(--tsuki-space-3); }
+
+.code-copy:hover { background: var(--tsuki-code-bg); }
+.code-copy[data-state="copied"] { opacity: 1; color: var(--tsuki-accent); border-color: var(--tsuki-accent); }
diff --git a/themes/tsuki/assets/css/tokens.css b/themes/tsuki/assets/css/tokens.css
index 4ccaf57..5611965 100644
--- a/themes/tsuki/assets/css/tokens.css
+++ b/themes/tsuki/assets/css/tokens.css
@@ -4,7 +4,7 @@
--tsuki-bg: #fbfaf7;
--tsuki-fg: #1a1a1a;
--tsuki-fg-muted: #666;
- --tsuki-fg-subtle: #888;
+ --tsuki-fg-subtle: #6b6b6b;
--tsuki-border: #e5e3dd;
--tsuki-accent: #4a6fa5;
--tsuki-accent-hover: #2c4a7a;
diff --git a/themes/tsuki/assets/js/code-copy.js b/themes/tsuki/assets/js/code-copy.js
index 9c7d20b..e7de689 100644
--- a/themes/tsuki/assets/js/code-copy.js
+++ b/themes/tsuki/assets/js/code-copy.js
@@ -1,6 +1,6 @@
-const COPY = "Sao chép";
-const COPIED = "Đã chép";
-const FAILED = "Lỗi";
+const dataset = document.documentElement.dataset;
+const COPY = dataset.copyCode || "Copy";
+const COPIED = dataset.copiedCode || "Copied";
if (navigator.clipboard) {
for (const pre of document.querySelectorAll("pre")) {
@@ -16,10 +16,14 @@ if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(code.innerText);
btn.textContent = COPIED;
+ btn.setAttribute("data-state", "copied");
} catch {
- btn.textContent = FAILED;
+ btn.textContent = COPY;
}
- setTimeout(() => { btn.textContent = COPY; }, 1500);
+ setTimeout(() => {
+ btn.textContent = COPY;
+ btn.removeAttribute("data-state");
+ }, 1500);
});
}
}
diff --git a/themes/tsuki/exampleSite/hugo.yaml b/themes/tsuki/exampleSite/hugo.yaml
index 1069cb5..15027b8 100644
--- a/themes/tsuki/exampleSite/hugo.yaml
+++ b/themes/tsuki/exampleSite/hugo.yaml
@@ -16,6 +16,15 @@ taxonomies:
permalinks:
post: /:year/:month/:day/:contentbasename/
+outputFormats:
+ LlmTxt:
+ mediaType: text/plain
+ baseName: llm
+ isPlainText: true
+
+outputs:
+ home: [HTML, RSS, LlmTxt]
+
markup:
goldmark:
renderer:
@@ -47,6 +56,12 @@ params:
search:
enable: true
+ breadcrumbs:
+ enable: true
+
+ prevNextNav:
+ enable: true
+
# Giscus comments — uncomment and fill in your repo IDs to enable.
# See https://giscus.app to generate values for your repo.
# comments:
diff --git a/themes/tsuki/i18n/en.yml b/themes/tsuki/i18n/en.yml
new file mode 100644
index 0000000..3e4af32
--- /dev/null
+++ b/themes/tsuki/i18n/en.yml
@@ -0,0 +1,131 @@
+- id: readingTime
+ translation: "{{ .Count }} min read"
+
+- id: wordCount
+ translation: "{{ .Count }} words"
+
+- id: updatedOn
+ translation: "Updated"
+
+- id: search
+ translation: "Search"
+
+- id: searchPlaceholder
+ translation: "Find posts..."
+
+- id: searchNoScript
+ translation: "Search requires JavaScript."
+
+- id: searchDisabled
+ translation: "Search is currently unavailable on this page."
+
+- id: clearSearch
+ translation: "Clear"
+
+- id: loadMore
+ translation: "Load more"
+
+- id: filters
+ translation: "Filters"
+
+- id: zeroResults
+ translation: "No results for [SEARCH_TERM]"
+
+- id: oneResult
+ translation: "[COUNT] result for [SEARCH_TERM]"
+
+- id: manyResults
+ translation: "[COUNT] results for [SEARCH_TERM]"
+
+- id: searching
+ translation: "Searching [SEARCH_TERM]..."
+
+- id: searchSuggestion
+ translation: "No results for [SEARCH_TERM]. Try: [DIFFERENT_TERMS]."
+
+- id: altSearch
+ translation: "No results for [SEARCH_TERM]. Showing results for [DIFFERENT_TERM]."
+
+- id: recentPosts
+ translation: "Recent posts"
+
+- id: relatedPosts
+ translation: "Related posts"
+
+- id: featuredProjects
+ translation: "Featured projects"
+
+- id: viewAll
+ translation: "View all"
+
+- id: tableOfContents
+ translation: "Table of contents"
+
+- id: prev
+ translation: "Previous"
+
+- id: next
+ translation: "Next"
+
+- id: posts
+ translation: "posts"
+
+- id: archiveEmpty
+ translation: "No posts yet."
+
+- id: month
+ translation: "{{ .Number | printf \"Month %d\" }}"
+
+- id: comments
+ translation: "Comments"
+
+- id: toggleTheme
+ translation: "Toggle theme"
+
+- id: skipToContent
+ translation: "Skip to content"
+
+- id: poweredBy
+ translation: "Powered by"
+
+- id: pageNotFound
+ translation: "Page not found."
+
+- id: backHome
+ translation: "Back home"
+
+- id: calloutNote
+ translation: "Note"
+
+- id: calloutTip
+ translation: "Tip"
+
+- id: calloutImportant
+ translation: "Important"
+
+- id: calloutWarning
+ translation: "Warning"
+
+- id: calloutCaution
+ translation: "Caution"
+
+- id: linkToSection
+ translation: "Link to section"
+
+- id: breadcrumbHome
+ translation: "Home"
+
+- id: breadcrumb
+ translation: "Breadcrumb"
+
+- id: prevPost
+ translation: "Previous post"
+
+- id: nextPost
+ translation: "Next post"
+
+- id: copyCode
+ translation: "Copy"
+
+- id: copiedCode
+ translation: "Copied"
diff --git a/themes/tsuki/i18n/vi.yml b/themes/tsuki/i18n/vi.yml
index ddb6c00..6db9182 100644
--- a/themes/tsuki/i18n/vi.yml
+++ b/themes/tsuki/i18n/vi.yml
@@ -108,3 +108,24 @@
- id: calloutCaution
translation: "Thận trọng"
+
+- id: linkToSection
+ translation: "Liên kết đến mục"
+
+- id: breadcrumbHome
+ translation: "Trang chủ"
+
+- id: breadcrumb
+ translation: "Đường dẫn"
+
+- id: prevPost
+ translation: "Bài trước"
+
+- id: nextPost
+ translation: "Bài tiếp"
+
+- id: copyCode
+ translation: "Sao chép"
+
+- id: copiedCode
+ translation: "Đã sao chép"
diff --git a/themes/tsuki/layouts/_markup/render-heading.html b/themes/tsuki/layouts/_markup/render-heading.html
index 7fa6b56..7f48155 100644
--- a/themes/tsuki/layouts/_markup/render-heading.html
+++ b/themes/tsuki/layouts/_markup/render-heading.html
@@ -1,6 +1,6 @@
{{- $level := .Level -}}
{{- $id := .Anchor -}}
- #
+ #
{{ .Text | safeHTML }}
diff --git a/themes/tsuki/layouts/_partials/breadcrumbs.html b/themes/tsuki/layouts/_partials/breadcrumbs.html
new file mode 100644
index 0000000..248e2c4
--- /dev/null
+++ b/themes/tsuki/layouts/_partials/breadcrumbs.html
@@ -0,0 +1,34 @@
+{{- /*
+ Breadcrumbs trail + BreadcrumbList schema.org JSON-LD.
+ Gated by params.breadcrumbs.enable (default off for backwards compat).
+ Skipped on home; renders Home → Section → Page for posts, Home → Section for section listings.
+*/ -}}
+{{- if site.Params.breadcrumbs.enable -}}
+{{- if not .IsHome -}}
+{{- $home := site.Home -}}
+{{- $section := .CurrentSection -}}
+{{- $isSection := and $section (ne $section.RelPermalink $home.RelPermalink) (ne $section.RelPermalink .RelPermalink) -}}
+{{- $isLeaf := and (eq .Kind "page") $isSection -}}
+{{- $homeName := i18n "breadcrumbHome" | default "Home" -}}
+
+
+ {{ $homeName }}
+ {{- if $isSection }}
+ {{ $section.LinkTitle | default $section.Title }}
+ {{- end }}
+ {{- if $isLeaf }}
+ {{ .LinkTitle | default .Title }}
+ {{- end }}
+
+
+{{- $items := slice (dict "@type" "ListItem" "position" 1 "name" $homeName "item" $home.Permalink) -}}
+{{- if $isSection -}}
+ {{- $items = $items | append (dict "@type" "ListItem" "position" 2 "name" ($section.LinkTitle | default $section.Title) "item" $section.Permalink) -}}
+{{- end -}}
+{{- if $isLeaf -}}
+ {{- $items = $items | append (dict "@type" "ListItem" "position" 3 "name" (.LinkTitle | default .Title) "item" .Permalink) -}}
+{{- end -}}
+{{- $bc := dict "@context" "https://schema.org" "@type" "BreadcrumbList" "itemListElement" $items -}}
+
+{{- end -}}
+{{- end -}}
diff --git a/themes/tsuki/layouts/_partials/footer.html b/themes/tsuki/layouts/_partials/footer.html
index 90e04de..b271561 100644
--- a/themes/tsuki/layouts/_partials/footer.html
+++ b/themes/tsuki/layouts/_partials/footer.html
@@ -10,12 +10,12 @@
-{{- $jsFiles := slice
- (resources.Get "js/theme-toggle.js")
- (resources.Get "js/code-copy.js")
--}}
-{{- $js := $jsFiles | resources.Concat "js/tsuki.bundle.js" | js.Build (dict "minify" true) | fingerprint -}}
-
+{{- $themeToggle := resources.Get "js/theme-toggle.js" | js.Build (dict "minify" true) | fingerprint -}}
+
+{{- if eq .Kind "page" }}
+{{- $codeCopy := resources.Get "js/code-copy.js" | js.Build (dict "minify" true) | fingerprint -}}
+
+{{- end }}
{{- if partial "toc-enabled.html" . }}
{{- $tocJs := resources.Get "js/toc-active.js" | js.Build (dict "minify" true) | fingerprint -}}
diff --git a/themes/tsuki/layouts/_partials/head.html b/themes/tsuki/layouts/_partials/head.html
index 7a56f2f..7bac76e 100644
--- a/themes/tsuki/layouts/_partials/head.html
+++ b/themes/tsuki/layouts/_partials/head.html
@@ -4,6 +4,8 @@
+
+
{{ $title }}
@@ -29,6 +31,14 @@
{{- end }}
+{{/* hreflang alternates — emit only on multilingual sites */}}
+{{- if hugo.IsMultilingual }}
+{{- range .AllTranslations }}
+
+{{- end }}
+{{- with site.Home }} {{- end }}
+{{- end }}
+
{{/* Pagination prev/next for SEO — only on paginated kinds with >1 page */}}
{{- if or (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") }}
{{- with .Paginator }}
@@ -43,20 +53,51 @@
-{{/* Theme CSS — concatenated, minified, fingerprinted (no build step required) */}}
-{{- $cssFiles := slice
+{{/* Speculation Rules — Chromium prefetch hints; gated, off by default */}}
+{{- $prefetch := site.Params.prefetch -}}
+{{- if and $prefetch $prefetch.enable }}
+
+{{- end }}
+
+{{/* Preconnect to Giscus on comment-enabled posts to shave third-party DNS+TLS handshake */}}
+{{- $giscusCfg := site.Params.comments.giscus -}}
+{{- if and (eq .Kind "page") $giscusCfg $giscusCfg.enable $giscusCfg.repo $giscusCfg.repoId $giscusCfg.categoryId (ne .Params.comments false) }}
+
+{{- end }}
+
+{{/* Theme CSS — per-page-kind bundles. Core loads everywhere; kind-specific bundles add only when needed. */}}
+{{- $coreFiles := slice
(resources.Get "css/tokens.css")
(resources.Get "css/reset.css")
(resources.Get "css/typography.css")
(resources.Get "css/layout.css")
(resources.Get "css/components.css")
- (resources.Get "css/home.css")
- (resources.Get "css/archive.css")
- (resources.Get "css/toc.css")
- (resources.Get "css/search.css")
- (resources.Get "css/comments.css")
- (resources.Get "css/callouts.css")
(resources.Get "css/view-transitions.css")
-}}
-{{- $css := $cssFiles | resources.Concat "css/tsuki.bundle.css" | minify | fingerprint -}}
-
+{{- $core := $coreFiles | resources.Concat "css/tsuki.core.css" | minify | fingerprint -}}
+
+
+{{- if .IsHome }}
+{{- $home := resources.Get "css/home.css" | minify | fingerprint -}}
+
+{{- end }}
+
+{{- if eq .Kind "page" }}
+{{- $singleFiles := slice
+ (resources.Get "css/toc.css")
+ (resources.Get "css/callouts.css")
+ (resources.Get "css/comments.css")
+ (resources.Get "css/single-extras.css")
+-}}
+{{- $single := $singleFiles | resources.Concat "css/tsuki.single.css" | minify | fingerprint -}}
+
+{{- end }}
+
+{{- if eq .Section "archives" }}
+{{- $archive := resources.Get "css/archive.css" | minify | fingerprint -}}
+
+{{- end }}
diff --git a/themes/tsuki/layouts/_partials/header.html b/themes/tsuki/layouts/_partials/header.html
index a5683f5..919d5a8 100644
--- a/themes/tsuki/layouts/_partials/header.html
+++ b/themes/tsuki/layouts/_partials/header.html
@@ -4,10 +4,11 @@
{{ partial "nav.html" . }}
diff --git a/themes/tsuki/layouts/_partials/lang-switcher.html b/themes/tsuki/layouts/_partials/lang-switcher.html
new file mode 100644
index 0000000..fd528db
--- /dev/null
+++ b/themes/tsuki/layouts/_partials/lang-switcher.html
@@ -0,0 +1,30 @@
+{{- /*
+ Inline language switcher. Auto-hidden on single-language sites.
+ Lists every configured language; marks the active one with aria-current="page".
+ Falls back to the localised home for languages where the current page has no translation.
+*/ -}}
+{{- if hugo.IsMultilingual -}}
+{{- $currentLang := .Language.Lang -}}
+{{- $page := . -}}
+
+ {{- range site.Languages -}}
+ {{- $lang := .Lang -}}
+ {{- /* Resolve the target URL: prefer the translated page; otherwise the language home */ -}}
+ {{- $href := "" -}}
+ {{- if eq $lang $currentLang -}}
+ {{- $href = $page.Permalink -}}
+ {{- else -}}
+ {{- range $page.AllTranslations -}}
+ {{- if eq .Language.Lang $lang -}}{{- $href = .Permalink -}}{{- end -}}
+ {{- end -}}
+ {{- if not $href -}}{{- $href = (printf "/%s/" $lang) | relLangURL -}}{{- end -}}
+ {{- end -}}
+
+
+ {{ .LanguageName | default (upper $lang) }}
+
+
+ {{- end -}}
+
+{{- end -}}
diff --git a/themes/tsuki/layouts/_partials/prev-next.html b/themes/tsuki/layouts/_partials/prev-next.html
new file mode 100644
index 0000000..43b60ec
--- /dev/null
+++ b/themes/tsuki/layouts/_partials/prev-next.html
@@ -0,0 +1,27 @@
+{{- /*
+ Prev/next post navigation within the same section.
+ Gated by params.prevNextNav.enable (default true).
+ Silently skipped when neither a prev nor a next exists.
+*/ -}}
+{{- if ne (site.Params.prevNextNav.enable | default true) false -}}
+{{- $prev := .PrevInSection -}}
+{{- $next := .NextInSection -}}
+{{- if or $prev $next -}}
+
+ {{- if $prev }}
+
+ ← {{ i18n "prevPost" | default "Previous post" }}
+ {{ $prev.LinkTitle | default $prev.Title }}
+
+ {{- else }}
+
+ {{- end }}
+ {{- if $next }}
+
+ {{ i18n "nextPost" | default "Next post" }} →
+ {{ $next.LinkTitle | default $next.Title }}
+
+ {{- end }}
+
+{{- end -}}
+{{- end -}}
diff --git a/themes/tsuki/layouts/baseof.html b/themes/tsuki/layouts/baseof.html
index 77cd36c..a906845 100644
--- a/themes/tsuki/layouts/baseof.html
+++ b/themes/tsuki/layouts/baseof.html
@@ -1,5 +1,7 @@
-
+
{{ partial "head.html" . }}
{{ block "head_extra" . }}{{ end }}
diff --git a/themes/tsuki/layouts/index.llmtxt.txt b/themes/tsuki/layouts/index.llmtxt.txt
new file mode 100644
index 0000000..aa4bebc
--- /dev/null
+++ b/themes/tsuki/layouts/index.llmtxt.txt
@@ -0,0 +1,30 @@
+{{- $profile := site.Data.profile -}}
+# {{ site.Title }}
+
+{{ with site.Params.description }}> {{ . }}{{ end }}
+
+{{- with $profile }}
+## About
+
+{{ with .name }}**{{ . }}**{{ with $profile.tagline }} — {{ . }}{{ end }}{{ end }}
+
+{{ with .bio }}{{ . }}{{ end }}
+{{- end }}
+
+## Recent posts
+
+{{- range first 10 (where site.RegularPages "Section" "post") }}
+- [{{ .Title }}]({{ .Permalink }}){{ with (.Description | default .Summary | plainify) }}: {{ . }}{{ end }}
+{{- end }}
+
+{{- with site.Data.projects.featured }}
+
+## Projects
+
+{{- range . }}
+- **{{ .title }}**{{ with .tagline }} — {{ . }}{{ end }}{{ with .repo }} — repo: {{ . }}{{ end }}{{ with .demo }} — demo: {{ . }}{{ end }}
+{{- end }}
+{{- end }}
+
+---
+Generated by [tsuki](https://github.com/tiennm99/tsuki) — Hugo blog + portfolio theme.
diff --git a/themes/tsuki/layouts/search/list.html b/themes/tsuki/layouts/search/list.html
index 2128dfc..2940254 100644
--- a/themes/tsuki/layouts/search/list.html
+++ b/themes/tsuki/layouts/search/list.html
@@ -1,8 +1,12 @@
{{ define "body_class" }}search{{ end }}
{{ define "head_extra" }}
+{{- $search := resources.Get "css/search.css" | minify | fingerprint -}}
+
{{- if site.Params.search.enable | default true }}
-
+{{- $pagefindCss := "/pagefind/pagefind-ui.css" | relURL }}
+
+
{{- end }}
{{ end }}
diff --git a/themes/tsuki/layouts/single.html b/themes/tsuki/layouts/single.html
index 26c5412..d1dbbc2 100644
--- a/themes/tsuki/layouts/single.html
+++ b/themes/tsuki/layouts/single.html
@@ -1,6 +1,8 @@
{{ define "body_class" }}post{{ end }}
{{ define "main" }}
+{{ partial "breadcrumbs.html" . }}
+