T-CREATOR

Preact でミニブログを 1 日で公開:ルーティング・MDX・SEO まで一気通貫

Preact でミニブログを 1 日で公開:ルーティング・MDX・SEO まで一気通貫

軽量で高速な Preact を使えば、本格的なブログを驚くほど短時間で構築できます。React と互換性がありながらわずか 3KB という軽さで、SEO に配慮したブログサイトを 1 日で完成させることも可能でしょう。

この記事では、Preact によるミニブログ構築の全工程を解説します。ルーティング設定から MDX による記事管理、SEO 対策まで、実務で使える技術を一気通貫でお伝えしますね。

背景

Preact の特徴と選択理由

Preact は React の軽量代替ライブラリとして知られ、以下の特徴を持っています。

#項目説明
1サイズわずか 3KB(gzip 圧縮時)で React の約 10 分の 1
2互換性React とほぼ同じ API を提供し、既存知識を活用可能
3パフォーマンスVirtual DOM の最適化により高速レンダリング
4学習コストReact 経験者なら即座に移行できる

ブログのような静的コンテンツ中心のサイトでは、JavaScript のバンドルサイズが読み込み速度に直結します。Preact を選択することで、初回表示速度を大幅に改善でき、SEO スコアの向上も期待できるでしょう。

モダンブログに求められる要件

現代の技術ブログには、以下の要件が必須となっています。

mermaidflowchart TB
    blog["ブログサイト"] --> routing["ルーティング<br/>機能"]
    blog --> content["コンテンツ<br/>管理"]
    blog --> seo["SEO 対策"]

    routing --> dynamic["動的ページ遷移"]
    routing --> spa["SPA 体験"]

    content --> mdx["MDX 記述"]
    content --> syntax["シンタックス<br/>ハイライト"]

    seo --> meta["メタタグ最適化"]
    seo --> sitemap["サイトマップ生成"]
    seo --> ogp["OGP 設定"]

この図は、ブログ構築に必要な 3 つの柱と、それぞれの実装要素を示しています。

これらの要件を満たすため、Preact エコシステムの各種ツールを組み合わせて構築していきます。

課題

従来のブログ構築における問題点

静的サイトジェネレーターや WordPress を使った従来のブログ構築には、いくつかの課題があります。

#課題影響
1セットアップの複雑さ環境構築に時間がかかり、初日に公開できない
2バンドルサイズの肥大化React ベースのフレームワークは初期ロードが遅い
3カスタマイズの制約テーマやプラグインの制限で自由度が低い
4ビルド時間の長さ記事数が増えると再ビルドに時間がかかる

特に個人ブログや技術ブログでは、シンプルで軽量な構成が望ましいものです。

Preact 特有の課題

Preact を選択する際には、以下の点に注意が必要でしょう。

mermaidflowchart LR
    challenge["Preact 利用時の<br/>課題"] --> ecosystem["エコシステムの<br/>少なさ"]
    challenge --> ssr["SSR/SSG の<br/>設定複雑さ"]
    challenge --> plugin["プラグインの<br/>互換性"]

    ecosystem --> solution1["preact-compat で<br/>React ライブラリ利用"]
    ssr --> solution2["Preact CLI または<br/>Vite を活用"]
    plugin --> solution3["Preact 専用<br/>パッケージ選定"]

この図は、Preact 固有の課題とその解決アプローチを示しています。

これらの課題を理解した上で、適切なツールと設計パターンを選択することが重要です。

解決策

プロジェクト構成の全体像

Preact でブログを構築する際の推奨構成を示します。

