Making Dynamic MDX Blogs Work with OpenNext on Cloudflare Workers Problem where a Next.js MDX blog works locally but displays empty pages when deployed to Cloudflare Workers using OpenNext, due to the incompatibility of runtime filesystem reads (`node:fs`) with the Worker's bundled environment. The solution involves moving blog file discovery and metadata parsing from runtime to build time, generating static TypeScript files and import statements that the Worker can use without accessing the filesystem during requests. This approach maintains the file-based authoring workflow while ensuring reliable deployment on Cloudflare Workers. I ran into a small but annoying problem while deploying a Next.js MDX blog to Cloudflare Workers with OpenNext. The blog worked locally. The build passed. OpenNext even listed the blog routes during the build. Then I opened the deployed site, and the blog page was empty. The issue was not MDX. It was not frontmatter. It was not a missing route. The real problem was that my blog code was still thinking like a normal Node.js app, while Cloudflare Workers runs from a bundled Worker output. The Short Version If your MDX blog works in next dev but shows empty pages or missing posts on Cloudflare Workers, check if you read blog files with node:fs at request time. This is the safer pattern: - Keep writing posts as .mdx files. - Parse the files during the build. - Generate a small TypeScript file with post metadata. - Generate another TypeScript file that statically imports every MDX post. - Render posts from that generated registry in production. That way the Worker does not need to scan your content/blog folder at runtime. What Broke The old setup looked something like this: python import fs from "node:fs"; import path from "node:path"; import matter from "gray-matter"; const postsDir = path.join process.cwd , "content", "blog" ; export function getAllPosts { return fs.readdirSync postsDir .map file = { const raw = fs.readFileSync path.join postsDir, file , "utf8" ; const { data } = matter raw ; return { slug: file.replace /\.mdx$/, "" , title: data.title, description: data.description, }; } ; } This feels fine in local development because the source files are right there on disk. But after OpenNext builds the app for Cloudflare Workers, the app is running from a Worker bundle. Cloudflare does support node:fs through a virtual filesystem, but that filesystem is not the same as having your project folder mounted in production. Files inside the Worker bundle are readable under /bundle , and /tmp is temporary for a request. So I stopped treating the source folder as runtime data. The Fix I moved blog discovery to build time. The build step reads the MDX files once, parses the frontmatter, and writes generated TypeScript files that the app can import normally. The mental model is simple: Authoring: content/blog/ .mdx Build: read MDX files parse frontmatter generate metadata generate static imports Runtime: import generated modules render the matching MDX component This keeps the nice file-based writing flow, but removes runtime filesystem reads from the deployed Worker. Step 1: Generate Blog Metadata I use a script before the build. It reads the .mdx files and creates metadata for the blog index, sitemap, RSS feed, and page metadata. python // scripts/generate-blog-data.mjs import fs from "node:fs"; import path from "node:path"; import matter from "gray-matter"; const root = process.cwd ; const postsDir = path.join root, "content", "blog" ; const outputDir = path.join root, "src", "lib", "blog" ; const files = fs.readdirSync postsDir .filter file = file.endsWith ".mdx" ; const posts = files.map file = { const slug = file.replace /\.mdx$/, "" ; const raw = fs.readFileSync path.join postsDir, file , "utf8" ; const { data, content } = matter raw ; const words = content.trim .split /\s+/ .filter Boolean .length; return { slug, title: data.title, description: data.description, date: data.date, readingTime: ${Math.max 1, Math.ceil words / 225 } min read , }; } ; fs.mkdirSync outputDir, { recursive: true } ; fs.writeFileSync path.join outputDir, "generated-posts.ts" , export const allBlogPosts = ${JSON.stringify posts, null, 2 } as const;\n ; The important part is not the exact script. The important part is when it runs. It runs before next build , not when someone opens /blog . Step 2: Generate Static MDX Imports The next file is the one that makes the Worker build reliable. Instead of doing this: await import ../../../content/blog/${slug}.mdx ; I generate static imports: python // src/lib/blog/generated-components.ts import type { ComponentType } from "react"; import Post0 from "../../../content/blog/first-post.mdx"; import Post1 from "../../../content/blog/second-post.mdx"; const blogPostComponents: Record