[{"content":"博客用的是 Hugo 的 PaperMod 主题，默认的导航栏在手机上是横向滚动的：菜单项一多就溢出屏幕，想看后面的得用手指拖着划，很难受。我希望把它改成常见的「点一下汉堡按钮弹出菜单」的形式，顺手还要让导航栏吸顶、点空白处能收回。这篇记录一下完整的改法。\nPaperMod 默认的导航结构 先看主题的头部模板 themes/PaperMod/layouts/_partials/header.html，结构大概是：\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;header class=\u0026#34;header\u0026#34;\u0026gt; \u0026lt;nav class=\u0026#34;header-nav\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;logo\u0026#34;\u0026gt; \u0026lt;!-- 站点名 --\u0026gt; \u0026lt;div class=\u0026#34;logo-switches\u0026#34;\u0026gt; \u0026lt;!-- 主题切换按钮、语言切换 --\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;ul id=\u0026#34;menu\u0026#34; class=\u0026#34;menu\u0026#34;\u0026gt; \u0026lt;!-- 菜单项：range site.Menus.main --\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;/header\u0026gt; 窄屏时 PaperMod 的处理方式是让 .menu 内部横向滚动：\n1 2 3 @media (max-width: 900px) { .menu { overflow-x: auto; white-space: nowrap; } } 这就是「要拖动」的根源。PaperMod 没有内置汉堡菜单。\n改法：覆盖头部模板，加一个 checkbox 不想直接改主题文件（升级主题会被覆盖），所以把主题的 _partials/header.html 复制到站点的 layouts/partials/header.html 同名覆盖（Hugo 站点目录优先于主题目录），然后在原结构上动三处：\n顺带弄清了一个约定：主题把自带的 header/footer 等模板放在 _partials/（下划线前缀表示\u0026quot;主题内部\u0026quot;），站点通常用标准目录 partials/。Hugo 会把两边的 partials/ 和 _partials/ 合并进同一个 partial 命名空间（调用时都不带目录前缀，直接 partial \u0026quot;header.html\u0026quot;），项目优先于主题——所以放在站点 partials/ 下的同名文件照样能盖掉主题 _partials/ 里的。只要站点内部两个目录别放同名文件就行。\n在 .logo 之后插一个隐藏的 \u0026lt;input type=\u0026quot;checkbox\u0026quot; id=\u0026quot;menu-toggle\u0026quot;\u0026gt;； 在 \u0026lt;ul class=\u0026quot;menu\u0026quot;\u0026gt; 后面加一个 \u0026lt;label for=\u0026quot;menu-toggle\u0026quot; class=\u0026quot;menu-toggle\u0026quot;\u0026gt;（画三条横线）； 文件末尾加几行 JS，实现「点空白处收回」。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;header class=\u0026#34;header\u0026#34;\u0026gt; \u0026lt;nav class=\u0026#34;header-nav\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;logo\u0026#34;\u0026gt;…标题 + 主题切换…\u0026lt;/div\u0026gt; \u0026lt;!-- ① 隐藏的开关：必须在 .menu 之前、与它同父级 --\u0026gt; \u0026lt;input type=\u0026#34;checkbox\u0026#34; id=\u0026#34;menu-toggle\u0026#34; class=\u0026#34;menu-toggle-input\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;ul id=\u0026#34;menu\u0026#34; class=\u0026#34;menu\u0026#34;\u0026gt;…菜单项…\u0026lt;/ul\u0026gt; \u0026lt;!-- ② 汉堡按钮放最后：flex 布局自动推到最右端 --\u0026gt; \u0026lt;label for=\u0026#34;menu-toggle\u0026#34; class=\u0026#34;menu-toggle\u0026#34; aria-label=\u0026#34;切换菜单\u0026#34; title=\u0026#34;菜单\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;menu-toggle-bar\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;menu-toggle-bar\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;menu-toggle-bar\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;/label\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;/header\u0026gt; 这里有个很关键的坑：checkbox 必须和 .menu 是同一个父元素下的兄弟，CSS 才能用通用兄弟选择器 :checked ~ .menu 选中菜单。我第一次把它随手塞进 .logo-switches 里，展开规则怎么都不生效，折腾半天才发现是结构位置不对。\n汉堡按钮的摆放也有讲究：放 .logo 里会挤在标题旁边，想要贴最右，就得放到菜单后面，space-between 布局会把它自动推到右端。\nCSS：900px 以下折叠，点按钮展开 汉堡按钮、三横线的基础样式，以及折叠/展开规则都写在站点的 assets/css/extended/custom.css 里。extended 目录下的 CSS 会被 Hugo 自动合并到主题样式之后，天然拥有覆盖主题规则的优先级：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 /* 隐藏的 checkbox：看不见，但状态可被 label 切换 */ .menu-toggle-input { position: absolute; opacity: 0; pointer-events: none; } /* 汉堡按钮：宽屏默认不显示 */ .menu-toggle { display: none; flex-direction: column; justify-content: center; align-items: center; gap: 4px; width: 28px; height: 28px; padding: 5px; cursor: pointer; border: 1px solid var(--border); border-radius: 6px; } .menu-toggle .menu-toggle-bar { display: block; width: 100%; height: 2px; border-radius: 1px; background: var(--primary); } @media screen and (max-width: 900px) { /* 窄屏才显示汉堡按钮 */ .menu-toggle { display: inline-flex; align-self: center; } /* 菜单默认收起；展开时作为悬浮面板，不撑高导航栏 */ .header-nav .menu { display: none; position: absolute; top: calc(100% + 6px); right: var(--gap); width: max-content; min-width: 120px; max-width: calc(100% - var(--gap) * 2); z-index: 110; flex-direction: column; align-items: stretch; padding: calc(var(--gap) * 0.5) calc(var(--gap) * 0.8); background: var(--entry); border: 1px solid var(--border); border-radius: calc(var(--radius) - 2px); box-shadow: 0 10px 28px rgba(60, 55, 45, 0.18); } .header-nav .menu li { width: 100%; } /* 菜单项之间用虚线分隔，比实线轻 */ .header-nav .menu li + li { border-top: 1px dashed var(--border); } .header-nav .menu li a { display: block; width: 100%; padding: calc(var(--gap) * 0.32) 0; } /* 勾选（点汉堡）后展开 */ .menu-toggle-input:checked ~ .menu { display: flex; } } li + li 是「给除了第一个以外的每个菜单项顶上画条虚线」，比给每个 li 都画、再单独去掉第一个的写法干净。\n为什么不用 JS 做展开？ 常见的做法是 JS 切换 class。但这里其实只需要一个隐藏 checkbox 就能完成「点一下开、再点一下关」：label 点击会原生切换 checkbox 的 checked 状态，:checked 时 CSS 展开菜单，完全不依赖 JS，也没有多余的 class 管理。这个「checkbox hack」在移动端菜单、折叠面板里都很好用。\n它做不到的只有一件事：点击菜单以外的空白处自动收回——checkbox 只能被自己的 label 切换，管不到页面其它位置的点击。这个需求就交给 JS 兜底，写在 header 模板末尾：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;script\u0026gt; (function () { const input = document.getElementById(\u0026#39;menu-toggle\u0026#39;); if (!input) return; // 菜单已展开时，点击导航栏以外的任意位置即收回 document.addEventListener(\u0026#39;click\u0026#39;, function (e) { if (input.checked \u0026amp;\u0026amp; !e.target.closest(\u0026#39;.header\u0026#39;)) { input.checked = false; } }); })(); \u0026lt;/script\u0026gt; e.target.closest('.header') 用来排除导航栏内部：如果点的是汉堡按钮（label），此时 checkbox 刚被切换，不该再被这个监听器改回去；点悬浮面板里的链接则直接跳转，页面刷新后状态自然复位。只监听「点在 .header 之外」的情况，就不会和按钮行为打架。\n面板外观：几轮微调 菜单展开的形态也调了几版，值得记一下过程：\n最初是文档流展开：菜单显示在导航栏下方第二行，会把导航栏撑高，视觉上是「导航栏长高了一块」，还和吸顶效果打架； 改成绝对定位悬浮：position: absolute 脱离文档流，面板悬在导航栏正下方 6px 处，盖在正文上方，导航栏高度稳定不跳动。配合 z-index: 110 保证盖住正文； 宽度贴内容：一开始面板左右拉满屏幕（left: 0; right: 0），手机上显得空旷；改固定 280px 又会在菜单项少时留白。最终用 width: max-content 让宽度刚好包住最长的菜单项，min-width: 120px 保证点击区域别太窄，max-width 兜底超窄屏不溢出； 位置靠右：面板 right: var(--gap)，右缘和汉堡按钮对齐，从按钮正下方落下来，符合手指操作习惯。 配色上直接复用主题变量：background: var(--entry) 是比页面底色略亮的卡片色（我博客的纸张色体系里 entry 就是浅一档的纸色），--border 画边框，阴影用了偏暖的半透明黑 rgba(60, 55, 45, 0.18)，浅色深色主题下都协调，无需为暗色单独写。\n相关的小改动 这次顺带做了几处配套调整，一并记录：\n导航栏吸顶：默认导航栏滚出屏幕就没了。给 .header 加 position: sticky; top: 0; z-index: 100; background: var(--theme)。注意要给背景色，否则正文滚到导航栏下方会透出来； 文章页侧栏避让：文章页右侧的目录/标签栏原本 sticky top: var(--gap)，导航栏吸顶后会被挡住，改成 top: calc(var(--header-height) + var(--gap))，正好落在导航栏之下； 移动端侧边卡片：文章页的分类/标签/系列卡片在窄屏原本 display: none，我把 HTML 移到正文之后、CSS 默认改为文档流展示（只有 .wide 时才绝对定位到右侧），手机端文章读完就能在底部看到这几张卡片。 涉及文件 文件 作用 layouts/partials/header.html 覆盖主题头部模板，加 checkbox、汉堡按钮、关闭脚本 assets/css/extended/custom.css 汉堡按钮、折叠/悬浮面板、导航栏吸顶样式 断点选在 900px，和 PaperMod 自带的窄屏断点一致，超过 900px 一切恢复桌面横排。就这样，手机上终于不用再拖着导航栏滑了。\n","permalink":"https://www.wkun.top/posts/hugo-papermod-%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%AF%BC%E8%88%AA%E6%A0%8F%E6%B1%89%E5%A0%A1%E8%8F%9C%E5%8D%95/","summary":"\u003cp\u003e博客用的是 Hugo 的 PaperMod 主题，默认的导航栏在手机上是\u003cstrong\u003e横向滚动\u003c/strong\u003e的：菜单项一多就溢出屏幕，想看后面的得用手指拖着划，很难受。我希望把它改成常见的「点一下汉堡按钮弹出菜单」的形式，顺手还要让导航栏吸顶、点空白处能收回。这篇记录一下完整的改法。\u003c/p\u003e","title":"Hugo PaperMod 移动端导航栏改造：汉堡菜单与悬浮面板"},{"content":"PaperMod 主题自带的 404 页面只有一句纯文字，很简陋；右侧侧边栏的「系列」卡片此前也只放了系列名链接，不显示系列里的其他文章。这次给博客补上自定义 404 页面，并把系列内导航做进右侧系列卡片里。本文记录实现过程。\n需求 自定义 404 页面：访客访问不存在的 URL 时，展示友好的页面（返回首页、搜索入口、推荐最近文章），而不是主题默认的一行字； 系列内导航：在右侧「系列」卡片里列出该系列全部文章，当前篇高亮、标注位置（第 x 篇 / 共 y 篇），方便读者顺着系列读下去。 一、自定义 404 页面 PaperMod 的默认 404 就是一个居中的文字页。要自定义，直接在站点根目录新建 layouts/404.html 覆盖主题模板即可（主题用的是 layouts/404.html 这个路径）。\n我的实现：\n大号艺术字 404，用 CSS 变量 var(--primary) / var(--secondary) 配色，自动适配明暗主题； 两个按钮：返回首页、搜索文章； 底部列最近 5 篇文章，访客不至于直接流失。 模板里「最近文章」用 Hugo 内置的 site.RegularPages 拿：\n1 2 3 {{- range first 5 (where site.RegularPages \u0026#34;Section\u0026#34; \u0026#34;posts\u0026#34;) }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34;\u0026gt;{{ .Title }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} 踩坑：光有 404.html 不够，还要服务器配合 Hugo 构建会生成静态的 public/404.html，但服务器不会自动把它当作错误页返回。\nNginx 需要在站点配置里加 error_page 404 /404.html;； Caddy 需要在 Caddyfile 里加： 1 2 3 4 handle_errors { rewrite * /404.html file_server } Caddy 和 Nginx 不一样，它不会自动找 404.html，必须显式用 handle_errors 处理。改完 systemctl reload caddy，再用 curl -sI https://你的域名/不存在的路径 验证是否返回 404 状态码。\n二、系列内导航（右侧系列卡片） 「系列」卡片之前只放了系列名链接。现在把它升级成完整的系列导航：列出该系列全部文章，当前篇蓝色高亮、标\u0026quot;当前\u0026quot;，并显示位置（第 x 篇 / 共 y 篇）。\n模板逻辑放在 layouts/single.html 的系列卡片里（卡片在侧边栏最底部，位于分类、标签之后）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 {{- if .Params.series }} \u0026lt;div class=\u0026#34;tags-aside-box series-aside-box\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;tags-aside-title\u0026#34;\u0026gt;系列\u0026lt;/div\u0026gt; {{- $current := . }} {{- range $term := ($.GetTerms $series) }} {{- $posts := $term.Pages.ByDate }} {{- $total := len $posts }} {{- $curIdx := 0 }} {{- range $i, $p := $posts }} {{- if eq $p.Permalink $current.Permalink }} {{- $curIdx = add $i 1 }} {{- end }} {{- end }} \u0026lt;div class=\u0026#34;series-nav\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;series-nav-head\u0026#34;\u0026gt; \u0026lt;a class=\u0026#34;series-nav-title\u0026#34; href=\u0026#34;{{ $term.Permalink }}\u0026#34;\u0026gt;{{ $term.LinkTitle }}\u0026lt;/a\u0026gt; \u0026lt;span class=\u0026#34;series-nav-count\u0026#34;\u0026gt;{{ $curIdx }}/{{ $total }}\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;ol class=\u0026#34;series-nav-list\u0026#34;\u0026gt; {{- range $i, $p := $posts }} \u0026lt;li{{ if eq $p.Permalink $current.Permalink }} class=\u0026#34;active\u0026#34;{{ end }}\u0026gt; \u0026lt;a href=\u0026#34;{{ $p.Permalink }}\u0026#34; title=\u0026#34;{{ $p.Title }}\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;series-nav-idx\u0026#34;\u0026gt;{{ add $i 1 }}\u0026lt;/span\u0026gt;{{ $p.Title }} {{- if eq $p.Permalink $current.Permalink }}\u0026lt;span class=\u0026#34;series-nav-cur\u0026#34;\u0026gt;当前\u0026lt;/span\u0026gt;{{- end }} \u0026lt;/a\u0026gt; \u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ol\u0026gt; \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/div\u0026gt; {{- end }} 几个要点：\n.Pages.ByDate 按发布时间升序拿系列文章，保证列表顺序和系列阅读顺序一致； 当前位置：遍历比对 Permalink，算出当前篇是第几篇（curIdx）和总数（total），显示成 3/5； 一篇文章属于多个系列时，range $term 会为每个系列渲染一个导航块，块之间用虚线分隔； 当前篇 \u0026lt;li\u0026gt; 加 class=\u0026quot;active\u0026quot;，用 CSS 高亮； 最初还加了\u0026quot;上一篇 / 下一篇\u0026quot;快捷跳转，后来发现列表本身就能跳转、用不上，删掉了。 样式放在 assets/css/extended/series.css，PaperMod 会自动加载 assets/css/extended/ 下的所有 CSS。核心几点：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* 系列卡片：高度按视口比例（45vh），头部固定、列表内部滚动 */ .series-aside-box { height: 45vh; display: flex; flex-direction: column; overflow: hidden; } .series-nav-list { list-style: none; margin: 0; padding: 0; border-top: 1px dashed var(--border); overflow-y: auto; flex: 1 1 auto; min-height: 0; } .series-nav-list li.active a { color: var(--accent-blue); font-weight: 600; } 用 45vh（视口高度的 45%）定死卡片高度：矮屏自动收缩、高屏拉伸，卡片大小不再跟随文章数量跳动，侧边栏整体稳定。头部（系列名 + 位置）始终固定，列表超出部分内部滚动。\n三、顺带：高亮统一成蓝色 这次一起把两处高亮改成了同一套蓝色，并定义了 CSS 变量，方便以后统一换色：\n1 2 3 4 5 6 :root { --accent-blue: #1a73e8; /* 亮色模式 */ } [data-theme=\u0026#34;dark\u0026#34;] { --accent-blue: #5b9df5; /* 暗色模式，暗底上更清晰 */ } 目录（TOC）当前章节高亮：.toc .active { color: var(--accent-blue); }，保留原有放大加粗效果； 系列导航当前篇：.series-nav-list li.active a { color: var(--accent-blue); }； 系列导航非当前篇：文字用主题主色 --primary（黑字）。主题默认的 --secondary 灰字在侧边栏里对比度偏低，看着费劲。 配色原则最后收敛成一句话：黑色正文 + 蓝色强调。全站四处强调色统一走 --accent-blue：目录当前章节、系列当前篇、胶囊悬停、链接悬停。\n四、侧边栏卡片布局微调 系列导航落地后，对右侧三张卡片（分类 / 标签 / 系列）做了一轮统一：\n顺序：分类 → 标签 → 系列（系列放最底部）； 分类/标签：维持胶囊外观，但文字从主题默认的灰字改成黑字，悬停从\u0026quot;背景加深\u0026quot;改为\u0026quot;文字变蓝\u0026quot;，和系列导航的 hover 反馈一致（期间试过把胶囊改成纯文字列表，和系列完全同构，但视觉效果不佳，改回了胶囊）； 系列卡片：height: 45vh 定死高度 + 列表内部滚动（见上文 CSS），头部固定、系列名和位置始终可见； 删除：系列卡片最初做的\u0026quot;上一篇 / 下一篇\u0026quot;快捷跳转删掉了——列表本身就能跳转，这个功能冗余。 小结 改动都不复杂，思路都是「覆盖主题模板 / 追加扩展样式」：\n404 页面 = 新建 layouts/404.html 覆盖主题默认，配好服务器错误页配置； 系列导航 = 纯模板逻辑 + assets/css/extended/ 下的扩展样式，不动主题源文件； 颜色统一用 CSS 变量管理，明暗主题各配一个值； 布局上，侧边栏卡片顺序、高度、滚动行为统一管理，避免各卡片风格割裂。 记录两个容易漏的点：一是 Caddy 不像 Nginx 会自动识别 404.html，必须加 handle_errors；二是系列列表用 45vh 定死卡片高度 + 内部滚动，避免长系列撑爆 sticky 侧边栏。\n","permalink":"https://www.wkun.top/posts/hugo-papermod-%E8%87%AA%E5%AE%9A%E4%B9%89404%E4%B8%8E%E7%B3%BB%E5%88%97%E5%AF%BC%E8%88%AA/","summary":"\u003cp\u003ePaperMod 主题自带的 404 页面只有一句纯文字，很简陋；右侧侧边栏的「系列」卡片此前也只放了系列名链接，不显示系列里的其他文章。这次给博客补上自定义 404 页面，并把系列内导航做进右侧系列卡片里。本文记录实现过程。\u003c/p\u003e","title":"Hugo PaperMod 自定义 404 页面与系列导航"},{"content":" 适用环境：CentOS Stream 9 / Rocky Linux 9\n难度：中等\n一、为什么要换 Nginx + Certbot（Let\u0026rsquo;s Encrypt）是经典组合，但日常维护有两个痛点：\n证书每 90 天要续期，虽然 Certbot 有定时任务，但偶尔还是会因为网络、配置问题失败，需要人工处理。 配置相对繁琐：server 块、ssl 证书路径、http 跳 https 的 301、gzip 等都要手写。 Caddy 的核心卖点是自动 HTTPS：\n首次启动访问域名时，自动向 Let\u0026rsquo;s Encrypt 申请证书，到期自动续期，无需任何定时任务。 配置极简，一个 Caddyfile 就能同时搞定静态站和反向代理。 默认启用 HTTP/2、gzip 压缩，零配置。 对于个人博客这类中小型站点，Caddy 能把最繁琐的证书管理彻底自动化。\n二、安装 Caddy CentOS Stream 9 通过官方 COPR 仓库安装：\n1 2 3 4 5 6 7 8 9 10 11 # 安装 dnf 的 copr 插件 sudo dnf install -y \u0026#39;dnf-command(copr)\u0026#39; # 启用 Caddy 官方仓库 sudo dnf copr enable @caddy/caddy # 安装 sudo dnf install -y caddy # 验证版本 caddy version 三、配置 Caddyfile 配置文件在 /etc/caddy/Caddyfile。语法核心是：域名后跟一个配置块，块内的指令按行排列。\n1 2 3 4 5 6 7 8 9 10 11 # 静态博客站点 www.wkun.top { root * /var/www/blog/public encode gzip zstd file_server } # 反向代理示例：把子域名转发到本机某个服务 # umami.wkun.top { # reverse_proxy 127.0.0.1:3000 # } 各指令说明：\nroot *：指定静态文件根目录，* 表示匹配所有路径。 encode gzip zstd：开启 gzip / zstd 压缩，减少传输体积。 file_server：开启静态文件服务。 reverse_proxy 127.0.0.1:3000：反向代理到本机 3000 端口，用于给其他服务（如自托管应用）提供域名和 HTTPS。 配置好后检查语法：\n1 sudo caddy validate --config /etc/caddy/Caddyfile 四、迁移步骤（顺序很重要） 关键点：先停 Nginx，再启 Caddy。ACME 证书验证需要占用 80 端口，如果 Nginx 还占着，Caddy 的证书签发会失败。\n1 2 3 4 5 6 7 8 9 10 # 1. 停止并禁用 Nginx，释放 80/443 端口 sudo systemctl stop nginx sudo systemctl disable nginx # 2. 启动 Caddy 并设为开机自启 sudo systemctl enable --now caddy sudo systemctl status caddy # 3. 查看 Caddy 日志，确认证书自动签发成功 journalctl -u caddy -n 50 --no-pager 日志中出现类似 certificate obtained successfully 即表示证书签发成功。这一步无需任何手动干预，Caddy 会自动完成：\n向 Let\u0026rsquo;s Encrypt 申请证书 写入证书文件 配置 HTTPS 并自动续期 注意：证书验证要求 80 端口对外可达，且域名 DNS 已解析到服务器。如果之前 Certbot 签过证书，Caddy 会检测并复用，无需手动迁移。\n五、验证 1 2 3 4 5 # 检查 HTTP 响应 curl -I https://www.wkun.top # 查看证书信息（确认是有效证书） echo | openssl s_client -connect wkun.top:443 2\u0026gt;/dev/null | openssl x509 -noout -subject -dates 浏览器访问站点，地址栏出现小锁图标即为成功。\n六、（可选）卸载 Nginx 与 Certbot 确认迁移无误、站点稳定运行几天后，再清理旧组件：\n1 sudo dnf remove -y nginx certbot python3-certbot-nginx 七、总结 迁移前后对比：\n项目 Nginx + Certbot Caddy 证书申请 手动配置 Certbot 自动 证书续期 定时任务，需维护 自动，无需关心 静态站配置 server 块 + ssl 等 20+ 行 3 行 反向代理 proxy_pass + 头信息等 1 行 HTTP/2、gzip 需手动开启 默认开启 对于\u0026quot;域名 + 静态站 + 少量反代\u0026quot;的典型个人博客场景，Caddy 用最少的配置换来了全自动的 HTTPS，省心很多。\n","permalink":"https://www.wkun.top/posts/caddy%E6%9B%BF%E6%8D%A2nginx%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E6%8C%87%E5%8D%97/","summary":"\u003cblockquote\u003e\n\u003cp\u003e适用环境：CentOS Stream 9 / Rocky Linux 9\u003c/p\u003e\n\u003cp\u003e难度：中等\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一为什么要换\"\u003e一、为什么要换\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eNginx + Certbot\u003c/code\u003e（Let\u0026rsquo;s Encrypt）是经典组合，但日常维护有两个痛点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e证书每 90 天要续期，虽然 Certbot 有定时任务，但偶尔还是会因为网络、配置问题失败，需要人工处理。\u003c/li\u003e\n\u003cli\u003e配置相对繁琐：\u003ccode\u003eserver\u003c/code\u003e 块、\u003ccode\u003essl\u003c/code\u003e 证书路径、\u003ccode\u003ehttp\u003c/code\u003e 跳 \u003ccode\u003ehttps\u003c/code\u003e 的 \u003ccode\u003e301\u003c/code\u003e、\u003ccode\u003egzip\u003c/code\u003e 等都要手写。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ccode\u003eCaddy\u003c/code\u003e 的核心卖点是\u003cstrong\u003e自动 HTTPS\u003c/strong\u003e：\u003c/p\u003e","title":"Caddy替换Nginx反向代理指南"},{"content":"想把文章的标签放到网页右侧，做法是仿照 PaperMod 已有的左侧目录（TOC）侧边栏机制，在后面又加上了分类和系列，做成三个独立卡片，随页面滚动固定不动。本文记录实现过程和踩的坑。\n需求 文章的标签、分类、系列显示在页面右侧； 宽屏时浮出到正文右侧并固定，窄屏（手机）时退回文档流，以卡片的形式跟在正文之后； 三个模块独立成卡片，随页面滚动时在屏幕中的位置不变。 思路：复用 TOC 的侧边栏机制 PaperMod 的目录（toc.html）在宽屏下是这么定位的：.toc-container.wide 用 position: absolute 挂在文章元素上，偏移到文章左侧外面，内部再用 position: sticky 让目录随滚动固定。右侧侧边栏完全照抄这套，只是方向相反：\n1 2 3 4 5 6 7 .tags-aside.wide { position: absolute; width: var(--toc-width); /* 300px */ right: calc((var(--toc-width) + var(--gap)) * -1); /* 平移到文章右侧外面 */ top: calc(var(--gap) * 2); height: 100%; /* 高度覆盖整个文章，sticky 才能全程生效 */ } （窄屏下不挂 wide，这条规则不生效，侧边栏就退回文档流。）\n关键点：aside 的高度必须是 100%（等于文章高度），内部的 sticky 才有足够的\u0026quot;可移动空间\u0026quot;，否则滚动到一半卡片就跟着跑了。\n模板改动（layouts/single.html） 1. 顶部定义 taxonomy 变量 1 2 3 {{- $tags := .Language.Params.Taxonomies.tag | default \u0026#34;tags\u0026#34; }} {{- $categories := .Language.Params.Taxonomies.category | default \u0026#34;categories\u0026#34; }} {{- $series := .Language.Params.Taxonomies.series | default \u0026#34;series\u0026#34; }} 2. 目录之后插入侧边栏 放在 partial \u0026quot;toc.html\u0026quot; 后面、正文之前。三个卡片用 tags-aside-sticky 包起来整体 sticky，哪个有内容就显示哪个：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 {{- if or .Params.tags .Params.categories .Params.series }} \u0026lt;aside id=\u0026#34;side-aside\u0026#34; class=\u0026#34;tags-aside\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;tags-aside-sticky\u0026#34;\u0026gt; {{- if .Params.categories }} \u0026lt;div class=\u0026#34;tags-aside-box\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;tags-aside-title\u0026#34;\u0026gt;分类\u0026lt;/div\u0026gt; \u0026lt;ul class=\u0026#34;post-tags\u0026#34;\u0026gt; {{- range ($.GetTerms $categories) }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34;\u0026gt;{{ .LinkTitle }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; {{- end }} {{- if .Params.tags }} \u0026lt;div class=\u0026#34;tags-aside-box\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;tags-aside-title\u0026#34;\u0026gt;标签\u0026lt;/div\u0026gt; \u0026lt;ul class=\u0026#34;post-tags\u0026#34;\u0026gt; {{- range ($.GetTerms $tags) }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34;\u0026gt;{{ .LinkTitle }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; {{- end }} {{- if .Params.series }} \u0026lt;div class=\u0026#34;tags-aside-box\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;tags-aside-title\u0026#34;\u0026gt;系列\u0026lt;/div\u0026gt; \u0026lt;ul class=\u0026#34;post-tags\u0026#34;\u0026gt; {{- range ($.GetTerms $series) }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34;\u0026gt;{{ .LinkTitle }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/div\u0026gt; \u0026lt;/aside\u0026gt; {{- end }} 3. 底部脚本：和左侧目录共用一份宽屏判断 判断条件是「文档宽 − 文章宽 − 2 × 侧边栏宽 − 4 × 间距 \u0026gt; 0」。这份判定左侧目录同样要用，而且两侧必须同时进入或退出 wide，否则会出现左边目录浮出、右边标签栏还留在文档流里的不对称。所以判定只留一份，两个 aside 在同一次遍历里一起切：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;script\u0026gt; (function () { // 左侧目录（#toc-container）与右侧标签栏（#side-aside）必须同时进入 / 退出 wide， // 否则两侧会不对称。所以宽度判定只保留这一份，两个元素在同一次遍历里一起切。 // 元素在 DOMContentLoaded 时才查，脚本放哪儿都不影响。 const style = getComputedStyle(document.body); const main = parseInt(style.getPropertyValue(\u0026#39;--article-width\u0026#39;), 10); const toc = parseInt(style.getPropertyValue(\u0026#39;--toc-width\u0026#39;), 10); const gap = parseInt(style.getPropertyValue(\u0026#39;--gap\u0026#39;), 10); function updateAsideWide() { const wide = (document.body.scrollWidth - main - (toc * 2) - (gap * 4)) \u0026gt; 0; [\u0026#39;toc-container\u0026#39;, \u0026#39;side-aside\u0026#39;].forEach((id) =\u0026gt; { const aside = document.getElementById(id); if (aside) aside.classList.toggle(\u0026#39;wide\u0026#39;, wide); }); } document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, updateAsideWide); window.addEventListener(\u0026#39;resize\u0026#39;, updateAsideWide); })(); \u0026lt;/script\u0026gt; --article-width、--toc-width、--gap 是 CSS 变量，站点里自定义过（见下），脚本从 getComputedStyle(document.body) 读出来计算，不用写死数字。\n这段脚本早期还负责隐藏 footer 里的标签胶囊（document.querySelector('.post-footer .post-tags')）。后来 footer 改成只放上下篇导航和分享按钮，那几行每次都取到 null，成了死代码，已经删掉；现在分类和标签只出现在右侧卡片里。\n样式（assets/css/extended/toc.css） 侧边栏样式追加在目录样式后面：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 /* ===== 文章标签侧边栏（右侧，与目录对称） ===== 窄屏（无 .wide）：以文档流展示在正文之后； 宽屏（.wide）：绝对定位到正文右侧。 */ .tags-aside { display: block; width: 100%; margin-top: calc(var(--gap) * 1.5); } .tags-aside.wide { position: absolute; width: var(--toc-width); right: calc((var(--toc-width) + var(--gap)) * -1); top: calc(var(--gap) * 2); height: 100%; margin-top: 0; border-left: 1px solid var(--border); padding-left: calc(var(--gap) * 0.5); } .tags-aside-sticky { position: sticky; /* 吸顶导航栏固定后，标签栏停留位置下移到导航栏之下 */ top: calc(var(--header-height) + var(--gap)); } .tags-aside-box { border: 1px solid var(--border); background: var(--entry); border-radius: var(--radius); padding: 0.8em 1em 1em; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10); } /* 暗色模式下阴影不明显，适当加深 */ [data-theme=\u0026#34;dark\u0026#34;] .tags-aside-box { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); } .tags-aside-title { font-weight: 600; font-size: 0.95rem; color: var(--secondary); margin-bottom: 0.7em; } .tags-aside-box + .tags-aside-box { margin-top: var(--gap); } （卡片里胶囊的间距与悬停配色在 series.css 的 .tags-aside .post-tags 里。）\n站点根部的 CSS 变量（toc.css 开头就有，这里一并记录）：\n1 2 3 4 5 :root { --nav-width: 1380px; --article-width: 650px; --toc-width: 300px; } 宽屏阈值 文章宽 + 2 × 侧边栏宽 + 4 × 间距 = 650 + 600 + 96 = 1346px 左右，文档宽度大于这个值才会让左右两条侧边栏一起浮出。\n过程中的几个决定 卡片顺序：分类在最上面、标签居中、系列在最下面（系列卡片内部是带 1/N 进度和「当前」标记的导航列表，样式在 series.css）； 卡片底色：尝试过 --code-bg（跟代码块太像）、淡蓝灰、米黄、带绿的浅色，最后觉得都不如默认的白色配边框和阴影干净，改回了 --entry； 整体 sticky：一开始把 position: sticky 写在每张卡片上，结果是三张卡片轮流吸附、各自固定，后来包了一层 tags-aside-sticky 容器，三张卡片作为一个整体固定； 只在有对应 frontmatter 时渲染：if .Params.categories 这样的判断，没有分类的文章不会出现空卡片。 小结 PaperMod 的左右侧边栏本质上都是 position: absolute 定位 + 内部 sticky 固定这套组合，摸清 TOC 的实现后右侧照抄就行。两个容易漏的点：aside 高度要 100%，以及 sticky 容器要包住所有卡片。宽屏/窄屏的切换靠一段几行的 JS 判断，而且左右两条侧边栏共用同一份判定，必须同时进出 wide，否则两侧会不对称。\n","permalink":"https://www.wkun.top/posts/hugo-papermod-%E6%96%87%E7%AB%A0%E5%88%86%E7%B1%BB%E7%B3%BB%E5%88%97%E6%A0%87%E7%AD%BE%E5%8F%B3%E4%BE%A7%E4%BE%A7%E8%BE%B9%E6%A0%8F/","summary":"\u003cp\u003e想把文章的标签放到网页右侧，做法是仿照 PaperMod 已有的左侧目录（TOC）侧边栏机制，在后面又加上了分类和系列，做成三个独立卡片，随页面滚动固定不动。本文记录实现过程和踩的坑。\u003c/p\u003e","title":"Hugo PaperMod 文章分类/系列/标签右侧侧边栏"},{"content":"折腾评论区的时候发现一个事：站点的评论区不会跟随主题变色，一直白底。查阅资料发现giscus官方配置中的data-theme=\u0026quot;preferred_color_scheme\u0026quot;表示跟随系统变色，我把系统颜色从浅色改成深色后，评论区就变成了深色，但是我希望评论区跟随主题变色，所以需要做一些调整。\n然后我给giscus的配置做了一些改动，让它跟随主题变色，但是出错了，切换主题后评论区并没有变色。\n排查 一开始我怀疑是监听写错了。评论区的主题同步逻辑大概是这样的：监听某个元素的变化，然后往 giscus 的 iframe 里发消息换主题。我监听的是 body 上的 class：\n1 2 // 我原来这么写的 return document.body.classList.contains(\u0026#39;dark\u0026#39;) ? \u0026#39;transparent_dark\u0026#39; : \u0026#39;light\u0026#39;; 结果怎么试都不生效。后来翻了 PaperMod 的源码才发现，它的主题切换根本不是给 body 加 class，而是改 \u0026lt;html\u0026gt; 元素上的 data-theme 属性：\n1 document.documentElement.dataset.theme = \u0026#39;dark\u0026#39;; 所以 body.classList.contains('dark') 永远都是 false，自然永远走亮色分支，监听的 class 变化也永远不会触发。\n另外还有个坑：giscus 开了 data-loading=\u0026quot;lazy\u0026quot;，iframe 要滚动到评论区附近才创建。如果在这之前切主题，postMessage 发出去没人接收，等 iframe 加载的时候只能用 script 标签上旧的 data-theme 初始化，主题还是错的。\n修复 检测和监听都改成 data-theme：\n1 2 3 4 5 6 7 8 9 10 11 12 13 function getGiscusTheme() { return document.documentElement.dataset.theme === \u0026#39;dark\u0026#39; ? \u0026#39;{{ absURL \u0026#34;css/giscus-dark.css\u0026#34; }}\u0026#39; : \u0026#39;{{ absURL \u0026#34;css/giscus-light.css\u0026#34; }}\u0026#39;; } const observer = new MutationObserver(() =\u0026gt; { sendGiscusTheme(getGiscusTheme()); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: [\u0026#39;data-theme\u0026#39;] }); 发消息的时候顺手把 script 标签和 iframe 上的 data-theme 属性也同步掉，就算 iframe 还没创建，等它加载的时候也能用对主题：\n1 2 3 4 5 6 7 8 9 10 11 12 function sendGiscusTheme(giscusTheme) { const script = container.querySelector(\u0026#39;script[src*=\u0026#34;giscus\u0026#34;]\u0026#39;); if (script) script.setAttribute(\u0026#39;data-theme\u0026#39;, giscusTheme); const iframe = document.querySelector(\u0026#39;iframe.giscus-frame\u0026#39;); if (!iframe || !iframe.contentWindow) return; iframe.dataset.theme = giscusTheme; iframe.contentWindow.postMessage( { giscus: { setConfig: { theme: giscusTheme } } }, \u0026#39;https://giscus.app\u0026#39; ); } 风格不统一的问题 主题能切换之后，又发现明暗两边的观感差太多。原因是我当初为了\u0026quot;融入暗色背景\u0026quot;选了 transparent_dark（透明主题），而亮色用的官方 light（自带背景和边框）。做完之后发现，暗色背景的评论区背景和边框是透明的，跟暗色背景融入在一起，跟正文的背景和边框完全不搭。\n干脆统一成卡片风格，给明暗各写一个自定义主题，颜色直接抄 PaperMod 的 CSS 变量，这样评论区跟正文就是一个色系的了。建了两个文件 static/css/giscus-light.css 和 static/css/giscus-dark.css，主要改这几个变量：\n--color-canvas-default（卡片背景）：亮色 #ffffff，暗色 #1d1e20，对应 PaperMod 的 --theme --color-canvas-subtle（输入框背景）：亮色 #f5f5f5，暗色 #37383e，对应 --code-bg --color-border-default（边框）：亮色 #eeeeee，暗色 #333333，对应 --border --color-fg-default / --color-fg-muted（文字）：对应 --primary / --secondary 按钮背景用 --code-bg / --entry，提交按钮保留 giscus 官方那个绿色 有一点要注意：自定义主题必须以 main 选择器包裹变量，而且 giscus 的语法高亮（--color-prettylights-*）、加载动画那些定义最好别删，不然评论区会缺样式。\n部署时的坑 giscus 的 iframe 是跨域加载自定义 CSS 的，服务器必须返回 Access-Control-Allow-Origin 头，不然评论区直接加载不出来。我是 Nginx 部署，加了一段：\n1 2 3 location /css/ { add_header Access-Control-Allow-Origin *; } 用 GitHub Pages 的话不用管，它自动带这个响应头。\n改完的代码 layouts/partials/comments.html 完整内容（数据源配置换成你自己的）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 \u0026lt;div class=\u0026#34;giscus\u0026#34; id=\u0026#34;giscus-container\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; (function() { const container = document.getElementById(\u0026#39;giscus-container\u0026#39;); // 主题状态在 \u0026lt;html data-theme=\u0026#34;...\u0026#34;\u0026gt; 上，PaperMod 切换主题就是改这个属性 function getGiscusTheme() { return document.documentElement.dataset.theme === \u0026#39;dark\u0026#39; ? \u0026#39;{{ absURL \u0026#34;css/giscus-dark.css\u0026#34; }}\u0026#39; : \u0026#39;{{ absURL \u0026#34;css/giscus-light.css\u0026#34; }}\u0026#39;; } // 同步 script/iframe 上的 data-theme + 发消息，懒加载下也不怕丢消息 function sendGiscusTheme(giscusTheme) { const script = container.querySelector(\u0026#39;script[src*=\u0026#34;giscus\u0026#34;]\u0026#39;); if (script) script.setAttribute(\u0026#39;data-theme\u0026#39;, giscusTheme); const iframe = document.querySelector(\u0026#39;iframe.giscus-frame\u0026#39;); if (!iframe || !iframe.contentWindow) return; iframe.dataset.theme = giscusTheme; iframe.contentWindow.postMessage( { giscus: { setConfig: { theme: giscusTheme } } }, \u0026#39;https://giscus.app\u0026#39; ); } const script = document.createElement(\u0026#39;script\u0026#39;); script.src = \u0026#39;https://giscus.app/client.js\u0026#39;; script.crossOrigin = \u0026#39;anonymous\u0026#39;; script.async = true; script.setAttribute(\u0026#39;data-loading\u0026#39;, \u0026#39;lazy\u0026#39;); // 替换成你自己的配置 script.setAttribute(\u0026#39;data-repo\u0026#39;, \u0026#39;用户名/仓库名\u0026#39;); script.setAttribute(\u0026#39;data-repo-id\u0026#39;, \u0026#39;你的repo-id\u0026#39;); script.setAttribute(\u0026#39;data-category\u0026#39;, \u0026#39;Announcements\u0026#39;); script.setAttribute(\u0026#39;data-category-id\u0026#39;, \u0026#39;你的category-id\u0026#39;); script.setAttribute(\u0026#39;data-mapping\u0026#39;, \u0026#39;pathname\u0026#39;); script.setAttribute(\u0026#39;data-strict\u0026#39;, \u0026#39;0\u0026#39;); script.setAttribute(\u0026#39;data-reactions-enabled\u0026#39;, \u0026#39;1\u0026#39;); script.setAttribute(\u0026#39;data-reactions-layout\u0026#39;, \u0026#39;compact\u0026#39;); script.setAttribute(\u0026#39;data-emit-metadata\u0026#39;, \u0026#39;0\u0026#39;); script.setAttribute(\u0026#39;data-input-position\u0026#39;, \u0026#39;top\u0026#39;); script.setAttribute(\u0026#39;data-theme\u0026#39;, getGiscusTheme()); // 加载前就设置好主题 script.setAttribute(\u0026#39;data-lang\u0026#39;, \u0026#39;zh-CN\u0026#39;); container.appendChild(script); // 监听 \u0026lt;html\u0026gt; 的 data-theme 变化 const observer = new MutationObserver(() =\u0026gt; { sendGiscusTheme(getGiscusTheme()); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: [\u0026#39;data-theme\u0026#39;] }); // 系统深色自动切换也要跟上 window.matchMedia(\u0026#39;(prefers-color-scheme: dark)\u0026#39;) .addEventListener(\u0026#39;change\u0026#39;, () =\u0026gt; sendGiscusTheme(getGiscusTheme())); })(); \u0026lt;/script\u0026gt; 小结 主题状态在 \u0026lt;html\u0026gt; 的 data-theme 属性上，不关 body 的事，监听对准它就行。懒加载丢消息那个问题也是顺手一起解决的。自定义主题的配色是照着 PaperMod 的 CSS 变量一个个抄的，弄完整体还算统一。\n折腾了这么久，评论区总算能跟着主题跑了。\n","permalink":"https://www.wkun.top/posts/giscus-%E8%AF%84%E8%AE%BA%E5%8C%BA%E8%B7%9F%E9%9A%8F%E4%B8%BB%E9%A2%98%E6%98%8E%E6%9A%97/","summary":"\u003cp\u003e折腾评论区的时候发现一个事：站点的评论区不会跟随主题变色，一直白底。查阅资料发现giscus官方配置中的\u003ccode\u003edata-theme=\u0026quot;preferred_color_scheme\u0026quot;\u003c/code\u003e表示跟随系统变色，我把系统颜色从浅色改成深色后，评论区就变成了深色，但是我希望评论区跟随主题变色，所以需要做一些调整。\u003c/p\u003e","title":"giscus 评论区跟随 PaperMod 主题明暗切换"},{"content":"最近发现手机端访问首页时，标题「半知半解，且过且学！」的打字机特效走完之后，文字右侧被截断、显示不全，而桌面端一切正常。本文记录排查过程与修复方案。\n问题描述 桌面端：标题完整显示，打字机动画正常。 手机端：动画结束后右侧文字被裁掉，最后一个字不完整。 原因分析 问题出在 assets/css/extended/custom.css 里打字机动画的宽度设计：\n1 2 3 4 5 6 .profile .profile_inner h1 { max-width: 100%; /* 手机窄屏下宽度被压到容器宽（约 292~347px） */ width: 14em; /* 动画终点目标宽度，14em ≈ 448px，超出容器 */ overflow: hidden; /* 超出的部分直接裁剪 */ white-space: nowrap; /* 禁止换行 */ } 标题「半知半解，且过且学！」是 10 个全角字符 ≈ 10em，但动画把宽度写死成了 14em（可能是按 14 个字符设计的）。三个属性叠加导致窄屏截断：\n桌面容器约 780px \u0026gt; 14em，文字完整； 手机容器约 292~347px \u0026lt; 14em，max-width: 100% 把宽度压到容器值，右侧文字被 overflow: hidden 裁掉，而 nowrap 又禁止换行，于是文字就\u0026quot;丢\u0026quot;了。 注：这里和下面的数字都按出问题时的标题「半知半解，且过且学！」（10 字）记录。后来首页标题换成了「拾光入笔，岁月留痕」（9 字），数字换了但改法一致，见文末「后续改进」。\n修复方案（保留手机端打字机效果） 将 custom.css 中 .profile .profile_inner h1 至 @media (prefers-reduced-motion...) 整段替换为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 .profile .profile_inner h1 { max-width: 100%; width: 10em; /* 与标题字符数一致：\u0026#34;半知半解，且过且学！\u0026#34; = 10 字符 */ overflow: hidden; white-space: nowrap; border-right: 1px solid currentColor; animation: profile-title-typing 2.4s steps(10, end), profile-title-caret 0.75s step-end infinite; } @keyframes profile-title-typing { from { width: 0; } to { width: 10em; } } @keyframes profile-title-caret { from, to { border-color: transparent; } 50% { border-color: currentColor; } } /* 无障碍：系统开启\u0026#34;减少动态效果\u0026#34;时直接静态显示 */ @media (prefers-reduced-motion: reduce) { .profile .profile_inner h1 { animation: none; width: auto; max-width: 100%; white-space: normal; border-right: 0; } } /* 手机端：缩小字号，保证 10em 的动画宽度能放下 */ /* 320px 屏容器约 292px，1.75em×10 = 280px，完整放得下 */ @media (max-width: 480px) { .profile .profile_inner h1 { font-size: 1.75em; } } 改动对照表 项 原来 现在 原因 width 14em 10em 精确等于文字宽度，不再白白占 4em steps() steps(14, end) steps(10, end) 步数 = 字符数，每步显示一个字 动画时长 3.2s 2.4s 保持每字约 0.24s 的节奏 手机端 无 font-size: 1.75em em 随字号缩放，10em 同步变小，窄屏放得下 原理：width: 10em 与 keyframes 里的 10em 都基于元素自身字号，媒体查询里只改 font-size，动画终点宽度等比缩放，打字机效果在手机上依然保留且完整显示。\n备选方案：手机端直接关闭动画 若认为手机端不需要打字机效果，仅追加以下媒体查询即可（标题自动换行、永不被裁）：\n1 2 3 4 5 6 7 8 @media (max-width: 768px) { .profile .profile_inner h1 { width: auto; white-space: normal; border-right: 0; animation: none; } } 后续改进：字符数改为自动生成 上面的方案修好了截断，但留了个隐患：width 和 steps() 里的数字必须和标题字数保持一致，标题一改就得回来手动同步，很容易忘。\n后来改成由模板按标题实际字数注入 CSS 变量，custom.css 里只留兜底值。\nlayouts/partials/extend_head.html 在首页注入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 {{- /* 首页标题打字机：按标题实际字数生成动画参数， 避免与 custom.css 里的兜底值不同步（1 个全角字按 1em 计）。 */ -}} {{- if .IsHome }} {{- with site.Params.profileMode }} {{- if .enabled }} {{- $chars := strings.RuneCount (.title | default site.Title) }} \u0026lt;style\u0026gt; .profile .profile_inner h1 { --type-chars: {{ $chars }}; --type-ease: steps({{ $chars }}, end); } \u0026lt;/style\u0026gt; {{- end }} {{- end }} {{- end }} custom.css 里改成全部走变量：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /* 首页标题打字机 --type-chars / --type-ease 由 extend_head.html 按 profileMode.title 的字数注入， 下面两行只是兜底值，改标题后不需要再手改这里。 */ .profile .profile_inner h1 { --type-chars: 9; --type-ease: steps(9, end); max-width: 100%; width: calc(var(--type-chars) * 1em); overflow: hidden; white-space: nowrap; border-right: 1px solid currentColor; animation: profile-title-typing calc(var(--type-chars) * 0.24s) var(--type-ease), profile-title-caret 0.75s step-end infinite; } @keyframes profile-title-typing { from { width: 0; } to { width: calc(var(--type-chars) * 1em); } } 几个要点：\nstrings.RuneCount 数的是字符数而不是字节数，中文标题一个字算 1； 全角字宽约等于 1em，所以 --type-chars 可以直接当 em 用：width: calc(var(--type-chars) * 1em)； steps() 同样能吃变量（--type-ease: steps({{ $chars }}, end)），动画时长也按字数算（calc(var(--type-chars) * 0.24s)），维持每字约 0.24s 的节奏； 模板只在首页、且 profileMode.enabled 时输出这段 \u0026lt;style\u0026gt;，其它页面不会带上无用样式； custom.css 里那两行兜底值保留的意义是：万一模板没注入，动画仍然能跑，不至于整个特效消失。 于是「改标题要同步改数字」这件事就不存在了。\n验证方法 浏览器 F12 → 设备模拟（手机图标）； 选择 iPhone 尺寸（375px）或更窄的 320px 视口； 刷新首页，等待打字机动画结束，确认标题完整显示； 桌面端确认打字机效果无回归。 注意事项 标题文字修改后，需同步更新 10em 和 steps(10, end) 中的数字为新字符数。这一条已废弃：字符数现在由 extend_head.html 自动注入（见「后续改进」），改标题不用再动 CSS； 若自定义了 --main-width 或页面内边距，仍要重新核算窄屏容器是否放得下标题（标题宽度 ≈ 字数 × font-size；本站标题 9 字，480px 以下 font-size: 1.75em）。 ","permalink":"https://www.wkun.top/posts/%E9%A6%96%E9%A1%B5%E6%89%93%E5%AD%97%E6%9C%BA%E7%89%B9%E6%95%88%E6%89%8B%E6%9C%BA%E7%AB%AF%E6%98%BE%E7%A4%BA%E4%B8%8D%E5%85%A8%E4%BF%AE%E5%A4%8D/","summary":"\u003cp\u003e最近发现手机端访问首页时，标题「半知半解，且过且学！」的打字机特效走完之后，文字右侧被截断、显示不全，而桌面端一切正常。本文记录排查过程与修复方案。\u003c/p\u003e","title":"首页打字机特效手机端文字显示不全的修复"},{"content":"Vscode禁止自动添加import 避免pylance自动添加import，打开设置，搜索autoimportcompletions，取消勾选即可。\n禁用自动格式化 我设置了全局格式化并且安装了prettier插件，但是某些格式的文件不需要格式化，比如markdown文件，所以需要禁用这些文件的自动格式化。\n方案一：\n在settings.json中添加如下配置：\n1 2 3 4 5 6 \u0026#34;[markdown]\u0026#34;: { \u0026#34;editor.formatOnSave\u0026#34;: false, \u0026#34;editor.formatOnType\u0026#34;: false, \u0026#34;editor.formatOnPaste\u0026#34;: false, \u0026#34;editor.defaultFormatter\u0026#34;: null } prettier插件\u0026quot;prettier.disableLanguages\u0026quot;: [\u0026quot;markdown\u0026quot;],已不再支持。\n方案二：\n在项目中添加.prettierignore文件，在文件中添加*.md，表示忽略所有markdown文件的格式化。\n","permalink":"https://www.wkun.top/posts/vscode%E8%87%AA%E5%AE%9A%E4%B9%89%E8%AE%BE%E7%BD%AE/","summary":"\u003ch2 id=\"vscode禁止自动添加import\"\u003eVscode禁止自动添加import\u003c/h2\u003e\n\u003cp\u003e避免pylance自动添加import，打开设置，搜索autoimportcompletions，取消勾选即可。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"图片1\" loading=\"lazy\" src=\"/images/posts/vscode-disable-auto-import.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"禁用自动格式化\"\u003e禁用自动格式化\u003c/h2\u003e\n\u003cp\u003e我设置了全局格式化并且安装了prettier插件，但是某些格式的文件不需要格式化，比如markdown文件，所以需要禁用这些文件的自动格式化。\u003c/p\u003e","title":"Vscode自定义设置"},{"content":"重启plasmashell kquitapp5 plasmashell \u0026amp;\u0026amp; kstart plasmashell ","permalink":"https://www.wkun.top/posts/archlinux%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7/","summary":"\u003ch4 id=\"重启plasmashell\"\u003e重启plasmashell\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003ekquitapp5 plasmashell \u0026amp;\u0026amp; kstart plasmashell\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"Linux使用技巧"},{"content":"firewalld的基本使用 1 2 3 4 5 6 7 8 9 10 11 # 启动 systemctl start firewalld # 查看状态 systemctl status firewalld # 停止 systemctl disable firewalld # 禁用 systemctl stop firewalld 配置firewalld-cmd 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 查看版本 firewall-cmd --version # 查看帮助 firewall-cmd --help # 显示状态 firewall-cmd --state # 查看所有打开的端口 firewall-cmd --zone=public --list-ports # 更新防火墙规则 firewall-cmd --reload # 查看区域信息 firewall-cmd --get-active-zones # 查看指定接口所属区域 firewall-cmd --get-zone-of-interface=eth0 # 拒绝所有包 firewall-cmd --panic-on # 取消拒绝状态 firewall-cmd --panic-off # 查看是否拒绝 firewall-cmd --query-panic 开启端口 1 2 3 4 5 6 7 8 9 10 11 # 添加 firewall-cmd --zone=public --add-port=80/tcp --permanent （--permanent永久生效，没有此参数重启后失效） # 重新载入 firewall-cmd --reload # 查看 firewall-cmd --zone= public --query-port=80/tcp # 删除 firewall-cmd --zone= public --remove-port=80/tcp --permanent ","permalink":"https://www.wkun.top/posts/firewalld%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8/","summary":"\u003ch2 id=\"firewalld的基本使用\"\u003efirewalld的基本使用\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 启动\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esystemctl start firewalld\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看状态\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esystemctl status firewalld \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 停止\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esystemctl disable firewalld\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 禁用\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esystemctl stop firewalld\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"配置firewalld-cmd\"\u003e配置firewalld-cmd\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e26\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e27\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e28\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e29\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看版本\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --version\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看帮助\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --help\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 显示状态\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --state\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看所有打开的端口\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --zone\u003cspan class=\"o\"\u003e=\u003c/span\u003epublic --list-ports\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 更新防火墙规则\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --reload\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看区域信息\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --get-active-zones\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看指定接口所属区域\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --get-zone-of-interface\u003cspan class=\"o\"\u003e=\u003c/span\u003eeth0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 拒绝所有包\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --panic-on\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 取消拒绝状态\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --panic-off\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看是否拒绝\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --query-panic\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"开启端口\"\u003e开启端口\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 添加\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --zone\u003cspan class=\"o\"\u003e=\u003c/span\u003epublic --add-port\u003cspan class=\"o\"\u003e=\u003c/span\u003e80/tcp --permanent   （--permanent永久生效，没有此参数重启后失效）\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 重新载入\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --reload\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 查看\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --zone\u003cspan class=\"o\"\u003e=\u003c/span\u003e public --query-port\u003cspan class=\"o\"\u003e=\u003c/span\u003e80/tcp\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 删除\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirewall-cmd --zone\u003cspan class=\"o\"\u003e=\u003c/span\u003e public --remove-port\u003cspan class=\"o\"\u003e=\u003c/span\u003e80/tcp --permanent\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e","title":"Firewalld的基本使用"},{"content":"selenium报错：urllib3.exceptions.ProxySchemeUnknown: Proxy URL had no scheme, should start with http:// or https:// 最近用Python+selenium做了一个自动化工具，但由于本地开启了全局代理，在发起请求的时候返回了上面的报错，经过一番研究，终于解决了这个问题。\n报错部分的代码：\n1 2 3 4 5 6 7 8 from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService url = \u0026#39;\u0026#39; options = webdriver.ChromeOptions() options.add_argument(\u0026#39;--headless\u0026#39;) # 无界面模式 options.add_argument(\u0026#34;--disable-blink-features=AutomationControlled\u0026#34;) driver = webdriver.Chrome(service=ChromeService(driver_path), options=options) driver.get(url) 研究发现是作者要请求的链接不能走代理，所以解决方法就清晰了：\n1. 关闭全局代理： 把代理关了，报错自然就没了，但是作者需要代理终端，所以全局代理还不能关。\n2. 修改代理： 可以把要请求的连接的域名加入到代理的排除列表里，理论上可行，但是失败了，有意思的是全局代理下失败了，但是在规则代理下生效了，不知道是不是规则代理的锅，但是作者没有时间去研究了，所以只能用下面的方法。\n3. 改代码： 这个应该算是最方便的做法了，只需要在上面的代码里加一行就行，修改后的代码如下：\n1 2 3 4 5 6 7 8 9 from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService url = \u0026#39;\u0026#39; options = webdriver.ChromeOptions() options.add_argument(\u0026#39;--headless\u0026#39;) # 无界面模式 options.add_argument(\u0026#34;--disable-blink-features=AutomationControlled\u0026#34;) options.ignore_local_proxy_environment_variables() # 忽略环境变量中的代理设置 driver = webdriver.Chrome(service=ChromeService(driver_path), options=options) driver.get(url) 最后作者用的是方法3，完美解决，有时间在研究研究终端代理排除的问题，期待更新……\n","permalink":"https://www.wkun.top/posts/selenium%E6%8A%A5%E9%94%99%E8%A7%A3%E5%86%B3/","summary":"\u003ch1 id=\"selenium报错urllib3exceptionsproxyschemeunknown-proxy-url-had-no-scheme-should-start-with-http-or-https\"\u003eselenium报错：urllib3.exceptions.ProxySchemeUnknown: Proxy URL had no scheme, should start with http:// or https://\u003c/h1\u003e\n\u003cp\u003e最近用Python+selenium做了一个自动化工具，但由于本地开启了全局代理，在发起请求的时候返回了上面的报错，经过一番研究，终于解决了这个问题。\u003c/p\u003e","title":"Selenium报错解决"},{"content":"一、安装 访问官网下载安装包，双击安装即可。\n安装完成后重启终端，运行py --help检查是否可用\n首次使用建议运行py install --configure，这会引导你开启 App Execution Aliases（系统设置 → 应用执行别名）。\n二、 Python版本管理 查看版本列表 1 2 3 4 py list # 查看所有已安装版本 py list --only-managed # 只显示通过 Manager 安装的版本 py list --online # 查看可在线安装的所有版本 py list --online 3.14 # 搜索特定系列 安装Python版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 安装指定版本 py install 3.14 py install 3.13 # 安装最新稳定版 py install default # 安装自由线程版（free-threaded） py install 3.14t # 安装特定架构 py install 3.14-arm64 # 强制重新安装 py install -f 3.14 # 更新所有已安装版本 py install -u 干运行（预览）： 1 py install --dry-run 3.14 卸载版本 1 2 3 py uninstall 3.13 # 卸载指定版本 py uninstall -y 3.13 # 无确认卸载 py uninstall --purge # 彻底清理所有 Manager 管理的内容（推荐完全重置时使用） 三、 使用与切换版本 运行指定版本 1 2 3 4 5 6 7 8 py -3.14 script.py py -V:3.14 script.py # 使用自由线程版 py -3.14t script.py # 运行模块 py -3.13 -m pip install requests 提示：-V: 可省略，直接写版本号（如 py -3.14）即可。\n参考链接 本文来自博客园，作者：a120608yby，原文链接：https://www.cnblogs.com/a120608yby/p/20198926\n","permalink":"https://www.wkun.top/posts/python-install-manager-guide/","summary":"\u003ch2 id=\"一安装\"\u003e一、安装\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e访问\u003ca href=\"https://www.python.org\"\u003e官网\u003c/a\u003e下载安装包，双击安装即可。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e安装完成后重启终端，运行\u003ccode\u003epy --help\u003c/code\u003e检查是否可用\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e首次使用建议运行\u003ccode\u003epy install --configure\u003c/code\u003e，这会引导你开启 \u003cstrong\u003eApp Execution Aliases\u003c/strong\u003e（系统设置 → 应用执行别名）。\u003c/p\u003e","title":"Python Install Manager 使用手册"},{"content":"这一部分主要是跟着问道凌虚的《Hugo折腾记》来做的（注：该站点目前已无法访问，链接仅作出处记录）。\n下面是我自行修改的配置 1 侧边目录及阅读进度条 侧边目录最初参考了在PaperMod中引入侧边目录和阅读进度显示，基本没有修改。后来把目录内容换成了Hugo内置的.TableOfContents（由Hugo直接生成嵌套\u0026lt;ul\u0026gt;），模板只保留外框和滚动高亮；阅读进度条则挂在「回到顶部」按钮上。下面记录最终的做法。\n1.1 配置（hugo.yaml） 目录收录的标题层级交给Hugo内置目录控制，保持h1~h6全收（我站内有文章用#当章节标题）：\n1 2 3 4 5 6 7 8 params: showToc: true TocOpen: true # 目录默认展开 markup: tableOfContents: startLevel: 1 endLevel: 6 ShowToc控制单篇文章是否渲染目录，TocOpen控制\u0026lt;details\u0026gt;默认是否展开。\n1.2 模板（layouts/partials/toc.html） 结构沿用PaperMod的外框，\u0026lt;aside\u0026gt; / .toc / \u0026lt;details\u0026gt; / .inner这几层是给toc.css定位和折叠用的：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 {{- $toc := .TableOfContents -}} {{- if strings.Contains $toc \u0026#34;\u0026lt;ul\u0026gt;\u0026#34; }} \u0026lt;aside id=\u0026#34;toc-container\u0026#34; class=\u0026#34;toc-container wide\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;toc\u0026#34;\u0026gt; \u0026lt;details {{if ($.Param \u0026#34;TocOpen\u0026#34;) }} open{{ end }}\u0026gt; \u0026lt;summary accesskey=\u0026#34;c\u0026#34; title=\u0026#34;(Alt + C)\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;details\u0026#34;\u0026gt;{{- i18n \u0026#34;toc\u0026#34; | default \u0026#34;Table of Contents\u0026#34; }}\u0026lt;/span\u0026gt; \u0026lt;/summary\u0026gt; \u0026lt;div class=\u0026#34;inner\u0026#34;\u0026gt; {{ $toc }} \u0026lt;/div\u0026gt; \u0026lt;/details\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/aside\u0026gt; 坑一：没有标题的文章会渲染出一个空目录框。 .TableOfContents在没有标题时返回的不是空字符串，而是一个空壳\u0026lt;nav id=\u0026quot;TableOfContents\u0026quot;\u0026gt;\u0026lt;/nav\u0026gt;，直接用with判断会渲染出空的「目录」框（旧模板是靠findRE数标题才挡住的）。所以这里改成判断目录里到底有没有列表：strings.Contains $toc \u0026quot;\u0026lt;ul\u0026gt;\u0026quot;。\n坑二：中文标题的锚点可能是转义过的。 目录里的href可能是原始中文，也可能是%xx转义形式，两种都要能对上标题的id。所以先把#后面的部分decodeURIComponent再建索引：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function buildTocIndex() { const index = new Map(); document.querySelectorAll(\u0026#39;.toc a[href^=\u0026#34;#\u0026#34;]\u0026#39;).forEach((link) =\u0026gt; { let id = link.getAttribute(\u0026#39;href\u0026#39;).slice(1); try { id = decodeURIComponent(id); } catch (e) { // 不是合法的百分号编码就按原样使用 } index.set(id, link); }); return index; } 坑三：目录不一定收录文档里的每个标题。 tocLinks.get(element.id)有可能取到undefined，早期没判空，一次null.classList抛错就把后面「回到顶部」的绑定一起中断了。所以取值一律判空：\n1 2 3 4 const tocLink = tocLinks.get(element.id); if (!tocLink) { return; } 滚动高亮的逻辑：取所有带id的h1~h6，滚动时把「位于视口上半部分」的标题设为当前项，给对应目录链接加.active，并把.inner内部滚动到让激活项居中：\n1 2 const scrollPosition = linkOffsetTop - (containerHeight / 2) + (linkHeight / 2); tocContainer.scrollTo({ top: scrollPosition, behavior: \u0026#39;smooth\u0026#39; }); 1.3 样式（assets/css/extended/toc.css） 定位思路是「外框absolute挂到正文左侧外面 + 内部sticky固定」，宽度用变量统一管理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 :root { --nav-width: 1380px; --article-width: 650px; --toc-width: 300px; } .toc-container.wide { position: absolute; height: 100%; /* 高度覆盖整篇，sticky 才能全程生效 */ 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: calc(var(--header-height) + var(--gap)); } 目录本身限制了高度、超出内部滚动，滚动条也顺手做了样式：\n1 2 3 4 5 6 7 8 9 10 11 12 .toc .inner { /*目录显示高度*/ max-height: 83vh; overflow-y: auto; } .toc .inner::-webkit-scrollbar-thumb { /*滚动条*/ background: var(--border); border: 7px solid var(--theme); border-radius: var(--radius); } 坑四：内置目录会多出一层「只含\u0026lt;ul\u0026gt;的空\u0026lt;li\u0026gt;」。 两种情况会出现这种空壳：一是正文最小标题层级高于startLevel（本站startLevel: 1，多数文章从h2起始），二是标题跳级（比如h2直接跳到h4）。它们会让内层列表多吃一次.toc li ul的缩进。用:only-child正好只命中这两层壳——真正的目录条目里必然带\u0026lt;a\u0026gt;，不会是only-child：\n1 2 3 .toc .inner li \u0026gt; ul:only-child { margin-inline-start: 0; } 1.4 阅读进度条（layouts/partials/footer.html） 进度条没单独做组件，直接塞进「回到顶部」按钮的\u0026lt;span id=\u0026quot;read_progress\u0026quot;\u0026gt;里：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;a href=\u0026#34;#top\u0026#34; aria-label=\u0026#34;go to top\u0026#34; title=\u0026#34;Go to Top (Alt + G)\u0026#34; class=\u0026#34;top-link\u0026#34; id=\u0026#34;top-link\u0026#34; accesskey=\u0026#34;g\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;topInner\u0026#34;\u0026gt; \u0026lt;svg class=\u0026#34;topSvg\u0026#34; xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; viewBox=\u0026#34;0 0 12 6\u0026#34; fill=\u0026#34;currentColor\u0026#34;\u0026gt; \u0026lt;path d=\u0026#34;M12 6H0l6-6z\u0026#34;/\u0026gt; \u0026lt;/svg\u0026gt; \u0026lt;span id=\u0026#34;read_progress\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;/span\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;script\u0026gt; document.addEventListener(\u0026#39;scroll\u0026#39;, function (e) { const readProgress = document.getElementById(\u0026#34;read_progress\u0026#34;); 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); }) \u0026lt;/script\u0026gt; 百分比 = 已滚动 /（文档总高 − 视口高），取整后写进按钮。\n「回到顶部」按钮的点击处理不在这个文件里，而在toc.html（跟目录高亮的脚本放在一起）：接管#top-link，用scrollTo做平滑回顶。\n1 2 3 4 5 6 7 const topLink = document.getElementById(\u0026#39;top-link\u0026#39;); if (topLink) { topLink.addEventListener(\u0026#39;click\u0026#39;, (event) =\u0026gt; { event.preventDefault(); window.scrollTo({ top: 0, behavior: \u0026#39;smooth\u0026#39; }); }); } 正文里的锚点跳转则是主题footer.html自带的接管逻辑，它判断了prefers-reduced-motion：系统开启「减少动态效果」时用不带参数的scrollIntoView()直接跳转，否则才用behavior: \u0026quot;smooth\u0026quot;。\n1.5 宽屏浮出的判定（layouts/single.html） 左右两侧（左侧目录#toc-container、右侧标签栏#side-aside）必须同时进入/退出.wide，否则会出现一边浮出、一边还留在文档流里的不对称。所以判定只留一份，两个元素在同一次遍历里一起切：\n1 2 3 4 5 6 7 8 9 10 function updateAsideWide() { const wide = (document.body.scrollWidth - main - (toc * 2) - (gap * 4)) \u0026gt; 0; [\u0026#39;toc-container\u0026#39;, \u0026#39;side-aside\u0026#39;].forEach((id) =\u0026gt; { const aside = document.getElementById(id); if (aside) aside.classList.toggle(\u0026#39;wide\u0026#39;, wide); }); } document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, updateAsideWide); window.addEventListener(\u0026#39;resize\u0026#39;, updateAsideWide); 判断条件就是「文档宽 − 文章宽 − 2 × 侧边栏宽 − 4 × 间距 \u0026gt; 0」，不满足（窄屏）时两侧侧边栏一起退回文档流。\n1.6 小结 侧边目录这套本质是「absolute外框 + sticky内容」，内容交给Hugo内置目录后，模板只剩外框和几段滚动脚本。几个容易漏的点：空目录框要判\u0026lt;ul\u0026gt;、中文锚点要解码、目录链接取值要判空，以及内置目录那层空壳\u0026lt;li\u0026gt;的缩进。\n2 代码高亮配置 在配置代码高亮时，我发现如果生成syntax-light.css和syntax-dark.css两个css文件，会导致代码高亮配置失效，并且之前配置好的侧边目录也会失效，回到文章正上方。\n因此我将两个css文件合并为一个syntax.css，`并且精简了css代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 /* ============================================================ Syntax Highlighting Light theme → GitHub style Dark theme → OneDark style ============================================================ */ /* ---- Common: Structure \u0026amp; layout ---- */ .chroma { -webkit-text-size-adjust: none; } .chroma .lnlinks { outline: none; text-decoration: none; color: inherit; } .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } .chroma .lnt, .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em; } .chroma .line { display: flex; } /* ============================================================ Light Theme — GitHub ============================================================ */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma { background-color: #f7f7f7; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .bg { background-color: #f7f7f7; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .lnt, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .ln { color: #7f7f7f; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .hl { background-color: #dedede; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .err { color: #f6f8fa; background-color: #82071e; } /* Light: Keywords */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .k, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .kc, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .kd, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .kn, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .kp, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .kr, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .kt { color: #cf222e; } /* Light: Names */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .na, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nc, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nx { color: #1f2328; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .no, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nd, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nt { color: #0550ae; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .ni, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nb, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nf, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .fm { color: #6639ba; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nl { color: #900; font-weight: bold; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nn { color: #24292e; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .bp { color: #6a737d; } /* Light: Variables */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .nv, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .vc, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .vg, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .vi, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .vm { color: #953800; } /* Light: Strings */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .s, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .sa, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .sb, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .sc, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .dl, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .sd, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .s2, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .se, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .sh, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .si, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .sx, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .sr, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .s1 { color: #0a3069; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .ss { color: #032f62; } /* Light: Numbers */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .m, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .mb, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .mf, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .mh, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .mi, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .il, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .mo { color: #0550ae; } /* Light: Operators */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .o, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .ow, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .or { color: #0550ae; } /* Light: Punctuation */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .p { color: #1f2328; } /* Light: Comments */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .c, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .ch, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .cm, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .c1, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .cs, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .cp, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .cpf { color: #57606a; } /* Light: Generic */ :root[data-theme=\u0026#34;light\u0026#34;] .chroma .gd { color: #82071e; background-color: #ffebe9; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .ge, :root[data-theme=\u0026#34;light\u0026#34;] .chroma .go { color: #1f2328; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .gi { color: #116329; background-color: #dafbe1; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .gl { text-decoration: underline; } :root[data-theme=\u0026#34;light\u0026#34;] .chroma .w { color: #fff; } /* ============================================================ Dark Theme — OneDark ============================================================ */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma { color: #abb2bf; background-color: #282c34; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .bg { color: #abb2bf; background-color: #282c34; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .lnt, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .ln { color: #55595f; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .hl { background-color: #3d4148; } /* Dark: Keywords */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .k, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .kd, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .kn, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .kp, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .kr { color: #c678dd; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .kc, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .kt { color: #e5c07b; } /* Dark: Names */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .n, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .na, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .no, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .ni, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .ne, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nl, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nn, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nx, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .py, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nt { color: #e06c75; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nc, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nb, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .bp { color: #e5c07b; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nd { color: #61afef; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nf { color: #61afef; font-weight: bold; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .fm { color: #56b6c2; font-weight: bold; } /* Dark: Variables */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .nv, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .vc, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .vg, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .vi, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .vm { color: #e06c75; } /* Dark: Strings */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .s, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .sa, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .sb, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .sc, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .dl, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .sd, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .s2, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .se, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .sh, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .si, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .sx, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .sr, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .s1, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .ss, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .gi { color: #98c379; } /* Dark: Numbers */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .m, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .mb, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .mf, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .mh, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .mi, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .il, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .mo { color: #d19a66; } /* Dark: Operators */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .o, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .ow, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .or { color: #56b6c2; } /* Dark: Punctuation */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .p { color: #abb2bf; } /* Dark: Comments */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .c, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .ch, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .cm, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .c1, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .cs, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .cp, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .cpf { color: #7f848e; } /* Dark: Generic */ :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .gd { color: #e06c75; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma .gi { font-weight: bold; } 顺便说一下，我用的配色方案是github+onedark。\n3 首页头像剪裁 首页头像想做成圆形、不变形。尺寸由hugo.yaml的profileMode控制（主题默认是 150，我改成了 120）：\n1 2 3 4 profileMode: imageUrl: \u0026#34;/avatar.jpg\u0026#34; imageWidth: 120 imageHeight: 120 CSS 只负责裁剪成圆形，在assets/css/extended/custom.css中添加：\n1 2 3 4 5 /* 首页头像 - 保持比例裁剪，不变形 */ .profile img { border-radius: 50%; object-fit: cover; } 主题的index_profile.html会按imageWidth × imageHeight缩放图片，并把尺寸写到img的width / height属性上，所以尺寸交给配置即可，CSS 里不必再写死width / height。\n4 导航栏图标 在hugo.yaml的导航配置中name字段后面添加图标，例如：\n1 2 3 4 - identifier: home name: \u0026#34;🏠 首页\u0026#34; url: / weight: 10 5 分页配置 5.1 配置文件：hugo.yaml 5.1.1 分页配置统一（修复\u0026quot;修改 paginate 不生效\u0026quot;） 问题：paginate: 5（已弃用）与 pagination.pagerSize: 10 同时存在，新版 Hugo 以 pagination.pagerSize 为准，导致每页 10 篇、9 篇文章只有 1 页，翻页按钮不显示。\n修改：\n删除顶部 paginate: 5 使用 pagination.pagerSize: 5 1 2 pagination: pagerSize: 5 5.1.2 清理重复配置键（修复\u0026quot;分享按钮偶尔出现\u0026quot;） 问题：params 下showShareButtons: true与ShowShareButtons: false是同一键（Hugo 键不区分大小写），值冲突且 Hugo 合并顺序随机，导致分享按钮有时显示有时不显示。showPageNums与ShowPageNums也是同理。\n修改：删除重复定义，每项只保留一处。目前 params 下只留下：\n1 2 params: ShowShareButtons: false 另外showPageNums后来也一并删掉了：自定义分页（见 5.2）用自己的页码渲染，不再依赖主题的页码开关，留着是个无效配置。\n5.2 模板文件：layouts/list.html（新建，覆盖主题） 5.2.1 数字页码 + 省略号 用$paginator.Pagers渲染完整页码，通过$window控制当前页前后显示的页码数，超出范围用省略号替代。$window: 3表示当前页前后各保留 3 页。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 {{- if gt $paginator.TotalPages 1 }} \u0026lt;footer class=\u0026#34;page-footer\u0026#34;\u0026gt; \u0026lt;nav class=\u0026#34;pagination\u0026#34;\u0026gt; {{- if $paginator.HasPrev }} \u0026lt;a class=\u0026#34;prev\u0026#34; href=\u0026#34;{{ $paginator.Prev.URL | absURL }}\u0026#34;\u0026gt;«\u0026lt;/a\u0026gt; {{- end }} {{- $current := $paginator.PageNumber }} {{- $total := $paginator.TotalPages }} {{- $window := 3 }} {{- $prevShown := 0 }} {{- range $paginator.Pagers }} {{- $pn := .PageNumber }} {{- $show := or (eq $pn 1) (eq $pn $total) (and (ge $pn (sub $current $window)) (le $pn (add $current $window))) }} {{- if $show }} {{- if and $prevShown (gt (sub $pn $prevShown) 1) }} \u0026lt;span class=\u0026#34;page-dots\u0026#34;\u0026gt;…\u0026lt;/span\u0026gt; {{- end }} \u0026lt;a class=\u0026#34;{{ if eq $pn $current }}active{{ end }}\u0026#34; href=\u0026#34;{{ .URL | absURL }}\u0026#34;\u0026gt;{{ $pn }}\u0026lt;/a\u0026gt; {{- $prevShown = $pn }} {{- end }} {{- end }} {{- if $paginator.HasNext }} \u0026lt;a class=\u0026#34;next\u0026#34; href=\u0026#34;{{ $paginator.Next.URL | absURL }}\u0026#34;\u0026gt;»\u0026lt;/a\u0026gt; {{- end }} \u0026lt;/nav\u0026gt; \u0026lt;/footer\u0026gt; {{- end }} 5.2.2 修复构建报错 unexpected {{end}} 问题：替换分页代码时多了一个 {{- end }}，报错 baseof.html apply base template failed unexpected {{end}}。\n修改：删除多余的 {{- end }}，保证尾部结构为：\n1 2 3 {{- end }} ← 分页 if 结束 {{- end }}{{/* end profileMode */}} ← profileMode if 结束 {{- end }}{{- /* end main */ -}} ← define main 结束 5.3 样式文件：assets/css/extended/custom.css 在文件末尾追加分页样式（最终版）：\n布局：« 上一页绝对定位钉在左端、下一页 »钉在右端，页码组justify-content: center始终居中（用绝对定位解决\u0026quot;第一页/最后一页页码被顶到两边\u0026quot;的问题） 按钮：极简扁平，浅色底 + 细边框 + 圆角，hover 加深；当前页实心反白 省略号：.page-dots淡色不可点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 /* ===== 分页按钮：页码始终居中，上下页固定在两端 ===== */ .page-footer { margin-top: 24px; padding-top: 20px; border-top: 1px dashed var(--border); } .pagination { position: relative; display: flex; justify-content: center; align-items: center; gap: 6px; } .pagination a { display: inline-flex; align-items: center; justify-content: center; min-width: 36px; height: 36px; padding: 0 14px; font-size: 14px; color: var(--primary); background: var(--entry); border: 1px solid var(--border); border-radius: 8px; transition: background 0.25s ease, color 0.25s ease, border-color 0.25s ease; } .pagination a:hover { background: var(--tertiary); border-color: var(--secondary); } .pagination a.active { background: var(--primary); border-color: var(--primary); color: var(--theme); cursor: default; pointer-events: none; } /* 上一页/下一页：绝对定位钉在两端，不影响页码居中 */ .pagination .prev { position: absolute; left: 0; top: 50%; transform: translateY(-50%); min-width: auto; padding: 0 18px; } .pagination .next { position: absolute; right: 0; top: 50%; transform: translateY(-50%); min-width: auto; padding: 0 18px; } /* 文字（不想要就删掉这两段） */ .pagination .prev::after { content: \u0026#34;上一页\u0026#34;; margin-inline-start: 6px; } .pagination .next::before { content: \u0026#34;下一页\u0026#34;; margin-inline-end: 6px; } /* 分页省略号 */ .pagination .page-dots { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 36px; font-size: 15px; color: var(--secondary); user-select: none; } 6 字体与纸张配色 6.1 字体加载（extend_head.html） 正文用衬线字体（Lora + 思源宋体），代码用等宽字体（Source Code Pro）。字体走 Google Fonts，layouts/partials/extend_head.html 里用 preload 先拿下来、onload 时再切成 stylesheet，不阻塞首屏渲染：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;link rel=\u0026#34;preconnect\u0026#34; href=\u0026#34;https://fonts.googleapis.com\u0026#34; /\u0026gt; \u0026lt;link rel=\u0026#34;preconnect\u0026#34; href=\u0026#34;https://fonts.gstatic.com\u0026#34; crossorigin /\u0026gt; \u0026lt;link rel=\u0026#34;preload\u0026#34; href=\u0026#34;https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700\u0026amp;family=Noto+Serif+SC:wght@400;600;700\u0026amp;display=swap\u0026#34; as=\u0026#34;style\u0026#34; onload=\u0026#34; this.onload = null; this.rel = \u0026#39;stylesheet\u0026#39;; \u0026#34; /\u0026gt; \u0026lt;noscript\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700\u0026amp;family=Noto+Serif+SC:wght@400;600;700\u0026amp;display=swap\u0026#34; /\u0026gt; \u0026lt;/noscript\u0026gt; 再到assets/css/extended/custom.css里指定字体族：\n1 2 3 4 5 6 7 8 9 10 11 body, .post-content { font-family: Lora, \u0026#34;Noto Serif SC\u0026#34;, \u0026#34;PingFang SC\u0026#34;, \u0026#34;Microsoft YaHei\u0026#34;, sans-serif; } code, pre, kbd, samp { font-family: \u0026#34;Source Code Pro\u0026#34;, monospace; } preload → stylesheet的切换要有noscript兜底：脚本被禁用时，字体样式仍然会正常加载。\n6.2 纸张色体系 主题默认浅色底是纯白。我把全站换成一套「纸张色」，同时解决首页与文章页底色不一致的问题（PaperMod 会给body.list套一层code-bg浅灰，而文章详情页是--theme纯白）：\n1 2 3 4 5 6 7 8 9 10 /* 让列表页跟随 --theme，再把 --theme 改成暖纸张色 */ .list { background: var(--theme); } :root { --footer-height: 84px; /* 页脚高度，404 页算高度时要引用 */ --theme: #f7f2e9; /* 页面底：暖纸张色 */ --entry: #fdfaf3; /* 卡片底：比页面略亮一档的纸张色 */ } 深色模式不是单纯的黑，而是一组「昏暗米纸」暖灰：\n1 2 3 4 5 6 7 8 9 10 11 :root[data-theme=\u0026#34;dark\u0026#34;] { --theme: #262421; /* 页面底：昏暗米灰 */ --entry: #2f2c27; /* 卡片/代码块底：略亮的米灰 */ --primary: #ded5c4; /* 标题与默认文字：米白 */ --content: #d3c9b5; /* 正文文字 */ --secondary: #a89d8a; /* 次要文字（时间、标签等） */ --tertiary: #403c33; /* hover 底色/按钮底 */ --code-block-bg: #2f2c27; --code-bg: #37332b; --border: #3d3a31; /* 分隔线/描边 */ } 之后所有自定义样式（侧边栏、卡片、分页、菜单悬浮面板、404 页）都复用这几个变量，明暗两边一起生效，不用为暗色单独写一套。\n6.3 顺带补一下行内代码颜色 第 2 节的代码高亮只管.chroma里的 token，没有语言标注的行内代码（fallback / text / bash）在浅色下会偏亮、看不清，单独补一条：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 :root[data-theme=\u0026#34;light\u0026#34;] .chroma code.language-fallback, :root[data-theme=\u0026#34;light\u0026#34;] .chroma code.language-text, :root[data-theme=\u0026#34;light\u0026#34;] .chroma code.language-bash, :root[data-theme=\u0026#34;light\u0026#34;] .chroma code[data-lang=\u0026#34;fallback\u0026#34;], :root[data-theme=\u0026#34;light\u0026#34;] .chroma code[data-lang=\u0026#34;text\u0026#34;], :root[data-theme=\u0026#34;light\u0026#34;] .chroma code[data-lang=\u0026#34;bash\u0026#34;] { color: #24292f; } :root[data-theme=\u0026#34;dark\u0026#34;] .chroma code.language-fallback, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma code.language-text, :root[data-theme=\u0026#34;dark\u0026#34;] .chroma code[data-lang=\u0026#34;fallback\u0026#34;], :root[data-theme=\u0026#34;dark\u0026#34;] .chroma code[data-lang=\u0026#34;text\u0026#34;] { color: #f8f8f2; } :root[data-theme=\u0026#34;light\u0026#34;] { --code-block-bg: #f8f8f8; } 7 页脚：备案与访问统计 7.1 备案信息注入（extend_footer.html） 备案信息配置写在hugo.yaml的params.footer下面：\n1 2 3 4 5 6 7 footer: # ICP 备案号；有值时会在页脚显示并链接到工信部备案查询 icp: \u0026#34;京ICP备2025140550号-1\u0026#34; # 公安备案文案；没有就留空 mps: \u0026#34;\u0026#34; # 公安备案号里的数字编码；没有就留空，模板里也会尝试从 mps 文案提取 mpsCode: \u0026#34;\u0026#34; 渲染放在layouts/partials/extend_footer.html，不直接改页脚结构，而是把备案项当数据交给一小段脚本插进去。两个细节：\n公安备案的链接需要号里的数字编码，优先取mpsCode，没配就从mps文案里用findRE \u0026quot;[0-9]{6,}\u0026quot;抠出来； 数据用jsonify生成 JSON，script属于 JS 上下文，必须用safeJS而不是safeHTML（否则会被转义，或让 Go 模板语法混进 JS 触发 linter 误报）。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {{- $mpsCode := $footer.mpsCode -}} {{- if and $footer.mps (not $mpsCode) -}} {{- $found := findRE \u0026#34;[0-9]{6,}\u0026#34; $footer.mps -}} {{- if $found }}{{ $mpsCode = index $found 0 }}{{ end -}} {{- end -}} {{- $beianItems := slice -}} {{- with $footer.icp -}} {{- $beianItems = $beianItems | append (dict \u0026#34;text\u0026#34; . \u0026#34;href\u0026#34; \u0026#34;https://beian.miit.gov.cn/\u0026#34;) -}} {{- end -}} {{- with $footer.mps -}} {{- $href := \u0026#34;\u0026#34; -}} {{- with $mpsCode }}{{ $href = printf \u0026#34;https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=%s\u0026#34; . }}{{ end -}} {{- $beianItems = $beianItems | append (dict \u0026#34;text\u0026#34; . \u0026#34;href\u0026#34; $href) -}} {{- end -}} {{- if $beianItems }} \u0026lt;script type=\u0026#34;application/json\u0026#34; id=\u0026#34;beian-items\u0026#34;\u0026gt;{{ $beianItems | jsonify | safeJS }}\u0026lt;/script\u0026gt; {{- end }} 脚本端把每一项插到页脚「Powered by」那句之前，并用·分隔：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const powered = footer.querySelector(\u0026#34;span:last-of-type\u0026#34;); beianItems.forEach((item) =\u0026gt; { const beian = document.createElement(\u0026#34;span\u0026#34;); beian.className = \u0026#34;footer-beian\u0026#34;; const link = document.createElement(\u0026#34;a\u0026#34;); link.href = item.href; link.target = \u0026#34;_blank\u0026#34;; link.rel = \u0026#34;noopener noreferrer\u0026#34;; link.textContent = item.text; beian.append(link); if (powered) { powered.before(beian, document.createTextNode(\u0026#34; · \u0026#34;)); } else { footer.append(document.createTextNode(\u0026#34; · \u0026#34;), beian); } }); 顺带说一下，--footer-height: 84px（见 6.2）就是给 404 页算min-height用的——404 页要撑满「视口 − 导航栏 − 页脚」的高度，页脚高度得有个变量可引用。\n7.2 不蒜子：站点 PV/UV + 文章阅读量 页脚统计（本站总访问量 / 访客数）同样在extend_footer.html：先把两个span插到页脚最前面，再引入不蒜子的脚本（顺序不能反，脚本要能接管这两个span）：\n1 2 3 4 const stats = document.createElement(\u0026#34;div\u0026#34;); stats.className = \u0026#34;footer-busuanzi\u0026#34;; stats.innerHTML = [\u0026#39;\u0026lt;span id=\u0026#34;busuanzi_container_site_pv\u0026#34;\u0026gt;本站总访问量 \u0026lt;span id=\u0026#34;busuanzi_value_site_pv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 次\u0026lt;/span\u0026gt;\u0026#39;, \u0026#39;\u0026lt;span id=\u0026#34;busuanzi_container_site_uv\u0026#34;\u0026gt;本站访客数 \u0026lt;span id=\u0026#34;busuanzi_value_site_uv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 人\u0026lt;/span\u0026gt;\u0026#39;].join(\u0026#34; · \u0026#34;); footer.prepend(stats); 1 \u0026lt;script async src=\u0026#34;https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 文章页的阅读量单独放在一个 partiallayouts/partials/busuanzi_page_pv.html里，挂在single.html的post-meta中（跟在时间后面）：\n1 2 3 4 5 6 {{- if not (.Param \u0026#34;hidePageViews\u0026#34;) -}} {{- if or (not .Date.IsZero) (.Param \u0026#34;ShowReadingTime\u0026#34;) (.Param \u0026#34;ShowWordCount\u0026#34;) -}} {{- printf \u0026#34;\u0026amp;nbsp;·\u0026amp;nbsp;\u0026#34; | safeHTML -}} {{- end -}} \u0026lt;span id=\u0026#34;busuanzi_container_page_pv\u0026#34;\u0026gt;阅读量 \u0026lt;span id=\u0026#34;busuanzi_value_page_pv\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 次\u0026lt;/span\u0026gt; {{- end -}} 前面那个·分隔符要判断一下：只有前面确实有日期 / 字数 / 阅读时间时才补，否则标题下面会孤零零挂一个间隔符。.Param \u0026quot;hidePageViews\u0026quot;可以在单篇文章里关掉阅读量。\n7.3 Umami 站点统计用 Umami，一行 script 挂在页脚：\n1 \u0026lt;script defer src=\u0026#34;https://cloud.umami.is/script.js\u0026#34; data-website-id=\u0026#34;f2531744-6410-447b-8b1f-37b5bc11170a\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 8 文章列表：封面右置 /posts/列表卡片默认是封面在上、文字在下。改成封面在右、文字在左，用 grid 把左侧文字和右侧封面分成两列：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 .post-entry { display: grid; grid-template-columns: minmax(0, 1fr) minmax(160px, 32%); column-gap: calc(var(--gap) * 1.5); align-items: center; } .post-entry .entry-header, .post-entry .entry-content, .post-entry .entry-footer { grid-column: 1; min-width: 0; } .post-entry .entry-header { grid-row: 1; } .post-entry .entry-content { grid-row: 2; } .post-entry .entry-footer { grid-row: 3; } .post-entry .entry-cover { grid-column: 2; grid-row: 1 / span 3; /* 封面跨三行，和左侧文字整体垂直居中 */ margin-bottom: 0; align-self: center; } .post-entry .entry-cover img { width: 100%; height: auto; } 小屏恢复默认竖排，避免图片把文字挤成一条：\n1 2 3 4 5 6 7 8 9 @media (max-width: 680px) { .post-entry { display: block; } .post-entry .entry-cover { margin-bottom: var(--gap); } } 样式放在assets/css/extended/post-cover-right.css，只命中.post-entry，不影响 profileMode 首页。\n9 自定义关于页 content/about.md用layout: \u0026quot;about\u0026quot;指定自定义模板，并在 frontmatter 里关掉文章页的一堆默认元素：\n1 2 3 4 5 6 7 8 9 title: \u0026#34;关于我\u0026#34; layout: \u0026#34;about\u0026#34; url: \u0026#34;/about/\u0026#34; description: \u0026#34;一个热衷于折腾技术与记录生活的普通人。\u0026#34; ShowToc: false # 不显示目录 ShowBreadCrumbs: false # 不显示面包屑 hideMeta: true # 不显示日期、字数、阅读时间 ShowPostNavLinks: false # 不显示上一篇/下一篇 disableAnchoredHeadings: true # 标题不加锚点 模板layouts/about.html比文章页简单得多——只有面包屑、标题、描述和正文，不带封面、目录、侧边栏、评论区：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {{- define \u0026#34;main\u0026#34; }} \u0026lt;article class=\u0026#34;post-single about-page\u0026#34;\u0026gt; \u0026lt;header class=\u0026#34;post-header about-header\u0026#34;\u0026gt; {{ partial \u0026#34;breadcrumbs.html\u0026#34; . }} \u0026lt;h1 class=\u0026#34;post-title entry-hint-parent\u0026#34;\u0026gt;{{ .Title }}\u0026lt;/h1\u0026gt; {{- if .Description }} \u0026lt;div class=\u0026#34;post-description\u0026#34;\u0026gt;{{ .Description }}\u0026lt;/div\u0026gt; {{- end }} \u0026lt;/header\u0026gt; {{- if .Content }} \u0026lt;div class=\u0026#34;post-content md-content\u0026#34;\u0026gt;{{- if not (.Param \u0026#34;disableAnchoredHeadings\u0026#34;) }} {{- partial \u0026#34;anchored_headings.html\u0026#34; .Content -}} {{- else }}{{ .Content }}{{ end }}\u0026lt;/div\u0026gt; {{- end }} \u0026lt;/article\u0026gt; {{- end }} 样式assets/css/extended/about.css做了几件事：正文收窄到 760px、标题居中放大、正文首图（头像）做成圆形描边头像：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .about-page { max-width: 760px; } .about-page .about-header { margin-bottom: 18px; text-align: center; } .about-page .post-content \u0026gt; p:first-child img { width: 136px; height: 136px; margin: 0 auto; border: 4px solid var(--entry); border-radius: 50%; box-shadow: 0 18px 42px rgba(15, 23, 42, 0.16); object-fit: cover; } 再用两张网格卡片排「内容方向」和「联系我」，小屏自动收成一列：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 .about-page .about-topics, .about-page .about-links { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 22px 0 10px; } .about-page .topic-card, .about-page .link-card { background: var(--entry); border: 1px solid var(--border); border-radius: var(--radius); transition: border-color 0.25s ease, transform 0.25s ease, background 0.25s ease; } .about-page .topic-card:hover, .about-page .link-card:hover { border-color: var(--secondary); background: var(--tertiary); transform: translateY(-2px); } @media (max-width: 640px) { .about-page .about-topics, .about-page .about-links { grid-template-columns: 1fr; } } 卡片直接复用--entry底 + --border描边，hover 上浮 2px，和右侧侧边栏卡片是同一套观感。\n10 回到顶部按钮与卡片阴影 回到顶部按钮被改成了「三角箭头 + 阅读进度数字」的小卡片，样式在assets/css/extended/top.css：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .topInner { display: grid; align-items: baseline; justify-items: center; margin: 7px; font-weight: 900; } .topSvg { width: 20px; } .top-link { padding: unset; background: var(--entry); 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%); } 按钮里的数字就是第 1.4 节那段脚本写进.topInner的#read_progress。\n另外把文章列表卡片和侧边栏卡片的阴影统一了（放在toc.css末尾）：\n1 2 3 4 5 6 7 8 .post-entry { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10); } /* 暗色模式下阴影不明显，适当加深 */ [data-theme=\u0026#34;dark\u0026#34;] .post-entry { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); } 11 首页打字机：参数改为自动生成 首页标题的打字机特效（详见《首页打字机特效手机端显示不全修复》）后来做了一次改进，和本节的字体加载同在extend_head.html里。\n原来的问题是：width和steps()里的字符数要跟标题字数手动对齐，标题一改就得回去改 CSS。现在改成按profileMode.title的实际字数自动注入 CSS 变量（1 个全角字按 1em 算）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 {{- if .IsHome }} {{- with site.Params.profileMode }} {{- if .enabled }} {{- $chars := strings.RuneCount (.title | default site.Title) }} \u0026lt;style\u0026gt; .profile .profile_inner h1 { --type-chars: {{ $chars }}; --type-ease: steps({{ $chars }}, end); } \u0026lt;/style\u0026gt; {{- end }} {{- end }} {{- end }} custom.css里的--type-chars: 9和steps(9, end)退化成兜底值，改标题后不用再手改：\n1 2 3 4 5 6 7 8 9 10 11 .profile .profile_inner h1 { --type-chars: 9; --type-ease: steps(9, end); max-width: 100%; width: calc(var(--type-chars) * 1em); overflow: hidden; white-space: nowrap; border-right: 1px solid currentColor; animation: profile-title-typing calc(var(--type-chars) * 0.24s) var(--type-ease), profile-title-caret 0.75s step-end infinite; } 动画时长也一并改成按字数算（calc(var(--type-chars) * 0.24s)），保持每个字约 0.24s 的节奏。\n参考文献 周鑫的个人博客：在PaperMod中引入侧边目录和阅读进度显示\n问道凌虚：Hugo折腾记（站点已无法访问）\n","permalink":"https://www.wkun.top/posts/hugo-papermod-%E8%87%AA%E5%AE%9A%E4%B9%89/","summary":"\u003cp\u003e这一部分主要是跟着问道凌虚的\u003ca href=\"https://www.zeyes.org/posts/tech/hugo-papermod-customization\"\u003e《Hugo折腾记》\u003c/a\u003e来做的（注：该站点目前已无法访问，链接仅作出处记录）。\u003c/p\u003e\n\u003ch2 id=\"下面是我自行修改的配置\"\u003e下面是我自行修改的配置\u003c/h2\u003e\n\u003ch3 id=\"1-侧边目录及阅读进度条\"\u003e1 侧边目录及阅读进度条\u003c/h3\u003e\n\u003cp\u003e侧边目录最初参考了\u003ca href=\"https://www.zhouxin.space/logs/introduce-side-toc-and-reading-percentage-to-papermod/\"\u003e在PaperMod中引入侧边目录和阅读进度显示\u003c/a\u003e，基本没有修改。后来把目录内容换成了Hugo内置的\u003ccode\u003e.TableOfContents\u003c/code\u003e（由Hugo直接生成嵌套\u003ccode\u003e\u0026lt;ul\u0026gt;\u003c/code\u003e），模板只保留外框和滚动高亮；阅读进度条则挂在「回到顶部」按钮上。下面记录最终的做法。\u003c/p\u003e\n\u003ch4 id=\"11-配置hugoyaml\"\u003e1.1 配置（hugo.yaml）\u003c/h4\u003e\n\u003cp\u003e目录收录的标题层级交给Hugo内置目录控制，保持h1~h6全收（我站内有文章用\u003ccode\u003e#\u003c/code\u003e当章节标题）：\u003c/p\u003e","title":"Hugo PaperMod 自定义"},{"content":"一、本地生成ssh_key 1 ssh-keygen -t ed25519 -C \u0026#34;yourname\u0026#34; 一路回车，不设置 passphrase（如果设置了密码短语，push 时还是要输一次，不能算完全免密）。\n生成后密钥文件在：\n1 2 3 C:\\Users\\你的用户名\\.ssh\\id_ed25519 # 私钥，千万别泄露 C:\\Users\\你的用户名\\.ssh\\id_ed25519.pub # 公钥，我们要用它 查看公钥内容：\n1 cat ~/.ssh/id_ed25519.pub 二、服务器：把公钥加进 git 用户的 authorized_keys 1、登录服务器，找到 git 用户的家目录，进入 .ssh 目录：\n1 ssh git@yourserver.com 2、在服务器上执行\n1 2 3 4 5 6 7 # 如果还没有 .ssh 目录 mkdir -p ~/.ssh chmod 700 ~/.ssh # 编辑 authorized_keys，把刚才复制的公钥粘贴进去 vim ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys 三、本地：测试免密登录 1 ssh git@yourserver.com 登陆成功，说明免密登录设置成功。\n四、本地：测试免密 push 1 git push origin master 如果出现 Permission denied (publickey).，说明没有成功，需要检查一下公钥是否正确添加到服务器上。\n如果出现 Everything up-to-date，说明成功，以后就可以免密 push 了。\n","permalink":"https://www.wkun.top/posts/blog%E5%85%8D%E5%AF%86push/","summary":"\u003ch1 id=\"一本地生成ssh_key\"\u003e一、本地生成ssh_key\u003c/h1\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-powershell\" data-lang=\"powershell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003essh-keygen\u003c/span\u003e \u003cspan class=\"n\"\u003e-t\u003c/span\u003e \u003cspan class=\"n\"\u003eed25519\u003c/span\u003e \u003cspan class=\"n\"\u003e-C\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;yourname\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e一路回车，不设置 passphrase（如果设置了密码短语，push 时还是要输一次，不能算完全免密）。\u003c/p\u003e\n\u003cp\u003e生成后密钥文件在：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eC:\\Users\\你的用户名\\.ssh\\id_ed25519      # 私钥，千万别泄露\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eC:\\Users\\你的用户名\\.ssh\\id_ed25519.pub  # 公钥，我们要用它\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e查看公钥内容：\u003c/p\u003e","title":"Blog免密push"},{"content":" 适用环境：Ubuntu / Debian / CentOS\n服务器配置：2核2G 及以上\n难度：中等\n💡 提示：如果不想用 Nginx，或想省去证书续期的麻烦，可以参考本站的《Caddy替换Nginx反向代理指南》——它同样适用于本文搭建的 Git 自动部署流程，只是把托管环节换成 Caddy。\n一、架构原理 这套方案的核心思想是分离关注点：\n本地：负责写作（Markdown）和版本管理（Git）。\n服务器：负责接收代码、构建（Hugo）、托管（Nginx）。\n自动化：通过Git Hooks实现push代码即发布。\n数据流：\n本地 git push→服务器 Git 裸仓库→触发 post-receive 钩子→拉取代码并 Hugo 构建→Nginx 展示静态页面\n二、服务器端环境准备 1. 安装基础软件 1 2 3 4 5 6 7 # Ubuntu / Debian sudo apt update sudo apt install -y nginx git # CentOS / Rocky sudo yum install -y nginx git sudo systemctl enable --now nginx 2. 安装 Hugo（Extended 版） Hugo是单文件二进制程序，直接下载即可。\n1 2 3 4 5 6 7 8 # 访问 https://github.com/gohugoio/hugo/releases 查看最新版本 # 以 Linux 64位 Extended 版为例 wget https://github.com/gohugoio/hugo/releases/download/v0.147.8/hugo_extended_0.147.8_linux-amd64.tar.gz tar -zxvf hugo_extended_*_linux-amd64.tar.gz sudo mv hugo /usr/local/bin/ # 验证安装 hugo version 3. 创建专用用户（安全推荐） 为了安全，不建议直接使用root进行git操作。\n1 2 sudo adduser git sudo su - git 三、服务器端 Git 配置 1. 创建裸仓库（Bare Repo） 裸仓库仅存储版本信息，不直接显示工作文件。\n1 2 3 4 5 # 以 git 用户身份执行 cd ~ mkdir blog.git cd blog.git git init --bare 2. 创建网站根目录 这是 Nginx 最终指向的目录，也是 Hugo 构建输出的地方。\n1 2 3 4 # 切回 root 或 sudo exit sudo mkdir -p /var/www/blog sudo chown -R git:git /var/www/blog 3. 配置 Git Hooks（核心） 当本地代码推送到服务器时，自动执行构建脚本。\n1 2 3 4 # 切换回 git 用户 sudo su - git cd ~/blog.git/hooks nano post-receive 粘贴以下脚本内容（请仔细检查路径）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash # 定义变量 GIT_REPO=/home/git/blog.git WORK_TREE=/var/www/blog # 清理旧文件（防止缓存干扰） rm -rf $WORK_TREE/* # 设置环境变量 export GIT_WORK_TREE=$WORK_TREE export GIT_DIR=$GIT_REPO # 检出最新代码 git checkout -f # 进入工作目录并构建 cd $WORK_TREE /usr/local/bin/hugo --minify echo \u0026#34;Hugo 构建完成！\u0026#34; 赋予执行权限：\n1 2 chmod +x post-receive exit 四、服务器端 Nginx 配置 编辑Nginx站点配置文件。\n1 sudo nano /etc/nginx/sites-available/default ⚠️ 重点：​ root必须指向 Hugo 生成的 public目录。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server { listen 80; server_name your_domain_or_IP; # 改为你的域名或服务器IP # 关键：必须指向 public 目录 root /var/www/blog/public; index index.html; location / { try_files $uri $uri/ =404; } # 开启 Gzip 压缩（提升访问速度） gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; } 测试并重载配置：\n1 2 sudo nginx -t sudo systemctl reload nginx 五、本地电脑配置 1. 安装 Hugo 确保本地已安装Hugo（用于预览和初始化）。\nWindows: choco install hugo-extended\nmacOS: brew install hugo\n2. 创建新站点 1 2 hugo new site myblog cd myblog 3. 添加主题（以 PaperMod 为例） 使用 Git Submodule 管理主题是最佳实践。\n1 2 git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod echo \u0026#34;theme = \u0026#39;PaperMod\u0026#39;\u0026#34; \u0026gt;\u0026gt; hugo.toml 4. 初始化 Git 并关联远程仓库 1 2 3 4 5 6 # 初始化仓库 git init # 关联服务器仓库 # 注意替换 your_server_ip git remote add origin git@your_server_ip:/home/git/blog.git 5. 修改配置文件 (hugo.toml) 1 2 3 4 baseURL = \u0026#39;http://your_domain_or_IP/\u0026#39; languageCode = \u0026#39;zh-cn\u0026#39; title = \u0026#39;我的自动化博客\u0026#39; theme = \u0026#39;PaperMod\u0026#39; 六、发布第一篇文章 1. 创建文章 1 hugo new content posts/first-post.md 编辑文章，确保draft: false。\n2. 提交并推送（见证奇迹） 1 2 3 4 5 git add . git commit -m \u0026#34;First release\u0026#34; git push origin master git push --recurse-submodules=on-demand origin master # 如果本地默认分支是 main，则使用：git push origin main 现在访问你的服务器 IP 或域名，博客应该已经成功展示！\n七、常见问题排查（FAQ） 1. Nginx 403 Forbidden 原因：文件权限不足。\n解决：\n1 2 3 sudo chown -R www-data:www-data /var/www/blog # Ubuntu/Debian # 或 sudo chown -R nginx:nginx /var/www/blog # CentOS 2. Nginx 404 Not Found 原因：Nginx 配置中root路径错误。\n解决：检查root是否指向了/var/www/blog/public，而不是 /var/www/blog。\n3. Permission denied (publickey) 原因：SSH密钥未配置。\n解决：将本地电脑的~/.ssh/id_rsa.pub内容追加到服务器的 /home/git/.ssh/authorized_keys文件中。\n1 2 # 本地执行快速解决 ssh-copy-id git@your_server_ip 4. 样式丢失（CSS 404） 原因：hugo.toml中的baseURL填写错误。\n解决：确保baseURL末尾包含/，且与访问地址完全一致。\n5. Git Hook 不执行 原因：post-receive文件没有执行权限。\n解决：在服务器上执行chmod +x ~/blog.git/hooks/post-receive。\n八、进阶优化 1. 启用 HTTPS 使用Certbot申请免费SSL 证书。\nbash sudo apt install certbot python3-certbot-nginx sudo certbot \u0026ndash;nginx -d your_domain\n2. 本地预览 写作时使用实时预览，无需每次都push。\n1 hugo server -D 访问http://localhost:1313。\n3. 资源清理 如果修改了主题配置但未生效，尝试在服务器手动清理缓存后重建：\n1 2 3 cd /var/www/blog rm -rf public resources hugo --minify ","permalink":"https://www.wkun.top/posts/hugo+nginx+git%E9%83%A8%E7%BD%B2%E5%8D%9A%E5%AE%A2%E6%8C%87%E5%8D%97/","summary":"\u003cblockquote\u003e\n\u003cp\u003e适用环境：Ubuntu / Debian / CentOS\u003c/p\u003e\n\u003cp\u003e服务器配置：2核2G 及以上\u003c/p\u003e\n\u003cp\u003e难度：中等\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e💡 提示：如果不想用 Nginx，或想省去证书续期的麻烦，可以参考本站的\u003ca href=\"/posts/caddy%E6%9B%BF%E6%8D%A2nginx%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E6%8C%87%E5%8D%97/\"\u003e《Caddy替换Nginx反向代理指南》\u003c/a\u003e——它同样适用于本文搭建的 Git 自动部署流程，只是把托管环节换成 Caddy。\u003c/p\u003e","title":"Hugo+Nginx+Git部署博客指南"},{"content":"\n你好，我是 wkun，一个热衷于折腾技术与记录生活的普通人。这里是我的小站「拾光笔记」，感谢你路过，也欢迎你常来坐坐。\n关于这个博客 在这个信息爆炸的时代，遇到一些有用的或有趣的东西，如果当时没记住，隔一段时间只剩模糊印象。于是我把博客当成一处存放时光的房间：技术心得与踩坑、生活感想与片段，都尽量写下来、留在这里。\n这个博客最初就是为了折腾一套自己的网站——Hugo + Caddy + Git 自动部署，加上 PaperMod 的种种自定义；这段边查边试的经历，也成了站内「博客搭建」系列的素材。\n至于写公开博客而非本地笔记，无非三个理由：\n写作是最好的复习：讲清楚一个知识点，比看懂它难得多。 记录真实的踩坑路径：比起正确的做法，我更想记下我是怎么做对的。 也许能帮到另一个赶路人：我曾在别人的博文里找到答案，也想把这份心意传下去。 这里有什么 站内文章大致围绕以下四个方向展开，随着折腾的继续会不断增多。你也可以通过顶部的分类、标签或系列入口快速浏览：\n网站建设 本站从零搭建的全过程：Hugo + PaperMod 的各种折腾、Nginx / Caddy 反向代理、Git 自动部署、giscus 评论等，其中「博客搭建」系列最为完整。 Linux 日常使用 Arch Linux 的经验：系统与 KDE 的使用技巧、firewalld 防火墙配置等偏实操的笔记。 编程开发 以 Python 为主的学习与实践记录：环境安装管理、Selenium 自动化及排错、VS Code 配置优化等。 生活记录 偶尔摘抄的名句与读书感悟。技术之外，也想为生活留一页温柔的注脚。 我在用什么 系统环境：Arch Linux + KDE 为主力，Windows 为辅 写作建站：Markdown + Hugo + VS Code，源码托管在 Git 私有仓库 服务器：Nginx / Caddy 托管，firewalld 管理防火墙，通过 Git 实现免密自动部署 偶尔折腾：Python 自动化脚本、Selenium、各类命令行小工具 工具只是手段，好用就行。上面这些不是标准答案，只是我当前顺手的组合，未来大概率还会变。\n关于转载与勘误 站内文章均为个人实践记录，写作时力求准确，但环境千差万别，难免有疏漏或过时之处。如果你发现错误，欢迎指正。 文中若引用了他人的文章、图片或代码，我都会尽量注明出处，在此一并致谢。 如需转载，请注明作者与出处，并保留原文链接；若要将本站内容用于商业用途，最好先和我打声招呼。 联系我 如果你对某篇文章有疑问，或想聊聊技术、谈谈生活，欢迎通过以下任一方式找到我：\n@ 邮箱wwkuun@163.com · 最常用的联系方式 G GitHubgithub.com/wwkuun · 源码与零散脚本 B Bilibilispace.bilibili.com/1652605396 · 偶尔更新的视频 T Telegramt.me/wwkuun · 想随便聊聊时 若点击邮件链接没有反应，请直接复制邮箱地址：wwkuun@163.com\n感谢你读到这里。如果站内某篇文章曾帮到你，欢迎在文章下方留言，或给我发一封邮件——那会是我继续写下去的重要动力。\n拾光入笔，岁月留痕。愿我们都能在时间里，留下属于自己的光。\n","permalink":"https://www.wkun.top/about/","summary":"\u003cp\u003e\u003cimg alt=\"wkun 的头像\" loading=\"lazy\" src=\"/avatar.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e你好，我是 \u003cstrong\u003ewkun\u003c/strong\u003e，一个热衷于折腾技术与记录生活的普通人。这里是我的小站「拾光笔记」，感谢你路过，也欢迎你常来坐坐。\u003c/p\u003e\n\u003ch2 id=\"关于这个博客\"\u003e关于这个博客\u003c/h2\u003e\n\u003cp\u003e在这个信息爆炸的时代，遇到一些有用的或有趣的东西，如果当时没记住，隔一段时间只剩模糊印象。于是我把博客当成一处存放时光的房间：技术心得与踩坑、生活感想与片段，都尽量写下来、留在这里。\u003c/p\u003e","title":"关于我"}]