playpark
ホーム会社概要サービスソリューションブログお知らせ気軽に相談する
playpark

あらゆる仕事を楽しむ

会社概要サービスソリューション気軽に相談する特定商取引法に基づく表記

© 2019-2026 合同会社playpark All Rights Reserved.

  1. ホーム
  2. ブログ
  3. 技術Tips
  4. 【Next.js 16】MDXブログを自作した話 - ISRと予約投稿で「ちょうどいい」を目指す
ブログ一覧に戻る
技術Tips

【Next.js 16】MDXブログを自作した話 - ISRと予約投稿で「ちょうどいい」を目指す

CMSを使えばいいのに、なぜかMDXでブログを作りたくなる病。ISR、予約投稿、関連記事スコアリングまで実装した記録です。

2026年4月6日16分で読める
Next.jsReactTypeScriptMDXフロントエンド
【Next.js 16】MDXブログを自作した話 - ISRと予約投稿で「ちょうどいい」を目指す

「CMSでいいじゃん」

わかってる。わかってるんです。でも、MDXで作りたくなる瞬間ってありませんか?

playparkのコーポレートサイトでは、結局Next.js 16 + React 19 + MDXでブログシステムを自作しました。車輪の再発明?いいえ、オーダーメイドの車輪です。(強がり)

この記事で学べること

  • MDXファイルベースのコンテンツ管理
  • ISRで「ビルドしなくても更新される」を実現
  • JST対応の予約投稿(日本で使うなら必須)
  • 関連記事のスコアリング(意外と奥が深い)

前提条件

項目バージョン備考
Node.js20.x 以上LTS推奨
Next.js16.xApp Router
React19.xServer Components対応
TypeScript5.x型安全は正義

依存パッケージ:

npm install gray-matter next-mdx-remote
  • gray-matter: frontmatter解析(定番)
  • next-mdx-remote: MDXをサーバーでコンパイル(クライアントに重いパーサー送らなくて済む)

あわせて読みたい

【Next.js App Router】実案件でパフォーマンス85%改善した技術選定
技術Tips26分

【Next.js App Router】実案件でパフォーマンス85%改善した技術選定

複数の業務システム開発を通じて得られた、Next.js App Router選定とパフォーマンス最適化の知見。実際の改善事例とともに解説します。

読む

実装方法

1. ディレクトリ構成

app/
├── blog/
│   ├── [slug]/page.tsx      # 記事詳細
│   ├── page.tsx              # 一覧
│   ├── page/[page]/page.tsx  # ページネーション
│   ├── category/[category]/  # カテゴリ別
│   └── tag/[tag]/            # タグ別

content/
└── blog/
    └── YYYY-MM-DD-slug.mdx   # 記事ファイル

lib/
├── blog.ts                   # データ取得
├── blog-categories.ts        # カテゴリ定義
└── mdx.ts                    # MDX処理

まあ、普通の構成です。奇をてらわない。

2. ファイル名ルール

content/blog/
├── 2026-01-13-first-article.mdx
├── 2026-01-15-second-article.mdx
└── 2026-02-27-this-article.mdx

frontmatter:

---
title: '記事タイトル'
date: 2026-04-06
category: tech-tips
tags:
  - Next.js
description: '説明文'
image: '/blog/2026-02-27-slug.webp'
---

slugはファイル名から自動生成。手動で書くの面倒だし、ミスるし。

// lib/blog.ts
const slug = file.replace(/\.mdx$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '');
// "2026-02-27-nextjs-mdx-blog-system.mdx" → "nextjs-mdx-blog-system"

3. ISRで自動更新

// app/blog/page.tsx
export const revalidate = 3600; // 1時間ごと

export default async function BlogPage() {
  const { posts } = await getPaginatedBlogPosts(1);
  return <BlogList posts={posts} />;
}

なぜ1時間? 予約投稿のため。未来日の記事が公開日時を過ぎたとき、最大1時間で自動表示。

