feat: Pagefind search and Giscus comments (Phase 7)

- search/list.html: Pagefind UI container + module init with full vi
  i18n strings; loads /pagefind/pagefind-ui.{css,js} from generated dir
- search-button.html in header, gated by params.search.enable
- comments.html for Giscus, opt-in via params.comments.giscus.enable;
  reads repo/repoId/category/categoryId/mapping/theme/lang from params
- giscus-theme.js: MutationObserver on data-theme, postMessage iframe
  on toggle so Giscus theme stays in sync with site
- search.css: header-actions row, search button, Pagefind UI tokens
  mapped to tsuki design tokens
- comments.css: min-block-size on iframe to prevent layout shift
- exampleSite/hugo.yaml: commented giscus params block as docs
- i18n/vi.yml: Pagefind UI strings (clear, loadMore, zeroResults...)

Both features no-op when disabled. CSS bundle 3.5 KB gz (budget 15).
This commit is contained in:
tiennm99 committed 2026-05-07 22:03:32 +07:00
1 parent 52d9eab2b1
commit 8c82df90c9
15 files changed
+211 -3

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
/* tsuki: Giscus comments — reserve space to prevent layout shift on iframe load */
.comments {
margin-block-start: var(--tsuki-space-12);
padding-block-start: var(--tsuki-space-8);
border-block-start: 1px solid var(--tsuki-border);
}
.comments-heading {
font-size: var(--tsuki-fs-xl);
margin: 0 0 var(--tsuki-space-4);
}
.giscus,
.giscus-frame { min-block-size: 12rem; }
.giscus-frame { width: 100%; border: none; }
+43
View File
@@ -0,0 +1,43 @@
/* 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; }
.search-container { margin-block: var(--tsuki-space-6); min-block-size: 6rem; }
/* Pagefind UI tokens — see https://pagefind.app/docs/ui/#styling */
:root {
--pagefind-ui-primary: var(--tsuki-accent);
--pagefind-ui-text: var(--tsuki-fg);
--pagefind-ui-background: var(--tsuki-bg);
--pagefind-ui-border: var(--tsuki-border);
--pagefind-ui-tag: var(--tsuki-code-bg);
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: var(--tsuki-radius);
--pagefind-ui-font: var(--tsuki-font-sans);
}
.search-noscript { color: var(--tsuki-fg-muted); }
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"/>
<path d="m20 20-3.5-3.5"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

