快轉到主要內容

Blog|自定義Hugo Blowfish主題

·4396 字·9 分鐘·
折騰小記 Hugo Blog
目錄

太喜歡倒騰了,博客暫時沒什麼有趣內容我卻打算針對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>

這裡主要是兩點:

  1. 給鏈接、圖標都用了一樣的class,.external-icon來控制圖標容器(位置/大小/對齊),.external-icon-svg控制其中的svg(顏色/懸停/其他效果),當然用其他名字也都可以。
  2. 用 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 }}
      &copy;
      {{ 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 = "網站名稱  &copy; { year } · Please Don't Reblog or Repost Without Asking Permission"
description = "網站描述"

year會自動替換為當前年份,其他部分均可以自定義。

這裡不加在 hugo.toml 中是為了方便管理,hugo.toml 是 hugo 讀取的內容, 而針對主題 Blowfish的修改通過 params.toml實現。

新的問題出現:
Don't的單引號一度顯示為全角符號的方括號,怎麼修改都找不到位置,讓我百思不得其解。
後來我想了一下,大概是因為我的languages.zh.toml文件中設置了isoCode = "zh",又或者是因為我的字體使用順序是繁體>簡體>英文導致默認使用了繁體標點(我已實驗過,字體是可以正常顯示英文符號的),總之結論像是 markdown 渲染的時候莫名其妙把我的標點符號給替換了。
那麼接下來就需要:

修正標點符號的渲染
#

解決方式有兩種:

  1. 用 html 實寫單引號

    博客名 © 2025 · Please Don&#39;t Reblog or Repost Without Asking Permission
    

    這樣渲染之後應該就ok了。

  2. 修改 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. 暱稱/暱称)

分為幾個步驟。

  1. 在 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 }}
  1. 在 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>
    

  2. 在 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>
  1. 在 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); // 可根据需要调整延迟
    });