fix: inline stack theme

This commit is contained in:
2026-07-31 15:38:57 +08:00
parent b59ffa7703
commit 3423112533
335 changed files with 16340 additions and 1 deletions
@@ -0,0 +1,49 @@
<article>
<a href="{{ .RelPermalink }}">
<div class="article-details">
<h2 class="article-title">
{{- .Title -}}
</h2>
{{- $description := .Params.description -}}
{{- $IsTerm := in .Kind "term" -}}
{{- if $IsTerm -}}
{{- $description = $description | default (T "list.page" (len .Pages)) -}}
{{- end -}}
{{- if or $description (not .Date.IsZero) -}}
{{/*
If it's a term, the date is extracted from the first page of the collection, which is not very useful.
In that case, we prefer to display the description (if set), or the number of pages within the collection.
*/}}
<footer class="article-meta">
{{- if or $IsTerm .Date.IsZero -}}
<span class="article-description">{{- $description -}}</span>
{{- else if not .Date.IsZero -}}
<time datetime="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
{{- .Date | time.Format .Site.Params.dateFormat.published -}}
</time>
{{- end }}
</footer>
{{- end -}}
</div>
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}}
{{ if $image }}
<div class="article-image">
{{ partial "helper/thumbnail-image" (dict
"Resource" $image.Resource
"Width" 60
"Height" 60
"Resize" .Site.Params.ImageProcessing.Thumbnail.Enabled
"Attributes" (dict
"src" $image.Permalink
"alt" .Title
"loading" "lazy"
"width" $image.Width
"height" $image.Height
)
) }}
</div>
{{ end }}
</a>
</article>
@@ -0,0 +1,4 @@
{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }}
<article class="{{ if $image }}has-image{{ end }}">
{{ partial "article/components/header" (dict "Page" . "IsList" true) }}
</article>
@@ -0,0 +1,37 @@
{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }}
<article class="{{ if $image }}has-image{{ end }}">
<a href="{{ .RelPermalink }}">
{{ if $image }}
<div class="article-image">
{{ partial "helper/thumbnail-image" (dict
"Resource" $image.Resource
"Width" 250
"Height" 150
"Resize" .Site.Params.ImageProcessing.Thumbnail.Enabled
"Attributes" (dict
"src" $image.Permalink
"alt" (printf "Featured image of post %s" .Title)
"loading" "lazy"
"width" $image.Width
"height" $image.Height
)
) }}
</div>
{{ end }}
<div class="article-details"
{{- if and $image (reflect.IsImageResourceProcessable $image.Resource) -}}
{{- $colors := $image.Resource.Colors -}}
{{- if $colors -}}
{{- $vibrant := index $colors 0 -}}
{{- $darkMuted := index $colors 1 | default $vibrant -}}
style="background: linear-gradient(0deg, {{ $darkMuted }}80 0%, {{ $vibrant }}C0 100%);"
{{- end -}}
{{- end -}}
>
<h2 class="article-title">
{{- .Title -}}
</h2>
</div>
</a>
</article>
@@ -0,0 +1,27 @@
<article class="{{ if .Params.image }}has-image {{ end }}main-article">
{{ partial "article/components/header" (dict "Page" . "IsList" false) }}
{{ if .Scratch.Get "TOCEnabled" }}
<aside class="article-toc">
<details>
<summary>
<div class="article-toc-title">
{{ partial "helper/icon" "chevron-right" }}
<span>{{ T "article.tableOfContents" }}</span>
</div>
</summary>
<nav class="toc-nav">
{{ partial "article/components/toc" . | safeHTML }}
</nav>
</details>
</aside>
{{ end }}
{{ partial "article/components/content" . }}
{{ partial "article/components/footer" . }}
{{ if or .Params.math .Site.Params.article.math }}
{{ partialCached "article/components/math.html" . }}
{{ end }}
</article>
@@ -0,0 +1,5 @@
<section class="article-content">
<!-- Refer to https://discourse.gohugo.io/t/responsive-tables-in-markdown/10639/5 -->
{{ $wrappedTable := printf "<div class=\"table-wrapper\">${1}</div>" }}
{{ .Content | replaceRE "(<table>(?:.|\n)+?</table>)" $wrappedTable | safeHTML }}
</section>
@@ -0,0 +1,76 @@
{{- $IsList := .IsList -}}
{{- $Page := .Page -}}
<div class="article-details">
{{ if $Page.Params.categories }}
<header class="article-category">
{{ range ($Page.GetTerms "categories") }}
{{ $color := partial "helper/color-from-str" .LinkTitle }}
{{ $BackgroundColor := default $color.BackgroundColor .Params.style.background }}
{{ $TextColor := default $color.TextColor .Params.style.color }}
<a href="{{ .RelPermalink }}" style="background-color: {{ $BackgroundColor | safeCSS }}; color: {{ $TextColor | safeCSS }};">
{{ .LinkTitle }}
</a>
{{ end }}
</header>
{{ end }}
<div class="article-title-wrapper">
<h2 class="article-title">
<a href="{{ $Page.RelPermalink }}">
{{- $Page.Title -}}
</a>
</h2>
{{ with $Page.Params.description }}
<h3 class="article-subtitle">
{{ . }}
</h3>
{{ end }}
</div>
{{ $showReadingTime := $Page.Params.readingTime | default ($Page.Site.Params.article.readingTime) }}
{{ $showDate := not $Page.Date.IsZero }}
{{ $showTime := or $showDate $showReadingTime }}
{{ $showTranslations := $Page.IsTranslated }}
{{ $showTags := and $Page.Site.Params.Article.List.ShowTags ($Page.GetTerms "tags") $IsList }}
{{ if or $showTime $showTranslations $showTags }}
<footer class="article-meta">
{{ if $showTime }}
<div class="inline-meta">
{{ if $showDate }}
{{ partial "helper/icon" "date" }}
<time class="article-time--published" datetime='{{ $Page.Date.Format "2006-01-02T15:04:05Z07:00" }}'>
{{- $Page.Date | time.Format $Page.Site.Params.dateFormat.published -}}
</time>
{{ end }}
{{ if $showReadingTime }}
{{ partial "helper/icon" "clock" }}
<time class="article-time--reading">
{{ T "article.readingTime" $Page.ReadingTime }}
</time>
{{ end }}
</div>
{{ end }}
{{ if $showTranslations }}
<div class="inline-meta">
{{ partial "helper/icon" "language" }}
{{ range $Page.Translations }}
<a href="{{ .RelPermalink }}" class="link">{{ .Lang }}</a>
{{ end }}
</div>
{{ end }}
{{ if $showTags }}
<div class="article-tags inline-meta">
{{ partial "helper/icon" "tag" }}
{{ range ($Page.GetTerms "tags") }}
<a href="{{ .RelPermalink }}" class="link">{{ .LinkTitle }}</a>
{{ end }}
</div>
{{ end }}
</footer>
{{ end }}
</div>
@@ -0,0 +1,19 @@
<footer class="article-meta article-footer">
{{ partial "article/components/tags" . }}
{{ if and (.Site.Params.article.license.enabled) (not (eq .Params.license false)) }}
<section class="article-copyright inline-meta">
{{ partial "helper/icon" "copyright" }}
<span>{{ default .Site.Params.article.license.default .Params.license | markdownify }}</span>
</section>
{{ end }}
{{- if ne .Lastmod .Date -}}
<section class="article-lastmod inline-meta">
{{ partial "helper/icon" "clock" }}
<span>
{{ T "article.lastUpdatedOn" }} {{ .Lastmod | time.Format .Site.Params.dateFormat.lastUpdated }}
</span>
</section>
{{- end -}}
</footer>
@@ -0,0 +1,25 @@
{{- $IsList := .IsList -}}
{{- $Page := .Page -}}
<header class="article-header">
{{- $image := partial "helper/image" (dict "Image" $Page.Params.image "Resources" $Page.Resources "Context" $Page) -}}
{{ if $image }}
<div class="article-image">
<a href="{{ $Page.RelPermalink }}">
{{ partial "helper/responsive-image" (dict
"Resource" $image.Resource
"Widths" (cond $Page.Site.Params.ImageProcessing.Content.Enabled $Page.Site.Params.ImageProcessing.Content.Widths nil)
"Attributes" (dict
"src" $image.Permalink
"width" $image.Width
"height" $image.Height
"alt" (printf "Featured image of post %s" $Page.Title)
"loading" "lazy"
"sizes" "(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px"
)
) }}
</a>
</div>
{{ end }}
{{ partial "article/components/details" (dict "Page" $Page "IsList" $IsList) }}
</header>
@@ -0,0 +1,39 @@
<div class="article-list--compact links">
{{ range $link := .Params.links }}
<article>
<a href="{{ $link.website }}" target="_blank" rel="noopener">
<div class="article-details">
<h2 class="article-title">
{{- $link.title -}}
</h2>
<footer class="article-meta">
{{ with $link.description }}
{{ . }}
{{ else }}
{{ $link.website }}
{{ end }}
</footer>
</div>
{{- $image := partial "helper/image" (dict "Image" $link.image "Resources" $.Resources "Context" .) -}}
{{- with $image -}}
<div class="article-image">
{{ partial "helper/thumbnail-image" (dict
"Resource" .Resource
"Width" 60
"Height" 60
"Resize" $.Site.Params.ImageProcessing.Thumbnail.Enabled
"Attributes" (dict
"src" $image.Permalink
"alt" $link.title
"loading" "lazy"
"width" .Width
"height" .Height
)
) }}
</div>
{{ end }}
</a>
</article>
{{ end }}
</div>
@@ -0,0 +1,21 @@
{{- partial "helper/external" (dict "Context" . "Namespace" "KaTeX") -}}
<script>
window.addEventListener("DOMContentLoaded", () => {
const elementsToRender = [".main-article", ".widget--toc"];
elementsToRender.forEach(selector => {
const element = document.querySelector(selector);
if (element) {
renderMathInElement(element, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false },
{ left: "\\(", right: "\\)", display: false },
{ left: "\\[", right: "\\]", display: true }
],
ignoredClasses: ["gist"]
});
}
});
});
</script>
@@ -0,0 +1,42 @@
{{- if .Store.Get "hasMermaid" -}}
{{- $cfg := site.Params.article.mermaid | default dict -}}
<div class="mermaid-modal" id="mermaid-modal">
<div class="mermaid-modal-header">
<div class="mermaid-modal-controls">
<button data-zoom="-1"> Zoom Out</button>
<button data-zoom="0">Reset (100%)</button>
<button data-zoom="1">+ Zoom In</button>
<button data-zoom="fit">Fit to Screen</button>
</div>
<button class="mermaid-modal-close" id="mermaid-modal-close">✕ Close (Esc)</button>
</div>
<div class="mermaid-modal-body" id="mermaid-modal-body">
<div class="mermaid-modal-content" id="mermaid-modal-content"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
{{- $opts := dict "minify" hugo.IsProduction "format" "esm" -}}
{{- $script := resources.Get "ts/mermaid.ts" | js.Build $opts -}}
{{- $jsConfig := dict
"transparentBackground" ($cfg.transparentBackground | default false)
"lightTheme" ($cfg.lightTheme | default "default")
"darkTheme" ($cfg.darkTheme | default "dark")
"lightThemeVariables" ($cfg.lightThemeVariables | default dict)
"darkThemeVariables" ($cfg.darkThemeVariables | default dict)
"securityLevel" ($cfg.securityLevel | default "strict")
"look" ($cfg.look | default "classic")
"htmlLabels" (not (eq ($cfg.htmlLabels | default true) false))
-}}
{{- with $cfg.maxTextSize }}{{ $jsConfig = merge $jsConfig (dict "maxTextSize" .) }}{{ end -}}
{{- with $cfg.maxEdges }}{{ $jsConfig = merge $jsConfig (dict "maxEdges" .) }}{{ end -}}
{{- with $cfg.fontSize }}{{ $jsConfig = merge $jsConfig (dict "fontSize" .) }}{{ end -}}
{{- with $cfg.fontFamily }}{{ $jsConfig = merge $jsConfig (dict "fontFamily" .) }}{{ end -}}
{{- with $cfg.curve }}{{ $jsConfig = merge $jsConfig (dict "curve" .) }}{{ end -}}
{{- with $cfg.logLevel }}{{ $jsConfig = merge $jsConfig (dict "logLevel" .) }}{{ end -}}
<script type="module">
import { initMermaidPage } from '{{ $script.RelPermalink }}';
initMermaidPage({{ $jsConfig | jsonify | safeJS }});
</script>
{{- end -}}
@@ -0,0 +1,32 @@
{{- $opts := dict "minify" hugo.IsProduction "format" "esm" -}}
{{- $galleryScript := resources.Get "ts/gallery.ts" | js.Build $opts -}}
{{ $style := hugo.Data.external.PhotoSwipe.Style }}
{{ $core := hugo.Data.external.PhotoSwipe.Core }}
{{ $lightbox := hugo.Data.external.PhotoSwipe.Lightbox }}
<script type="module">
import gallery from '{{ $galleryScript.RelPermalink }}';
const articleContent = document.querySelector('.article-content');
const shouldLoad = articleContent && (articleContent.querySelectorAll('figure').length > 0 || articleContent.querySelectorAll('img.gallery-image').length > 0);
if (shouldLoad) {
gallery(articleContent);
const PhotoSwipeLightbox = (await import("{{ $lightbox | safeJS }}")).default;
const styleHref = "{{ $style | safeJS }}";
const styleTag = document.createElement('link');
styleTag.rel = 'stylesheet';
styleTag.href = styleHref;
document.head.appendChild(styleTag);
const lightbox = new PhotoSwipeLightbox({
gallerySelector: '.article-content',
childSelector: 'figure a.image-link',
pswpModule: () => import("{{ $core | safeJS }}")
});
lightbox.init();
}
</script>
@@ -0,0 +1,13 @@
{{ $related := (.Site.RegularPages.Related .) | first 5 }}
{{ with $related }}
<aside>
<h2 class="section-title">{{ T "article.relatedContent" }}</h2>
<div class="subsection-list">
<div class="article-list--tile">
{{ range . }}
{{ partial "article-list/tile" . }}
{{ end }}
</div>
</div>
</aside>
{{ end }}
@@ -0,0 +1,7 @@
{{ if .Params.Tags }}
<section class="article-tags">
{{ range (.GetTerms "tags") }}
<a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
{{ end }}
</section>
{{ end }}
@@ -0,0 +1,6 @@
{{/* Remove <nav id="TableOfContents"> and </nav> from .TableOfContents */}}
{{ $toc := .TableOfContents }}
{{ $toc = replace $toc "<nav id=\"TableOfContents\">" "" }}
{{ $toc = replace $toc "</nav>" "" }}
{{ return $toc }}
@@ -0,0 +1,28 @@
{{/*
Comments include entry.
- If functional cookie consent is required, render a placeholder + template and load commentsConsent.ts.
- Otherwise, render provider partial directly.
*/}}
{{ if .Site.Params.comments.enabled }}
{{- $needsConsent := and .Site.Params.cookies.enabled .Site.Params.cookies.categories.functional -}}
{{- if $needsConsent -}}
{{/* Consent-gated comments - show placeholder until functional consent */}}
<div id="comments-consent-placeholder" class="consent-placeholder">
<p>{{ T "cookies.commentsDisabled" }}</p>
<button class="cookie-btn cookie-btn--primary" data-cookie-action="reopen">
{{ T "cookies.managePreferences" }}
</button>
</div>
<div id="comments-container" style="display: none;"></div>
<template id="comments-template">
{{ partial (printf "comments/provider/%s" .Site.Params.comments.provider) . }}
</template>
{{- $opts := dict "minify" hugo.IsProduction -}}
{{/* commentsConsent.ts is intentionally built and loaded here as an independent entry */}}
{{- $commentsScript := resources.Get "ts/commentsConsent.ts" | js.Build $opts | fingerprint -}}
<script type="text/javascript" src="{{ $commentsScript.RelPermalink }}" integrity="{{ $commentsScript.Data.Integrity }}" defer></script>
{{- else -}}
{{/* No consent required - load comments normally */}}
{{ partial (printf "comments/provider/%s" .Site.Params.comments.provider) . }}
{{- end -}}
{{ end }}
@@ -0,0 +1,30 @@
{{- $style := resources.Get "scss/partials/comments/artalk.scss" | toCSS | minify -}}
{{- with .Site.Params.comments.artalk -}}
<link rel="stylesheet" href="{{ hugo.Data.external.Artalk.Style }}">
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
<div id="Comments" class="artalk-container"></div>
<script type="module">
import Artalk from '{{ hugo.Data.external.Artalk.Script }}';
function isDarkMode() {
return document.documentElement.dataset.scheme === 'dark';
}
const artalkInstance = Artalk.init({
el: '#Comments',
pageKey: '{{ $.RelPermalink }}',
pageTitle: '{{ $.Title }}',
server: '{{ .serverUrl }}',
site: '{{ .site }}',
darkMode: isDarkMode(),
locale: 'auto',
});
window.addEventListener('onColorSchemeChange', (e) => {
if (!artalkInstance || typeof artalkInstance.setDarkMode !== 'function') return;
artalkInstance.setDarkMode(e.detail === 'dark');
});
</script>
{{- end -}}
@@ -0,0 +1,46 @@
<script
src="https://beaudar.lipk.org/client.js"
repo="{{ .Site.Params.comments.beaudar.repo }}"
issue-term="{{ .Site.Params.comments.beaudar.issueTerm }}"
{{ with .Site.Params.comments.beaudar.label }}
label="{{ . }}"
{{ end }}
theme="{{ .Site.Params.comments.beaudar.theme }}"
crossorigin="anonymous"
async
></script>
<style>
.beaudar {
max-width: unset;
}
</style>
<script>
let beaudarLoaded = false;
function setBeaudarTheme(theme) {
let beaudar = document.querySelector(".beaudar iframe");
if (beaudar) {
beaudar.contentWindow.postMessage(
{
type: "set-theme",
theme: `github-${theme}`,
},
"https://beaudar.lipk.org"
);
}
}
addEventListener("message", (event) => {
if (event.origin !== "https://beaudar.lipk.org") return;
/// Called when Beaudar is ready
beaudarLoaded = true;
setBeaudarTheme(document.documentElement.dataset.scheme);
});
window.addEventListener("onColorSchemeChange", (e) => {
if (!beaudarLoaded) return;
setBeaudarTheme(e.detail);
});
</script>
@@ -0,0 +1,29 @@
{{- with .Site.Params.comments.cactus -}}
{{- partial "helper/external" (dict "Context" $ "Namespace" "Cactus") -}}
<style>
.cactus-editor-textarea {
color: var(--body-text-color);
}
.cactus-comment-header {
color: var(--card-text-color-main);
}
.cactus-message-text > p {
color: var(--body-text-color);
}
</style>
<div id="comment-section"></div>
<script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "{{ .defaultHomeserverUrl | safeJS }}",
serverName: "{{ .serverName }}",
siteName: "{{ .siteName }}",
commentSectionId: "{{ $.File.UniqueID }}"
})
</script>
{{- end -}}
@@ -0,0 +1,13 @@
<script defer src="{{ .Site.Params.comments.comentario.url }}/comentario.js"></script>
<comentario-comments auto-init="{{ .Site.Params.comments.comentario.autoInit }}"
auto-non-interactive-sso="{{ .Site.Params.comments.comentario.autoNonInteractiveSSO }}"
css-override="{{ .Site.Params.comments.comentario.cssOverride }}" lang="{{ .Site.Params.comments.comentario.lang }}"
live-update="{{ .Site.Params.comments.comentario.liveUpdate }}"
max-level="{{ .Site.Params.comments.comentario.maxLevel }}"
no-fonts="{{ .Site.Params.comments.comentario.noFonts }}"
theme="{{ .Site.Params.comments.comentario.theme }}"></comentario-comments>
<script>
window.addEventListener('onColorSchemeChange', (e) => {
document.querySelector('comentario-comments').setAttribute('theme', e.detail);
})
</script>
@@ -0,0 +1,27 @@
{{- $host := .Site.Params.comments.cusdis.host -}}
<div id="cusdis_thread" data-host="{{ $host }}" data-app-id="{{ .Site.Params.comments.cusdis.id }}"
data-page-id="{{ .File.UniqueID }}" data-page-url="{{ .Permalink }}" data-page-title="{{ .Title }}"></div>
<script async defer src="{{ $host }}/js/cusdis.es.js"></script>
<script>
function setCusdisTheme(theme) {
let cusdis = document.querySelector('#cusdis_thread iframe');
if (cusdis) {
window.CUSDIS.setTheme(theme);
cusdis.contentWindow.document.body.style.backgroundColor = window.getComputedStyle(document.body).backgroundColor;
}
}
window.addEventListener('load', function () {
let iframe = document.querySelector("#cusdis_thread iframe");
if (iframe) {
let observer = new MutationObserver(() => {
let scrollHeight = iframe.contentWindow.document.body.scrollHeight;
iframe.style.height = scrollHeight + "px";
});
observer.observe(iframe.contentWindow.document.body, { childList: true, subtree: true });
}
});
window.addEventListener('onColorSchemeChange', (e) => {
setCusdisTheme(e.detail)
})
</script>
@@ -0,0 +1,22 @@
<div class="disqus-container">
{{ partial "disqus.html" . }}
</div>
<style>
.disqus-container {
background-color: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
padding: var(--card-padding);
}
</style>
<script>
window.addEventListener('onColorSchemeChange', (e) => {
if (typeof DISQUS == 'object') {
DISQUS.reset({
reload: true
});
}
})
</script>
@@ -0,0 +1,65 @@
{{- $pc := .Site.Config.Privacy.Disqus -}}
{{- $disqusjs := .Site.Params.Comments.disqusjs -}}
{{- if and (not $pc.Disable) (and $disqusjs.Shortname $disqusjs.ApiKey) -}}
{{- $style := resources.Get "scss/partials/comments/disqusjs.scss" | toCSS | minify -}}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
<div class="disqus-container">
<div id="disqus_thread"></div>
<script type="application/javascript">
let disqusjs;
function loadDisqusJS() {
disqusjs = new DisqusJS({
shortname: {{ $disqusjs.Shortname }},
siteName: {{ .Site.Title }},
apikey: {{ $disqusjs.ApiKey }},
{{ with $disqusjs.ApiUrl }}api: {{ . }},{{ end }}
{{ with $disqusjs.Admin }}admin: {{ . }},{{ end }}
{{ with $disqusjs.AdminLabel }}adminLabel: {{ . }}{{ end }}
});
}
function lazyLoadDisqusJS() {
const excludedHosts = ["localhost", "127.0.0.1"];
const hostname = window.location.hostname;
if (excludedHosts.includes(hostname)) {
document.getElementById('disqus_thread').innerHTML =
'Disqus comments not available by default when the website is previewed locally.';
return;
}
let d = document.createElement('script');
d.src = 'https://cdn.jsdelivr.net/npm/disqusjs@1.3/dist/disqus.js';
d.async = false;
document.body.appendChild(d);
d.onload = () => {
loadDisqusJS();
window.addEventListener('onColorSchemeChange', (e) => {
if (disqusjs) {
loadDisqusJS();
}
})
}
}
let runningOnBrowser = typeof window !== "undefined";
let isBot = runningOnBrowser && !("onscroll" in window) || typeof navigator !== "undefined" && /(gle|ing|ro|msn)bot|crawl|spider|yand|duckgo/i.test(navigator.userAgent);
let supportsIntersectionObserver = runningOnBrowser && "IntersectionObserver" in window;
if (!isBot && supportsIntersectionObserver) {
let disqus_observer = new IntersectionObserver(function(entries) {
if (entries[0].isIntersecting) {
lazyLoadDisqusJS();
disqus_observer.disconnect();
}
});
disqus_observer.observe(document.getElementById('disqus_thread'));
} else {
lazyLoadDisqusJS();
}
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
</div>
{{- end -}}
@@ -0,0 +1,52 @@
{{- with .Site.Params.comments.giscus -}}
<script
src="https://giscus.app/client.js"
data-repo="{{- .repo -}}"
data-repo-id="{{- .repoID -}}"
data-category="{{- .category -}}"
data-category-id="{{- .categoryID -}}"
data-mapping="{{- .mapping -}}"
data-strict="{{- .strict -}}"
data-reactions-enabled="{{- .reactionsEnabled -}}"
data-emit-metadata="{{- .emitMetadata -}}"
data-input-position="{{- .inputPosition -}}"
data-theme="{{- .lightTheme -}}"
data-lang="{{- .lang -}}"
data-loading="{{- .loading -}}"
crossorigin="anonymous"
async
></script>
<script>
function setGiscusTheme(theme) {
let giscus = document.querySelector("iframe.giscus-frame");
if (giscus) {
giscus.contentWindow.postMessage(
{
giscus: {
setConfig: {
theme: theme,
},
},
},
"https://giscus.app"
);
}
}
(function () {
addEventListener("message", (e) => {
if (event.origin !== "https://giscus.app") return;
handler();
});
window.addEventListener("onColorSchemeChange", handler);
function handler() {
if (document.documentElement.dataset.scheme === "light") {
setGiscusTheme('{{- .lightTheme -}}');
} else {
setGiscusTheme('{{- .darkTheme -}}');
}
}
})();
</script>
{{- end -}}
@@ -0,0 +1,32 @@
{{- with .Site.Params.comments.gitalk -}}
<div id="gitalk-container"></div>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/gitalk@1.7.2/dist/gitalk.css"
/>
<script src="https://cdn.jsdelivr.net/npm/gitalk@1.7.2/dist/gitalk.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/blueimp-md5@2.18.0/js/md5.min.js"></script>
<script>
const gitalk = new Gitalk({
clientID: "{{- .clientID -}}",
clientSecret: "{{- .clientSecret -}}",
repo: "{{- .repo -}}",
owner: "{{- .owner -}}",
admin: ["{{- .admin -}}"],
distractionFreeMode: false, // Facebook-like distraction free mode
id: md5(location.pathname), // Max Location.pathname Legth:75 https://github.com/gitalk/gitalk/issues/102
proxy: {{- .proxy -}},
});
(function () {
const excludedHosts = ["localhost", "127.0.0.1"];
const hostname = window.location.hostname;
if (excludedHosts.includes(hostname)) {
document.getElementById("gitalk-container").innerHTML =
"Gitalk comments not available by default when the website is previewed locally.";
return;
}
gitalk.render("gitalk-container");
})();
</script>
{{ end }}
@@ -0,0 +1,29 @@
{{- with .Site.Params.comments.remark42 -}}
<div id="remark42"></div>
<script>
var remark_config = {
host: '{{ .host }}',
site_id: '{{ .site }}',
components: ['embed'],
url: '{{ $.Permalink }}',
max_shown_comments: {{ .max_shown_comments }},
theme: document.documentElement.dataset.scheme,
page_title: '{{ $.Title }}',
locale: '{{ .locale }}',
show_email_subscription: {{ .show_email_subscription }}
};
!function(e, n) {
for (var o = 0; o < e.length; o++) {
var r = n.createElement('script'),
c = '.js',
d = n.head || n.body;
'noModule' in r ? (r.type = 'module', c = '.mjs') : r.async = !0, r.defer = !0, r.src = remark_config.host + '/web/' + e[o] + c, d.appendChild(r)
}
}(remark_config.components || ['embed'], document);
window.addEventListener('onColorSchemeChange', (e) => {
window.REMARK42.changeTheme(e.detail);
})
</script>
{{- end -}}
@@ -0,0 +1,61 @@
<script src="//cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.all.min.js"></script>
<div id="tcomment"></div>
<style>
.twikoo {
background-color: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
padding: var(--card-padding);
}
:root[data-scheme="dark"] {
--twikoo-body-text-color-main: rgba(255, 255, 255, 0.9);
--twikoo-body-text-color: rgba(255, 255, 255, 0.7);
}
.twikoo .el-input-group__prepend,
.twikoo .tk-action-icon,
.twikoo .tk-submit-action-icon,
.twikoo .tk-time,
.twikoo .tk-comments-no,
.twikoo .tk-comments-count {
color: var(--twikoo-body-text-color);
}
.twikoo .el-input__inner,
.twikoo .el-textarea__inner,
.twikoo .tk-preview-container,
.twikoo .tk-content,
.twikoo .tk-nick,
.twikoo .tk-send,
.twikoo .tk-comments-no,
.twikoo .el-input__count,
.twikoo .tk-submit-action-icon {
color: var(--twikoo-body-text-color-main)!important;
}
.twikoo .el-button{
color: var(--twikoo-body-text-color)!important;
}
.twikoo .el-input__count {
color: var(--twikoo-body-text-color) !important;
}
.OwO .OwO-body {
background-color: var(--body-background) !important;
color: var(--body-text-color) !important;
}
</style>
{{- with .Site.Params.comments.twikoo -}}
<script>
twikoo.init({
envId: '{{- .envId -}}',
el: '#tcomment',
{{- with .region -}}
region: '{{- . -}}',
{{- end -}}
{{- with .path -}}
path: '{{- . -}}',
{{- end -}}
{{- with .lang -}}
lang: '{{- . -}}',
{{- end -}}
})
</script>
{{- end -}}
@@ -0,0 +1,46 @@
<script src="https://utteranc.es/client.js"
repo="{{ .Site.Params.comments.utterances.repo }}"
issue-term="{{ .Site.Params.comments.utterances.issueTerm }}"
{{ with .Site.Params.comments.utterances.label }}
label="{{ . }}"
{{ end }}
crossorigin="anonymous"
async
>
</script>
<style>
.utterances {
max-width: unset;
}
</style>
<script>
let utterancesLoaded = false;
function setUtterancesTheme(theme) {
let utterances = document.querySelector('.utterances iframe');
if (utterances) {
utterances.contentWindow.postMessage(
{
type: 'set-theme',
theme: `github-${theme}`
},
'https://utteranc.es'
);
}
}
addEventListener('message', event => {
if (event.origin !== 'https://utteranc.es') return;
/// Called when Utterances is ready
utterancesLoaded = true;
setUtterancesTheme(document.documentElement.dataset.scheme)
});
window.addEventListener('onColorSchemeChange', (e) => {
if (!utterancesLoaded) return;
setUtterancesTheme(e.detail)
})
</script>
@@ -0,0 +1,27 @@
{{- with .Site.Params.comments.vssue -}}
<link rel="stylesheet" href="https://unpkg.com/vssue/dist/vssue.min.css" />
<div id="vssue"></div>
<script src="https://unpkg.com/vue@2/dist/vue.runtime.min.js"></script>
<script src="https://unpkg.com/vssue/dist/vssue.{{ .platform }}.min.js"></script>
<script>
new Vue({
el: "#vssue",
render: (h) =>
h("Vssue", {
props: {
title: "{{ $.Title }}",
options: {
autoCreateIssue: {{ .autoCreateIssue }},
owner: "{{ .owner }}",
repo: "{{ .repo }}",
clientId: "{{ .clientId }}",
clientSecret: "{{ .clientSecret }}",
},
},
}),
});
</script>
{{- end -}}
@@ -0,0 +1,34 @@
<script src='//unpkg.com/@waline/client@v2/dist/waline.js'></script>
<link href='//unpkg.com/@waline/client@v2/dist/waline.css' rel='stylesheet'/>
<div id="waline" class="waline-container"></div>
<style>
.waline-container {
background-color: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
padding: var(--card-padding);
--waline-font-size: var(--article-font-size);
}
.waline-container .wl-count {
color: var(--card-text-color-main);
}
</style>
{{- with .Site.Params.comments.waline -}}
{{- $config := dict "el" "#waline" "dark" `html[data-scheme="dark"]` -}}
{{- $replaceKeys := dict "serverurl" "serverURL" "requiredmeta" "requiredMeta" "wordlimit" "wordLimit" "pagesize" "pageSize" "imageuploader" "imageUploader" "texrenderer" "texRenderer" "turnstilekey" "turnstileKey" -}}
{{- range $key, $val := . -}}
{{- if ne $val nil -}}
{{- $replaceKey := index $replaceKeys $key -}}
{{- $k := default $key $replaceKey -}}
{{- $config = merge $config (dict $k $val) -}}
{{- end -}}
{{- end -}}
<script>
/// Waline client configuration see: https://waline.js.org/en/reference/client.html
Waline.init({{ $config | jsonify | safeJS }});
</script>
{{- end -}}
@@ -0,0 +1,50 @@
{{- if not site.Config.Privacy.GoogleAnalytics.Disable -}}
{{- with site.Config.Services.GoogleAnalytics.ID -}}
{{- if strings.HasPrefix (lower .) "ua-" -}}
{{- warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." -}}
{{- else -}}
{{/* Consent-gated Google Analytics - only loads after analytics consent */}}
<script>
(function() {
var gaId = {{ . }};
var loaded = false;
var respectDNT = {{ site.Config.Privacy.GoogleAnalytics.RespectDoNotTrack }};
function loadGA() {
if (loaded) return;
// Respect Do Not Track browser setting if configured
if (respectDNT) {
var dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack);
if (dnt == "1" || dnt == "yes") {
return;
}
}
loaded = true;
var script = document.createElement('script');
script.async = true;
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + gaId;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', gaId);
}
window.addEventListener('onCookieConsentChange', function(e) {
if (e.detail && e.detail.analytics) {
loadGA();
}
});
if (window.cookieConsent && window.cookieConsent.hasConsent('analytics')) {
loadGA();
}
})();
</script>
{{- end -}}
{{- end -}}
{{- end -}}
@@ -0,0 +1,68 @@
{{- $cfg := .Site.Params.cookies -}}
{{- $categories := $cfg.categories -}}
<div id="cookie-consent-banner" class="cookie-banner" aria-hidden="true" role="dialog" aria-labelledby="cookie-banner-title">
<div class="cookie-banner__content">
<div class="cookie-banner__text">
<strong id="cookie-banner-title">{{ T "cookies.title" }}</strong>
<p>{{ T "cookies.text" }}</p>
</div>
<div class="cookie-banner__actions">
<button class="cookie-btn cookie-btn--secondary" data-cookie-action="deny">
{{ T "cookies.deny" }}
</button>
<button class="cookie-btn cookie-btn--primary" data-cookie-action="accept">
{{ T "cookies.acceptAll" }}
</button>
</div>
{{- if $cfg.showSettings }}
<button class="cookie-banner__settings-link" data-cookie-action="settings">
{{ T "cookies.managePreferences" }}
</button>
{{- end }}
{{/* Settings panel */}}
<div id="cookie-settings-panel" class="cookie-settings" aria-hidden="true">
<h3>{{ T "cookies.settingsTitle" }}</h3>
<div class="cookie-category">
<label>
<input type="checkbox" name="necessary" checked disabled />
<strong>{{ T "cookies.necessary.title" }}</strong>
</label>
<p>{{ T "cookies.necessary.text" }}</p>
</div>
{{- if $categories.analytics -}}
<div class="cookie-category">
<label>
<input type="checkbox" name="analytics" data-cookie-category="analytics" />
<strong>{{ T "cookies.analytics.title" }}</strong>
</label>
<p>{{ T "cookies.analytics.text" }}</p>
</div>
{{- end -}}
{{- if $categories.functional -}}
<div class="cookie-category">
<label>
<input type="checkbox" name="functional" data-cookie-category="functional" />
<strong>{{ T "cookies.functional.title" }}</strong>
</label>
<p>{{ T "cookies.functional.text" }}</p>
</div>
{{- end -}}
<div class="cookie-settings__actions">
<button class="cookie-btn cookie-btn--secondary" data-cookie-action="cancel">
{{ T "cookies.cancel" }}
</button>
<button class="cookie-btn cookie-btn--primary" data-cookie-action="save">
{{ T "cookies.savePreferences" }}
</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,9 @@
{{- if .Site.Params.cookies.enabled -}}
{{/* Render the banner HTML */}}
{{ partial "cookies/banner.html" . }}
{{/* Load the consent manager script */}}
{{- $opts := dict "minify" hugo.IsProduction -}}
{{- $script := resources.Get "ts/cookies.ts" | js.Build $opts | fingerprint -}}
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>
{{- end -}}
@@ -0,0 +1,17 @@
<!-- Use site subtitle by default -->
{{ $description := .Site.Params.sidebar.subtitle }}
<!-- Seprate description exists -->
{{ if .Site.Params.description }}
{{ $description = .Site.Params.description }}
{{ end }}
{{ if .Description }}
<!-- Page description exists -->
{{ $description = .Description }}
{{ else if .IsPage }}
<!-- Use page summary -->
{{ $description = .Summary }}
{{ end }}
{{ return (replaceRE "\n" " " $description | plainify) }}
@@ -0,0 +1,14 @@
{{- $pageTitle := .Title -}}
{{- $siteTitle := .Site.Title -}}
{{- $paginator := partial "helper/paginator.html" . -}}
{{- $title := slice -}}
{{- if and $paginator $paginator.HasPrev -}}
<!-- Add page number-->
{{ $title = $title | append (printf "Page %d" $paginator.PageNumber) }}
{{- end -}}
{{- $title = $title | append (default $siteTitle $pageTitle) -}}
{{ return delimit $title " - " }}
@@ -0,0 +1,18 @@
{{- $opts := dict
"minify" hugo.IsProduction
"params" (dict
"codeblock" (dict
"copy" (i18n "article.codeblock.copy")
"copied" (i18n "article.codeblock.copied")
)
)
-}}
{{- $script := resources.Get "ts/main.ts" | js.Build $opts | fingerprint -}}
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>
{{- with resources.Get "ts/custom.ts" -}}
{{/* Place your custom script in HUGO_SITE_FOLDER/assets/ts/custom.ts */}}
{{- $customScript := . | js.Build $opts | fingerprint -}}
<script type="text/javascript" src="{{ $customScript.RelPermalink }}" defer></script>
{{- end -}}
@@ -0,0 +1,23 @@
{{- $ThemeVersion := "4.0.3" -}}
<footer class="site-footer">
<section class="copyright">
&copy;
{{ if and (.Site.Params.footer.since) (ne .Site.Params.footer.since (int (now.Format "2006"))) }}
{{ .Site.Params.footer.since }} -
{{ end }}
{{ now.Format "2006" }} {{ default .Site.Title .Site.Copyright }}
</section>
<section class="powerby">
{{ with .Site.Params.footer.customText }}
{{ . | safeHTML }} <br/>
{{ end }}
{{- $Generator := `<a href="https://gohugo.io/" target="_blank" rel="noopener">Hugo</a>` -}}
{{- $Theme := printf `<b><a href="https://github.com/CaiJimmy/hugo-theme-stack" target="_blank" rel="noopener" data-version="%s">Stack</a></b>` $ThemeVersion -}}
{{- $DesignedBy := `<a href="https://jimmycai.com" target="_blank" rel="noopener">Jimmy</a>` -}}
{{ T "footer.builtWith" (dict "Generator" $Generator) | safeHTML }} <br />
{{ T "footer.designedBy" (dict "Theme" $Theme "DesignedBy" $DesignedBy) | safeHTML }}
</section>
</footer>
@@ -0,0 +1,3 @@
{{ partial "cookies/include.html" . }}
{{ partialCached "footer/components/script.html" . }}
{{ partial "footer/custom.html" . }}
@@ -0,0 +1,39 @@
{{- $defaultColorScheme := .Site.Params.colorScheme.default -}}
{{- if not .Site.Params.colorScheme.toggle -}}
{{/* If toggle is disabled, force default scheme */}}
<script>
(function() {
const colorSchemeKey = 'StackColorScheme';
localStorage.setItem(colorSchemeKey, "{{ $defaultColorScheme }}");
})();
</script>
{{- else -}}
{{/* Otherwise set to default scheme only if no preference is set by user */}}
<script>
(function() {
const colorSchemeKey = 'StackColorScheme';
if(!localStorage.getItem(colorSchemeKey)){
localStorage.setItem(colorSchemeKey, "{{ $defaultColorScheme }}");
}
})();
</script>
{{- end -}}
<script>
(function() {
const colorSchemeKey = 'StackColorScheme';
const colorSchemeItem = localStorage.getItem(colorSchemeKey);
const supportDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches === true;
if (colorSchemeItem == 'dark' || colorSchemeItem === 'auto' && supportDarkMode) {
/**
* Enable dark mode if:
* 1. If dark mode is set already (in local storage)
* 2. Auto mode & prefere color scheme is dark
*/
document.documentElement.dataset.scheme = 'dark';
} else {
document.documentElement.dataset.scheme = 'light';
}
})();
</script>
@@ -0,0 +1,4 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap">
@@ -0,0 +1,37 @@
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
{{- $description := partialCached "data/description" . .RelPermalink -}}
<meta name='description' {{ printf "content=%q" $description | safeHTMLAttr }}>
{{ with .Params.Keywords }}<meta name="keywords" content="{{ delimit . ", " }}">{{ end }}
{{- $title := partial "data/title" . -}}
<title>{{ $title }}</title>
{{ with .Params.canonicalUrl }}
<link rel="canonical" href="{{ . }}">
{{ else }}
<link rel="canonical" href="{{ .Permalink }}">
{{ end }}
{{- partial "head/custom-font.html" . -}}
{{- partial "head/style.html" . -}}
{{- partial "head/script.html" . -}}
{{- partial "head/opengraph/include.html" . -}}
{{- range .AlternativeOutputFormats -}}
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
{{- end -}}
{{- $favicon := partial "helper/image" (dict "Image" .Site.Params.favicon "Resources" resources "Context" .) -}}
{{ with $favicon }}
<link rel="shortcut icon" href="{{ .Permalink }}" />
{{ end }}
{{- if .Site.Params.cookies.enabled -}}
{{- partial "cookies/analytics.html" . -}}
{{- else -}}
{{- template "_internal/google_analytics.html" . -}}
{{- end -}}
{{- partial "head/custom.html" . -}}
@@ -0,0 +1,2 @@
{{ partial "head/opengraph/provider/base" . }}
{{ partial "head/opengraph/provider/twitter" . }}
@@ -0,0 +1,43 @@
{{- $title := partialCached "data/title" . .RelPermalink -}}
{{- $description := partialCached "data/description" . .RelPermalink -}}
<meta property='og:title' {{ printf "content=%q" $title | safeHTMLAttr }}>
<meta property='og:description' {{ printf "content=%q" $description | safeHTMLAttr }}>
<meta property='og:url' content='{{ .Permalink }}'>
<meta property='og:site_name' content='{{ .Site.Title }}'>
<meta property='og:type' content='
{{- if .IsPage -}}
article
{{- else -}}
website
{{- end -}}
'>
{{- with .Params.locale -}}
<meta property='og:locale' content='{{ . }}'>
{{- end -}}
{{- if .IsPage -}}
<meta property='article:section' content='{{ .Section | title }}' />
{{- range .Params.tags -}}
<meta property='article:tag' content='{{ . }}' />
{{- end -}}
{{- end -}}
{{- if .IsPage -}}
{{- if not .Date.IsZero -}}
<meta property='article:published_time' content='{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}'/>
{{- end -}}
{{- if not .Lastmod.IsZero -}}
<meta property='article:modified_time' content='{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}'/>
{{- end -}}
{{- else -}}
{{- if not .Site.Lastmod.IsZero -}}
<meta property='og:updated_time' content='{{ .Site.Lastmod.Format " 2006-01-02T15:04:05-07:00 " | safeHTML }}'/>
{{- end -}}
{{- end -}}
{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }}
{{- if $image -}}
<meta property='og:image' content='{{ absURL $image.Permalink }}' />
{{- end -}}
@@ -0,0 +1,16 @@
{{- with .Site.Params.opengraph.twitter.site -}}
<meta name="twitter:site" content="@{{ . }}">
<meta name="twitter:creator" content="@{{ . }}">
{{- end -}}
{{- $title := partialCached "data/title" . .RelPermalink -}}
{{- $description := partialCached "data/description" . .RelPermalink -}}
<meta name="twitter:title" {{ printf "content=%q" $title | safeHTMLAttr }}>
<meta name="twitter:description" {{ printf "content=%q" $description | safeHTMLAttr }}>
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}}
{{- if $image -}}
<meta name="twitter:card" content="{{ .Site.Params.opengraph.twitter.card }}">
<meta name="twitter:image" content='{{ absURL $image.Permalink }}' />
{{- end -}}
@@ -0,0 +1,3 @@
{{ $sass := resources.Get "scss/style.scss" }}
{{ $style := $sass | toCSS | minify | resources.Fingerprint "sha256" }}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
@@ -0,0 +1,13 @@
{{/*
This helper is used to generate a background color and text color from a string
Used to generate deterministic colors for tags, categories, etc.
*/}}
{{- $str := . -}}
{{- $hash := hash.FNV32a $str -}}
{{- $h := mod $hash 360 -}}
{{- $backgroundColor := printf "hsl(%d, 60%%, 84%%)" $h -}}
{{- $textColor := printf "hsl(%d, 60%%, 15%%)" $h -}}
{{- return dict "BackgroundColor" $backgroundColor "TextColor" $textColor "Hue" $h -}}
@@ -0,0 +1,29 @@
{{- $List := index hugo.Data.external .Namespace -}}
{{- with $List -}}
{{- range . -}}
{{- if eq .type "script" -}}
<script
src="{{ .src }}"
{{- with .integrity -}}
integrity="{{ . }}"
{{- end -}}
crossorigin="anonymous"
{{ if .defer }}defer{{ end }}
>
</script>
{{- else if eq .type "style" -}}
<link
rel="stylesheet"
href="{{ .src }}"
{{- with .integrity -}}
integrity="{{ . }}"
{{- end -}}
crossorigin="anonymous"
>
{{- else -}}
{{- errorf "Error: unknown external resource type: %s" .type -}}
{{- end -}}
{{- end -}}
{{- else -}}
{{- errorf "Error: external resource '%s' is not found" .Namespace -}}
{{- end -}}
@@ -0,0 +1,6 @@
{{- $iconFile := resources.GetMatch (printf "icons/%s.svg" .) -}}
{{- if $iconFile -}}
{{- $iconFile.Content | safeHTML -}}
{{- else -}}
{{- errorf "Error: icon '%s.svg' is not found under 'assets/icons' folder" . -}}
{{- end -}}
@@ -0,0 +1,72 @@
/// Params:
/// Resources: Where to search for images
/// Image: Image URL
/// Context: page context (used to access the configuration)
/// Returns:
/// Resource: Hugo image resource object
/// Permalink: Image URL
/// Height: Image height
/// Width: Image width
{{ $result := dict }}
{{ if .Image }}
{{ $url := urls.Parse .Image }}
{{ $permalink := .Image }}
{{ $result = dict
"Resource" nil
"Permalink" $permalink
"Height" nil
"Width" nil
}}
{{ $resource := dict }}
{{ $local := true }}
{{ if not (in (slice "https" "http") $url.Scheme) }}
{{/* Local image */}}
{{ $resource = .Resources.Get (printf "%s" (.Image | safeURL)) }}
{{ else }}
{{/* Remote image */}}
{{ with try (resources.GetRemote $url (dict "timeout" .Context.Site.Params.imageProcessing.external.timeout)) }}
{{ with .Err }}
{{ warnf "Failed to fetch remote resource %q: %s" $url . }}
{{ else with .Value }}
{{ $resource = . }}
{{ $local = false }}
{{ else }}
{{ warnf "Unable to get remote resource %q" $url }}
{{ end }}
{{ end }}
{{ end }}
{{ if $resource }}
{{ if and .Context.Site.Params.imageProcessing.autoOrient (reflect.IsImageResourceProcessable $resource) }}
{{ $filter := images.AutoOrient }}
{{ $resource = $resource | images.Filter $filter }}
{{ end }}
{{ if $local }}
{{ $permalink = $resource.RelPermalink }}
{{ end }}
{{ if reflect.IsImageResourceWithMeta $resource }}
{{ $result = dict
"Resource" $resource
"Permalink" $permalink
"Height" $resource.Height
"Width" $resource.Width
}}
{{ else }}
{{ $result = dict
"Resource" $resource
"Permalink" $permalink
"Height" nil
"Width" nil
}}
{{ end }}
{{ end }}
{{ end }}
{{ return $result }}
@@ -0,0 +1,7 @@
{{- $pages := .Pages -}}
{{- if eq .Context.Site.Params.SortBy "lastmod" -}}
{{- $pages = $pages.ByLastmod.Reverse -}}
{{- end -}}
{{- return $pages -}}
@@ -0,0 +1,16 @@
{{- $pages := .Pages -}}
{{- if .IsHome -}}
{{- $pages = where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
{{- else if or (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") -}}
{{- $subsections := .Sections -}}
{{- $pages = .Pages | complement $subsections -}}
{{- if eq (len $pages) 0 -}}
{{- $pages = $subsections -}}
{{- end -}}
{{- end -}}
{{- $pages := partial "helper/pages-sort.html" (dict "Pages" $pages "Context" .) -}}
{{- return $pages -}}
@@ -0,0 +1,8 @@
{{- $result := slice -}}
{{- if or .IsHome (eq .Kind "section") (eq .Kind "taxonomy") (eq .Kind "term") -}}
{{- $pages := partial "helper/pages.html" . -}}
{{- $result = .Paginate $pages -}}
{{- end -}}
{{- return $result -}}
@@ -0,0 +1,33 @@
{{- /*
Params:
Resource: Hugo image resource object (Optional, for processing)
Widths: Slice of widths to generate for srcset
Attributes: Map of HTML attributes for the <img> tag
*/ -}}
{{- $resource := .Resource -}}
{{- $widths := .Widths -}}
{{- $attributes := .Attributes | default dict -}}
{{/* Generate srcset if Resource and Widths are provided */}}
{{- if and $widths $resource (reflect.IsImageResourceProcessable $resource) -}}
{{- $srcset := slice -}}
{{- range $widths -}}
{{- if lt . $resource.Width -}}
{{- $resized := $resource.Resize (printf "%dx" .) -}}
{{- $srcset = $srcset | append (printf "%s %dw" $resized.RelPermalink .) -}}
{{- end -}}
{{- end -}}
{{- if gt (len $srcset) 0 -}}
{{- $permalink := default $resource.RelPermalink (index $attributes "src") -}}
{{- $srcset = $srcset | append (printf "%s %dw" $permalink $resource.Width) -}}
{{- $attributes = merge $attributes (dict "srcset" (delimit $srcset ", ")) -}}
{{- end -}}
{{- end -}}
<img {{- range $k, $v := $attributes -}}
{{- if $v -}}
{{- printf " %s=%q" (lower $k) (printf "%v" $v) | safeHTMLAttr -}}
{{- end -}}
{{- end -}}>
@@ -0,0 +1,41 @@
{{- /*
Params:
Resource: Hugo image resource object (Optional, for processing)
Width: Image width (Required)
Height: Image height (Required)
Resize: Whether to perform resize (Optional, default: false)
Attributes: Map of HTML attributes for the <img> tag
*/ -}}
{{- $resource := .Resource -}}
{{- $width := .Width -}}
{{- $height := .Height -}}
{{- $resize := .Resize -}}
{{- $attributes := .Attributes | default dict -}}
{{- if and $resize $resource (reflect.IsImageResourceProcessable $resource) $width $height -}}
{{/* Create thumbnail with 1x/2x descriptors */}}
{{- $srcset := slice -}}
{{- range (slice 1 2) -}}
{{- $w := mul $width . -}}
{{- $h := mul $height . -}}
{{- if and (le $w $resource.Width) (le $h $resource.Height) -}}
{{- $resized := $resource.Fill (printf "%dx%d" $w $h) -}}
{{- $srcset = $srcset | append (printf "%s %dx" $resized.RelPermalink .) -}}
{{- if eq . 1 -}}
{{- $attributes = merge $attributes (dict "src" $resized.RelPermalink) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if gt (len $srcset) 0 -}}
{{- $attributes = merge $attributes (dict "width" $width "height" $height "srcset" (delimit $srcset ", ")) -}}
{{- end -}}
{{- end -}}
<img {{- range $k, $v := $attributes -}}
{{- if $v -}}
{{- printf " %s=%q" (lower $k) (printf "%v" $v) | safeHTMLAttr -}}
{{- end -}}
{{- end -}}>
@@ -0,0 +1,142 @@
{{- $pag := $.Paginator -}}
{{- if gt $pag.TotalPages 1 -}}
{{- /* Find page 2's URL to extract the pagination base path for JavaScript */ -}}
{{- $page2Url := "" -}}
{{- range $pag.Pagers -}}
{{- if eq .PageNumber 2 -}}
{{- $page2Url = .URL -}}
{{- end -}}
{{- end -}}
<nav
class="pagination"
role="navigation"
aria-label="pagination"
data-total="{{ $pag.TotalPages }}"
data-first-url="{{ $pag.First.URL }}"
data-format-url="{{ $page2Url }}">
{{- /* Previous page button */ -}}
<a
class="page-link {{ if not $pag.Prev }}disabled{{ end }}"
{{- if $pag.Prev -}}
href="{{ $pag.Prev.URL }}"
aria-label="Previous page"
{{- end -}}
{{- if not $pag.Prev -}}aria-disabled="true"{{- end -}}>
{{ partial "helper/icon" "chevron-left" }}
</a>
{{- /* Page numbers logic */ -}}
{{- /* Compute visible middle window [start, end] (excluding first and last page). */ -}}
{{- /* Keep 3 middle pages when possible; if total <= 5, show all middle pages. */ -}}
{{- $curr := $pag.PageNumber -}}
{{- $total := $pag.TotalPages -}}
{{- $middleMinPage := 2 -}}
{{- $middleMaxPage := sub $total 1 -}}
{{- $start := $middleMinPage -}}
{{- $end := $middleMaxPage -}}
{{- if gt $total 5 -}}
{{- $start = sub $curr 1 -}}
{{- if lt $start $middleMinPage -}}
{{- $start = $middleMinPage -}}
{{- end -}}
{{- $end = add $start 2 -}}
{{- if gt $end $middleMaxPage -}}
{{- $end = $middleMaxPage -}}
{{- $start = sub $end 2 -}}
{{- end -}}
{{- end -}}
{{- /* First page */ -}}
<a
class="page-link {{ if eq $curr 1 }}current{{ end }}"
aria-label="Page 1"
{{- if eq $curr 1 -}}aria-current="page"{{- end -}}
{{- if ne $curr 1 -}}href="{{ $pag.First.URL }}"{{- end -}}>
1
</a>
{{- /* Left ellipsis - show if the window start is beyond page 2 */ -}}
{{- if gt $start $middleMinPage -}}
<button
type="button"
class="page-link pagination-link pagination-jump-trigger"
aria-label="{{ i18n "pagination.jumpToPage" | default "Jump to page" }}">
{{ partial "helper/icon" "dots" }}
</button>
{{- end -}}
{{- /* Middle pages - show pages in [start, end] */ -}}
{{- range $pag.Pagers -}}
{{- if and (ge .PageNumber $start) (le .PageNumber $end) -}}
{{- $hideOnMobile := and (ne .PageNumber $curr) (ne .PageNumber (sub $curr 1)) (ne .PageNumber (add $curr 1)) -}}
<a
class="page-link {{ if eq .PageNumber $curr }}current{{ end }} {{ if $hideOnMobile }}hide-on-mobile{{ end }}"
{{- if eq .PageNumber $curr -}}aria-current="page"{{- end -}}
href="{{ .URL }}"
aria-label="Page {{ .PageNumber }}">
{{ .PageNumber }}
</a>
{{- end -}}
{{- end -}}
{{- /* Right ellipsis - show if the window end is before the second-to-last page */ -}}
{{- if lt $end $middleMaxPage -}}
<button
type="button"
class="page-link pagination-link pagination-jump-trigger"
aria-label="{{ i18n "pagination.jumpToPage" | default "Jump to page" }}">
{{ partial "helper/icon" "dots" }}
</button>
{{- end -}}
{{- /* Last page */ -}}
<a
class="page-link {{ if eq $curr $total }}current{{ end }}"
{{- if ne $curr $total -}}href="{{ $pag.Last.URL }}"{{- end -}}
aria-label="Page {{ $total }}"
{{- if eq $curr $total -}}aria-current="page"{{- end -}}>
{{ $total }}
</a>
{{- /* Next page button */ -}}
<a
class="page-link {{ if not $pag.Next }}disabled{{ end }}"
{{- if $pag.Next -}}
href="{{ $pag.Next.URL }}"
aria-label="Next page"
{{- end -}}
{{- if not $pag.Next -}}aria-disabled="true"{{- end -}}>
{{ partial "helper/icon" "chevron-right" }}
</a>
</nav>
<dialog id="pagination-jump-dialog" aria-labelledby="pagination-jump-title">
<form method="dialog" class="pagination-jump-form">
<header>
<label id="pagination-jump-title" class="title" for="pagination-jump-input">
{{ i18n "pagination.jumpToPage" | default "Jump to page:" }}
</label>
<span class="subtitle">(1 - {{ $pag.TotalPages }})</span>
</header>
<div class="pagination-jump-input-group">
<input
type="number"
id="pagination-jump-input"
min="1"
max="{{ $pag.TotalPages }}"
autofocus
autocomplete="off"
required>
<button type="submit" class="btn btn-primary">
{{ i18n "pagination.jump" | default "Go" }}
</button>
</div>
<div class="pagination-jump-hint">
<kbd>Enter</kbd>
<span>{{ i18n "pagination.pressEnter" | default "Press Enter to jump" }}</span>
</div>
</form>
</dialog>
{{- end -}}
@@ -0,0 +1,95 @@
<aside class="sidebar left-sidebar sticky {{ if .Site.Params.sidebar.compact }}compact{{ end }}">
<button class="hamburger hamburger--spin" type="button" id="toggle-menu" aria-label="{{ T `toggleMenu` }}">
<span class="hamburger-box">
<span class="hamburger-inner"></span>
</span>
</button>
<header>
{{ with .Site.Params.sidebar.avatar }}
{{- $avatar := partial "helper/image" (dict "Image" . "Resources" resources "Context" $) -}}
<figure class="site-avatar">
<a href="{{ $.Site.Home.RelPermalink }}">
<img src="{{ $avatar.Permalink }}"
width="{{ $avatar.Width }}"
height="{{ $avatar.Height }}"
class="site-logo"
loading="lazy"
alt="Avatar">
</a>
{{ with $.Site.Params.sidebar.emoji }}
<span class="emoji">{{ . }}</span>
{{ end }}
</figure>
{{ end }}
<div class="site-meta">
<h1 class="site-name"><a href="{{ .Site.Home.RelPermalink }}">{{ .Site.Title }}</a></h1>
<h2 class="site-description">{{ .Site.Params.sidebar.subtitle }}</h2>
</div>
</header>
{{- with .Site.Menus.social -}}
<ol class="menu-social">
{{ range . }}
<li>
<a
href='{{ .URL }}'
{{ if .Params.newTab }}target="_blank"{{ end }}
{{ with .Name }}title="{{ . }}"{{ end }}
rel="me"
>
{{ $icon := default "link" .Params.Icon }}
{{ with $icon }}
{{ partial "helper/icon" . }}
{{ end }}
</a>
</li>
{{ end }}
</ol>
{{- end -}}
<ol class="menu" id="main-menu">
{{ $currentPage := . }}
{{ range .Site.Menus.main }}
{{ $active := or (eq $currentPage.RelPermalink .URL) (or ($currentPage.HasMenuCurrent "main" .) ($currentPage.IsMenuCurrent "main" .)) }}
<li {{ if $active }} class='current'{{ end }}>
<a href='{{ .URL }}' {{ if .Params.newTab }}target="_blank"{{ end }}>
{{ with .Params.Icon }}
{{ partial "helper/icon" . }}
{{ end }}
<span>{{- .Name -}}</span>
</a>
</li>
{{ end }}
<li class="menu-bottom-section">
<ol class="menu">
{{- $currentLanguageCode := .Lang -}}
{{ if hugo.IsMultilingual }}
<li id="i18n-switch">
{{ partial "helper/icon" "language" }}
<select name="language" title="language" onchange="window.location.href = this.selectedOptions[0].value">
{{ range .Site.Home.AllTranslations }}
{{ $target := . }}
{{ range $.AllTranslations }}
{{ if eq .Lang $target.Lang }}
{{ $target = . }}
{{ end }}
{{ end }}
<option value="{{ $target.RelPermalink }}" {{ if eq $target.Lang $currentLanguageCode }}selected{{ end }}>{{ .Lang }}</option>
{{ end }}
</select>
</li>
{{ end }}
{{ if .Site.Params.colorScheme.toggle }}
<li id="dark-mode-toggle">
{{ partial "helper/icon" "toggle-left" }}
{{ partial "helper/icon" "toggle-right" }}
<span>{{ T "darkMode" }}</span>
</li>
{{ end }}
</ol>
</li>
</ol>
</aside>
@@ -0,0 +1,16 @@
{{- $scope := default "homepage" .Scope -}}
{{- $context := .Context -}}
{{- with (index .Context.Site.Params.widgets $scope) -}}
<aside class="sidebar right-sidebar sticky">
{{ range $widget := . }}
{{ if templates.Exists (printf "_partials/widget/%s.html" .type) }}
<!-- Ensure that the `params` is not nil -->
{{- $params := default dict .params -}}
{{ partial (printf "widget/%s" .type) (dict "Context" $context "Params" $params) }}
{{ else }}
{{ warnf "Widget %s not found" .type }}
{{ end }}
{{ end }}
</aside>
{{ end }}
@@ -0,0 +1,37 @@
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "archives") -}}
{{- $context := .Context -}}
{{- $limit := default 5 .Params.limit -}}
{{- if $query -}}
{{- $archivesPage := index $query 0 -}}
<section class="widget archives">
<div class="widget-icon">
{{ partial "helper/icon" "infinity" }}
</div>
<h2 class="widget-title section-title">
<a href="{{ $archivesPage.RelPermalink }}">
{{ T "widget.archives.title" }}
</a>
</h2>
{{ $pages := partial "helper/pages.html" $context.Site.Home }}
{{ $archives := $pages.GroupByDate "2006" }}
<div class="widget-archive--list">
{{ range $index, $item := first (add $limit 1) ($archives) }}
{{- $id := lower (replace $item.Key " " "-") -}}
<div class="archives-year">
<a href="{{ $archivesPage.RelPermalink }}#{{ $id }}">
{{ if eq $index $limit }}
<span class="year">{{ T "widget.archives.more" }}</span>
{{ else }}
<span class="year">{{ .Key }}</span>
<span class="count">{{ len $item.Pages }}</span>
{{ end }}
</a>
</div>
{{ end }}
</div>
</section>
{{- else -}}
{{- warnf "Archives page not found. Create a page with layout: archives." -}}
{{- end -}}
@@ -0,0 +1,2 @@
{{ $params := merge (dict "taxonomy" "categories" "title" (T "widget.categoriesCloud.title") "icon" "categories") .Params }}
{{ partial "widget/taxonomy" (dict "Context" .Context "Params" $params) }}
@@ -0,0 +1,16 @@
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "search") -}}
{{- if $query -}}
{{- $searchPage := index $query 0 -}}
<form action="{{ $searchPage.RelPermalink }}" class="search-form widget" {{ with .OutputFormats.Get "json" -}}data-json="{{ .RelPermalink }}" {{- end }}>
<p>
<label>{{ T "search.title" }}</label>
<input name="keyword" required placeholder="{{ T `search.placeholder` }}" />
<button title="{{ T `search.title` }}">
{{ partial "helper/icon" "search" }}
</button>
</p>
</form>
{{- else -}}
{{- warnf "Search page not found. Create a page with layout: search." -}}
{{- end -}}
@@ -0,0 +1,2 @@
{{ $params := merge (dict "taxonomy" "tags" "title" (T "widget.tagCloud.title") "icon" "tag") .Params }}
{{ partial "widget/taxonomy" (dict "Context" .Context "Params" $params) }}
@@ -0,0 +1,39 @@
{{- $context := .Context -}}
{{- if not .Params.taxonomy -}}
{{- warnf "Taxonomy field is required" -}}
{{- end -}}
{{- $taxonomy := index $context.Site.Taxonomies .Params.taxonomy -}}
{{- if not $taxonomy -}}
{{- warnf "Taxonomy %s not found" .Params.taxonomy -}}
{{- end -}}
{{- $taxonomyPage := $taxonomy.Page -}}
{{- $widgetTitle := .Params.title -}}
{{- $limit := default 10 .Params.limit -}}
{{- $icon := default .Params.taxonomy .Params.icon -}}
{{- $showLink := default true .Params.showLink -}}
<section class="widget tagCloud">
<div class="widget-icon">
{{ partial "helper/icon" $icon }}
</div>
<h2 class="widget-title section-title">
{{ if and $showLink $taxonomyPage }}
<a href="{{ $taxonomyPage.RelPermalink }}">
{{ $widgetTitle }}
</a>
{{ else }}
{{ $widgetTitle }}
{{ end }}
</h2>
<div class="tagCloud-tags">
{{ range first $limit $taxonomy.ByCount }}
<a href="{{ .Page.RelPermalink }}">
{{ .Page.Title }}
</a>
{{ end }}
</div>
</section>
@@ -0,0 +1,14 @@
{{ if (.Context.Scratch.Get "TOCEnabled") }}
<section class="widget archives">
<div class="widget-icon">
{{ partial "helper/icon" "hash" }}
</div>
<h2 class="widget-title section-title">{{ T "article.tableOfContents" }}</h2>
<div class="widget--toc">
<nav class="toc-nav" id="TableOfContents">
{{ partial "article/components/toc" .Context | safeHTML }}
</nav>
</div>
</section>
{{ end }}