Compare commits

...
22 Commits
Author SHA1 Message Date
Jimmy Cai c0f57bab7a release: 4.0.1 2026-04-26 20:21:35 +02:00
JimmyandGitHub 752fd5805f fix(image): correct permalink handling for images placed in static folder (#1330)
* fix(image): correct permalink handling for images placed in `static` folder

related: https://github.com/CaiJimmy/hugo-theme-stack/discussions/1307

* Remove urls.AbsURL
2026-04-26 20:20:51 +02:00
0fc8264927 fix: comment scripts bypass cookie consent (#1308)
* fix: comment scripts bypass cookie consent

When GDPR cookie consent is enabled, the comments container is simply
hidden with CSS. However, this still allows all third-party script
tags within the container to be evaluated and downloaded by the
browser.

This PR uses the <template> tag which stops inner scripts from evaluating.
Once consent is granted, the template content is cloned and inserted
into the container, and the scripts are manually loaded in order.

* refactor(comments): move consent-gated loader to TS asset

* fix(comments): add integrity attribute to comments consent script

* style: align comments consent docs and naming

* refactor(comments): simplify script handling

---------

Co-authored-by: Jimmy Cai <jimmy@cai.im>
2026-04-26 18:51:22 +02:00
55340540b5 feat: support Artalk comment system (#1306)
* feat: support artalk comment system

* chore: change url from testingcf to cdn prefix

* refactor(comments): align artalk provider style and keep custom css overrides

* fix: update Artalk CDN links to use the correct URL

* refactor(comments): simplify Artalk implementation

* fix(artalk): update script tag to use ESM for Artalk

* fix(artalk): set locale to 'auto' for Artalk comments

* refactor: move artalk style and script link to external.toml

---------

Co-authored-by: Jimmy Cai <jimmy@cai.im>
2026-04-26 18:05:54 +02:00
powerfullzandGitHub df9253a8e9 fix(cookies): avoid banner wrapper blocking clicks (#1327) 2026-04-26 15:47:55 +02:00
96202fea2b accessibility: fix prohibited ARIA attributes for disabled previous page & next page elements (#1312)
accessibility: fix prohibited ARIA attributes for previous and next page button, if they are disabled

Co-authored-by: Jimmy Cai <jimmy@cai.im>
2026-04-26 15:42:06 +02:00
powerfullzandGitHub e55168d873 fix: optimize code block selection experience (#1311) 2026-04-26 13:50:19 +02:00
Jimmy Cai e0418efe2f chore: update Hugo version to 0.160.1 in build script 2026-04-26 13:43:09 +02:00
JimmyandGitHub 08a8b5d767 feat: codeblock improvements (#1328)
* fix: do not show codeblock copy button if clipboard api is not available

related: https://github.com/CaiJimmy/hugo-theme-stack/issues/1309

* fix: remove duplicated codeblock script in main.ts

* feat: add i18n support to codeblock copy button

* feat: add translation strings for codeblock copy button
2026-04-26 13:40:46 +02:00
powerfullzandGitHub 9ddd11797b fix: replace deprecated .Site.Data with hugo.Data (#1322)
fix: replace deprecated .Site.Data with hugo.Data

Use hugo.Data instead of deprecated .Site.Data (Hugo v0.156.0+)
2026-04-26 13:16:38 +02:00
powerfullzandGitHub 6f3ab760f9 fix: correct permalink casing for OpenGraph and Twitter image tags (#1324) 2026-04-26 13:14:34 +02:00
4f2764e0a5 fix: related content overflow and horizontal scroll issues (#1305)
* fix: resolve related content card overflow and page horizontal scroll

* fix: prevent related content overflow on mobile while preserving shadow space

* fix: align related content cards and prevent shadow clipping

Expands the left margin of the related content carousel to match the main article, preserving the full card shadow. Adds a visual mask gradient to gracefully fade out the right edge.

* feat: add mask image also to left side of subsection list, which is activated when scrolling

* feat: fade out right mask image near the end of horizontal scroll

---------

Co-authored-by: Jimmy Cai <jimmy@cai.im>
2026-04-26 13:12:49 +02:00
37f6a77a1a feat: Add jump-to-page dialog and improve pagination logic and styling (#1303)
* feat: add jump-to-page dialog on pagination ellipsis

- Replace pagination windowing algorithm with a tighter 5-item window
  (curr-1 / curr / curr+1, clamped at edges) and hide-on-mobile for
  pages more than 1 step away from the current page
- Ellipsis spans now carry the pagination-jump-trigger class and open
  a native <dialog> on click instead of being static decorations
- Add <dialog id="pagination-jump-dialog"> with number input, Go button,
  and Enter-key hint; data-total / data-first-url / data-format-url
  attributes on <nav> give the JS all it needs to build the target URL
- Add assets/ts/pagination.ts: setupPaginationJump() with Fisher-Yates
  backdrop-click and ESC handling, animated open/close via .closing class
- Wire setupPaginationJump() into Stack.init() in main.ts
- Extend pagination.scss with .hide-on-mobile, .pagination-jump-trigger
  hover style, and the full #pagination-jump-dialog styling + keyframe
  scale-up/scale-down animations
- Add [pagination] i18n keys (jumpToPage / jump / pressEnter) to
  en / zh / zh-hant-tw / zh-hant-hk / ja

* style: align pagination code with upstream conventions

- pagination.html: collapse multi-line span/class/aria attrs to single
  lines; use HTML5 void element syntax; fix hint div indentation
- pagination.scss: 2-space indent → 4-space indent
- pagination.ts: tab indent → 4-space indent; export const arrow fn →
  export function declaration

* chore: update layouts/_partials/pagination.html

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: add aria-labelledby attrributes for better accesibility

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: use { once: true } for the listener and guard against re-entry when .closing is already set.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* style: replace hardcoded values with design tokens and improve dialog UX

- Replace all hardcoded px spacing in .page-link with --spacing-* tokens
- Use @include card-style in #pagination-jump-dialog; override with --shadow-l2
- Replace padding: 24px with var(--card-padding) for responsive dialog padding
- Align header title/subtitle to space-between for cleaner layout
- Replace hardcoded gaps/padding in form with spacing tokens
- Fix input/button border-radius from 8px to 6px (better proportion for 44px height)
- Fix focus ring: use color-mix(accent-color 20%) so it's visible in dark mode
- Add :focus-visible + outline:none to fully suppress native browser outline
- Remove translateY from button hover for more restrained interaction
- Scope transition to specific properties (border-color, box-shadow, background-color)

* chore: add pagination test articles for easier debugging

* style: enhance backdrop animations for pagination jump dialog

* style: replace pagination ellipsis spans with buttons for improved accessibility

* fix: check if browser supports dialog API before continue with pagination jump setup

* fix: retain focus on last active element when closing pagination jump dialog

* fix: style for page-link button (pagination-jump)

* style: adjust width of pagination jump dialog, add container padding

* style: simplify

* refactor: simplify pagination link logic

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jimmy Cai <jimmy@cai.im>
2026-04-26 12:41:31 +02:00
JimmyandGitHub 8e3687431f fix: ensure taxonomy widget title is only linked if a taxonomy page exits (#1314)
fix: ensure taxonomy widget title is only linked if a taxonomy page exists
2026-03-21 12:01:29 +01:00
JimmyandGitHub cfcdb20d17 refactor: remove custom scrollbar styles and related variables (#1304) 2026-03-08 13:23:23 +01:00
0c941ebb04 fix: skip inaccessible images to avoid build failure (#1301)
* skip inaccessible images to avoid build failure

* undo changes to $result and $local

* fix: initialize $resource as a dictionary instead of an empty string

* feat: add external image processing timeout configuration

---------

Co-authored-by: Jimmy Cai <jimmy@cai.im>
2026-03-08 13:18:21 +01:00
JimmyandGitHub f13c478718 fix: check if resource exists before accessing .RelPermalink (#1299)
* fix: check if resource exists before accessing `.RelPermalink`

closes https://github.com/CaiJimmy/hugo-theme-stack/issues/1297

* Set default value of $result in `helper/image` to dict
2026-03-06 00:13:13 +01:00
JimmyandGitHub bde2914dec chore: Remove featuredImageField from configuration (#1298)
chore: Remove `featuredImageField` from configuration.
2026-03-05 23:56:27 +01:00
JimmyandGitHub f5adf4a273 fix: do not display the date of term pages (#1296) 2026-03-05 18:25:04 +01:00
JimmyandGitHub 07fa7615eb refactor: simplify article-list/tile partial context passing (#1295)
refactor: simplify `article-list/tile` partial context passing and internal usage.
2026-03-05 18:15:05 +01:00
JimmyandGitHub 628bb9a501 feat: use images.AutoOrient in helper/image (#1294)
* feat: use images.AutoOrient in `helper/image`

closes https://github.com/CaiJimmy/hugo-theme-stack/issues/1292

* fix: check if IsImageResourceProcessable before using filter

* remove local field

* feat: add configurable image auto-orientation via a new `autoOrient` parameter in site configuration
2026-03-05 17:58:40 +01:00
JimmyandGitHub 11aa9e349a refactor: rename article-footer to article-meta (#1293)
* refactor: rename `article-footer` to `article-meta`, and remove unnecesary structures

* fix: Update default article license URL to point to Creative Commons.
2026-03-05 17:10:35 +01:00
89 changed files with 1314 additions and 312 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
main() {
DART_SASS_VERSION=1.97.1
GO_VERSION=1.25.5
HUGO_VERSION=0.157.0
HUGO_VERSION=0.160.1
NODE_VERSION=24.12.0
export TZ=Europe/Oslo
+10 -23
View File
@@ -71,12 +71,11 @@
gap: 8px;
}
.article-footer,
.article-translations,
.article-header-tags {
.article-meta {
display: flex;
color: var(--card-text-color-tertiary);
gap: var(--spacing-md);
flex-direction: column;
gap: var(--spacing-lg);
font-size: 1.4rem;
svg {
vertical-align: middle;
@@ -86,30 +85,22 @@
flex-shrink: 0;
}
&,
time,
a {
a,
span {
font-size: 1.4rem;
color: var(--card-text-color-tertiary);
}
& > div {
& > .inline-meta {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-md);
}
}
.article-time {
flex-wrap: wrap;
}
.article-translations,
.article-header-tags {
& > div {
flex-wrap: wrap;
}
}
.article-category,
.article-tags {
display: flex;
@@ -172,11 +163,7 @@
object-fit: cover;
}
}
.article-footer {
font-size: 1.4rem;
}
.article-preview {
font-size: 1.4rem;
color: var(--card-text-color-tertiary);
+2 -22
View File
@@ -1,6 +1,7 @@
html {
font-size: 62.5%;
overflow-y: scroll;
scrollbar-gutter: stable;
}
* {
@@ -17,25 +18,4 @@ body {
-ms-text-autospace: ideograph-alpha;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* scrollbar styles for Firefox */
* {
scrollbar-width: auto;
scrollbar-color: var(--scrollbar-thumb) transparent;
}
/**/
/* scrollbar styles for Chromium */
::-webkit-scrollbar {
height: auto;
}
::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
}
::-webkit-scrollbar-track {
background-color: transparent;
}
/**/
}
+80
View File
@@ -0,0 +1,80 @@
@import "../../breakpoints";
@import "../../mixins";
.artalk-container {
@include card-style;
padding: 5px;
@include respond(md) {
padding: var(--card-padding);
}
}
.artalk {
--at-color-bg: var(--card-background);
--at-color-font: var(--card-text-color-main);
--at-color-sub: var(--card-text-color-secondary);
--at-color-main: var(--accent-color);
--at-color-border: var(--card-separator-color);
--at-border-radius: var(--card-border-radius);
--at-color-link: var(--accent-color);
}
.artalk a {
color: var(--at-color-link);
text-decoration: none;
transition: color 0.3s ease;
&:hover {
color: var(--accent-color-darker);
}
}
.artalk .atk-content a,
.artalk .atk-editor-plug-preview a {
color: var(--accent-color);
box-shadow: 0px -2px 0px rgba(var(--link-background-color), var(--link-background-opacity)) inset;
transition: all 0.3s ease;
&:hover {
color: var(--accent-color-darker);
box-shadow: 0px calc(-1rem * var(--article-line-height)) 0px rgba(var(--link-background-color), var(--link-background-opacity-hover)) inset;
}
}
[data-scheme="dark"] .artalk {
--at-color-main: var(--accent-color);
--at-color-link: var(--accent-color);
--at-color-bg: var(--card-background);
--at-color-bg-transl: rgba(66, 66, 66, 0.95);
--at-color-bg-grey: rgba(255, 255, 255, 0.08);
--at-color-bg-grey-transl: rgba(255, 255, 255, 0.06);
--at-color-bg-light: rgba(255, 255, 255, 0.08);
--at-color-border: var(--card-separator-color);
--at-color-font: var(--card-text-color-main);
--at-color-deep: var(--card-text-color-main);
--at-color-sub: var(--card-text-color-secondary);
--at-color-meta: var(--card-text-color-secondary);
--at-color-grey: var(--card-text-color-secondary);
--at-color-gradient: linear-gradient(
180deg,
transparent,
var(--card-background)
);
}
.artalk code {
font-family: var(--code-font-family);
}
.artalk .atk-avatar img {
border-radius: var(--card-border-radius);
}
[data-scheme="dark"] .artalk .atk-avatar img {
filter: brightness(0.75);
}
[data-scheme="dark"] .artalk .atk-send-btn {
color: var(--accent-color-text);
}
+2
View File
@@ -5,6 +5,7 @@
right: 0;
z-index: 9999;
padding: var(--container-padding);
pointer-events: none;
&[aria-hidden="true"] {
display: none;
@@ -21,6 +22,7 @@
display: flex;
flex-direction: column;
gap: var(--spacing-md);
pointer-events: auto;
@include respond(md) {
flex-direction: row;
+7 -1
View File
@@ -52,12 +52,18 @@
/* LineNumbersTable */
.chroma .lnt {
margin-right: 0.4em;
margin-right: 0;
user-select: none;
padding: 0 0.4em 0 0.4em;
color: #7f7f7f;
display: block;
}
/* Optimise code block selection experience */
span.line {
margin-left: 0.4em;
}
/* LineNumbers */
.chroma .ln {
margin-right: 0.4em;
+1 -39
View File
@@ -45,41 +45,7 @@
.article-footer {
margin: var(--card-padding);
margin-top: 0;
section:not(:first-child) {
margin-top: var(--card-padding);
}
section {
color: var(--card-text-color-tertiary);
text-transform: uppercase;
display: flex;
align-items: center;
font-size: 1.4rem;
gap: 15px;
svg {
width: 20px;
height: 20px;
stroke-width: 1.33;
}
}
.article-tags {
text-transform: unset;
}
.article-copyright,
.article-lastmod {
a {
color: var(--body-text-color);
}
a.link {
box-shadow: unset;
}
}
gap: var(--spacing-2xl);
}
}
}
@@ -167,10 +133,6 @@
display: flex;
flex-direction: column;
color: var(--card-text-color-main);
::-webkit-scrollbar-thumb {
background-color: var(--card-separator-color);
}
}
.widget--toc {
+79 -5
View File
@@ -44,24 +44,79 @@
}
}
@property --left-edge-fade-size {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --right-edge-fade-size {
syntax: "<length>";
inherits: false;
initial-value: 64px;
}
.subsection-list {
--shadow-overlap-x: calc(var(--shadow-l2-blur) + 4px);
--shadow-overlap-y: calc(var(--shadow-l2-blur) + var(--shadow-l2-y) + 4px);
--edge-fade-size: 64px;
--left-edge-fade-size: 0px;
--right-edge-fade-size: var(--edge-fade-size);
overflow-x: auto;
/// Limit the horizontal overlap to the container padding to prevent page overflow on mobile.
/// On desktop, it still allows the full shadow to be visible.
--overlap-x: min(var(--shadow-overlap-x), var(--container-padding));
/// These values are computed from the shadow of the article-list--tile (which uses shadow-l1 and shadow-l2)
/// so that the shadow is not cut off
margin-left: calc(var(--shadow-overlap-x) * -1);
margin-right: calc(var(--shadow-overlap-x) * -1);
/// so that the shadow is not cut off, while staying within the viewport on small screens.
/// Clamp both sides to prevent page-level horizontal overflow.
margin-left: calc(var(--overlap-x) * -1);
margin-right: calc(var(--overlap-x) * -1);
margin-bottom: calc(var(--shadow-overlap-y) * -1);
/// Smooth fade on both horizontal edges to avoid hard cuts where shadows are clamped.
--subsection-mask-image: linear-gradient(
to right,
transparent 0%,
rgb(0 0 0 / 0.06) calc(var(--left-edge-fade-size) * 0.12),
rgb(0 0 0 / 0.2) calc(var(--left-edge-fade-size) * 0.3),
rgb(0 0 0 / 0.45) calc(var(--left-edge-fade-size) * 0.52),
rgb(0 0 0 / 0.72) calc(var(--left-edge-fade-size) * 0.76),
black var(--left-edge-fade-size),
black calc(100% - var(--right-edge-fade-size)),
rgb(0 0 0 / 0.72) calc(100% - (var(--right-edge-fade-size) * 0.76)),
rgb(0 0 0 / 0.45) calc(100% - (var(--right-edge-fade-size) * 0.52)),
rgb(0 0 0 / 0.2) calc(100% - (var(--right-edge-fade-size) * 0.3)),
rgb(0 0 0 / 0.06) calc(100% - (var(--right-edge-fade-size) * 0.12)),
transparent 100%
);
-webkit-mask-image: var(--subsection-mask-image);
mask-image: var(--subsection-mask-image);
// CSS-only: left fade ramps in quickly once horizontal scrolling starts,
// and right fade turns off near the end of horizontal scroll.
animation: subsection-left-fade linear both, subsection-right-fade linear both;
animation-timeline: scroll(self inline);
animation-range: 1px 24px, 0% 100%;
animation-timing-function: ease-out, linear;
[dir="rtl"] & {
margin-left: calc(var(--overlap-x) * -1);
margin-right: calc(var(--overlap-x) * -1);
}
.article-list--tile {
display: flex;
padding: var(--spacing-xs) var(--shadow-overlap-x) var(--shadow-overlap-y) var(--shadow-overlap-x);
padding: var(--spacing-xs) var(--overlap-x) var(--shadow-overlap-y) var(--overlap-x);
width: max-content;
gap: var(--spacing-lg);
[dir="rtl"] & {
padding: var(--spacing-xs) var(--overlap-x) var(--shadow-overlap-y) var(--overlap-x);
}
article {
flex-shrink: 0;
@@ -75,4 +130,23 @@
}
}
}
}
}
@keyframes subsection-left-fade {
from {
--left-edge-fade-size: 0px;
}
to {
--left-edge-fade-size: var(--edge-fade-size);
}
}
@keyframes subsection-right-fade {
0%,
92% {
--right-edge-fade-size: var(--edge-fade-size);
}
100% {
--right-edge-fade-size: 0px;
}
}
+216 -5
View File
@@ -3,22 +3,44 @@
@include card-style;
overflow: hidden;
.hide-on-mobile,
.pagination-link {
display: none;
@include respond(md) {
display: flex;
}
}
.page-link {
padding: 16px 0;
padding: var(--spacing-sm) 0;
margin: 0;
border: 0;
background: none;
font: inherit;
border-radius: 0;
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 auto;
min-width: 48px;
min-width: 36px;
color: var(--card-text-color-secondary);
text-decoration: none;
@include respond(md) {
min-width: 48px;
padding: var(--spacing-md) 0;
}
&:first-child,
&:last-child {
flex: 0 0 48px;
padding: 16px;
flex: 0 0 var(--spacing-3xl);
padding: var(--spacing-sm);
@include respond(md) {
flex: 0 0 48px;
padding: var(--spacing-md);
}
}
&:hover:not(.current):not(.disabled) {
@@ -44,5 +66,194 @@
width: 20px;
height: 20px;
}
&.pagination-jump-trigger {
cursor: pointer;
}
}
}
}
#pagination-jump-dialog {
border: none;
@include card-style;
box-shadow: var(--shadow-l2);
color: var(--body-text-color);
margin: auto; // Center dialog
width: calc(100% - (var(--container-padding) * 2));
max-width: 320px;
padding: var(--card-padding);
&::backdrop {
background-color: rgba(0, 0, 0, 0.4);
opacity: 0;
animation: backdrop-fade-in 0.3s ease forwards;
}
&[open] {
animation: scale-up 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
&.closing {
animation: scale-down 0.2s ease forwards;
&::backdrop {
animation: backdrop-fade-out 0.2s ease forwards;
}
}
.pagination-jump-form {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-xs);
label {
cursor: pointer;
}
.title {
font-size: 1.6rem;
font-weight: 600;
color: var(--card-text-color-main);
margin: 0;
}
.subtitle {
font-size: 1.3rem;
color: var(--card-text-color-tertiary);
}
}
.pagination-jump-input-group {
display: flex;
gap: var(--spacing-sm);
align-items: stretch;
input[type="number"] {
flex: 1;
width: 100%;
min-width: 0;
padding: 0 var(--spacing-md);
height: 44px; // Explicit height for perfect alignment
border: 1.5px solid var(--card-separator-color);
border-radius: 6px;
background-color: var(--body-background);
color: var(--card-text-color-main);
font-size: 1.6rem;
outline: none;
-webkit-appearance: none;
appearance: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
/* Hide spinner */
-moz-appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&:focus,
&:focus-visible {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color) 20%, transparent);
}
}
.btn-primary {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 var(--spacing-xl);
height: 44px; // Match input height
background-color: var(--accent-color);
color: var(--accent-color-text);
border: none;
border-radius: 6px;
font-size: 1.5rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
background-color: var(--accent-color-darker);
box-shadow: var(--shadow-l1);
}
&:active {
box-shadow: none;
}
}
}
.pagination-jump-hint {
display: flex;
align-items: center;
justify-content: flex-start;
gap: var(--spacing-sm);
font-size: 1.3rem;
color: var(--card-text-color-tertiary);
margin-top: calc(var(--spacing-xs) * -1);
kbd {
background-color: var(--body-background);
border: 1px solid var(--card-separator-color);
border-radius: 4px;
padding: 3px 6px;
font-family: var(--base-font-family);
font-size: 1.1rem;
box-shadow: 0 2px 0 var(--card-separator-color);
color: var(--card-text-color-main);
font-weight: 600;
line-height: 1;
}
}
}
@keyframes scale-up {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes scale-down {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
@keyframes backdrop-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes backdrop-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
-5
View File
@@ -19,17 +19,12 @@
--section-separation: 40px;
--scrollbar-thumb: hsl(0, 0%, 85%);
--scrollbar-track: var(--body-background);
&[data-scheme="dark"] {
--body-background: #303030;
--accent-color: #ecf0f1;
--accent-color-darker: #bdc3c7;
--accent-color-text: #000;
--body-text-color: rgba(255, 255, 255, 0.7);
--scrollbar-thumb: hsl(0, 0%, 40%);
--scrollbar-track: var(--body-background);
}
}
+41
View File
@@ -0,0 +1,41 @@
import * as params from '@params';
export function setupCodeCopy() {
/**
* Add copy button to code block
*/
const highlights = document.querySelectorAll('.article-content div.highlight');
const copyText = params.codeblock.copy,
copiedText = params.codeblock.copied;
if (!navigator.clipboard) {
/// Clipboard API is only supported in secure contexts (HTTPS)
console.warn('Clipboard API not supported, copy button will not work.');
return;
}
highlights.forEach(highlight => {
const copyButton = document.createElement('button');
copyButton.innerHTML = copyText;
copyButton.classList.add('copyCodeButton');
highlight.appendChild(copyButton);
const codeBlock = highlight.querySelector('code[data-lang]');
if (!codeBlock) return;
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(codeBlock.textContent)
.then(() => {
copyButton.textContent = copiedText;
setTimeout(() => {
copyButton.textContent = copyText;
}, 1000);
})
.catch(err => {
alert(err)
console.log('Something went wrong', err);
});
});
});
};
+141
View File
@@ -0,0 +1,141 @@
/*
* Consent-gated comments bootstrap.
*
* This module controls whether third-party comments are rendered based on
* functional cookie consent, and injects template scripts in sequence to
* avoid eager or duplicate script execution.
*/
/**
* Consent event payload used by the comments gate.
* It mirrors cookie consent state shape but keeps optional fields for safety.
*/
interface CommentsConsentState {
necessary?: boolean;
analytics?: boolean;
functional?: boolean;
timestamp?: number;
}
/**
* Resolve functional consent with layered fallback:
* 1) explicit event detail, 2) CookieConsent public API, 3) dataset mirror.
*/
const hasFunctionalConsent = (consentDetail?: CommentsConsentState | null): boolean => {
const cookieConsent = (window as Window & {
cookieConsent?: {
hasConsent?: (category: 'necessary' | 'analytics' | 'functional') => boolean;
};
}).cookieConsent;
return (
consentDetail?.functional ??
cookieConsent?.hasConsent?.('functional') ??
document.documentElement.dataset.consentFunctional === 'true'
);
};
interface DeferredScript {
placeholder: Comment;
script: HTMLScriptElement;
}
const prepareDeferredScripts = (fragment: DocumentFragment): DeferredScript[] => {
return Array.from(fragment.querySelectorAll('script')).map(script => {
const placeholder = document.createComment('comments-script-placeholder');
script.replaceWith(placeholder);
return { placeholder, script };
});
};
const activateDeferredScripts = async (scripts: DeferredScript[]): Promise<void> => {
// Inject scripts sequentially so third-party embeds can rely on execution order.
for (const { placeholder, script } of scripts) {
if (!placeholder.parentNode) {
continue;
}
const newScript = document.createElement('script');
Array.from(script.attributes).forEach(attr => {
newScript.setAttribute(attr.name, attr.value);
});
if (script.textContent) {
newScript.text = script.textContent;
}
let done: Promise<void> = Promise.resolve();
if (newScript.src) {
done = new Promise<void>(resolve => {
newScript.onload = newScript.onerror = () => resolve();
});
}
placeholder.replaceWith(newScript);
await done;
}
};
const initCommentsConsent = (): void => {
// Main entry: gate comments rendering by functional cookie consent.
const placeholder = document.getElementById('comments-consent-placeholder') as HTMLElement | null;
const container = document.getElementById('comments-container') as HTMLElement | null;
const template = document.getElementById('comments-template') as HTMLTemplateElement | null;
if (!placeholder || !container || !template) {
return;
}
let commentsLoaded = false;
let commentsLoading = false;
const showComments = async (): Promise<void> => {
placeholder.style.display = 'none';
container.style.display = 'block';
if (commentsLoaded || commentsLoading) {
return;
}
// Prevent duplicate template hydration while scripts are still loading.
commentsLoading = true;
try {
const clone = template.content.cloneNode(true) as DocumentFragment;
const scripts = prepareDeferredScripts(clone);
container.appendChild(clone);
await activateDeferredScripts(scripts);
commentsLoaded = true;
} finally {
commentsLoading = false;
}
};
const hideComments = (): void => {
placeholder.style.display = 'block';
container.style.display = 'none';
};
window.addEventListener('onCookieConsentChange', (event: Event) => {
// Cross-module contract: this event is dispatched by cookies.ts.
const customEvent = event as CustomEvent<CommentsConsentState | null>;
if (hasFunctionalConsent(customEvent.detail)) {
showComments();
} else {
hideComments();
}
});
if (hasFunctionalConsent()) {
showComments();
} else {
hideComments();
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCommentsConsent, { once: true });
} else {
initCommentsConsent();
}
+1
View File
@@ -175,6 +175,7 @@ class CookieConsent {
}
private dispatchConsentEvent(): void {
// Cross-module event consumed by consent-gated features (for example commentsConsent.ts).
const event = new CustomEvent('onCookieConsentChange', {
detail: this.state
});
+4 -31
View File
@@ -10,6 +10,8 @@ import createElement from './createElement';
import StackColorScheme from './colorScheme';
import { setupScrollspy } from './scrollspy';
import { setupSmoothAnchors } from './smoothAnchors';
import { setupPaginationJump } from './pagination';
import { setupCodeCopy } from './code-copy';
let Stack = {
init: () => {
@@ -22,39 +24,10 @@ let Stack = {
if (articleContent) {
setupSmoothAnchors();
setupScrollspy();
setupCodeCopy();
}
/**
* Add copy button to code block
*/
const highlights = document.querySelectorAll('.article-content div.highlight');
const copyText = `Copy`,
copiedText = `Copied!`;
highlights.forEach(highlight => {
const copyButton = document.createElement('button');
copyButton.innerHTML = copyText;
copyButton.classList.add('copyCodeButton');
highlight.appendChild(copyButton);
const codeBlock = highlight.querySelector('code[data-lang]');
if (!codeBlock) return;
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(codeBlock.textContent)
.then(() => {
copyButton.textContent = copiedText;
setTimeout(() => {
copyButton.textContent = copyText;
}, 1000);
})
.catch(err => {
alert(err)
console.log('Something went wrong', err);
});
});
});
setupPaginationJump();
new StackColorScheme(document.getElementById('dark-mode-toggle')!);
}
+104
View File
@@ -0,0 +1,104 @@
export function setupPaginationJump() {
const triggers = document.querySelectorAll<HTMLButtonElement>('.pagination-jump-trigger');
const dialog = document.getElementById('pagination-jump-dialog') as HTMLDialogElement;
if (!dialog || triggers.length === 0) return;
const nav = document.querySelector('.pagination') as HTMLElement;
const input = document.getElementById('pagination-jump-input') as HTMLInputElement;
const form = dialog.querySelector('.pagination-jump-form') as HTMLFormElement;
const supportsDialog = typeof dialog.showModal === 'function' && typeof dialog.close === 'function';
let lastFocusedElement: HTMLElement | null = null;
if (!supportsDialog || !nav || !input || !form) return;
const closeDialog = (): void => {
if (dialog.classList.contains('closing')) return;
dialog.classList.add('closing');
dialog.addEventListener(
'animationend',
() => {
dialog.classList.remove('closing');
dialog.close();
if (lastFocusedElement?.isConnected) {
lastFocusedElement.focus();
}
},
{ once: true }
);
};
// Open dialog when triggers are clicked
triggers.forEach((trigger) => {
trigger.addEventListener('click', () => {
const activeElement = document.activeElement;
lastFocusedElement = activeElement instanceof HTMLElement ? activeElement : trigger;
dialog.showModal();
input.value = '';
input.focus();
});
});
// Handle ESC key closing the dialog
dialog.addEventListener('cancel', (e) => {
e.preventDefault();
closeDialog();
});
// Close dialog when clicking backdrop
dialog.addEventListener('click', (e) => {
const rect = dialog.getBoundingClientRect();
const isInDialog =
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width;
if (!isInDialog) {
closeDialog();
}
});
// Allow Enter key to trigger validation and submit
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (form.reportValidity()) {
form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
}
});
// Handle form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const targetPage = parseInt(input.value);
if (isNaN(targetPage) || targetPage < 1) return;
const totalPages = parseInt(nav.dataset.total || '0');
if (targetPage > totalPages) return;
const firstUrl = nav.dataset.firstUrl || '';
const formatUrl = nav.dataset.formatUrl || '';
let targetUrl = '';
if (targetPage === 1) {
targetUrl = firstUrl;
} else {
// formatUrl is the URL for page 2. E.g., /tags/page/2/ or /page/2/
// Replace the '2' before the trailing slash or .html with the target page number
targetUrl = formatUrl.replace(/2([^\d]*)$/, `${targetPage}$1`);
}
if (targetUrl) {
window.location.href = targetUrl;
}
closeDialog();
});
}
+12 -4
View File
@@ -1,10 +1,9 @@
mainSections = ["post"]
featuredImageField = "image"
rssFullContent = true
mainSections = ["post"]
rssFullContent = true
# Accepted values: "default", "lastmod"
# default = see https://gohugo.io/quick-reference/glossary/#default-sort-order
# lastmod = sort by last modified date, in descending order
SortBy = "default"
SortBy = "default"
[footer]
since = 2026
@@ -76,6 +75,11 @@ SortBy = "default"
default = "auto"
[imageProcessing]
autoOrient = false
[imageProcessing.external]
timeout = "5s"
[imageProcessing.content]
widths = [800, 1600, 2400]
enabled = true
@@ -97,6 +101,10 @@ SortBy = "default"
enabled = true
provider = "disqus"
[comments.artalk]
serverUrl = ""
site = ""
[comments.disqusjs]
shortname = ""
apiUrl = ""
+4
View File
@@ -13,3 +13,7 @@ Cactus = [
Style = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe.css"
Core = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe.esm.min.js"
Lightbox = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe-lightbox.esm.min.js"
[Artalk]
Style = "https://cdn.jsdelivr.net/npm/artalk@2.9.1/dist/Artalk.css"
Script = "https://cdn.jsdelivr.net/npm/artalk@2.9.1/+esm"
+1 -1
View File
@@ -12,7 +12,7 @@ hasCJKLanguage = false
path = "github.com/CaiJimmy/hugo-theme-stack/v3"
[pagination]
pagerSize = 5
pagerSize = 3
[permalinks]
post = "/p/:slug/"
+1 -1
View File
@@ -13,7 +13,7 @@ favicon = "img/avatar.png"
[article]
[article.license]
enabled = true
default = "Licensed under CC BY-NC-SA 4.0"
default = "Licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)"
[widgets]
homepage = [
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 01
date: 2000-01-01
description: Test post 01 for pagination behavior.
slug: pagination-test-01
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **01**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 02
date: 2000-01-02
description: Test post 02 for pagination behavior.
slug: pagination-test-02
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **02**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 03
date: 2000-01-03
description: Test post 03 for pagination behavior.
slug: pagination-test-03
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **03**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 04
date: 2000-01-04
description: Test post 04 for pagination behavior.
slug: pagination-test-04
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **04**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 05
date: 2000-01-05
description: Test post 05 for pagination behavior.
slug: pagination-test-05
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **05**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 06
date: 2000-01-06
description: Test post 06 for pagination behavior.
slug: pagination-test-06
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **06**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 07
date: 2000-01-07
description: Test post 07 for pagination behavior.
slug: pagination-test-07
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **07**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 08
date: 2000-01-08
description: Test post 08 for pagination behavior.
slug: pagination-test-08
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **08**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 09
date: 2000-01-09
description: Test post 09 for pagination behavior.
slug: pagination-test-09
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **09**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 10
date: 2000-01-10
description: Test post 10 for pagination behavior.
slug: pagination-test-10
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **10**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 11
date: 2000-01-11
description: Test post 11 for pagination behavior.
slug: pagination-test-11
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **11**.
+14
View File
@@ -0,0 +1,14 @@
---
title: Pagination Test 12
date: 2000-01-12
description: Test post 12 for pagination behavior.
slug: pagination-test-12
tags:
- pagination
- test
categories:
- Testing
draft: false
---
This is pagination test post **12**.
+4
View File
@@ -29,6 +29,10 @@ darkMode = "الوضع الداكن"
warning = "تحذير"
caution = "تنبيه"
[article.codeblock]
copy = "نسخ"
copied = "تم النسخ!"
[notFound]
title = "غير موجود"
subtitle = "تعذر العثور على الصفحة المطلوبة."
+4
View File
@@ -30,6 +30,10 @@ darkMode = "Цёмны рэжым"
warning = "Папярэджанне"
caution = "Асцярога"
[article.codeblock]
copy = "Капіяваць"
copied = "Скапіявана!"
[notFound]
title = "Не знойдзена"
subtitle = "Запытваемая старонка не існуе"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Тъмен Режим"
warning = "Предупреждение"
caution = "Внимание"
[article.codeblock]
copy = "Копирай"
copied = "Копирано!"
[notFound]
title = "Не е намерено"
subtitle = "Страницата която търсите не е открита"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "ডার্ক মোড"
warning = "সতর্কতা"
caution = "সাবধানতা"
[article.codeblock]
copy = "কপি"
copied = "কপি হয়েছে!"
[notFound]
title = "পাওয়া যায়নি"
subtitle = "এই পাতাটি বিদ্যমান নেই"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Mode fosc"
warning = "Avís"
caution = "Precaució"
[article.codeblock]
copy = "Copia"
copied = "Copiat!"
[notFound]
title = "No Trobat"
subtitle = "Aquesta pàgina no existeix"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Tmavý režim"
warning = "Varování"
caution = "Upozornění"
[article.codeblock]
copy = "Kopírovat"
copied = "Zkopírováno!"
[notFound]
title = "Nenalezeno"
subtitle = "Tato stránka neexistuje"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Dunkler Modus"
warning = "Warnung"
caution = "Vorsicht"
[article.codeblock]
copy = "Kopieren"
copied = "Kopiert!"
[notFound]
title = "Seite nicht gefunden"
subtitle = "Diese Seite existiert nicht"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Σκοτεινό θέμα"
warning = "Προειδοποίηση"
caution = "Προσοχή"
[article.codeblock]
copy = "Αντιγραφή"
copied = "Αντιγράφηκε!"
[notFound]
title = "Δε βρέθηκε"
subtitle = "Η σελίδα δε βρέθηκε."
+9
View File
@@ -29,6 +29,10 @@ darkMode = "Dark Mode"
warning = "Warning"
caution = "Caution"
[article.codeblock]
copy = "Copy"
copied = "Copied!"
[notFound]
title = "Not Found"
subtitle = "This page does not exist"
@@ -76,3 +80,8 @@ darkMode = "Dark Mode"
[cookies.functional]
title = "Functional"
text = "These cookies enable features like comments and embedded content."
[pagination]
jumpToPage = "Jump to page"
jump = "Go"
pressEnter = "Press Enter to jump"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Modo oscuro"
warning = "Advertencia"
caution = "Precaución"
[article.codeblock]
copy = "Copiar"
copied = "¡Copiado!"
[notFound]
title = "No Encontrado"
subtitle = "Esta página no existe"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "حالت شب"
warning = "هشدار"
caution = "احتیاط"
[article.codeblock]
copy = "کپی"
copied = "کپی شد!"
[notFound]
title = "یافت نشد"
subtitle = "این صحه وجود ندارد"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Mode sombre"
warning = "Avertissement"
caution = "Attention"
[article.codeblock]
copy = "Copier"
copied = "Copié !"
[notFound]
title = "Page non trouvée"
subtitle = "Cette page n'existe pas."
+4
View File
@@ -29,6 +29,10 @@ darkMode = "डार्क मोड"
warning = "चेतावनी"
caution = "सावधानी"
[article.codeblock]
copy = "कॉपी"
copied = "कॉपी हो गया!"
[notFound]
title = "404 नहीं मिला।"
subtitle = "यह पृष्ठ मौजूद नहीं है।"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Sötét Mód"
warning = "Figyelmeztetés"
caution = "Óvatosság"
[article.codeblock]
copy = "Másolás"
copied = "Másolva!"
[notFound]
title = "Nem található"
subtitle = "Ez az oldal nem létezik"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Mode Gelap"
warning = "Peringatan"
caution = "Perhatian"
[article.codeblock]
copy = "Salin"
copied = "Disalin!"
[notFound]
title = "Tidak ditemukan"
subtitle = "Halaman yang Anda akses tidak ditemukan."
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Modalità scura"
warning = "Avvertenza"
caution = "Attenzione"
[article.codeblock]
copy = "Copia"
copied = "Copiato!"
[notFound]
title = "Non trovato"
subtitle = "Questa pagina non esiste."
+9
View File
@@ -20,6 +20,10 @@ darkMode = "ダークモード"
warning = "警告"
caution = "注意"
[article.codeblock]
copy = "コピー"
copied = "コピーしました!"
[notFound]
title = "404 Not Found"
subtitle = "指定されたページは存在しません。"
@@ -67,3 +71,8 @@ darkMode = "ダークモード"
[cookies.functional]
title = "機能的"
text = "これらのCookieは、コメントや埋め込みコンテンツなどの機能を有効にします."
[pagination]
jumpToPage = "ページにジャンプ"
jump = "ジャンプ"
pressEnter = "Enter キーでジャンプ"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "다크 모드"
warning = "경고"
caution = "주의"
[article.codeblock]
copy = "복사"
copied = "복사됨!"
[notFound]
title = "찾을 수 없음"
subtitle = "페이지를 찾을 수 없습니다."
+4
View File
@@ -24,6 +24,10 @@ darkMode = "Donkere modus"
warning = "Waarschuwing"
caution = "Let op"
[article.codeblock]
copy = "Kopiëren"
copied = "Gekopieerd!"
[notFound]
title = "Niet gevonden"
subtitle = "Deze pagina bestaat niet."
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Mòde fosc"
warning = "Avertiment"
caution = "Atencion"
[article.codeblock]
copy = "Copiar"
copied = "Copiat!"
[notFound]
title = "Non trobat"
subtitle = "Aquesta pagina existís pas"
+4
View File
@@ -35,6 +35,10 @@ darkMode = "Tryb ciemny"
warning = "Ostrzeżenie"
caution = "Uwaga"
[article.codeblock]
copy = "Kopiuj"
copied = "Skopiowano!"
[notFound]
title = "Nie znaleziono"
subtitle = "Ta strona nie istnieje"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Modo Escuro"
warning = "Aviso"
caution = "Cuidado"
[article.codeblock]
copy = "Copiar"
copied = "Copiado!"
[notFound]
title = "Não Encontrado"
subtitle = "Esta página não existe."
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Modo Escuro"
warning = "Aviso"
caution = "Cuidado"
[article.codeblock]
copy = "Copiar"
copied = "Copiado!"
[notFound]
title = "Não Encontrado"
subtitle = "Esta página não existe."
+4
View File
@@ -30,6 +30,10 @@ darkMode = "Тёмный режим"
warning = "Предупреждение"
caution = "Осторожно"
[article.codeblock]
copy = "Копировать"
copied = "Скопировано!"
[notFound]
title = "Не найдено"
subtitle = "Запрашиваемая страница не существует"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Tmavý režim"
warning = "Varovanie"
caution = "Upozornenie"
[article.codeblock]
copy = "Kopírovať"
copied = "Skopírované!"
[notFound]
title = "Nenájdené"
subtitle = "Tato stránka neexistuje"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "ธีมมืด"
warning = "คำเตือน"
caution = "ระวัง"
[article.codeblock]
copy = "คัดลอก"
copied = "คัดลอกแล้ว!"
[notFound]
title = "ไม่พบหัวข้อ"
subtitle = "ไม่พบหน้านี้ในระบบ"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Koyu Mod"
warning = "Uyarı"
caution = "Dikkat"
[article.codeblock]
copy = "Kopyala"
copied = "Kopyalandı!"
[notFound]
title = "Bulunamadı"
subtitle = "Aradığınız sayfa mevcut değil."
+4
View File
@@ -30,6 +30,10 @@ darkMode = "Темна тема"
warning = "Попередження"
caution = "Обережно"
[article.codeblock]
copy = "Копіювати"
copied = "Скопійовано!"
[notFound]
title = "Не знайдено"
subtitle = "Ця сторінка не існує"
+4
View File
@@ -29,6 +29,10 @@ darkMode = "Chế độ nền tối"
warning = "Cảnh báo"
caution = "Thận trọng"
[article.codeblock]
copy = "Sao chép"
copied = "Đã sao chép!"
[notFound]
title = "Không tìm thấy"
subtitle = "Trang này không tồn tại"
+9
View File
@@ -20,6 +20,10 @@ darkMode = "深色模式"
warning = "警告"
caution = "注意"
[article.codeblock]
copy = "複製"
copied = "已複製!"
[notFound]
title = "找不到頁面"
subtitle = "本頁面並不存在"
@@ -67,3 +71,8 @@ darkMode = "深色模式"
[cookies.functional]
title = "功能性"
text = "這些 Cookie 啟用評論和嵌入內容等功能."
[pagination]
jumpToPage = "跳轉到頁碼"
jump = "跳轉"
pressEnter = "按 Enter 鍵跳轉"
+9
View File
@@ -20,6 +20,10 @@ darkMode = "夜晚模式"
warning = "警告"
caution = "注意"
[article.codeblock]
copy = "複製"
copied = "已複製!"
[notFound]
title = "404 錯誤"
subtitle = "頁面不存在"
@@ -67,3 +71,8 @@ darkMode = "夜晚模式"
[cookies.functional]
title = "功能性"
text = "這些 Cookie 啟用評論和嵌入內容等功能."
[pagination]
jumpToPage = "跳轉到頁碼"
jump = "跳轉"
pressEnter = "按 Enter 鍵跳轉"
+9
View File
@@ -20,6 +20,10 @@ darkMode = "暗色模式"
warning = "警告"
caution = "注意"
[article.codeblock]
copy = "复制"
copied = "已复制!"
[notFound]
title = "404 错误"
subtitle = "页面不存在"
@@ -67,3 +71,8 @@ darkMode = "暗色模式"
[cookies.functional]
title = "功能性"
text = "这些 Cookie 启用评论和嵌入内容等功能."
[pagination]
jumpToPage = "跳转到页码"
jump = "跳转"
pressEnter = "按 Enter 键跳转"
+1 -1
View File
@@ -1,4 +1,4 @@
{{- $image := partial "helper/image" (dict "Resources" .Page.Resources "Image" .Destination) -}}
{{- $image := partial "helper/image" (dict "Resources" .Page.Resources "Image" .Destination "Context" .Page) -}}
{{- $alt := .PlainText | safeHTML -}}
{{- $title := .Title | markdownify -}}
+10 -6
View File
@@ -11,19 +11,23 @@
{{- $description = $description | default (T "list.page" (len .Pages)) -}}
{{- end -}}
{{- if or $description (not .Date.IsZero) -}}
<footer class="article-footer">
{{- if 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>
{{- else if $description -}}
<span class="article-description">{{- $description -}}</span>
{{ end }}
{{- end }}
</footer>
{{- end -}}
</div>
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources) -}}
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}}
{{ if $image }}
<div class="article-image">
{{ partial "helper/thumbnail-image" (dict
+1 -1
View File
@@ -1,4 +1,4 @@
{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources) }}
{{ $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>
+5 -5
View File
@@ -1,16 +1,16 @@
{{ $image := partial "helper/image" (dict "Image" .context.Params.image "Resources" .context.Resources) }}
{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }}
<article class="{{ if $image }}has-image{{ end }}">
<a href="{{ .context.RelPermalink }}">
<a href="{{ .RelPermalink }}">
{{ if $image }}
<div class="article-image">
{{ partial "helper/thumbnail-image" (dict
"Resource" $image.Resource
"Width" 250
"Height" 150
"Resize" .context.Site.Params.ImageProcessing.Thumbnail.Enabled
"Resize" .Site.Params.ImageProcessing.Thumbnail.Enabled
"Attributes" (dict
"src" $image.Permalink
"alt" (printf "Featured image of post %s" .context.Title)
"alt" (printf "Featured image of post %s" .Title)
"loading" "lazy"
"width" $image.Width
"height" $image.Height
@@ -30,7 +30,7 @@
{{- end -}}
>
<h2 class="article-title">
{{- .context.Title -}}
{{- .Title -}}
</h2>
</div>
</a>
@@ -30,48 +30,47 @@
{{ $showReadingTime := $Page.Params.readingTime | default ($Page.Site.Params.article.readingTime) }}
{{ $showDate := not $Page.Date.IsZero }}
{{ $showFooter := or $showDate $showReadingTime }}
{{ if $showFooter }}
<footer class="article-footer">
{{ if $showDate }}
<div>
{{ 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>
{{ $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 $showReadingTime }}
<div>
{{ partial "helper/icon" "clock" }}
<time class="article-time--reading">
{{ T "article.readingTime" $Page.ReadingTime }}
</time>
</div>
{{ end }}
</footer>
{{ end }}
{{ if $Page.IsTranslated }}
<footer class="article-translations">
{{ partial "helper/icon" "language" }}
<div>
{{ if $showTranslations }}
<div class="inline-meta">
{{ partial "helper/icon" "language" }}
{{ range $Page.Translations }}
<a href="{{ .RelPermalink }}" class="link">{{ .Language.LanguageName }}</a>
{{ end }}
</div>
</footer>
{{ end }}
{{ end }}
{{ if and $Page.Site.Params.Article.List.ShowTags ($Page.GetTerms "tags") $IsList }}
<footer class="article-header-tags">
{{ partial "helper/icon" "tag" }}
<div>
{{ 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>
</footer>
{{ end }}
</footer>
{{ end }}
</div>
@@ -1,15 +1,15 @@
<footer class="article-footer">
<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">
<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">
<section class="article-lastmod inline-meta">
{{ partial "helper/icon" "clock" }}
<span>
{{ T "article.lastUpdatedOn" }} {{ .Lastmod | time.Format .Site.Params.dateFormat.lastUpdated }}
@@ -1,7 +1,7 @@
{{- $IsList := .IsList -}}
{{- $Page := .Page -}}
<header class="article-header">
{{- $image := partial "helper/image" (dict "Image" $Page.Params.image "Resources" $Page.Resources) -}}
{{- $image := partial "helper/image" (dict "Image" $Page.Params.image "Resources" $Page.Resources "Context" $Page) -}}
{{ if $image }}
<div class="article-image">
<a href="{{ $Page.RelPermalink }}">
@@ -6,7 +6,7 @@
<h2 class="article-title">
{{- $link.title -}}
</h2>
<footer class="article-footer">
<footer class="article-meta">
{{ with $link.description }}
{{ . }}
{{ else }}
@@ -15,7 +15,7 @@
</footer>
</div>
{{- $image := partial "helper/image" (dict "Image" $link.image "Resources" $.Resources) -}}
{{- $image := partial "helper/image" (dict "Image" $link.image "Resources" $.Resources "Context" .) -}}
{{- with $image -}}
<div class="article-image">
{{ partial "helper/thumbnail-image" (dict
@@ -1,9 +1,9 @@
{{- $opts := dict "minify" hugo.IsProduction "format" "esm" -}}
{{- $galleryScript := resources.Get "ts/gallery.ts" | js.Build $opts -}}
{{ $style := .Site.Data.external.PhotoSwipe.Style }}
{{ $core := .Site.Data.external.PhotoSwipe.Core }}
{{ $lightbox := .Site.Data.external.PhotoSwipe.Lightbox }}
{{ $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 }}';
@@ -29,4 +29,4 @@
});
lightbox.init();
}
</script>
</script>
@@ -5,7 +5,7 @@
<div class="subsection-list">
<div class="article-list--tile">
{{ range . }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "articleList") }}
{{ partial "article-list/tile" . }}
{{ end }}
</div>
</div>
+13 -32
View File
@@ -1,3 +1,8 @@
{{/*
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 -}}
@@ -8,40 +13,16 @@
{{ T "cookies.managePreferences" }}
</button>
</div>
<div id="comments-container" style="display: none;">
<div id="comments-container" style="display: none;"></div>
<template id="comments-template">
{{ partial (printf "comments/provider/%s" .Site.Params.comments.provider) . }}
</div>
<script>
(function() {
var placeholder = document.getElementById('comments-consent-placeholder');
var container = document.getElementById('comments-container');
function showComments() {
if (placeholder) placeholder.style.display = 'none';
if (container) container.style.display = 'block';
}
function hideComments() {
if (placeholder) placeholder.style.display = 'block';
if (container) container.style.display = 'none';
}
window.addEventListener('onCookieConsentChange', function(e) {
if (e.detail && e.detail.functional) {
showComments();
} else {
hideComments();
}
});
// Check if already consented
if (window.cookieConsent && window.cookieConsent.hasConsent('functional')) {
showComments();
}
})();
</script>
</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 }}
{{ 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 -}}
@@ -1,4 +1,12 @@
{{- $opts := dict "minify" hugo.IsProduction -}}
{{- $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>
@@ -7,4 +15,4 @@
{{/* 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 -}}
{{- end -}}
+1 -1
View File
@@ -1,4 +1,4 @@
{{- $ThemeVersion := "4.0.0" -}}
{{- $ThemeVersion := "4.0.1" -}}
<footer class="site-footer">
<section class="copyright">
&copy;
+1 -1
View File
@@ -23,7 +23,7 @@
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
{{- end -}}
{{- $favicon := partial "helper/image" (dict "Image" .Site.Params.favicon "Resources" resources) -}}
{{- $favicon := partial "helper/image" (dict "Image" .Site.Params.favicon "Resources" resources "Context" .) -}}
{{ with $favicon }}
<link rel="shortcut icon" href="{{ .Permalink }}" />
{{ end }}
@@ -37,7 +37,7 @@
{{- end -}}
{{- end -}}
{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources) }}
{{ $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) }}
{{- if $image -}}
<meta property='og:image' content='{{ absURL $image.permalink }}' />
<meta property='og:image' content='{{ absURL $image.Permalink }}' />
{{- end -}}
@@ -9,8 +9,8 @@
<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) -}}
{{- $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 }}' />
<meta name="twitter:image" content='{{ absURL $image.Permalink }}' />
{{- end -}}
+2 -2
View File
@@ -1,4 +1,4 @@
{{- $List := index .Context.Site.Data.external .Namespace -}}
{{- $List := index hugo.Data.external .Namespace -}}
{{- with $List -}}
{{- range . -}}
{{- if eq .type "script" -}}
@@ -26,4 +26,4 @@
{{- end -}}
{{- else -}}
{{- errorf "Error: external resource '%s' is not found" .Namespace -}}
{{- end -}}
{{- end -}}
+26 -8
View File
@@ -1,6 +1,7 @@
/// Params:
/// Resources: Where to search for images
/// Image: Image URL
/// Context: page context (used to access the configuration)
/// Returns:
/// Resource: Hugo image resource object
@@ -8,32 +9,49 @@
/// Height: Image height
/// Width: Image width
{{ $result := "" }}
{{ $result := dict }}
{{ if .Image }}
{{ $url := urls.Parse .Image }}
{{ $permalink := .Image }}
{{ $resource := "" }}
{{ $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)) }}
{{ $permalink = $resource.RelPermalink }}
{{ else }}
{{/* Remote image */}}
{{ with try (resources.GetRemote $url) }}
{{ with try (resources.GetRemote $url (dict "timeout" .Context.Site.Params.imageProcessing.external.timeout)) }}
{{ with .Err }}
{{ errorf "%s" . }}
{{ warnf "Failed to fetch remote resource %q: %s" $url . }}
{{ else with .Value }}
{{ $resource = . }}
{{ $local = false }}
{{ else }}
{{ errorf "Unable to get remote resource %q" $url }}
{{ warnf "Unable to get remote resource %q" $url }}
{{ end }}
{{ end }}
{{ end }}
{{ with $resource }}
{{ if reflect.IsImageResourceWithMeta . }}
{{ 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
+133 -61
View File
@@ -1,70 +1,142 @@
{{- $pag := $.Paginator -}}
{{- if gt $pag.TotalPages 1 -}}
<nav class="pagination" role="navigation" aria-label="pagination">
{{- /* Previous page button */ -}}
<a class="page-link {{ if not $pag.Prev }}disabled{{ end }}"
{{- if $pag.Prev -}}href="{{ $pag.Prev.URL }}" {{- end -}}
aria-label="Previous page"
{{- if not $pag.Prev -}}aria-disabled="true"{{- end -}}>
{{ partial "helper/icon" "chevron-left" }}
</a>
{{- /* Page numbers logic */ -}}
{{- $window := 4 -}}
{{- $showFirst := true -}}
{{- $showLast := true -}}
{{- /* First page */ -}}
<a class="page-link {{ if eq $pag.PageNumber 1 }}current{{ end }}" aria-label="Page 1"
{{- if eq $pag.PageNumber 1 -}}aria-current="page" {{- end -}}
{{- if ne $pag.PageNumber 1 -}}href="{{ $pag.First.URL }}"{{- end -}}>
1
</a>
{{- /* Left ellipsis - show if current page is > 3 */ -}}
{{- if gt $pag.PageNumber 3 -}}
<span class="page-link pagination-link">
{{ partial "helper/icon" "dots" }}
</span>
{{- end -}}
{{- /* Middle pages - show current and neighbors */ -}}
{{- /* Find page 2's URL to extract the pagination base path for JavaScript */ -}}
{{- $page2Url := "" -}}
{{- range $pag.Pagers -}}
{{- if and (gt .PageNumber 1) (lt .PageNumber $pag.TotalPages) -}}
{{- if and (ge .PageNumber (sub $pag.PageNumber $window)) (le .PageNumber (add $pag.PageNumber $window)) -}}
<a class="page-link {{ if eq .PageNumber $pag.PageNumber }}current{{ end }}"
{{- if eq .PageNumber $pag.PageNumber -}}aria-current="page" {{- end -}} href="{{ .URL }}"
aria-label="Page {{ .PageNumber }}">
{{ .PageNumber }}
</a>
{{- end -}}
{{- if eq .PageNumber 2 -}}
{{- $page2Url = .URL -}}
{{- end -}}
{{- end -}}
{{- /* Right ellipsis - show if current page is < last-2 */ -}}
{{- if lt $pag.PageNumber (sub $pag.TotalPages 2) -}}
<span class="page-link pagination-link">
{{ partial "helper/icon" "dots" }}
</span>
{{- 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>
{{- /* Last page (if more than 1 page total) */ -}}
{{- /* 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 $pag.TotalPages 1 -}}
<a class="page-link {{ if eq $pag.PageNumber $pag.TotalPages }}current{{ end }}"
{{- if ne $pag.PageNumber $pag.TotalPages -}}href="{{ $pag.Last.URL }}" {{- end -}}
aria-label="Page {{ $pag.TotalPages }}"
{{- if eq $pag.PageNumber $pag.TotalPages -}}aria-current="page"{{- end -}}>
{{ $pag.TotalPages }}
</a>
{{- end -}}
{{- 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 -}}
{{- /* Next page button */ -}}
<a class="page-link {{ if not $pag.Next }}disabled{{ end }}"
{{- if $pag.Next -}}href="{{ $pag.Next.URL }}" {{- end -}}
aria-label="Next page"
{{- if not $pag.Next -}}aria-disabled="true"{{- end -}}>
{{ partial "helper/icon" "chevron-right" }}
</a>
</nav>
{{- 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 -}}
+1 -1
View File
@@ -7,7 +7,7 @@
<header>
{{ with .Site.Params.sidebar.avatar }}
{{- $avatar := partial "helper/image" (dict "Image" . "Resources" resources) -}}
{{- $avatar := partial "helper/image" (dict "Image" . "Resources" resources "Context" $) -}}
<figure class="site-avatar">
<a href="{{ $.Site.Home.RelPermalink }}">
<img src="{{ $avatar.Permalink }}"
+1 -1
View File
@@ -20,7 +20,7 @@
{{ partial "helper/icon" $icon }}
</div>
<h2 class="widget-title section-title">
{{ if $showLink }}
{{ if and $showLink $taxonomyPage }}
<a href="{{ $taxonomyPage.RelPermalink }}">
{{ $widgetTitle }}
</a>
+1 -1
View File
@@ -8,7 +8,7 @@
<div class="subsection-list">
<div class="article-list--tile">
{{ range $terms }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "taxonomy") }}
{{ partial "article-list/tile" . }}
{{ end }}
</div>
</div>
+2 -2
View File
@@ -17,7 +17,7 @@
{{ end }}
</div>
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources) -}}
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}}
{{ if $image }}
<div class="section-image">
{{ partial "helper/thumbnail-image" (dict
@@ -51,7 +51,7 @@
<div class="subsection-list">
<div class="article-list--tile">
{{ range . }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "section") }}
{{ partial "article-list/tile" . }}
{{ end }}
</div>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@
{{- range $pages -}}
{{- $data := dict "title" .Title "date" .Date "permalink" .RelPermalink "content" (.Plain) -}}
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources) -}}
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}}
{{- if $image -}}
{{- $data = merge $data (dict "image" $image.Permalink) -}}
{{- end -}}
+2 -2
View File
@@ -50,9 +50,9 @@
{{- if .Site.Params.RSSFullContent -}}
{{- $content = .Content -}}
{{- end -}}
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources) -}}
{{- $image := partial "helper/image" (dict "Image" .Params.image "Resources" .Resources "Context" .) -}}
{{- if $image -}}
{{- $imgTag := printf "<img src=\"%s\" alt=\"Featured image of post %s\" />" ($image.permalink | absURL) .Title -}}
{{- $imgTag := printf "<img src=\"%s\" alt=\"Featured image of post %s\" />" ($image.Permalink | absURL) .Title -}}
{{- $content = printf "%s%s" $imgTag $content -}}
{{- end -}}
{{- $content | transform.XMLEscape | safeHTML -}}