折腾评论区的时候发现一个事:站点的评论区不会跟随主题变色,一直白底。查阅资料发现giscus官方配置中的data-theme="preferred_color_scheme"表示跟随系统变色,我把系统颜色从浅色改成深色后,评论区就变成了深色,但是我希望评论区跟随主题变色,所以需要做一些调整。

然后我给giscus的配置做了一些改动,让它跟随主题变色,但是出错了,切换主题后评论区并没有变色。

排查

一开始我怀疑是监听写错了。评论区的主题同步逻辑大概是这样的:监听某个元素的变化,然后往 giscus 的 iframe 里发消息换主题。我监听的是 body 上的 class:

1
2
// 我原来这么写的
return document.body.classList.contains('dark') ? 'transparent_dark' : 'light';

结果怎么试都不生效。后来翻了 PaperMod 的源码才发现,它的主题切换根本不是给 body 加 class,而是改 <html> 元素上的 data-theme 属性:

1
document.documentElement.dataset.theme = 'dark';

所以 body.classList.contains('dark') 永远都是 false,自然永远走亮色分支,监听的 class 变化也永远不会触发。

另外还有个坑:giscus 开了 data-loading="lazy",iframe 要滚动到评论区附近才创建。如果在这之前切主题,postMessage 发出去没人接收,等 iframe 加载的时候只能用 script 标签上旧的 data-theme 初始化,主题还是错的。

修复

检测和监听都改成 data-theme:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function getGiscusTheme() {
  return document.documentElement.dataset.theme === 'dark'
    ? '{{ absURL "css/giscus-dark.css" }}'
    : '{{ absURL "css/giscus-light.css" }}';
}

const observer = new MutationObserver(() => {
  sendGiscusTheme(getGiscusTheme());
});
observer.observe(document.documentElement, {
  attributes: true,
  attributeFilter: ['data-theme']
});

发消息的时候顺手把 script 标签和 iframe 上的 data-theme 属性也同步掉,就算 iframe 还没创建,等它加载的时候也能用对主题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function sendGiscusTheme(giscusTheme) {
  const script = container.querySelector('script[src*="giscus"]');
  if (script) script.setAttribute('data-theme', giscusTheme);

  const iframe = document.querySelector('iframe.giscus-frame');
  if (!iframe || !iframe.contentWindow) return;
  iframe.dataset.theme = giscusTheme;
  iframe.contentWindow.postMessage(
    { giscus: { setConfig: { theme: giscusTheme } } },
    'https://giscus.app'
  );
}

风格不统一的问题

主题能切换之后,又发现明暗两边的观感差太多。原因是我当初为了"融入暗色背景"选了 transparent_dark(透明主题),而亮色用的官方 light(自带背景和边框)。做完之后发现,暗色背景的评论区背景和边框是透明的,跟暗色背景融入在一起,跟正文的背景和边框完全不搭。

干脆统一成卡片风格,给明暗各写一个自定义主题,颜色直接抄 PaperMod 的 CSS 变量,这样评论区跟正文就是一个色系的了。建了两个文件 static/css/giscus-light.css 和 static/css/giscus-dark.css,主要改这几个变量:

  • --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-*)、加载动画那些定义最好别删,不然评论区会缺样式。

部署时的坑

giscus 的 iframe 是跨域加载自定义 CSS 的,服务器必须返回 Access-Control-Allow-Origin 头,不然评论区直接加载不出来。我是 Nginx 部署,加了一段:

1
2
3
location /css/ {
    add_header Access-Control-Allow-Origin *;
}

用 GitHub Pages 的话不用管,它自动带这个响应头。

改完的代码

layouts/partials/comments.html 完整内容(数据源配置换成你自己的):

 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
<div class="giscus" id="giscus-container"></div>

<script>
(function() {
  const container = document.getElementById('giscus-container');
  // 主题状态在 <html data-theme="..."> 上,PaperMod 切换主题就是改这个属性
  function getGiscusTheme() {
    return document.documentElement.dataset.theme === 'dark'
      ? '{{ absURL "css/giscus-dark.css" }}'
      : '{{ absURL "css/giscus-light.css" }}';
  }

  // 同步 script/iframe 上的 data-theme + 发消息,懒加载下也不怕丢消息
  function sendGiscusTheme(giscusTheme) {
    const script = container.querySelector('script[src*="giscus"]');
    if (script) script.setAttribute('data-theme', giscusTheme);

    const iframe = document.querySelector('iframe.giscus-frame');
    if (!iframe || !iframe.contentWindow) return;
    iframe.dataset.theme = giscusTheme;
    iframe.contentWindow.postMessage(
      { giscus: { setConfig: { theme: giscusTheme } } },
      'https://giscus.app'
    );
  }

  const script = document.createElement('script');
  script.src = 'https://giscus.app/client.js';
  script.crossOrigin = 'anonymous';
  script.async = true;
  script.setAttribute('data-loading', 'lazy');

  // 替换成你自己的配置
  script.setAttribute('data-repo', '用户名/仓库名');
  script.setAttribute('data-repo-id', '你的repo-id');
  script.setAttribute('data-category', 'Announcements');
  script.setAttribute('data-category-id', '你的category-id');
  script.setAttribute('data-mapping', 'pathname');
  script.setAttribute('data-strict', '0');
  script.setAttribute('data-reactions-enabled', '1');
  script.setAttribute('data-reactions-layout', 'compact');
  script.setAttribute('data-emit-metadata', '0');
  script.setAttribute('data-input-position', 'top');
  script.setAttribute('data-theme', getGiscusTheme()); // 加载前就设置好主题
  script.setAttribute('data-lang', 'zh-CN');

  container.appendChild(script);

  // 监听 <html> 的 data-theme 变化
  const observer = new MutationObserver(() => {
    sendGiscusTheme(getGiscusTheme());
  });
  observer.observe(document.documentElement, {
    attributes: true,
    attributeFilter: ['data-theme']
  });

  // 系统深色自动切换也要跟上
  window.matchMedia('(prefers-color-scheme: dark)')
    .addEventListener('change', () => sendGiscusTheme(getGiscusTheme()));
})();
</script>

小结

主题状态在 <html> 的 data-theme 属性上,不关 body 的事,监听对准它就行。懒加载丢消息那个问题也是顺手一起解决的。自定义主题的配色是照着 PaperMod 的 CSS 变量一个个抄的,弄完整体还算统一。

折腾了这么久,评论区总算能跟着主题跑了。