このブログの構成

Kyoichi
78 views
#React#Vite#TypeScript#Architecture#Frontend

モダンなブログサイトの構築

個人のブログ&ポートフォリオサイトを構築するにあたり、最新のフロントエンド技術とスケーラブルなアーキテクチャの採用を目指しました。この記事では、技術選定の過程とアーキテクチャ設計について、実装の詳細とともに解説します。

プロジェクトの要件と課題

個人サイトの構築を始めるにあたり、まず私が直面した課題について説明させていただきます。

最も重要な課題は、コンテンツの管理のしやすさでした。技術ブログと個人ブログという2種類の記事を扱う必要があり、それぞれに適した形式でコンテンツを管理する必要がありました。特に技術記事では、コードブロックや画像の配置が重要になってきます。そのため、Markdownでの執筆が可能な環境を整えることを重視しました。

次に考慮したのが、保守性と拡張性です。個人サイトとはいえ、長期的な運用を見据えた設計が必要です。新しい機能を追加する際に、既存のコードに大きな変更を加えることなく実装できる構造を目指しました。また、コードの再利用性を高め、同じような実装を繰り返すことを避けたいと考えました。

そして最後に、パフォーマンスとユーザー体験の最適化です。ブログサイトである以上、記事の表示速度は重要な要素となります。高速なページ遷移と効率的なビルドプロセスの実現、そしてSEO対策の実装が必要でした。

アーキテクチャ設計

これらの課題を解決するため、クリーンアーキテクチャの考え方を基礎としたアーキテクチャを採用しました。以下が、プロジェクトの基本構造です:

src/
├── components/     # プレゼンテーション層
│   ├── blog/      # ブログ関連のコンポーネント
│   ├── products/  # プロダクト関連のコンポーネント
│   ├── ui/        # 共通UIコンポーネント
│   ├── home/      # ホーム画面特有のコンポーネント
│   └── seo/       # SEO関連のコンポーネント
├── pages/         # ページコンポーネント(ルーティング)
├── hooks/         # カスタムフック
├── lib/          # ビジネスロジック層
│   ├── content/   # コンテンツ管理
│   ├── markdown/  # Markdown処理
│   └── utils/     # ユーティリティ
├── types/        # 型定義
└── content/      # コンテンツ(データ層)
    ├── blog/     # ブログ記事
    ├── tech/     # 技術記事
    └── products/ # プロダクト情報

このアーキテクチャの特徴は、責務の明確な分離にあります。UIの実装を担当するコンポーネント層、ビジネスロジックを扱うロジック層、そしてデータを管理するコンテンツ層が、それぞれ独立して存在しています。

コンポーネント層では、再利用可能な基本的なUIパーツをuiディレクトリに配置し、特定の機能に紐づくコンポーネントは個別のディレクトリで管理しています。これにより、コンポーネントの再利用性が高まり、また機能追加時の影響範囲を最小限に抑えることができます。

また、ブログ、プロダクト、技術記事といった機能ごとに明確にドメインを分離しています。各ドメインに関連するコードを集約することで、機能の追加や変更が他の部分に影響を与えにくい構造を実現しています。

技術スタックの選定

技術スタックの選定にあたっては、開発効率、保守性、パフォーマンスの3つの観点から検討を重ねました。以下では、主要な技術選定について、その理由と実装の詳細を説明します。

Viteによる開発環境の構築

開発環境とビルドツールの選定において、最も重視したのは開発時の体験です。従来のCreate React Appと比較検討した結果、Viteを採用することを決定しました。

Viteの最大の特徴は、開発サーバーの圧倒的な速さです。これは、事前のバンドルを必要とせず、ESモジュールを直接利用するアプローチによって実現されています。実際の開発において、サーバーの起動がほぼ瞬時に行われ、ホットリロードも高速であることを実感しています。

以下が、本プロジェクトのVite設定の核となる部分です:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
    },
  },
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
      },
    },
  },
});

この設定では、パスエイリアスを導入することで、ソースコード内での相対パスの複雑化を防いでいます。また、本番ビルドではRollupを使用して出力を最適化しています。これにより、コードの最小化や不要なコードの除去、アセットの最適化が自動的に行われます。

React + TypeScriptによるUI実装

フロントエンドのメインフレームワークとしてReactを採用し、型システムとしてTypeScriptを導入しました。この組み合わせは、現代のWeb開発において標準的な選択肢となっていますが、本プロジェクトにおいても大きな価値をもたらしています。

特にブログサイトでは、記事データの構造が重要です。TypeScriptを活用することで、以下のようにコンテンツの型を明確に定義することができました:

// src/types/blog.ts
export interface BlogPost {
  title: string;
  date: string;
  status: 'published' | 'draft';
  slug: string;
  content: string;
  thumbnail?: string;
}

export interface TechPost extends BlogPost {
  tags: string[];
}

この型定義により、コンテンツの構造が明確になっただけでなく、必須フィールドの漏れを防ぎ、IDEによる入力補完とエラー検出も活用できるようになりました。

UIコンポーネントの実装においては、再利用性と保守性を重視しました。以下は、サイト全体で使用する基本的なボタンコンポーネントの例です:

// src/components/ui/Button.tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}

export function Button({
  variant = 'primary',
  size = 'md',
  isLoading,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={clsx(
        'rounded-lg transition-colors',
        variantStyles[variant],
        sizeStyles[size],
        isLoading && 'opacity-50 cursor-not-allowed'
      )}
      disabled={isLoading}
      {...props}
    >
      {isLoading ? <LoadingSpinner /> : children}
    </button>
  );
}

このコンポーネントは、プロパティの型安全性を確保しながら、様々な状況で再利用可能な設計となっています。また、ローディング状態の管理やスタイルのバリエーションなど、よく使用される機能をまとめることで、実装の重複を防いでいます。

Tailwind CSSによるスタイリング設計

CSSフレームワークの選定は、開発効率とサイトのパフォーマンスに大きく影響する重要な決定でした。検討の結果、Tailwind CSSを採用することにしました。

従来のコンポーネント指向のCSSフレームワークとは異なり、Tailwind CSSはユーティリティファーストのアプローチを取ります。当初は、HTMLが長くなることやクラス名の管理に対する懸念もありましたが、実際の開発において、その懸念を上回るメリットを感じることができました。

以下は、ブログ記事のカードコンポーネントの実装例です:

// src/components/blog/PostCard.tsx
export function PostCard({ post }: PostCardProps) {
  return (
    <article className="bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow p-6">
      <header className="space-y-2 mb-4">
        <h2 className="text-xl font-semibold text-gray-900 line-clamp-2">
          {post.title}
        </h2>
        <time className="text-sm text-gray-500">
          {formatDate(post.date)}
        </time>
      </header>
      <p className="text-gray-600 line-clamp-3">
        {post.excerpt}
      </p>
    </article>
  );
}

このアプローチの最大の利点は、CSS設計における意思決定の簡素化です。BEMなどの命名規則を考える必要がなく、また、スタイルの影響範囲を常に予測可能な状態に保つことができます。

さらに、プロジェクト全体で一貫したデザインシステムを構築するため、Tailwindの設定をカスタマイズしました:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#f0f9ff',
          600: '#0284c7',
        },
      },
      typography: {
        DEFAULT: {
          css: {
            maxWidth: 'none',
            color: '#374151',
          },
        },
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
  ],
};

このカスタマイズにより、サイト全体で使用するカラースキームやタイポグラフィを統一し、一貫性のあるデザインを実現しています。特に、ブログ記事の本文スタイリングには@tailwindcss/typographyプラグインを活用し、読みやすい文章スタイルを実現しています。

Contentlayerによるコンテンツ管理

ブログサイトにおいて、コンテンツ管理システムの選択は特に重要です。様々なヘッドレスCMSやマークダウンパーサーを検討した結果、Contentlayerを採用することにしました。

Contentlayerの特筆すべき点は、マークダウンファイルから型安全なデータを生成できることです。以下が、コンテンツ型の定義例です:

// contentlayer.config.ts
import { defineDocumentType, makeSource } from '@contentlayer/source-files';

export const Blog = defineDocumentType(() => ({
  name: 'Blog',
  filePathPattern: 'blog/**/*.md',
  fields: {
    title: { type: 'string', required: true },
    date: { type: 'date', required: true },
    status: {
      type: 'enum',
      options: ['published', 'draft'],
      required: true
    },
    tags: { type: 'list', of: { type: 'string' }, required: false }
  },
  computedFields: {
    slug: {
      type: 'string',
      resolve: (doc) => doc._raw.sourceFileName.replace(/\.md$/, '')
    },
    url: {
      type: 'string',
      resolve: (doc) => `/blog/${doc._raw.sourceFileName.replace(/\.md$/, '')}`
    }
  }
}));

この設定により、マークダウンファイルのフロントマターが自動的にバリデーションされ、型安全な形でコンテンツを管理できるようになりました。また、コンテンツの取得と処理も簡潔に実装することができます:

// src/lib/content.ts
import { allBlogs } from 'contentlayer/generated';

export function getAllPublishedPosts() {
  return allBlogs
    .filter(post => post.status === 'published')
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}

export function getPostsByTag(tag: string) {
  return getAllPublishedPosts()
    .filter(post => post.tags?.includes(tag));
}

この実装により、記事の公開状態の管理やタグによるフィルタリングなどが、型安全な形で実現できています。また、ビルド時に全てのコンテンツが検証されるため、本番環境でのコンテンツの不整合を防ぐことができます。

SEO対策の実装

ブログサイトにとって、検索エンジン最適化は非常に重要な要素です。本サイトでは、React Helmet Asyncを活用して、体系的なSEO対策を実装しました。

まず、全てのページで共通して使用するSEOコンポーネントを作成しました:

// src/components/seo/Head.tsx
export function Head({
  title,
  description,
  type = 'website',
  image,
  noindex = false,
}: HeadProps) {
  const fullTitle = title ? `${title} | たにぐち きょういち` : 'たにぐち きょういち';

  return (
    <Helmet>
      <title>{fullTitle}</title>
      <meta name="description" content={description} />
      <meta property="og:title" content={fullTitle} />
      <meta property="og:description" content={description} />
      <meta property="og:type" content={type} />
      {image && <meta property="og:image" content={image} />}
      
      <link rel="canonical" href={`https://taniguchi-kyoichi.com${path}`} />
      {noindex && <meta name="robots" content="noindex,nofollow" />}
    </Helmet>
  );
}

さらに、構造化データ(JSON-LD)を実装することで、検索結果での表示を最適化しています:

// src/components/seo/JsonLd.tsx
export function JsonLd({ type, data }: JsonLdProps) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': type,
    ...data,
  };

  return (
    <Helmet>
      <script type="application/ld+json">
        {JSON.stringify(jsonLd)}
      </script>
    </Helmet>
  );
}

これらのコンポーネントを各ページで適切に使用することで、以下のようなSEO対策を実現しています:

  • 適切なタイトルとメタディスクリプションの設定
  • OGPによるSNSでのシェア最適化
  • 構造化データによる検索結果の強化
  • canonicalタグによる重複コンテンツの防止

まとめ

このブログサイトの構築を通じて、モダンなWebフロントエンド開発の実践的な知見を得ることができました。特に以下の点で、当初の目標を達成できたと考えています:

  1. 開発効率の向上
    Vite、TypeScript、Tailwind CSSの組み合わせにより、効率的な開発フローを確立できました。特にViteの高速な開発サーバーとホットリロードは、開発体験を大きく向上させています。

  2. 保守性の確保
    クリーンアーキテクチャの採用とTypeScriptによる型安全性の確保により、将来の機能追加や修正に強い構造を実現できました。

  3. コンテンツ管理の最適化
    Contentlayerを活用することで、型安全なコンテンツ管理システムを構築。執筆からデプロイまでのワークフローを効率化できました。

今後の展望として、以下の改善を検討しています:

  • 記事検索機能の実装
  • コメント機能の追加
  • パフォーマンスのさらなる最適化
  • アクセシビリティの向上

技術の進化は常に続いているため、これらの改善を進めながら、新しい技術やベストプラクティスも積極的に取り入れていきたいと考えています。

このブログの構成 | Tech | 谷口恭一