Eureka 切换到 PaperMod 主题已经快半年时间了,经过一番折腾和优化,基本达到了我想要的效果。本文记录了我对这个主题的各种改造,如果你也在使用 PaperMod,下面的内容可能对你有帮助。

环境信息:

  • Hugo 版本: v0.147.2+extended
  • PaperMod 版本: v8.0

优化主页个人信息展示

对 PaperMod 的 Home-Info 布局做了优化,增加了头像展示和图标悬浮高亮效果,支持响应式布局。

Home-Info

  1. 创建 layouts/partials/home_info.html 文件:
点击展开完整代码
{{- with site.Params.homeInfoParams }}
<article class="first-entry home-info">
    <div class="home-info-container home-info-main-container">
        <div class="home-info-content-wrapper">
            {{- with site.Params.homeInfoParams }}
            <div class="home-info-avatar home-info-avatar-container">
                {{- if .ImageUrl -}}
                {{- $imgSrc := .ImageUrl | absURL }}
                {{- $img := resources.Get .ImageUrl }}
                {{- if $img }}
                {{- $size := printf "%dx%d" (.ImageWidth | default 100) (.ImageHeight | default 100) }}
                {{- $img = $img.Resize $size }}
                {{- $imgSrc = $img.Permalink }}
                {{- end }}
                <img id="home-info-avatar" 
                     draggable="false" 
                     src="{{ $imgSrc }}" 
                     alt="{{ .Title | default "profile image" }}" 
                     height="{{ .ImageHeight | default 100 }}" 
                     width="{{ .ImageWidth | default 100 }}" 
                     class="home-info-avatar-img" />
                {{- end }}
            </div>
            {{- end }}
            <div class="entry-main home-info-text-content">
                <header class="entry-header">
                    <h1>{{ .Title | markdownify }}</h1>
                </header>
                <div class="entry-content">
                    {{ .Content | markdownify }}
                </div>
            </div>
        </div>
        <footer class="entry-footer">
            {{ partial "social_icons.html" (dict "align" site.Params.homeInfoParams.AlignSocialIconsTo) }}
        </footer>
    </div>
</article>
{{- end -}}
  1. assets/css/extended/blank.css 文件中添加样式:
点击展开完整代码
/* Home Info Layout Styles */
.home-info-main-container {
    display: flex;
    flex-direction: column;
    gap: 24px;
    max-width: 100%;
}

.home-info-content-wrapper {
    display: flex;
    align-items: center;
    gap: 32px;
}

.home-info-avatar-container {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    position: relative;
}

.home-info-avatar-container::after {
    content: '';
    position: absolute;
    right: -16px;
    top: 50%;
    transform: translateY(-50%);
    width: 1px;
    height: 60px;
    background-color: #e5e5e5;
}

.home-info-text-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-top: 8px;
}

.home-info-avatar-img {
    border-radius: 50% !important;
    border: 2px solid #f0f0f0;
    transition: transform 0.2s ease;
}

.home-info-avatar-img:hover {
    transform: scale(1.02);
}

/* 响应式设计 */
@media (max-width: 768px) {
    .home-info-content-wrapper {
        flex-direction: column;
        gap: 20px;
        text-align: center;
    }
    
    .home-info-text-content {
        margin-top: 0;
    }
    
    /* 移动端隐藏分隔线 */
    .home-info-avatar-container::after {
        display: none;
    }
    
    /* 移动端社交图标居中 */
    .home-info .entry-footer {
        display: flex;
        justify-content: center;
        align-items: center;
    }
}

/* 图标悬浮高亮 */
.social-icons svg:hover {
    transition: 0.15s;
}

.social-icons a[href*='mailto']:hover svg {
    color: #ea4335 !important;
}

.social-icons a[href*='github']:hover svg {
    color: #7c3aed !important;
}

.social-icons a[href*='index.xml']:hover svg {
    color: #ff6600 !important;
}
  1. config.yaml 中配置头像地址(支持本地或远程图片):
params:
  homeInfoParams:
    Title: "她和她的猫"
    ImageUrl: /images/avatar.jpeg
    Content: 那一天,我被她抱回了家。从此以后,我成了她的猫。

移除主页冗余分页

在 PaperMod 主题中,默认会为主页的文章列表生成分页(/,/page/2/…),这导致主页和文章列表页面(/posts/,/posts/page/2/…)内容重复,产生了大量冗余页面。

为了解决这个问题,我重构了主页布局,让主页只展示最新的几篇文章,不再生成分页。用户可以通过「查看更多」按钮跳转到文章列表页面。

优化前后对比

Paginator pages 从 62 降到 43,减少了 19 个冗余页面

创建 layouts/index.html 文件,覆盖主题的首页模板。

点击展开完整代码
{{- define "main" }}

{{- if site.Params.profileMode.enabled }}
{{- partial "index_profile.html" . }}
{{- else }} {{/* if not profileMode */}}

{{- if .Content }}
<div class="post-content">
  {{- if not (.Param "disableAnchoredHeadings") }}
  {{- partial "anchored_headings.html" .Content -}}
  {{- else }}{{ .Content }}{{ end }}
</div>
{{- end }}

{{- $pages := where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- $pages = where $pages "Params.hiddenInHomeList" "!=" "true"  }}

{{- if site.Params.homeInfoParams }}
{{- partial "home_info.html" . }}
{{- end }}

{{- $displayPages := first 3 $pages }}
{{- range $index, $page := $displayPages }}

{{- $class := "post-entry" }}

{{- $user_preferred := or site.Params.disableSpecial1stPost site.Params.homeInfoParams }}
{{- if (and (eq $index 0) (not $user_preferred)) }}
{{- $class = "first-entry" }}
{{- end }}

<article class="{{ $class }}">
  {{- $isHidden := (.Param "cover.hiddenInList") | default (.Param "cover.hidden") | default false }}
  {{- partial "cover.html" (dict "cxt" . "IsSingle" false "isHidden" $isHidden) }}
  <header class="entry-header">
    <h2 class="entry-hint-parent">
      {{- .Title }}
      {{- if .Draft }}
      <span class="entry-hint" title="Draft">
        <svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 -960 960 960" fill="currentColor">
          <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" />
        </svg>
      </span>
      {{- end }}
    </h2>
  </header>
  {{- if (ne (.Param "hideSummary") true) }}
  <div class="entry-content">
    <p>{{ .Summary | plainify | htmlUnescape }}{{ if .Truncated }}...{{ end }}</p>
  </div>
  {{- end }}
  {{- if not (.Param "hideMeta") }}
  <footer class="entry-footer">
    {{- partial "post_meta.html" . -}}
  </footer>
  {{- end }}
  <a class="entry-link" aria-label="post link to {{ .Title | plainify }}" href="{{ .Permalink }}"></a>
</article>
{{- end }}

{{- if gt (len $pages) 3 }}
<footer class="page-footer">
  <nav class="pagination">
    <a class="next" href="/posts/">
      查看更多&nbsp;»
    </a>
  </nav>
</footer>
{{- end }}

{{- end }}{{/* end profileMode */}}

{{- end }}{{- /* end main */ -}} 

解决中文字数统计问题

Hugo 默认的字数统计对中日韩(CJK)文字不准确,需要在 config.yaml 中开启 hasCJKLanguage 选项:

hasCJKLanguage: true

解决图片加载抖动(CLS)问题

使用 PageSpeed Insights 检测博客时,发现 CLS(Cumulative Layout Shift,累积布局偏移)分数偏高,页面加载时图片会造成明显的抖动现象。

CLS 是 Google 评估网站用户体验的重要指标之一,分数过高通常是因为图片加载时浏览器不知道应该预留多大的空间,等图片加载完成后就会把下面的内容挤下去,导致页面跳动。

