ゆうふの研究室
Web制作更新日 2026.09.23

Cloudflare WorkersでMDX記事が表示されなかった原因と解決方法

Next.jsで制作したブログをCloudflare Workersへ公開した際に、MDXの記事一覧が空になり、個別記事が404になった問題の原因と解決方法をまとめます。

はじめに

「ゆうふの研究室」では、ブログの記事をMDXファイルで管理しています。

ローカル環境では記事一覧と個別記事のどちらも正常に表示できていましたが、Cloudflare Workersへデプロイすると、記事が表示されなくなる問題が発生しました。

具体的には、次のような状態です。

  • 記事一覧ページに記事が1件も表示されない
  • GitHubにはMDXファイルが存在している
  • 個別記事を開くと404になる
  • ローカル環境では正常に表示される
  • ビルド自体はエラーにならない

この記事では、問題が発生した構成、原因を調べた方法、最終的に採用した解決方法を記録します。

使用している構成

「ゆうふの研究室」では、主に次の技術を使用しています。

  • Next.js
  • TypeScript
  • MDX
  • Tailwind CSS
  • GitHub
  • Cloudflare Workers
  • OpenNext

記事のMDXファイルは、次のような場所に保存しています。

content/
└─ posts/
   ├─ self-introduction.mdx
   ├─ building-yufu-lab-blog.mdx
   └─ cloudflare-workers-mdx-not-found.mdx

ファイル名から拡張子を取り除いたものを、記事のslugとして使用します。

例えば、次のファイルの場合、

self-introduction.mdx

記事のURLは次のようになります。

https://yufu-lab.com/articles/self-introduction

最初に採用していた読み込み方法

最初は、Next.jsのサーバー側でNode.jsのファイルシステムを使用し、content/postsにあるMDXファイルを直接読み込んでいました。

import fs from "node:fs";
import path from "node:path";

const postsDirectory = path.join(process.cwd(), "content", "posts");

記事一覧を取得するときは、readdirSyncでディレクトリ内のファイルを取得します。

const fileNames = fs
  .readdirSync(postsDirectory)
  .filter((fileName) => /\.mdx?$/.test(fileName));

それぞれのファイルをreadFileSyncで読み込み、frontmatterと本文へ分割する構成でした。

const fullPath = path.join(postsDirectory, fileName);
const fileContent = fs.readFileSync(fullPath, "utf8");

ローカル環境ではcontent/postsがパソコン上に存在するため、この方法で問題なく記事を取得できていました。

Cloudflareへ公開すると記事が消えた

GitHubへ変更をpushし、Cloudflare Workersへのデプロイも正常に完了しました。

しかし、公開されたサイトの記事一覧を確認すると、MDXファイルを追加しているにもかかわらず、記事が1件も表示されませんでした。

記事一覧で使用している処理は、次のような単純なものでした。

const posts = getAllPosts();

ローカル環境ではpostsに記事が入りますが、公開環境では空の配列になっていました。

さらに、記事一覧に一時的に表示できるようになった後も、次のURLを開くと404が表示されました。

/articles/self-introduction

GitHubにはMDXファイルがあり、ビルドログにも記事のルートが表示されていたため、最初はリンクやslugの生成方法に問題があると考えていました。

原因

原因は、公開後のCloudflare Workersから、ローカル環境と同じ方法でMDXファイルを読み込もうとしていたことでした。

開発中のパソコンには、次のディレクトリが実際に存在します。

content/posts

一方、Cloudflare Workersでは、アプリケーションがWorker用の成果物としてまとめられて実行されます。

そのため、公開後のリクエスト処理でnode:fsを使用しても、開発環境と同じディレクトリ構成やMDXファイルを参照できるとは限りません。

この違いによって、公開環境では記事ファイルを取得できず、結果として記事一覧が空になっていました。

個別記事についても、slugに対応する記事データが見つからないため、Next.jsのnotFound()が実行されて404になっていました。

解決方法

この問題は、MDXファイルを公開後に読み込むのではなく、ビルド前にJSONへ変換する方法で解決しました。

処理の流れは次のとおりです。

content/postsのMDXファイル
        ↓
ビルド前に変換スクリプトを実行
        ↓
posts.generated.jsonを生成
        ↓
Next.jsからJSONを静的に読み込む
        ↓
Cloudflare Workersへデプロイ

JSONファイルはNext.jsのビルド成果物に含められるため、公開後にファイルシステムへアクセスする必要がありません。

MDXからJSONを生成する

まず、MDXファイルを読み込んでJSONへ変換するスクリプトを用意しました。

scripts/generate-posts.mjs

このスクリプトでは、次の情報をMDXファイルから取得します。

  • slug
  • title
  • description
  • date
  • updatedAt
  • category
  • tags
  • thumbnail
  • published
  • content

生成先は次のファイルです。

content/posts.generated.json

生成されるデータは、次のような形式になります。

[
  {
    "slug": "self-introduction",
    "title": "はじめまして、ゆうふです",
    "description": "ゆうふの研究室を始めた理由や、制作しているものを紹介します。",
    "date": "2026-09-21",
    "updatedAt": "2026-09-21",
    "category": "このブログ",
    "tags": ["自己紹介", "個人開発"],
    "published": true,
    "content": "記事の本文"
  }
]

ビルド前に変換処理を実行する

記事を追加するたびに手動でJSONを更新すると、変換を忘れる可能性があります。

そこで、package.jsonのprebuildを使用して、ビルド前に記事データを生成するようにしました。

