mirror of
https://github.com/tiennm99/tsuki.git
synced 2026-06-01 20:13:50 +00:00
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:
+20
-18
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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 -}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user