太喜歡倒騰了,博客暫時沒什麼有趣內容我卻打算針對blowfish的主題自定義做個筆記,踩了太多坑了。
(有糾結要不要一個筆記發一篇文,看我這個廢話程度,我覺得…也不是沒可能。)
給文章外鏈加上圖標 #
Stack主題用家可直接參照Kyo老師的博文 Hugo Stack主题装修笔记
,簡潔明瞭相當實用。
不過 Blowfish 和 Stack 有些不同,在此基礎上對 render-link.html 做一些小修改:
<a href="{{ .Destination | safeURL }}"
{{- with .Title -}}
title="{{ . }}"
{{- end }}
{{- if or (strings.HasPrefix .Destination "http:") (strings.HasPrefix .Destination "https:") }} target="_blank"{{ end }}>
{{- .Text | safeHTML -}}
{{ if strings.HasPrefix .Destination "http" }}
<span style="display: inline-block; margin-left: -0.1em; font-size: inherit; line-height: 1;">
<svg width="0.8em" height="0.8em" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg" style="fill: currentColor;">
<path d="m13 3l3.293 3.293l-7 7l1.414 1.414l7-7L21 11V3z" />
<path d="M19 19H5V5h7l-2-2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2h14c1.103 0 2-.897 2-2v-5l-2-2v7z"/>
</svg>
</span>
{{ end }}
</a>
修改點主要在 width 和 height,且增加了display: inline-block;
這一句,使圖標和文字保持在一行。如還需要讓鼠標移動到外鏈時改變顏色,那麽請繼續往下看:
增加鼠標懸停變色效果 #
如果像我一樣需要針對淺色/深色模式做兩種變色效果,第一步,可以先把 render-link.html 代碼修改如下:
<a class="external-link" href="{{ .Destination | safeURL }}"
{{- with .Title }} title="{{ . }}"{{- end }}
{{- if or (strings.HasPrefix .Destination "http://") (strings.HasPrefix .Destination "https://") }} target="_blank" rel="noopener"{{ end }}>
{{- .Text | safeHTML }}
{{- if or (strings.HasPrefix .Destination "http://") (strings.HasPrefix .Destination "https://") }}
<span class="external-icon">
<svg class="external-icon-svg" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="m13 3l3.293 3.293l-7 7l1.414 1.414l7-7L21 11V3z"/>
<path d="M19 19H5V5h7l-2-2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2h14c1.103 0 2-.897 2-2v-5l-2-2v7z"/>
</svg>
</span>
{{- end }}
</a>
這裡主要是兩點:
- 給鏈接、圖標都用了一樣的class,
.external-icon
來控制圖標容器(位置/大小/對齊),.external-icon-svg
控制其中的svg(顏色/懸停/其他效果),當然用其他名字也都可以。 - 用 css 來統一修改顏色及其他樣式。
接下來在你的博客文件夾/assets/css/custom.css 中添加以下代碼:
/* 修改 外链及icon 相关颜色 */
/* 外链图标链接整体样式 */
.external-link {
text-decoration: none;
}
.external-link:hover {
color: rgb(var(--color-neutral-300));/* 浅色模式 */
}
html.dark .external-link:hover {
color: rgb(var(--color-neutral-200));/* 深色模式 */
}
/* 外链图标本体对齐 + 间距 */
.external-icon {
display: inline-block;
margin-left: -0.08em; /* 控制图标和外链的间距 */
line-height: 1;
}
/* SVG 大小和颜色适配文字 */
.external-icon-svg {
width: 0.8em;
height: 0.8em;
fill: currentColor;
transition: fill 0.2s ease;
}
我這裡的兩個顏色值是根據自己的主題配色來的,如果讀者朋友有自己的配色方案,改color屬性即可。
博客整體配色方案的更改可見另一篇博文:
Blog|更改Hugo Blowfish主題配色
指定Hugo時區 #
Hugo 默認是 UTC 時間而不是東八區時間,有時候寫了文章會被hugo判定文章來自「未來」,所以不會顯示在文章列表裡。 在博客文件夾/config/_default/hugo.toml中加入下面這行代碼即可:
timeZone = "Asia/Shanghai"
當然也可以文章加時間戳,但我覺得沒必要,後面要顯示「最後編輯時間」時再修改。
自定義footer文字&渲染修正 #
首先,原本 layouts/partials/footer.html 部分版權信息的代碼是這樣的:
點我展開
{{/* Copyright */}}
{{ if .Site.Params.footer.showCopyright | default true }}
<p class="text-sm text-neutral-500 dark:text-neutral-400">
{{- with replace .Site.Params.copyright "{ year }" now.Year }}
{{ . | markdownify }}
{{- else }}
©
{{ now.Format "2006" }}
{{ .Site.Params.Author.name | markdownify }}
{{- end }}
</p>
{{ end }}
一般情況下,如果什麼都不寫,footer區域會自動顯示為© 2025
。
如要實現©符號前的文字自定義,則需要:
修改hugo.toml #
我在 footer.html 中加了一句 copyright 的內容:
東井 © 2025 · Please Don't Reblog or Repost Without Asking Permission
要實現這樣的效果,只需要在 博客文件夾/config/_default/params.toml 文件末加一句如下代碼即可:
# 修改版权信息
copyright = "網站名稱 © { year } · Please Don't Reblog or Repost Without Asking Permission"
description = "網站描述"
year會自動替換為當前年份,其他部分均可以自定義。
新的問題出現:
Don't
的單引號一度顯示為全角符號的方括號』
,怎麼修改都找不到位置,讓我百思不得其解。
後來我想了一下,大概是因為我的languages.zh.toml
文件中設置了isoCode = "zh"
,又或者是因為我的字體使用順序是繁體>簡體>英文導致默認使用了繁體標點(我已實驗過,字體是可以正常顯示英文符號的),總之結論像是 markdown 渲染的時候莫名其妙把我的標點符號給替換了。
那麼接下來就需要:
修正標點符號的渲染 #
解決方式有兩種:
-
用 html 實寫單引號
博客名 © 2025 · Please Don't Reblog or Repost Without Asking Permission
這樣渲染之後應該就ok了。
-
修改 Markdownify 屬性
涉及到單引號的部分都這麼一個個修改也太麻煩,還可以嘗試的另一條路是:
針對版權信息部分去除markdownify
,修改為safeHTML
,讓他別這麼自動轉換了。{{- with replace .Site.Params.copyright "{ year }" now.Year }} {{ . | markdownify }} <!-- 这里改成 safeHTML --> {{- else }} © {{ now.Format "2006" }} {{ .Site.Params.Author.name | markdownify }} <!-- 这里也改成 safeHTML --> {{- end }}
這樣就可以確保自定義的footer文字和渲染都是正確的了。
代碼塊設置為MacOS風格 #
還是參考了Kyo老師的博文 Hugo Stack主题装修笔记Part3
,感謝。
先開始一直報錯沒想到如此簡單就適配了Blowfish。效果見下方折疊區。
將如下代碼插入到/custom.css後面:
點我展開
/* 代码块容器样式 */
.highlight {
max-width: 100%;
position: relative;
border-radius: 10px;
margin-left: -5px;
margin-right: -10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* 添加 macOS 风格的标题栏 */
.highlight::before {
content: '';
height: 30px;
width: 100%;
background: #f0f0f0;
display: block;
border-bottom: 1px solid #ddd;
position: relative;
}
/* 深色模式下的标题栏样式 */
.dark .highlight::before {
background: #0e220f;
border-bottom: 1px solid #404040;
}
/* 添加控制按钮 */
.highlight::after {
content: '';
position: absolute;
top: 9px;
left: 13px;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #ff5f56;
box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f;
z-index: 1;
}
/* 代码块内容区域样式 */
.highlight pre {
margin: 0;
padding: 1.5rem;
width: auto;
}
.prose .chroma {
border-radius: 0;
}
/* 代码块复制按钮样式 */
.copy-button {
height: 30px;
}
.highlight:hover .copyCodeButton {
opacity: 1;
}
做法稍微有些不同是我這裡直接插入css代碼來做那三個按鈕了。
另外就是為了適配當前深色模式下的博客配色,深色模式下的MacOS标题栏顏色也有加深,可根據實際情況進行修改。
代碼塊文本高亮 #
很多其他hugo主題都內置了代碼高亮,而Blowfish 本身的高亮有點讓人迷茫。我個人是沒找到能否換配色方案,所以就自行更換了。
定位至博客所在文件夾,執行如下命令:
# 生成亮色主题样式
hugo gen chromastyles --style=github > assets/css/syntax-light.css
# 生成暗色主题样式
hugo gen chromastyles --style=monokai > assets/css/syntax-dark.css
然後在layouts/partials文件夾下新建extend-head.html,輸入以下內容,引入這兩個代碼塊高亮方案:
{{ $syntaxLight := resources.Get "css/syntax-light.css" }}
{{ $syntaxDark := resources.Get "css/syntax-dark.css" }}
<link rel="stylesheet" href="{{ $syntaxLight.RelPermalink }}">
<link rel="stylesheet" href="{{ $syntaxDark.RelPermalink }}">
刷新博客即可。
當然也可以把兩個css放在static/css下,引用就不需要這麼複雜了。
給文章增加簡繁切換按鈕 #
這個功能也是看似無用但我一直想加。效果大概如圖:
原意是放在文章信息旁邊,這樣點擊按鈕之後就會將整個頁面切換為簡體。再點一下即可切換回繁體。
轉換也不僅限於繁體頁面,若是簡體中文書寫的頁面也會自動識別,確定為簡體中文後按鈕就會顯示"簡體中文"。
bug也有:
- 我希望內容是簡體時,按鈕顯示「简体中文」而非繁體,但這是寫死在blowfish模版裡的,我主題語言指定是繁體,這裡就註定是繁體(笑)。
- twikoo部分字詞轉換不完整(e.g. 暱稱/暱称)
分為幾個步驟。
- 在 layouts/partials/basic.html 中新增一段代碼。因為要把按鈕放在文章信息欄,所以加在那一長串 partials的後面。我是加在了zen-mode這一段的後面
{{ if and (eq $scope "single") (.Params.showZenMode | default (.Site.Params.article.showZenMode | default false)) }}
{{ $meta.Add "partials" (slice (partial "meta/zen-mode.html" .)) }}
{{ end }}
在這段代碼後新增如下代碼:
<!--新增简繁切换功能-->
{{ if eq $scope "single" }}
{{ $meta.Add "partials" (slice (partial "meta/language-switch.html" .)) }}
{{ end }}
-
在 layouts/partials/meta 文件夹下新建 language-switch.html,代码如下:
點我展開
<span class="inline-flex items-center"> <button id="langToggleBtn" class="lang-btn" title="切換語言" aria-label="切換語言" > <span id="langLabel">語言</span><!-- 临时 placeholder,由 JS 替换 --> <!-- 翻译图标 --> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="translate-icon"> <path d="m5 8 6 6" /> <path d="m4 14 6-6 2-3" /> <path d="M2 5h12" /> <path d="M7 2h1" /> <path d="m22 22-5-10-5 10" /> <path d="M14 18h6" /> </svg> </button> </span> <style> .lang-btn { display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0.8rem; font-size: 0.875rem; border: 1px solid var(--color-neutral-300); border-radius: 0.5rem; background: var(--color-neutral-100); color: var(--color-neutral-900); cursor: pointer; } [data-dark-mode] .lang-btn { background: var(--color-neutral-800); color: var(--color-neutral-100); border-color: var(--color-neutral-600); } .lang-btn:hover { background: var(--color-neutral-200); } [data-dark-mode] .lang-btn:hover { background: var(--color-neutral-700); } </style>
-
在 layouts/partials/footer.html 結尾的前加入一段:
<!--增加opencc库-->
<!-- 加载 opencc-js -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/full.js"></script>
<!-- 加载你的 lang-toggle.js -->
<script src="/js/lang-toggle.js"></script>
- 在 static/js 下新建 language-toggle.js 文件,代碼如下:
點我展開
document.addEventListener("DOMContentLoaded", () => { const btn = document.getElementById("langToggleBtn"); const label = document.getElementById("langLabel"); if (!btn || !label) return; const langMap = { s: "cn", t: "hk" }; let originalLang = 's'; // 默认简体 let currentLang = 's'; function detectInitialLang() { const text = document.body.innerText; const simplifiedChars = ['个', '简', '么', '国', '这', '没', '图']; const traditionalChars = ['個', '簡', '麼', '國', '這', '沒', '圖']; let simplifiedCount = 0; let traditionalCount = 0; simplifiedChars.forEach(char => { if (text.includes(char)) simplifiedCount++; }); traditionalChars.forEach(char => { if (text.includes(char)) traditionalCount++; }); return traditionalCount >= simplifiedCount ? 't' : 's'; } function updateButtonUI(lang) { if (lang === 's') { label.textContent = "简体中文"; btn.title = "当前为简体中文,切换为繁体中文"; } else { label.textContent = "繁體中文"; btn.title = "當前為繁體中文,切換為簡體中文"; } } // ✅ 延迟语言检测,确保内容渲染完成 setTimeout(() => { originalLang = detectInitialLang(); currentLang = originalLang; updateButtonUI(currentLang); }, 200); // 200ms 延迟,一般足够等待 DOM 完全渲染 // ✅ 转换内容逻辑 setTimeout(() => { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { const el = node.parentElement; if (!el) return NodeFilter.FILTER_ACCEPT; // 避免转换语言按钮上的所有文本 if ( el.closest('#langToggleBtn') || el.closest('#langLabel') ) { return NodeFilter.FILTER_REJECT; } // 排除网站标题 if ( el.closest('.logo') || el.closest('a[href="/"]') || el.closest('meta[property="og:title"]') ) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); while (walker.nextNode()) { textNodes.push({ node: walker.currentNode, originalText: walker.currentNode.textContent, convertedText: null }); } btn.addEventListener("click", () => { if (currentLang === originalLang) { const targetLang = originalLang === 's' ? 't' : 's'; const converter = OpenCC.Converter({ from: langMap[originalLang], to: langMap[targetLang] }); textNodes.forEach(item => { if (item.convertedText === null) { item.convertedText = converter(item.originalText); } item.node.textContent = item.convertedText; }); currentLang = targetLang; } else { textNodes.forEach(item => { item.node.textContent = item.originalText; }); currentLang = originalLang; } updateButtonUI(currentLang); }); }, 300); });
注意點
-
標題簡繁體意思大有不同,所以此處沒有把標題加入轉換範圍;
-
個人習慣不喜歡台灣中文下的一些表達方式(e.g. 自行车vs腳踏車)所以選了hk,喜歡台灣中文的話將
const langMap = { s: "cn", t: "hk" };
這裡的hk改為tw即可大概就可以了。 -
導航菜單也會隨之進行簡繁體轉化,如果博客標題+導航菜單均不需要變化,那也可以照抄下面這個js文件(直接替換即可):
點我展開
document.addEventListener("DOMContentLoaded", () => { const btn = document.getElementById("langToggleBtn"); const label = document.getElementById("langLabel"); if (!btn || !label) return; const langMap = { s: "cn", t: "hk" }; // 延迟执行,确保所有 DOM 元素加载完毕 setTimeout(() => { const contentEl = document.querySelector("#main-content"); if (!contentEl) return; // 语言初始检测逻辑 const detectInitialLang = (text) => { const simplifiedChars = ['个', '简', '么', '国', '这', '没', '图']; const traditionalChars = ['個', '簡', '麼', '國', '這', '沒', '圖']; let sCount = 0, tCount = 0; for (const ch of simplifiedChars) if (text.includes(ch)) sCount++; for (const ch of traditionalChars) if (text.includes(ch)) tCount++; return tCount > sCount ? "t" : "s"; }; const originalLang = detectInitialLang(contentEl.innerText); let currentLang = originalLang; // 用 TreeWalker 收集所有可转换的文本节点 const textNodes = []; const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; // 排除按钮及按钮内部的文字 if (parent.closest("#langToggleBtn")) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); while (walker.nextNode()) { textNodes.push({ node: walker.currentNode, originalText: walker.currentNode.textContent, convertedText: null, // 缓存 }); } // 更新按钮 UI 文案 function updateButtonUI(lang) { if (lang === "s") { label.textContent = "简体中文"; btn.title = "目前为简体中文,点击切换为繁体"; } else { label.textContent = "繁體中文"; btn.title = "目前為繁體中文,點擊切換為簡體"; } } updateButtonUI(currentLang); // 点击切换逻辑 btn.addEventListener("click", () => { const targetLang = currentLang === "s" ? "t" : "s"; const converter = OpenCC.Converter({ from: langMap[currentLang], to: langMap[targetLang], }); textNodes.forEach((item) => { if (targetLang === originalLang) { item.node.textContent = item.originalText; } else { if (item.convertedText === null) { item.convertedText = converter(item.originalText); } item.node.textContent = item.convertedText; } }); currentLang = targetLang; updateButtonUI(currentLang); }); }, 100); // 可根据需要调整延迟 });