{
  "scripts": {
    "generate:posts": "node scripts/generate-posts.mjs",
    "prebuild": "npm run generate:posts",
    "build": "next build"
  }
}

npm run buildを実行すると、先にprebuildが呼び出されます。

実際の流れは次のようになります。

npm run generate:posts
        ↓
content/posts.generated.jsonを生成
        ↓
next build

これにより、GitHubへ新しいMDXファイルを追加してデプロイすると、最新の記事データを含んだJSONが自動的に作られます。

Next.jsからJSONを読み込む

記事を取得するlib/posts.tsでは、ファイルシステムを使用せず、生成済みのJSONを直接importします。

import generatedPosts from "../content/posts.generated.json";

export type Post = {
  slug: string;
  title: string;
  description: string;
  date: string;
  updatedAt?: string;
  category: string;
  tags: string[];
  thumbnail?: string;
  published: boolean;
  content: string;
};

const posts = generatedPosts as Post[];

公開中の記事を取得する関数は、次のようにしています。

export function getAllPosts({ includeDrafts = false } = {}) {
  return posts
    .filter((post) => includeDrafts || post.published)
    .sort(
      (a, b) =>
        new Date(b.date).getTime() - new Date(a.date).getTime(),
    );
}

個別記事はslugを使って取得します。

export function getPostBySlug(slug: string) {
  return getAllPosts({ includeDrafts: true }).find(
    (post) => post.slug === slug && post.published,
  );
}

これで、Cloudflare Workers上でも記事一覧のデータを取得できるようになりました。

個別記事の404を修正する

記事一覧が表示されるようになっても、個別記事だけ404になる問題が残りました。

個別記事では、URLから受け取ったslugを使って記事を検索します。

type ArticlePageProps = {
  params: Promise<{ slug: string }>;
};

ページ内では、次のようにslugを取得します。

export default async function ArticlePage({
  params,
}: ArticlePageProps) {
  const { slug } = await params;
  const post = getPostBySlug(slug);

  if (!post) {
    notFound();
  }

  return (
    <main>
      <h1>{post.title}</h1>
      <MarkdownContent content={post.content} />
    </main>
  );
}

今回は、記事データを生成済みJSONへ含めたうえで、個別記事のルートを動的に処理する構成に変更しました。

export const dynamic = "force-dynamic";

記事ページの主な構成は次のとおりです。

import { notFound } from "next/navigation";
import { MarkdownContent } from "../../components/MarkdownContent";
import { getPostBySlug } from "../../../lib/posts";

type ArticlePageProps = {
  params: Promise<{ slug: string }>;
};

export const dynamic = "force-dynamic";

export default async function ArticlePage({
  params,
}: ArticlePageProps) {
  const { slug } = await params;
  const post = getPostBySlug(slug);

  if (!post) {
    notFound();
  }

  return (
    <main>
      <article>
        <h1>{post.title}</h1>
        <p>{post.description}</p>

        <MarkdownContent content={post.content} />
      </article>
    </main>
  );
}

この変更によって、記事一覧から個別記事を開けるようになりました。

確認したポイント

修正後は、ローカル環境だけでなく公開環境でも次の内容を確認しました。

  • 記事一覧に公開済みの記事が表示される
  • 新しい順に記事が並ぶ
  • 下書きの記事は表示されない
  • カテゴリーで絞り込める
  • タグで絞り込める
  • 記事タイトルから個別記事を開ける
  • URLへ直接アクセスしても404にならない
  • 本文がMarkdownとして表示される
  • Cloudflareへの再デプロイ後も記事が残る

特に、記事一覧が表示されることと、個別記事を直接URLから開けることは別々に確認しました。

一覧ページだけ正常でも、動的ルートの設定によっては個別記事が404になる場合があるためです。

今回学んだこと

今回の問題を通して、ローカル環境で動作しているだけでは、本番環境でも同じように動作するとは限らないことを改めて学びました。

特に、ファイルシステムへアクセスする処理は、実行する環境の影響を受けます。

Next.jsをCloudflare Workersへデプロイする場合は、公開後に元のMDXファイルを探すのではなく、ビルド時に必要なデータへ変換して成果物に含める方法が扱いやすいと感じました。

また、問題を調べるときは、次の順番で確認すると原因を切り分けやすくなります。

  1. GitHubに対象ファイルが存在するか
  2. ビルド時に対象ファイルを取得できているか
  3. 生成したJSONに記事が含まれているか
  4. 公開環境で記事一覧のデータを取得できているか
  5. 個別記事のslugが一致しているか
  6. Cloudflare Workersのログにエラーがないか

最初からすべての原因を推測するのではなく、記事データがどの段階まで届いているかを順番に確認することが大切でした。

まとめ

今回発生した問題は、Cloudflare Workersの公開環境から、ローカルと同じようにMDXファイルを直接読み込もうとしたことが原因でした。

最終的には、次の対応で解決しました。

  • MDXをビルド前にJSONへ変換する
  • prebuildで変換処理を自動化する
  • 生成したJSONをNext.jsから静的にimportする
  • slugを使ってJSON内の記事を取得する
  • 個別記事のルートを公開環境で動作する構成へ変更する

MDXで記事を書ける手軽さを残しながら、Cloudflare Workersでも記事一覧と個別記事を正常に表示できるようになりました。

今後も、サイト制作中に発生した問題や、その解決方法を「ゆうふの研究室」に記録していきます。