tutoriales.com

¡Construyendo un Blog SEO-Friendly con Next.js y MDX! 🚀

Este tutorial te guiará paso a paso en la creación de un blog moderno y SEO-friendly utilizando Next.js y MDX. Aprenderás a integrar contenido Markdown, generar páginas dinámicas para tus posts y a optimizar tu sitio para los motores de búsqueda.

Intermedio25 min de lectura5 views
Reportar error

Next.js es un framework de React que se ha vuelto indispensable para el desarrollo web moderno, especialmente cuando se trata de sitios estáticos o semi-estáticos como los blogs. Su capacidad para generar páginas estáticas (SSG) y renderizar en el servidor (SSR) lo convierte en la elección perfecta para la optimización SEO. Combinado con MDX, que te permite incrustar JSX directamente en tus archivos Markdown, la flexibilidad para crear contenido enriquecido es inmensa.

En este tutorial, construiremos las bases de un blog donde cada post será un archivo .mdx, lo que simplificará la gestión de contenido y permitirá a los desarrolladores o autores escribir posts con sintaxis Markdown familiar, pero con el poder de los componentes de React cuando sea necesario.


🎯 Objetivos del Tutorial

Al finalizar este tutorial, serás capaz de:

  • Configurar un proyecto Next.js desde cero para un blog.
  • Integrar MDX para escribir posts con Markdown y JSX.
  • Generar rutas dinámicas para cada post de forma automática.
  • Mostrar una lista de posts en la página de inicio.
  • Implementar una estructura básica para posts individuales.
  • Optimizar el blog para SEO con metadatos.

🛠️ Requisitos Previos

Antes de empezar, asegúrate de tener instalado lo siguiente:

  • Node.js (versión 18 o superior)
  • npm o Yarn
  • Conocimientos básicos de React y Next.js
  • Un editor de código (VS Code es altamente recomendado)

🚀 1. Configuración Inicial del Proyecto Next.js

Empezaremos creando una nueva aplicación Next.js. Abriremos nuestra terminal y ejecutaremos el siguiente comando:

npx create-next-app@latest my-nextjs-blog --ts --eslint --tailwind --app

Este comando creará un nuevo proyecto llamado my-nextjs-blog con TypeScript, ESLint, Tailwind CSS y el App Router de Next.js, que es el enfoque moderno y recomendado. Cuando te pregunte, puedes elegir las opciones por defecto.

Una vez creado el proyecto, navegamos a su directorio:

cd my-nextjs-blog
💡 Consejo: Si no quieres usar Tailwind CSS, puedes omitir `--tailwind`. Para este tutorial, es opcional pero ayuda con la presentación.

📝 2. Integrando MDX en Next.js

Para poder usar archivos .mdx en nuestro blog, necesitamos instalar algunas dependencias y configurar Next.js.

2.1. Instalación de Dependencias MDX

Instalamos las librerías necesarias:

npm install @next/mdx @mdx-js/loader @mdx-js/react

2.2. Configuración de next.config.mjs

Ahora, necesitamos modificar nuestro archivo next.config.mjs para que Next.js sepa cómo manejar los archivos .mdx. Reemplaza el contenido de tu next.config.mjs con esto:

import nextMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
}

const withMDX = nextMDX({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
    // Si quieres pasar componentes globales a MDX, puedes hacerlo aquí
    // providerImportSource: '@mdx-js/react',
  },
})

export default withMDX(nextConfig)

Con esta configuración, Next.js reconocerá los archivos .md y .mdx como páginas y los procesará adecuadamente.


📁 3. Creando la Estructura de Contenido para Posts

Vamos a organizar nuestros posts en una carpeta dedicada. Crea un directorio posts en la raíz de tu proyecto (al mismo nivel que app, public, etc.).

my-nextjs-blog/ ├── app/ ├── public/ ├── posts/ <-- Aquí guardaremos los archivos .mdx └── ...

3.1. Primer Post MDX

Crea un archivo posts/mi-primer-post.mdx con el siguiente contenido:

