Hugo-PaperMod主题自定义

Hugo PaperMod 主题轻量且美观,参考网上的教程,并借助 AI 和自己的理解加以修改,记录下来供自己和他人参阅。

Note

更换了主题, PaperMod 站点访问链接:PaperMod Blog

准备

PaperMod 提供了丰富的自定义入口,提供了 extended_head.htmlextended_footer.html 来修改,并且 assets/css/extended/ 下可以添加任意名称 CSS 文件,主题都会引入。

悬浮动画

CSS 的修改直接在 assets/css/extended/ 文件夹下新建文件写入,分开方便调试,后面都一样不再赘述。

 1/* 悬浮动画 */
 2/* 左上角logo悬浮动画 */
 3.logo a:hover {
 4    transition: 0.15s;
 5    color: grey;
 6  }
 7
 8/* 首页icon悬浮动画 */
 9svg:hover {
10    transition: 0.15s;
11    transform: scaleX(1.1) scaleY(1.1);
12}
13
14.social-icons a svg:hover{
15    color: #ffbb3d !important;
16
17}
18/* 模式切换按钮悬浮动画 */
19#moon:hover {
20    transition: 0.15s;
21    color: deepskyblue;
22}
23
24#sun:hover {
25    transition: 0.15s;
26    color: gold;
27}
28/* 菜单栏文字悬浮动画 */
29#menu a:hover {
30    transition: 0.15s;
31    color: grey;
32}

首页信息居中

 1/* 首页信息居中 */
 2.first-entry .entry-header {
 3    align-self: center;
 4}
 5.home-info .entry-content {
 6    display: flex;
 7    flex-direction: column;
 8    justify-content: center;
 9    align-items: center;
10}
11.first-entry .entry-footer {
12    display: flex;
13    justify-content: center;
14    align-items: center;
15}

字体修改

字体使用了两种,一个纯英文字体 Nunito,一个鸿蒙字体,字体设置中先使用英文字体,再使用中文,这样可以中英文字体分开,代码块字体也可以设置不一样的。

~/layouts/partials/extended_head.html 中引入字体,并且在 CSS 中添加样式:

1{{/*  字体引入  */}}
2<link rel="stylesheet" href="https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css" />
3<link rel="preconnect" href="https://fonts.googleapis.com">
4<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
5<link href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" rel="stylesheet">
1body {
2    font-family: Nunito, HarmonyOS_Regular, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
3}
4
5.post-content pre, code {
6    font-family: consolas, sans-serif;
7}

首页文章封面图侧边显示

首页文章列表的封面图很高,导致一页放不下几篇文章,希望可以把封面图放到文章信息侧边,有两种方法可以实现。

一种只需要添加自定义 CSS 文件到目录下就可以实现,简单方便,只是文章没有封面图时,文章的标题和描述会两列显示,即标题占用到了封面的位置。

另一种方法是通过修改模板文件实现,可以通过站点设置控制显示在左侧、右侧或默认的顶部。参考了这篇文章:Hugo博客文章封面图片缩小并移到侧边 | PaperMod主题

方法一

可以直接到 GitHub 下载,这个项目还包含了其他的功能,具体访问仓库地址,或者直接在 assets/css/extended/ 目录下新建文件粘贴下面的内容:

 1/* Allocate a single column when the width of the page is small. */
 2.post-entry {
 3    display: grid;
 4    grid-template-columns: 1fr;
 5    grid-gap: 5px 0px;
 6}
 7
 8/* Allocate two columns when there is enough width. *
 9 * The thumbnail is placed in the first column, while the rest of
10 * the children are placed in the second column. */
11@media (min-width: 700px) {
12    .post-entry {
13        grid-template-columns: 2fr 3fr;
14        grid-gap: 0px 10px;
15    }
16}
17
18.post-entry .entry-cover {
19    max-width: fit-content;
20    margin: auto;
21    grid-row: span 3;
22}
23
24.post-entry .entry-header {
25    align-self: center;
26}
27
28.post-entry .entry-content {
29    align-self: center;
30}
31
32.post-entry .entry-footer {
33    align-self: end;
34}

上述代码封面图显示在左侧,如果想要显示在右侧,替换成下面的:

 1/* Allocate a single column when the width of the page is small. */
 2.post-entry {
 3    display: grid;
 4    grid-template-columns: 1fr;
 5    grid-gap: 5px 0px;
 6}
 7
 8/* Allocate two columns when there is enough width. */
 9/* The thumbnail is placed in the second column, while the rest of */
10/* the children are placed in the first column. */
11@media (min-width: 700px) {
12    .post-entry {
13        grid-template-columns: 3fr 2fr;
14        grid-gap: 0px 10px;
15    }
16    .post-entry .entry-cover {
17        grid-column: 2;
18        grid-row: 1 / span 3;
19    }
20}
21
22.post-entry .entry-cover {
23    max-width: fit-content;
24    margin: auto;
25}
26
27.post-entry .entry-header {
28    align-self: center;
29}
30
31.post-entry .entry-content {
32    align-self: center;
33}
34
35.post-entry .entry-footer {
36    align-self: end;
37}

方法二

这种方法较为复杂,好处是可以通过参数控制直接控制显示左侧或者右侧。

