Closed11

SvelteKitのダークモード + SSRについて調べる。

ymgnymgn

Themeはlightdarkautoの3つが定義されていた。

// hooks.server.ts

export type Theme = 'light' | 'dark' | 'auto'

また、app.d.tsのLocalsとPageDataにthemeが生えていた。

// app.d.ts

declare global {
  namespace App {
      interface Locals {
        theme: Theme
      }
      interface PageData {
        theme: Theme
      }
  }
}

app.htmlのhtml要素にdata-themeという属性が付与されていた。

<!-- app.html-->

<!DOCTYPE html>
<html ... data-theme="%THEME%">
	<head>
		...
	</head>
	<body data-sveltekit-preload-data="hover">
		...
	</body>
</html>
ymgnymgn

hooks.server.tsにてテーマ切り替えとキャッシュの処理が実装されている。
テーマに関する処理のみ抜き出すとこんな感じ。

// hooks.server.ts

export type Theme = 'light' | 'dark' | 'auto'

export const isValidTheme = (theme: FormDataEntryValue | null): theme is Theme =>
	!!theme && (theme === 'light' || theme === 'dark' || theme === 'auto')

const FIVE_MINUTES_IN_SECONDS = 5 * 60

export const handle: Handle = async ({event, resolve}) => {
	const theme = event.cookies.get('theme') ?? 'auto'
	if (isValidTheme(theme)) {
		event.locals.theme = theme
	}
	event.setHeaders({
		'cache-control': `private, max-age=${FIVE_MINUTES_IN_SECONDS}`,
	})
	const response = await resolve(event, {
		transformPageChunk: ({html}) => html.replace('%THEME%', theme),
	})

	return response
}
ymgnymgn

Headerコンポーネントでテーマの切り替えUIとロジック部分の実装がされている。
テーマのUIとロジック部分のみ抜き出すとこんな感じ。
formのPOSTリクエストを送っている?
ThemeToggleIconは中身はただのsvgで、data-themeの値によって見た目を切り替えているっぽい。

<script lang="ts">
  import {browser} from '$app/environment';
  import {applyAction, enhance} from '$app/forms';

  import ThemeToggleIcon from './ThemeToggleIcon.svelte';
  import {theme} from '$lib/stores/theme';
  import type {Theme} from '../../../hooks.server';

  const deriveNextTheme = (theme: Theme): Theme => {
    switch (theme) {
      case 'dark':
        return 'light';
      case 'light':
        return 'dark';
      case 'auto':
      default:
        if (!browser) return 'auto';
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'light' : 'dark';
    }
  };
  $: nextTheme = deriveNextTheme($theme);
</script>

<header class="container flex items-center justify-between px-2 py-4">
  <nav>
    <form
      method="POST"
      action="/?/theme"
      use:enhance={async () => {
        $theme = nextTheme;
        return async ({result}) => {
          await applyAction(result);
        };
      }}
    >
      <input name="theme" value={nextTheme} hidden />
      <button class="w-8">
        <ThemeToggleIcon />
      </button>
    </form>
  </nav>
</header>
ymgnymgn

lib/stores/theme.tsにwritableのThemeストアがあった。

// theme.ts

import {writable} from 'svelte/store'
import type {Theme} from '../../hooks.server'

export const theme = writable<Theme>()
ymgnymgn

src/routes/+layout.server.ts
ただテーマを返しているだけのように見えるけど、何をしているのだろう。

import type {LayoutServerLoad} from './$types';

export const load: LayoutServerLoad = async ({locals}) => {
  const {theme} = locals;

  return {theme};
};
ymgnymgn

src/routes/layout.svelte
テーマが変わるたびにdocumentElement.dataset.themeを書き換えてるっぽい。
datasetって何だろう。

<script lang="ts">
  import '@fontsource/inter/400.css';
  import '@fontsource/mansalva/400.css';
  import '@fontsource/noto-color-emoji/emoji.css';

  import '../app.css';
  import type {LayoutServerData} from './$types';
  import Header from '$lib/components/Header/Header.svelte';
  import Footer from '$lib/components/Footer/Footer.svelte';
  import {theme} from '$lib/stores/theme';
  import {browser} from '$app/environment';
  export let data: LayoutServerData;

  $theme = data.theme;
  $: browser && (document.documentElement.dataset.theme = $theme);
</script>

<svelte:head>
  <meta property="og:type" content="article" />
  <meta property="og:site_name" content="Moving Scapes" />
</svelte:head>

<Header />
<slot />
<Footer />
ymgnymgn

+page.server.ts

Form actionsでCookieにテーマをセットしている。

import {fail, type Actions} from '@sveltejs/kit';
import { isValidTheme } from '../hooks.server';

const TEN_YEARS_IN_SECONDS = 10 * 365 * 24 * 60 * 60;

export const actions: Actions = {
  theme: async ({cookies, request}) => {
    const data = await request.formData();
    const theme = data.get('theme');

    if (!isValidTheme(theme)) {
      return fail(400, {theme, missing: true});
    }
    cookies.set('theme', theme, {path: '/', maxAge: TEN_YEARS_IN_SECONDS});

    return {success: true};
  },
};
ymgnymgn

独自の変更箇所をメモ。

app.htmlのdata-themeを削除して、<html lang="en" class="%THEME%">とした。
導入予定のshadcn-svelteがデフォルトでclass表記でのダークモード対応のため。

src/routes/+layout.svelteも上記と同様の理由で、次のようにclassListを弄るように変更した。

<script lang="ts">
  import { browser } from "$app/environment";
  import Header from "$lib/components/custom/header/header.svelte";
  import type { LayoutServerData } from "./$types";
  import { theme } from "$lib/stores/theme";
  import "../app.postcss";

  export let data: LayoutServerData;

  $theme = data.theme;
  $: if (browser) {
      document.documentElement.classList.remove('light', 'dark');
      document.documentElement.classList.add($theme);
     }
</script>

<Header />
<slot />
このスクラップは2023/09/26にクローズされました