---
title: Mi Primer Post en Next.js y MDX
date: '2023-10-26'
author: Tu Nombre
description: ¡Una introducción al mundo de los blogs con Next.js y MDX!
image: /images/post1-hero.jpg
---

# ¡Hola Mundo MDX!

Este es el contenido de mi primer post. Está escrito en **Markdown**.

También podemos incrustar **componentes de React** directamente aquí:

import { SomeComponent } from '../components/SomeComponent'

<SomeComponent />

Este es un ejemplo de cómo MDX nos da un control increíble sobre nuestro contenido.

## Una Lista de Cosas

*   Elemento uno
*   Elemento dos
*   Elemento tres

<div class="callout tip">💡 <strong>Consejo:</strong> El bloque de metadatos al principio se llama "frontmatter" y se usa para datos como título, fecha, etc.</div>

3.2. Creando un Componente de Ejemplo (Opcional)

Si quieres probar la funcionalidad de incrustar componentes React, crea components/SomeComponent.tsx:

// components/SomeComponent.tsx
import React from 'react';

export function SomeComponent() {
  return (
    <div style={{
      padding: '1rem',
      backgroundColor: '#e6f7ff',
      borderLeft: '4px solid #3399ff',
      margin: '1rem 0'
    }}>
      <p>¡Hola desde un componente de React dentro de MDX!</p>
      <button style={{ backgroundColor: '#3399ff', color: 'white', padding: '0.5rem 1rem', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
        Haz clic
      </button>
    </div>
  );
}

📄 4. Leyendo y Procesando Contenido MDX (App Router)

Ahora viene la parte clave: leer estos archivos .mdx y convertirlos en páginas web dinámicas. En el App Router de Next.js, esto se hace con los server components y funciones como generateStaticParams.

4.1. Instalación de Dependencias para Lectura

Para leer los archivos del sistema y parsear el frontmatter, necesitaremos algunas utilidades:

npm install gray-matter

4.2. Helper para Leer Posts

Crea un archivo lib/posts.ts para contener la lógica de lectura de posts:

// lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

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

export interface PostData {
  id: string;
  title: string;
  date: string;
  author: string;
  description: string;
  image?: string;
  contentHtml: string; // El contenido MDX renderizado a HTML o string
}

export function getSortedPostsData() {
  // Get file names under /posts
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData: PostData[] = fileNames.map((fileName) => {
    // Remove ".mdx" from file name to get id
    const id = fileName.replace(/\.mdx$/, '');

    // Read markdown file as string
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents);

    // Combine the data with the id
    return {
      id,
      ...(matterResult.data as { title: string; date: string; author: string; description: string; image?: string; }),
      contentHtml: matterResult.content, // Conservamos el contenido crudo MDX por ahora
    };
  });

  // Sort posts by date
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1;
    } else {
      return -1;
    }
  });
}

export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory);
  return fileNames.map((fileName) => {
    return {
      slug: fileName.replace(/\.mdx$/, ''),
    };
  });
}

export async function getPostData(id: string) {
  const fullPath = path.join(postsDirectory, `${id}.mdx`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents);

  // Convert MDX content to React component dynamically on the server
  // For MDX, we just return the content string; Next.js handles rendering it dynamically.
  return {
    id,
    ...(matterResult.data as { title: string; date: string; author: string; description: string; image?: string; }),
    contentHtml: matterResult.content, // MDX content as string
  };
}
📌 Nota: En este punto, `contentHtml` en `getPostData` sigue siendo el contenido crudo MDX. Next.js App Router con `@next/mdx` se encargará de renderizarlo directamente cuando lo importemos en una página.

🌐 5. Creando la Página de Índice de Posts

Ahora, mostraremos todos nuestros posts en la página principal.

Modifica app/page.tsx para listar los posts:

// app/page.tsx
import Link from 'next/link';
import { getSortedPostsData } from '../lib/posts';
import { Metadata } from 'next';
import Image from 'next/image';

