从 Eureka 切换到 PaperMod 主题已经快半年时间了,经过一番折腾和优化,基本达到了我想要的效果。本文记录了我对这个主题的各种改造,如果你也在使用 PaperMod,下面的内容可能对你有帮助。
环境信息:
- Hugo 版本: v0.147.2+extended
- PaperMod 版本: v8.0
优化主页个人信息展示
对 PaperMod 的 Home-Info 布局做了优化,增加了头像展示和图标悬浮高亮效果,支持响应式布局。
- 创建
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 -}}
- 在
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;
}
- 在
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/">
查看更多 »
</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
目录下,否则无法自动获取尺寸。
优化阅读体验
对字体、排版、间距等进行了优化,主要借鉴了 Dvel 和 atpX 的博客。
创建 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;
}
显示文章所属的分类和系列
为了方便快速查看相关内容,我在文章标题下方的元数据区域增加了分类和系列的链接。
- 创建
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 " " | 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 " " | safeHTML }}
{{- $scratch.Add "meta" (slice (string $seriesString)) }}
{{- end }}
{{- with ($scratch.Get "meta") }}
{{- delimit . " · " | safeHTML -}}
{{- end -}}
- 在
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。