¡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.
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
📝 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
};
}
🌐 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>
);
}
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
metadatao una funcióngenerateMetadatadirectamente desde ellayout.tsxopage.tsxdel App Router.Ya hemos añadido un ejemplo básico en
app/page.tsxyapp/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
generateMetadataenapp/posts/[slug]/page.tsx.En el ejemplo anterior,
generateMetadatalee 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:).
7.2. Sitemap y RSS (Opcional Avanzado)
Para un SEO más avanzado, considerarías:
- Generación de Sitemap: Puedes generar un
sitemap.xmldiná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.
8.2. Despliegue
Next.js es increíblemente fácil de desplegar, especialmente en plataformas como Vercel (creadores de Next.js).
-
Vercel: Simplemente conecta tu repositorio de Git (GitHub, GitLab, Bitbucket) a Vercel, y automáticamente detectará tu proyecto Next.js y lo desplegará. Cada
pusha 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. -
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.Con esto, tienes una base sólida para un blog moderno, flexible y optimizado con Next.js y MDX. ¡A escribir posts!
Tutoriales relacionados
- ¡Despliega tu App Next.js como un Pro! Guía Completa con Vercelbeginner15 min
- ¡Next.js en el Edge! Cómo Usar Edge Functions para Apps Más Rápidas y Globales 🚀intermediate15 min
- Aprovechando la Carga de Datos en el Cliente con SWR en Next.js App Router ⚡intermediate15 min
- ¡Rutas Dinámicas y Anidadas en Next.js con el App Router! 🚀 Guía Completaintermediate20 min
- Optimización Avanzada de Imágenes en Next.js con next/image y Soluciones Personalizadas 📸intermediate18 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!