export const metadata: Metadata = {
  title: 'Mi Blog con Next.js y MDX',
  description: 'Un blog moderno y SEO-friendly construido con Next.js y MDX.',
};

export default function Home() {
  const allPostsData = getSortedPostsData();

  return (
    <div className="container mx-auto px-4 py-8 max-w-2xl">
      <h1 className="text-4xl font-bold text-center mb-10 text-gray-800">Bienvenido a mi Blog</h1>

      <section className="mb-10">
        <h2 className="text-2xl font-semibold mb-6 text-gray-700">Últimos Posts</h2>
        <ul className="space-y-8">
          {allPostsData.map(({ id, date, title, description, image }) => (
            <li key={id} className="bg-white shadow-lg rounded-lg overflow-hidden flex flex-col md:flex-row">
              {image && (
                <div className="md:w-1/3 w-full relative h-48 md:h-auto">
                  <Image src={image} alt={title} layout="fill" objectFit="cover" />
                </div>
              )}
              <div className="p-6 md:w-2/3 w-full">
                <Link href={`/posts/${id}`} className="text-blue-600 hover:text-blue-800 transition duration-300">
                  <h3 className="text-xl font-bold mb-2">{title}</h3>
                </Link>
                <small className="text-gray-500 block mb-3">{new Date(date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })}</small>
                <p className="text-gray-700 mb-4">{description}</p>
                <Link href={`/posts/${id}`} className="inline-block bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded transition duration-300">
                  Leer más
                </Link>
              </div>
            </li>
          ))}
        </ul>
      </section>
    </div>
  );
}

Para que los <Image> de Next.js funcionen, deberás crear la carpeta public/images y colocar una imagen post1-hero.jpg dentro (o adaptar la ruta y el nombre).


🗺️ 6. Creando Rutas Dinámicas para Posts Individuales

Cada post tendrá su propia URL, por ejemplo, /posts/mi-primer-post. Para esto, usaremos las rutas dinámicas del App Router.

6.1. Estructura de Rutas Dinámicas

Crea la siguiente estructura de archivos y carpetas:

my-nextjs-blog/ ├── app/ │ ├── posts/ │ │ ├── [slug]/ │ │ │ └── page.tsx <-- Aquí se renderizarán los posts individuales │ │ └── layout.tsx │ └── page.tsx └── ...

El [slug] indica que es un segmento dinámico de la ruta. Next.js inyectará el valor de slug en las props del componente de la página.

6.2. layout.tsx para Posts (Opcional pero recomendado)

Puedes crear un app/posts/layout.tsx para definir un layout común para todos tus posts. Por ejemplo, un ancho máximo de contenido:

// app/posts/layout.tsx
export default function PostsLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="container mx-auto px-4 py-8 max-w-3xl">
      {children}
    </div>
  );
}

6.3. page.tsx para Post Individual

Ahora, crea el archivo app/posts/[slug]/page.tsx para renderizar el contenido de cada post:

// app/posts/[slug]/page.tsx
import { getPostData, getAllPostIds, PostData } from '../../../lib/posts';
import { notFound } from 'next/navigation';
import { MDXRemote } from 'next-mdx-remote'; // No más, el App Router lo hace automáticamente
import { serialize } from 'next-mdx-remote/serialize';
import { MDXRemote as MDXRemoteApp } from 'next-mdx-remote/rsc'; // For App Router
import { Metadata } from 'next';
import Image from 'next/image';

// Importa tus componentes si los usas en MDX
import { SomeComponent } from '../../../components/SomeComponent';

interface PostProps {
  params: { slug: string };
}

export async function generateStaticParams() {
  const postIds = getAllPostIds();
  return postIds.map((postId) => ({
    slug: postId.slug,
  }));
}