解决办法就是为图片添加正确的宽高属性,让浏览器在加载前预留空间。

创建 layouts/_default/_markup/render-image.html 文件:

点击展开完整代码
{{- $u := urls.Parse .Destination -}}
{{- $src := $u.String -}}
{{- $img := "" -}}
{{- $width := "" -}}
{{- $height := "" -}}
{{- $aspectRatio := "" -}}

{{- if not $u.IsAbs -}}
  {{- $path := strings.TrimPrefix "./" $u.Path -}}
  
  {{- /* 查找图片:优先页面资源,其次 assets 目录 */ -}}
  {{- $img = or (.PageInner.Resources.Get $path) (resources.Get (strings.TrimPrefix "/" $path)) -}}
  
  {{- if $img -}}
    {{- /* 获取图片基本信息 */ -}}
    {{- $src = $img.RelPermalink -}}
    
    {{- /* 只对栅格图片获取宽高,SVG 跳过 */ -}}
    {{- if ne $img.MediaType.SubType "svg" -}}
      {{- /* 确保宽高有效(大于 0) */ -}}
      {{- if and (gt $img.Width 0) (gt $img.Height 0) -}}
        {{- $width = printf "%d" $img.Width -}}
        {{- $height = printf "%d" $img.Height -}}
        {{- $aspectRatio = printf "%.4f" (div (float $img.Width) (float $img.Height)) -}}
      {{- end -}}
    {{- end -}}
    
    {{- /* 保留原始 URL 的 query 和 fragment */ -}}
    {{- with $u.RawQuery -}}
      {{- $src = printf "%s?%s" $src . -}}
    {{- end -}}
    {{- with $u.Fragment -}}
      {{- $src = printf "%s#%s" $src . -}}
    {{- end -}}
  {{- else -}}
    {{- /* 如果找不到,保持原始路径(static 目录) */ -}}
    {{- $src = $u.String -}}
  {{- end -}}
{{- end -}}

{{- /* 设置基础属性 */ -}}
{{- $attributes := dict "alt" .Text "src" $src "loading" "lazy" "decoding" "async" -}}

{{- /* 添加 title 属性(如果存在) */ -}}
{{- with .Title -}}
  {{- $attributes = merge $attributes (dict "title" (. | transform.HTMLEscape)) -}}
{{- end -}}

{{- /* 如果获取到了尺寸信息,设置宽高和宽高比 */ -}}
{{- if and $width $height -}}
  {{- $attributes = merge $attributes (dict "width" $width "height" $height) -}}
  {{- $style := printf "max-width: 100%%; height: auto; aspect-ratio: %s;" $aspectRatio -}}
  {{- $attributes = merge $attributes (dict "style" $style) -}}
{{- else -}}
  {{- /* 如果没有尺寸信息,至少保持响应式 */ -}}
  {{- $attributes = merge $attributes (dict "style" "max-width: 100%; height: auto;") -}}
{{- end -}}

{{- /* 合并用户自定义属性 */ -}}
{{- $attributes = merge .Attributes $attributes -}}

{{- if .Title -}}
<figure>
  <img
    {{- range $k, $v := $attributes -}}
      {{- if $v -}}
        {{- printf " %s=%q" $k $v | safeHTMLAttr -}}
      {{- end -}}
    {{- end -}}>
  <figcaption><p>{{ .Title | markdownify }}</p></figcaption>
</figure>
{{- else -}}
<img
  {{- range $k, $v := $attributes -}}
    {{- if $v -}}
      {{- printf " %s=%q" $k $v | safeHTMLAttr -}}
    {{- end -}}
  {{- end -}}>
{{- end -}}

💡 提示:图片需要放在文章同目录(Page Bundle)或 assets 目录下,否则无法自动获取尺寸。

优化阅读体验

对字体、排版、间距等进行了优化,主要借鉴了 DvelatpX 的博客。

创建 assets/css/extended/reading.css 文件:

