2025.05.07

gatsby-tailwindcss-darkmode-seo

Gatsby × Tailwindcssでダークモードを実装する

  • Gatsby
  • UI
  • SEO

ダークモードのSEO効果

ダークモード自体には直接的なSEO効果はないものの、眼精疲労や睡眠障害などへの配慮を行うことによるユーザー体験の上昇は、離脱率を下げ、間接的にSEOに好影響あると言えます。この記事ではGatsby.jsとTailwindcssでダークモードの実装を行いました。

コンテキストでテーマ管理

ThemeContextを作成してdark, light, systemの3つを、グローバルに状態管理を行います。

import React, { createContext, useContext, useEffect, useState } from "react"

type Theme = "dark" | "light" | "system"

interface ThemeContextType {
  theme: Theme
  setTheme: (theme: Theme) => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("system")

  useEffect(() => {
    const savedTheme = localStorage.getItem("theme") as Theme
    if (savedTheme) {
      setTheme(savedTheme)
    }
  }, [])

  useEffect(() => {
    const root = window.document.documentElement
    root.classList.remove("light", "dark")

    if (theme === "system") {
      const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
        .matches
        ? "dark"
        : "light"
      root.classList.add(systemTheme)
    } else {
      root.classList.add(theme)
    }

    localStorage.setItem("theme", theme)
  }, [theme])

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (context === undefined) {
    throw new Error("useTheme must be used within a ThemeProvider")
  }
  return context
}

トグルボタンの用意

import React from "react"
import { useTheme } from "@/contexts/ThemeContext"
import { Sun, Moon, Monitor } from "lucide-react"

export function ThemeToggle() {
  const { theme, setTheme } = useTheme()

  return (
    <div className="flex items-center space-x-2">
      <button
        onClick={() => setTheme("light")}
        className={`p-2 rounded-lg ${
          theme === "light"
            ? "bg-primary text-primary-foreground"
            : "bg-muted text-muted-foreground"
        }`}
      >
        <Sun className="w-5 h-5" />
      </button>
      <button
        onClick={() => setTheme("dark")}
        className={`p-2 rounded-lg ${
          theme === "dark"
            ? "bg-primary text-primary-foreground"
            : "bg-muted text-muted-foreground"
        }`}
      >
        <Moon className="w-5 h-5" />
      </button>
      <button
        onClick={() => setTheme("system")}
        className={`p-2 rounded-lg ${
          theme === "system"
            ? "bg-primary text-primary-foreground"
            : "bg-muted text-muted-foreground"
        }`}
      >
        <Monitor className="w-5 h-5" />
      </button>
    </div>
  )
}

スタイルの切り替え

スタイルの切り替えは、上記のようにuseThemeを利用するか、Tailwindのdark:修飾子を使用、もしくは次のようにcssで定義します。

.dark {
    --background: 0 0% 3.9%;
    --foreground: 0 0% 98%;
    --card: 0 0% 3.9%;
    /* 中略 */
}

グローバルなラッパーコンポーネントの設定

gatsby-browser.jsで、GatsbyのwrapRootElement関数と先ほどのThemeProviderで、アプリケーション全体をラップするコンポーネントを定義します。

import React from "react"
import { ThemeProvider } from "./src/contexts/ThemeContext"
import "./src/styles/global.css"

export const wrapRootElement = ({ element }) => {
  return <ThemeProvider>{element}</ThemeProvider>
}

SSR対応

gatsby-browser.jsではクライアントサイドでのハイドレーション時にテーマを適用しました。同様に サーバーサイドでのレンダリング時にはgatsby-ssr.jsでテーマを適用し、これによりページの初期表示時からテーマが正しく適用されます。

import React from "react"
import { ThemeProvider } from "./src/contexts/ThemeContext"
import "./src/styles/global.css"

export const wrapRootElement = ({ element }) => {
  return <ThemeProvider>{element}</ThemeProvider>
}

/**
 * @type {import('gatsby').GatsbySSR['onRenderBody']}
 */
export const onRenderBody = ({ setHtmlAttributes }) => {
  setHtmlAttributes({ lang: `en` })
}

最後にトグルボタンを設置したい場所にThemeToggleを置いて完了です。

import { ThemeToggle } from "@/components/ui/ThemeToggle"

const Footer = () => (
  <footer className="bg-white dark:bg-black">
    <ThemeToggle />
  </footer>
)

export default Footer

最後に

ダークモード対応は、実装コストはかかるものの、間接的なSEO効果や、ユーザー体験の向上が期待できるため、Webサイトの目的やユーザーのニーズに応じて、ぜひ導入を検討してみてはいかがでしょうか!