export async function generateMetadata({ params }: PostProps): Promise<Metadata> {
  const postData = await getPostData(params.slug);
  if (!postData) {
    return {};
  }
  return {
    title: postData.title,
    description: postData.description,
    // Agrega más metadatos SEO aquí
    openGraph: {
      title: postData.title,
      description: postData.description,
      type: 'article',
      url: `https://yourdomain.com/posts/${postData.id}`,
      images: postData.image ? [{ url: `https://yourdomain.com${postData.image}` }] : [],
    },
    twitter: {
      card: 'summary_large_image',
      title: postData.title,
      description: postData.description,
      images: postData.image ? [`https://yourdomain.com${postData.image}`] : [],
    },
  };
}

export default async function Post({ params }: PostProps) {
  const postData = await getPostData(params.slug);

  if (!postData) {
    notFound();
  }

  // Aquí, MDXRemote (o su equivalente para App Router) ya no es necesario como en Pages Router
  // El App Router de Next.js con @next/mdx maneja la importación y renderizado directamente.
  // Simplemente importa el archivo MDX como un componente de React.
  // Sin embargo, para la carga dinámica de posts desde la carpeta 'posts', necesitamos un enfoque diferente.
  // Usaremos 'next-mdx-remote/rsc' para esto en el App Router.

  const components = {
    SomeComponent, // Pasa tus componentes aquí
    // Por ejemplo: img: (props) => <Image {...props} />
  };

  return (
    <article className="prose lg:prose-xl mx-auto py-8">
      <h1 className="text-4xl font-extrabold text-gray-900 mb-4">{postData.title}</h1>
      <p className="text-gray-600 mb-6">Por {postData.author} el {new Date(postData.date).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })}</p>

      {postData.image && (
        <div className="relative w-full h-80 mb-8 rounded-lg overflow-hidden">
          <Image src={postData.image} alt={postData.title} layout="fill" objectFit="cover" />
        </div>
      )}

      <div className="prose prose-blue max-w-none">
        {/* La clave aquí es usar MDXRemote para RSC (Server Components) */}
        {/* Esto requiere que el contenido MDX sea serializado en el servidor si no lo importamos directamente */}
        {/* Dado que estamos leyendo de 'posts', necesitamos esta aproximación */}
        <MDXRemoteApp source={postData.contentHtml} components={components} />
      </div>
    </article>
  );
}

⚠️ Advertencia: Para que `MDXRemoteApp` funcione, asegúrate de haber instalado `next-mdx-remote`. Si aún no lo has hecho, ejecuta: `npm install next-mdx-remote`.

6.4. Configuración del global.css

Para que la clase prose funcione (que nos da estilos de tipografía agradables para el contenido Markdown), necesitamos agregar tailwindcss/typography a nuestro tailwind.config.ts y asegurarnos de que el CSS global se importe.

En tailwind.config.ts:

// tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './posts/**/*.{js,ts,jsx,tsx,mdx}', // Asegúrate de incluir tu carpeta de posts
  ],
  theme: {
    extend: {
      backgroundImage: {
        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
        'gradient-conic':
          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'), // Agrega esto
  ],
}
export default config

Instala el plugin:

npm install @tailwindcss/typography

Luego, asegúrate de que app/globals.css esté importado en app/layout.tsx (lo cual create-next-app ya hace por defecto).


🔍 7. Optimización SEO y Metadatos

La optimización para motores de búsqueda (SEO) es crucial para un blog. Next.js facilita esto con su API de metadatos.

7.1. Metadatos Estáticos y Dinámicos

  • Metadatos Estáticos (layout.tsx, page.tsx): Para metadatos que son comunes a todo el sitio o a una sección, puedes exportar un objeto metadata o una función generateMetadata directamente desde el layout.tsx o page.tsx del App Router.

    Ya hemos añadido un ejemplo básico en app/page.tsx y app/posts/[slug]/page.tsx.

  • Metadatos Dinámicos (generateMetadata): Para posts individuales, donde cada post tiene un título, descripción, imagen, etc., únicos, utilizamos la función generateMetadata en app/posts/[slug]/page.tsx.

    En el ejemplo anterior, generateMetadata lee los datos del post (title, description, image) de nuestro frontmatter y los inyecta en las etiquetas <title>, <meta name="description">, Open Graph (og:) y Twitter (twitter:).

🔥 Importante: Asegúrate de reemplazar `https://yourdomain.com` con el dominio real de tu blog cuando lo despliegues para que las URLs absolutas en Open Graph y Twitter Cards sean correctas.

7.2. Sitemap y RSS (Opcional Avanzado)

Para un SEO más avanzado, considerarías:

  • Generación de Sitemap: Puedes generar un sitemap.xml dinámicamente o estáticamente para ayudar a los motores de búsqueda a indexar todas tus páginas.
  • Feed RSS: Ofrecer un feed RSS es útil para que los usuarios y agregadores de noticias puedan seguir tu contenido.

Estos temas son más avanzados y requerirían implementaciones adicionales fuera del alcance básico de este tutorial, pero son extensiones importantes para un blog de producción.


✅ 8. Pruebas y Despliegue

8.1. Ejecutar el Proyecto

Para ver tu blog en acción, ejecuta:

npm run dev

Abre tu navegador y ve a http://localhost:3000. Deberías ver la lista de posts y poder hacer clic en ellos para ver los detalles.

💡 Consejo: Si realizas cambios en los archivos `.mdx` o en la lógica de `lib/posts.ts` y no ves los cambios, a veces es necesario reiniciar el servidor de desarrollo.

8.2. Despliegue

Next.js es increíblemente fácil de desplegar, especialmente en plataformas como Vercel (creadores de Next.js).

  1. Vercel: Simplemente conecta tu repositorio de Git (GitHub, GitLab, Bitbucket) a Vercel, y automáticamente detectará tu proyecto Next.js y lo desplegará. Cada push a tu rama principal (ej. main) activará un nuevo despliegue. Los posts MDX se generarán como páginas estáticas en tiempo de construcción, lo que resulta en un rendimiento excelente.

  2. Otras Plataformas: Puedes desplegar en otras plataformas que soporten Node.js, como Netlify, AWS Amplify, Render, etc. Generalmente, solo necesitas construir el proyecto (npm run build) y luego servir los archivos estáticos y la función de servidor Next.js.


📈 Futuras Mejoras

Este tutorial te ha dado una base sólida. Aquí hay algunas ideas para llevar tu blog al siguiente nivel:

  • Estilos y Componentes: Mejora los estilos con Tailwind CSS, crea componentes reutilizables para elementos MDX (ej. <img> personalizado, <CodeBlock>).
  • Paginación: Implementa paginación para blogs con muchos posts.
  • Búsqueda: Agrega una funcionalidad de búsqueda para encontrar posts fácilmente.
  • Comentarios: Integra un sistema de comentarios (ej. Disqus, Utterances).
  • Etiquetas/Categorías: Permite clasificar posts con etiquetas y categorías.
  • Imágenes Optimizadas: Utiliza el componente <Image> de Next.js para todas las imágenes y considera una CDN de imágenes.
  • Analíticas: Integra Google Analytics o alguna otra herramienta de analíticas para rastrear el tráfico.
¿Por qué MDX y no solo Markdown? MDX extiende Markdown permitiéndote incrustar componentes JSX directamente. Esto significa que puedes tener todo el poder de React dentro de tus posts, creando experiencias de contenido altamente interactivas y personalizadas, más allá de lo que el Markdown puro puede ofrecer. Es ideal para tutoriales técnicos, demos interactivas o cualquier contenido que necesite lógica UI.
Paso 1: Configuración del Proyecto Next.js
Paso 2: Integración de MDX
Paso 3: Creación de Contenido MDX
Paso 4: Lógica para Leer Posts
Paso 5: Página de Índice de Posts
Paso 6: Rutas Dinámicas para Posts
Paso 7: Optimización SEO con Metadatos
Paso 8: Pruebas y Despliegue
Usuario Navegador Next.js Server Archivos MDX (posts/) gray-matter (frontmatter) @next/mdx (procesar) HTML/CSS/JS Página de Blog

Con esto, tienes una base sólida para un blog moderno, flexible y optimizado con Next.js y MDX. ¡A escribir posts!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!