mermaidflowchart TB
    subgraph build["ビルドツール"]
        vite["Vite<br/>(高速バンドル)"]
    end

    subgraph framework["フレームワーク"]
        preact["Preact<br/>(コア)"]
        preactRouter["preact-router<br/>(ルーティング)"]
    end

    subgraph content_management["コンテンツ管理"]
        mdx["MDX<br/>(記事記述)"]
        frontmatter["gray-matter<br/>(メタデータ解析)"]
    end

    subgraph styling["スタイリング"]
        css["CSS Modules"]
    end

    subgraph seo_tools["SEO ツール"]
        helmet["preact-helmet<br/>(メタタグ)"]
        sitemap_gen["sitemap.js<br/>(サイトマップ)"]
    end

    vite --> preact
    preact --> preactRouter
    preact --> mdx
    mdx --> frontmatter
    preact --> helmet
    preact --> css

この構成により、最小限の依存関係で必要な機能をすべて実装できます。

技術スタック選定の理由

各技術を選定した理由を整理します。

#技術理由
1VitePreact との相性が良く、開発サーバーが高速
2preact-routerシンプルな API でルーティングを実装可能
3MDXReact/Preact コンポーネントを Markdown 内で利用可能
4gray-matterFrontmatter の解析が簡単
5preact-helmetメタタグの動的管理に対応

この組み合わせにより、開発体験と実行パフォーマンスの両立が実現できるでしょう。

具体例

プロジェクトのセットアップ

まず、Vite を使って Preact プロジェクトを初期化します。

bashyarn create vite my-preact-blog --template preact
cd my-preact-blog
yarn install

このコマンドで、Preact + Vite の基本構成が作成されます。開発サーバーは yarn dev で起動可能です。

次に、必要なパッケージをインストールしましょう。

bashyarn add preact-router preact-helmet gray-matter
yarn add -D @mdx-js/rollup remark-gfm rehype-highlight

これらのパッケージで、ルーティング、SEO、MDX サポートを実現します。

Vite の設定

vite.config.js を編集して MDX サポートを追加します。

javascriptimport { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import mdx from '@mdx-js/rollup';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';

export default defineConfig({
  plugins: [
    // MDX プラグインを Preact より先に配置
    mdx({
      remarkPlugins: [remarkGfm], // GitHub Flavored Markdown サポート
      rehypePlugins: [rehypeHighlight], // シンタックスハイライト
    }),
    preact(),
  ],
});

この設定により、.mdx ファイルを Preact コンポーネントとしてインポートできるようになります。

MDX のプラグインオプションでは、GFM(テーブルやタスクリストなど)とコードハイライトを有効化しています。

プロジェクトディレクトリ構造

ブログアプリケーションのディレクトリ構造を設計します。

csharpmy-preact-blog/
├── src/
│   ├── components/        # 再利用可能なコンポーネント
│   │   ├── Header.jsx
│   │   ├── Footer.jsx
│   │   └── ArticleCard.jsx
│   ├── pages/            # ページコンポーネント
│   │   ├── Home.jsx
│   │   ├── Article.jsx
│   │   └── NotFound.jsx
│   ├── posts/            # MDX 記事ファイル
│   │   ├── first-post.mdx
│   │   └── second-post.mdx
│   ├── utils/            # ユーティリティ関数
│   │   └── posts.js
│   ├── app.jsx           # ルートコンポーネント
│   └── main.jsx          # エントリーポイント
├── public/               # 静的ファイル
└── vite.config.js

この構造により、コンポーネントと記事を明確に分離し、保守性を高めています。

ルーティングの実装

src​/​app.jsx でアプリケーション全体のルーティングを設定しましょう。

javascriptimport { h } from 'preact';
import Router from 'preact-router';
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
import Article from './pages/Article';
import NotFound from './pages/NotFound';

まず必要なモジュールをインポートします。preact-router がルーティングの中核です。

次に、アプリケーションコンポーネントを定義します。

javascriptexport function App() {
  return (
    <div id='app'>
      <Header />
      <main>
        <Router>
          <Home path='/' />
          <Article path='/posts/:slug' />
          <NotFound default />
        </Router>
      </main>
      <Footer />
    </div>
  );
}

Router コンポーネント内で各ページを定義し、path プロパティで URL パターンを指定します。:slug は動的パラメータです。

ヘッダーコンポーネントの実装

src​/​components​/​Header.jsx でサイトヘッダーを作成します。

javascriptimport { h } from 'preact';
import { Link } from 'preact-router/match';
import './Header.css';

export default function Header() {
  return (
    <header className='header'>
      <nav className='nav'>
        <Link href='/' className='logo'>
          My Tech Blog
        </Link>
        <ul className='nav-links'>
          <li>
            <Link href='/'>ホーム</Link>
          </li>
          <li>
            <Link href='/about'>About</Link>
          </li>
        </ul>
      </nav>
    </header>
  );
}

preact-routerLink コンポーネントを使うことで、SPA 風のページ遷移が実現できます。

記事一覧ページの実装

src​/​pages​/​Home.jsx でトップページを作成しましょう。

javascriptimport { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { Helmet } from 'preact-helmet';
import { getAllPosts } from '../utils/posts';
import ArticleCard from '../components/ArticleCard';
import './Home.css';

必要なフックと記事取得関数をインポートします。preact-helmet で SEO 対策を行います。

次に、コンポーネント本体を実装します。

javascriptexport default function Home() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // 記事一覧を非同期で取得
    getAllPosts().then(setPosts);
  }, []);

  return (
    <div className='home'>
      <Helmet>
        <title>ホーム | My Tech Blog</title>
        <meta
          name='description'
          content='Preact で構築した技術ブログです'
        />
        <meta property='og:title' content='My Tech Blog' />
        <meta property='og:type' content='website' />
      </Helmet>

      <h1>最新記事</h1>
      <div className='articles-grid'>
        {posts.map((post) => (
          <ArticleCard key={post.slug} post={post} />
        ))}
      </div>
    </div>
  );
}

