mirror of
https://github.com/tiennm99/blog.git
synced 2026-10-11 12:09:02 +00:00
feat: v0.3.0 — Tier A parity, per-kind CSS, llm.txt, a11y baseline
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, <details> 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, <meta name=theme-color> 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. - <html lang> 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/
This commit is contained in:
1 parent
2a6cbed44a
commit
b2834749c9
20 files changed
+472
-61
No files matched your search
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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); }
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -1,6 +1,6 @@
|
||||
{{- $level := .Level -}}
|
||||
{{- $id := .Anchor -}}
|
||||
<h{{ $level }} id="{{ $id }}">
|
||||
<a class="heading-anchor" href="#{{ $id }}" aria-label="Liên kết đến phần này">#</a>
|
||||
<a class="heading-anchor" href="#{{ $id }}" aria-label="{{ i18n "linkToSection" | default "Link to section" }}">#</a>
|
||||
{{ .Text | safeHTML }}
|
||||
</h{{ $level }}>
|
||||
@@ -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" -}}
|
||||
<nav class="breadcrumbs" aria-label="{{ i18n "breadcrumb" | default "Breadcrumb" }}">
|
||||
<ol>
|
||||
<li><a href="{{ $home.Permalink }}">{{ $homeName }}</a></li>
|
||||
{{- if $isSection }}
|
||||
<li><a href="{{ $section.Permalink }}">{{ $section.LinkTitle | default $section.Title }}</a></li>
|
||||
{{- end }}
|
||||
{{- if $isLeaf }}
|
||||
<li aria-current="page">{{ .LinkTitle | default .Title }}</li>
|
||||
{{- end }}
|
||||
</ol>
|
||||
</nav>
|
||||
{{- $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 -}}
|
||||
<script type="application/ld+json">{{ $bc | jsonify | safeJS }}</script>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -10,12 +10,12 @@
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
{{- $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 -}}
|
||||
<script type="module" src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}" crossorigin="anonymous"></script>
|
||||
{{- $themeToggle := resources.Get "js/theme-toggle.js" | js.Build (dict "minify" true) | fingerprint -}}
|
||||
<script type="module" src="{{ $themeToggle.RelPermalink }}" integrity="{{ $themeToggle.Data.Integrity }}" crossorigin="anonymous"></script>
|
||||
{{- if eq .Kind "page" }}
|
||||
{{- $codeCopy := resources.Get "js/code-copy.js" | js.Build (dict "minify" true) | fingerprint -}}
|
||||
<script type="module" src="{{ $codeCopy.RelPermalink }}" integrity="{{ $codeCopy.Data.Integrity }}" crossorigin="anonymous"></script>
|
||||
{{- end }}
|
||||
{{- 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>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="generator" content="tsuki">
|
||||
<meta name="theme-color" content="#fbfaf7" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#14151a" media="(prefers-color-scheme: dark)">
|
||||
|
||||
<title>{{ $title }}</title>
|
||||
<meta name="description" content="{{ $description }}">
|
||||
@@ -29,6 +31,14 @@
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ site.Title }}" href="{{ .Permalink }}">
|
||||
{{- end }}
|
||||
|
||||
{{/* hreflang alternates — emit only on multilingual sites */}}
|
||||
{{- if hugo.IsMultilingual }}
|
||||
{{- range .AllTranslations }}
|
||||
<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
|
||||
{{- end }}
|
||||
{{- with site.Home }}<link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">{{- 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 @@
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
||||
|
||||
{{/* 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 }}
|
||||
<script type="speculationrules">
|
||||
{{- with $prefetch.rules -}}{{ . | safeJS }}{{- else -}}
|
||||
{"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":"/search/*"}}]},"eagerness":"moderate"}]}
|
||||
{{- end -}}
|
||||
</script>
|
||||
{{- 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) }}
|
||||
<link rel="preconnect" href="https://giscus.app" crossorigin>
|
||||
{{- 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 -}}
|
||||
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}" crossorigin="anonymous">
|
||||
{{- $core := $coreFiles | resources.Concat "css/tsuki.core.css" | minify | fingerprint -}}
|
||||
<link rel="stylesheet" href="{{ $core.RelPermalink }}" integrity="{{ $core.Data.Integrity }}" crossorigin="anonymous">
|
||||
|
||||
{{- if .IsHome }}
|
||||
{{- $home := resources.Get "css/home.css" | minify | fingerprint -}}
|
||||
<link rel="stylesheet" href="{{ $home.RelPermalink }}" integrity="{{ $home.Data.Integrity }}" crossorigin="anonymous">
|
||||
{{- 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 -}}
|
||||
<link rel="stylesheet" href="{{ $single.RelPermalink }}" integrity="{{ $single.Data.Integrity }}" crossorigin="anonymous">
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Section "archives" }}
|
||||
{{- $archive := resources.Get "css/archive.css" | minify | fingerprint -}}
|
||||
<link rel="stylesheet" href="{{ $archive.RelPermalink }}" integrity="{{ $archive.Data.Integrity }}" crossorigin="anonymous">
|
||||
{{- end }}
|
||||
@@ -4,10 +4,11 @@
|
||||
</a>
|
||||
{{ partial "nav.html" . }}
|
||||
<div class="site-header-actions">
|
||||
{{ partial "lang-switcher.html" . }}
|
||||
{{- 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>
|
||||
<button type="button" class="theme-toggle" aria-label="{{ i18n "toggleTheme" | default "Toggle theme" }}" aria-pressed="false" data-theme-toggle hidden>
|
||||
<span aria-hidden="true">◐</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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 := . -}}
|
||||
<ul class="lang-switcher" aria-label="Languages">
|
||||
{{- 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 -}}
|
||||
<li>
|
||||
<a href="{{ $href }}" lang="{{ $lang }}"
|
||||
{{- if eq $lang $currentLang }} aria-current="page"{{ end }}>
|
||||
{{ .LanguageName | default (upper $lang) }}
|
||||
</a>
|
||||
</li>
|
||||
{{- end -}}
|
||||
</ul>
|
||||
{{- end -}}
|
||||
@@ -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 -}}
|
||||
<nav class="prev-next" aria-label="{{ i18n "relatedPosts" }}">
|
||||
{{- if $prev }}
|
||||
<a class="prev-next-prev" rel="prev" href="{{ $prev.Permalink }}">
|
||||
<span class="prev-next-label">← {{ i18n "prevPost" | default "Previous post" }}</span>
|
||||
<span class="prev-next-title">{{ $prev.LinkTitle | default $prev.Title }}</span>
|
||||
</a>
|
||||
{{- else }}
|
||||
<span class="prev-next-spacer" aria-hidden="true"></span>
|
||||
{{- end }}
|
||||
{{- if $next }}
|
||||
<a class="prev-next-next" rel="next" href="{{ $next.Permalink }}">
|
||||
<span class="prev-next-label">{{ i18n "nextPost" | default "Next post" }} →</span>
|
||||
<span class="prev-next-title">{{ $next.LinkTitle | default $next.Title }}</span>
|
||||
</a>
|
||||
{{- end }}
|
||||
</nav>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -1,5 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ site.LanguageCode | default "vi" }}">
|
||||
<html lang="{{ site.Language.LanguageCode | default site.Language.Lang | default "en" }}"
|
||||
data-copy-code="{{ i18n "copyCode" | default "Copy" }}"
|
||||
data-copied-code="{{ i18n "copiedCode" | default "Copied" }}">
|
||||
<head>
|
||||
{{ partial "head.html" . }}
|
||||
{{ block "head_extra" . }}{{ end }}
|
||||
|
||||
@@ -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.
|
||||
@@ -1,8 +1,12 @@
|
||||
{{ define "body_class" }}search{{ end }}
|
||||
|
||||
{{ define "head_extra" }}
|
||||
{{- $search := resources.Get "css/search.css" | minify | fingerprint -}}
|
||||
<link rel="stylesheet" href="{{ $search.RelPermalink }}" integrity="{{ $search.Data.Integrity }}" crossorigin="anonymous">
|
||||
{{- if site.Params.search.enable | default true }}
|
||||
<link rel="stylesheet" href="{{ "/pagefind/pagefind-ui.css" | relURL }}">
|
||||
{{- $pagefindCss := "/pagefind/pagefind-ui.css" | relURL }}
|
||||
<link rel="preload" as="style" href="{{ $pagefindCss }}" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="{{ $pagefindCss }}"></noscript>
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{{ define "body_class" }}post{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
{{ partial "breadcrumbs.html" . }}
|
||||
|
||||
<article class="post">
|
||||
<header class="post-header">
|
||||
<h1 class="post-title">{{ .Title }}</h1>
|
||||
@@ -29,6 +31,8 @@
|
||||
{{ partial "toc.html" . }}
|
||||
{{- end }}
|
||||
|
||||
{{ partial "prev-next.html" . }}
|
||||
|
||||
{{ partial "related-posts.html" . }}
|
||||
|
||||
{{ partial "comments.html" . }}
|
||||
|
||||
Reference in new issue
Block a user