<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>toishi&apos;s blog</title>
		<link>https://toishi.dev</link>
		<description>SvelteKit で作った技術ブログ</description>
		<language>ja</language>
		<lastBuildDate>Tue, 28 Apr 2026 00:00:00 GMT</lastBuildDate>
		<atom:link href="https://toishi.dev/rss.xml" rel="self" type="application/rss+xml" />
		
		<item>
			<title>Claude Code の Hooks 機能 — イベント駆動で自動化する</title>
			<link>https://toishi.dev/posts/claude-code-hooks</link>
			<guid isPermaLink="true">https://toishi.dev/posts/claude-code-hooks</guid>
			<pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
			<description>Claude Code の Hooks を使って、ツール呼び出しやファイル変更時に自動でコマンドを実行する方法を解説します。</description>
			<category>claude-code</category><category>ai</category>
			<content:encoded><![CDATA[<p>Claude Code の <strong>Hooks</strong> は、特定のイベント（ファイル変更、コマンド実行など）が発生したときに、自動でシェルコマンドを実行する機能です。開発ワークフローの自動化に活用できます。</p>
<h2>前提</h2>
<ul>
<li>Claude Code がインストール済みであること</li>
<li><code>~/.claude/settings.json</code> または <code>.claude/settings.json</code> を編集できること</li>
<li>Hook 内で実行するコマンド（Prettier・型チェッカー等）がプロジェクトに導入されていること</li>
</ul>
<h2>Hooks とは</h2>
<p>Hooks は「Claude Code がツールを呼び出すとき」や「ファイルを変更したとき」などのイベントに反応して、指定したコマンドを自動実行する仕組みです。</p>
<p>Git の pre-commit hook に似ていますが、Claude Code 固有のイベントに対応しています。</p>
<h2>設定方法</h2>
<p><code>~/.claude/settings.json</code> または <code>.claude/settings.json</code> に Hooks を定義します。</p>
<pre><code class="language-json">{
	&quot;hooks&quot;: {
		&quot;PreToolUse&quot;: [
			{
				&quot;matcher&quot;: &quot;Edit|Write&quot;,
				&quot;hook&quot;: &quot;echo &#39;ファイルを変更します&#39;&quot;
			}
		],
		&quot;PostToolUse&quot;: [
			{
				&quot;matcher&quot;: &quot;Edit|Write&quot;,
				&quot;hook&quot;: &quot;pnpm lint --fix&quot;
			}
		]
	}
}
</code></pre>
<h2>イベントの種類</h2>
<table>
<thead>
<tr>
<th>イベント</th>
<th>タイミング</th>
</tr>
</thead>
<tbody><tr>
<td><code>PreToolUse</code></td>
<td>ツール実行の前</td>
</tr>
<tr>
<td><code>PostToolUse</code></td>
<td>ツール実行の後</td>
</tr>
<tr>
<td><code>Notification</code></td>
<td>通知発生時</td>
</tr>
<tr>
<td><code>Stop</code></td>
<td>Claude Code の応答完了時</td>
</tr>
</tbody></table>
<h2>実用的な Hook の例</h2>
<h3>1. ファイル変更時に自動フォーマット</h3>
<pre><code class="language-json">{
	&quot;hooks&quot;: {
		&quot;PostToolUse&quot;: [
			{
				&quot;matcher&quot;: &quot;Edit|Write&quot;,
				&quot;hook&quot;: &quot;prettier --write $CLAUDE_FILE_PATH&quot;
			}
		]
	}
}
</code></pre>
<p>Claude Code がファイルを編集・作成するたびに、Prettier で自動フォーマットされます。</p>
<h3>2. ファイル変更時に型チェック</h3>
<pre><code class="language-json">{
	&quot;hooks&quot;: {
		&quot;PostToolUse&quot;: [
			{
				&quot;matcher&quot;: &quot;Edit|Write&quot;,
				&quot;hook&quot;: &quot;pnpm check 2&gt;&amp;1 | head -20&quot;
			}
		]
	}
}
</code></pre>
<h3>3. 特定のファイルの変更をブロック</h3>
<pre><code class="language-json">{
	&quot;hooks&quot;: {
		&quot;PreToolUse&quot;: [
			{
				&quot;matcher&quot;: &quot;Edit|Write&quot;,
				&quot;hook&quot;: &quot;if echo $CLAUDE_FILE_PATH | grep -q &#39;.env&#39;; then echo &#39;BLOCK: .env ファイルの変更は許可されていません&#39; &amp;&amp; exit 1; fi&quot;
			}
		]
	}
}
</code></pre>
<p><code>.env</code> ファイルの変更を防止します。Hook が非ゼロで終了すると、ツールの実行がブロックされます。</p>
<h3>4. 応答完了時に通知</h3>
<pre><code class="language-json">{
	&quot;hooks&quot;: {
		&quot;Stop&quot;: [
			{
				&quot;hook&quot;: &quot;osascript -e &#39;display notification \&quot;Claude Code の作業が完了しました\&quot; with title \&quot;Claude Code\&quot;&#39;&quot;
			}
		]
	}
}
</code></pre>
<p>長いタスクの完了をデスクトップ通知で知らせます（macOS の場合）。</p>
<h2>matcher パターン</h2>
<p><code>matcher</code> にはツール名の正規表現を指定します。</p>
<pre><code class="language-json">// Edit または Write ツールにマッチ
&quot;matcher&quot;: &quot;Edit|Write&quot;

// Bash ツールにマッチ
&quot;matcher&quot;: &quot;Bash&quot;

// すべてのツールにマッチ
&quot;matcher&quot;: &quot;.*&quot;
</code></pre>
<h2>環境変数</h2>
<p>Hook のコマンド内では、以下の環境変数が使えます:</p>
<table>
<thead>
<tr>
<th>変数</th>
<th>説明</th>
</tr>
</thead>
<tbody><tr>
<td><code>CLAUDE_FILE_PATH</code></td>
<td>操作対象のファイルパス</td>
</tr>
<tr>
<td><code>CLAUDE_TOOL_NAME</code></td>
<td>実行されるツール名</td>
</tr>
</tbody></table>
<h2>ブロックの仕組み</h2>
<p><code>PreToolUse</code> の Hook が以下の条件を満たすと、ツールの実行がブロックされます:</p>
<ol>
<li>終了コードが非ゼロ</li>
<li>stdout に <code>BLOCK:</code> で始まるメッセージがある</li>
</ol>
<pre><code class="language-bash"># ブロックする場合
echo &quot;BLOCK: この操作は許可されていません&quot; &amp;&amp; exit 1

# ブロックしない場合（情報表示のみ）
echo &quot;INFO: ファイルを変更します&quot;
</code></pre>
<h2>設定のスコープ</h2>
<table>
<thead>
<tr>
<th>ファイル</th>
<th>スコープ</th>
</tr>
</thead>
<tbody><tr>
<td><code>~/.claude/settings.json</code></td>
<td>グローバル（全プロジェクト）</td>
</tr>
<tr>
<td><code>.claude/settings.json</code></td>
<td>プロジェクト固有</td>
</tr>
</tbody></table>
<p>プロジェクト固有の Hook は、チームで共有できます（Git にコミット可能）。</p>
<h2>注意点</h2>
<ul>
<li>Hook のコマンドが長時間実行されると、Claude Code の応答が遅くなる</li>
<li>エラーが発生した場合、Claude Code のフィードバックとして表示される</li>
<li>Hook は同期的に実行される</li>
</ul>
<h2>まとめ</h2>
<ul>
<li>Hooks はイベント駆動で自動コマンドを実行する機能</li>
<li><code>PreToolUse</code> でツール実行前のチェック、<code>PostToolUse</code> で後処理</li>
<li>ファイルの自動フォーマット、型チェック、変更のブロックなどに活用</li>
<li><code>settings.json</code> で設定し、プロジェクト単位で管理できる</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://docs.anthropic.com/en/docs/claude-code/hooks">Claude Code Hooks ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Claude Code でテストを書く — AI と TDD を実践する</title>
			<link>https://toishi.dev/posts/claude-code-testing</link>
			<guid isPermaLink="true">https://toishi.dev/posts/claude-code-testing</guid>
			<pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate>
			<description>Claude Code を活用したテスト駆動開発（TDD）のワークフロー。テストの生成、実行、修正の流れを紹介します。</description>
			<category>claude-code</category><category>ai</category><category>testing</category>
			<content:encoded><![CDATA[<p>テストを書くのは重要だけど面倒 — そんなときこそ Claude Code の出番です。AI にテストの生成を任せつつ、テスト駆動開発（TDD）のサイクルを回す方法を紹介します。</p>
<h2>前提</h2>
<ul>
<li>Claude Code がインストール済みであること</li>
<li>プロジェクトにテストランナー（Vitest / Jest など）が導入されていること</li>
<li>本記事のサンプルコードは Vitest + <code>vitest-browser-svelte</code> を使った SvelteKit プロジェクトを想定</li>
</ul>
<h2>Claude Code とテスト</h2>
<p>Claude Code はテストに関して以下のことができます:</p>
<ul>
<li>テストコードの生成</li>
<li>テストの実行と結果の確認</li>
<li>失敗したテストの原因分析と修正</li>
<li>テストカバレッジの確認</li>
</ul>
<h2>基本的なワークフロー</h2>
<h3>1. テストを書いてもらう</h3>
<pre><code class="language-text">src/lib/utils/posts.ts の getAllPosts 関数のテストを書いて
</code></pre>
<p>Claude Code はソースコードを読み、適切なテストを生成します。</p>
<pre><code class="language-ts">// src/lib/utils/posts.test.ts
import { describe, expect, it } from &#39;vitest&#39;;
import { getAllPosts } from &#39;./posts&#39;;

describe(&#39;getAllPosts&#39;, () =&gt; {
	it(&#39;記事が日付の降順で返される&#39;, async () =&gt; {
		const posts = await getAllPosts();
		for (let i = 1; i &lt; posts.length; i++) {
			expect(new Date(posts[i - 1].date).getTime()).toBeGreaterThanOrEqual(
				new Date(posts[i].date).getTime()
			);
		}
	});

	it(&#39;下書き記事が含まれない&#39;, async () =&gt; {
		const posts = await getAllPosts();
		expect(posts.every((p) =&gt; !p.draft)).toBe(true);
	});

	it(&#39;すべての記事に必須フィールドがある&#39;, async () =&gt; {
		const posts = await getAllPosts();
		for (const post of posts) {
			expect(post.slug).toBeTruthy();
			expect(post.title).toBeTruthy();
			expect(post.date).toBeTruthy();
		}
	});
});
</code></pre>
<h3>2. テストを実行する</h3>
<pre><code class="language-text">テストを実行して
</code></pre>
<p>Claude Code が <code>pnpm test</code> を実行し、結果を確認します。</p>
<h3>3. 失敗したテストを修正</h3>
<pre><code class="language-text">失敗しているテストを確認して修正して
</code></pre>
<p>失敗の原因を分析し、テストコードまたは実装コードを修正します。</p>
<h2>TDD のサイクル</h2>
<p>Claude Code で TDD（Red → Green → Refactor）を回すことができます。</p>
<h3>Red: まずテストを書く</h3>
<pre><code class="language-text">記事をタグでフィルタリングする関数のテストを先に書いて。
関数はまだ実装しなくていい。
</code></pre>
<pre><code class="language-ts">describe(&#39;getPostsByTag&#39;, () =&gt; {
	it(&#39;指定したタグの記事だけ返される&#39;, async () =&gt; {
		const posts = await getPostsByTag(&#39;svelte&#39;);
		expect(posts.length).toBeGreaterThan(0);
		expect(posts.every((p) =&gt; p.tags.includes(&#39;svelte&#39;))).toBe(true);
	});

	it(&#39;存在しないタグは空配列を返す&#39;, async () =&gt; {
		const posts = await getPostsByTag(&#39;nonexistent-tag&#39;);
		expect(posts).toEqual([]);
	});
});
</code></pre>
<h3>Green: テストを通す実装を書く</h3>
<pre><code class="language-text">テストが通るように getPostsByTag 関数を実装して
</code></pre>
<h3>Refactor: リファクタリング</h3>
<pre><code class="language-text">テストが通ったまま、コードをリファクタリングして
</code></pre>
<h2>コンポーネントテストの生成</h2>
<pre><code class="language-text">PostCard コンポーネントのテストを書いて
</code></pre>
<pre><code class="language-ts">// src/lib/components/blog/PostCard.svelte.test.ts
import { render } from &#39;vitest-browser-svelte&#39;;
import { expect, test } from &#39;vitest&#39;;
import PostCard from &#39;./PostCard.svelte&#39;;

test(&#39;記事タイトルが表示される&#39;, async () =&gt; {
	const screen = render(PostCard, {
		slug: &#39;test&#39;,
		title: &#39;テスト記事&#39;,
		description: &#39;説明文&#39;,
		date: &#39;2026-03-28&#39;,
		tags: [&#39;svelte&#39;]
	});

	await expect.element(screen.getByText(&#39;テスト記事&#39;)).toBeVisible();
});

test(&#39;タグがバッジとして表示される&#39;, async () =&gt; {
	const screen = render(PostCard, {
		slug: &#39;test&#39;,
		title: &#39;テスト記事&#39;,
		description: &#39;説明文&#39;,
		date: &#39;2026-03-28&#39;,
		tags: [&#39;svelte&#39;, &#39;typescript&#39;]
	});

	await expect.element(screen.getByText(&#39;svelte&#39;)).toBeVisible();
	await expect.element(screen.getByText(&#39;typescript&#39;)).toBeVisible();
});
</code></pre>
<h2>効果的な指示のコツ</h2>
<h3>エッジケースを意識させる</h3>
<pre><code class="language-text">getAllPosts のテストを書いて。
特に以下のケースを含めて:
- 記事が0件の場合
- draft: true の記事がある場合
- 日付が同じ記事が複数ある場合
</code></pre>
<h3>テストの種類を指定する</h3>
<pre><code class="language-text">ユニットテストだけ書いて（外部依存なし）
インテグレーションテストを書いて（実際のファイルシステムを使う）
</code></pre>
<h3>既存のテストのスタイルに合わせる</h3>
<pre><code class="language-text">既存のテストファイルのスタイルに合わせて書いて
</code></pre>
<p>Claude Code は既存のテストコードを読み、同じスタイル（describe/it の構造、アサーションの書き方）に合わせます。</p>
<h2>テスト実行の自動化</h2>
<pre><code class="language-text">テストを実行して、失敗があればすべて修正して。
全部通るまで繰り返して。
</code></pre>
<p>Claude Code はテスト → 修正のサイクルを自動で回してくれます。</p>
<h2>まとめ</h2>
<ul>
<li>Claude Code にテスト生成を任せると、基本的なケースからエッジケースまでカバーできる</li>
<li>TDD のサイクル（Red → Green → Refactor）を AI と一緒に回せる</li>
<li>失敗したテストの原因分析と修正も自動化できる</li>
<li>既存のテストスタイルに合わせたコード生成が可能</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://docs.anthropic.com/en/docs/claude-code/overview">Claude Code ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Claude Code の Plan モードで設計してから実装する</title>
			<link>https://toishi.dev/posts/claude-code-plan-mode</link>
			<guid isPermaLink="true">https://toishi.dev/posts/claude-code-plan-mode</guid>
			<pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate>
			<description>Claude Code の Plan モードを使って、実装前に設計を固めるワークフロー。コードの変更なしに調査・計画ができます。</description>
			<category>claude-code</category><category>ai</category>
			<content:encoded><![CDATA[<p>大きな変更を加えるとき、いきなりコードを書き始めると方向性を見失うことがあります。Claude Code の <strong>Plan モード</strong> を使えば、コードを一切変更せずに調査と設計を行えます。</p>
<h2>前提</h2>
<ul>
<li>Claude Code がインストール済みで、ターミナルから <code>claude</code> を起動できること</li>
<li>調査対象のコードベースが手元にあること（Plan モードはファイルを読んで設計するため）</li>
</ul>
<h2>Plan モードとは</h2>
<p>Plan モードは、Claude Code に「読み取り専用」の制約を課すモードです。ファイルの読み込みや検索はできますが、ファイルの編集やコマンドの実行は行いません。</p>
<h2>Plan モードの起動</h2>
<pre><code class="language-text">/plan タグフィルタリング機能を追加したい
</code></pre>
<p>または、会話の途中で:</p>
<pre><code class="language-text">Plan モードに切り替えて
</code></pre>
<h2>ワークフロー</h2>
<p>Plan モードは以下のフェーズで進みます。</p>
<h3>Phase 1: 調査</h3>
<p>Claude Code がコードベースを探索して、現状を理解します。</p>
<ul>
<li>関連するファイルの特定</li>
<li>既存の実装パターンの把握</li>
<li>再利用できる関数やコンポーネントの発見</li>
</ul>
<pre><code class="language-text">Claude: 以下のファイルを確認しました:
- src/lib/utils/posts.ts — getAllPosts() でタグ情報を取得済み
- src/routes/tags/[tag]/+page.ts — タグ別一覧は既に実装済み
- src/lib/components/blog/PostCard.svelte — タグの表示なし
</code></pre>
<h3>Phase 2: 設計</h3>
<p>調査結果に基づいて、実装計画を立てます。</p>
<pre><code class="language-markdown">## 実装計画

### 変更するファイル

1. src/lib/components/blog/PostCard.svelte
   - tags props を追加
   - Badge コンポーネントでタグを表示

2. src/routes/+page.svelte
   - PostCard に tags を渡す

### 再利用する既存コード

- Badge コンポーネント（src/lib/components/ui/badge）
- getAllTags()（src/lib/utils/posts.ts）
</code></pre>
<h3>Phase 3: 確認</h3>
<p>ユーザーに計画の確認を求めます。</p>
<pre><code class="language-text">Claude: この計画で進めてよろしいですか？
修正したい点があればお知らせください。
</code></pre>
<h3>Phase 4: 実行</h3>
<p>Plan モードを終了して実装に移ります。計画ファイルに基づいて、効率的にコードを修正します。</p>
<h2>Plan モードが有効な場面</h2>
<h3>大きなリファクタリング</h3>
<p>影響範囲を事前に把握してから作業できます。</p>
<pre><code class="language-text">/plan コンポーネントの状態管理を Store から Runes に移行したい
</code></pre>
<h3>新機能の追加</h3>
<p>既存コードとの整合性を確認してから設計できます。</p>
<pre><code class="language-text">/plan RSS フィード生成機能を追加したい
</code></pre>
<h3>アーキテクチャの検討</h3>
<p>コードを読んで現状を把握し、改善案を提案してもらえます。</p>
<pre><code class="language-text">/plan パフォーマンス改善のためにできることを調査して
</code></pre>
<h2>Plan モードの利点</h2>
<h3>1. 安全にコードを探索できる</h3>
<p>ファイルが変更されないので、安心してコードベースを調査できます。</p>
<h3>2. 実装の方向性を事前に合意できる</h3>
<p>計画を確認してから実装に入るので、手戻りが減ります。</p>
<h3>3. 見落としを防げる</h3>
<p>Claude Code が関連するファイルを網羅的に調査してくれるので、影響範囲の見落としが減ります。</p>
<h2>計画ファイル</h2>
<p>Plan モード中に作成された計画は、ファイルとして保存されます。実装フェーズではこのファイルを参照しながら作業が進みます。</p>
<pre><code class="language-text">~/.claude/plans/
└── golden-popping-star.md  ← 計画ファイル
</code></pre>
<h2>まとめ</h2>
<ul>
<li>Plan モードはコード変更なしに調査・設計ができる</li>
<li>調査 → 設計 → 確認 → 実行のフェーズで進む</li>
<li>大きな変更やリファクタリングの前に使うと効果的</li>
<li>計画を事前に合意することで手戻りを減らせる</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://docs.anthropic.com/en/docs/claude-code/overview">Claude Code ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Claude Code のメモリ機能 — 会話を跨いでコンテキストを保持する</title>
			<link>https://toishi.dev/posts/claude-code-memory</link>
			<guid isPermaLink="true">https://toishi.dev/posts/claude-code-memory</guid>
			<pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate>
			<description>Claude Code のメモリシステムの仕組みと活用方法。ユーザー情報、フィードバック、プロジェクト情報を永続化する方法を解説します。</description>
			<category>claude-code</category><category>ai</category>
			<content:encoded><![CDATA[<p>Claude Code は会話が終わると通常コンテキストを失いますが、<strong>メモリ機能</strong>を使うことで、重要な情報を会話を跨いで保持できます。</p>
<h2>前提</h2>
<ul>
<li>Claude Code がインストール済みであること</li>
<li>メモリ機能をサポートしているバージョンであること（最近のリリースで利用可能）</li>
<li><code>~/.claude/projects/&lt;project&gt;/memory/</code> ディレクトリへの書き込み権限があること</li>
</ul>
<h2>メモリの仕組み</h2>
<p>メモリはファイルベースのシステムで、<code>~/.claude/projects/&lt;project&gt;/memory/</code> ディレクトリに Markdown ファイルとして保存されます。</p>
<pre><code class="language-text">~/.claude/projects/blog-app/memory/
├── MEMORY.md           ← インデックスファイル
├── user_role.md         ← ユーザー情報
├── feedback_testing.md  ← フィードバック
└── project_deploy.md    ← プロジェクト情報
</code></pre>
<p><code>MEMORY.md</code> はインデックスで、各メモリファイルへのリンクと概要が記載されます。</p>
<h2>メモリの種類</h2>
<h3>1. user — ユーザー情報</h3>
<p>ユーザーの役割、経験、好みに関する情報です。</p>
<pre><code class="language-markdown">---
name: ユーザーの技術背景
description: ユーザーの技術スタックと経験レベル
type: user
---

バックエンドエンジニアで、Go を10年書いている。
フロントエンド（Svelte）は初めて触る。
</code></pre>
<p>この情報があると、Claude Code はフロントエンドの説明をするときにバックエンドの概念に例えて説明してくれます。</p>
<h3>2. feedback — フィードバック</h3>
<p>作業の進め方に関するユーザーからの指示です。</p>
<pre><code class="language-markdown">---
name: テストの書き方
description: テストに関するユーザーの好み
type: feedback
---

インテグレーションテストでは DB のモックを使わず、実際の DB に接続する。
**Why:** 過去にモックと本番の乖離でデプロイ障害が発生した。
**How to apply:** テストコードを書くときは、常に実 DB 接続を前提にする。
</code></pre>
<h3>3. project — プロジェクト情報</h3>
<p>進行中の作業、意思決定、締め切りなどの情報です。</p>
<pre><code class="language-markdown">---
name: マージフリーズ
description: モバイルリリースに伴うマージフリーズの予定
type: project
---

2026-03-25 以降、非クリティカルな PR のマージを凍結する。
**Why:** モバイルチームがリリースブランチを切るため。
**How to apply:** この日以降の PR 作成時に警告する。
</code></pre>
<h3>4. reference — 外部リソースへの参照</h3>
<p>外部システムやドキュメントの場所です。</p>
<pre><code class="language-markdown">---
name: バグトラッカー
description: バグ管理に使っているツールの情報
type: reference
---

パイプラインのバグは Linear プロジェクト「INGEST」で管理している。
</code></pre>
<h2>メモリの保存方法</h2>
<p>Claude Code に「覚えておいて」と伝えるだけです。</p>
<pre><code class="language-text">データベースのテストではモックを使わないでほしい。覚えておいて。
</code></pre>
<p>Claude Code がメモリファイルを作成し、<code>MEMORY.md</code> のインデックスを更新します。</p>
<h2>メモリの活用場面</h2>
<h3>次回の会話で自動参照</h3>
<p>新しい会話を始めたとき、Claude Code は <code>MEMORY.md</code> を読み込み、関連するメモリを参照します。</p>
<pre><code class="language-text"># 新しい会話
テストを書いて

# Claude Code は過去のフィードバックを参照して、
# DB モックを使わないテストを書いてくれる
</code></pre>
<h3>明示的に参照</h3>
<pre><code class="language-text">前に話したデプロイの件、どうなってたっけ？
</code></pre>
<h2>メモリに保存すべきでないもの</h2>
<ul>
<li><strong>コードのパターンや規約</strong> — コードを見れば分かる</li>
<li><strong>Git の履歴</strong> — <code>git log</code> で確認できる</li>
<li><strong>一時的な作業内容</strong> — 現在の会話で完結する情報</li>
<li><strong>CLAUDE.md に書いてあること</strong> — 二重管理になる</li>
</ul>
<h2>メモリの管理</h2>
<h3>更新</h3>
<p>情報が古くなったら、Claude Code に更新を依頼します。</p>
<pre><code class="language-text">マージフリーズは解除された。メモリを更新して。
</code></pre>
<h3>削除</h3>
<p>不要になったメモリも削除できます。</p>
<pre><code class="language-text">デプロイに関するメモリはもう不要。削除して。
</code></pre>
<h2>まとめ</h2>
<ul>
<li>メモリはファイルベースの永続化システム</li>
<li>ユーザー情報、フィードバック、プロジェクト情報、外部参照の 4 種類</li>
<li>「覚えておいて」で保存、次回の会話で自動参照</li>
<li>コードから分かる情報は保存しない</li>
<li>古くなったメモリは適宜更新・削除する</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://docs.anthropic.com/en/docs/claude-code/memory">Claude Code メモリ ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>CLAUDE.md 活用術 — プロジェクト固有のルールを AI に伝える</title>
			<link>https://toishi.dev/posts/claude-code-claude-md</link>
			<guid isPermaLink="true">https://toishi.dev/posts/claude-code-claude-md</guid>
			<pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
			<description>CLAUDE.md ファイルを使って Claude Code にプロジェクトのコンテキストを伝える方法。効果的な書き方と実例を紹介します。</description>
			<category>claude-code</category><category>ai</category>
			<content:encoded><![CDATA[<p>Claude Code を使うとき、毎回プロジェクトの説明をするのは非効率です。<code>CLAUDE.md</code> ファイルにプロジェクトの情報を書いておけば、Claude Code が自動で読み込んで理解してくれます。</p>
<h2>前提</h2>
<ul>
<li>Claude Code がインストール済みで、ターミナルから <code>claude</code> コマンドを実行できること</li>
<li>設定対象のプロジェクトがあるか、グローバル設定の場合 <code>~/.claude/</code> ディレクトリが存在すること</li>
</ul>
<h2>CLAUDE.md とは</h2>
<p>プロジェクトのルートに配置する Markdown ファイルで、Claude Code が会話開始時に自動で読み込みます。プロジェクト固有のルール、コマンド、アーキテクチャなどを記述します。</p>
<h2>配置場所</h2>
<table>
<thead>
<tr>
<th>パス</th>
<th>スコープ</th>
</tr>
</thead>
<tbody><tr>
<td><code>~/.claude/CLAUDE.md</code></td>
<td>グローバル（全プロジェクト共通）</td>
</tr>
<tr>
<td><code>プロジェクトルート/CLAUDE.md</code></td>
<td>プロジェクト固有</td>
</tr>
</tbody></table>
<p>グローバル設定には言語設定や共通ルールを、プロジェクト設定にはプロジェクト固有の情報を書きます。</p>
<h2>書くべき内容</h2>
<h3>1. よく使うコマンド</h3>
<pre><code class="language-markdown">## コマンド

- `pnpm dev` — 開発サーバー起動
- `pnpm build` — 本番ビルド
- `pnpm check` — 型チェック
- `pnpm lint` — リント実行
- `pnpm test` — テスト実行
</code></pre>
<p>Claude Code がビルドやテストを実行するとき、正しいコマンドを使ってくれます。</p>
<h3>2. アーキテクチャの概要</h3>
<pre><code class="language-markdown">## アーキテクチャ

- デプロイ先: Cloudflare Pages
- 記事は `src/posts/*.md` に Markdown で配置
- すべてのルートは `prerender = true`（静的生成）
- UI コンポーネントは shadcn-svelte ベース
</code></pre>
<h3>3. コーディング規約</h3>
<pre><code class="language-markdown">## 規約

- Svelte 5 Runes を使用（$state, $derived, $effect）
- `.md` ファイル以外は runes モード強制
- 日本語でコメント・ドキュメントを記述
</code></pre>
<h3>4. 注意事項</h3>
<pre><code class="language-markdown">## 注意

- 開発サーバーでは Pagefind（全文検索）が動作しない
- 検索機能のテストは `pnpm build &amp;&amp; pnpm preview` で確認
</code></pre>
<h2>このブログの CLAUDE.md</h2>
<p>実際にこのプロジェクトで使っている CLAUDE.md の抜粋です:</p>
<pre><code class="language-markdown"># CLAUDE.md

## General

このプロジェクトのドキュメントやCLAUDE.mdは日本語で記述すること。

## コマンド

pnpm dev # 開発サーバー起動
pnpm build # 本番ビルド
pnpm check # 型チェック
pnpm test # 全テスト実行

## アーキテクチャ

**デプロイ先**: Cloudflare Pages
**記事管理**: src/posts/\*.md に Markdown ファイルを置く
**UI コンポーネント**: shadcn-svelte / bits-ui ベース
**Svelte**: Svelte 5 Runes モード強制
</code></pre>
<h2>効果的な書き方のコツ</h2>
<h3>簡潔に書く</h3>
<p>CLAUDE.md が長すぎると、重要な情報が埋もれます。箇条書きや表を使って簡潔にまとめましょう。</p>
<h3>「なぜ」を書く</h3>
<p>ルールだけでなく理由も書くと、Claude Code が意図を理解してエッジケースでも適切な判断をしてくれます。</p>
<pre><code class="language-markdown"># ❌ ルールだけ

- `adapter-static` を使わない

# ✅ 理由も書く

- `adapter-cloudflare` を使用（将来の SSR 対応を見据えて）
</code></pre>
<h3>変更があったら更新する</h3>
<p>プロジェクトの構成が変わったら CLAUDE.md も更新しましょう。古い情報があると、Claude Code が間違った前提で作業する可能性があります。</p>
<h2>グローバル CLAUDE.md</h2>
<p><code>~/.claude/CLAUDE.md</code> にはプロジェクト横断の設定を書きます。</p>
<pre><code class="language-markdown"># グローバル設定

## 言語

- 常に日本語で回答する
- コード・コマンド・技術用語はそのまま英語

## 行動指針

- 破壊的な操作は事前に確認する
- 結論を先に伝える
</code></pre>
<h2>まとめ</h2>
<p>CLAUDE.md は Claude Code との「共有ドキュメント」です。プロジェクトのコンテキストを事前に伝えておくことで、毎回の説明が不要になり、より的確なアウトプットが得られます。Git にコミットしておけば、チームメンバーも同じコンテキストで Claude Code を使えます。</p>
<h2>参照</h2>
<ul>
<li><a href="https://claude.ai/code">Claude Code 公式サイト</a></li>
<li><a href="https://docs.anthropic.com/en/docs/claude-code/memory">Claude Code — CLAUDE.md ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Claude Code でブログを作った話 — このサイトができるまで</title>
			<link>https://toishi.dev/posts/claude-code-blog-development</link>
			<guid isPermaLink="true">https://toishi.dev/posts/claude-code-blog-development</guid>
			<pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate>
			<description>このブログサイトを Claude Code を使って構築した過程を振り返ります。AI とのペアプログラミングで得た知見と実践的なワークフローを共有します。</description>
			<category>claude-code</category><category>ai</category><category>svelte-kit</category>
			<content:encoded><![CDATA[<p>このブログは Claude Code を使って構築しました。プロジェクトの初期化からデプロイまで、AI とのペアプログラミングでどのように進めたかを振り返ります。</p>
<h2>最初のステップ — プロジェクト設計</h2>
<p>まず Claude Code にやりたいことを伝えました。</p>
<pre><code class="language-text">SvelteKit + Tailwind CSS + shadcn-svelte で
Markdown ベースの技術ブログを作りたい。
Cloudflare Pages にデプロイする。
全文検索は Pagefind を使う。
</code></pre>
<p>Claude Code はこの要件を受けて、プロジェクトの構成を提案くれます。ディレクトリ構造、必要なパッケージ、設定ファイルの内容まで、一通りの計画を立ててくれます。</p>
<h2>段階的な実装</h2>
<p>一度にすべてを作ろうとせず、機能ごとに段階的に進めました。</p>
<h3>Phase 1: 基盤構築</h3>
<pre><code class="language-text">1. SvelteKit プロジェクトの初期化
2. Tailwind CSS + shadcn-svelte の設定
3. 基本的なレイアウト（ヘッダー、フッター）
</code></pre>
<h3>Phase 2: 記事管理</h3>
<pre><code class="language-text">1. mdsvex の設定
2. 記事用レイアウトコンポーネント
3. 記事一覧ページ
4. 記事詳細ページ
</code></pre>
<h3>Phase 3: 機能追加</h3>
<pre><code class="language-text">1. タグによるフィルタリング
2. Pagefind による全文検索
3. ダークモード
</code></pre>
<h2>Claude Code が特に役立った場面</h2>
<h3>設定ファイルの作成</h3>
<p><code>svelte.config.js</code>、<code>vite.config.ts</code>、ESLint、Prettier などの設定ファイルは、バージョンによって書き方が変わるため調べるのが大変です。Claude Code はプロジェクトの依存関係を確認した上で、適切なバージョンの設定を生成してくれます。</p>
<h3>shadcn-svelte コンポーネントのカスタマイズ</h3>
<p>shadcn-svelte のコンポーネントを追加した後、ブログに合わせたカスタマイズも Claude Code に依頼しました。既存のコードスタイルを理解した上で修正してくれるので、一貫性のあるコードが維持できます。</p>
<h3>デバッグ</h3>
<p>ビルドエラーや型エラーが発生したとき、エラーメッセージを貼り付けるだけで原因を特定し、修正を提案してくれます。特に TypeScript の型エラーは、コンテキストを理解した上での修正が必要なので、AI の支援が効果的でした。</p>
<h2>学んだこと</h2>
<h3>AI に任せて良いこと</h3>
<ul>
<li><strong>定型的な設定ファイル</strong> — 正確さが求められるが創造性は不要</li>
<li><strong>ボイラープレートコード</strong> — ルーティング、データ取得のパターン</li>
<li><strong>リファクタリング</strong> — 既存のパターンに沿った改善</li>
</ul>
<h3>人間が判断すべきこと</h3>
<ul>
<li><strong>アーキテクチャの選択</strong> — どの技術を使うか</li>
<li><strong>デザインの方向性</strong> — 見た目の好み</li>
<li><strong>機能の優先順位</strong> — 何を先に作るか</li>
</ul>
<h2>ワークフローのコツ</h2>
<h3>1. CLAUDE.md を活用する</h3>
<p>プロジェクトのルールや構成を <code>CLAUDE.md</code> に書いておくと、Claude Code が毎回コンテキストを理解してくれます。</p>
<pre><code class="language-markdown"># CLAUDE.md

## コマンド

- pnpm dev — 開発サーバー
- pnpm build — ビルド

## アーキテクチャ

- 記事は src/posts/\*.md に配置
- すべてのルートは prerender = true
</code></pre>
<h3>2. 小さな単位で依頼する</h3>
<p>「ブログ全体を作って」より「記事一覧ページを作って」の方が品質の高い結果が得られます。</p>
<h3>3. 生成されたコードを理解する</h3>
<p>AI が書いたコードをそのまま使うのではなく、内容を理解してから採用しましょう。理解できないコードは、説明を求めてから判断します。</p>
<h2>まとめ</h2>
<p>Claude Code との協業により、技術選定からデプロイまでの開発プロセスが大幅に効率化されました。特に設定ファイルの生成やデバッグでの時間節約が大きかった。AI は万能ではありませんが、適切に活用すれば強力な開発パートナーになります。</p>
<h2>参照</h2>
<ul>
<li><a href="https://claude.ai/code">Claude Code 公式サイト</a></li>
<li><a href="https://docs.anthropic.com/en/docs/claude-code/overview">Claude Code ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Cloudflare Pages のプレビュー環境を活用する — PR ごとの自動デプロイ</title>
			<link>https://toishi.dev/posts/cloudflare-pages-preview</link>
			<guid isPermaLink="true">https://toishi.dev/posts/cloudflare-pages-preview</guid>
			<pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
			<description>Cloudflare Pages のプレビューデプロイ機能を使って、PR ごとに確認環境を自動生成する方法を紹介します。</description>
			<category>cloudflare</category>
			<content:encoded><![CDATA[<p>Cloudflare Pages には<strong>プレビューデプロイ</strong>という機能があります。プルリクエストやブランチごとに固有の URL が自動生成され、マージ前にサイトの動作確認ができます。</p>
<h2>前提</h2>
<ul>
<li>Cloudflare Pages にデプロイ済みのプロジェクトがあること</li>
<li>GitHub または GitLab とのリポジトリ連携が設定済みであること（プレビューは Git 連携プロジェクトでのみ有効）</li>
</ul>
<h2>プレビューデプロイの仕組み</h2>
<p>Git 連携を設定している場合、以下のタイミングで自動デプロイされます:</p>
<table>
<thead>
<tr>
<th>イベント</th>
<th>デプロイ先</th>
</tr>
</thead>
<tbody><tr>
<td>main ブランチへの push</td>
<td>プロダクション（<code>project.pages.dev</code>）</td>
</tr>
<tr>
<td>その他のブランチへの push</td>
<td>プレビュー（<code>&lt;commit-hash&gt;.project.pages.dev</code>）</td>
</tr>
<tr>
<td>PR の作成・更新</td>
<td>プレビュー（PR にコメントで URL が通知される）</td>
</tr>
</tbody></table>
<h2>PR へのコメント通知</h2>
<p>PR を作成すると、Cloudflare Pages のボットが自動でコメントを投稿します。</p>
<pre><code class="language-text">Deploying with Cloudflare Pages

Latest commit: abc1234
Status: ✅ Deploy successful!
Preview URL: https://abc1234.blog-app.pages.dev
</code></pre>
<p>レビュアーはこの URL にアクセスするだけで、変更後のサイトを確認できます。</p>
<h2>ブランチごとの URL</h2>
<p>プレビューデプロイの URL はいくつかのパターンがあります:</p>
<ul>
<li><code>&lt;commit-hash&gt;.project.pages.dev</code> — コミットハッシュベース</li>
<li><code>&lt;branch-name&gt;.project.pages.dev</code> — ブランチ名ベース</li>
</ul>
<p>ブランチ名ベースの URL は、同じブランチの最新コミットを常に反映します。</p>
<h2>プレビューブランチの制御</h2>
<p>デフォルトではすべてのブランチがプレビュー対象ですが、設定で制御できます。</p>
<ol>
<li>ダッシュボードでプロジェクトを開く</li>
<li><strong>Settings</strong> → <strong>Builds &amp; deployments</strong></li>
<li><strong>Configure preview deployments</strong> で設定:<ul>
<li><strong>All non-production branches</strong> — すべてのブランチ（デフォルト）</li>
<li><strong>Custom branches</strong> — 特定のブランチパターンのみ</li>
<li><strong>None</strong> — プレビュー無効</li>
</ul>
</li>
</ol>
<p>ビルド回数を節約したい場合は、特定のブランチパターン（例: <code>feature/*</code>）だけに限定できます。</p>
<h2>プレビュー環境の環境変数</h2>
<p>プレビュー環境用の環境変数を個別に設定できます。</p>
<ol>
<li><strong>Settings</strong> → <strong>Environment variables</strong></li>
<li><strong>Preview</strong> タブで変数を設定</li>
</ol>
<pre><code class="language-text"># プレビュー環境専用の設定例
PUBLIC_SITE_URL=https://preview.blog-app.pages.dev
ENABLE_DRAFT_POSTS=true
</code></pre>
<p>これを利用して、プレビュー環境では下書き記事を表示するといった使い方もできます。</p>
<h2>活用シーン</h2>
<h3>記事のレビュー</h3>
<p>ブログ記事を書いたら、PR を作成してプレビュー URL を共有。実際の見た目を確認してからマージできます。</p>
<h3>デザイン変更の確認</h3>
<p>レイアウトやスタイルの変更時に、本番環境に影響を与えずに確認。スマートフォンでの表示もプレビュー URL で確認できます。</p>
<h3>複数の変更を並行して確認</h3>
<p>ブランチごとに別の URL が生成されるので、複数の変更を同時に確認・比較できます。</p>
<h2>ローカルプレビューとの使い分け</h2>
<pre><code class="language-bash">pnpm preview  # ローカルで確認（自分だけ見える）
</code></pre>
<p>ローカルプレビュー（<code>pnpm preview</code>）は即座に確認できますが、他の人には共有できません。Cloudflare Pages のプレビューデプロイは URL を共有するだけでチームメンバーに確認してもらえます。</p>
<h2>まとめ</h2>
<ul>
<li>PR ごとにプレビュー URL が自動生成される</li>
<li>環境変数をプレビュー専用に設定できる</li>
<li>ビルド対象ブランチを制限してビルド回数を節約できる</li>
<li>記事レビューやデザイン確認に活用できる</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://developers.cloudflare.com/pages/configuration/preview-deployments/">Cloudflare Pages プレビューデプロイ ドキュメント</a></li>
<li><a href="https://pages.cloudflare.com/">Cloudflare Pages 公式サイト</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Cloudflare Pages に SvelteKit をデプロイする手順と Tips</title>
			<link>https://toishi.dev/posts/cloudflare-pages-deploy</link>
			<guid isPermaLink="true">https://toishi.dev/posts/cloudflare-pages-deploy</guid>
			<pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
			<description>SvelteKit プロジェクトを Cloudflare Pages にデプロイする方法。adapter-cloudflare の設定からカスタムドメインまで解説します。</description>
			<category>cloudflare</category><category>svelte-kit</category>
			<content:encoded><![CDATA[<p>Cloudflare Pages は静的サイトとサーバーレスファンクションの両方をホスティングできるプラットフォームです。SvelteKit の <code>adapter-cloudflare</code> を使えば、シームレスにデプロイできます。</p>
<h2>前提</h2>
<ul>
<li>Cloudflare のアカウント（無料プラン可）</li>
<li>デプロイ対象の SvelteKit プロジェクト</li>
<li>GitHub または GitLab のリポジトリ（Git 連携を使う場合）</li>
<li>Node.js v22 以上（v24 を推奨。本記事のビルド設定例で <code>NODE_VERSION=24</code> を使用）</li>
</ul>
<h2>adapter-cloudflare のセットアップ</h2>
<pre><code class="language-bash">pnpm add -D @sveltejs/adapter-cloudflare
</code></pre>
<pre><code class="language-js">// svelte.config.js
import adapter from &#39;@sveltejs/adapter-cloudflare&#39;;

const config = {
	kit: {
		adapter: adapter({
			routes: {
				exclude: [&#39;&lt;build&gt;&#39;, &#39;&lt;prerendered&gt;&#39;, &#39;&lt;files&gt;&#39;, &#39;/pagefind/*&#39;]
			}
		})
	}
};
</code></pre>
<p><code>routes.exclude</code> で静的ファイルや Pagefind のインデックスを Functions の処理対象から除外しています。</p>
<h2>Cloudflare ダッシュボードからデプロイ</h2>
<ol>
<li>GitHub にリポジトリを push</li>
<li>Cloudflare ダッシュボード → <strong>Workers &amp; Pages</strong> → <strong>Create</strong> → <strong>Pages</strong> → <strong>Connect to Git</strong></li>
<li>ビルド設定を入力:</li>
</ol>
<table>
<thead>
<tr>
<th>項目</th>
<th>値</th>
</tr>
</thead>
<tbody><tr>
<td>Framework preset</td>
<td>SvelteKit</td>
</tr>
<tr>
<td>Build command</td>
<td><code>pnpm build</code></td>
</tr>
<tr>
<td>Build output directory</td>
<td><code>.svelte-kit/cloudflare</code></td>
</tr>
</tbody></table>
<ol start="4">
<li>環境変数を追加:</li>
</ol>
<table>
<thead>
<tr>
<th>変数</th>
<th>値</th>
</tr>
</thead>
<tbody><tr>
<td><code>NODE_VERSION</code></td>
<td><code>24</code></td>
</tr>
</tbody></table>
<ol start="5">
<li><strong>Save and Deploy</strong> をクリック</li>
</ol>
<p>初回ビルドには数分かかります。完了すると <code>https://&lt;project-name&gt;.pages.dev</code> でサイトが公開されます。</p>
<h2>wrangler CLI でのデプロイ</h2>
<p>ダッシュボードを使わず、CLI からもデプロイできます。</p>
<pre><code class="language-bash">pnpm build
pnpm wrangler pages deploy .svelte-kit/cloudflare
</code></pre>
<p>初回は対話的にプロジェクト名を聞かれます。2 回目以降はプロジェクト名を指定できます。</p>
<h2>カスタムドメインの設定</h2>
<ol>
<li>Cloudflare ダッシュボードでプロジェクトを開く</li>
<li><strong>Custom domains</strong> → <strong>Set up a custom domain</strong></li>
<li>ドメイン名を入力（例: <code>blog.example.com</code>）</li>
<li>DNS レコードが自動で追加される</li>
</ol>
<p>Cloudflare で DNS を管理している場合は、SSL 証明書も自動で発行されます。</p>
<h2>環境変数とシークレット</h2>
<p>ビルド時に必要な環境変数は、ダッシュボードの <strong>Settings</strong> → <strong>Environment variables</strong> で設定します。</p>
<pre><code class="language-text"># ビルド時に使う環境変数
PUBLIC_SITE_URL=https://blog.example.com
</code></pre>
<p>SvelteKit では <code>$env/static/public</code> や <code>$env/static/private</code> でアクセスできます。</p>
<pre><code class="language-ts">import { PUBLIC_SITE_URL } from &#39;$env/static/public&#39;;
</code></pre>
<h2>ビルドキャッシュ</h2>
<p>Cloudflare Pages はデフォルトで <code>node_modules</code> をキャッシュします。依存関係が変わらない限りインストールがスキップされ、ビルドが高速化されます。</p>
<h2>Web Analytics</h2>
<p>Cloudflare Pages には無料の Web Analytics が付属しています。</p>
<ol>
<li>ダッシュボードでプロジェクトを開く</li>
<li><strong>Web Analytics</strong> → <strong>Enable</strong></li>
</ol>
<p>JavaScript のスニペットが自動挿入され、ページビューやリファラーなどの基本的なアナリティクスが取得できます。プライバシーに配慮した設計で、Cookie を使用しません。</p>
<h2>_headers と _redirects</h2>
<p>静的ファイルで HTTP ヘッダーやリダイレクトを設定できます。</p>
<pre><code class="language-text"># static/_headers
/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff

/assets/*
  Cache-Control: public, max-age=31536000, immutable
</code></pre>
<pre><code class="language-text"># static/_redirects
/old-path /new-path 301
/blog/* /posts/:splat 301
</code></pre>
<h2>料金</h2>
<p>Cloudflare Pages の無料プラン:</p>
<ul>
<li>ビルド: 500 回/月</li>
<li>帯域: 無制限</li>
<li>サイト数: 無制限</li>
<li>カスタムドメイン: 無制限</li>
</ul>
<p>個人ブログには十分すぎるスペックです。</p>
<h2>まとめ</h2>
<p>Cloudflare Pages は SvelteKit との相性が良く、無料で高速なデプロイ環境を提供してくれます。Git 連携による自動デプロイ、プレビュー環境、Web Analytics など、ブログ運営に必要な機能が揃っています。</p>
<h2>参照</h2>
<ul>
<li><a href="https://pages.cloudflare.com/">Cloudflare Pages 公式サイト</a></li>
<li><a href="https://developers.cloudflare.com/pages/">Cloudflare Pages ドキュメント</a></li>
<li><a href="https://github.com/sveltejs/kit/tree/main/packages/adapter-cloudflare">@sveltejs/adapter-cloudflare GitHub</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>SvelteKit で OGP 画像を自動生成する — satori + resvg-js でプリレンダリング</title>
			<link>https://toishi.dev/posts/sveltekit-og-image</link>
			<guid isPermaLink="true">https://toishi.dev/posts/sveltekit-og-image</guid>
			<pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
			<description>satori と resvg-js を使って、SvelteKit のビルド時に記事ごとの OGP 画像を静的生成する方法を解説します。</description>
			<category>svelte-kit</category><category>satori</category><category>cloudflare</category>
			<content:encoded><![CDATA[<p>SNS でシェアされたときに表示される OGP 画像（<code>og:image</code>）は、クリック率に影響する重要な要素です。記事ごとに専用の画像を用意するのが理想ですが、手作業では限界があります。</p>
<p>SvelteKit のプリレンダリングと <a href="https://github.com/vercel/satori">satori</a> を組み合わせると、ビルド時に各記事の OGP 画像を自動生成できます。</p>
<h2>前提</h2>
<ul>
<li>SvelteKit プロジェクト（本記事は <code>adapter-cloudflare</code> を使った構成を想定）</li>
<li><code>prerender = true</code> で静的出力する設計になっていること</li>
<li>Node.js v22 以上（<code>@resvg/resvg-js</code> のネイティブモジュールが必要）</li>
<li>日本語を表示する場合は Noto Sans JP などの TTF フォントファイルを用意</li>
</ul>
<h2>使うパッケージ</h2>
<ul>
<li><strong>satori</strong>: HTML/CSS（Flexbox）の記述から SVG を生成するライブラリ（Vercel 製）</li>
<li><strong>@resvg/resvg-js</strong>: SVG → PNG 変換</li>
</ul>
<pre><code class="language-bash">pnpm add -D satori @resvg/resvg-js
</code></pre>
<p>日本語テキストをレンダリングするには、フォントファイルが必要です。<a href="https://fonts.google.com/noto/specimen/Noto+Sans+JP">Google Fonts</a> から Noto Sans JP の TTF をダウンロードして <code>src/lib/server/fonts/</code> に置きます。</p>
<h2>画像生成関数</h2>
<p><code>src/lib/server/og.ts</code> に生成ロジックをまとめます。</p>
<pre><code class="language-ts">import satori from &#39;satori&#39;;
import { Resvg } from &#39;@resvg/resvg-js&#39;;
import { readFileSync } from &#39;node:fs&#39;;
import { resolve } from &#39;node:path&#39;;

const fontRegular = readFileSync(resolve(&#39;src/lib/server/fonts/NotoSansJP-Regular.ttf&#39;));
const fontBold = readFileSync(resolve(&#39;src/lib/server/fonts/NotoSansJP-Bold.ttf&#39;));

export async function generateOgImage(options: {
	title: string;
	date?: string;
	slug?: string;
}): Promise&lt;Buffer&gt; {
	const svg = await satori(
		{
			type: &#39;div&#39;,
			props: {
				style: {
					width: &#39;100%&#39;,
					height: &#39;100%&#39;,
					display: &#39;flex&#39;,
					flexDirection: &#39;column&#39;,
					background: &#39;#0f0f23&#39;,
					padding: &#39;60px 72px&#39;,
					fontFamily: &#39;Noto Sans JP&#39;
				},
				children: [
					{
						type: &#39;h1&#39;,
						props: {
							style: {
								fontSize: &#39;52px&#39;,
								fontWeight: 700,
								color: &#39;#f1f5f9&#39;,
								margin: 0
							},
							children: options.title
						}
					}
				]
			}
		},
		{
			width: 1200,
			height: 630,
			fonts: [
				{ name: &#39;Noto Sans JP&#39;, data: fontRegular, weight: 400, style: &#39;normal&#39; },
				{ name: &#39;Noto Sans JP&#39;, data: fontBold, weight: 700, style: &#39;normal&#39; }
			]
		}
	);

	const resvg = new Resvg(svg, { fitTo: { mode: &#39;width&#39;, value: 1200 } });
	return resvg.render().asPng();
}
</code></pre>
<h3>satori のレイアウトルール</h3>
<p>satori は <strong>Flexbox のみ</strong> をサポートします。<code>display: grid</code> は使えません。また、<code>children</code> に文字列を直接渡す代わりに、テキストノードはプリミティブ値（文字列）で渡します。</p>
<pre><code class="language-ts">// ✅ 正しい書き方
{ type: &#39;span&#39;, props: { children: &#39;テキスト&#39; } }

// ❌ 動かない
{ type: &#39;span&#39;, props: { children: &lt;span&gt;テキスト&lt;/span&gt; } }
</code></pre>
<h2>ルートを作る</h2>
<h3>記事ごとの OGP 画像</h3>
<p><code>src/routes/posts/[slug]/og.png/+server.ts</code> を作成します。</p>
<pre><code class="language-ts">import { generateOgImage } from &#39;$lib/server/og&#39;;
import { getAllPosts } from &#39;$lib/utils/posts&#39;;
import { error } from &#39;@sveltejs/kit&#39;;
import type { RequestHandler } from &#39;./$types&#39;;

export const prerender = true;

export async function entries() {
	const posts = await getAllPosts();
	return posts.map((p) =&gt; ({ slug: p.slug }));
}

export const GET: RequestHandler = async ({ params }) =&gt; {
	const posts = await getAllPosts();
	const post = posts.find((p) =&gt; p.slug === params.slug);
	if (!post) throw error(404);

	const png = await generateOgImage({
		title: post.title,
		date: post.date,
		slug: post.slug
	});

	return new Response(png, {
		headers: {
			&#39;Content-Type&#39;: &#39;image/png&#39;,
			&#39;Cache-Control&#39;: &#39;public, max-age=31536000, immutable&#39;
		}
	});
};
</code></pre>
<p><code>entries()</code> を定義することで、<code>prerender = true</code> のときにすべての記事スラッグに対して静的ファイルが生成されます。</p>
<h3>サイト共通の OGP 画像</h3>
<p><code>src/routes/og.png/+server.ts</code> でトップページ用の画像も生成できます。</p>
<pre><code class="language-ts">import { generateOgImage } from &#39;$lib/server/og&#39;;
import type { RequestHandler } from &#39;./$types&#39;;

export const prerender = true;

export const GET: RequestHandler = async () =&gt; {
	const png = await generateOgImage({ title: &quot;toishi&#39;s blog&quot; });
	return new Response(png, {
		headers: { &#39;Content-Type&#39;: &#39;image/png&#39; }
	});
};
</code></pre>
<h2><code>&lt;meta&gt;</code> タグを設定する</h2>
<p>生成した画像を <code>og:image</code> として使うには、各ページの <code>&lt;svelte:head&gt;</code> に記述します。</p>
<p><code>src/lib/layouts/post.svelte</code>（記事レイアウト）の場合:</p>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
	import { siteConfig } from &#39;$lib/config&#39;;
	let { title, date, slug, children } = $props();
	const ogImageUrl = `${siteConfig.url}/posts/${slug}/og.png`;
&lt;/script&gt;

&lt;svelte:head&gt;
	&lt;title&gt;{title}&lt;/title&gt;
	&lt;meta property=&quot;og:title&quot; content={title} /&gt;
	&lt;meta property=&quot;og:image&quot; content={ogImageUrl} /&gt;
	&lt;meta name=&quot;twitter:card&quot; content=&quot;summary_large_image&quot; /&gt;
&lt;/svelte:head&gt;
</code></pre>
<h2>動作確認</h2>
<p>OGP 画像は開発サーバーでは生成されません。<code>pnpm build</code> → <code>pnpm preview</code> で確認します。</p>
<pre><code class="language-bash">pnpm build
pnpm preview
# http://localhost:4173/posts/your-slug/og.png でアクセスできる
</code></pre>
<h2>注意点</h2>
<h3>ファイルパスの解決</h3>
<p><code>readFileSync(resolve(&#39;src/lib/server/fonts/...&#39;))</code> は、<strong>Node.js の実行ディレクトリ（プロジェクトルート）からの相対パス</strong> で解決されます。Cloudflare Pages のビルド環境でも動作しますが、実行コンテキストが変わる場合は <code>import.meta.url</code> を使った方が確実です。</p>
<h3>フォントサイズとテキストの折り返し</h3>
<p>satori はデフォルトではテキストの折り返しが起きません。長いタイトルに対応するには、<code>fontSize</code> を文字数で動的に調整するか、<code>maxWidth</code> と <code>flexWrap: &#39;wrap&#39;</code> を設定します。</p>
<pre><code class="language-ts">fontSize: title.length &gt; 30 ? &#39;44px&#39; : &#39;52px&#39;;
</code></pre>
<h3>Cloudflare Pages での Node.js 互換性</h3>
<p><code>readFileSync</code> など Node.js の API を使うには、<code>wrangler.jsonc</code> に以下が必要です。</p>
<pre><code class="language-jsonc">{
	&quot;compatibility_flags&quot;: [&quot;nodejs_als&quot;]
}
</code></pre>
<h2>まとめ</h2>
<ul>
<li>satori で HTML/CSS（Flexbox）から SVG を生成し、resvg-js で PNG に変換</li>
<li><code>+server.ts</code> + <code>prerender = true</code> + <code>entries()</code> でビルド時に全記事分を静的生成</li>
<li>日本語を含む場合は TTF フォントの明示的な指定が必要</li>
<li>確認は <code>pnpm build</code> + <code>pnpm preview</code> で行う</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://github.com/vercel/satori">satori GitHub</a></li>
<li><a href="https://github.com/yisibl/resvg-js">resvg-js GitHub</a></li>
<li><a href="https://svelte.dev/docs/kit/routing#server">SvelteKit +server.ts ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Pagefind で静的サイトに全文検索を追加する</title>
			<link>https://toishi.dev/posts/pagefind-static-search</link>
			<guid isPermaLink="true">https://toishi.dev/posts/pagefind-static-search</guid>
			<pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
			<description>Pagefind を使って静的サイトに高速な全文検索機能を実装する方法。SvelteKit との統合方法も解説します。</description>
			<category>pagefind</category><category>svelte-kit</category>
			<content:encoded><![CDATA[<p>静的サイトに検索機能を付けたいとき、外部サービス（Algolia など）を使うのが一般的でした。Pagefind はビルド時に検索インデックスを生成し、クライアントサイドで動作する軽量な全文検索ライブラリです。外部サービス不要で、無料で使えます。</p>
<h2>前提</h2>
<ul>
<li>静的 HTML を出力するサイト（SvelteKit + <code>prerender = true</code> を想定）</li>
<li>Node.js v22 以上</li>
<li>ビルド出力ディレクトリにアクセスして Pagefind CLI を実行できる環境</li>
</ul>
<h2>Pagefind の仕組み</h2>
<ol>
<li><strong>ビルド時</strong>: HTML ファイルをスキャンしてインデックスを生成</li>
<li><strong>実行時</strong>: ブラウザで JavaScript を使ってインデックスを検索</li>
</ol>
<p>インデックスは分割されて必要な部分だけ遅延読み込みされるため、数千ページのサイトでも高速に動作します。</p>
<h2>セットアップ</h2>
<pre><code class="language-bash">pnpm add -D pagefind
</code></pre>
<p><code>package.json</code> の build スクリプトに pagefind を追加します。</p>
<pre><code class="language-json">{
	&quot;scripts&quot;: {
		&quot;build&quot;: &quot;vite build &amp;&amp; pagefind --site .svelte-kit/cloudflare&quot;
	}
}
</code></pre>
<p><code>--site</code> にビルド出力先ディレクトリを指定します。SvelteKit + adapter-cloudflare の場合は <code>.svelte-kit/cloudflare</code> です。</p>
<h2>検索対象の指定</h2>
<p>デフォルトでは <code>&lt;body&gt;</code> 全体がインデックス対象ですが、<code>data-pagefind-body</code> 属性でスコープを絞れます。</p>
<pre><code class="language-svelte">&lt;!-- src/lib/layouts/post.svelte --&gt;
&lt;article class=&quot;prose&quot;&gt;
	&lt;h1&gt;{title}&lt;/h1&gt;
	&lt;div data-pagefind-body&gt;
		{@render children()}
	&lt;/div&gt;
&lt;/article&gt;
</code></pre>
<p>ナビゲーションやフッターを除外し、記事本文だけを検索対象にできます。</p>
<h3>除外したい要素</h3>
<pre><code class="language-html">&lt;div data-pagefind-body&gt;
	&lt;p&gt;この段落は検索対象&lt;/p&gt;
	&lt;nav data-pagefind-ignore&gt;
		&lt;!-- ここは検索対象外 --&gt;
	&lt;/nav&gt;
&lt;/div&gt;
</code></pre>
<h2>SvelteKit での検索コンポーネント</h2>
<p>Pagefind は動的に JavaScript を読み込むため、SvelteKit では <code>onMount</code> 内でインポートします。</p>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
	import { onMount } from &#39;svelte&#39;;

	let pagefind: any = $state(null);
	let query = $state(&#39;&#39;);
	let results = $state&lt;any[]&gt;([]);

	onMount(async () =&gt; {
		try {
			pagefind = await import(/* @vite-ignore */ &#39;/pagefind/pagefind.js&#39;);
			await pagefind.init();
		} catch {
			console.warn(&#39;Pagefind not available&#39;);
		}
	});

	async function search() {
		if (!pagefind || !query) {
			results = [];
			return;
		}
		const response = await pagefind.debouncedSearch(query);
		if (!response) return;
		results = await Promise.all(response.results.map((r: any) =&gt; r.data()));
	}

	$effect(() =&gt; {
		query;
		search();
	});
&lt;/script&gt;

&lt;input type=&quot;text&quot; bind:value={query} placeholder=&quot;記事を検索...&quot; /&gt;

&lt;ul&gt;
	{#each results as result}
		&lt;li&gt;
			&lt;a href={result.url}&gt;
				&lt;strong&gt;{result.meta.title}&lt;/strong&gt;
				&lt;p&gt;{@html result.excerpt}&lt;/p&gt;
			&lt;/a&gt;
		&lt;/li&gt;
	{/each}
&lt;/ul&gt;
</code></pre>
<h3>ポイント</h3>
<ul>
<li><code>/* @vite-ignore */</code> で Vite の静的解析を回避</li>
<li><code>debouncedSearch</code> で入力中の無駄なリクエストを抑制</li>
<li><code>result.data()</code> は遅延評価 — 表示する結果だけデータを取得</li>
<li><code>result.excerpt</code> には検索語がハイライトされた HTML が含まれる</li>
</ul>
<h2>開発時の注意</h2>
<p>Pagefind のインデックスはビルド時に生成されるため、<strong>開発サーバー（<code>pnpm dev</code>）では検索が動作しません</strong>。検索機能を確認するには:</p>
<pre><code class="language-bash">pnpm build    # ビルド + インデックス生成
pnpm preview  # ビルド結果をローカルプレビュー
</code></pre>
<h2>メタデータの活用</h2>
<p>Pagefind はページのメタデータも自動で収集します。</p>
<pre><code class="language-html">&lt;!-- title タグが meta.title になる --&gt;
&lt;title&gt;記事のタイトル&lt;/title&gt;

&lt;!-- meta description が meta.description になる --&gt;
&lt;meta name=&quot;description&quot; content=&quot;記事の説明&quot; /&gt;
</code></pre>
<p>カスタムメタデータも追加できます。</p>
<pre><code class="language-html">&lt;div data-pagefind-meta=&quot;category:技術&quot;&gt;
	&lt;!-- この要素のテキストが category メタデータに --&gt;
&lt;/div&gt;
</code></pre>
<h2>フィルタリング</h2>
<p>タグやカテゴリでフィルタリングすることも可能です。</p>
<pre><code class="language-html">&lt;div data-pagefind-filter=&quot;tag:svelte&quot;&gt;Svelte の記事&lt;/div&gt;
</code></pre>
<pre><code class="language-ts">const results = await pagefind.search(&#39;query&#39;, {
	filters: { tag: &#39;svelte&#39; }
});
</code></pre>
<h2>Cloudflare Pages との相性</h2>
<p>Pagefind はクライアントサイドで完結するため、Cloudflare Pages のような静的ホスティングと相性が抜群です。サーバーサイドの処理が不要なので、追加コストなしで検索機能を提供できます。</p>
<h2>まとめ</h2>
<ul>
<li>Pagefind はビルド時にインデックスを生成するクライアントサイド検索</li>
<li>外部サービス不要、無料で使える</li>
<li><code>data-pagefind-body</code> で検索範囲を制御</li>
<li>開発サーバーでは動かない（ビルド後に確認）</li>
<li>静的ホスティングとの相性が良い</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://pagefind.app/">Pagefind 公式サイト</a></li>
<li><a href="https://github.com/CloudCannon/pagefind">Pagefind GitHub</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>TypeScript の satisfies 演算子を活用する — 型安全と型推論の両立</title>
			<link>https://toishi.dev/posts/typescript-satisfies</link>
			<guid isPermaLink="true">https://toishi.dev/posts/typescript-satisfies</guid>
			<pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate>
			<description>TypeScript 4.9 で追加された satisfies 演算子の使い方と活用パターンを紹介。型アノテーションとの違いを理解します。</description>
			<category>typescript</category>
			<content:encoded><![CDATA[<p>TypeScript 4.9 で導入された <code>satisfies</code> 演算子は、型の検証と型推論を両立させる強力な機能です。従来の型アノテーション（<code>: Type</code>）では失われていた型の詳細情報を保持できます。</p>
<h2>前提</h2>
<ul>
<li>TypeScript 4.9 以上が動作するプロジェクト</li>
<li><code>tsconfig.json</code> で <code>strict: true</code> を有効にしておくと、<code>satisfies</code> の効果がより分かりやすい</li>
</ul>
<h2>従来の問題</h2>
<p>型アノテーションを使うと、リテラル型の情報が失われます。</p>
<pre><code class="language-ts">type Route = {
	path: string;
	method: &#39;GET&#39; | &#39;POST&#39;;
};

// 型アノテーション — method が &#39;GET&#39; | &#39;POST&#39; に広がる
const route: Route = {
	path: &#39;/api/posts&#39;,
	method: &#39;GET&#39;
};

// route.method は &#39;GET&#39; | &#39;POST&#39; 型
// &#39;GET&#39; であることが失われている
</code></pre>
<h2>satisfies の登場</h2>
<pre><code class="language-ts">const route = {
	path: &#39;/api/posts&#39;,
	method: &#39;GET&#39;
} satisfies Route;

// route.method は &#39;GET&#39; 型（リテラル型が保持される！）
// かつ Route 型の制約も検証済み
</code></pre>
<p><code>satisfies</code> は「この値が型 <code>Route</code> を満たすことを検証するが、推論された型をそのまま使う」という意味です。</p>
<h2>実践的な活用パターン</h2>
<h3>1. 設定オブジェクト</h3>
<pre><code class="language-ts">type Config = {
	theme: &#39;light&#39; | &#39;dark&#39;;
	language: string;
	features: Record&lt;string, boolean&gt;;
};

const config = {
	theme: &#39;dark&#39;,
	language: &#39;ja&#39;,
	features: {
		search: true,
		comments: false,
		analytics: true
	}
} satisfies Config;

// config.theme は &#39;dark&#39; 型（&#39;light&#39; | &#39;dark&#39; ではない）
// config.features.search は boolean（Record の制約も満たす）
</code></pre>
<h3>2. ルーティング定義</h3>
<pre><code class="language-ts">type Routes = Record&lt;string, { path: string; prerender?: boolean }&gt;;

const routes = {
	home: { path: &#39;/&#39;, prerender: true },
	about: { path: &#39;/about&#39;, prerender: true },
	search: { path: &#39;/search&#39;, prerender: false }
} satisfies Routes;

// routes.home が存在することが型で保証される
// routes.notExist はコンパイルエラー
</code></pre>
<p>型アノテーション（<code>const routes: Routes = ...</code>）だと、キー名が <code>string</code> に広がって <code>routes.home</code> の補完が効きません。</p>
<h3>3. カラーパレット</h3>
<pre><code class="language-ts">type Color = `#${string}` | `rgb(${string})`;
type Palette = Record&lt;string, Color&gt;;

const palette = {
	primary: &#39;#3b82f6&#39;,
	secondary: &#39;#64748b&#39;,
	accent: &#39;rgb(236, 72, 153)&#39;
} satisfies Palette;

// palette.primary は &#39;#3b82f6&#39; 型
// palette.unknown はエラー
</code></pre>
<h3>4. イベントハンドラのマップ</h3>
<pre><code class="language-ts">type EventHandlers = Record&lt;string, (...args: any[]) =&gt; void&gt;;

const handlers = {
	click: (e: MouseEvent) =&gt; console.log(e.clientX),
	keydown: (e: KeyboardEvent) =&gt; console.log(e.key),
	submit: (data: FormData) =&gt; console.log(data)
} satisfies EventHandlers;

// handlers.click の引数が MouseEvent として推論される
</code></pre>
<h2>satisfies vs 型アノテーション</h2>
<table>
<thead>
<tr>
<th>特徴</th>
<th><code>: Type</code></th>
<th><code>satisfies Type</code></th>
</tr>
</thead>
<tbody><tr>
<td>型の検証</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>リテラル型の保持</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>キー名の補完</td>
<td>❌（Record 時）</td>
<td>✅</td>
</tr>
<tr>
<td>余剰プロパティチェック</td>
<td>✅</td>
<td>✅</td>
</tr>
</tbody></table>
<h2>as const satisfies</h2>
<p><code>as const</code> と組み合わせることで、さらに厳密な型推論が得られます。</p>
<pre><code class="language-ts">const endpoints = {
	users: &#39;/api/users&#39;,
	posts: &#39;/api/posts&#39;,
	tags: &#39;/api/tags&#39;
} as const satisfies Record&lt;string, `/${string}`&gt;;

// endpoints.users は &#39;/api/users&#39; リテラル型（readonly）
// 値が &#39;/api/&#39; で始まることも検証済み
</code></pre>
<h2>このブログでの活用パターン</h2>
<p>このブログでも、サイト設定や JSON-LD スキーマの組み立てなど、いくつかの場所で <code>satisfies</code> が活躍します。</p>
<h3>サイト設定の凍結</h3>
<p><code>siteConfig</code> は全ページから参照するため、キーや値の型が緩むと参照側で面倒です。<code>as const satisfies</code> で「キー名はリテラルのまま」「URL は <code>https://</code> で始まる文字列」を同時に保証できます。</p>
<pre><code class="language-ts">// src/lib/config.ts
type SiteConfig = {
	title: string;
	description: string;
	url: `https://${string}`;
};

export const siteConfig = {
	title: &quot;toishi&#39;s blog&quot;,
	description: &#39;SvelteKit で作った技術ブログ&#39;,
	url: &#39;https://toishi.dev&#39;
} as const satisfies SiteConfig;

// siteConfig.url は &#39;https://toishi.dev&#39; リテラル型のまま
// &#39;http://&#39; で始めるとコンパイルエラー
</code></pre>
<h3>PostMeta のデフォルト値</h3>
<p>下書き記事のテンプレートやテスト用のフィクスチャを書くとき、<code>PostMeta</code> 型を満たしつつフィールドの値を具体的に保持しておきたい場面があります。</p>
<pre><code class="language-ts">// src/lib/utils/posts.ts
import type { PostMeta } from &#39;./posts&#39;;

const draftFixture = {
	slug: &#39;draft-post&#39;,
	title: &#39;下書き記事&#39;,
	description: &#39;&#39;,
	date: &#39;2026-04-30&#39;,
	tags: [&#39;svelte&#39;],
	draft: true
} satisfies PostMeta;

// draftFixture.draft は true 型（boolean ではない）
// テストで draft 記事のみのケースを書くときに便利
</code></pre>
<h3>JSON-LD のスキーマ定義</h3>
<p>構造化データのキー名を厳密に保ちたいときも、<code>as const satisfies</code> が効きます。<code>@type</code> 等のフィールドを文字列リテラル型のまま渡せるので、Google の構造化データテストツール側で定義された値とのミスマッチが起きにくくなります。</p>
<pre><code class="language-ts">type JsonLd = Record&lt;string, unknown&gt; &amp; { &#39;@context&#39;: string; &#39;@type&#39;: string };

const websiteLd = {
	&#39;@context&#39;: &#39;https://schema.org&#39;,
	&#39;@type&#39;: &#39;WebSite&#39;,
	name: siteConfig.title,
	url: siteConfig.url
} as const satisfies JsonLd;
</code></pre>
<h2>まとめ</h2>
<p><code>satisfies</code> は「型チェックは厳密に、推論はリッチに」を実現する演算子です。設定オブジェクトやルーティング定義など、型の制約を守りつつリテラル型の情報を保持したい場面で活躍します。</p>
<h2>参照</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html">TypeScript 4.9 リリースノート（satisfies 演算子）</a></li>
<li><a href="https://www.typescriptlang.org/">TypeScript 公式サイト</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Vitest + Playwright で Svelte コンポーネントをテストする</title>
			<link>https://toishi.dev/posts/vitest-svelte-testing</link>
			<guid isPermaLink="true">https://toishi.dev/posts/vitest-svelte-testing</guid>
			<pubDate>Sun, 05 Apr 2026 00:00:00 GMT</pubDate>
			<description>Vitest を使ったSvelte コンポーネントのユニットテスト方法。ブラウザモードでの実行と、テストの書き方を解説します。</description>
			<category>vitest</category><category>svelte</category><category>testing</category>
			<content:encoded><![CDATA[<p>Vitest は Vite ベースのテストフレームワークで、SvelteKit プロジェクトとの相性が抜群です。このブログでは Vitest + Playwright のブラウザモードを使って、Svelte コンポーネントを実際のブラウザ上でテストしています。</p>
<h2>前提</h2>
<ul>
<li>SvelteKit プロジェクト（Svelte 5 を想定）</li>
<li>Node.js v22 以上</li>
<li>Playwright のブラウザバイナリをダウンロードできるネットワーク環境（初回 <code>npx playwright install</code> でセットアップ）</li>
</ul>
<h2>セットアップ</h2>
<pre><code class="language-bash">pnpm add -D vitest @vitest/browser-playwright playwright vitest-browser-svelte
</code></pre>
<p><code>vite.config.ts</code> にテスト設定を追加:</p>
<pre><code class="language-ts">import { sveltekit } from &#39;@sveltejs/kit/vite&#39;;
import { defineConfig } from &#39;vite&#39;;

export default defineConfig({
	plugins: [sveltekit()],
	test: {
		include: [&#39;src/**/*.{test,spec}.{js,ts}&#39;],
		browser: {
			enabled: true,
			provider: &#39;playwright&#39;,
			instances: [{ browser: &#39;chromium&#39; }]
		}
	}
});
</code></pre>
<h2>テストの種類</h2>
<p>このプロジェクトでは 2 種類のテスト環境を使い分けています:</p>
<table>
<thead>
<tr>
<th>パターン</th>
<th>環境</th>
<th>用途</th>
</tr>
</thead>
<tbody><tr>
<td><code>*.svelte.test.ts</code></td>
<td>ブラウザ（Playwright）</td>
<td>コンポーネントテスト</td>
</tr>
<tr>
<td><code>*.test.ts</code></td>
<td>Node.js</td>
<td>ユーティリティ関数テスト</td>
</tr>
</tbody></table>
<h2>コンポーネントテストの書き方</h2>
<p><code>vitest-browser-svelte</code> の <code>render</code> を使ってコンポーネントをレンダリングします。</p>
<pre><code class="language-ts">// src/lib/components/Counter.svelte.test.ts
import { render } from &#39;vitest-browser-svelte&#39;;
import { expect, test } from &#39;vitest&#39;;
import Counter from &#39;./Counter.svelte&#39;;

test(&#39;初期値が表示される&#39;, async () =&gt; {
	const screen = render(Counter, { count: 0 });
	await expect.element(screen.getByText(&#39;0&#39;)).toBeVisible();
});

test(&#39;クリックでカウントが増える&#39;, async () =&gt; {
	const screen = render(Counter, { count: 0 });
	const button = screen.getByRole(&#39;button&#39;);
	await button.click();
	await expect.element(screen.getByText(&#39;1&#39;)).toBeVisible();
});
</code></pre>
<h3>Props の渡し方</h3>
<pre><code class="language-ts">render(PostCard, {
	title: &#39;テスト記事&#39;,
	description: &#39;説明文&#39;,
	date: &#39;2026-03-15&#39;,
	slug: &#39;test-post&#39;,
	tags: [&#39;svelte&#39;, &#39;test&#39;]
});
</code></pre>
<h3>イベントのテスト</h3>
<pre><code class="language-ts">test(&#39;ボタンクリックでイベントが発火する&#39;, async () =&gt; {
	let clicked = false;
	const screen = render(Button, {
		onclick: () =&gt; {
			clicked = true;
		}
	});

	await screen.getByRole(&#39;button&#39;).click();
	expect(clicked).toBe(true);
});
</code></pre>
<h2>ユーティリティ関数のテスト</h2>
<pre><code class="language-ts">// src/lib/utils/posts.test.ts
import { describe, expect, it } from &#39;vitest&#39;;
import { getAllPosts, getAllTags } from &#39;./posts&#39;;

describe(&#39;getAllPosts&#39;, () =&gt; {
	it(&#39;記事が日付の降順で返される&#39;, async () =&gt; {
		const posts = await getAllPosts();
		for (let i = 1; i &lt; posts.length; i++) {
			expect(new Date(posts[i - 1].date).getTime()).toBeGreaterThanOrEqual(
				new Date(posts[i].date).getTime()
			);
		}
	});

	it(&#39;下書き記事が含まれない&#39;, async () =&gt; {
		const posts = await getAllPosts();
		expect(posts.every((p) =&gt; !p.draft)).toBe(true);
	});
});

describe(&#39;getAllTags&#39;, () =&gt; {
	it(&#39;タグとカウントのマップが返される&#39;, async () =&gt; {
		const tags = await getAllTags();
		expect(tags).toBeInstanceOf(Map);
		for (const [, count] of tags) {
			expect(count).toBeGreaterThan(0);
		}
	});
});
</code></pre>
<h2>テストの実行</h2>
<pre><code class="language-bash">pnpm test          # 全テスト実行（単発）
pnpm test:unit     # ウォッチモード（開発中に便利）
</code></pre>
<h2>テストのコツ</h2>
<h3>1. アクセシビリティロールで要素を探す</h3>
<pre><code class="language-ts">// ❌ テスト ID やクラス名に依存
screen.getByTestId(&#39;submit-btn&#39;);

// ✅ アクセシビリティロールを使う
screen.getByRole(&#39;button&#39;, { name: &#39;送信&#39; });
</code></pre>
<h3>2. 非同期の変更を待つ</h3>
<p>Svelte のリアクティビティは非同期で反映されるため、<code>await</code> を使います。</p>
<pre><code class="language-ts">await button.click();
await expect.element(screen.getByText(&#39;更新後のテキスト&#39;)).toBeVisible();
</code></pre>
<h3>3. テストごとにクリーンアップ</h3>
<p><code>render</code> は各テスト後に自動でクリーンアップされるので、手動での cleanup は不要です。</p>
<h2>まとめ</h2>
<ul>
<li>Vitest + Playwright でブラウザ上のコンポーネントテストが可能</li>
<li><code>.svelte.test.ts</code> はブラウザ環境、<code>.test.ts</code> は Node.js 環境</li>
<li><code>vitest-browser-svelte</code> の <code>render</code> でコンポーネントをレンダリング</li>
<li>アクセシビリティロールでの要素取得を推奨</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://vitest.dev/">Vitest 公式サイト</a></li>
<li><a href="https://vitest.dev/guide/browser/">Vitest ブラウザモード ドキュメント</a></li>
<li><a href="https://github.com/vitest-dev/vitest-browser-svelte">vitest-browser-svelte GitHub</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>ESLint + Prettier を SvelteKit プロジェクトに設定する</title>
			<link>https://toishi.dev/posts/eslint-prettier-setup</link>
			<guid isPermaLink="true">https://toishi.dev/posts/eslint-prettier-setup</guid>
			<pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
			<description>SvelteKit プロジェクトでの ESLint と Prettier の設定方法。Svelte ファイルの対応と、競合を避ける設定を解説します。</description>
			<category>eslint</category><category>prettier</category><category>svelte-kit</category>
			<content:encoded><![CDATA[<p>コードの品質と一貫性を保つために、ESLint（静的解析）と Prettier（フォーマッター）を併用するのが一般的です。SvelteKit プロジェクトでは <code>.svelte</code> ファイルの対応が必要で、いくつか注意点があります。</p>
<h2>前提</h2>
<ul>
<li>Node.js v22 以上</li>
<li>ESLint v9 以上（Flat Config 前提）</li>
<li>Prettier v3 以上</li>
<li>本記事は SvelteKit プロジェクトを対象としているが、Node.js プロジェクト全般に応用可能</li>
</ul>
<h2>パッケージのインストール</h2>
<p>SvelteKit のプロジェクト作成時に ESLint と Prettier を選択すると自動でインストールされますが、手動で追加する場合:</p>
<pre><code class="language-bash">pnpm add -D eslint prettier \
  eslint-plugin-svelte \
  eslint-config-prettier \
  prettier-plugin-svelte \
  prettier-plugin-tailwindcss \
  typescript-eslint \
  globals
</code></pre>
<h2>ESLint の設定（Flat Config）</h2>
<p>ESLint v9 以降は Flat Config（<code>eslint.config.js</code>）が標準です。</p>
<pre><code class="language-js">// eslint.config.js
import js from &#39;@eslint/js&#39;;
import prettier from &#39;eslint-config-prettier&#39;;
import svelte from &#39;eslint-plugin-svelte&#39;;
import globals from &#39;globals&#39;;
import ts from &#39;typescript-eslint&#39;;

export default ts.config(
	js.configs.recommended,
	...ts.configs.recommended,
	...svelte.configs.recommended,
	prettier,
	...svelte.configs.prettier,
	{
		languageOptions: {
			globals: {
				...globals.browser,
				...globals.node
			}
		}
	},
	{
		files: [&#39;**/*.svelte&#39;, &#39;**/*.svelte.ts&#39;, &#39;**/*.svelte.js&#39;],
		languageOptions: {
			parserOptions: {
				parser: ts.parser
			}
		}
	},
	{
		ignores: [&#39;build/&#39;, &#39;.svelte-kit/&#39;, &#39;dist/&#39;, &#39;.wrangler/&#39;, &#39;.vercel/&#39;]
	}
);
</code></pre>
<h3>ポイント</h3>
<ul>
<li><code>eslint-config-prettier</code> で Prettier と競合するルールを無効化</li>
<li><code>eslint-plugin-svelte</code> で <code>.svelte</code> ファイルを解析</li>
<li><code>svelte.configs.prettier</code> で Svelte 固有の Prettier 競合ルールも無効化</li>
<li><code>ignores</code> でビルド出力を除外</li>
</ul>
<h2>Prettier の設定</h2>
<pre><code class="language-json">// .prettierrc
{
	&quot;useTabs&quot;: true,
	&quot;singleQuote&quot;: true,
	&quot;trailingComma&quot;: &quot;none&quot;,
	&quot;printWidth&quot;: 100,
	&quot;plugins&quot;: [&quot;prettier-plugin-svelte&quot;, &quot;prettier-plugin-tailwindcss&quot;],
	&quot;overrides&quot;: [
		{
			&quot;files&quot;: &quot;*.svelte&quot;,
			&quot;options&quot;: {
				&quot;parser&quot;: &quot;svelte&quot;
			}
		}
	]
}
</code></pre>
<h3>プラグインの役割</h3>
<ul>
<li><strong>prettier-plugin-svelte</strong> — <code>.svelte</code> ファイルのフォーマット対応</li>
<li><strong>prettier-plugin-tailwindcss</strong> — Tailwind CSS のクラス名を推奨順序に自動ソート</li>
</ul>
<h2>.prettierignore</h2>
<pre><code class="language-text"># .prettierignore
build/
.svelte-kit/
dist/
.wrangler/
pnpm-lock.yaml
</code></pre>
<h2>npm スクリプト</h2>
<pre><code class="language-json">{
	&quot;scripts&quot;: {
		&quot;lint&quot;: &quot;prettier --check . &amp;&amp; eslint .&quot;,
		&quot;format&quot;: &quot;prettier --write .&quot;
	}
}
</code></pre>
<p><code>lint</code> はチェックのみ（CI 用）、<code>format</code> は自動修正です。</p>
<h2>エディタ連携</h2>
<h3>VS Code</h3>
<p><code>.vscode/settings.json</code>:</p>
<pre><code class="language-json">{
	&quot;editor.formatOnSave&quot;: true,
	&quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;,
	&quot;[svelte]&quot;: {
		&quot;editor.defaultFormatter&quot;: &quot;svelte.svelte-vscode&quot;
	},
	&quot;eslint.validate&quot;: [&quot;javascript&quot;, &quot;typescript&quot;, &quot;svelte&quot;]
}
</code></pre>
<h3>推奨拡張機能</h3>
<ul>
<li><strong>Svelte for VS Code</strong> — Svelte 言語サポート</li>
<li><strong>ESLint</strong> — リアルタイム ESLint チェック</li>
<li><strong>Prettier</strong> — 保存時自動フォーマット</li>
</ul>
<h2>Tailwind CSS クラスの自動ソート</h2>
<p><code>prettier-plugin-tailwindcss</code> により、クラス名が Tailwind の推奨順序に自動ソートされます。</p>
<pre><code class="language-svelte">&lt;!-- Before --&gt;
&lt;div class=&quot;p-4 flex bg-white rounded-lg shadow-md items-center&quot;&gt;

&lt;!-- After（自動ソート） --&gt;
&lt;div class=&quot;flex items-center rounded-lg bg-white p-4 shadow-md&quot;&gt;
</code></pre>
<h2>CI での実行</h2>
<p>GitHub Actions で lint を自動実行する例:</p>
<pre><code class="language-yaml">- name: Lint
  run: pnpm lint

- name: Type check
  run: pnpm check
</code></pre>
<p><code>pnpm lint</code> が失敗すると PR のマージをブロックできます。</p>
<h2>まとめ</h2>
<ul>
<li>ESLint で静的解析、Prettier でフォーマット</li>
<li><code>eslint-config-prettier</code> で競合を解消</li>
<li><code>.svelte</code> ファイルは専用プラグインで対応</li>
<li><code>prettier-plugin-tailwindcss</code> でクラス名を自動ソート</li>
<li>保存時の自動フォーマットでストレスフリーに</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://eslint.org/">ESLint 公式サイト</a></li>
<li><a href="https://prettier.io/">Prettier 公式サイト</a></li>
<li><a href="https://github.com/sveltejs/eslint-plugin-svelte">eslint-plugin-svelte GitHub</a></li>
<li><a href="https://github.com/tailwindlabs/prettier-plugin-tailwindcss">prettier-plugin-tailwindcss GitHub</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>SvelteKit でダークモードを実装する — Tailwind CSS + システム設定連携</title>
			<link>https://toishi.dev/posts/dark-mode-sveltekit</link>
			<guid isPermaLink="true">https://toishi.dev/posts/dark-mode-sveltekit</guid>
			<pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate>
			<description>SvelteKit + Tailwind CSS でダークモード切り替えを実装する方法。システム設定との連携やフラッシュ防止のテクニックも紹介します。</description>
			<category>svelte-kit</category><category>tailwindcss</category>
			<content:encoded><![CDATA[<p>ダークモードは今や Web サイトの標準的な機能です。SvelteKit + Tailwind CSS で、ユーザーの好みに応じたテーマ切り替えを実装する方法を解説します。</p>
<h2>前提</h2>
<ul>
<li>SvelteKit プロジェクト（Svelte 5 Runes モードを想定）</li>
<li>Tailwind CSS v4 が導入済みで、<code>@custom-variant dark (&amp;:is(.dark *))</code> のようなクラスベースのダークモード設定が可能なこと</li>
<li>shadcn-svelte を使う場合は CSS 変数（<code>--background</code> 等）が <code>:root</code> と <code>.dark</code> の両方で定義されていること</li>
</ul>
<h2>Tailwind CSS のダークモード</h2>
<p>Tailwind CSS は <code>dark:</code> プレフィックスでダークモード用のスタイルを記述できます。</p>
<pre><code class="language-html">&lt;div class=&quot;bg-white text-black dark:bg-gray-900 dark:text-white&quot;&gt;
	ライトモードでは白背景、ダークモードでは暗い背景
&lt;/div&gt;
</code></pre>
<h2>テーマの管理方法</h2>
<p>テーマの管理には 3 つのアプローチがあります。</p>
<h3>1. システム設定に従う（prefers-color-scheme）</h3>
<pre><code class="language-css">@media (prefers-color-scheme: dark) {
	:root {
		color-scheme: dark;
	}
}
</code></pre>
<p>OS やブラウザの設定に自動で追従します。最もシンプルですが、ユーザーが手動で切り替えられません。</p>
<h3>2. クラスベース（手動切り替え）</h3>
<p><code>&lt;html&gt;</code> 要素にクラスを付与してテーマを制御します。</p>
<pre><code class="language-html">&lt;html class=&quot;dark&quot;&gt;
	&lt;!-- dark: プレフィックスが有効になる --&gt;
&lt;/html&gt;
</code></pre>
<h3>3. ハイブリッド（推奨）</h3>
<p>システム設定をデフォルトにしつつ、ユーザーが手動でも切り替えられるアプローチです。</p>
<h2>実装</h2>
<h3>テーマの状態管理</h3>
<pre><code class="language-ts">// theme.svelte.ts
type Theme = &#39;light&#39; | &#39;dark&#39; | &#39;system&#39;;

let theme = $state&lt;Theme&gt;(&#39;system&#39;);

export function getTheme() {
	return theme;
}

export function setTheme(newTheme: Theme) {
	theme = newTheme;
	localStorage.setItem(&#39;theme&#39;, newTheme);
	applyTheme(newTheme);
}

function applyTheme(t: Theme) {
	const isDark =
		t === &#39;dark&#39; || (t === &#39;system&#39; &amp;&amp; window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches);

	document.documentElement.classList.toggle(&#39;dark&#39;, isDark);
}

export function initTheme() {
	const saved = localStorage.getItem(&#39;theme&#39;) as Theme | null;
	theme = saved ?? &#39;system&#39;;
	applyTheme(theme);

	// システム設定の変更を監視
	window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).addEventListener(&#39;change&#39;, () =&gt; {
		if (theme === &#39;system&#39;) {
			applyTheme(&#39;system&#39;);
		}
	});
}
</code></pre>
<h3>テーマ切り替えボタン</h3>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
	import { getTheme, setTheme } from &#39;./theme.svelte.ts&#39;;
	import Sun from &#39;@lucide/svelte/icons/sun&#39;;
	import Moon from &#39;@lucide/svelte/icons/moon&#39;;
	import Monitor from &#39;@lucide/svelte/icons/monitor&#39;;

	const themes = [
		{ value: &#39;light&#39;, icon: Sun, label: &#39;ライト&#39; },
		{ value: &#39;dark&#39;, icon: Moon, label: &#39;ダーク&#39; },
		{ value: &#39;system&#39;, icon: Monitor, label: &#39;システム&#39; }
	] as const;
&lt;/script&gt;

&lt;div class=&quot;flex gap-1&quot;&gt;
	{#each themes as t}
		&lt;button
			onclick={() =&gt; setTheme(t.value)}
			class=&quot;rounded-md p-2&quot;
			class:bg-muted={getTheme() === t.value}
			aria-label={t.label}
		&gt;
			&lt;t.icon size={16} /&gt;
		&lt;/button&gt;
	{/each}
&lt;/div&gt;
</code></pre>
<h3>フラッシュ防止</h3>
<p>SSG（静的サイト生成）では、HTML が読み込まれてから JavaScript が実行されるまでの間にデフォルトテーマが表示されてしまう「フラッシュ」が発生します。</p>
<p>これを防ぐために、<code>&lt;head&gt;</code> にインラインスクリプトを追加します。</p>
<pre><code class="language-html">&lt;!-- src/app.html --&gt;
&lt;head&gt;
	&lt;script&gt;
		(function () {
			const theme = localStorage.getItem(&#39;theme&#39;) ?? &#39;system&#39;;
			const isDark =
				theme === &#39;dark&#39; ||
				(theme === &#39;system&#39; &amp;&amp; window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches);
			if (isDark) document.documentElement.classList.add(&#39;dark&#39;);
		})();
	&lt;/script&gt;
&lt;/head&gt;
</code></pre>
<p>このスクリプトは HTML パース時に同期的に実行されるため、ページが表示される前にテーマが適用されます。</p>
<h2>shadcn-svelte のダークモード対応</h2>
<p>shadcn-svelte のコンポーネントは CSS 変数でテーマカラーを管理しているため、ダークモードに自動対応しています。</p>
<pre><code class="language-css">:root {
	--background: 0 0% 100%;
	--foreground: 222.2 84% 4.9%;
}

.dark {
	--background: 222.2 84% 4.9%;
	--foreground: 210 40% 98%;
}
</code></pre>
<p><code>dark</code> クラスの有無で CSS 変数が切り替わり、すべてのコンポーネントの色が連動して変わります。</p>
<h2>@tailwindcss/typography のダークモード</h2>
<pre><code class="language-html">&lt;article class=&quot;prose dark:prose-invert&quot;&gt;
	&lt;!-- Markdown コンテンツ --&gt;
&lt;/article&gt;
</code></pre>
<p><code>dark:prose-invert</code> を追加するだけで、記事のテキスト色が反転します。</p>
<h2>まとめ</h2>
<ul>
<li>Tailwind CSS の <code>dark:</code> プレフィックスでスタイルを切り替え</li>
<li><code>localStorage</code> + システム設定のハイブリッドが実用的</li>
<li><code>app.html</code> のインラインスクリプトでフラッシュを防止</li>
<li>shadcn-svelte は CSS 変数で自動対応</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://tailwindcss.com/docs/dark-mode">Tailwind CSS ダークモード ドキュメント</a></li>
<li><a href="https://svelte.dev/">Svelte 公式サイト</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>@tailwindcss/typography で Markdown を美しくスタイリングする</title>
			<link>https://toishi.dev/posts/tailwind-typography</link>
			<guid isPermaLink="true">https://toishi.dev/posts/tailwind-typography</guid>
			<pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate>
			<description>Tailwind CSS の Typography プラグインを使って、Markdown コンテンツに読みやすいタイポグラフィを適用する方法を解説します。</description>
			<category>tailwindcss</category>
			<content:encoded><![CDATA[<p>Markdown で書いた記事をそのまま表示すると、見出しや段落にスタイルが当たらず見づらくなります。<code>@tailwindcss/typography</code> プラグインの <code>prose</code> クラスを使えば、一行追加するだけで美しいタイポグラフィが手に入ります。</p>
<h2>前提</h2>
<ul>
<li>Tailwind CSS v4 が導入済みのプロジェクト（v3 でも動作するが本記事は v4 の <code>@plugin</code> 構文を使用）</li>
<li>Markdown を HTML に変換する仕組みがあること（mdsvex / MDX / Markdown-it 等）</li>
</ul>
<h2>セットアップ</h2>
<pre><code class="language-bash">pnpm add -D @tailwindcss/typography
</code></pre>
<p>Tailwind CSS v4 では、CSS ファイルでプラグインを読み込みます。</p>
<pre><code class="language-css">@import &#39;tailwindcss&#39;;
@plugin &#39;@tailwindcss/typography&#39;;
</code></pre>
<h2>基本の使い方</h2>
<p><code>prose</code> クラスを親要素に付けるだけです。</p>
<pre><code class="language-html">&lt;article class=&quot;prose&quot;&gt;
	&lt;h1&gt;記事タイトル&lt;/h1&gt;
	&lt;p&gt;Markdown から生成された HTML に、自動的に適切なスタイルが当たります。&lt;/p&gt;
	&lt;ul&gt;
		&lt;li&gt;リストアイテム 1&lt;/li&gt;
		&lt;li&gt;リストアイテム 2&lt;/li&gt;
	&lt;/ul&gt;
	&lt;pre&gt;&lt;code&gt;console.log(&#39;コードブロックも整形される&#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;/article&gt;
</code></pre>
<p><code>prose</code> が適用する主なスタイル:</p>
<ul>
<li>見出し（h1〜h4）のサイズ・余白・太さ</li>
<li>段落の行間（line-height）と余白</li>
<li>リスト（ul / ol）のマーカーとインデント</li>
<li>コードブロックの背景色とパディング</li>
<li>リンクの色と下線</li>
<li>blockquote のボーダーとスタイル</li>
<li>テーブルのボーダーとセルパディング</li>
</ul>
<h2>ダークモード対応</h2>
<p><code>dark:prose-invert</code> を追加すると、ダークモードで配色が反転します。</p>
<pre><code class="language-html">&lt;article class=&quot;prose dark:prose-invert&quot;&gt;
	&lt;!-- コンテンツ --&gt;
&lt;/article&gt;
</code></pre>
<h2>サイズバリエーション</h2>
<p>コンテンツの表示サイズを変更できます。</p>
<pre><code class="language-html">&lt;article class=&quot;prose prose-sm&quot;&gt;&lt;!-- 小さめ --&gt;&lt;/article&gt;
&lt;article class=&quot;prose&quot;&gt;&lt;!-- デフォルト --&gt;&lt;/article&gt;
&lt;article class=&quot;prose prose-lg&quot;&gt;&lt;!-- 大きめ --&gt;&lt;/article&gt;
&lt;article class=&quot;prose prose-xl&quot;&gt;&lt;!-- さらに大きめ --&gt;&lt;/article&gt;
</code></pre>
<h2>カスタマイズ</h2>
<h3>prose 内の要素を個別にスタイリング</h3>
<p><code>prose</code> クラスの中でも、Tailwind のユーティリティクラスで上書きできます。</p>
<pre><code class="language-css">@layer components {
	.prose pre {
		@apply bg-muted text-foreground;
	}

	.prose code {
		@apply rounded bg-muted px-1 py-0.5 text-sm;
	}

	.prose a {
		@apply text-primary underline-offset-4;
	}
}
</code></pre>
<h3>not-prose で一部を除外</h3>
<p><code>prose</code> の中で、特定の要素にスタイルを当てたくない場合は <code>not-prose</code> を使います。</p>
<pre><code class="language-html">&lt;article class=&quot;prose&quot;&gt;
	&lt;p&gt;ここは prose スタイルが当たる&lt;/p&gt;

	&lt;div class=&quot;not-prose&quot;&gt;
		&lt;!-- ここは prose の影響を受けない --&gt;
		&lt;div class=&quot;grid grid-cols-2 gap-4&quot;&gt;
			&lt;div class=&quot;rounded-lg bg-blue-100 p-4&quot;&gt;カード 1&lt;/div&gt;
			&lt;div class=&quot;rounded-lg bg-blue-100 p-4&quot;&gt;カード 2&lt;/div&gt;
		&lt;/div&gt;
	&lt;/div&gt;

	&lt;p&gt;ここも prose スタイルが当たる&lt;/p&gt;
&lt;/article&gt;
</code></pre>
<p>このブログでは、タグのバッジ表示部分に <code>not-prose</code> を使って、prose のスタイルが干渉しないようにしています。</p>
<h2>最大幅の制御</h2>
<p>デフォルトでは <code>prose</code> に <code>max-width: 65ch</code> が設定されています。記事の読みやすさのためですが、全幅にしたい場合は <code>max-w-none</code> で解除できます。</p>
<pre><code class="language-html">&lt;article class=&quot;prose max-w-none&quot;&gt;
	&lt;!-- 全幅で表示 --&gt;
&lt;/article&gt;
</code></pre>
<h2>SvelteKit ブログでの実践例</h2>
<p>mdsvex のレイアウトコンポーネントで <code>prose</code> を適用するのが一般的です。</p>
<pre><code class="language-svelte">&lt;!-- src/lib/layouts/post.svelte --&gt;
&lt;script lang=&quot;ts&quot;&gt;
	let { title, date, children } = $props();
&lt;/script&gt;

&lt;article class=&quot;mx-auto prose px-4 py-12 dark:prose-invert&quot;&gt;
	&lt;h1&gt;{title}&lt;/h1&gt;
	&lt;time class=&quot;text-muted-foreground&quot;&gt;
		{new Date(date).toLocaleDateString(&#39;ja-JP&#39;)}
	&lt;/time&gt;
	&lt;div data-pagefind-body&gt;
		{@render children()}
	&lt;/div&gt;
&lt;/article&gt;
</code></pre>
<h2>まとめ</h2>
<p><code>@tailwindcss/typography</code> は、Markdown コンテンツの見た目を劇的に改善する必須プラグインです。<code>prose</code> クラス一つで、見出し・段落・リスト・コード・テーブルすべてに適切なタイポグラフィが適用されます。ブログや技術ドキュメントを作るなら、まず入れておきたいプラグインです。</p>
<h2>参照</h2>
<ul>
<li><a href="https://tailwindcss.com/docs/typography-plugin">@tailwindcss/typography ドキュメント</a></li>
<li><a href="https://github.com/tailwindlabs/tailwindcss-typography">tailwindcss-typography GitHub</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Tailwind CSS v4 の変更点まとめ — v3 からの移行で知っておくこと</title>
			<link>https://toishi.dev/posts/tailwindcss-v4</link>
			<guid isPermaLink="true">https://toishi.dev/posts/tailwindcss-v4</guid>
			<pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate>
			<description>Tailwind CSS v4 で変わったこと。CSS ベースの設定、新しいカラーシステム、パフォーマンス改善などを解説します。</description>
			<category>tailwindcss</category>
			<content:encoded><![CDATA[<p>Tailwind CSS v4 は、設定の仕組みが大きく変わったメジャーアップデートです。v3 から移行する際に押さえておくべきポイントをまとめます。</p>
<h2>前提</h2>
<ul>
<li>Vite ベースのプロジェクト（PostCSS 経由でも使えるが本記事は <code>@tailwindcss/vite</code> を推奨構成として扱う）</li>
<li>Node.js v22 以上</li>
<li>v3 から移行する場合は、既存の <code>tailwind.config.js</code> の内容を CSS に書き換える準備があること</li>
</ul>
<h2>最大の変更: CSS ベースの設定</h2>
<p>v3 までは <code>tailwind.config.js</code>（JavaScript）で設定していましたが、v4 では <strong>CSS ファイル内で設定</strong> します。</p>
<h3>Before（v3）</h3>
<pre><code class="language-js">// tailwind.config.js
module.exports = {
	theme: {
		extend: {
			colors: {
				brand: &#39;#3b82f6&#39;
			}
		}
	}
};
</code></pre>
<h3>After（v4）</h3>
<pre><code class="language-css">/* app.css */
@import &#39;tailwindcss&#39;;

@theme {
	--color-brand: #3b82f6;
}
</code></pre>
<p><code>tailwind.config.js</code> は不要になり、<code>@theme</code> ディレクティブで CSS 変数としてテーマを定義します。</p>
<h2>Vite プラグインとして動作</h2>
<p>v4 は PostCSS プラグインではなく、<strong>Vite プラグイン</strong>として動作します（PostCSS 経由でも使えますが、Vite プラグインが推奨）。</p>
<pre><code class="language-ts">// vite.config.ts
import tailwindcss from &#39;@tailwindcss/vite&#39;;

export default defineConfig({
	plugins: [tailwindcss()]
});
</code></pre>
<p>これにより、ビルドパフォーマンスが大幅に向上しています。</p>
<h2>@import による取り込み</h2>
<p>v3 では <code>@tailwind base; @tailwind components; @tailwind utilities;</code> の 3 行が必要でしたが、v4 では 1 行です。</p>
<pre><code class="language-css">/* v3 */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* v4 */
@import &#39;tailwindcss&#39;;
</code></pre>
<h2>新しいカラーパレット</h2>
<p>v4 ではカラーパレットが OKLCH ベースに更新されました。より広い色域をサポートし、色の知覚的な均一性が向上しています。</p>
<pre><code class="language-css">@theme {
	--color-primary: oklch(0.6 0.2 250);
}
</code></pre>
<h2>コンテナクエリのネイティブサポート</h2>
<p>v3 では <code>@tailwindcss/container-queries</code> プラグインが必要でしたが、v4 ではビルトインです。</p>
<pre><code class="language-html">&lt;div class=&quot;@container&quot;&gt;
	&lt;div class=&quot;grid grid-cols-1 @lg:grid-cols-2&quot;&gt;
		&lt;!-- コンテナ幅に応じてレイアウト変更 --&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h2>移行のポイント</h2>
<h3>1. tailwind.config.js → @theme</h3>
<p>設定を CSS に移行します。多くのプロジェクトでは <code>@theme</code> ブロックに移すだけで済みます。</p>
<h3>2. content の設定が不要に</h3>
<p>v4 は自動でソースファイルを検出するため、<code>content</code> の設定が不要になりました。</p>
<h3>3. プラグインの互換性</h3>
<p>v3 のプラグインがそのまま動かない場合があります。<code>@tailwindcss/typography</code> は v4 対応版を使いましょう。</p>
<pre><code class="language-bash">pnpm add -D @tailwindcss/typography@latest
</code></pre>
<h3>4. 一部のクラス名の変更</h3>
<table>
<thead>
<tr>
<th>v3</th>
<th>v4</th>
</tr>
</thead>
<tbody><tr>
<td><code>bg-opacity-50</code></td>
<td><code>bg-black/50</code> （既に v3 でも使えた）</td>
</tr>
<tr>
<td><code>ring-offset-2</code></td>
<td>統合された ring ユーティリティ</td>
</tr>
</tbody></table>
<h2>SvelteKit での設定例</h2>
<p>このブログで実際に使っている設定を抜粋します。<code>vite.config.ts</code> 側はプラグインを並べるだけです。</p>
<pre><code class="language-ts">// vite.config.ts
import { sveltekit } from &#39;@sveltejs/kit/vite&#39;;
import tailwindcss from &#39;@tailwindcss/vite&#39;;
import { defineConfig } from &#39;vite&#39;;

export default defineConfig({
	plugins: [tailwindcss(), sveltekit()]
});
</code></pre>
<p>CSS 側がやや複雑になります。ポイントは 4 つです。</p>
<pre><code class="language-css">/* src/routes/layout.css */
@import &#39;tailwindcss&#39;;
@import &#39;tw-animate-css&#39;;
@import &#39;shadcn-svelte/tailwind.css&#39;;
@import &#39;@fontsource-variable/geist&#39;;

@custom-variant dark (&amp;:is(.dark *));
@plugin &#39;@tailwindcss/typography&#39;;

:root {
	--background: oklch(1 0 0);
	--foreground: oklch(0.145 0 0);
	--primary: oklch(0.45 0.16 155);
	--muted: oklch(0.97 0 0);
	/* ... */
}

.dark {
	--background: oklch(0.145 0 0);
	--foreground: oklch(0.985 0 0);
	--primary: oklch(0.65 0.18 155);
	/* ... */
}

@theme inline {
	--font-sans: &#39;Geist Variable&#39;, sans-serif;
	--color-background: var(--background);
	--color-foreground: var(--foreground);
	--color-primary: var(--primary);
	/* ... */
}
</code></pre>
<h3>ポイント</h3>
<ol>
<li><strong><code>@import &#39;tailwindcss&#39;</code> を先頭に置く</strong> — その後にプラグインや shadcn-svelte の追加 CSS を読み込む</li>
<li><strong><code>@custom-variant dark</code> でクラスベースのダークモードを定義</strong> — v3 の <code>darkMode: &#39;class&#39;</code> 相当</li>
<li><strong><code>:root</code> と <code>.dark</code> で CSS 変数を上書き</strong> — JavaScript で <code>&lt;html class=&quot;dark&quot;&gt;</code> をトグルすれば配色が一斉に切り替わる</li>
<li><strong><code>@theme inline</code> で CSS 変数を Tailwind ユーティリティへ橋渡し</strong> — <code>bg-background</code> や <code>text-primary</code> などのクラスが、上で定義した <code>--background</code> / <code>--primary</code> を参照するようになる</li>
</ol>
<p><code>@theme inline</code> の <code>inline</code> は、変数の値を埋め込まずに <code>var()</code> 参照のまま展開する指定です。これにより <code>.dark</code> での再定義が遅延評価され、同じ <code>bg-background</code> クラスがライト/ダークで適切に切り替わります。</p>
<h2>まとめ</h2>
<p>Tailwind CSS v4 は設定方法が大きく変わりましたが、「CSS の中で完結する」というシンプルな方向への進化です。パフォーマンスも向上しているので、新規プロジェクトでは v4 を選択するのが良いでしょう。</p>
<h2>参照</h2>
<ul>
<li><a href="https://tailwindcss.com/">Tailwind CSS 公式サイト</a></li>
<li><a href="https://tailwindcss.com/docs/upgrade-guide">Tailwind CSS v4 アップグレードガイド</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Svelte 4 の Store から Svelte 5 Runes への移行ガイド</title>
			<link>https://toishi.dev/posts/svelte5-migration-from-stores</link>
			<guid isPermaLink="true">https://toishi.dev/posts/svelte5-migration-from-stores</guid>
			<pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate>
			<description>Svelte 4 の Store（writable / readable / derived）を Svelte 5 の Runes に書き換える方法を、具体例とともに解説します。</description>
			<category>svelte</category><category>svelte5</category>
			<content:encoded><![CDATA[<p>Svelte 5 では Runes の登場により、Store を使わなくてもリアクティブな状態管理が可能になりました。既存の Store ベースのコードをどう移行するか、パターン別に見ていきます。</p>
<h2>前提</h2>
<ul>
<li>移行元の Svelte 4 プロジェクト（<code>writable</code> / <code>derived</code> / <code>readable</code> を使ったコードがあること）</li>
<li>Svelte 5 にバージョンアップ済み、または並行で動作確認できる環境</li>
<li>Svelte 5 は Svelte 4 の Store と後方互換性があるため、段階的な移行が可能</li>
</ul>
<h2>writable → $state</h2>
<p>最も基本的な移行パターンです。</p>
<h3>Before（Svelte 4）</h3>
<pre><code class="language-ts">// stores.ts
import { writable } from &#39;svelte/store&#39;;
export const count = writable(0);
</code></pre>
<pre><code class="language-svelte">&lt;!-- Component.svelte --&gt;
&lt;script&gt;
	import { count } from &#39;./stores&#39;;
&lt;/script&gt;

&lt;button on:click={() =&gt; $count++}&gt;{$count}&lt;/button&gt;
</code></pre>
<h3>After（Svelte 5）</h3>
<p>コンポーネント内で完結する状態なら、そのまま <code>$state</code> に置き換えます。</p>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
	let count = $state(0);
&lt;/script&gt;

&lt;button onclick={() =&gt; count++}&gt;{count}&lt;/button&gt;
</code></pre>
<h2>コンポーネント間で共有する状態</h2>
<p>複数コンポーネントで状態を共有していた場合、<code>.svelte.ts</code> ファイルで Runes を使えます。</p>
<pre><code class="language-ts">// counter.svelte.ts
let count = $state(0);

export function getCount() {
	return count;
}

export function increment() {
	count++;
}

export function reset() {
	count = 0;
}
</code></pre>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
	import { getCount, increment } from &#39;./counter.svelte.ts&#39;;
&lt;/script&gt;

&lt;button onclick={increment}&gt;{getCount()}&lt;/button&gt;
</code></pre>
<p>ポイントは <strong>ファイル拡張子が <code>.svelte.ts</code></strong> であること。これにより、ファイルのトップレベルで <code>$state</code> などの Runes が使えます。</p>
<h2>derived → $derived</h2>
<h3>Before</h3>
<pre><code class="language-ts">import { writable, derived } from &#39;svelte/store&#39;;

const items = writable([1, 2, 3]);
const total = derived(items, ($items) =&gt; $items.reduce((a, b) =&gt; a + b, 0));
</code></pre>
<h3>After</h3>
<pre><code class="language-ts">// cart.svelte.ts
let items = $state([1, 2, 3]);
let total = $derived(items.reduce((a, b) =&gt; a + b, 0));
</code></pre>
<p><code>$derived</code> は依存関係を自動追跡するので、明示的に依存元を指定する必要がありません。</p>
<h2>readable → $state（読み取り専用の公開）</h2>
<p>外部に読み取り専用で公開したい場合は、getter 関数を使います。</p>
<h3>Before</h3>
<pre><code class="language-ts">import { readable } from &#39;svelte/store&#39;;

export const time = readable(new Date(), (set) =&gt; {
	const interval = setInterval(() =&gt; set(new Date()), 1000);
	return () =&gt; clearInterval(interval);
});
</code></pre>
<h3>After</h3>
<pre><code class="language-ts">// time.svelte.ts
let time = $state(new Date());

const interval = setInterval(() =&gt; {
	time = new Date();
}, 1000);

// getter で読み取り専用として公開
export function getTime() {
	return time;
}
</code></pre>
<h2>クラスベースのアプローチ</h2>
<p>より構造化したい場合は、クラスで状態をカプセル化できます。</p>
<pre><code class="language-ts">// todo-store.svelte.ts
export class TodoStore {
	items = $state&lt;{ text: string; done: boolean }[]&gt;([]);

	get remaining() {
		return this.items.filter((item) =&gt; !item.done).length;
	}

	get completed() {
		return this.items.filter((item) =&gt; item.done).length;
	}

	add(text: string) {
		this.items.push({ text, done: false });
	}

	toggle(index: number) {
		this.items[index].done = !this.items[index].done;
	}

	remove(index: number) {
		this.items.splice(index, 1);
	}
}
</code></pre>
<pre><code class="language-svelte">&lt;script lang=&quot;ts&quot;&gt;
	import { TodoStore } from &#39;./todo-store.svelte.ts&#39;;

	const todos = new TodoStore();
&lt;/script&gt;

&lt;p&gt;残り {todos.remaining} 件&lt;/p&gt;
{#each todos.items as todo, i}
	&lt;label&gt;
		&lt;input type=&quot;checkbox&quot; checked={todo.done} onchange={() =&gt; todos.toggle(i)} /&gt;
		{todo.text}
	&lt;/label&gt;
{/each}
</code></pre>
<h2>まとめ</h2>
<ol>
<li><strong>段階的に移行できる</strong> — Svelte 5 は Store との後方互換性を持っています。一気に書き換える必要はありません</li>
<li><strong><code>.svelte.ts</code> を忘れずに</strong> — Runes をモジュールのトップレベルで使うには <code>.svelte.ts</code> 拡張子が必要です</li>
<li><strong><code>$:</code> ラベルは <code>$derived</code> / <code>$effect</code> に</strong> — 値の計算は <code>$derived</code>、副作用は <code>$effect</code> に分離しましょう</li>
<li><strong><code>on:click</code> → <code>onclick</code></strong> — イベントハンドラの書き方も変わっています</li>
</ol>
<p>Store はまだ使えますが、新しいコードでは Runes を使うことで、よりシンプルで直感的な状態管理ができます。</p>
<h2>参照</h2>
<ul>
<li><a href="https://svelte.dev/docs/svelte/v5-migration-guide">Svelte 5 移行ガイド</a></li>
<li><a href="https://svelte.dev/docs/svelte/what-are-runes">Svelte Runes とは</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>Vite の import.meta.glob を使ったファイルの動的インポート</title>
			<link>https://toishi.dev/posts/import-meta-glob</link>
			<guid isPermaLink="true">https://toishi.dev/posts/import-meta-glob</guid>
			<pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
			<description>import.meta.glob の使い方と活用パターン。ブログ記事の一括取得や、コンポーネントの動的ロードなどの実例を紹介します。</description>
			<category>vite</category><category>svelte-kit</category>
			<content:encoded><![CDATA[<p><code>import.meta.glob</code> は Vite が提供するファイルの動的インポート機能です。glob パターンでファイルを一括で取得でき、ブログの記事一覧取得などに活用できます。</p>
<h2>前提</h2>
<ul>
<li>Vite ベースのプロジェクト（SvelteKit / Vue / 素の Vite アプリなど）</li>
<li>本記事のサンプルは SvelteKit + mdsvex を使った構成を想定</li>
</ul>
<h2>基本の使い方</h2>
<h3>遅延インポート（デフォルト）</h3>
<pre><code class="language-ts">const modules = import.meta.glob(&#39;/src/posts/*.md&#39;);
</code></pre>
<p>返り値は、ファイルパスをキー、動的 import 関数を値とするオブジェクトです:</p>
<pre><code class="language-ts">{
  &#39;/src/posts/hello-world.md&#39;: () =&gt; import(&#39;/src/posts/hello-world.md&#39;),
  &#39;/src/posts/blog-setup.md&#39;: () =&gt; import(&#39;/src/posts/blog-setup.md&#39;),
}
</code></pre>
<p>各モジュールは関数を呼び出したときに初めて読み込まれます（遅延ロード）。</p>
<h3>即時インポート（eager）</h3>
<pre><code class="language-ts">const modules = import.meta.glob(&#39;/src/posts/*.md&#39;, { eager: true });
</code></pre>
<p><code>eager: true</code> を指定すると、ビルド時にすべてのモジュールが即座に読み込まれます:</p>
<pre><code class="language-ts">{
  &#39;/src/posts/hello-world.md&#39;: { metadata: {...}, default: Component },
  &#39;/src/posts/blog-setup.md&#39;: { metadata: {...}, default: Component },
}
</code></pre>
<h2>ブログでの活用 — 記事一覧の取得</h2>
<p>このブログで実際に使っているパターンです。</p>
<pre><code class="language-ts">// src/lib/utils/posts.ts
export type PostMeta = {
	slug: string;
	title: string;
	description: string;
	date: string;
	tags: string[];
	draft?: boolean;
};

export async function getAllPosts(): Promise&lt;PostMeta[]&gt; {
	const modules = import.meta.glob(&#39;/src/posts/*.md&#39;, { eager: true });
	const posts: PostMeta[] = [];

	for (const [path, mod] of Object.entries(modules)) {
		const m = mod as { metadata: Omit&lt;PostMeta, &#39;slug&#39;&gt; };
		const slug = path.split(&#39;/&#39;).pop()!.replace(&#39;.md&#39;, &#39;&#39;);
		if (m.metadata.draft) continue;
		posts.push({ slug, ...m.metadata });
	}

	return posts.sort((a, b) =&gt; +new Date(b.date) - +new Date(a.date));
}
</code></pre>
<h3>ポイント</h3>
<ul>
<li><code>eager: true</code> — SSG ではビルド時にすべて読み込むので eager が適切</li>
<li><code>mod.metadata</code> — mdsvex が Markdown のフロントマターを <code>metadata</code> として export</li>
<li><code>mod.default</code> — Svelte コンポーネントとしてレンダリング可能</li>
</ul>
<h2>glob パターン</h2>
<p><code>import.meta.glob</code> は glob パターンを受け付けます。</p>
<pre><code class="language-ts">// 特定のディレクトリ
import.meta.glob(&#39;/src/posts/*.md&#39;);

// ネストしたディレクトリも含む
import.meta.glob(&#39;/src/posts/**/*.md&#39;);

// 複数パターン
import.meta.glob([&#39;/src/posts/*.md&#39;, &#39;/src/drafts/*.md&#39;]);

// 除外パターン
import.meta.glob(&#39;/src/posts/*.md&#39;, {
	// _で始まるファイルを除外
});
</code></pre>
<h2>import オプション</h2>
<h3>as: &#39;raw&#39; — 生のテキストとして取得</h3>
<pre><code class="language-ts">const files = import.meta.glob(&#39;/src/data/*.json&#39;, {
	eager: true,
	import: &#39;default&#39;,
	query: &#39;?raw&#39;
});
</code></pre>
<h3>import — 特定の export だけ取得</h3>
<pre><code class="language-ts">// metadata だけ取得（コンポーネント本体は不要な場合）
const metas = import.meta.glob(&#39;/src/posts/*.md&#39;, {
	eager: true,
	import: &#39;metadata&#39;
});
</code></pre>
<h2>動的ルートとの組み合わせ</h2>
<p>SvelteKit の動的ルートで、プリレンダリング対象のパスを列挙するのに使えます。</p>
<pre><code class="language-ts">// src/routes/posts/[slug]/+page.ts
export async function entries() {
	const modules = import.meta.glob(&#39;/src/posts/*.md&#39;);
	return Object.keys(modules).map((path) =&gt; ({
		slug: path.split(&#39;/&#39;).pop()!.replace(&#39;.md&#39;, &#39;&#39;)
	}));
}

export async function load({ params }) {
	const post = await import(`/src/posts/${params.slug}.md`);
	return {
		content: post.default,
		meta: post.metadata
	};
}
</code></pre>
<h2>タグ一覧の生成</h2>
<p>記事のメタデータから全タグを収集する例:</p>
<pre><code class="language-ts">export async function getAllTags(): Promise&lt;Map&lt;string, number&gt;&gt; {
	const posts = await getAllPosts();
	const tagMap = new Map&lt;string, number&gt;();

	for (const post of posts) {
		for (const tag of post.tags ?? []) {
			tagMap.set(tag, (tagMap.get(tag) ?? 0) + 1);
		}
	}

	return tagMap;
}
</code></pre>
<h2>注意点</h2>
<ol>
<li><strong>glob パターンは静的でなければならない</strong> — 変数を使ったパターンは不可</li>
<li><strong>ビルド時に解決される</strong> — ファイルの追加・削除は再ビルドが必要</li>
<li><strong>パスは <code>/src/</code> から始まる</strong> — プロジェクトルートからの絶対パス</li>
</ol>
<pre><code class="language-ts">// ❌ 変数は使えない
const dir = &#39;posts&#39;;
import.meta.glob(`/src/${dir}/*.md`);

// ✅ リテラルで指定
import.meta.glob(&#39;/src/posts/*.md&#39;);
</code></pre>
<h2>まとめ</h2>
<p><code>import.meta.glob</code> は Vite の強力な機能で、ファイルシステムベースのコンテンツ管理に最適です。ブログの記事一覧、タグ一覧、動的ルートの生成など、SvelteKit の静的サイト構築で欠かせないツールです。</p>
<h2>参照</h2>
<ul>
<li><a href="https://vite.dev/guide/features.html#glob-import">Vite Glob Import ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>mdsvex で Markdown ブログを作る — Svelte コンポーネントを記事に埋め込む</title>
			<link>https://toishi.dev/posts/mdsvex-guide</link>
			<guid isPermaLink="true">https://toishi.dev/posts/mdsvex-guide</guid>
			<pubDate>Sat, 21 Mar 2026 00:00:00 GMT</pubDate>
			<description>mdsvex を使って Markdown 内で Svelte コンポーネントを利用する方法。設定からカスタムレイアウト、rehype プラグインまで解説します。</description>
			<category>mdsvex</category><category>svelte-kit</category><category>svelte</category>
			<content:encoded><![CDATA[<p>mdsvex は「Markdown for Svelte」の略で、Markdown ファイルを Svelte コンポーネントとして扱えるプリプロセッサです。MDX（React 向け）の Svelte 版と考えると分かりやすいでしょう。</p>
<h2>前提</h2>
<ul>
<li>SvelteKit プロジェクト（Svelte 5 を想定）</li>
<li>Node.js v22 以上</li>
<li>既に <code>svelte.config.js</code> が存在し、<code>vitePreprocess()</code> などの preprocess を設定できる状態</li>
</ul>
<h2>セットアップ</h2>
<pre><code class="language-bash">pnpm add -D mdsvex
</code></pre>
<p><code>svelte.config.js</code> に設定を追加します。</p>
<pre><code class="language-js">import { mdsvex } from &#39;mdsvex&#39;;

const config = {
	extensions: [&#39;.svelte&#39;, &#39;.svx&#39;, &#39;.md&#39;],
	preprocess: [
		vitePreprocess(),
		mdsvex({
			extensions: [&#39;.svx&#39;, &#39;.md&#39;]
		})
	]
};
</code></pre>
<p><code>.md</code> ファイルを <code>extensions</code> に追加することで、Markdown ファイルが Svelte コンポーネントとして処理されます。</p>
<h2>フロントマター</h2>
<p>Markdown ファイルの先頭に YAML 形式でメタデータを記述できます。</p>
<pre><code class="language-markdown">---
title: 記事のタイトル
description: 記事の説明
date: 2026-03-06
tags:
  - svelte
  - markdown
---

本文がここから始まります。
</code></pre>
<p>フロントマターの値は <code>metadata</code> として export されるので、<code>import</code> で取得できます。</p>
<pre><code class="language-ts">const post = await import(&#39;./my-post.md&#39;);
console.log(post.metadata.title); // &quot;記事のタイトル&quot;
</code></pre>
<h2>カスタムレイアウト</h2>
<p>すべての記事に共通のレイアウトを適用するには、<code>layout</code> オプションを使います。</p>
<pre><code class="language-js">mdsvex({
	extensions: [&#39;.svx&#39;, &#39;.md&#39;],
	layout: {
		_: &#39;./src/lib/layouts/post.svelte&#39;
	}
});
</code></pre>
<p><code>_</code> はデフォルトレイアウトを意味します。レイアウトコンポーネントではフロントマターの値が Props として渡されます。</p>
<pre><code class="language-svelte">&lt;!-- src/lib/layouts/post.svelte --&gt;
&lt;script lang=&quot;ts&quot;&gt;
	let { title, date, tags = [], children } = $props();
&lt;/script&gt;

&lt;article class=&quot;prose dark:prose-invert&quot;&gt;
	&lt;h1&gt;{title}&lt;/h1&gt;
	&lt;time&gt;{new Date(date).toLocaleDateString(&#39;ja-JP&#39;)}&lt;/time&gt;
	{@render children()}
&lt;/article&gt;
</code></pre>
<h2>Markdown 内で Svelte コンポーネントを使う</h2>
<p>mdsvex の強力な機能の一つが、Markdown 内での Svelte コンポーネントの利用です。</p>
<pre><code class="language-markdown">---
title: インタラクティブな記事
---

&lt;script&gt;
  import Counter from &#39;$lib/components/Counter.svelte&#39;;
&lt;/script&gt;

普通の Markdown テキストです。

&lt;Counter /&gt;

↑ ボタンをクリックしてみてください。
</code></pre>
<p>これにより、静的な Markdown に動的なインタラクションを追加できます。</p>
<h2>rehype / remark プラグイン</h2>
<p>mdsvex は rehype（HTML 処理）と remark（Markdown 処理）のプラグインに対応しています。</p>
<h3>rehype-slug — 見出しに ID を自動付与</h3>
<pre><code class="language-bash">pnpm add -D rehype-slug
</code></pre>
<pre><code class="language-js">import rehypeSlug from &#39;rehype-slug&#39;;

mdsvex({
	rehypePlugins: [rehypeSlug]
});
</code></pre>
<p>これにより <code>## セットアップ</code> が <code>&lt;h2 id=&quot;セットアップ&quot;&gt;</code> になり、見出しへの直接リンクが可能になります。</p>
<h2>コードブロックのシンタックスハイライト</h2>
<p>mdsvex はデフォルトで PrismJS によるシンタックスハイライトをサポートしています。Markdown のコードブロックに言語を指定するだけです。</p>
<pre><code class="language-markdown">```ts
const message: string = &#39;Hello, mdsvex!&#39;;
```
</code></pre>
<p>Shiki を使いたい場合は、rehype プラグインとして組み込むことも可能です。</p>
<h2>import.meta.glob で記事を一括取得</h2>
<p>SvelteKit の <code>import.meta.glob</code> と組み合わせて、すべての記事のメタデータを取得できます。</p>
<pre><code class="language-ts">export async function getAllPosts() {
	const modules = import.meta.glob(&#39;/src/posts/*.md&#39;, { eager: true });
	const posts = [];

	for (const [path, mod] of Object.entries(modules)) {
		const slug = path.split(&#39;/&#39;).pop()!.replace(&#39;.md&#39;, &#39;&#39;);
		const { metadata } = mod as { metadata: PostMeta };
		if (!metadata.draft) {
			posts.push({ slug, ...metadata });
		}
	}

	return posts.sort((a, b) =&gt; +new Date(b.date) - +new Date(a.date));
}
</code></pre>
<h2>まとめ</h2>
<p>mdsvex を使うことで、Markdown の手軽さと Svelte の表現力を両立できます。ブログや技術ドキュメントの構築に最適です。</p>
<ul>
<li>Markdown ファイルを Svelte コンポーネントとして処理</li>
<li>フロントマターでメタデータを管理</li>
<li>カスタムレイアウトで統一的なデザイン</li>
<li>Svelte コンポーネントを記事に埋め込み可能</li>
<li>rehype / remark プラグインで拡張できる</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://mdsvex.pngwn.io/">mdsvex 公式サイト</a></li>
<li><a href="https://github.com/pngwn/MDsveX">mdsvex GitHub</a></li>
</ul>
]]></content:encoded>
		</item>
		<item>
			<title>SvelteKit で静的サイトを作る — prerender の仕組みと設定</title>
			<link>https://toishi.dev/posts/sveltekit-prerendering</link>
			<guid isPermaLink="true">https://toishi.dev/posts/sveltekit-prerendering</guid>
			<pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate>
			<description>SvelteKit のプリレンダリング機能を使って静的サイトを生成する方法。設定のコツと注意点を解説します。</description>
			<category>svelte-kit</category><category>cloudflare</category>
			<content:encoded><![CDATA[<p>SvelteKit はサーバーサイドレンダリング（SSR）がデフォルトですが、<code>prerender</code> オプションを使うことで静的サイトジェネレーター（SSG）としても利用できます。このブログも全ページをプリレンダリングして Cloudflare Pages にデプロイしています。</p>
<h2>前提</h2>
<ul>
<li>SvelteKit プロジェクト（Svelte 5 を想定）</li>
<li>Node.js v22 以上</li>
<li>全ページ静的化を想定する場合は、ユーザー固有データやクエリパラメータに依存するページを含まないこと</li>
</ul>
<h2>prerender の基本</h2>
<p>ページごとに <code>+page.ts</code> で <code>prerender</code> を <code>true</code> に設定します。</p>
<pre><code class="language-ts">// src/routes/+page.ts
export const prerender = true;
</code></pre>
<p>レイアウトに設定すると、配下のすべてのページに適用されます。</p>
<pre><code class="language-ts">// src/routes/+layout.ts
export const prerender = true;
</code></pre>
<h2>動的ルートのプリレンダリング</h2>
<p><code>[slug]</code> のような動的ルートは、ビルド時にどのパスを生成するか知る必要があります。<code>entries</code> 関数でパスの一覧を返します。</p>
<pre><code class="language-ts">// src/routes/posts/[slug]/+page.ts
export const prerender = true;

export async function entries() {
	const modules = import.meta.glob(&#39;/src/posts/*.md&#39;);
	return Object.keys(modules).map((path) =&gt; ({
		slug: path.split(&#39;/&#39;).pop()!.replace(&#39;.md&#39;, &#39;&#39;)
	}));
}
</code></pre>
<p>SvelteKit はページ内のリンクを辿って自動的にプリレンダリング対象を発見する <strong>クローリング</strong> も行います。トップページからリンクされているページは <code>entries</code> なしでもプリレンダリングされることがあります。ただし、確実に生成するためには <code>entries</code> を明示するのが安全です。</p>
<h2>adapter-static vs adapter-cloudflare</h2>
<p>全ページを静的にするなら <code>adapter-static</code> も選択肢ですが、このブログでは <code>adapter-cloudflare</code> を使っています。</p>
<pre><code class="language-js">// svelte.config.js
import adapter from &#39;@sveltejs/adapter-cloudflare&#39;;

const config = {
	kit: {
		adapter: adapter()
	}
};
</code></pre>
<p><code>adapter-cloudflare</code> でも <code>prerender = true</code> のページは静的ファイルとして出力されるので、実質的に SSG と同じ動作になります。将来的にサーバーサイドの処理（API ルート、フォーム処理など）を追加したくなったときに柔軟に対応できるメリットがあります。</p>
<h2>prerender できないケース</h2>
<p>以下のようなページはプリレンダリングできません。</p>
<ul>
<li><strong>ユーザー固有のデータを表示するページ</strong> — ログイン状態に依存する場合は SSR が必要</li>
<li><strong>動的な検索結果</strong> — クエリパラメータに基づく結果は静的に生成できない</li>
<li><strong>フォーム送信を処理するページ</strong> — <code>+page.server.ts</code> の <code>actions</code> を使う場合</li>
</ul>
<pre><code class="language-ts">// これは prerender できない
export async function load({ url }) {
	const q = url.searchParams.get(&#39;q&#39;); // 無限のパターンがある
	return { results: await search(q) };
}
</code></pre>
<h2>handleHttpError の設定</h2>
<p>プリレンダリング中にリンク切れがあるとビルドが失敗します。開発中は <code>warn</code> に設定しておくと便利です。</p>
<pre><code class="language-js">// svelte.config.js
const config = {
	kit: {
		prerender: {
			handleHttpError: &#39;warn&#39;
		}
	}
};
</code></pre>
<p>本番環境では <code>fail</code>（デフォルト）にしてリンク切れを検知するのがおすすめです。</p>
<h2>ビルドと確認</h2>
<pre><code class="language-bash">pnpm build    # プリレンダリング実行
pnpm preview  # ビルド結果をローカルで確認
</code></pre>
<p><code>.svelte-kit/cloudflare/</code> に生成された HTML ファイルを確認すると、各ページが静的ファイルとして出力されていることが分かります。</p>
<h2>まとめ</h2>
<ul>
<li><code>prerender = true</code> でページを静的に出力できる</li>
<li>動的ルートは <code>entries</code> でパスを列挙する</li>
<li><code>adapter-cloudflare</code> でも prerender は使える — 静的と動的のハイブリッド構成が可能</li>
<li>全ページ prerender にすれば実質 SSG として運用できる</li>
</ul>
<h2>参照</h2>
<ul>
<li><a href="https://svelte.dev/docs/kit/page-options#prerender">SvelteKit プリレンダリング ドキュメント</a></li>
<li><a href="https://svelte.dev/docs/kit/">SvelteKit ドキュメント</a></li>
</ul>
]]></content:encoded>
		</item>
	</channel>
</rss>