旧ブログ(ISSEN)から移行しました

【Next.js】TOKOSテックブログをリニューアル!技術構成と工夫を紹介

【Next.js】TOKOSテックブログをリニューアル!技術構成と工夫を紹介

西原月熙
西原月熙13分で読めます
はてなブックマーク

はじめに

こんにちは、株式会社TOKOSのツキヤです。
TOKOSテックブログを、WordPressからNext.jsへフルリニューアルしました🎉

旧ブログはWordPressで運用していましたが、カスタマイズの自由度やパフォーマンス面で課題を感じていました。
「自分たちの技術力を活かして、ゼロから理想のブログを作ろう」ということで、Next.jsでの再構築に踏み切りました💪

本記事では、リニューアル後のテックブログの技術構成や工夫したポイントを紹介します。

技術スタック・インフラ構成

まずは使用している技術の全体像です。

使用技術

カテゴリ技術
フレームワークNext.js 16(App Router)
スタイリングTailwind CSS
記事の管理MDX(next-mdx-remote-client
シンタックスハイライトrehype-pretty-code + Shiki

インフラ・SEO等

カテゴリ技術
ホスティングVercel
ランタイムNode.js 24系
SEO対策JSON-LD、OGP、RSS、サイトマップ
アナリティクスGoogle Analytics(@next/third-parties)
画像最適化AVIF + WebP

主な選定ポイントを紹介します。

Next.js

静的コンテンツ中心のブログサイトであるため、SSG可能なNext.jsを選定しました。
npm run buildでほぼ全てのページが静的生成されるようにしました💪

$ npm run build
 
# ...
 
Route (app)
 /
 /_not-found
 /about
 ƒ /api/og
 /api/search
 /apple-icon.png
 /articles/[slug]
 /articles/tokos-tech-blog-renewal
 /articles/css-text-decoration
 /articles/image-flow-wave
 [+149 more paths]
 /authors
 /authors/[slug]
 /authors/nishihara-tsukiya
 /authors/sugita-yusuke
 /authors/shiomi-naoki
 [+4 more paths]
 /feed.xml
 /news
 /news/[slug]
 /news/2026-03-07
 /news/migration-from-issen
 /pages/[page]
 /pages/1
 /pages/2
 /pages/3
 [+10 more paths]
 /robots.txt
 /sitemap.xml
 /tags/[slug]
 /tags/react
 /tags/nextjs
 /tags/javascript
 [+12 more paths]
 /tags/[slug]/[page]
 /tags/react/1
 /tags/react/2
 /tags/react/3
 [+25 more paths]
 
 
  (Static)   prerendered as static content
  (SSG)      prerendered as static HTML (uses generateStaticParams)
ƒ  (Dynamic)  server-rendered on demand

さらに、可能な限りServer Componentsに寄せ、パフォーマンスを最適化しました。

また、next.config.tsの設定は下記のとおりです。

next.config.ts
import type { NextConfig } from "next"
 
const nextConfig: NextConfig = {
  reactCompiler: true,
  typedRoutes: true,
  experimental: {
    viewTransition: true,
  },
  images: {
    formats: ["image/avif", "image/webp"],
  },
}
 
export default nextConfig

reactCompilerを有効にすることで、useMemomemouseCallbackを手動で書く必要がなくなります。
typedRoutesにより、ルーティングの型安全性を確保しています。
また、viewTransitionでページ遷移時のネイティブなアニメーションを実現しています✨

MDX(ファイルベース)

MicroCMS等のヘッドレスCMSではなく、Gitで記事をバージョン管理する方式を選びました。
これによりランニングコストの削減と、記事もコードと同じようにPRベースでレビューできるようになりました。

ツキヤツキヤ

MDXはMarkdownとReactコンポーネントを組み合わせたファイル形式で、Markdown内にReactコンポーネントを埋め込むことができます🙌

こだわりポイント

全文検索(Fuse.js)

ブログに検索機能は欠かせません。
Algoliaなどの外部サービスを使う選択肢もありましたが、記事数の規模・コスト面を考えるとクライアントサイド検索で十分と判断しました。
そこで、軽量かつ外部依存のないファジー検索(あいまい検索)ライブラリであるFuse.jsを採用しています。

Fuse.js | Fuse.js

Lightweight fuzzy-search library, in JavaScript

www.fusejs.io

実装のポイントは以下のとおりです。

  • ビルド時にインデックスを静的生成: Route Handlerでforce-staticを指定し、ビルド時にJSONを生成
  • 遅延読み込み: 検索モーダルを初めて開いた時に、インデックスとFuse.jsをdynamic importで読み込み
  • Cmd+K / Ctrl+Kショートカット: キーボードだけで素早く検索を開始できる
  • キーボード操作: 矢印キーで結果を選択し、Enterで遷移

検索インデックスのエンドポイントは、たったこれだけです。

src/app/api/search/route.ts
import { NextResponse } from "next/server"
import { getSearchIndex } from "@/lib/content/articles"
 
export const dynamic = "force-static"
 
export const GET = () => {
  const searchIndex = getSearchIndex()
  return NextResponse.json(searchIndex)
}

force-staticを指定することで、ビルド時に静的なJSONファイルとして生成されます。
ユーザーがモーダルを開いた時だけこのJSONを取得し、Fuse.jsで検索する仕組みです。

ツキヤツキヤ

かなり心地良い検索体験を実現できました😌

ダークモード

ダークモードはnext-themesで実装しています。
ライト・ダーク・システムの3モードに対応しています。

GitHub - pacocoursey/next-themes: Perfect Next.js dark mode in 2 lines of code. Support System preference and any other theme with no flashing

Perfect Next.js dark mode in 2 lines of code. Support System preference and any other theme with no flashing - pacocoursey/next-themes

github.com

実装で工夫した点を紹介します。

ちらつき防止のためのCSSベースアイコン切替

テーマの切替ボタンでは、JavaScriptの状態に依存せずCSSのdark:クラスでアイコンを出し分けています。
これにより、ページ読み込み時のアイコンのちらつきを防いでいます。

src/app/_components/ThemeToggle.tsx
{/* CSSでテーマに応じてアイコンを切り替え(ちらつき防止) */}
<Icon name="sun" className="size-6 dark:hidden" />
<Icon name="moon" className="hidden size-6 dark:block" />

disableTransitionOnChangeの活用

ThemeProviderdisableTransitionOnChangeを設定し、テーマ切替時にCSSトランジションを一時無効化しています。
これにより、テーマが切り替わる瞬間の中間状態が見えてしまうちらつきを防止できます。

src/app/_components/ThemeProvider.tsx
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
  {children}
</NextThemesProvider>

ハイドレーションエラー対策

テーマに依存するUI(ドロップダウンメニューなど)は、マウント後にのみ表示する必要があります。
本ブログではReact 18から追加されたuseSyncExternalStoreを使ったuseIsMountedフックでこれを実現しています。

src/lib/hooks/useIsMounted.ts
export const useIsMounted = () => {
  return useSyncExternalStore(
    () => noop,
    () => true, // クライアント: true
    () => false, // サーバー: false
  )
}

useEffect + useStateでも同じことはできますが、useSyncExternalStoreを使うことで初回レンダリングから正しい値が適用され、マウント後の不要なstate更新を避けられます。
このフックでサーバーとクライアントのHTML不一致によるハイドレーションエラーを回避しています。

SEO対策

内部対策

Next.jsのMetadata APIを活用し、SEOの内部対策を網羅的に実装しています。

titleタグ・descriptionタグ

ルートレイアウトで共通のメタデータを定義し、各ページのgenerateMetadataで個別に上書きする構成です。
タイトルにはtemplateを使用し、「記事タイトル | TOKOS Tech Blog」の形式で統一しています。

src/app/layout.tsx(メタデータ抜粋)
export const metadata: Metadata = {
  metadataBase: new URL(SITE_CONFIG.url),
  title: {
    default: SITE_CONFIG.name,
    template: `%s | ${SITE_CONFIG.name}`,
  },
  description: SITE_CONFIG.description,
  alternates: {
    canonical: SITE_CONFIG.url,
    types: {
      "application/rss+xml": `${SITE_CONFIG.url}/feed.xml`,
    },
  },
}

canonical URL

全ページにalternates.canonicalを設定し、URLの正規化を行っています。
記事ページではgenerateMetadata内で動的にcanonical URLを生成しています。

indexの制御

robots.tsでサイト全体のクロール許可を定義しつつ、ページ単位でもrobots: { index: false }で制御できるようにしています。
例えばお知らせページ(/news/)はSEO上のメリットが薄いため、意図的にnoindexとしています。

src/app/robots.ts
export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/api/", "/news/"],
    },
    sitemap: `${SITE_CONFIG.url}/sitemap.xml`,
  }
}