点击展开完整代码
/* === 1. CSS 变量定义 === */
:root {
    /* 颜色 */
    --primary: #1a1b1c;
    --content: #333435;
    --secondary: #666;
    --sec-color: #f2f3f4;
    --link-color: #2d8cdc;
    --code-bg: #f5f5f5;
    --sec-note-color: #6e6e6e;
    
    /* 字体 */
    --font-fallback: -apple-system, BlinkMacSystemFont, system-ui, sans-serif, 'Color Emoji';
    --font-family: 'Inter', var(--font-fallback);
    --code-font-family: 'Fira Code', Menlo, 'Lucida Console', 'DejaVu Sans Mono', var(--font-fallback);
}

/* 暗色模式 */
.dark {
    --primary: #f2f2f2;
    --content: #e3e3e3;
    --sec-color: #2A2C2B;
    --sec-note-color: #808080;
}

/* === 2. 全局字体设置 === */
body {
    font-family: var(--font-family);
    font-size: 18px;
    margin: 0;
}

/* 标题字重 */
h1, h2, h3, h4, h5, h6 {
    font-weight: 700;
}

/* 代码字体 */
.post-content code, 
.post-content code span {
    font-family: var(--code-font-family);
}

/* === 3. 文章标题样式 === */
.post-title {
    font-size: 34px;
}

.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4,
.post-content h5,
.post-content h6 {
    margin-bottom: 18px;
    font-weight: 600;
}

.post-content h1 {
    margin-top: 48px;
    padding-bottom: 13px;
    border-bottom: 1px solid var(--sec-color);
}

.post-content h2 {
    font-size: 24px;
    margin-top: 48px;
    padding-bottom: 13px;
    border-bottom: 1px solid var(--sec-color);
}

.post-content h3 {
    font-size: 22px;
    margin-top: 32px;
}

.post-content h4 {
    font-size: 20px;
    margin-top: 23px;
}

.post-content h5 {
    font-size: 16px;
    margin-top: 18px;
}

.post-content h6 {
    font-size: 14px;
    margin-top: 16px;
}

/* === 4. 正文样式 === */
.post-content {
    line-height: 1.86;
}

.post-content p,
.post-content blockquote,
.post-content figure,
.post-content table {
    margin: 18px 0;
}

.post-content blockquote {
    color: var(--sec-note-color);
}

.post-content hr {
    margin: 64px 128px;
}

.post-content ul,
.post-content ol,
.post-content dl,
.post-content li {
    margin: 8px 0;
}

/* === 5. 链接样式 === */
.post-content a {
    color: var(--link-color);
    box-shadow: none;
    text-decoration: none;
}

.post-content a:hover {
    text-decoration: underline;
}

/* === 6. 行内代码样式 === */
.post-content code {
    margin: unset;
    padding: 5px 7px;
    border-radius: 8px;
}

/* === 6.5. 折叠块样式 === */
.post-content details summary {
    cursor: zoom-in;
    user-select: none;
}

.post-content details[open] summary {
    cursor: zoom-out;
}

/* === 7. 图片样式 === */
.post-content img {
    margin: auto;
    max-width: 100%;
    height: auto;
    transition: opacity 0.3s ease;
}

.post-content figure {
    margin: 27px 0;
    text-align: center;
}

.post-content figure img {
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.post-content figure img:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}

.post-content figcaption {
    margin-top: 9px;
    font-size: 16px;
    color: var(--secondary);
    font-style: italic;
}

/* === 8. 移动端响应式优化 === */
@media (max-width: 768px) {
    .post-content img {
        border-radius: 4px;
    }
    
    .post-content figure img:hover {
        transform: none;
    }
}

/* === 9. 防止滚动条导致页面抖动 === */
html {
    overflow-y: scroll;
}

:root {
    overflow-y: auto;
    overflow-x: hidden;
}

:root body {
    position: absolute;
    width: 100vw;
    overflow: hidden;
}