読み込み中...図を読み込み中

「毎秒revalidateすればいいじゃん」→ はい、Vercelの請求書が怖いです。

4. JST対応の予約投稿

日本のサービスなので日本時間で判定。これ、意外と忘れがち。

// lib/blog.ts
function getTodayInJST(): Date {
  const now = new Date();
  const jstOffset = 9 * 60 * 60 * 1000; // UTC+9
  const jstTime = new Date(now.getTime() + jstOffset);
  jstTime.setUTCHours(0, 0, 0, 0);
  return jstTime;
}

function parseBlogPost(file: string, fileContent: string): BlogListItem | null {
  const { data, content } = matter(fileContent);

  // 本番環境では未来日の記事をスキップ
  if (data.date && process.env.NODE_ENV === 'production') {
    const postDate = parseDateAsJST(data.date);
    const today = getTodayInJST();
    if (postDate > today) {
      return null; // まだ早い
    }
  }

  // ...
}

date: 2026-03-01と書けば、3月1日0時(JST)に自動公開。深夜0時に手動公開する必要なし。(そもそも寝てる)

5. 関連記事スコアリング

単純な「同じカテゴリ」だと微妙。スコアリングで関連度を計算。

// lib/blog.ts
export async function getRelatedPosts(
  currentSlug: string,
  category: BlogCategory,
  tags: string[],
  limit: number = 3
): Promise<BlogListItem[]> {
  const allPosts = await getAllBlogPosts();

  const scoredPosts = allPosts
    .filter((post) => post.slug !== currentSlug)
    .map((post) => {
      let score = 0;

      // 同じカテゴリ: +3点
      if (post.category === category) {
        score += 3;
      }

      // タグ一致: 各+2点
      tags.forEach((tag) => {
        if (post.tags.includes(tag)) {
          score += 2;
        }
      });

      return { post, score };
    })
    .filter(({ score }) => score > 0)
    .sort((a, b) => b.score - a.score);

  return scoredPosts.slice(0, limit).map(({ post }) => post);
}

設計: カテゴリ一致(大分類)を重視しつつ、タグ複数マッチでボーナス。

「Next.js」の記事に「Rust」の記事が関連で出てきたら困るでしょ。(出てきたことある)

6. 読了時間(日本語対応)

英語の「200 words/min」をそのまま使うと、日本語記事で読了時間がバグる。

// lib/blog.ts
function calculateReadingTime(content: string): number {
  const charCount = content.length;
  const readingTime = Math.ceil(charCount / 400); // 日本語: 約400字/分
  return Math.max(1, readingTime);
}

400文字/分で計算。英語とは単位が違う。これ、海外のライブラリそのまま使うと事故る。

あわせて読みたい

【React】playpark Lab UI - 泡が出るビーカーを作った話(真面目に)
実験レポート16分

【React】playpark Lab UI - 泡が出るビーカーを作った話(真面目に)

「コーポレートサイト、堅すぎない?」から始まった実験。科学実験室をモチーフにした10種類のUIコンポーネントと、SSR対応アニメーションの設計パターンを公開します。

読む

動作確認

開発環境で確認

npm run dev
  1. http://localhost:3000/blog でブログ一覧が表示される
  2. 記事をクリックして詳細ページが表示される
  3. 関連記事が正しく表示される

予約投稿のテスト

// 開発環境では未来日も表示される(NODE_ENV !== 'production')
// 本番環境でのみ非表示になることを確認

// 方法1: 本番ビルドして確認
npm run build && npm run start

// 方法2: 環境変数で確認
NODE_ENV=production npm run dev

ISRの確認(Vercel)

  1. 記事を更新してデプロイ
  2. 1時間以内にページが更新されることを確認
  3. x-vercel-cache: STALE → HIT の遷移を確認

あわせてチェック

15分で課題を整理しませんか?

記事の内容について質問や相談があれば、営業トークなしの15分ヒアリングで整理します。

15分ヒアリングを予約気軽に相談する

