solucion-errores·8 min de lectura

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:

bash
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:

json
// 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:

bash
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:

bash
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 .mdx a JavaScript
  • El loader de @next/mdx usa 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.

1

Instala next-mdx-remote y desinstala @next/mdx

bash
npm install next-mdx-remote gray-matter
npm uninstall @next/mdx @mdx-js/loader @mdx-js/react
2

Simplifica tu next.config.ts

Elimina la configuracion de MDX del config:

typescript
// next.config.ts - Limpio, sin createMDX
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  pageExtensions: ['js', 'jsx', 'ts', 'tsx'],
};
 
export default nextConfig;
3

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/:

plaintext
content/
└── blog/
    ├── mi-primer-post.mdx
    └── segundo-post.mdx
mdx
---
title: "Mi primer post"
description: "Un post de ejemplo"
slug: "mi-primer-post"
---
 
# Mi primer post
 
Contenido del post con **Markdown** y componentes.
4

Crea una funcion para leer el contenido

typescript
// 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);
  });
}
5

Renderiza con MDXRemote en tu page.tsx

typescript
// 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:

json
// 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 + Webpacknext-mdx-remote + Turbopack
Compatibilidad TurbopackNoSi
Velocidad de buildBuenaMejor (Turbopack)
Control del pipelineLimitadoTotal
Syntax highlightingrehype pluginsrehype-pretty-code
Frontmatterexport const metadataYAML con gray-matter
Mantenimiento futuroInciertoEstable

¿Cuales son las ventajas de migrar?

Ademas de resolver el error de Turbopack, migrar a next-mdx-remote te da:

  1. Frontmatter YAML: Metadata del post en formato estandar, no como exports de JavaScript
  2. Syntax highlighting avanzado: Integra rehype-pretty-code para bloques de codigo con colores reales
  3. Builds mas rapidos: Turbopack es significativamente mas rapido que Webpack en proyectos grandes
  4. Separacion de contenido y codigo: Tu contenido vive en content/, no mezclado con la app
  5. generateStaticParams: SSG completo para todas tus paginas de contenido

Verificando que funciona

Despues de migrar, verifica que todo funciona correctamente:

1

Limpia el cache y reinicia

bash
rm -rf .next
npm run dev
2

Navega a un post en tu navegador

http://localhost:3000/blog/mi-primer-post

3

Verifica que no hay errores

Si ves el contenido renderizado correctamente con syntax highlighting, todo esta funcionando.

4

Corre el build de produccion

bash
npm run build

Verifica 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:

  1. Migra a next-mdx-remote para compatibilidad total con Turbopack
  2. Mueve tu contenido a una carpeta content/ con frontmatter YAML
  3. Usa rehype-pretty-code para 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

#nextjs-16#turbopack#mdx#webpack

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.