首先复制主题 layouts\_default\list.html 文件到根目录下,在其中修改,找到大概 66<article></article> 包裹的元素,将代码换成这部分:

 1<article class="{{ $class }}{{ if and .Site.Params.homeCoverPosition .Params.cover.image }} cover-{{ .Site.Params.homeCoverPosition }}{{ end }}">
 2  {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
 3  {{- if and (not $isHidden) .Params.cover.image }}
 4  <div class="post-content-wrapper">
 5    <div class="post-cover">
 6      {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" $isHidden) }}
 7    </div>
 8    <div class="post-info">
 9  {{- else }}
10  <div class="post-content-wrapper">
11    <div class="post-info">
12  {{- end }}
13      <header class="entry-header">
14        <h2 class="entry-hint-parent">
15          {{- .Title }}
16          {{- if .Draft }}
17          <span class="entry-hint" title="Draft">
18            <svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" fill="currentColor">
19              <path d="M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z" />
20            </svg>
21          </span>
22          {{- end }}
23        </h2>
24      </header>
25      {{- if (ne (.Param "hideSummary") true) }}
26      <div class="entry-content">
27        <p>{{ .Summary | plainify | htmlUnescape }}{{ if .Truncated }}...{{ end }}</p>
28      </div>
29      {{- end }}
30      {{- if not (.Param "hideMeta") }}
31      <footer class="entry-footer">
32        {{- partial "post_meta.html" . -}}
33      </footer>
34      {{- end }}
35    </div>
36  </div>
37  <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
38</article>

新建一个文件放置 CSS 代码:

 1.post-entry {
 2  overflow: hidden;
 3}
 4
 5.post-content-wrapper {
 6  display: flex;
 7  flex-direction: column;
 8  height: 100%;
 9}
10
11.cover-right .post-content-wrapper .post-cover .entry-cover,
12.cover-left .post-content-wrapper .post-cover .entry-cover 
13{
14  margin-bottom: unset;
15  margin-top: unset;
16}
17
18.entry-cover {
19  overflow: hidden;
20  display: flex;
21  justify-content: center;
22  align-items: center;
23  margin-top: var(--gap);
24}
25
26.cover-left .post-content-wrapper,
27.cover-right .post-content-wrapper {
28  flex-direction: row;
29  align-items: center;
30}
31
32.cover-left .post-cover,
33.cover-right .post-cover {
34  width: 40%;
35  margin-bottom: 0;
36  margin-right: 20px;
37}
38
39.cover-right .post-content-wrapper {
40  flex-direction: row-reverse;
41}
42
43.cover-right .post-cover {
44  margin-right: 0;
45  margin-left: 20px;
46}
47
48.cover-left .post-info,
49.cover-right .post-info {
50  width: 60%;
51}
52
53/* 修复文章详情页图片描述位置 */
54.post-single .entry-cover {
55  flex-direction: column;
56  margin-bottom: 10px;
57}
58
59
60/* 移动设备默认上方 */
61@media (max-width: 768px) {
62  .cover-left .post-content-wrapper,
63  .cover-right .post-content-wrapper {
64    flex-direction: column;
65  }
66
67  .cover-left .post-cover,
68  .cover-right .post-cover,
69  .cover-left .post-info,
70  .cover-right .post-info {
71    width: 100%;
72    margin-right: 0;
73    margin-left: 0;
74  }
75  .entry-cover {
76    margin-bottom: var(--gap) !important;
77  }
78}

最后在站点配置中添加配置来控制封面图位置:

1params:
2  homeCoverPosition: right # left/right/top

代码块功能

优化了代码块的显示,添加了下面的功能:

  • 语言显示
  • Mac 风格外观
  • 代码块折叠

复制主题的 layouts\partials\footer.html 到根目录下,找到其中代码块复制功能部分,大概在 95 行左右,替换代码:

  1{{- if (and (eq .Kind "page") (ne .Layout "archives") (ne .Layout "search") (or (.Param "ShowCodeCopyButtons") (.Param "ShowMacDots") (.Param "ShowCodeLang") (.Param "ShowExpandButton"))) }}
  2<style>
  3    .code-toolbar {
  4        display: flex;
  5        justify-content: space-between;
  6        align-items: center;
  7        padding: 5px 10px;
  8        {{/*  background: var(--code-block-bg);  */}}
  9        background: #232323;
 10        border-top-left-radius: 5px;
 11        border-top-right-radius: 5px;
 12        font-size: 0.8em;
 13        position: relative;
 14    }
 15    .mac-dots {
 16        width: 12px;
 17        height: 12px;
 18        border-radius: 50%;
 19        background-color: #ff5f56;
 20        box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f;
 21        margin-right: 5px;
 22    }
 23    .lang-label {
 24        flex-grow: 1;
 25        text-align: center;
 26        margin: 0 5px;
 27        color: rgba(255,255,255,.8);
 28    }
 29    .toolbar-group {
 30        display: flex;
 31        align-items: center;
 32    }
 33    .expand-button, .copy-code {
 34        background: none;
 35        border: none;
 36        cursor: pointer;
 37        padding: 0 5px;
 38    }
 39    .highlight {
 40        position: relative;
 41    }
 42    .highlight.collapsible {
 43        max-height: {{ .Site.Params.codeMaxHeight | default "300px" }};
 44        overflow: hidden;
 45    }
 46    .highlight.expanded {
 47        max-height: none;
 48    }
 49    .highlight pre {
 50        margin-bottom: 0;
 51    }
 52    .expand-button {
 53        position: absolute;
 54        bottom: 0;
 55        left: 50%;
 56        transform: translateX(-50%);
 57        background-color: transparent;
 58        padding: 5px 10px;
 59        border-radius: 5px 5px 0 0;
 60        display: none;
 61        height: 30px;
 62        &:hover {
 63            background: rgba(255,255,255,.1);
 64            color: #fff;
 65        }
 66    }
 67    .highlight.collapsible .expand-button {
 68        display: block;
 69    }
 70    .highlight table {
 71        margin-bottom: 0;
 72    }
 73    .post-content pre code {
 74        overflow-x: auto;
 75        overflow-y: hidden;
 76    }
 77</style>
 78
 79<script>
 80    document.addEventListener('DOMContentLoaded', () => {
 81        const codeBlocks = document.querySelectorAll('.highlight');
 82        const maxHeight = parseInt('{{ .Site.Params.codeMaxHeight | default "300" }}');
 83        
 84        codeBlocks.forEach((block) => {
 85            const pre = block.querySelector('pre');
 86            const code = pre.querySelector('code');
 87
 88            // Determine if a toolbar is needed
 89            let toolbarNeeded = false;
 90            if ({{ .Param "ShowMacDots" }} || {{ .Param "ShowCodeLang" }}) {
 91                toolbarNeeded = true;
 92            }
 93
 94            if (toolbarNeeded) {
 95                const toolbar = document.createElement('div');
 96                toolbar.classList.add('code-toolbar');
 97                block.insertBefore(toolbar, block.firstChild);
 98
 99                const leftGroup = document.createElement('div');
100                leftGroup.classList.add('toolbar-group');
101                toolbar.appendChild(leftGroup);
102
103                const rightGroup = document.createElement('div');
104                rightGroup.classList.add('toolbar-group');
105                toolbar.appendChild(rightGroup);
106
107                if ({{ .Param "ShowMacDots" }}) {
108                    const macDots = document.createElement('div');
109                    macDots.classList.add('mac-dots');
110                    leftGroup.appendChild(macDots);
111                }
112
113                if ({{ .Param "ShowCodeLang" }}) {
114                    let language = '';
115                    const possibleElements = [
116                        block,
117                        block.querySelector('code'),
118                        block.querySelector('pre > code'),
119                        block.querySelector('pre'),
120                        block.querySelector('td:nth-child(2) code')
121                    ];
122
123                    for (const element of possibleElements) {
124                        if (element && element.className) {
125                            const elementLanguageClass = element.className.split(' ').find(cls => cls.startsWith('language-'));
126                            if (elementLanguageClass) {
127                                language = elementLanguageClass.replace('language-', '');
128                                break;
129                            }
130                        }
131                    }
132
133                    if (language) {
134                        const langLabel = document.createElement('div');
135                        langLabel.classList.add('lang-label');
136                        langLabel.textContent = language;
137                        toolbar.insertBefore(langLabel, rightGroup);
138                    }
139                }
140
141                if ({{ .Param "ShowCodeCopyButtons" }}) {
142                    const copyButton = document.createElement('button');
143                    copyButton.classList.add('copy-code');
144                    copyButton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
145                    rightGroup.appendChild(copyButton);
146
147                    copyButton.addEventListener('click', () => {
148                        let textToCopy = code.textContent;
149                        if (code.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
150                            textToCopy = Array.from(code.parentNode.parentNode.parentNode.querySelectorAll('td:nth-child(2)'))
151                                .map(td => td.textContent)
152                                .join('\n');
153                        }
154                        
155                        if ('clipboard' in navigator) {
156                            navigator.clipboard.writeText(textToCopy);
157                            copyingDone();
158                            return;
159                        }
160
161                        const textArea = document.createElement('textarea');
162                        textArea.value = textToCopy;
163                        document.body.appendChild(textArea);
164                        textArea.select();
165                        try {
166                            document.execCommand('copy');
167                            copyingDone();
168                        } catch (e) { };
169                        document.body.removeChild(textArea);
170                    });
171
172                    function copyingDone() {
173                        copyButton.innerHTML = '{{- i18n "code_copied" | default "copied!" }}';
174                        setTimeout(() => {
175                            copyButton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
176                        }, 2000);
177                    }
178                }
179            } else if ({{ .Param "ShowCodeCopyButtons" }}) {
180                const copyButton = document.createElement('button');
181                copyButton.classList.add('copy-code');
182                copyButton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
183                if (block.classList.contains("highlight")) {
184                    block.appendChild(copyButton);
185                } else if (block.parentNode.firstChild == block) {
186                    // td containing LineNos
187                } else if (code.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
188                    // table containing LineNos and code
189                    code.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copyButton);
190                } else {
191                    // code blocks not having highlight as parent class
192                    code.parentNode.appendChild(copyButton);
193                }
194
195                copyButton.addEventListener('click', () => {
196                    let textToCopy = code.textContent;
197                    if (code.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
198                        textToCopy = Array.from(code.parentNode.parentNode.parentNode.querySelectorAll('td:nth-child(2)'))
199                            .map(td => td.textContent)
200                            .join('\n');
201                    }
202                    
203                    if ('clipboard' in navigator) {
204                        navigator.clipboard.writeText(textToCopy);
205                        copyingDone();
206                        return;
207                    }
208
209                    const textArea = document.createElement('textarea');
210                    textArea.value = textToCopy;
211                    document.body.appendChild(textArea);
212                    textArea.select();
213                    try {
214                        document.execCommand('copy');
215                        copyingDone();
216                    } catch (e) { };
217                    document.body.removeChild(textArea);
218                });
219
220                function copyingDone() {
221                    copyButton.innerHTML = '{{- i18n "code_copied" | default "copied!" }}';
222                    setTimeout(() => {
223                        copyButton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
224                    }, 2000);
225                }
226            }
227
228            if ({{ .Param "ShowExpandButton" }}) {
229                const expandButton = document.createElement('button');
230                expandButton.classList.add('expand-button');
231                expandButton.innerHTML = '&#9660;'; // Down arrow
232                block.appendChild(expandButton);
233
234                if (pre.offsetHeight > maxHeight) {
235                    block.classList.add('collapsible');
236                    expandButton.style.display = 'block';
237
238                    expandButton.addEventListener('click', () => {
239                        block.classList.toggle('expanded');
240                        expandButton.innerHTML = block.classList.contains('expanded') ? '&#9650;' : '&#9660;';
241                    });
242                }
243            }
244        });
245    });
246</script>
247{{- end }}
Tip

这里直接将样式写入了模板中,工具栏的背景可以自己更换,可以设置成代码块背景色成为一个整体,也可以自己更改。

然后同样在站点配置文件中添加参数控制开关:

1params:
2# 代码块功能
3  ShowMacDots: true # Mac色块
4  ShowCodeLang: true # 语言显示
5  ShowExpandButton: true # 代码块折叠
6  ShowCodeCopyButtons: true # 代码块复制按钮
7  codeMaxHeight: "300px" # 代码块最大折叠高度

添加 Waline 评论

将主题目录下的 layouts\partials\comments.html 文件复制到站点根目录,写入代码:

 1{{ if .Site.Params.walineServer }}
 2<div id="waline"></div>
 3<script>
 4    Waline.init({
 5        el: '#waline',
 6        //path: location.pathname,
 7        dark: "body.dark",
 8        serverURL: "{{.Site.Params.walineServer}}",
 9    });
10    
11    </script>
12{{ end }}

然后在 extended_head.html 文件中引入 jscss

1{{/*  Waline评论引入  */}}
2{{ if and (.Site.Params.walineServer) (.IsPage) }}
3<script src="https://unpkg.com/@waline/client@v2/dist/waline.js"></script>
4  <link
5    rel="stylesheet"
6    href="https://unpkg.com/@waline/client@v2/dist/waline.css"
7  />
8{{ end }}

最后在站点配置中配置自己的地址:

1params:
2  comments: true
3  walineServer: https://waline.vercel.app

目录侧边显示

有三种方法更改目录到侧边,两种是直接添加自定义 CSS,一种需要修改模板文件。

方法一

直接添加自定义 CSS 样式,方法来自:Commit Make ToC float

方法比较简单,缺点是目录没有激活项高亮,不会随着页面滚动而滚动。

 1.toc {
 2    padding: 14px;
 3    border: solid 1px lightgray;
 4    font-size: 12px;
 5}
 6
 7
 8@media (min-width: 1280px) {
 9    .toc {
10        position: sticky;
11        float: left;
12        --toc-left: calc(100vw / 50);
13        left: var(--toc-left); /* _minimum_ distance from left screen border */
14        top: 100px;
15        margin-left: -1000px; /* overruled by left */
16
17        width: calc((100vw - var(--main-width) - 2 * var(--gap)) / 2 - 2 * var(--toc-left));
18        padding: 14px;
19        border: solid 1px lightgray;
20        font-size: 12px;
21    }
22
23    .toc .inner {
24        padding: 0;
25    }
26
27    .toc details summary {
28        margin-inline-start: 0;
29        margin-bottom: 10px;
30    }
31
32}
33
34
35
36summary {
37    cursor: pointer !important;
38}

方法二

同样使用 CSS 实现,访问项目地址

下载下来添加到自己的目录即可,还包含了文章缩略图。

Important

测试后发现,如果只添加目录样式,会有错位,需要添加他的自定义设置文件,但是文章总体布局会变宽,具体自行测试。

方法三

更改模板文件,使用 toc-containerwideCSS 类,在 JavaScript 中动态添加或移除这些类,以响应屏幕宽度的变化。动态样式控制通过 checkTocPosition () 函数来实现,确保目录在不同屏幕大小下的合适显示,最初来自一个外国博主的 PR,访问详细信息:Toc on the side #675 ,可以参考博客:Sulv’s Blog | Hugo 博客目录放在侧边 | PaperMod 主题

复制模板文件 ~/themes\PaperMod\layouts\partials\toc.html 到站点根目录下,替换内容并添加样式:

  1{{- $headers := findRE "<h[1-6].*?>(.|\n])+?</h[1-6]>" .Content -}}
  2{{- $has_headers := ge (len $headers) 1 -}}
  3{{- if $has_headers -}}
  4<aside id="toc-container" class="toc-container wide">
  5    <div class="toc">
  6        <details {{if (.Param "TocOpen") }} open{{ end }}>
  7            <summary accesskey="c" title="(Alt + C)">
  8                <span class="details">{{- i18n "toc" | default "Table of Contents" }}</span>
  9            </summary>
 10
 11            <div class="inner">
 12                {{- $largest := 6 -}}
 13                {{- range $headers -}}
 14                {{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
 15                {{- $headerLevel := len (seq $headerLevel) -}}
 16                {{- if lt $headerLevel $largest -}}
 17                {{- $largest = $headerLevel -}}
 18                {{- end -}}
 19                {{- end -}}
 20
 21                {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}}
 22
 23                {{- $.Scratch.Set "bareul" slice -}}
 24                <ul>
 25                    {{- range seq (sub $firstHeaderLevel $largest) -}}
 26                    <ul>
 27                        {{- $.Scratch.Add "bareul" (sub (add $largest .) 1) -}}
 28                        {{- end -}}
 29                        {{- range $i, $header := $headers -}}
 30                        {{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
 31                        {{- $headerLevel := len (seq $headerLevel) -}}
 32
 33                        {{/* get id="xyz" */}}
 34                        {{- $id := index (findRE "(id=\"(.*?)\")" $header 9) 0 }}
 35
 36                        {{- /* strip id="" to leave xyz, no way to get regex capturing groups in hugo */ -}}
 37                        {{- $cleanedID := replace (replace $id "id=\"" "") "\"" "" }}
 38                        {{- $header := replaceRE "<h[1-6].*?>((.|\n])+?)</h[1-6]>" "$1" $header -}}
 39
 40                        {{- if ne $i 0 -}}
 41                        {{- $prevHeaderLevel := index (findRE "[1-6]" (index $headers (sub $i 1)) 1) 0 -}}
 42                        {{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
 43                        {{- if gt $headerLevel $prevHeaderLevel -}}
 44                        {{- range seq $prevHeaderLevel (sub $headerLevel 1) -}}
 45                        <ul>
 46                            {{/* the first should not be recorded */}}
 47                            {{- if ne $prevHeaderLevel . -}}
 48                            {{- $.Scratch.Add "bareul" . -}}
 49                            {{- end -}}
 50                            {{- end -}}
 51                            {{- else -}}
 52                            </li>
 53                            {{- if lt $headerLevel $prevHeaderLevel -}}
 54                            {{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}}
 55                            {{- if in ($.Scratch.Get "bareul") . -}}
 56                        </ul>
 57                        {{/* manually do pop item */}}
 58                        {{- $tmp := $.Scratch.Get "bareul" -}}
 59                        {{- $.Scratch.Delete "bareul" -}}
 60                        {{- $.Scratch.Set "bareul" slice}}
 61                        {{- range seq (sub (len $tmp) 1) -}}
 62                        {{- $.Scratch.Add "bareul" (index $tmp (sub . 1)) -}}
 63                        {{- end -}}
 64                        {{- else -}}
 65                    </ul>
 66                    </li>
 67                    {{- end -}}
 68                    {{- end -}}
 69                    {{- end -}}
 70                    {{- end }}
 71                    <li>
 72                        <a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
 73                        {{- else }}
 74                    <li>
 75                        <a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
 76                        {{- end -}}
 77                        {{- end -}}
 78                        <!-- {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}} -->
 79                        {{- $firstHeaderLevel := $largest }}
 80                        {{- $lastHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers (sub (len $headers) 1)) 1) 0)) }}
 81                    </li>
 82                    {{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
 83                    {{- if in ($.Scratch.Get "bareul") (add . $firstHeaderLevel) }}
 84                </ul>
 85                {{- else }}
 86                </ul>
 87                </li>
 88                {{- end -}}
 89                {{- end }}
 90                </ul>
 91            </div>
 92        </details>
 93    </div>
 94</aside>
 95<script>
 96    let activeElement;
 97    let elements;
 98    window.addEventListener('DOMContentLoaded', function (event) {
 99        checkTocPosition();
100
101        elements = document.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]');
102         // Make the first header active
103         activeElement = elements[0];
104         const id = encodeURI(activeElement.getAttribute('id')).toLowerCase();
105         document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
106     }, false);
107
108    window.addEventListener('resize', function(event) {
109        checkTocPosition();
110    }, false);
111
112    window.addEventListener('scroll', () => {
113        // Check if there is an object in the top half of the screen or keep the last item active
114        activeElement = Array.from(elements).find((element) => {
115            if ((getOffsetTop(element) - window.pageYOffset) > 0 && 
116                (getOffsetTop(element) - window.pageYOffset) < window.innerHeight/2) {
117                return element;
118            }
119        }) || activeElement
120
121        elements.forEach(element => {
122             const id = encodeURI(element.getAttribute('id')).toLowerCase();
123             if (element === activeElement){
124                 document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
125             } else {
126                 document.querySelector(`.inner ul li a[href="#${id}"]`).classList.remove('active');
127             }
128         })
129     }, false);
130
131    const main = parseInt(getComputedStyle(document.body).getPropertyValue('--article-width'), 10);
132    const toc = parseInt(getComputedStyle(document.body).getPropertyValue('--toc-width'), 10);
133    const gap = parseInt(getComputedStyle(document.body).getPropertyValue('--gap'), 10);
134
135    function checkTocPosition() {
136        const width = document.body.scrollWidth;
137
138        if (width - main - (toc * 2) - (gap * 4) > 0) {
139            document.getElementById("toc-container").classList.add("wide");
140        } else {
141            document.getElementById("toc-container").classList.remove("wide");
142        }
143    }
144
145    function getOffsetTop(element) {
146        if (!element.getClientRects().length) {
147            return 0;
148        }
149        let rect = element.getBoundingClientRect();
150        let win = element.ownerDocument.defaultView;
151        return rect.top + win.pageYOffset;   
152    }
153</script>
154{{- end }}
 1:root {
 2    --nav-width: 1380px;
 3    --article-width: 650px;
 4    --toc-width: 300px;
 5}
 6
 7.toc {
 8    margin: 0 2px 40px 2px;
 9    border: 1px solid var(--border);
10    background: var(--entry);
11    border-radius: var(--radius);
12    padding: 0.4em;
13}
14
15.toc-container.wide {
16    position: absolute;
17    height: 100%;
18    border-right: 1px solid var(--border);
19    left: calc((var(--toc-width) + var(--gap)) * -1);
20    top: calc(var(--gap) * 2);
21    width: var(--toc-width);
22}
23
24.wide .toc {
25    position: sticky;
26    top: var(--gap);
27    border: unset;
28    background: unset;
29    border-radius: unset;
30    width: 100%;
31    margin: 0 2px 40px 2px;
32}
33
34.toc details summary {
35    cursor: zoom-in;
36    margin-inline-start: 20px;
37    padding: 12px 0;
38}
39
40.toc details[open] summary {
41    font-weight: 500;
42}
43
44.toc-container.wide .toc .inner {
45    margin: 0;
46}
47
48.active {
49    font-size: 110%;
50    font-weight: 600;
51}
52
53.toc ul {
54    list-style-type: circle;
55}
56
57.toc .inner {
58    margin: 0 0 0 20px;
59    padding: 0px 15px 15px 20px;
60    font-size: 16px;
61
62    /*目录显示高度*/
63    max-height: 83vh;
64    overflow-y: auto;
65}
66
67.toc .inner::-webkit-scrollbar-thumb {  /*滚动条*/
68    background: var(--border);
69    border: 7px solid var(--theme);
70    border-radius: var(--radius);
71}
72
73.toc li ul {
74    margin-inline-start: calc(var(--gap) * 0.5);
75    list-style-type: none;
76}
77
78.toc li {
79    list-style: none;
80    font-size: 0.95rem;
81    padding-bottom: 5px;
82}
83
84.toc li a:hover {
85    color: var(--secondary);
86}

添加网站访问量

参考之前的文章:Hugo-Diary 主题修改记录

添加 GitHub 风格的 Alert 块引用

参考之前的文章:Hugo博客添加GitHub风格的Alert块引用

Important

需要 Hugo 版本在 0.132 以上

新建 ~/layouts/_default/_markup/render-blockquote-alert.html,写入模板内容:

 1{{ $alertTypes := dict
 2    "note" "<path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path>"
 3    "tip" "<path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path>"
 4    "important" "<path d=\"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path>"
 5    "warning" "<path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path>"
 6    "caution" "<path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path>"
 7  }}
 8  {{ if eq .Type "alert" }}
 9    <blockquote class="alert-blockquote alert-{{ .AlertType }}">
10      <p class="alert-heading">
11        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
12          {{ index $alertTypes .AlertType | safeHTML }}
13        </svg>
14        <span>{{ or (i18n .AlertType) (title .AlertType) }}</span>
15      </p>
16      {{ .Text | safeHTML }}
17    </blockquote>
18  {{ else }}
19    <blockquote>
20      {{ .Text | safeHTML }}
21    </blockquote>
22  {{ end }}
23
24
25<style>
26  :root {
27    --alert-title-color: #fff;
28    --alert-content-color: inherit;
29  }
30  
31  .post-content .alert-blockquote {
32    --title-background-color: #166dd0;
33    --content-background-color: #e7f2fa;
34    padding: 0;
35    margin: 1rem 0;
36    border-radius: 4px;
37    color: var(--alert-content-color);
38    border-inline-start: none;
39  }
40  
41  .alert-blockquote * {
42    color: inherit;
43  }
44  
45  .alert-blockquote .alert-heading {
46    padding: 4px 18px;
47    border-radius: 4px 4px 0 0;
48    font-weight: 600;
49    color: var(--alert-title-color);
50    display: flex;
51    align-items: center;
52    background: var(--title-background-color);
53    margin-bottom: 0;
54  }
55  
56  .alert-heading svg {
57    width: 1em;
58    height: 1em;
59    margin-right: 0.5rem;
60    fill: currentColor;
61  }
62  
63  .alert-blockquote > :not(.alert-heading) {
64    padding: 18px;
65    background-color: var(--content-background-color);
66  }
67  
68  .alert-blockquote p:last-child {
69    text-align: justify;
70    margin-bottom: 0;
71  }
72  
73  .alert-blockquote.alert-tip { --title-background-color: #1a7f37; --content-background-color: #efe; }
74  .alert-blockquote.alert-important { --title-background-color: #8250df; --content-background-color: #f5f0ff; }
75  .alert-blockquote.alert-warning { --title-background-color: #9a6700; --content-background-color: #fff8c5; }
76  .alert-blockquote.alert-caution { --title-background-color: #cf222e; --content-background-color: #ffebe9; }
77  
78  body.dark {
79    :root {
80      --alert-content-color: #d0d7dd;
81    }
82  
83    .post-content .alert-blockquote {
84      --title-background-color: #58a6ff;
85      --content-background-color: #0d1d30;
86    }
87  
88    .alert-blockquote.alert-tip { --title-background-color: #3fb950; --content-background-color: #0f2a1b; }
89    .alert-blockquote.alert-important { --title-background-color: #a371f7; --content-background-color: #2a1d3f; }
90    .alert-blockquote.alert-warning { --title-background-color: #d29922; --content-background-color: #3b2300; }
91    .alert-blockquote.alert-caution { --title-background-color: #f85149; --content-background-color: #3d0c0c; }
92  }
93</style>
Important

下面这是 GitHub 风格

 1<style>
 2    .post-content .alert-blockquote {
 3      border-left: 4px solid;
 4      border-radius: 5px;
 5    }
 6    
 7    .alert-blockquote .alert-heading {
 8      display: flex;
 9      align-items: center;
10      font-weight: 600;
11      margin-bottom: 0.5rem;
12    }
13    
14    .alert-blockquote .alert-heading svg {
15      margin-right: 0.5rem;
16      fill: currentColor;
17    }
18
19    /* 左侧border颜色 */
20    .post-content .alert-note { border-left-color: #0969da; }
21    .post-content .alert-tip { border-left-color: #1a7f37; }
22    .post-content .alert-important { border-left-color: #8250df; }
23    .post-content .alert-warning { border-left-color: #9a6700; }
24    .post-content .alert-caution { border-left-color: #cf222e; }
25
26    /* 标题颜色 */
27    .alert-note .alert-heading { color: #0969da; }
28    .alert-tip .alert-heading { color: #1a7f37; }
29    .alert-important .alert-heading { color: #8250df; }
30    .alert-warning .alert-heading { color: #9a6700; }
31    .alert-caution .alert-heading { color: #cf222e; }
32    
33    body.dark .alert-note .alert-heading { color: #58a6ff; }
34    body.dark .alert-tip .alert-heading { color: #3fb950; }
35    body.dark .alert-important .alert-heading { color: #a371f7; }
36    body.dark .alert-warning .alert-heading { color: #d29922; }
37    body.dark .alert-caution .alert-heading { color: #f85149; }
38</style>

热力图

代码直接复制的原博主代码,访问原文,也可以参考下面两篇文章:

新建一个短代码 ~/layouts/shortcodes/heatmap.html

  1<p style="text-align: center;">文章统计</p>
  2<div class="heatmap_container"> <!-- 全部用 Flex 排版 -->
  3    <div class="heatmap_content">
  4        <div class="heatmap_week">
  5            <span>Mon</span>
  6            <span>&nbsp;</span> <!-- 不需要显示的星期用空格表示 -->
  7            <span>Wed</span>
  8            <span>&nbsp;</span>
  9            <span>Fri</span>
 10            <span>&nbsp;</span>
 11            <span>Sun</span>
 12        </div>
 13        <div class="heatmap_main">
 14            <div class="month heatmap_month">
 15                <!-- js 检测屏幕宽度动态生成月份 -->
 16            </div>
 17            <div id="heatmap" class="heatmap">
 18                <!-- js 检测屏幕宽度动态生成年度日历小方块 -->
 19            </div>
 20        </div>
 21    </div>
 22    <div class="heatmap_footer">
 23        <div class="heatmap_less">Less</div>
 24        <div class="heatmap_level">
 25            <span class="heatmap_level_item heatmap_level_0"></span>
 26            <span class="heatmap_level_item heatmap_level_1"></span>
 27            <span class="heatmap_level_item heatmap_level_2"></span>
 28            <span class="heatmap_level_item heatmap_level_3"></span>
 29            <span class="heatmap_level_item heatmap_level_4"></span>
 30        </div>
 31        <div class="heatmap_more">More</div>
 32    </div>
 33</div>
 34
 35
 36<style>
 37    :root {
 38        /* GitHub Light Color */
 39        --ht-main: #334155;
 40        --ht-day-bg: #ebedf0;
 41        --ht-tooltip: #24292f;
 42        --ht-tooltip-bg: #fff;
 43        --ht-lv-0: #ebedf0;
 44        --ht-lv-1: #9be9a8;
 45        --ht-lv-2: #40c463;
 46        --ht-lv-3: #30a14e;
 47        --ht-lv-4: #216e39;
 48    }
 49    
 50    body.dark {
 51        /* GitHub Dark Dimmed Color */
 52        --ht-main: #94a3b8;
 53        --ht-day-bg: #161b22;
 54        --ht-tooltip: #24292f;
 55        --ht-tooltip-bg: #fff;
 56        --ht-lv-0: #161b22;
 57        --ht-lv-1: #0e4429;
 58        --ht-lv-2: #006d32;
 59        --ht-lv-3: #26a641;
 60        --ht-lv-4: #39d353;
 61    }
 62    
 63    .heatmap_container {
 64        display: flex;
 65        flex-direction: column;
 66        align-items: center;
 67        font-size: 10px;
 68        line-height: 12px;
 69        color: var(--ht-main);
 70    }
 71    
 72    .heatmap_content {
 73        display: flex;
 74        flex-direction: row;
 75        align-items: flex-end
 76    }
 77    
 78    .heatmap_week {
 79        display: flex;
 80        margin-top: 0.25rem;
 81        margin-right: 2px;
 82        flex-direction: column;
 83        justify-content: flex-end;
 84        align-items: flex-end;
 85        text-align: right
 86    }
 87    
 88    .heatmap_main {
 89        display: flex;
 90        flex-direction: column
 91    }
 92    
 93    .heatmap_month {
 94        display: flex;
 95        margin-top: 0.25rem;
 96        margin-right: 0.25rem;
 97        justify-content: space-around;
 98        align-items: flex-end;
 99        text-align: right;
100    }
101    
102    .heatmap {
103        display: flex;
104        flex-direction: row;
105        height: 84px;
106    }
107    
108    .heatmap_footer {
109        display: flex;
110        margin-top: 0.5rem;
111        align-items: center
112    }
113    
114    .heatmap_level {
115        display: flex;
116        gap: 2px;
117        margin: 0 0.25rem;
118        flex-direction: row;
119        align-items: center;
120        width: max-content;
121        height: 10px
122    }
123    
124    .heatmap_level_item {
125        display: block;
126        border-radius: 0.125rem;
127        width: 10px;
128        height: 10px;
129    }
130    
131    .heatmap_level_0 {
132        background: var(--ht-lv-0);
133    }
134    
135    .heatmap_level_1 {
136        background: var(--ht-lv-1);
137    }
138    
139    .heatmap_level_2 {
140        background: var(--ht-lv-2);
141    }
142    
143    .heatmap_level_3 {
144        background: var(--ht-lv-3);
145    }
146    
147    .heatmap_level_4 {
148        background: var(--ht-lv-4);
149    }
150    
151    .heatmap_week {
152        display: flex;
153        flex-direction: column;
154    }
155    
156    .heatmap_day {
157        width: 10px;
158        height: 10px;
159        background-color: var(--ht-day-bg);
160        margin: 1px;
161        border-radius: 2px;
162        display: inline-block;
163        position: relative;
164    }
165    
166    .heatmap_tooltip {
167        position: absolute;
168        bottom: 12px;
169        left: 50%;
170        width: max-content;
171        color: var(--ht-tooltip);
172        background-color: var(--ht-tooltip-bg);
173        font-size: 12px;
174        line-height: 16px;
175        padding: 8px;
176        border-radius: 3px;
177        white-space: pre-wrap;
178        opacity: 1;
179        transition: 0.3s;
180        z-index: 1000;
181        text-align: right;
182        transform: translateX(-50%);
183    }
184    
185    .heatmap_tooltip_count,
186    .heatmap_tooltip_post {
187        display: inline-block;
188    }
189    
190    .heatmap_tooltip_title,
191    .heatmap_tooltip_date {
192        display: block;
193    }
194    
195    .heatmap_tooltip_date {
196        margin: 0 0.25rem;
197    }
198    
199    .heatmap_day_level_0 {
200        background-color: var(--ht-lv-0);
201    }
202    
203    .heatmap_day_level_1 {
204        background-color: var(--ht-lv-1);
205    }
206    
207    .heatmap_day_level_2 {
208        background-color: var(--ht-lv-2);
209    }
210    
211    .heatmap_day_level_3 {
212        background-color: var(--ht-lv-3);
213    }
214    
215    .heatmap_day_level_4 {
216        background-color: var(--ht-lv-4);
217    }
218</style>
219
220
221<script>
222    // 获取最近一年的文章数据
223{{ $pages := where .Site.RegularPages "Date" ">" (now.AddDate -1 0 0) }}
224{{ $pages := $pages.Reverse }}
225var blogInfo = {
226    "pages": [
227        {{ range $index, $element := $pages }}
228            {
229                "title": "{{ replace (replace .Title "" "") "" "" }}",
230                "date": "{{ .Date.Format "2006-01-02" }}",
231                "year": "{{ .Date.Format "2006" }}",
232                "month": "{{ .Date.Format "01" }}",
233                "day": "{{ .Date.Format "02" }}",
234                "word_count": "{{ .WordCount }}"
235            }{{ if ne (add $index 1) (len $pages) }},{{ end }}
236            {{ end }}
237    ]
238};
239// console.log(blogInfo)
240
241let currentDate = new Date();
242currentDate.setFullYear(currentDate.getFullYear() - 1);
243
244let startDate;
245
246let monthDiv = document.querySelector('.month');
247let monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
248
249if (window.innerWidth < 768) {
250    numMonths = 6;
251} else {
252    numMonths = 12;
253}
254
255let startMonthIndex = (currentDate.getMonth() - (numMonths - 1) + 12) % 12;
256for (let i = startMonthIndex; i < startMonthIndex + numMonths; i++) {
257    let monthSpan = document.createElement('span');
258    let monthIndex = i % 12;
259    monthSpan.textContent = monthNames[monthIndex];
260    monthDiv.appendChild(monthSpan);
261}
262
263function getStartDate() {
264    const today = new Date();
265
266    if (window.innerWidth < 768) {
267        numMonths = 6;
268    } else {
269        numMonths = 12;
270    }
271
272    const startDate = new Date(today.getFullYear(), today.getMonth() - numMonths + 1, 1, today.getHours(), today.getMinutes(), today.getSeconds());
273
274    while (startDate.getDay() !== 1) {
275        startDate.setDate(startDate.getDate() + 1);
276    }
277
278    return startDate;
279}
280
281function getWeekDay(date) {
282    const day = date.getDay();
283    return day === 0 ? 6 : day - 1;
284}
285
286function createDay(date, title, count, post) {
287    const day = document.createElement("div");
288
289    day.className = "heatmap_day";
290
291    day.setAttribute("data-title", title);
292    day.setAttribute("data-count", count);
293    day.setAttribute("data-post", post);
294    day.setAttribute("data-date", date);
295
296    day.addEventListener("mouseenter", function () {
297        const tooltip = document.createElement("div");
298        tooltip.className = "heatmap_tooltip";
299
300        let tooltipContent = "";
301
302        if (post && parseInt(post, 10) !== 0) {
303            tooltipContent += '<span class="heatmap_tooltip_post">' + '共 ' + post + ' 篇' + '</span>';
304        }
305
306        if (count && parseInt(count, 10) !== 0) {
307            tooltipContent += '<span class="heatmap_tooltip_count">' + ' ' + count + ' 字;' + '</span>';
308        }
309
310        if (title && parseInt(title, 10) !== 0) {
311            tooltipContent += '<span class="heatmap_tooltip_title">' + title + '</span>';
312        }
313
314        if (date) {
315            tooltipContent += '<span class="heatmap_tooltip_date">' + date + '</span>';
316        }
317
318        tooltip.innerHTML = tooltipContent;
319        day.appendChild(tooltip);
320    });
321
322    day.addEventListener("mouseleave", function () {
323        const tooltip = day.querySelector(".heatmap_tooltip");
324        if (tooltip) {
325            day.removeChild(tooltip);
326        }
327    });
328
329    if (count == 0 ) {
330        day.classList.add("heatmap_day_level_0");
331    } else if (count > 0 && count < 1000) {
332        day.classList.add("heatmap_day_level_1");
333    } else if (count >= 1000 && count < 2000) {
334        day.classList.add("heatmap_day_level_2");
335    } else if (count >= 2000 && count < 3000) {
336        day.classList.add("heatmap_day_level_3");
337    } else {
338        day.classList.add("heatmap_day_level_4");
339    }
340
341    return day;
342}
343
344function createWeek() {
345    const week = document.createElement('div');
346    week.className = 'heatmap_week';
347    return week;
348}
349
350function createHeatmap() {
351    const container = document.getElementById('heatmap');
352    const startDate = getStartDate();
353    const endDate = new Date();
354    const weekDay = getWeekDay(startDate);
355
356    let currentWeek = createWeek();
357    container.appendChild(currentWeek);
358
359    let currentDate = startDate;
360    let i = 0;
361
362    while (currentDate <= endDate) {
363        if (i % 7 === 0 && i !== 0) {
364            currentWeek = createWeek();
365            container.appendChild(currentWeek);
366        }
367
368        const dateString = `${currentDate.getFullYear()}-${("0" + (currentDate.getMonth()+1)).slice(-2)}-${("0" + (currentDate.getDate())).slice(-2)}`;
369
370        const articleDataList = blogInfo.pages.filter(page => page.date === dateString);
371
372        if (articleDataList.length > 0) {
373            const titles = articleDataList.map(data => data.title);
374            const title = titles.map(t => `《${t}》`).join('<br />');
375
376            let count = 0;
377            let post = articleDataList.length;
378
379            articleDataList.forEach(data => {
380                count += parseInt(data.word_count, 10);
381            });
382
383            const formattedDate = formatDate(currentDate);
384            const day = createDay(formattedDate, title, count, post);
385            currentWeek.appendChild(day);
386        } else {
387            const formattedDate = formatDate(currentDate);
388            const day = createDay(formattedDate, '', '0', '0');
389            currentWeek.appendChild(day);
390        }
391
392        i++;
393        currentDate.setDate(currentDate.getDate() + 1);
394    }
395}
396
397function formatDate(date) {
398    const options = { month: 'short', day: 'numeric', year: 'numeric' };
399    return date.toLocaleDateString('en-US', options);
400}
401
402createHeatmap();
403</script>

可以修改其中的颜色适应自己的博客,使用方法如下:

1{\{< heatmap >}\}

图片展示

图片排版优化

代码来自:木木木木木博客 正文中的图片排版优化,直接通过 CSS 来,在自定义样式目录新建文件添加下面的样式:

1.post-content p:has(> img:nth-child(2)){column-count:2;column-gap:8px;margin:6px 0;}
2.post-content p:has(> img:nth-child(3)){column-count:3;}
3.post-content p:has(> img:nth-child(4)){column-count:4;}
4.post-content p:has(> img:nth-child(5)){column-count:5;}
5.post-content p:has(> img:nth-child(6)){column-count:4;}
6.post-content p:has(> img:nth-child(2)) img{display:inherit;}
7.post-content p:has(> img:nth-child(6)) img{margin-bottom:8px;}

使用时直接以 markdown 书写即可,中间不要有空行。

图片瀑布流

代码来自:木木木木木博客extend_footer.html 中添加 js

 1{{/*  gallery瀑布流  */}}
 2<script src="https://immmmm.com/waterfall.min.js"></script>
 3<script src="https://immmmm.com/imgStatus.min.js"></script>
 4<script>
 5  document.addEventListener('DOMContentLoaded', () => {
 6    //外链 gallery 标签相册瀑布流
 7    var photosAll = document.getElementsByTagName('gallery') || '';
 8    if(photosAll){
 9      for(var i=0;i < photosAll.length;i++){
10        photosAll[i].innerHTML = '<div class="gallery-photos">'+photosAll[i].innerHTML+'</div>'
11        var photosIMG = photosAll[i].getElementsByTagName('img')
12        for(var j=0;j < photosIMG.length;j++){
13          wrap(photosIMG[j], document.createElement('div'));
14        }
15      }
16    }
17    function wrap(el, wrapper) {
18      wrapper.className = "gallery-photo";
19      el.parentNode.insertBefore(wrapper, el);
20      wrapper.appendChild(el);
21    }
22    //相册瀑布流
23    let galleryPhotos = document.querySelectorAll('.gallery-photos') || ''
24    if(galleryPhotos){
25      imgStatus.watch('.gallery-photo img', function(imgs) {
26        if(imgs.isDone()){
27          for(var i=0;i < galleryPhotos.length;i++){
28            waterfall(galleryPhotos[i]);
29            let pagePhoto = galleryPhotos[i].querySelectorAll('.gallery-photo');
30            for(var j=0;j < pagePhoto.length;j++){pagePhoto[j].className += " visible"};
31          }
32        }
33      });
34      window.addEventListener('resize', function () {
35        for(var i=0;i < galleryPhotos.length;i++){
36          waterfall(galleryPhotos[i]);
37        }
38      });
39    }
40  });
41</script>
42<script type="text/javascript" src="https://immmmm.com/view-image.js"></script>
43<script>
44  window.ViewImage && ViewImage.init('.gallery-photo img')
45</script>
Tip

建议将代码中的 JS 文件放入自己博客根目录 static 文件夹下引入。

引入样式:

 1/* gallery瀑布流 */
 2.gallery-photos{width:100%;}
 3.gallery-photo{width:24.9%;position: relative;visibility: hidden;overflow: hidden;}
 4.gallery-photo.visible{visibility: visible;animation: fadeIn 2s;}
 5.gallery-photo img{display: block;width:100%;border-radius:0;padding:4px;animation: fadeIn 1s;cursor: pointer;transition: all .4s ease-in-out;}
 6.gallery-photo span.photo-title,.gallery-photo span.photo-time{background: rgba(0, 0, 0, 0.3);padding:0px 8px;font-size:0.9rem;color: #fff;display:none;animation: fadeIn 1s;}
 7.gallery-photo span.photo-title{position:absolute;bottom:4px;left:4px;}
 8.gallery-photo span.photo-time{position:absolute;top:4px;left:4px;font-size:0.8rem;}
 9.gallery-photo:hover span.photo-title{display:block;}
10.gallery-photo:hover img{transform: scale(1.1);}
11@media screen and (max-width: 1280px) {
12    .gallery-photo{width:33.3%;}
13}
14@media screen and (max-width: 860px) {
15    .gallery-photo{width:49.9%;}
16}
17@media (max-width: 683px){
18    .photo-time{display: none;}
19}
20@keyframes fadeIn{
21    0% {opacity: 0;}
22   100% {opacity: 1;}
23}

使用如下:

1<gallery>
2    <img src="https://xxxxx.jpg">
3    <img src="https://xxxxx.jpg">
4    <img src="https://xxxxx.jpg">
5</gallery>
Tip

可以引入外链图片,也可以引入本地图片,默认路径为 ~/static/ ,如果使用 markdown 直接将图片放置到一行,不要换行。

图片橱窗

通过橱窗样式展示图片,代码来自:codepen

新建 ~/layouts/shortcodes/image-showcase.html,直接将所有内容放入其中:

  1<div class="app">
  2    <form class="plate">
  3      {{ range $index, $src := .Params }}
  4        <label class="item">
  5          <input type="radio" name="item" {{ if eq $index 0 }}checked="checked"{{ end }}/>
  6          <img src="{{ $src }}"/>
  7        </label>
  8      {{ end }}
  9    </form>
 10  </div>
 11  
 12
 13<style>
 14/** side by side **/
 15.app:nth-of-type(1) {
 16    place-self: center right;
 17  }
 18  .app:nth-of-type(2) {
 19    place-self: center left;
 20  }
 21  
 22  .app {
 23    --preview-item-width: calc(100% / (var(--item-count) - 1));
 24    --preview-item-height: 10%;
 25    width: 50vmin;
 26    height: 80vmin;
 27    margin: auto;
 28  }
 29  
 30  .app [type="radio"] {
 31    display: none;
 32  }
 33  
 34  .app .plate {
 35    width: 100%;
 36    height: 100%;
 37    position: relative;
 38  }
 39  
 40  .app .plate .item {
 41    display: block;
 42    width: var(--preview-item-width);
 43    height: var(--preview-item-height);
 44    position: absolute;
 45    bottom: 0;
 46    left: var(--left);
 47    transform-origin: top left;
 48    transition: transform 0.5s, bottom 0.6s, left 0.6s, width 0.3s,
 49      box-shadow 0.6s;
 50  }
 51  
 52  .app .plate .item img {
 53    display: block;
 54    width: 100%;
 55    height: 100%;
 56    object-fit: cover;
 57  }
 58  
 59  .app .plate .item.active {
 60    --preview-item-width: 100%;
 61    bottom: var(--preview-item-height); /* bubble up */
 62    left: 0 !important;
 63    height: calc(100% - var(--preview-item-height));
 64    box-shadow: 0 0 0 transparent;
 65    animation: anim 2s 1;
 66    transform: translate3d(0, 0, -10px);
 67    transition: transform 0.5s, bottom 0.6s, left 0.6s, width 0.3s, box-shadow 0s;
 68  }
 69  
 70  .app .plate .item.active img {
 71    object-fit: contain;
 72  }
 73  
 74  /* 
 75  optional
 76  */
 77  
 78  .app .plate {
 79    perspective: 100px;
 80    perspective-origin: center center;
 81    transform-style: preserve-3d;
 82    pointer-events: none;
 83  }
 84  
 85  .app .plate::after {
 86    content: "";
 87    display: block;
 88    width: 100%;
 89    height: 15px;
 90    position: absolute;
 91    bottom: 0;
 92    background: linear-gradient(transparent, rgba(0, 0, 0, 0.1));
 93    transform: rotateX(90deg);
 94    transform-origin: bottom center;
 95  }
 96  
 97  .app .plate .item:not(.active) {
 98    transform-origin: center;
 99    transform: scale(0.8) translate3d(0, 0, -5px);
100    pointer-events: auto;
101  }
102  
103  .app .plate .item:not(.active):hover {
104    transform-origin: center;
105    transform: scale(0.8) translate3d(0, -1px, -5px);
106    box-shadow: 0 20px 10px -10px rgba(0, 0, 0, 0.3);
107    cursor: pointer;
108  }
109  
110  /*
111  animation
112  */
113  
114  @keyframes anim {
115    from {
116      transform: rotateY(6deg) rotateX(3deg);
117    }
118  }
119</style>
120
121
122<script>
123document.querySelectorAll('.app').forEach(init)
124
125function init(app) {
126  const items = app.querySelectorAll('.item')
127  app.style.setProperty('--item-count', items.length)
128  const form = app.querySelector('.plate')
129  form.addEventListener('input', () => update(app))
130  update(app)
131}
132
133function update(app) {
134  const items = Array.from(app.querySelectorAll('.item'))
135  const active = items.filter(x => x.querySelector(':checked'))[0]
136  const inactives = items.filter(x => x != active)
137  // toggle class
138  items.forEach(x => x.classList.toggle('active', x === active))
139  // re-calc anim props
140  inactives.forEach((x, i, xs) => x.style.setProperty('--left', `${i / xs.length}e2%`))
141}
142</script>

使用短代码如下:

1{\{< image-showcase "/images/pic1.jpg" "/images/pic2.jpg" "/images/pic3.jpg" >}\}
Tip

图片地址默认仍然是博客根目录下 static 文件夹,或者引入外链图片也可以

原始代码来自:一种在 MemE 主题中实现轮播图功能的思路

修改代码来自:Hugo | 在文章中插入轮播图片

新建 ~/lauouts/shortcodes/loop.html

 1{{ if .Site.Params.enableimgloop }}
 2    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/css/swiper.min.css">
 3    <!-- Swiper -->
 4    <div class="swiper-container">
 5        <div class="swiper-wrapper">
 6            {{$itItems := split (.Get 0) ","}}
 7            {{range $itItems }}
 8            <div class="swiper-slide">
 9                <img src="{{.}}" alt="">
10            </div>
11            {{end}}
12        </div>
13        <!-- Add Pagination -->
14        <div class="swiper-pagination"></div>
15    </div>
16
17    <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/3.4.2/js/swiper.min.js"></script>
18     <!-- Initialize Swiper -->
19     <script>
20        var swiper = new Swiper('.swiper-container', {
21            pagination: '.swiper-pagination',
22            paginationClickable: true,
23        //自动调节高度
24        autoHeight: true,
25        //键盘左右方向键控制
26        keyboardControl : true,
27        //鼠标滑轮控制
28        mousewheelControl : true,
29        //自动切换
30        //autoplay : 5000,
31        //懒加载
32        lazyLoading : true,
33		lazyLoadingInPrevNext : true,
34		//无限循环
35		loop : true,
36        });
37        </script>
38{{ end }}
39
40<style>
41    .swiper-container {
42        width: 100%;
43        margin: 2em auto;
44        height: 330px;
45        border-radius: 10px;
46    
47    }
48    .swiper-slide {
49        text-align: center;
50        font-size: 18px;
51        background-color: #fff;
52        /* Center slide text vertically */
53        display: flex;
54        justify-content: center;
55        align-items: center;
56        img {
57            margin: 0 !important;
58        }
59    }
60    
61</style>

使用方法: 在站点配置文件中写入参数:

1params:
2  enableimgloop: true
1{\{< loop "/images/1.jpg,/images/2.jpg,/images/3.jpg" >}\}

参考

Hugo-PaperMod主题自定义

https://blog.grew.cc/posts/papermod-modify/

作者

Tom

创建时间

2024-08-28

最后更新时间

2024-08-28

许可协议

CC BY 4.0