背景

在之前的文章中,我们已经成功为使用PaperMod主题的Hugo博客配置了Giscus评论系统。但在使用过程中发现了一个问题:当切换博客的深浅色主题时,评论区并不会自动跟随变化,这导致了视觉上的不一致。

今天我们来解决这个问题,让Giscus评论系统能够智能地跟随PaperMod主题变化。

注意:本文方案专门针对Hugo PaperMod主题设计,如需适配其他主题,需要根据具体主题的实现方式调整代码。

问题分析

默认情况下,Giscus的主题配置是静态的:

data-theme="preferred_color_scheme"

这种配置会让评论区始终跟随系统偏好设置,而不是博客当前的主题状态。当用户手动切换博客主题时,评论区仍然保持原样,造成了不一致的用户体验。

解决方案

1. 动态主题检测

首先,我们需要创建一个能够准确检测当前主题的函数:

function getCurrentTheme() {
  // 先检查body上的dark类(PaperMod主题的实现方式)
  if (document.body.classList.contains('dark')) {
    return 'dark';
  }
  // 再检查localStorage中保存的主题
  const savedTheme = localStorage.getItem('pref-theme');
  if (savedTheme) {
    return savedTheme === 'dark' ? 'dark' : 'light';
  }
  // 最后检查系统偏好
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

这个函数按优先级检测主题:

  1. 页面当前状态(body类名)
  2. 用户保存的偏好设置
  3. 系统偏好设置

2. Giscus主题设置函数

接下来实现动态更新Giscus主题的函数:

function setGiscusTheme() {
  const currentTheme = getCurrentTheme();
  const iframe = document.querySelector('iframe.giscus-frame');
  if (iframe) {
    iframe.contentWindow.postMessage({
      giscus: {
        setConfig: {
          theme: currentTheme
        }
      }
    }, 'https://giscus.app');
    return true;
  }
  return false;
}

3. 主题变化监听

使用MutationObserver监听DOM变化,当主题切换时自动更新评论区:

window.addEventListener('DOMContentLoaded', function() {
  // 创建观察器监听body类名变化
  const observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
        setTimeout(setGiscusTheme, 10);
      }
    });
  });
  
  // 开始观察
  observer.observe(document.body, {
    attributes: true,
    attributeFilter: ['class']
  });
});

4. 按钮点击监听

为了更快响应用户的主题切换操作,我们还添加了点击事件监听:

document.addEventListener('click', function(e) {
  const themeToggle = e.target.closest('button[aria-label*="theme"], .theme-toggle, #theme-toggle');
  if (themeToggle) {
    setTimeout(setGiscusTheme, 20);
  }
});

5. 初始化主题设置

最重要的是解决页面刷新时的主题初始化问题:

// 初始主题 - 页面加载时立即确定
const initialTheme = getCurrentTheme();

// 立即设置初始主题到script标签上
const giscusScript = document.currentScript.previousElementSibling;
giscusScript.setAttribute('data-theme', initialTheme);

完整实现

将以上代码整合到 layouts/partials/comments.html 文件中:

{{- /* Giscus Comments */ -}}
{{- if .Site.Params.comments -}}
{{- if .Site.Params.giscus.repo -}}
<script>
  // 获取当前主题
  function getCurrentTheme() {
    if (document.body.classList.contains('dark')) {
      return 'dark';
    }
    const savedTheme = localStorage.getItem('pref-theme');
    if (savedTheme) {
      return savedTheme === 'dark' ? 'dark' : 'light';
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  }
  
  const initialTheme = getCurrentTheme();
  
  function setGiscusTheme() {
    const currentTheme = getCurrentTheme();
    const iframe = document.querySelector('iframe.giscus-frame');
    if (iframe) {
      iframe.contentWindow.postMessage({
        giscus: {
          setConfig: {
            theme: currentTheme
          }
        }
      }, 'https://giscus.app');
      return true;
    }
    return false;
  }
</script>

<script src="https://giscus.app/client.js"
    data-repo="{{ .Site.Params.giscus.repo }}"
    data-repo-id="{{ .Site.Params.giscus.repoId }}"
    data-category="{{ .Site.Params.giscus.category }}"
    data-category-id="{{ .Site.Params.giscus.categoryId }}"
    data-mapping="{{ .Site.Params.giscus.mapping }}"
    data-strict="0"
    data-reactions-enabled="{{ .Site.Params.giscus.reactionsEnabled }}"
    data-emit-metadata="{{ .Site.Params.giscus.emitMetadata }}"
    data-input-position="{{ .Site.Params.giscus.inputPosition }}"
    data-lang="{{ .Site.Params.giscus.lang }}"
    data-loading="lazy"
    crossorigin="anonymous"
    async>
</script>

<script>
  // 立即设置初始主题
  const giscusScript = document.currentScript.previousElementSibling;
  giscusScript.setAttribute('data-theme', initialTheme);
  
  // 监听主题变化
  window.addEventListener('DOMContentLoaded', function() {
    const observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
          setTimeout(setGiscusTheme, 10);
        }
      });
    });
    
    observer.observe(document.body, {
      attributes: true,
      attributeFilter: ['class']
    });
    
    document.addEventListener('click', function(e) {
      const themeToggle = e.target.closest('button[aria-label*="theme"], .theme-toggle, #theme-toggle');
      if (themeToggle) {
        setTimeout(setGiscusTheme, 20);
      }
    });
    
    const checkGiscus = setInterval(function() {
      if (setGiscusTheme()) {
        clearInterval(checkGiscus);
      }
    }, 500);
    
    setTimeout(() => clearInterval(checkGiscus), 5000);
  });
</script>
{{- end -}}
{{- end -}}

优化细节

响应速度优化

为了提升主题切换的响应速度,我们将延迟时间优化到了最小值:

  • DOM变化检测:10ms延迟
  • 按钮点击检测:20ms延迟

兼容性处理

代码考虑了多种主题检测方式,确保在不同情况下都能正确工作:

  • 页面状态优先(实时)
  • 本地存储其次(用户偏好)
  • 系统偏好兜底(默认值)

效果验证

实施以上方案后,Giscus评论系统将能够:

  1. 页面刷新时:自动匹配当前页面主题
  2. 手动切换时:快速响应主题变化(10-20ms延迟)
  3. 兼容性良好:支持多种主题检测方式

总结

通过动态主题检测和实时同步机制,我们成功解决了Giscus评论系统的主题适配问题。这种实现方式不仅解决了视觉一致性问题,还提供了良好的用户体验。

关键技术点:

  • MutationObserver监听DOM变化
  • PostMessage API进行iframe通信
  • 多层级主题检测机制
  • 优化的响应延迟设置

这套方案专门针对PaperMod主题设计。对于其他Hugo主题,需要根据具体主题的实现方式调整以下部分:

  • 主题检测方式:不同主题可能使用不同的类名或属性来标识主题状态
  • localStorage键名:每个主题保存偏好设置的键名可能不同
  • 切换按钮选择器:各主题的主题切换按钮HTML结构和属性不同

适配其他主题的方法

如需适配其他主题,请先检查该主题的:

  1. 主题状态存储方式:查看主题如何在DOM中标识当前状态(类名、data属性等)
  2. 本地存储键名:检查主题使用的localStorage键名
  3. 切换按钮结构:找到主题切换按钮的选择器

然后相应修改getCurrentTheme()函数和按钮选择器即可。