useEffect で初回レンダリング時に記事を取得し、Helmet でページのメタ情報を設定しています。

記事カードコンポーネント

src​/​components​/​ArticleCard.jsx で記事一覧のカード表示を実装します。

javascriptimport { h } from 'preact';
import { Link } from 'preact-router/match';
import './ArticleCard.css';

export default function ArticleCard({ post }) {
  const { slug, title, description, date, tags } = post;

  return (
    <article className='article-card'>
      <Link href={`/posts/${slug}`}>
        <h2>{title}</h2>
        <p className='description'>{description}</p>
        <div className='meta'>
          <time>
            {new Date(date).toLocaleDateString('ja-JP')}
          </time>
          <div className='tags'>
            {tags?.map((tag) => (
              <span key={tag} className='tag'>
                {tag}
              </span>
            ))}
          </div>
        </div>
      </Link>
    </article>
  );
}

記事のメタデータを受け取り、カード形式で表示します。クリックすると記事詳細ページに遷移する仕組みです。

記事データ管理ユーティリティ

src​/​utils​/​posts.js で記事データの取得ロジックを実装しましょう。

javascript// すべての MDX ファイルを動的インポート
const postModules = import.meta.glob('../posts/*.mdx', {
  eager: true,
});

/**
 * すべての記事データを取得
 * @returns {Promise<Array>} 記事オブジェクトの配列
 */
export async function getAllPosts() {
  const posts = [];

  for (const path in postModules) {
    const mod = postModules[path];
    const slug = path
      .replace('../posts/', '')
      .replace('.mdx', '');

    posts.push({
      slug,
      ...mod.frontmatter, // frontmatter からメタデータを取得
    });
  }

  // 日付の降順でソート
  return posts.sort(
    (a, b) => new Date(b.date) - new Date(a.date)
  );
}

Vite の import.meta.glob を使って、すべての MDX ファイルを自動的に読み込んでいます。

次に、特定の記事を取得する関数を追加します。

javascript/**
 * スラッグから記事データを取得
 * @param {string} slug - 記事のスラッグ
 * @returns {Promise<Object|null>} 記事オブジェクト
 */
export async function getPostBySlug(slug) {
  const mod = postModules[`../posts/${slug}.mdx`];

  if (!mod) return null;

  return {
    slug,
    ...mod.frontmatter,
    Component: mod.default, // MDX コンポーネント本体
  };
}

この関数は、記事の内容を表示するために必要な MDX コンポーネントを返します。

MDX 記事ファイルの作成

src​/​posts​/​first-post.mdx で実際の記事を作成しましょう。

mdx---
title: 'Preact で始める軽量 Web アプリ開発'
description: 'Preact の基本から実践まで、軽量フレームワークの魅力を解説します'
date: '2025-01-15'
tags: ['Preact', 'JavaScript', 'フロントエンド']
---

# はじめに

Preact は React の軽量代替として注目されているライブラリです。
わずか 3KB という小ささながら、React とほぼ同じ API を提供しています。

# Preact の特徴

Preact が選ばれる理由は以下の通りです。

- **軽量**: gzip 圧縮時わずか 3KB
- **高速**: 仮想 DOM の最適化により高速レンダリング
- **互換性**: React エコシステムとの互換性

# まとめ

Preact を使えば、パフォーマンスを犠牲にせず、
モダンな Web アプリケーションを構築できるでしょう。

Frontmatter(--- で囲まれた部分)にメタデータを記述し、本文は通常の Markdown で記述します。

記事詳細ページの実装

src​/​pages​/​Article.jsx で個別記事を表示するページを作成します。

javascriptimport { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { Helmet } from 'preact-helmet';
import { getPostBySlug } from '../utils/posts';
import './Article.css';

記事取得関数と必要なモジュールをインポートします。

コンポーネント本体を実装しましょう。

javascriptexport default function Article({ slug }) {
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // スラッグから記事を取得
    getPostBySlug(slug).then((data) => {
      setPost(data);
      setLoading(false);
    });
  }, [slug]);

  if (loading) {
    return <div className='loading'>読み込み中...</div>;
  }

  if (!post) {
    return (
      <div className='not-found'>記事が見つかりません</div>
    );
  }

  const { Component, title, description, date, tags } =
    post;

  return (
    <article className='article'>
      <Helmet>
        <title>{title} | My Tech Blog</title>
        <meta name='description' content={description} />
        <meta property='og:title' content={title} />
        <meta
          property='og:description'
          content={description}
        />
        <meta property='og:type' content='article' />
      </Helmet>

      <header className='article-header'>
        <h1>{title}</h1>
        <div className='article-meta'>
          <time>
            {new Date(date).toLocaleDateString('ja-JP')}
          </time>
          <div className='tags'>
            {tags?.map((tag) => (
              <span key={tag} className='tag'>
                {tag}
              </span>
            ))}
          </div>
        </div>
      </header>

      <div className='article-content'>
        <Component />
      </div>
    </article>
  );
}

MDX から取得した Component をレンダリングすることで、記事本文が表示されます。SEO 対策として、各記事固有のメタタグも設定していますね。

シンタックスハイライトのスタイル追加

コードブロックを見やすくするため、highlight.js のスタイルを追加します。

src​/​main.jsx でスタイルシートをインポートしましょう。

javascriptimport { render } from 'preact';
import { App } from './app';
import './index.css';

// シンタックスハイライト用スタイル
import 'highlight.js/styles/github-dark.css';

render(<App />, document.getElementById('app'));

これで、記事内のコードブロックが適切にハイライト表示されるようになります。

SEO 対策の実装

サイト全体の SEO 設定を src​/​app.jsx に追加します。

javascriptimport { h } from 'preact';
import { Helmet } from 'preact-helmet';
import Router from 'preact-router';
// ... その他のインポート

export function App() {
  return (
    <div id='app'>
      <Helmet
        defaultTitle='My Tech Blog'
        titleTemplate='%s | My Tech Blog'
      >
        <html lang='ja' />
        <meta charset='utf-8' />
        <meta
          name='viewport'
          content='width=device-width, initial-scale=1'
        />
        <meta name='theme-color' content='#673ab8' />
      </Helmet>

      <Header />
      <main>
        <Router>
          <Home path='/' />
          <Article path='/posts/:slug' />
          <NotFound default />
        </Router>
      </main>
      <Footer />
    </div>
  );
}

HelmetdefaultTitletitleTemplate により、各ページで統一感のあるタイトル表示が可能です。

サイトマップ生成スクリプト

scripts​/​generate-sitemap.js でサイトマップを自動生成します。

javascriptimport fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(
  fileURLToPath(import.meta.url)
);
const postsDir = path.join(__dirname, '../src/posts');
const siteUrl = 'https://your-blog.com';

まず、必要なモジュールと定数を定義します。siteUrl は実際のドメインに置き換えてください。

サイトマップの XML を生成する処理を実装しましょう。

javascript// MDX ファイルからスラッグを取得
const posts = fs
  .readdirSync(postsDir)
  .filter((file) => file.endsWith('.mdx'))
  .map((file) => file.replace('.mdx', ''));

// サイトマップ XML を生成
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>${siteUrl}/</loc>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
  ${posts
    .map(
      (slug) => `
  <url>
    <loc>${siteUrl}/posts/${slug}</loc>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>`
    )
    .join('')}
</urlset>`;

// public ディレクトリに出力
fs.writeFileSync(
  path.join(__dirname, '../public/sitemap.xml'),
  sitemap
);

console.log('✓ サイトマップを生成しました');

このスクリプトは、すべての記事から自動的にサイトマップを生成します。

package.json にスクリプトを追加しましょう。

json{
  "scripts": {
    "dev": "vite",
    "build": "vite build && node scripts/generate-sitemap.js",
    "preview": "vite preview"
  }
}

ビルド時に自動でサイトマップが生成されるようになりました。

RSS フィード生成

scripts​/​generate-rss.js で RSS フィードも生成します。

javascriptimport fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(
  fileURLToPath(import.meta.url)
);
const postsDir = path.join(__dirname, '../src/posts');
const siteUrl = 'https://your-blog.com';
const siteTitle = 'My Tech Blog';
const siteDescription = 'Preact で構築した技術ブログ';

RSS フィードに必要な情報を定義します。

次に、各記事のメタデータを読み込んで RSS を生成しましょう。

javascript// 各記事のメタデータを読み込み
const posts = fs
  .readdirSync(postsDir)
  .filter((file) => file.endsWith('.mdx'))
  .map((file) => {
    const content = fs.readFileSync(
      path.join(postsDir, file),
      'utf-8'
    );
    const { data } = matter(content);
    const slug = file.replace('.mdx', '');
    return { ...data, slug };
  })
  .sort((a, b) => new Date(b.date) - new Date(a.date));

// RSS XML を生成
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>${siteTitle}</title>
    <link>${siteUrl}</link>
    <description>${siteDescription}</description>
    <language>ja</language>
    <atom:link href="${siteUrl}/rss.xml" rel="self" type="application/rss+xml" />
    ${posts
      .map(
        (post) => `
    <item>
      <title>${post.title}</title>
      <link>${siteUrl}/posts/${post.slug}</link>
      <description>${post.description}</description>
      <pubDate>${new Date(
        post.date
      ).toUTCString()}</pubDate>
      <guid>${siteUrl}/posts/${post.slug}</guid>
    </item>`
      )
      .join('')}
  </channel>
</rss>`;

fs.writeFileSync(
  path.join(__dirname, '../public/rss.xml'),
  rss
);

console.log('✓ RSS フィードを生成しました');

gray-matter を使って各記事の Frontmatter を解析し、RSS アイテムを生成しています。

404 ページの実装

src​/​pages​/​NotFound.jsx でエラーページを作成します。

javascriptimport { h } from 'preact';
import { Link } from 'preact-router/match';
import { Helmet } from 'preact-helmet';
import './NotFound.css';

export default function NotFound() {
  return (
    <div className='not-found'>
      <Helmet>
        <title>404 - ページが見つかりません</title>
        <meta name='robots' content='noindex' />
      </Helmet>

      <h1>404</h1>
      <p>お探しのページが見つかりませんでした。</p>
      <Link href='/' className='home-link'>
        ホームに戻る
      </Link>
    </div>
  );
}

SEO を考慮して、404 ページには noindex を設定しています。

スタイリングの実装

src​/​index.css でグローバルスタイルを定義します。

css:root {
  --primary-color: #673ab8;
  --text-color: #333;
  --bg-color: #fff;
  --border-color: #e0e0e0;
  --code-bg: #f5f5f5;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont,
    'Segoe UI', sans-serif;
  color: var(--text-color);
  background-color: var(--bg-color);
  line-height: 1.6;
}

main {
  max-width: 960px;
  margin: 0 auto;
  padding: 2rem 1rem;
  min-height: calc(100vh - 200px);
}

CSS カスタムプロパティを使うことで、テーマカラーを一元管理しています。

記事詳細ページのスタイルも追加しましょう。

css.article {
  max-width: 720px;
  margin: 0 auto;
}

.article-header {
  margin-bottom: 2rem;
  padding-bottom: 1rem;
  border-bottom: 2px solid var(--border-color);
}

.article-header h1 {
  font-size: 2rem;
  margin-bottom: 1rem;
  line-height: 1.3;
}

.article-meta {
  display: flex;
  gap: 1rem;
  align-items: center;
  color: #666;
  font-size: 0.9rem;
}

.article-content {
  font-size: 1.05rem;
}

.article-content h2 {
  margin-top: 2rem;
  margin-bottom: 1rem;
  font-size: 1.5rem;
}

.article-content pre {
  background-color: var(--code-bg);
  padding: 1rem;
  border-radius: 4px;
  overflow-x: auto;
  margin: 1rem 0;
}

.article-content code {
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 0.9em;
}

記事本文の可読性を高めるため、適切な行間とフォントサイズを設定しています。

ビルドと公開

開発サーバーで動作確認を行いましょう。

bashyarn dev

ブラウザで http:​/​​/​localhost:5173 を開くと、ブログサイトが表示されます。

問題がなければ、本番用にビルドします。

bashyarn build

dist ディレクトリに静的ファイルが生成され、サイトマップと RSS フィードも自動生成されます。

Vercel や Netlify などの静的ホスティングサービスにデプロイすれば、すぐに公開できるでしょう。

デプロイ設定(Vercel の例)

Vercel へのデプロイは非常に簡単です。

bash# Vercel CLI をインストール
yarn global add vercel

# プロジェクトルートでデプロイ
vercel

初回デプロイ時にいくつか質問されますが、基本的にデフォルトの設定で問題ありません。

プロジェクトルートに vercel.json を作成して、細かい設定も可能です。

json{
  "buildCommand": "yarn build",
  "outputDirectory": "dist",
  "devCommand": "yarn dev",
  "installCommand": "yarn install",
  "framework": "vite",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

この設定により、SPA ルーティングが正しく動作するようになります。

パフォーマンス最適化

Preact の軽量性を最大限活かすため、いくつかの最適化を行いましょう。

vite.config.js にビルド最適化設定を追加します。

javascriptimport { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import mdx from '@mdx-js/rollup';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';

export default defineConfig({
  plugins: [
    mdx({
      remarkPlugins: [remarkGfm],
      rehypePlugins: [rehypeHighlight],
    }),
    preact(),
  ],
  build: {
    // チャンク分割の最適化
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['preact', 'preact/hooks'],
          router: ['preact-router'],
        },
      },
    },
    // 圧縮設定
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // console.log を削除
      },
    },
  },
});

