Tutorial: Cómo crear un Blog Profesional con Next.js y Wisp CMS
Si te gusta la experiencia de escritura de Medium o Notion pero quieres tener control total sobre el código de tu web (sin pagar hosting de CMS caros), Wisp es la pieza que te falta.
En esta guía conectaremos la API de Wisp con Next.js 14/15 para montar un blog en menos de 15 minutos.
¿Qué necesitas?
- Node.js instalado.
- Una cuenta gratuita en wisp.blog.
Paso 1: Configurar Wisp
- Ve a wisp.blog y regístrate.
- Crea un nuevo "Blog". Llámalo como quieras (ej: "Mi Blog Dev").
- Ve a la pestaña Setup en tu panel de Wisp.
- Copia tu Blog ID (será una cadena larga de números y letras). Lo necesitaremos en el código.
- (Opcional pero recomendado) Escribe un artículo de prueba y publícalo para tener algo que mostrar.
Paso 2: Crear el proyecto Next.js
Abre tu terminal y crea una app nueva. Usaremos TypeScript y Tailwind CSS (el estándar actual).
npx create-next-app@latest mi-blog-wisp
# Responde:
# TypeScript: Yes
# Tailwind CSS: Yes
# App Router: Yes
Entra en la carpeta e instala el cliente oficial de Wisp. Es una librería pequeñita que facilita mucho las llamadas a la API (no necesitas hacer fetch manual).
cd mi-blog-wisp
npm install @wisp-cms/client
También instalaremos el plugin de tipografía de Tailwind para que el contenido del post (que viene en HTML) se vea bonito automáticamente:
npm install -D @tailwindcss/typography
Configura el plugin: Abre tailwind.config.ts y añádelo:
// tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
// ... resto de tu config
plugins: [
require('@tailwindcss/typography'),
],
};
export default config;
Paso 3: Conectar la API (El cliente)
Vamos a crear un archivo para inicializar Wisp. Crea un archivo lib/wisp.ts (crea la carpeta lib si no existe).
// lib/wisp.ts
import { buildWispClient } from "@wisp-cms/client";
export const wisp = buildWispClient({
blogId: "PEGA_AQUI_TU_BLOG_ID", // Reemplaza esto con el ID del Paso 1
});
Nota: En un proyecto real, deberías guardar el ID en una variable de entorno .env.
Paso 4: Crear la página principal (Lista de Posts)
Vamos a editar app/page.tsx. Aquí pediremos la lista de artículos a Wisp y los mostraremos.
// app/page.tsx
import { wisp } from "@/lib/wisp";
import Link from "next/link";
import Image from "next/image";
export default async function Home() {
// 1. Pedimos los posts a Wisp (por defecto trae 20)
const result = await wisp.getPosts({ limit: 10 });
return (
<main className="max-w-4xl mx-auto py-10 px-4">
<h1 className="text-4xl font-bold mb-8">Mi Blog Personal</h1>
<div className="grid gap-8">
{result.posts.map((post) => (
<article key={post.id} className="border-b pb-8">
<Link href={`/blog/${post.slug}`} className="group">
{/* Imagen de portada si existe */}
{post.image && (
<div className="relative w-full h-64 mb-4 overflow-hidden rounded-lg">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover transition-transform group-hover:scale-105"
/>
</div>
)}
<h2 className="text-2xl font-bold group-hover:text-blue-600 transition-colors">
{post.title}
</h2>
<p className="text-gray-600 mt-2">
{post.description}
</p>
<span className="text-sm text-gray-400 mt-4 block">
Leer más →
</span>
</Link>
</article>
))}
</div>
</main>
);
}
Paso 5: La página del Post Individual
Ahora necesitamos crear la página dinámica que mostrará el contenido.
Crea la estructura de carpetas: app/blog/[slug]/page.tsx.
// app/blog/[slug]/page.tsx
import { wisp } from "@/lib/wisp";
import { notFound } from "next/navigation";
import Image from "next/image";
interface Params {
params: {
slug: string;
};
}
// 1. Esto genera los metadatos SEO automáticamente (Título, Descripción)
export async function generateMetadata({ params }: Params) {
const result = await wisp.getPost(params.slug);
if (!result || !result.post) return { title: "Post no encontrado" };
return {
title: result.post.title,
description: result.post.description,
openGraph: {
images: [result.post.image || ""],
},
};
}
// 2. La página del post
export default async function BlogPostPage({ params }: Params) {
const result = await wisp.getPost(params.slug);
if (!result || !result.post) {
return notFound();
}
const { title, content, publishedAt, image, tags } = result.post;
return (
<article className="max-w-3xl mx-auto py-10 px-4">
{/* Cabecera */}
<header className="mb-8 text-center">
<h1 className="text-4xl md:text-5xl font-extrabold mb-4">{title}</h1>
<p className="text-gray-500">
{new Date(publishedAt).toLocaleDateString()}
</p>
</header>
{/* Imagen Principal */}
{image && (
<div className="relative w-full h-80 md:h-96 mb-10 rounded-xl overflow-hidden shadow-lg">
<Image src={image} alt={title} fill className="object-cover" priority />
</div>
)}
{/*
CONTENIDO DEL POST
Usamos la clase 'prose' de Tailwind Typography.
Esto estiliza automáticamente el HTML que nos da Wisp.
*/}
<div
className="prose prose-lg prose-blue mx-auto"
dangerouslySetInnerHTML={{ __html: content }}
/>
{/* Tags */}
<div className="mt-10 flex gap-2">
{tags.map((tag) => (
<span key={tag.id} className="bg-gray-100 px-3 py-1 rounded-full text-sm">
#{tag.name}
</span>
))}
</div>
</article>
);
}
Paso 6: Probar
- Ejecuta el servidor:
npm run dev. - Abre
http://localhost:3000. - Deberías ver tu lista de posts. Si haces clic en uno, te llevará al artículo completo perfectamente estilizado.
¿Por qué esto es mejor que Hashnode estándar?
- Velocidad: Usas Next.js App Router. Puedes implementar Server Components para que la carga sea instantánea.
- SEO Canónico: Tú controlas el dominio desde el principio.
- Wisp "Magic": Si en Wisp escribes y pegas imágenes, o incrustas tweets, la API te devuelve el HTML listo para renderizar. El
dangerouslySetInnerHTMLcombinado contailwind-typographyhace que se vea profesional sin que escribas ni una línea de CSS para los párrafos, listas o citas.
Siguientes pasos (Despliegue)
Sube tu código a GitHub y conéctalo a Vercel. ¡Tu blog estará online gratis y con CDN global en minutos!
Escrito por
Joaquin Sáez