显示文章所属的分类和系列

为了方便快速查看相关内容,我在文章标题下方的元数据区域增加了分类和系列的链接。

  1. 创建 layouts/partials/post_meta.html 文件:
点击展开完整代码
{{- $scratch := newScratch }}

{{- if not .Date.IsZero -}}
{{- $scratch.Add "meta" (slice (printf "<span title='%s'>%s</span>" (.Date) (.Date | time.Format (default "January 2, 2006" site.Params.DateFormat)))) }}
{{- end }}

{{- if (.Param "ShowReadingTime") -}}
{{- $scratch.Add "meta" (slice (i18n "read_time" .ReadingTime | default (printf "%d min" .ReadingTime))) }}
{{- end }}

{{- if (.Param "ShowWordCount") -}}
{{- $scratch.Add "meta" (slice (i18n "words" .WordCount | default (printf "%d words" .WordCount))) }}
{{- end }}

{{- if not (.Param "hideAuthor") -}}
{{- with (partial "author.html" .) }}
{{- $scratch.Add "meta" (slice .) }}
{{- end }}
{{- end }}

{{- $categories := .Language.Params.Taxonomies.category | default "categories" }}
{{- with ($.GetTerms $categories) }}
{{- $categoryLinks := slice }}
{{- range . }}
{{- $categoryLinks = $categoryLinks | append (printf "<a href=\"%s\">%s</a>" .Permalink .LinkTitle) }}
{{- end }}
{{- $categoryString := delimit $categoryLinks "&nbsp;" | safeHTML }}
{{- $scratch.Add "meta" (slice (string $categoryString)) }}
{{- end }}

{{- $series := .Language.Params.Taxonomies.series | default "series" }}
{{- with ($.GetTerms $series) }}
{{- $seriesLinks := slice }}
{{- range . }}
{{- $seriesLinks = $seriesLinks | append (printf "<a href=\"%s\">%s</a>" .Permalink .LinkTitle) }}
{{- end }}
{{- $seriesString := delimit $seriesLinks "&nbsp;" | safeHTML }}
{{- $scratch.Add "meta" (slice (string $seriesString)) }}
{{- end }}

{{- with ($scratch.Get "meta") }}
{{- delimit . "&nbsp;·&nbsp;" | safeHTML -}}
{{- end -}}
  1. assets/css/extended/blank.css 中添加相关样式:
.post-meta a,
.archive-meta a,
.entry-footer a {
    color: var(--secondary) !important;
    text-decoration: none;
    transition: color 0.2s ease;
}

.post-meta a:hover,
.archive-meta a:hover,
.entry-footer a:hover {
    color: var(--primary);
    text-decoration: underline;
}

使用 Waline 评论系统

Waline 提供了多种部署方式,在体验了 LeanCloud 和 Vercel 这两种无服务部署方式后,发现速度太慢,最后用 Docker 自建了服务。

创建 layouts/partials/comments.html 文件:

<link rel="stylesheet" href="https://unpkg.com/@waline/client@v3/dist/waline.css" />

<div id="waline"></div>
<script type="module">
  import { init } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';

  setTimeout(() => {
    init({
      el: '#waline',
      serverURL: 'https://你的 Waline 服务端地址',
      reaction: true,
      imageUploader: false,
      search: false,
      lang: 'zh-CN',
      dark: 'body[class="dark"]',  // 适配 PaperMod 暗黑模式
      emoji: [
        'https://unpkg.com/@waline/emojis@1.2.0/alus',
        '/images/waline/emoji/huaji',
      ]
    });
  }, 1000);
</script>

配置说明:

  • dark 字段用于适配 PaperMod 的暗黑模式
  • emoji 可以使用 Waline 官方表情包,也支持自定义表情包
  • 其它字段根据官方文档和个人喜好调整

如果你也想使用滑稽表情包,可以参考 qwqcode/huaji 和我的 info.json