如何為 WordPress 主題無痛加入深色模式?Git Patch 與高效率 Prompt 實戰

在現代網頁設計中,深色模式(Dark Mode / Dark Theme)已經是用戶體驗(UX)的基本標配。除了降低在暗處閱讀時的眼部疲勞,也能延長 OLED 螢幕的電池續航力。

本篇技術筆記以開源主題 Guntenbee 為例,完整記錄如何架構一個具備「系統主題偏好自動偵測 + Cookie 記憶 + 切換按鈕 + 防閃爍 (Anti-flicker)」的深色模式方案。同時針對日後「原作者主題升級」時如何以零成本(Git Patch / Child Theme)或極低 Token 的 AI 提示詞無痛重新套用進行深度解析。


一、核心需求與架構設計

一個健全的深色模式切換機制需要具備以下要點:

  1. 使用者優先權階層:
    • 若使用者曾在站上點擊切換過,以 Cookie 儲存的偏好 為最優先。
    • 若為首次造訪或未設定 Cookie,自動偵測使用者的系統外觀模式(prefers-color-scheme: dark)。
  2. 避免白光閃爍(Anti-flicker / FOUC):
    • 瀏覽器解析 HTML 時,如果等到完整腳本下載並在 DOMContentLoaded 才執行加 class,畫面上會出現短暫的白光閃爍。因此需在 <head> 注入極輕量的行內(Inline)腳本提早設定 class。
  3. 直覺易用的開關位置:
    • 將切換開關置入 Header 頂部導覽列(<div class="head-mast-inner">),確保各裝置與版型都能一致顯示。

二、功能實作步驟

1. 樣式實作:獨立樣式表 (css/dark-theme.css)

將所有深色模式樣式統一抽離至獨立檔案,避免直接污染原作者的 style.css,大幅降低後續維護與合併時的衝突風險。

/* ==========================================================================
   Dark Theme Styles
   ========================================================================== */

/* 全域背景與文字 */
body.theme-dark {
  background-color: #121417;
  color: #c9d1d9;
}

body.theme-dark h1,
body.theme-dark h2,
body.theme-dark h3,
body.theme-dark h4,
body.theme-dark h5,
body.theme-dark h6 {
  color: #f0f6fc;
}

/* 頂部導覽、底欄與行動端選單 */
body.theme-dark .head-mast,
body.theme-dark .head-outro,
body.theme-dark .navigation-mobile-wrap {
  background-color: #181b20;
  color: #c9d1d9;
}

/* 下拉選單 */
body.theme-dark .navigation-main ul {
  background-color: #1a1e24;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45);
  border: 1px solid #2b3036;
}

/* 輸入框與表單元件 */
body.theme-dark input[type="text"],
body.theme-dark input[type="search"],
body.theme-dark select,
body.theme-dark textarea {
  background-color: #1a1e24;
  color: #e6edf3;
  border-color: #343a42;
}

/* 主題切換按鈕與 SVG 圖示切換 */
.theme-switch-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: transparent;
  border: 0;
  cursor: pointer;
  color: inherit;
  transition: color 0.18s ease, background-color 0.18s ease;
}

.theme-switch-icon svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
}

/* 淺色模式顯示月亮,深色模式顯示太陽 */
.theme-switch-icon .icon-sun { display: none; }
.theme-switch-icon .icon-moon { display: inline-block; }
body.theme-dark .theme-switch-icon .icon-sun { display: inline-block; }
body.theme-dark .theme-switch-icon .icon-moon { display: none; }

2. 行為邏輯:偵測與持久化 (js/theme-switch.js)

透過原生 JavaScript 實現無相依性(Vanilla JS)的偵測與 Cookie 儲存:

(function () {
  'use strict';

  var COOKIE_NAME = 'guntenbee_theme';
  var COOKIE_DAYS = 365;

  function getCookie(name) {
    var match = document.cookie.match(new RegExp('(^|;\\s*)' + name + '=([^;]+)'));
    return match ? decodeURIComponent(match[2]) : null;
  }

  function setCookie(name, value, days) {
    var expires = '';
    if (days) {
      var date = new Date();
      date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
      expires = '; expires=' + date.toUTCString();
    }
    document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/; SameSite=Lax';
  }

  function getSystemTheme() {
    return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
      ? 'dark'
      : 'light';
  }

  function getPreferredTheme() {
    var savedTheme = getCookie(COOKIE_NAME);
    return (savedTheme === 'dark' || savedTheme === 'light') ? savedTheme : getSystemTheme();
  }

  function applyTheme(theme) {
    var isDark = (theme === 'dark');
    document.body.classList.toggle('theme-dark', isDark);
    document.body.classList.toggle('theme-light', !isDark);
    document.documentElement.classList.toggle('theme-dark', isDark);
    document.documentElement.classList.toggle('theme-light', !isDark);

    var toggles = document.querySelectorAll('.theme-switch-toggle');
    toggles.forEach(function (btn) {
      btn.setAttribute('aria-pressed', isDark ? 'true' : 'false');
      btn.setAttribute('title', isDark ? '切換至淺色模式' : '切換至深色模式');
    });
  }

  function init() {
    applyTheme(getPreferredTheme());

    document.addEventListener('click', function (e) {
      var toggle = e.target.closest('.theme-switch-toggle');
      if (toggle) {
        e.preventDefault();
        var currentTheme = document.body.classList.contains('theme-dark') ? 'dark' : 'light';
        var nextTheme = (currentTheme === 'dark') ? 'light' : 'dark';
        setCookie(COOKIE_NAME, nextTheme, COOKIE_DAYS);
        applyTheme(nextTheme);
      }
    });

    // 監聽作業系統外觀模式動態變化
    if (window.matchMedia) {
      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
        if (!getCookie(COOKIE_NAME)) {
          applyTheme(getSystemTheme());
        }
      });
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

3. WordPress 資源載入與防閃爍 (inc/scripts-styles.php)

在 wp_enqueue_scripts 鉤子中註冊資源,並加入極速 Inline 腳本:

// Enqueue 樣式與腳本
wp_register_style( 'gutenbee-dark-theme', get_theme_file_uri( '/css/dark-theme.css' ), array( 'gutenbee-style' ), gutenbee_asset_version() );
wp_enqueue_style( 'gutenbee-dark-theme' );

// 防閃爍腳本:在頁面剛開始解析 DOM 時搶先將 theme-dark 注入 <html>
$anti_flicker_js = "(function(){try{var m=document.cookie.match(/(^|;\\s*)guntenbee_theme=([^;]+)/);var t=m?decodeURIComponent(m[2]):null;var d=(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light';if((t||d)==='dark'){document.documentElement.classList.add('theme-dark');}}catch(e){}})();";
wp_add_inline_script( 'gutenbee-dependencies', $anti_flicker_js, 'before' );

wp_register_script( 'gutenbee-theme-switch', get_theme_file_uri( '/js/theme-switch.js' ), array(), gutenbee_asset_version(), true );
wp_enqueue_script( 'gutenbee-theme-switch' );

4. 範本標籤與按鈕輸出 (inc/template-tags.php & Header Templates)

  1. 定義輔助函數:function gutenbee_the_theme_switch_button() { ?> <button type="button" class="theme-switch-toggle" aria-label="切換深淺模式"> <span class="theme-switch-icon" aria-hidden="true"> <svg class="icon-moon" viewBox="0 0 24 24"><path d="M12.3 2a10 10 0 0 0-.19 14 10 10 0 0 0 11.63 4A10 10 0 1 1 12.3 2z"/></svg> <svg class="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="12" y1="21" x2="12" y2="23" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="1" y1="12" x2="3" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="21" y1="12" x2="23" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> </span> </button> <?php }
  2. 在 template-parts/header/header-full.php、header-split.php、header-navbar.php 的 <div class="head-mast-inner"> 內部直接呼叫 gutenbee_the_theme_switch_button() 即可完美呈現。

三、未來原作者升級主題時的處理策略

當原主題作者發布新版本更新時,若直接覆蓋整個主題資料夾,客製化代碼將會遺失。以下提供三種不同維護情境的應對方式:

策略 A:建立子主題(Child Theme)—— 永遠不被覆蓋(推薦最佳實踐)

WordPress 的標準升級防禦機制是子主題。只需將新寫好的 dark-theme.css、theme-switch.js 及對應 template parts 放進子主題資料夾,日後父主題無論升級幾次,客製化邏輯都不會被覆蓋。

策略 B:利用 Git Patch 自動打補丁(零 Token 快速升級)

若習慣直接於主題目錄工作,建議在功能完成時立即產生 patch 檔案備份:

# 產出補丁檔
git diff > dark-theme.patch

當日後原作者有新版發布時:

# 下載並解壓縮覆蓋原主題後,執行一鍵套用
git apply --reject --whitespace=fix dark-theme.patch

git apply 具備自動比對行號上下文與建立新檔案的能力,速度快且完全不花費任何 AI token。


策略 C:低 Token 的精準 AI 提示詞(AI Prompt Template)

若在遠端或未配備 Git 的環境,需請 AI 助理重新在原作者新版主題植入 Dark Theme 功能時,千萬不要使用模糊指令(例如「幫我在這個主題加深色模式」,這會讓 AI 花費數萬 Token 去探索全文結構與測試選擇器)。

直接複製使用以下經結構化優化的 精準 Prompt:

請為 @guntenbee 主題實作 Dark Theme 功能。請直接依照以下規格建立檔案與注入代碼,不需漫遊搜尋其他檔案:

1. 新增樣式與腳本:
   - 建立 `css/dark-theme.css`:定義 `body.theme-dark` 樣式(深色底色 #121417、文字 #c9d1d9、容器/選單 #181b20)與 `.theme-switch-toggle` 按鈕 SVG 切換動態。
   - 建立 `js/theme-switch.js`:讀取/寫入 Cookie `guntenbee_theme`(效期 365 天);若無 Cookie 則以 `prefers-color-scheme: dark` 為準,並監聽 OS 主題變化與按鈕點擊事件。

2. 註冊與防閃爍(修改 `inc/scripts-styles.php`):
   - 在 `gutenbee_enqueue_scripts` 中以 `wp_enqueue_style` 載入 `dark-theme.css`。
   - 注入 Inline Script 到 `<head>`:若 Cookie 或系統偏好為 dark,即時加上 `document.documentElement.classList.add('theme-dark')` 以防止白光閃爍。
   - 以 `wp_enqueue_script` 載入 `theme-switch.js`。

3. 按鈕與鉤子整合:
   - 在 `inc/template-tags.php` 新增 `gutenbee_the_theme_switch_button()` 輸出太陽/月亮按鈕 HTML。
   - 在 `template-parts/header/header-*.php` 各樣板的 `<div class="head-mast-inner">` 的 `head-content-slot` 中呼叫該按鈕函式。
   - 在 `inc/default-hooks.php` 的 `gutenbee_body_class` 過濾器中檢查 `$_COOKIE['guntenbee_theme']`,有 dark 值即附加 `theme-dark` class。

為什麼這個 Prompt 具備高性價比(Low Token Usage)?

  1. 消除探勘步驟:精確指出檔案名稱(如 inc/scripts-styles.php、template-parts/header/header-*.php),AI 不需調用目錄搜尋工具消耗大量 Context Window。
  2. 精準函式命名:直接給予目標鉤子(gutenbee_body_class、gutenbee_enqueue_scripts),AI 思考路徑縮短,輸出 token 減少 70% 以上。
  3. 成果可預測且易於維護:模組化規範使每一次生成的樣式結構一致,日後維護與問題排查極為單純。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *