Next.jsでDev.toライクな3カラムレイアウトを実装する

ブログサイトのレイアウトを改善する際、[Dev.to](https://dev.to/)のような3カラム構成は非常に効果的な選択肢の一つです。この記事では、Next.jsとTailwind CSSを用いてDev.toライクな3カラムレイアウトを実装する方法について、実践的な知見を共有します。

# はじめに

技術ブログを運営していると、以下のような課題に直面することが多いのではないでしょうか：

- 見出しや関連情報へのジャンプが分かりにくい
- デスクトップ画面での余白が効果的に活用できていない
- タグ一覧や執筆者情報などの追加情報を表示するスペースが不足している
- レスポンシブ対応が不完全で、モバイル表示時に使いづらい

これらの課題を解決するため、Dev.toで採用されているような3カラムレイアウトの実装方法について解説します。

# 前提条件

この記事で説明する実装には、以下の技術スタックを使用します：

- Next.js（App Router）
- TypeScript
- Tailwind CSS
- React 18以上

# Dev.toスタイルの3カラムレイアウトとは

Dev.toスタイルの3カラムレイアウトは、以下のような構成を特徴としています：

- 左サイドバー（240px）
  - サイト全体のナビゲーション
  - シリーズ記事リンク
  - タグ一覧
  - 著者情報
- メインコンテンツ（可変幅）
  - 記事本文
  - 広いスペースでの快適な読書体験
- 右サイドバー（240px）
  - シェアボタン
  - 目次
  - 関連記事
  - モバイル時はスライドアウトメニュー化

この構成により、技術系ブログで多くの読者が慣れ親しんだUIを提供しながら、ナビゲーション性とスペース活用を向上させることができます。

# 実装のポイント

## 1. 基本的なレイアウト構造

まず、3カラムレイアウトの基本構造を実装します。Tailwind CSSのユーティリティクラスを活用することで、簡潔なコードでレスポンシブ対応を実現できます。

```tsx
// components/layouts/BlogLayout.tsx
import { ReactNode } from 'react'
import LeftSidebar from './LeftSidebar'
import RightSidebar from './RightSidebar'

type Props = {
  children: ReactNode
}

export default function BlogLayout({ children }: Props) {
  return (
    <div className="relative min-h-screen lg:flex">
      {/* 左サイドバー */}
      <aside className="fixed inset-y-0 left-0 hidden w-60 overflow-y-auto border-r border-gray-200 lg:block">
        <LeftSidebar />
      </aside>

      {/* メインコンテンツ */}
      <main className="mx-auto w-full max-w-3xl px-4 py-8 lg:ml-60 xl:px-0">
        {children}
      </main>

      {/* 右サイドバー */}
      <aside className="fixed inset-y-0 right-0 hidden w-60 overflow-y-auto border-l border-gray-200 xl:block">
        <RightSidebar />
      </aside>
    </div>
  )
}
```

ここでのポイントは以下の通りです：

- `lg:flex`を使用して、大画面時のみフレックスボックスレイアウトを適用
- サイドバーは`w-60`（240px）で固定幅を設定
- メインコンテンツは`max-w-3xl`で最大幅を制限
- 左サイドバーは`lg:block`で、右サイドバーは`xl:block`でブレークポイントを分けて表示

## 2. モバイル対応の実装

モバイル対応では、サイドバーをドロワーメニューとして実装します。これにより、限られた画面スペースを効率的に活用できます。

```tsx
// components/layouts/MobileSidebar.tsx
import { useState } from 'react'

export default function MobileSidebar() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <>
      {/* ハンバーガーメニューボタン */}
      <button
        onClick={() => setIsOpen(true)}
        className="fixed left-4 top-4 z-50 block lg:hidden"
        aria-label="メニューを開く"
        aria-expanded={isOpen}
        aria-controls="mobile-menu"
      >
        <svg className="h-6 w-6" /* ... */ />
      </button>

      {/* オーバーレイ */}
      {isOpen && (
        <div
          className="fixed inset-0 z-40 bg-black bg-opacity-50 lg:hidden"
          onClick={() => setIsOpen(false)}
        />
      )}

      {/* ドロワーメニュー */}
      <div
        id="mobile-menu"
        className={`fixed inset-y-0 left-0 z-50 w-60 transform overflow-y-auto bg-white transition duration-300 ease-in-out lg:hidden ${
          isOpen ? 'translate-x-0' : '-translate-x-full'
        }`}
      >
        <LeftSidebar />
      </div>
    </>
  )
}
```

実装のポイント：

- `useState`でドロワーの開閉状態を管理
- `transform`と`translate`でスライドアニメーションを実現
- 背景のオーバーレイで操作を制限
- アクセシビリティに配慮したARIA属性の設定

## 3. シェアボタンの実装

モバイルとデスクトップで異なるシェア機能を提供するため、Web Share APIを活用します。

```tsx
// components/ShareButton.tsx
'use client'

import { useEffect, useState } from 'react'

export default function ShareButton({ url, title }: { url: string; title: string }) {
  const [canShare, setCanShare] = useState(false)

  useEffect(() => {
    setCanShare(!!navigator.share)
  }, [])

  const handleShare = async () => {
    try {
      if (navigator.share) {
        await navigator.share({
          title,
          url,
        })
      } else {
        // フォールバック：URLをクリップボードにコピー
        await navigator.clipboard.writeText(url)
        alert('URLをコピーしました')
      }
    } catch (error) {
      console.error('シェアに失敗しました:', error)
    }
  }

  return (
    <button
      onClick={handleShare}
      className="inline-flex items-center rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
    >
      <svg className="mr-2 h-5 w-5" /* ... */ />
      {canShare ? 'シェア' : 'URLをコピー'}
    </button>
  )
}
```

実装のポイント：

- `use client`ディレクティブでクライアントコンポーネントとして宣言
- `navigator.share`の存在チェックでWeb Share APIの対応を判定
- フォールバックとしてクリップボードコピー機能を実装

# 実装時の注意点

## 1. ハイドレーションエラーへの対策

Next.jsでSSRを使用する場合、ブラウザ固有のAPIを直接参照するとハイドレーションエラーが発生する可能性があります。これを防ぐため：

- クライアントサイド機能は`use client`ディレクティブを使用して分離
- `useEffect`内でブラウザAPIの参照を行う
- SSR時とCSR時で表示が異なる部分は適切にローディング状態を表示

## 2. アクセシビリティへの配慮

3カラムレイアウトを実装する際は、以下のアクセシビリティ対応が重要です：

- 適切なARIA属性の設定（`aria-label`、`aria-expanded`、`aria-controls`）
- キーボード操作のサポート
- スクリーンリーダー対応の階層構造
- 十分なコントラスト比の確保

## 3. パフォーマンスの最適化

レイアウトのパフォーマンスを最適化するためのポイント：

- サーバーコンポーネントとクライアントコンポーネントの適切な分離
- 画像の最適化（`next/image`の活用）
- 不要なリレンダリングの防止
- Tailwind CSSのパージによる最適化

# コード公開

この記事で紹介した実装のサンプルコードは、以下のGitHubリポジトリで公開しています：

[yonaka15/tech-jugoya-ai](https://github.com/yonaka15/tech-jugoya-ai)

実装時の参考にしていただければ幸いです。

# 参考資料

この記事は以下の記事を参考に、Qiita向けに再構成したものです：

- [Dev.toスタイルのブログ記事レイアウトを導入してみた | tech.jugoya.ai](https://tech.jugoya.ai/blog/2025-01-13-adopt-devto-style-layout)

# まとめ

Dev.toスタイルの3カラムレイアウトは、技術ブログのUI/UX改善に非常に効果的です。主なメリットは：

- ナビゲーション性の向上
- 画面スペースの効率的な活用
- モバイルフレンドリーな対応
- 拡張性の高い設計

実装時は、レスポンシブデザイン、ハイドレーションエラー、アクセシビリティなど、考慮すべき要素が多くありますが、本記事で紹介した方法を参考に、段階的に改善を進めることをお勧めします。

また、このレイアウト構造は将来的な機能追加（シリーズ記事の目次連動やブックマーク機能など）にも対応しやすい設計となっています。

Next.jsとTailwind CSSを使用した実装例を通じて、読者の皆さんがより良い技術ブログの UI/UX を実現する一助となれば幸いです。