Resolviendo el error de Turbopack con MDX en NextJS 16
guia completa para solucionar el error client-only cannot be imported from a Server Component cuando usas Turbopack y MDX en NextJS 16.
Resolviendo el error de Turbopack con MDX en NextJS 16
Si estas usando NextJS 16 con MDX para crear contenido (como documentacion o un blog), probablemente te hayas encontrado con el error de Turbopack con MDX cuando intentas correr tu proyecto:
Error: 'client-only' cannot be imported from a Server Component module.Este articulo cubre que esta pasando, por que ocurre y como solucionarlo definitivamente.
El contexto: Turbopack es el default en NextJS 16
Turbopack es el bundler oficial de NextJS, disenado para ser mas rapido que Webpack. A partir de NextJS 16, Turbopack es el bundler por defecto tanto para desarrollo como para builds de produccion:
// package.json - Turbopack se usa automaticamente
{
"scripts": {
"dev": "next dev",
"build": "next build"
}
}Turbopack en NextJS 16
A diferencia de versiones anteriores donde Turbopack era opcional, en NextJS 16 es el bundler por defecto. Ya no necesitas la flag --turbopack -- se activa automaticamente.
El problema: @next/mdx + Turbopack = Error
Cuando intentas usar @next/mdx con Turbopack, ves este error:
Error: loader C:\...\node_modules\@next\mdx\mdx-js-loader.js
for match "#next-mdx" does not have serializable options.
Ensure that options passed are plain JavaScript objects and values.O este otro:
Error: 'client-only' cannot be imported from a Server Component module.
It should only be used from a Client Component.¿Por que pasa esto?
El problema es que Turbopack no es compatible con el loader de @next/mdx. El paquete @next/mdx usa un sistema de loaders de Webpack que Turbopack no puede procesar:
- MDX necesita transformar archivos
.mdxa JavaScript - El loader de
@next/mdxusa configuraciones complejas con plugins de remark/rehype - Turbopack no puede serializar estas configuraciones
- Resultado: el build falla
La solucion recomendada: Migrar a next-mdx-remote
La solucion definitiva es reemplazar @next/mdx con next-mdx-remote. Esta libreria no depende del loader de Webpack, procesa MDX en runtime y es totalmente compatible con Turbopack.
Instala next-mdx-remote y desinstala @next/mdx
npm install next-mdx-remote gray-matter
npm uninstall @next/mdx @mdx-js/loader @mdx-js/reactSimplifica tu next.config.ts
Elimina la configuracion de MDX del config:
// next.config.ts - Limpio, sin createMDX
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx'],
};
export default nextConfig;Mueve tu contenido MDX a una carpeta content/
En lugar de usar archivos page.mdx dentro de app/, guarda tus archivos MDX con frontmatter YAML en content/blog/:
content/
└── blog/
├── mi-primer-post.mdx
└── segundo-post.mdx---
title: "Mi primer post"
description: "Un post de ejemplo"
slug: "mi-primer-post"
---
# Mi primer post
Contenido del post con **Markdown** y componentes.Crea una funcion para leer el contenido
// lib/content.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const CONTENT_DIR = path.join(process.cwd(), "content", "blog");
export function getPostBySlug(slug: string) {
const filePath = path.join(CONTENT_DIR, `${slug}.mdx`);
const fileContent = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(fileContent);
return {
frontmatter: data,
content,
};
}
export function getAllPosts() {
const files = fs.readdirSync(CONTENT_DIR)
.filter((f) => f.endsWith(".mdx"));
return files.map((file) => {
const slug = file.replace(".mdx", "");
return getPostBySlug(slug);
});
}Renderiza con MDXRemote en tu page.tsx
// app/blog/[slug]/page.tsx
import { MDXRemote } from "next-mdx-remote/rsc";
import { getPostBySlug, getAllPosts } from "@/lib/content";
export function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({
slug: post.frontmatter.slug,
}));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const { frontmatter, content } = getPostBySlug(slug);
return (
<article>
<h1>{frontmatter.title}</h1>
<MDXRemote source={content} />
</article>
);
}Con next-mdx-remote, tus archivos MDX compilan sin errores con Turbopack. Ademas, ganas control total sobre el pipeline de contenido.
Solucion temporal: Forzar Webpack
Si no puedes migrar a next-mdx-remote de inmediato, puedes forzar el uso de Webpack:
// package.json
{
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack"
}
}Webpack esta deprecado
En NextJS 16, Webpack es una opcion de compatibilidad que eventualmente se eliminara. Usala como solucion temporal mientras migras a next-mdx-remote.
Comparacion de enfoques
| Aspecto | @next/mdx + Webpack | next-mdx-remote + Turbopack |
|---|---|---|
| Compatibilidad Turbopack | No | Si |
| Velocidad de build | Buena | Mejor (Turbopack) |
| Control del pipeline | Limitado | Total |
| Syntax highlighting | rehype plugins | rehype-pretty-code |
| Frontmatter | export const metadata | YAML con gray-matter |
| Mantenimiento futuro | Incierto | Estable |
¿Cuales son las ventajas de migrar?
Ademas de resolver el error de Turbopack, migrar a next-mdx-remote te da:
- Frontmatter YAML: Metadata del post en formato estandar, no como exports de JavaScript
- Syntax highlighting avanzado: Integra
rehype-pretty-codepara bloques de codigo con colores reales - Builds mas rapidos: Turbopack es significativamente mas rapido que Webpack en proyectos grandes
- Separacion de contenido y codigo: Tu contenido vive en
content/, no mezclado con la app - generateStaticParams: SSG completo para todas tus paginas de contenido
Verificando que funciona
Despues de migrar, verifica que todo funciona correctamente:
Limpia el cache y reinicia
rm -rf .next
npm run devNavega a un post en tu navegador
http://localhost:3000/blog/mi-primer-post
Verifica que no hay errores
Si ves el contenido renderizado correctamente con syntax highlighting, todo esta funcionando.
Corre el build de produccion
npm run buildVerifica que el build completa sin errores y que generateStaticParams genera todas tus paginas.
Conclusion
El error de Turbopack con MDX en NextJS 16 ocurre porque @next/mdx no es compatible con el nuevo bundler por defecto. La solucion definitiva:
- Migra a
next-mdx-remotepara compatibilidad total con Turbopack - Mueve tu contenido a una carpeta
content/con frontmatter YAML - Usa
rehype-pretty-codepara syntax highlighting de calidad
Para la mayoria de proyectos, la migracion toma menos de una hora y el resultado es un pipeline de contenido mas robusto y rapido. Si quieres entender mejor como interactuan Server y Client Components con el ciclo de vida de React, eso te ayudara a diagnosticar errores similares.
Recursos adicionales
Preguntas frecuentes
¿Por que ocurre el error de client-only con Turbopack y MDX en NextJS 16?
El error ocurre porque el loader de @next/mdx usa configuraciones que Turbopack no puede serializar correctamente. En NextJS 16, Turbopack es el bundler por defecto, asi que el error aparece desde el primer momento si usas @next/mdx.
¿Cual es la mejor solucion para usar MDX con Turbopack en NextJS 16?
La solucion recomendada es migrar de @next/mdx a next-mdx-remote. Esta libreria no depende del loader de Webpack, funciona con Turbopack sin problemas y te da mas control sobre el pipeline de contenido.
¿En que situaciones aparece el error de Turbopack con MDX?
El error aparece cuando tu proyecto NextJS 16 tiene archivos .mdx configurados con @next/mdx. Como Turbopack es el bundler por defecto en v16, el error aparece al correr next dev o next build sin necesidad de ninguna flag extra.