基于个性化需求,对PaperMod的布局样式和功进行调整,现将其记录如下:

选择PaperMod的理由:

  • 仍在维护更新
  • 界面干净整洁
  • 支持语法高亮
  • SEO 友好等等

主题安装与基础配置

参照文档,此处略过。

虽然标题是主题折腾记录,但有三件事还是值得反复提醒自己:

  1. 少折腾主题
  2. 少折腾主题
  3. 少折腾主题!

好,下面开始折腾。

使用 Chroma 生成代码高亮,参考自这里

生成 CSS 类 hugo gen chromastyles --style="dracula" > assets/css/extended/chroma.css 通过 hugo gen 命令可以生成 Chroma 的样式,在 PaperMod Wiki 中也有提到,凡是在 assets/css/extended 下的 css 文件,都会被自动加载。

侧边目录+阅读进度,参考自这里

侧边目录

在 PaperMod 中,目录相关的 html 代码定义在 layouts/partials/toc.html 中,为了修改它,只要创建一个 <your_hugo_site>/layouts/partials/toc.html 覆盖即可,在其中粘贴如下代码:

{{- $headers := findRE "<h[1-6].*?>(.|\n])+?</h[1-6]>" .Content -}}
{{- $has_headers := ge (len $headers) 1 -}}
{{- if $has_headers -}}
<aside id="toc-container" class="toc-container wide">
    <div class="toc">
        <details {{if (.Param "TocOpen") }} open{{ end }}>
            <summary accesskey="c" title="(Alt + C)">
                <span class="details">{{- i18n "toc" | default "Table of Contents" }}</span>
            </summary>

            <div class="inner">
                {{- $largest := 6 -}}
                {{- range $headers -}}
                {{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
                {{- $headerLevel := len (seq $headerLevel) -}}
                {{- if lt $headerLevel $largest -}}
                {{- $largest = $headerLevel -}}
                {{- end -}}
                {{- end -}}

                {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}}

                {{- $.Scratch.Set "bareul" slice -}}
                <ul>
                    {{- range seq (sub $firstHeaderLevel $largest) -}}
                    <ul>
                        {{- $.Scratch.Add "bareul" (sub (add $largest .) 1) -}}
                        {{- end -}}
                        {{- range $i, $header := $headers -}}
                        {{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
                        {{- $headerLevel := len (seq $headerLevel) -}}

                        {{/* get id="xyz" */}}
                        {{- $id := index (findRE "(id=\"(.*?)\")" $header 9) 0 }}

                        {{- /* strip id="" to leave xyz, no way to get regex capturing groups in hugo */ -}}
                        {{- $cleanedID := replace (replace $id "id=\"" "") "\"" "" }}
                        {{- $header := replaceRE "<h[1-6].*?>((.|\n])+?)</h[1-6]>" "$1" $header -}}

                        {{- if ne $i 0 -}}
                        {{- $prevHeaderLevel := index (findRE "[1-6]" (index $headers (sub $i 1)) 1) 0 -}}
                        {{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
                        {{- if gt $headerLevel $prevHeaderLevel -}}
                        {{- range seq $prevHeaderLevel (sub $headerLevel 1) -}}
                        <ul>
                            {{/* the first should not be recorded */}}
                            {{- if ne $prevHeaderLevel . -}}
                            {{- $.Scratch.Add "bareul" . -}}
                            {{- end -}}
                            {{- end -}}
                            {{- else -}}
                            </li>
                            {{- if lt $headerLevel $prevHeaderLevel -}}
                            {{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}}
                            {{- if in ($.Scratch.Get "bareul") . -}}
                        </ul>
                        {{/* manually do pop item */}}
                        {{- $tmp := $.Scratch.Get "bareul" -}}
                        {{- $.Scratch.Delete "bareul" -}}
                        {{- $.Scratch.Set "bareul" slice}}
                        {{- range seq (sub (len $tmp) 1) -}}
                        {{- $.Scratch.Add "bareul" (index $tmp (sub . 1)) -}}
                        {{- end -}}
                        {{- else -}}
                    </ul>
                    </li>
                    {{- end -}}
                    {{- end -}}
                    {{- end -}}
                    {{- end }}
                    <li>
                        <a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
                        {{- else }}
                    <li>
                        <a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
                        {{- end -}}
                        {{- end -}}
                        <!-- {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}} -->
                        {{- $firstHeaderLevel := $largest }}
                        {{- $lastHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers (sub (len $headers) 1)) 1) 0)) }}
                    </li>
                    {{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
                    {{- if in ($.Scratch.Get "bareul") (add . $firstHeaderLevel) }}
                </ul>
                {{- else }}
                </ul>
                </li>
                {{- end -}}
                {{- end }}
                </ul>
            </div>
        </details>
    </div>
</aside>
<script>
    let activeElement;
    let elements;
    
    document.addEventListener('DOMContentLoaded', function (event) {
        checkTocPosition();
    
        elements = document.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]');
        if (elements.length > 0) {
            // Make the first header active
            activeElement = elements[0];
            const id = encodeURI(activeElement.getAttribute('id')).toLowerCase();
            document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
        }
    
        // Add event listener for the "back to top" link
        const topLink = document.getElementById('top-link');
        if (topLink) {
            topLink.addEventListener('click', (event) => {
                // Prevent the default action
                event.preventDefault();
    
                // Smooth scroll to the top
                window.scrollTo({ top: 0, behavior: 'smooth' });
            });
        }
    }, false);
    
    window.addEventListener('resize', function(event) {
        checkTocPosition();
    }, false);
    
    window.addEventListener('scroll', () => {
        // Get the current scroll position
        const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
    
        // Check if the scroll position is at the top of the page
        if (scrollPosition === 0) {
            return;
        }
    
        // Ensure elements is a valid NodeList
        if (elements && elements.length > 0) {
            // Check if there is an object in the top half of the screen or keep the last item active
            activeElement = Array.from(elements).find((element) => {
                if ((getOffsetTop(element) - scrollPosition) > 0 && 
                    (getOffsetTop(element) - scrollPosition) < window.innerHeight / 2) {
                    return element;
                }
            }) || activeElement;
    
            elements.forEach(element => {
                const id = encodeURI(element.getAttribute('id')).toLowerCase();
                const tocLink = document.querySelector(`.inner ul li a[href="#${id}"]`);
                if (element === activeElement){
                    tocLink.classList.add('active');
    
                    // Ensure the active element is in view within the .inner container
                    const tocContainer = document.querySelector('.toc .inner');
                    const linkOffsetTop = tocLink.offsetTop;
                    const containerHeight = tocContainer.clientHeight;
                    const linkHeight = tocLink.clientHeight;
    
                    // Calculate the scroll position to center the active link
                    const scrollPosition = linkOffsetTop - (containerHeight / 2) + (linkHeight / 2);
                    tocContainer.scrollTo({ top: scrollPosition, behavior: 'smooth' });
                } else {
                    tocLink.classList.remove('active');
                }
            });
        }
    }, false);
    
    const main = parseInt(getComputedStyle(document.body).getPropertyValue('--article-width'), 10);
    const toc = parseInt(getComputedStyle(document.body).getPropertyValue('--toc-width'), 10);
    const gap = parseInt(getComputedStyle(document.body).getPropertyValue('--gap'), 10);
    
    function checkTocPosition() {
        const width = document.body.scrollWidth;
    
        if (width - main - (toc * 2) - (gap * 4) > 0) {
            document.getElementById("toc-container").classList.add("wide");
        } else {
            document.getElementById("toc-container").classList.remove("wide");
        }
    }
    
    function getOffsetTop(element) {
        if (!element.getClientRects().length) {
            return 0;
        }
        let rect = element.getBoundingClientRect();
        let win = element.ownerDocument.defaultView;
        return rect.top + win.pageYOffset;   
    }
    
</script>
{{- end }}

其中,后半部分为 js 代码,根据阅读内容滚动并加粗相应标题就由其实现。

然后,添加 css 样式的代码,创建 <your_hugo_site>/assets/css/extended/toc.css 文件,并拷贝以下内容:

:root {
    --nav-width: 1380px;
    --article-width: 650px;
    --toc-width: 300px;
}

.toc {
    margin: 0 2px 40px 2px;
    border: 1px solid var(--border);
    background: var(--entry);
    border-radius: var(--radius);
    padding: 0.4em;
}

.toc-container.wide {
    position: absolute;
    height: 100%;
    border-right: 1px solid var(--border);
    left: calc((var(--toc-width) + var(--gap)) * -1);
    top: calc(var(--gap) * 2);
    width: var(--toc-width);
}

.wide .toc {
    position: sticky;
    top: var(--gap);
    border: unset;
    background: unset;
    border-radius: unset;
    width: 100%;
    margin: 0 2px 40px 2px;
}

.toc details summary {
    cursor: zoom-in;
    margin-inline-start: 20px;
    padding: 12px 0;
}

.toc details[open] summary {
    font-weight: 500;
}

.toc-container.wide .toc .inner {
    margin: 0;
}

.active {
    font-size: 110%;
    font-weight: 600;
}

.toc ul {
    list-style-type: circle;
}

.toc .inner {
    margin: 0 0 0 20px;
    padding: 0px 15px 15px 20px;
    font-size: 16px;

    /*目录显示高度*/
    max-height: 83vh;
    overflow-y: auto;
}

.toc .inner::-webkit-scrollbar-thumb {  /*滚动条*/
    background: var(--border);
    border: 7px solid var(--theme);
    border-radius: var(--radius);
}

.toc li ul {
    margin-inline-start: calc(var(--gap) * 0.5);
    list-style-type: none;
}

.toc li {
    list-style: none;
    font-size: 0.95rem;
    padding-bottom: 5px;
}

.toc li a:hover {
    color: var(--secondary);
}

阅读百分比 阅读百分比实现的核心思想就是每当发生滚动事件时,根据滚动条高度计算当前阅读进度。这里我们将进度的数字显示在 TOP 按钮上,TOP 按钮定义在 footer.html 中,因此我们要创建 <your_hugo_site>/layouts/partials/footer.html,将主题中对应位置的 footer.html 内容拷贝进去,然后修改 TOP 按钮相关的代码,原代码为:

{{- if (not site.Params.disableScrollToTop) }}
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
        <path d="M12 6H0l6-6z" />
    </svg>
</a>
{{- end }}

我们要在其中添加一个用于展示进度的 span 和更新进度的 js 代码,即修改为:

{{- if (not .Site.Params.disableScrollToTop) }}
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
    <span class="topInner">
        <svg class="topSvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
            <path d="M12 6H0l6-6z"/>
        </svg>
        <span id="read_progress"></span>
    </span>
</a>

<script>
    document.addEventListener('scroll', function (e) {
        const readProgress = document.getElementById("read_progress");
        const scrollHeight = document.documentElement.scrollHeight;
        const clientHeight = document.documentElement.clientHeight;
        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        readProgress.innerText = ((scrollTop / (scrollHeight - clientHeight)).toFixed(2) * 100).toFixed(0);
    })
</script>
{{- end }}

然后添加相关 css 代码,即创建 <your_hugo_site>/assets/css/extended/top.css 文件,并拷贝以下内容:

/*top*/
.topInner {
    display: grid;
    align-items: baseline;
    justify-items: center;
    margin: 7px;
    font-weight: 900;
}

.topSvg {
    width: 20px;
}

.top-link {
    padding: unset;
}

/*到顶部*/
.top-link {
    background: var(--entry);

    -webkit-transition: box-shadow 0.4s ease, transform 0.4s ease;
    -moz-transition: box-shadow 0.4s ease, transform 0.4s ease;
    -o-transition: box-shadow 0.4s ease, transform 0.4s ease;

    transition: box-shadow 0.4s ease, transform 0.4s ease;
    box-shadow: 0px 2px 4px rgb(5 10 15 / 5%), 0px 7px 13px -3px rgb(5 10 15 / 30%);
}

.dark .top-link {
    background: var(--entry);

    -webkit-transition: box-shadow 0.4s ease, transform 0.4s ease;
    -moz-transition: box-shadow 0.4s ease, transform 0.4s ease;
    -o-transition: box-shadow 0.4s ease, transform 0.4s ease;

    transition: box-shadow 0.4s ease, transform 0.4s ease;
    box-shadow: 0px 2px 4px rgb(5 10 15 / 5%), 0px 7px 13px -3px rgb(5 10 15 / 30%);
}

.top-link:hover {
    color: rgb(108, 108, 108);

    /*-webkit-transform: scale(1.1);*/
    /*-moz-transform: scale(1.1);*/
    /*-ms-transform: scale(1.1);*/
    /*-o-transform: scale(1.1);*/
    /*transform: scale(1.1);*/

    transition: box-shadow 0.4s ease, transform 0.4s ease;
    box-shadow: 0px 4px 8px rgb(5 10 15 / 5%), 0px 7px 13px -3px rgb(5 10 15 / 30%);
}

.dark .top-link:hover {
    color: rgba(180, 181, 182, .8);

    /*-webkit-transform: scale(1.1);*/
    /*-moz-transform: scale(1.1);*/
    /*-ms-transform: scale(1.1);*/
    /*-o-transform: scale(1.1);*/

    /*transform: scale(1.1);*/

    transition: box-shadow 0.4s ease, transform 0.4s ease;
    box-shadow: 0px 4px 8px rgb(5 10 15 / 5%), 0px 7px 13px -3px rgb(5 10 15 / 30%);
}

插入百度统计的错误处理

修改post_meta头部信息

定位到文件:themes/PaperMod/layouts/partials/post_meta.html,替换如下代码

{{- $scratch := newScratch }}
<!-- 创建时间 -->
{{- if not .Date.IsZero -}}
{{- $scratch.Add "meta" (slice (printf "创建:&nbsp;<span title='%s'>%s</span>" (.Date) (.Date.Format (default "January 2, 2006" .Site.Params.DateFormat)))) }}
{{- end }}

<!-- 更新时间 -->
{{- if (.Param "ShowLastMod") -}}
{{- $scratch.Add "meta" (slice (printf "更新:&nbsp;%s" (.Lastmod.Format (.Site.Params.dateFormat | default "2006-01-02")))) }}
{{- end }}

<!-- 统计字数 -->
{{- if (.Param "ShowWordCounts") -}}
{{- $scratch.Add "meta" (slice (default (printf "字数:&nbsp;%d字" .WordCount))) }}
{{- end }}

<!-- 大概需要花费的阅读时间 -->
{{- if (.Param "ShowReadingTime") -}}
{{- $scratch.Add "meta" (slice (default (printf "时长: %d分钟" .ReadingTime))) }}
{{- end }}

<!-- 作者 -->
{{- with (partial "author.html" .) }}
{{- $scratch.Add "meta" (slice .) }}
{{- end }}

<!-- 分隔方式 -->
{{- with ($scratch.Get "meta") }}
{{- delimit . "&nbsp;|&nbsp;" -}}
{{- end -}}

作者的中文显示要找themes/PaperMod/layouts/partials/author.html,设置如下:

{{- if or .Params.author site.Params.author }}
作者:&nbsp;{{- $author := (.Params.author | default site.Params.author) }}
{{- $author_type := (printf "%T" $author) }}
{{- if (or (eq $author_type "[]string") (eq $author_type "[]interface {}")) }}
{{- (delimit $author ", " ) }}
{{- else }}
{{- $author }}
{{- end }}
{{- end -}}

因为有些字段是自己加的,所以还要在config.yml文件的params字段下加上这些字段:

params:
    DateFormat: "2006-01-02"
    ShowWordCounts: true
    ShowReadingTime: true
    ShowLastMod: true

在每篇文章开头记得加上“date”、“lastmod”、“author”这三个字段,已经集成在 archetypes/default.md 里面了,这样每次创建文章就会自动生成,生成文章的命令:

hugo new --kind default content/posts/xx.md

标签分类和文章页面中文化

要将标签和分类等页面改为中文需要在对应文件夹下添加 _index.md 文件。

content\categories\_index.md

---
title: "分类"
---

content\tags\_index.md

---
title: "标签"
---

content\posts_index.md

---
title: "文章"
---

这样修改语言的工作就完成了。