構造化データ(JSON-LD)

schema-dtsライブラリを使い、型安全に構造化データを出力しています。
Organization、WebSite、BlogPosting、BreadcrumbListの4種類を実装しています。

その他の対策

  • OGP / X(Twitter)カード: 全ページに設定し、記事ページではOG画像を動的生成
  • サイトマップ: sitemap.tsで全ページを動的に生成し、優先度と更新頻度を設定
  • RSSフィード: feed.xmlで配信し、alternatesでリンクを公開

Lighthouseオール100点

上記の内部対策とパフォーマンス最適化により、Lighthouseの全4カテゴリで100点を達成できました🎉🎉

主に貢献しているポイントは以下のとおりです。

  • Performance: 全ページSSGによる高速表示、AVIF/WebPによる画像最適化、dynamic importによるバンドル分割
  • Accessibility: セマンティックなHTML、適切なaria属性、十分なコントラスト比
  • Best Practices: HTTPS配信、適切なContent Security設定
  • SEO: メタデータ、canonical、構造化データ、サイトマップの網羅的な実装
ツキヤツキヤ

オール100点は気持ちいいです😎

執筆体験の改善

技術ブログは「書き続けること」が大切です。
そのため、執筆体験の改善にも力を入れています。

再掲ですが、本ブログの記事は全てMDXファイルとしてGitリポジトリで管理しています。
CMSを使わず、コードと同じようにPRベースで執筆・レビューできる方がエンジニアにとって自然なワークフローなはずです。
この「記事もコード」というアプローチを前提に、textlintやバリデーションによる品質チェックを組み込んでいます。