注意点・Tips

タイムゾーンの罠

  • サーバー時刻に依存しない: Vercelのサーバーは UTC。JST変換を忘れると9時間ズレる
  • Date オブジェクトの罠: new Date('2026-03-01') はローカルタイムゾーンで解釈される → 明示的にJST変換

ISRの落とし穴

  • revalidate短すぎ: コスト増加(Vercel課金)
  • revalidate長すぎ: 記事更新が反映されない
  • 1時間がバランス良い(個人の感想)

MDXコンポーネント埋め込み

MDX最大の強み。技術記事で「実際に動くデモ」を埋め込める。

# 記事タイトル

普通のマークダウンです。

<InteractiveDemo />

↑ Reactコンポーネントが動く

ただし、ビルド時に静的化されるのでクライアントにMDXパーサーを送らなくていい。軽い。これ大事。

なぜCMSじゃなくMDXなのか

理由詳細
Git管理git diff で記事の変更履歴が見える。PRで「この表現どうですか」ができる
コンポーネント埋め込みインタラクティブなデモを記事内に配置可能
静的化ビルド時にHTMLになる。パーサー不要で軽量
CMSの管理画面を開くのが面倒本音

まとめ

やったこと感想
MDXベースGitで管理できて最高
ISRビルドしなくて済むの神
予約投稿深夜に起きなくていい
関連記事スコアリング、地味に楽しい

「CMSでいいじゃん」は正論。でも、自分たちのワークフローに最適化されたシステムには独自の価値があります。

...と言いつつ、記事が増えてきたら「やっぱCMS入れるか」ってなるかも。(フラグ)

あわせて読みたい

  • ブログ運用を完全自動化 — GitHubリポジトリから記事・サムネイル・SNS投稿まで — このブログシステムの運用自動化事例

→ 気軽に相談する

この技術が解決した業務課題

記事の技術が実際のプロジェクトでどう活かされているかをご紹介します

【AI採用管理】Gemini API × Next.jsで書類選考を自動化した受託開発事例

Gemini APIとNext.jsで採用書類選考を自動化。中小企業の採用DXをplayparkが受託開発で支援した事例をご紹介します。

事例を読む
About playpark

「あらゆる仕事を楽しむ」をミッションに、業務自動化・AI活用を手がける合同会社です。このブログの運用自体も、自社開発のClaude Code Skillsで完全自動化しています。

会社概要サービス
気軽に相談する
ブログ一覧に戻る

関連記事

すべての記事
【Next.js App Router】実案件でパフォーマンス85%改善した技術選定
技術Tips
2026年1月15日26分で読める
【Next.js App Router】実案件でパフォーマンス85%改善した技術選定

複数の業務システム開発を通じて得られた、Next.js App Router選定とパフォーマンス最適化の知見。実際の改善事例とともに解説します。

Next.jsReactTypeScript+2
【React】playpark Lab UI - 泡が出るビーカーを作った話(真面目に)
実験レポート
2026年4月9日16分で読める
【React】playpark Lab UI - 泡が出るビーカーを作った話(真面目に)

「コーポレートサイト、堅すぎない?」から始まった実験。科学実験室をモチーフにした10種類のUIコンポーネントと、SSR対応アニメーションの設計パターンを公開します。

ReactTypeScriptFramer Motion+3
【Claude Code】受託開発を1週間で完了させた開発プロセスの全貌
実験レポート
2026年4月8日11分で読める
【Claude Code】受託開発を1週間で完了させた開発プロセスの全貌

Claude Codeを活用して採用管理システムの受託開発を1週間で完了。並列開発、コードレビュー、パフォーマンス最適化まで、AI駆動の開発プロセスを実践知見として共有します。

Claude CodeAI受託開発+2

この技術、実際の現場ではこう使われています

記事で紹介した技術が、実際のビジネス課題をどう解決したか。導入事例で具体的なイメージをつかめます。

導入事例を見る気軽に相談する