+16
View File
@@ -0,0 +1,16 @@
const giscus = document.querySelector("[data-giscus]");
if (giscus) {
const root = document.documentElement;
const send = () => {
const frame = document.querySelector("iframe.giscus-frame");
if (!frame) return;
const explicit = root.getAttribute("data-theme");
const theme = explicit
|| (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
frame.contentWindow.postMessage(
{ giscus: { setConfig: { theme } } },
"https://giscus.app"
);
};
new MutationObserver(send).observe(root, { attributes: true, attributeFilter: ["data-theme"] });
}
@@ -0,0 +1,4 @@
---
title: "Tìm kiếm"
description: "Tìm kiếm trong toàn bộ bài viết và ghi chú."
---
+19
View File
@@ -31,3 +31,22 @@ markup:
params:
description: Example site for the tsuki Hugo theme.
search:
enable: true
# Giscus comments — uncomment and fill in your repo IDs to enable.
# See https://giscus.app to generate values for your repo.
# comments:
# giscus:
# enable: true
# repo: tiennm99/tsuki
# repoId: R_kgDO...
# category: Announcements
# categoryId: DIC_kwDO...
# mapping: pathname # pathname | url | title | og:title
# strict: "0"
# reactionsEnabled: "1"
# inputPosition: bottom # top | bottom
# theme: preferred_color_scheme
# lang: vi
+24
View File
@@ -61,5 +61,29 @@
- 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 }}"
@@ -0,0 +1,24 @@
{{- $g := site.Params.comments.giscus -}}
{{- if and $g $g.enable -}}
{{- if ne .Params.comments false -}}
<section class="comments" aria-labelledby="comments-heading">
<h2 id="comments-heading" class="comments-heading">{{ i18n "comments" | default "Bình luận" }}</h2>
<div class="giscus" data-giscus></div>
<script src="https://giscus.app/client.js"
data-repo="{{ $g.repo }}"
data-repo-id="{{ $g.repoId }}"
data-category="{{ $g.category | default "Announcements" }}"
data-category-id="{{ $g.categoryId }}"
data-mapping="{{ $g.mapping | default "pathname" }}"
data-strict="{{ $g.strict | default "0" }}"
data-reactions-enabled="{{ $g.reactionsEnabled | default "1" }}"
data-emit-metadata="0"
data-input-position="{{ $g.inputPosition | default "bottom" }}"
data-theme="{{ $g.theme | default "preferred_color_scheme" }}"
data-lang="{{ $g.lang | default "vi" }}"
data-loading="lazy"
crossorigin="anonymous"
async></script>
</section>
{{- end -}}
{{- end -}}
@@ -20,3 +20,8 @@
{{- $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 }}
{{- $giscus := site.Params.comments.giscus -}}
{{- if and (eq .Kind "page") $giscus $giscus.enable (ne .Params.comments false) }}
{{- $giscusJs := resources.Get "js/giscus-theme.js" | js.Build (dict "minify" true) | fingerprint -}}
<script type="module" src="{{ $giscusJs.RelPermalink }}" integrity="{{ $giscusJs.Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
+2
View File
@@ -73,6 +73,8 @@
(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/view-transitions.css")
-}}
{{- $css := $cssFiles | resources.Concat "css/tsuki.bundle.css" | minify | fingerprint -}}
+8 -3
View File
@@ -3,7 +3,12 @@
{{- with site.Params.profile.name }}{{ . }}{{ else }}{{ site.Title }}{{ end -}}
</a>
{{ partial "nav.html" . }}
<button type="button" class="theme-toggle" aria-label="{{ i18n "toggleTheme" | default "Toggle theme" }}" data-theme-toggle hidden>
<span aria-hidden="true">◐</span>
</button>
<div class="site-header-actions">
{{- if site.Params.search.enable | default true }}
{{ partial "search-button.html" . }}
{{- end }}
<button type="button" class="theme-toggle" aria-label="{{ i18n "toggleTheme" | default "Toggle theme" }}" data-theme-toggle hidden>
<span aria-hidden="true">◐</span>
</button>
</div>
</header>
@@ -0,0 +1,3 @@
<a class="search-button" href="{{ "/search/" | relURL }}" aria-label="{{ i18n "search" | default "Tìm kiếm" }}">
{{ partial "icon.html" "search" }}
</a>
Whitespace-only changes.
+43
View File
@@ -0,0 +1,43 @@
{{ define "body_class" }}search{{ end }}
{{ define "head_extra" }}
<link rel="stylesheet" href="{{ "/pagefind/pagefind-ui.css" | relURL }}">
{{ end }}
{{ define "main" }}
<header class="list-header">
<h1 class="list-title">{{ .Title | default (i18n "search") }}</h1>
{{- with .Description }}<p class="list-description">{{ . }}</p>{{ end }}
</header>
<div id="search" class="search-container" data-pagefind-ui></div>
<noscript>
<p class="search-noscript">{{ i18n "searchNoScript" | default "Tìm kiếm yêu cầu JavaScript." }}</p>
</noscript>
{{ end }}
{{ define "scripts" }}
<script type="module">
import { PagefindUI } from "{{ "/pagefind/pagefind-ui.js" | relURL }}";
new PagefindUI({
element: "#search",
showSubResults: true,
showImages: false,
resetStyles: false,
translations: {
placeholder: {{ i18n "searchPlaceholder" | default "Tìm bài viết..." | jsonify }},
clear_search: {{ i18n "clearSearch" | default "Xoá" | jsonify }},
load_more: {{ i18n "loadMore" | default "Xem thêm" | jsonify }},
search_label: {{ i18n "search" | default "Tìm kiếm" | jsonify }},
filters_label: {{ i18n "filters" | default "Bộ lọc" | jsonify }},
zero_results: {{ i18n "zeroResults" | default "Không có kết quả cho [SEARCH_TERM]" | jsonify }},
many_results: {{ i18n "manyResults" | default "[COUNT] kết quả cho [SEARCH_TERM]" | jsonify }},
one_result: {{ i18n "oneResult" | default "[COUNT] kết quả cho [SEARCH_TERM]" | jsonify }},
alt_search: {{ i18n "altSearch" | default "Không có kết quả cho [SEARCH_TERM]. Đang hiển thị kết quả cho [DIFFERENT_TERM]." | jsonify }},
search_suggestion: {{ i18n "searchSuggestion" | default "Không có kết quả cho [SEARCH_TERM]. Thử các từ khoá: [DIFFERENT_TERMS]." | jsonify }},
searching: {{ i18n "searching" | default "Đang tìm [SEARCH_TERM]..." | jsonify }}
}
});
</script>
{{ end }}
+2
View File
@@ -28,4 +28,6 @@
{{- if and (gt .WordCount 400) (ne .Params.toc false) }}
{{ partial "toc.html" . }}
{{- end }}
{{ partial "comments.html" . }}
{{ end }}