textlintによる日本語チェック

textlintを導入し、日本語の品質を自動でチェックしています。

.textlintrc.json(抜粋)
{
  "rules": {
    "preset-ja-technical-writing": {
      "sentence-length": { "max": 120 },
      "no-mix-dearu-desumasu": {
        "preferInBody": "ですます"
      }
    },
    "preset-ja-spacing": {
      "ja-space-between-half-and-full-width": {
        "space": "never"
      }
    },
    "prh": {
      "rulePaths": ["./prh/tech-terms.yml"]
    }
  }
}

主な機能は以下のとおりです。

  • preset-ja-technical-writing: 一文の長さ制限、「ですます」調の統一、連続する漢字の制限など
  • preset-ja-spacing: 半角と全角の間のスペースルールなど
  • prh(校正辞書): 技術用語の表記統一(例: typescriptTypeScriptnextjsNext.js
ツキヤツキヤ

prhの技術用語統一は、複数人で執筆する場合に揺れを防ぐのに便利です✨

自前のバリデーション

textlintに加えて、独自のバリデーションスクリプトも用意しています。

チェック項目は以下のとおりです。

  • SEO: タイトル(25〜65文字)、description(50〜160文字)、本文(最低1000文字)
  • 見出し構造: H1不使用、H2始まり、階層の飛び越しなし
  • コードブロック: 全てに言語指定があるか
  • 画像: alt属性の有無
  • 内部リンク: InternalLinkの参照先が存在するか

CI/CDパイプライン

PR作成時には、GitHub Actionsで以下のチェックが自動実行されます。

フロントエンドCI(全PR):

  • ESLint
  • ビルド

コンテンツCI(MDX変更時):

  • textlint(日本語チェック)
  • バリデーション(SEO・見出し構造など)

レビュー前に機械的なチェックが完了するため、レビュアーは記事の内容に集中できます💪

AIの活用

本ブログでは、開発から運用までAIを積極的に活用しています。

バイブコーディング

本ブログの実装はClaude Codeをメインに使い、ほぼ全てのコードをAIに生成してもらいました。

ツキヤツキヤ

自分でゴリゴリ書いたコードはほとんどないです😎

AIに一貫したコードを出力してもらうために、プロジェクトルートのCLAUDE.mdにコーディング規約を記載しています。
Claude Codeはこのファイルを自動で読み込み、ルールに従ってコードを生成してくれます。

CLAUDE.md(コーディングルール抜粋)
## コーディングルール
 
- バレルファイルを使用しない
- useMemo, useCallbackを使用しない
- next/linkはprefetchをfalseに設定する
- 関数定義スタイル:
  - コンポーネント: function宣言を使用
  - コンポーネント以外(hooks, ユーティリティ等): アロー関数を使用

このように規約を明文化しておくことで、AIが生成するコードの品質とスタイルが安定します。

記事執筆のSkill

Claude CodeのSkill機能を活用し、記事の執筆フローを仕組み化しています。
Skillとは、特定タスクの手順や仕様をClaude Codeに教えるための仕組みです。

.claude/skills/article-writer/配下に、以下のような構成でスキルを定義しています。

.claude/skills/article-writer/ のディレクトリ構成
.claude/skills/article-writer/
├── SKILL.md                  # 執筆フロー・全体方針
└── references/
    ├── frontmatter.md        # frontmatterの仕様
    ├── mdx-components.md     # MDXコンポーネントの使い方
    ├── code-blocks.md        # コードブロック記法
    ├── writing-style.md      # textlintルール・文体ガイド
    └── review-checklist.md   # レビューチェックリスト

Claude Codeが記事を書く流れは以下のとおりです。

  1. テーマと対象読者の確認
  2. アウトラインの提案(H2/H3構造)
  3. frontmatter.mdを参照してメタデータを作成
  4. writing-style.mdに従い、textlint準拠の日本語で本文を執筆
  5. npm run textlintnpm run validatenpm run buildで検証

references/配下に仕様を定義しておくことで、AIが参照しながら記事を書いてくれます。
フォーマットのブレがなく、検証も一発で通ることが多いです。

ツキヤツキヤ

AIと一緒に記事を書く感覚です✨

自動レビュー

PRを作成するとGitHub Actions上でClaude Codeによる自動レビューが実行されます。
レビューはコードと記事で分けており、それぞれ異なる観点でチェックしています。

  • claude-code-review.yml: .ts.tsx等のコード変更に対し、バグやセキュリティの観点でレビュー
  • claude-article-review.yml: MDXファイルの変更に対し、読みやすさや技術的正確性の観点でレビュー

例えば記事レビューでは、以下のような観点をプロンプトで指定しています。

claude-article-review.yml(プロンプト抜粋)
prompt: |
  このプルリクエストに含まれるMDXファイル(記事)をレビューしてください。
  「読者がこの記事を読んで満足するか」という観点で改善提案をしてください。
 
  ## 記事レビューの重点
  ### 読みやすさ・構成
  - 導入部で記事の目的や対象読者が明確か
  - 見出し構造が論理的で、読者が流れを追いやすいか
  ### 技術的正確性
  - コードサンプルが正しく動作しそうか
  - 古い情報や非推奨の手法がないか

おわりに

WordPressからNext.jsへのリニューアルで、開発・執筆体験が大きく向上しました。
全ページが静的生成(SSG)されているため表示速度は高速で、Turbopackによる開発サーバーの起動も快適です。

最大の成果は、textlintやバリデーション、AIによる開発・執筆フローで「記事を書くハードル」を下げられたことだと感じています。
品質チェックを自動化し、AIにレビューを任せることで、書くことそのものに集中できる環境を作れました。

今後は以下のような改善にも取り組んでいきたいです。

  • 記事へのコメント・リアクション機能
  • 関連記事の自動レコメンド
  • パフォーマンスモニタリングの導入

リニューアルを通じて学んだのは、「AIに任せられる部分はどんどん任せ、人間は判断と品質に集中する」という考え方です。
これからも技術発信を続けていきます💪

この記事を書いた人

西原月熙
西原月熙

TOKOSのテックリード。上流工程からコーディング、インフラ系まで色々やっている器用貧乏です。最近は特にフロントエンドのキャッチアップに力を入れています💪 好きな音楽はボーカロイドです🤖