mirror of
https://github.com/tiennm99/tsuki.git
synced 2026-08-02 22:23:12 +00:00
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/
104 lines
4.2 KiB
HTML
104 lines
4.2 KiB
HTML
{{- $title := cond .IsHome site.Title (printf "%s · %s" .Title site.Title) -}}
|
|
{{- $description := .Description | default .Summary | default site.Params.description | default site.Title | plainify -}}
|
|
|
|
<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 }}">
|
|
<link rel="canonical" href="{{ .Permalink }}">
|
|
|
|
{{/* Theme flash prevention — must run before <body> */}}
|
|
<script>
|
|
(function () {
|
|
try {
|
|
var stored = localStorage.getItem('tsuki-theme');
|
|
if (stored === 'dark' || stored === 'light') {
|
|
document.documentElement.setAttribute('data-theme', stored);
|
|
}
|
|
} catch (e) {}
|
|
})();
|
|
</script>
|
|
|
|
{{/* SEO: OpenGraph + Twitter Cards + JSON-LD Article */}}
|
|
{{ partial "head/seo.html" . }}
|
|
|
|
{{/* RSS autodiscovery */}}
|
|
{{- with .OutputFormats.Get "RSS" }}
|
|
<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 }}
|
|
{{- if gt .TotalPages 1 }}
|
|
{{- if .HasPrev }}<link rel="prev" href="{{ .Prev.URL }}">{{ end }}
|
|
{{- if .HasNext }}<link rel="next" href="{{ .Next.URL }}">{{ end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
|
|
{{/* Favicon */}}
|
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
|
|
|
{{/* 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/view-transitions.css")
|
|
-}}
|
|
{{- $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 }}
|