feat: core layouts and partials (Phase 3)

- baseof.html with named blocks (head_extra, body_class, main, scripts)
- single.html with data-pagefind-body wrapper
- list.html using post-card partial
- home.html placeholder (Phase 4 fills hero/projects/recent-posts)
- 404.html with vi i18n
- _partials: head (full meta + OG + Twitter Card + RSS autodiscovery + theme-flash script), header, nav, footer, meta, post-card, icon
- Sample post fixture in exampleSite to validate render

ci: bump GitHub Actions to Node 24-compatible versions

- actions/checkout v4 → v6
- actions/setup-node v4 → v6, node 20 → 22
- actions/upload-pages-artifact v3 → v5
- actions/deploy-pages v4 → v5
This commit is contained in:
2026-05-07 20:45:31 +07:00
parent 05d71218dc
commit 4f17588f19
16 changed files with 238 additions and 5 deletions
+5 -5
View File
@@ -20,7 +20,7 @@ jobs:
env:
HUGO_VERSION: 0.154.0
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
@@ -32,9 +32,9 @@ jobs:
extended: true
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
cache: npm
- name: Install dependencies
@@ -53,7 +53,7 @@ jobs:
fi
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5
with:
path: exampleSite/public
@@ -65,4 +65,4 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
@@ -0,0 +1,23 @@
---
title: "Mẫu bài viết đầu tiên"
date: 2026-05-07T19:00:00+07:00
draft: false
tags: ["hugo", "tsuki", "demo"]
categories: ["demo"]
description: "Bài viết mẫu cho tsuki theme."
---
Đây là một bài viết mẫu để kiểm tra layout của tsuki.
## Heading thứ hai
Một đoạn văn ngắn với **chữ in đậm***chữ nghiêng*`mã inline`.
```python
def hello():
print("Xin chào, tsuki!")
```
- Mục một
- Mục hai
- Mục ba
+9
View File
@@ -0,0 +1,9 @@
{{ define "body_class" }}error-page{{ end }}
{{ define "main" }}
<section class="error-404">
<h1>404</h1>
<p>{{ i18n "pageNotFound" | default "Trang không tồn tại." }}</p>
<p><a href="{{ "/" | relURL }}">{{ i18n "backHome" | default "Về trang chủ" }}</a></p>
</section>
{{ end }}
View File
+11
View File
@@ -0,0 +1,11 @@
<footer class="site-footer">
<p class="copyright">
© {{ now.Year }}
{{- with site.Params.profile.name }} {{ . }}{{ else }} {{ site.Title }}{{ end }}.
</p>
<p class="powered-by">
<span>{{ i18n "poweredBy" | default "Powered by" }}</span>
<a href="https://gohugo.io" rel="noopener">Hugo</a> +
<a href="https://github.com/tiennm99/tsuki" rel="noopener">tsuki</a>
</p>
</footer>
+68
View File
@@ -0,0 +1,68 @@
{{- $title := cond .IsHome site.Title (printf "%s · %s" .Title site.Title) -}}
{{- $description := .Description | default .Summary | default site.Params.description | default site.Title -}}
{{- $ogImage := .Params.image | default site.Params.profile.avatar -}}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="generator" content="Hugo {{ hugo.Version }} + tsuki">
<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>
{{/* OpenGraph */}}
<meta property="og:title" content="{{ $title }}">
<meta property="og:description" content="{{ $description }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:site_name" content="{{ site.Title }}">
{{- with $ogImage }}
<meta property="og:image" content="{{ . | absURL }}">
{{- end }}
{{- if and .IsPage .Date }}
<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
{{- with .Lastmod }}
<meta property="article:modified_time" content="{{ .Format "2006-01-02T15:04:05Z07:00" }}">
{{- end }}
{{- end }}
{{/* Twitter Card */}}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $title }}">
<meta name="twitter:description" content="{{ $description }}">
{{- with $ogImage }}
<meta name="twitter:image" content="{{ . | absURL }}">
{{- end }}
{{/* RSS autodiscovery */}}
{{- with .OutputFormats.Get "RSS" }}
<link rel="alternate" type="application/rss+xml" title="{{ site.Title }}" href="{{ .Permalink }}">
{{- end }}
{{/* Pagination prev/next for SEO — only on paginated kinds */}}
{{- if or .IsHome (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") }}
{{- with .Paginator }}
{{- if .HasPrev }}<link rel="prev" href="{{ .Prev.URL }}">{{ end }}
{{- if .HasNext }}<link rel="next" href="{{ .Next.URL }}">{{ end }}
{{- end }}
{{- end }}
{{/* Favicon */}}
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="32x32">
{{/* Theme CSS — placeholder; Phase 6 wires resources.Get */}}
{{/* {{ $css := resources.Get "css/tsuki.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}"> */}}
+9
View File
@@ -0,0 +1,9 @@
<header class="site-header">
<a class="site-title" href="{{ "/" | relURL }}">
{{- 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>
</header>
View File
+4
View File
@@ -0,0 +1,4 @@
{{- $name := . -}}
{{- with resources.Get (printf "icons/%s.svg" $name) -}}
{{ .Content | safeHTML }}
{{- end -}}
+20
View File
@@ -0,0 +1,20 @@
<div class="post-meta">
{{- with .Date }}
<time datetime="{{ .Format "2006-01-02" }}" class="post-date">
{{ . | time.Format ":date_long" }}
</time>
{{- end }}
{{- if gt .ReadingTime 0 }}
<span class="reading-time">
{{ i18n "readingTime" (dict "Count" .ReadingTime) | default (printf "%d min" .ReadingTime) }}
</span>
{{- end }}
{{- with .Params.tags }}
<span class="post-tags">
{{- range $i, $tag := . -}}
{{- if $i }}, {{ end -}}
<a href="{{ printf "/tags/%s/" (urlize $tag) | relURL }}">#{{ $tag }}</a>
{{- end -}}
</span>
{{- end }}
</div>
+10
View File
@@ -0,0 +1,10 @@
<nav class="site-nav" aria-label="Main">
<ul>
{{- range site.Menus.main }}
{{- $current := or ($.IsMenuCurrent "main" .) ($.HasMenuCurrent "main" .) }}
<li>
<a href="{{ .URL }}"{{ if $current }} aria-current="page"{{ end }}>{{ .Name }}</a>
</li>
{{- end }}
</ul>
</nav>
+14
View File
@@ -0,0 +1,14 @@
{{- $page := . -}}
<article class="post-card">
<h3 class="post-card-title">
<a href="{{ $page.RelPermalink }}">{{ $page.Title }}</a>
</h3>
{{- with $page.Date }}
<time datetime="{{ .Format "2006-01-02" }}" class="post-card-date">
{{ . | time.Format ":date_medium" }}
</time>
{{- end }}
{{- with ($page.Summary | plainify) }}
<p class="post-card-summary">{{ . | truncate 180 }}</p>
{{- end }}
</article>
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="{{ site.LanguageCode | default "vi" }}">
<head>
{{ partial "head.html" . }}
{{ block "head_extra" . }}{{ end }}
</head>
<body class="{{ block "body_class" . }}{{ end }}">
{{ partial "header.html" . }}
<main>
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
{{ block "scripts" . }}{{ end }}
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
{{ define "body_class" }}home{{ end }}
{{ define "main" }}
{{/* Phase 4 fills hero/projects/recent-posts here. Placeholder for now. */}}
<section class="home-placeholder">
<h1>{{ site.Title }}</h1>
{{- with site.Params.description }}<p>{{ . }}</p>{{ end }}
</section>
{{ end }}
+14
View File
@@ -0,0 +1,14 @@
{{ define "body_class" }}list{{ end }}
{{ define "main" }}
<header class="list-header">
<h1 class="list-title">{{ .Title }}</h1>
{{- with .Description }}<p class="list-description">{{ . }}</p>{{ end }}
</header>
<div class="post-list">
{{- range .Pages }}
{{ partial "post-card.html" . }}
{{- end }}
</div>
{{ end }}
+27
View File
@@ -0,0 +1,27 @@
{{ define "body_class" }}post{{ end }}
{{ define "main" }}
<article class="post">
<header class="post-header">
<h1 class="post-title">{{ .Title }}</h1>
{{ partial "meta.html" . }}
{{- with .Description }}
<p class="post-description">{{ . }}</p>
{{- end }}
</header>
<div class="post-content" data-pagefind-body>
{{ .Content }}
</div>
<footer class="post-footer">
{{- with .Params.tags }}
<ul class="post-tag-list">
{{- range . }}
<li><a href="{{ printf "/tags/%s/" (urlize .) | relURL }}">#{{ . }}</a></li>
{{- end }}
</ul>
{{- end }}
</footer>
</article>
{{ end }}