ベンダーコードを分離することで、キャッシュ効率が向上します。

記事検索機能の追加

ユーザビリティ向上のため、簡易的な記事検索機能を実装しましょう。

src​/​components​/​SearchBox.jsx を作成します。

javascriptimport { h } from 'preact';
import { useState } from 'preact/hooks';
import './SearchBox.css';

export default function SearchBox({ posts, onFilter }) {
  const [query, setQuery] = useState('');

  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value);

    // タイトルまたは説明文で検索
    const filtered = posts.filter(
      (post) =>
        post.title
          .toLowerCase()
          .includes(value.toLowerCase()) ||
        post.description
          .toLowerCase()
          .includes(value.toLowerCase()) ||
        post.tags?.some((tag) =>
          tag.toLowerCase().includes(value.toLowerCase())
        )
    );

    onFilter(filtered);
  };

  return (
    <div className='search-box'>
      <input
        type='search'
        placeholder='記事を検索...'
        value={query}
        onInput={handleSearch}
        className='search-input'
      />
    </div>
  );
}

タイトル、説明文、タグのいずれかに検索キーワードが含まれる記事を抽出します。

src​/​pages​/​Home.jsx に検索ボックスを統合しましょう。

javascriptexport default function Home() {
  const [posts, setPosts] = useState([]);
  const [filteredPosts, setFilteredPosts] = useState([]);

  useEffect(() => {
    getAllPosts().then((data) => {
      setPosts(data);
      setFilteredPosts(data); // 初期表示はすべての記事
    });
  }, []);

  return (
    <div className='home'>
      <Helmet>
        <title>ホーム | My Tech Blog</title>
        <meta
          name='description'
          content='Preact で構築した技術ブログです'
        />
      </Helmet>

      <h1>最新記事</h1>
      <SearchBox
        posts={posts}
        onFilter={setFilteredPosts}
      />

      <div className='articles-grid'>
        {filteredPosts.map((post) => (
          <ArticleCard key={post.slug} post={post} />
        ))}
      </div>
    </div>
  );
}

検索結果に応じて表示される記事が動的に変わります。

全体のワークフロー図解

ここまで実装した機能がどのように連携するか、全体像を確認しましょう。

mermaidsequenceDiagram
    participant User as 読者
    participant Browser as ブラウザ
    participant Router as preact-router
    participant Component as ページコンポーネント
    participant Utils as posts.js
    participant MDX as MDX ファイル

    User->>Browser: URL アクセス
    Browser->>Router: ルーティング処理
    Router->>Component: 該当ページをレンダリング
    Component->>Utils: 記事データ要求
    Utils->>MDX: MDX ファイル読み込み
    MDX-->>Utils: frontmatter + Component
    Utils-->>Component: 記事データ返却
    Component->>Browser: HTML レンダリング
    Browser->>User: ページ表示

この図は、ユーザーのアクセスから記事表示までのデータフローを示しています。

Preact のシンプルなアーキテクチャにより、各レイヤーの責務が明確になっていますね。

まとめ

Preact を活用することで、わずか 1 日で本格的なブログサイトを構築できました。

この記事で実装した主要機能を振り返りましょう。

#機能実装内容
1ルーティングpreact-router で SPA ライクな遷移を実現
2コンテンツ管理MDX で記事を記述し、Frontmatter でメタデータ管理
3SEO 対策preact-helmet でメタタグ、サイトマップ、RSS を自動生成
4スタイリングCSS Modules で保守性の高いスタイル管理
5検索機能クライアントサイドでの簡易記事検索
6パフォーマンスVite + Preact で高速なビルドと軽量な成果物

Preact の軽量性と React 互換性により、学習コストを抑えながら高パフォーマンスなブログを実現できます。さらに拡張するなら、コメント機能やタグページ、ダークモード対応なども検討できるでしょう。

この構成をベースに、あなただけの技術ブログを今日から始めてみてはいかがでしょうか。

関連リンク