fix(website): migrate blog from next-mdx-remote to @next/mdx

next-mdx-remote evaluates MDX at runtime with a different React version
than Next.js 16's internal canary, causing build failures. Switch to
@next/mdx which compiles MDX through Next.js's own bundler pipeline.

- Replace next-mdx-remote with @next/mdx, @mdx-js/mdx, @mdx-js/loader
- Use dynamic imports for blog MDX files instead of MDXRemote
- Add remark-frontmatter so compiler skips YAML frontmatter blocks
- Gate cache/CSP headers to production only (immutable dev chunks
  were poisoning browser cache)

Made-with: Cursor
This commit is contained in:
Ayush Rodrigues
2026-04-27 17:24:09 +01:00
parent 26998c88b7
commit cdc1c2bade
5 changed files with 48 additions and 15 deletions

View File

@@ -2,7 +2,6 @@ import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { MDXRemote } from "next-mdx-remote/rsc";
import { mdxComponents } from "@/components/blogComponents";
import { getAllPosts, getPostBySlug } from "@/lib/blogUtils";
import type { BlogParams } from "@/lib/types";
@@ -45,11 +44,23 @@ function formatDate(dateString: string | null) {
});
}
async function loadMdxContent({ slug }: { slug: string }) {
try {
const mod = await import(`@/content/blog/${slug}.mdx`);
return mod.default as React.ComponentType<{ components?: Record<string, React.ComponentType> }>;
} catch {
return null;
}
}
export default async function BlogPostPage({ params }: { params: BlogParams }) {
const { slug } = await params;
const post = getPostBySlug({ slug });
if (!post) notFound();
const Content = await loadMdxContent({ slug });
if (!Content) notFound();
return (
<div className="py-16 md:py-24 bg-[#0F0F0F]">
<div className="max-w-[720px] mx-auto px-4 xl:px-0">
@@ -109,7 +120,7 @@ export default async function BlogPostPage({ params }: { params: BlogParams }) {
<hr className="border-[#292929] mb-12" />
<article className="prose prose-invert prose-lg max-w-none">
<MDXRemote source={post.source} components={mdxComponents} />
<Content components={mdxComponents} />
</article>
</div>
</div>

View File

@@ -0,0 +1,6 @@
import type { MDXComponents } from "mdx/types";
import { mdxComponents } from "@/components/blogComponents";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return { ...mdxComponents, ...components };
}

View File

@@ -1,5 +1,10 @@
import createMDX from "@next/mdx";
const isProd = process.env.NODE_ENV === "production";
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["ts", "tsx", "md", "mdx"],
allowedDevOrigins: ["*.ngrok-free.dev"],
experimental: {
optimizePackageImports: ["framer-motion", "motion", "gsap", "@gsap/react"],
@@ -11,10 +16,9 @@ const nextConfig = {
// formats: ["image/avif", "image/webp"],
},
async headers() {
if (!isProd) return [];
return [
// Next.js chunks include a content hash in their filename, so they are
// safe to cache indefinitely. Vercel sets this automatically on its CDN,
// but the explicit header also covers self-hosted deployments.
{
source: "/_next/static/(.*)",
headers: [
@@ -24,8 +28,6 @@ const nextConfig = {
},
],
},
// Lottie JSON files are large (1.42.6 MB each) and change infrequently.
// A 1-year cache means repeat visitors skip the expensive re-download.
{
source: "/animation/(.*)",
headers: [
@@ -35,8 +37,6 @@ const nextConfig = {
},
],
},
// Public images: 7-day TTL with a 1-day stale-while-revalidate window
// so content updates eventually propagate without blocking visitors.
{
source: "/images/(.*)",
headers: [
@@ -76,4 +76,10 @@ const nextConfig = {
},
};
export default nextConfig;
const withMDX = createMDX({
options: {
remarkPlugins: ["remark-frontmatter"],
},
});
export default withMDX(nextConfig);

View File

@@ -10,6 +10,11 @@
},
"dependencies": {
"@gsap/react": "^2.1.2",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.2.4",
"@types/react": "^19.2.14",
"@vercel/analytics": "^2.0.1",
"framer-motion": "^12.38.0",
"gray-matter": "^4.0.3",
@@ -19,12 +24,11 @@
"matter-js": "^0.20.0",
"motion": "^12.38.0",
"next": "16.2.4",
"next-mdx-remote": "^6.0.0",
"ngrok": "^5.0.0-beta.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-syntax-highlighter": "^16.1.1",
"@types/react": "^19.2.14"
"remark-frontmatter": "^5.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.2",

View File

@@ -107,6 +107,10 @@
"version": "0.1.0",
"dependencies": {
"@gsap/react": "^2.1.2",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.2.4",
"@types/react": "^19.2.14",
"@vercel/analytics": "^2.0.1",
"framer-motion": "^12.38.0",
@@ -117,11 +121,11 @@
"matter-js": "^0.20.0",
"motion": "^12.38.0",
"next": "16.2.4",
"next-mdx-remote": "^6.0.0",
"ngrok": "^5.0.0-beta.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-syntax-highlighter": "^16.1.1",
"remark-frontmatter": "^5.0.0",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.2",
@@ -1248,6 +1252,8 @@
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
"@mdx-js/loader": ["@mdx-js/loader@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
@@ -1304,6 +1310,8 @@
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.1", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA=="],
"@next/mdx": ["@next/mdx@16.2.4", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-e/3bgla+/oF3vDlndI0eFPa0bnP47HPVA0InsAJi7Jr3DwV8WpEGuOcm/3PdI5/93FfNiBhMVeVHZpm1sFlmJw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="],
@@ -4370,8 +4378,6 @@
"next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="],
"next-mdx-remote": ["next-mdx-remote@6.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^4.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ=="],
"next-mdx-remote-client": ["next-mdx-remote-client@1.1.7", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "remark-mdx-remove-esm": "^1.3.1", "serialize-error": "^13.0.1", "vfile": "^6.0.3", "vfile-matter": "^5.0.1" }, "peerDependencies": { "react": ">= 18.3.0 < 19.0.0", "react-dom": ">= 18.3.0 < 19.0.0" } }, "sha512-12Ap5Z/tFIETMXFSBTH2IFEhJAso7MvOJ5ICyesA4q6FM4vtAcmb+4ZKa4tV1IVQJLBVqOhaEfIESZzdwjmrQQ=="],
"ngrok": ["ngrok@5.0.0-beta.2", "", { "dependencies": { "extract-zip": "^2.0.1", "got": "^11.8.5", "lodash.clonedeep": "^4.5.0", "uuid": "^7.0.0 || ^8.0.0", "yaml": "^2.2.2" }, "optionalDependencies": { "hpagent": "^0.1.2" }, "bin": { "ngrok": "bin/ngrok" } }, "sha512-UzsyGiJ4yTTQLCQD11k1DQaMwq2/SsztBg2b34zAqcyjS25qjDpogMKPaCKHwe/APRTHeel3iDXcVctk5CNaCQ=="],