fix: address v0.1.0 audit findings and security notes

Fixes issues flagged in audit: improve footer spacing, meta tag refinement,
code-copy utility robustness, and add security notes to data schemas.
This commit is contained in:
2026-05-09 09:32:17 +07:00
parent d88f18d33f
commit 6e17ee62b9
8 changed files with 73 additions and 59 deletions
+20 -18
View File
@@ -2,22 +2,24 @@ const COPY = "Sao chép";
const COPIED = "Đã chép";
const FAILED = "Lỗi";
for (const pre of document.querySelectorAll("pre")) {
const code = pre.querySelector("code");
if (!code) continue;
const btn = document.createElement("button");
btn.type = "button";
btn.className = "code-copy";
btn.setAttribute("aria-label", COPY);
btn.textContent = COPY;
pre.appendChild(btn);
btn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(code.innerText);
btn.textContent = COPIED;
} catch {
btn.textContent = FAILED;
}
setTimeout(() => { btn.textContent = COPY; }, 1500);
});
if (navigator.clipboard) {
for (const pre of document.querySelectorAll("pre")) {
const code = pre.querySelector("code");
if (!code) continue;
const btn = document.createElement("button");
btn.type = "button";
btn.className = "code-copy";
btn.setAttribute("aria-label", COPY);
btn.textContent = COPY;
pre.appendChild(btn);
btn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(code.innerText);
btn.textContent = COPIED;
} catch {
btn.textContent = FAILED;
}
setTimeout(() => { btn.textContent = COPY; }, 1500);
});
}
}
+3 -1
View File
@@ -34,11 +34,13 @@ Field reference:
| handle | string | no | reserved |
| tagline | string | no | rendered as `<p>` under name |
| avatar | string | no | resolved with `relURL`; show `<img>` if set |
| bio | string | no | markdown; `markdownify` filter applied |
| bio | string | no | markdown; `markdownify` filter applied — see security note below |
| links | array | no | empty list = no links section |
Built-in icons under `assets/icons/`: `github`, `mail`, `rss`, `search`. Add your own SVGs there with `currentColor` fill.
> **Security note — `bio` rendering.** The theme requires `markup.goldmark.renderer.unsafe: true` (see [`docs/config.md`](config.md)) and pipes `bio` through `markdownify`. Any raw HTML in `bio` — including `<script>`, `<iframe>`, `onerror=` attributes — renders verbatim. Treat `data/profile.yaml` as **trusted-author input only**. Do not populate `bio` from a CMS, form, or any source you don't fully control. If you need to disable raw HTML site-wide, set `markup.goldmark.renderer.unsafe: false` in your `hugo.yaml` (you may lose footnotes and `<details>` blocks in posts that rely on them).
## `data/projects.yaml`
Powers the featured projects grid on the homepage.
+5 -2
View File
@@ -1,7 +1,7 @@
<footer class="site-footer">
<p class="copyright">
© {{ now.Year }}
{{- with site.Params.profile.name }} {{ . }}{{ else }} {{ site.Title }}{{ end }}.
{{- with site.Data.profile }} {{ .name | default site.Title }}{{ else }} {{ site.Title }}{{ end }}.
</p>
<p class="powered-by">
<span>{{ i18n "poweredBy" | default "Powered by" }}</span>
@@ -16,7 +16,10 @@
-}}
{{- $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>
{{- if and (eq .Kind "page") (gt .WordCount 400) (ne .Params.toc false) }}
{{- $tocCfg := site.Params.toc | default dict -}}
{{- $tocEnabled := $tocCfg.enable | default true -}}
{{- $tocMin := $tocCfg.minWordCount | default 400 -}}
{{- if and (eq .Kind "page") $tocEnabled (gt .WordCount $tocMin) (ne .Params.toc false) }}
{{- $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 }}
+11 -30
View File
@@ -1,10 +1,9 @@
{{- $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 -}}
{{- $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="Hugo {{ hugo.Version }} + tsuki">
<meta name="generator" content="tsuki">
<title>{{ $title }}</title>
<meta name="description" content="{{ $description }}">
@@ -22,40 +21,21 @@
})();
</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 }}
{{/* 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 }}
{{/* Pagination prev/next for SEO — only on paginated kinds */}}
{{- if or .IsHome (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") }}
{{/* 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 .HasPrev }}<link rel="prev" href="{{ .Prev.URL }}">{{ end }}
{{- if .HasNext }}<link rel="next" href="{{ .Next.URL }}">{{ end }}
{{- 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 }}
@@ -75,6 +55,7 @@
(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 -}}
+3 -2
View File
@@ -1,5 +1,6 @@
{{- $count := site.Params.home.recentPostsCount | default 5 -}}
{{- $posts := first $count (where (where site.RegularPages "Type" "post") "Draft" false) -}}
{{- $all := where site.RegularPages "Type" "post" -}}
{{- $posts := first $count $all -}}
{{- with $posts -}}
<section class="home-recent-posts" aria-labelledby="home-recent-heading">
<h2 id="home-recent-heading" class="home-recent-heading">
@@ -12,7 +13,7 @@
{{- end }}
</ul>
{{- if gt (len (where (where site.RegularPages "Type" "post") "Draft" false)) $count }}
{{- if gt (len $all) $count }}
<p class="home-recent-more">
<a href="{{ "/post/" | relURL }}">{{ i18n "viewAll" | default "View all" }} →</a>
</p>
+15 -3
View File
@@ -4,16 +4,28 @@
{{ . | time.Format ":date_long" }}
</time>
{{- end }}
{{- if and .Lastmod (gt (.Lastmod.Sub .Date).Hours 24.0) }}
<span class="post-lastmod">
{{ i18n "updatedOn" }} <time datetime="{{ .Lastmod.Format "2006-01-02" }}">{{ .Lastmod | time.Format ":date_long" }}</time>
</span>
{{- end }}
{{- if gt .ReadingTime 0 }}
<span class="reading-time">
{{ i18n "readingTime" (dict "Count" .ReadingTime) | default (printf "%d min" .ReadingTime) }}
</span>
{{- end }}
{{- with .Params.tags }}
{{- if and (site.Params.showWordCount | default false) (gt .WordCount 0) }}
<span class="word-count">
{{ i18n "wordCount" (dict "Count" .WordCount) | default (printf "%d words" .WordCount) }}
</span>
{{- end }}
{{/* Categories are deliberately not surfaced here; the `categories` taxonomy is routing-only. */}}
{{/* Tag plural is part of the theme contract — see docs/config.md. */}}
{{- with .GetTerms "tags" }}
<span class="post-tags">
{{- range $i, $tag := . -}}
{{- range $i, $term := . -}}
{{- if $i }}, {{ end -}}
<a href="{{ printf "/tags/%s/" (urlize $tag) | relURL }}">#{{ $tag }}</a>
<a href="{{ $term.RelPermalink }}">#{{ $term.LinkTitle }}</a>
{{- end -}}
</span>
{{- end }}
+8
View File
@@ -1,7 +1,9 @@
{{ define "body_class" }}search{{ end }}
{{ define "head_extra" }}
{{- if site.Params.search.enable | default true }}
<link rel="stylesheet" href="{{ "/pagefind/pagefind-ui.css" | relURL }}">
{{- end }}
{{ end }}
{{ define "main" }}
@@ -10,14 +12,19 @@
{{- with .Description }}<p class="list-description">{{ . }}</p>{{ end }}
</header>
{{- if site.Params.search.enable | default true }}
<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>
{{- else }}
<p class="search-disabled">{{ i18n "searchDisabled" | default "Tìm kiếm hiện không khả dụng trên trang này." }}</p>
{{- end }}
{{ end }}
{{ define "scripts" }}
{{- if site.Params.search.enable | default true }}
<script type="module">
import { PagefindUI } from "{{ "/pagefind/pagefind-ui.js" | relURL }}";
new PagefindUI({
@@ -40,4 +47,5 @@
}
});
</script>
{{- end }}
{{ end }}
+8 -3
View File
@@ -15,19 +15,24 @@
</div>
<footer class="post-footer">
{{- with .Params.tags }}
{{- with .GetTerms "tags" }}
<ul class="post-tag-list">
{{- range . }}
<li><a href="{{ printf "/tags/%s/" (urlize .) | relURL }}">#{{ . }}</a></li>
<li><a href="{{ .RelPermalink }}">#{{ .LinkTitle }}</a></li>
{{- end }}
</ul>
{{- end }}
</footer>
</article>
{{- if and (gt .WordCount 400) (ne .Params.toc false) }}
{{- $tocCfg := site.Params.toc | default dict -}}
{{- $tocEnabled := $tocCfg.enable | default true -}}
{{- $tocMin := $tocCfg.minWordCount | default 400 -}}
{{- if and $tocEnabled (gt .WordCount $tocMin) (ne .Params.toc false) }}
{{ partial "toc.html" . }}
{{- end }}
{{ partial "related-posts.html" . }}
{{ partial "comments.html" . }}
{{ end }}