Files
tsuki/layouts/_partials/head.html
T
tiennm99 d1828c9590 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/
2026-05-15 18:55:47 +07:00

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 }}