Usar la API de Hashnode con Astro: Despliegue Fácil en Vercel
Cómo usar la API de Hashnode con Astro y desplegarlo en Vercel
Introducción
¿Te gustaría tener tu propio sitio web con un diseño personalizado pero sin perder el excelente editor de Hashnode? En este artículo aprenderás a crear un blog con Astro que consume contenido directamente desde la API de Hashnode y cómo desplegarlo en Vercel.
¿Por qué esta combinación?
Hashnode: Editor potente con Markdown, sintaxis de código, imágenes y SEO integrado
Astro: Framework ultrarrápido con excelente rendimiento y SEO
Vercel: Despliegue automático y CDN global sin configuración
Requisitos previos
Node.js 18 o superior instalado
Una cuenta en Hashnode
Una cuenta en Vercel (gratis)
Conocimientos básicos de JavaScript/TypeScript
Paso 1: Configurar tu blog en Hashnode
Crea una cuenta en Hashnode
Configura tu blog en Hashnode (puedes usar un subdominio gratuito)
Escribe algunos artículos de prueba
Obtén tu Publication ID desde la configuración de tu blog
Para encontrar tu Publication ID:
Ve a tu dashboard de Hashnode
Entra en la configuración de tu publicación
Busca el ID en la URL o en la sección de API
Paso 2: Crear el proyecto con Astro
Abre tu terminal y ejecuta:
npm create astro@latest mi-blog-hashnode
cd mi-blog-hashnode
Selecciona las siguientes opciones:
Template: Empty
TypeScript: Yes
Install dependencies: Yes
Git repository: Yes
Paso 3: Instalar dependencias necesarias
npm install graphql-request graphql
Paso 4: Configurar variables de entorno
Crea un archivo .env en la raíz del proyecto:
HASHNODE_PUBLICATION_ID=tu-publication-id-aqui
Paso 5: Crear el cliente de la API de Hashnode
Crea el archivo src/lib/hashnode.ts:
import { GraphQLClient, gql } from 'graphql-request';
const endpoint = 'https://gql.hashnode.com';
const client = new GraphQLClient(endpoint);
export interface Post {
id: string;
title: string;
brief: string;
slug: string;
coverImage?: {
url: string;
};
content: {
markdown: string;
};
publishedAt: string;
tags?: Array<{
name: string;
slug: string;
}>;
author: {
name: string;
profilePicture?: string;
};
}
const GET_POSTS = gql`
query GetPosts($host: String!, $first: Int!) {
publication(host: $host) {
posts(first: $first) {
edges {
node {
id
title
brief
slug
coverImage {
url
}
publishedAt
tags {
name
slug
}
author {
name
profilePicture
}
}
}
}
}
}
`;
const GET_POST = gql`
query GetPost($host: String!, $slug: String!) {
publication(host: $host) {
post(slug: $slug) {
id
title
brief
slug
coverImage {
url
}
content {
markdown
}
publishedAt
tags {
name
slug
}
author {
name
profilePicture
}
}
}
}
`;
export async function getPosts(host: string, first = 20): Promise<Post[]> {
const data: any = await client.request(GET_POSTS, { host, first });
return data.publication.posts.edges.map((edge: any) => edge.node);
}
export async function getPost(host: string, slug: string): Promise<Post | null> {
const data: any = await client.request(GET_POST, { host, slug });
return data.publication.post;
}
Paso 6: Crear la página principal
Edita src/pages/index.astro:
---
import { getPosts } from '../lib/hashnode';
const publicationHost = 'tu-blog.hashnode.dev'; // Reemplaza con tu host de Hashnode
const posts = await getPosts(publicationHost, 10);
---
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mi Blog</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 3rem;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: #2563eb;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.post-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s;
text-decoration: none;
color: inherit;
display: block;
}
.post-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.post-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.post-content {
padding: 1.5rem;
}
.post-title {
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: #1e293b;
}
.post-brief {
color: #64748b;
margin-bottom: 1rem;
}
.post-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
color: #94a3b8;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Mi Blog</h1>
<p>Artículos técnicos y tutoriales</p>
</header>
<div class="posts-grid">
{posts.map(post => (
<a href={`/post/${post.slug}`} class="post-card">
{post.coverImage && (
<img
src={post.coverImage.url}
alt={post.title}
class="post-image"
/>
)}
<div class="post-content">
<h2 class="post-title">{post.title}</h2>
<p class="post-brief">{post.brief}</p>
<div class="post-meta">
<span>{post.author.name}</span>
<span>{new Date(post.publishedAt).toLocaleDateString('es-ES')}</span>
</div>
</div>
</a>
))}
</div>
</div>
</body>
</html>
Paso 7: Crear la página de artículo individual
Crea src/pages/post/[slug].astro:
---
import { getPost, getPosts } from '../../lib/hashnode';
import { marked } from 'marked';
const { slug } = Astro.params;
const publicationHost = 'tu-blog.hashnode.dev'; // Reemplaza con tu host
if (!slug) {
return Astro.redirect('/');
}
const post = await getPost(publicationHost, slug);
if (!post) {
return Astro.redirect('/');
}
const htmlContent = marked(post.content.markdown);
export async function getStaticPaths() {
const publicationHost = 'tu-blog.hashnode.dev';
const posts = await getPosts(publicationHost, 50);
return posts.map(post => ({
params: { slug: post.slug }
}));
}
---
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{post.title}</title>
<meta name="description" content={post.brief}>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.back-link {
display: inline-block;
margin-bottom: 2rem;
color: #2563eb;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
article {
background: white;
border-radius: 8px;
padding: 3rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.cover-image {
width: 100%;
height: 400px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 2rem;
}
h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #1e293b;
}
.meta {
color: #64748b;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.content {
font-size: 1.125rem;
line-height: 1.8;
}
.content h2 {
margin-top: 2rem;
margin-bottom: 1rem;
color: #1e293b;
}
.content h3 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #334155;
}
.content p {
margin-bottom: 1rem;
}
.content pre {
background: #1e293b;
color: #e2e8f0;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
margin-bottom: 1rem;
}
.content code {
background: #f1f5f9;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
}
.content pre code {
background: none;
padding: 0;
}
.content a {
color: #2563eb;
text-decoration: none;
}
.content a:hover {
text-decoration: underline;
}
.content ul, .content ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
.content li {
margin-bottom: 0.5rem;
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">← Volver al inicio</a>
<article>
{post.coverImage && (
<img
src={post.coverImage.url}
alt={post.title}
class="cover-image"
/>
)}
<h1>{post.title}</h1>
<div class="meta">
<p>Por {post.author.name} • {new Date(post.publishedAt).toLocaleDateString('es-ES', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}</p>
</div>
<div class="content" set:html={htmlContent} />
</article>
</div>
</body>
</html>
Instala marked para procesar Markdown:
npm install marked
Paso 8: Configurar Astro para producción
Edita astro.config.mjs:
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
build: {
inlineStylesheets: 'auto'
}
});
Paso 9: Desplegar en Vercel
Opción 1: Desde la interfaz de Vercel
Sube tu código a GitHub
Ve a Vercel
Haz clic en "Add New Project"
Importa tu repositorio de GitHub
Vercel detectará automáticamente que es un proyecto Astro
Añade las variables de entorno:
HASHNODE_PUBLICATION_ID: Tu ID de publicación
Haz clic en "Deploy"
Opción 2: Desde la CLI de Vercel
npm install -g vercel
vercel login
vercel
Sigue las instrucciones en pantalla. En la configuración, añade tus variables de entorno.
Paso 10: Configurar redepliegue automático
Para que tu sitio se actualice automáticamente cuando publiques en Hashnode:
En Vercel, ve a tu proyecto → Settings → Git
Copia el "Deploy Hook URL"
En Hashnode, ve a tu blog → Settings → Webhooks
Añade el Deploy Hook URL de Vercel
Selecciona el evento "post.published"
Ahora, cada vez que publiques un artículo en Hashnode, Vercel reconstruirá tu sitio automáticamente.
Optimizaciones adicionales
Añadir regeneración incremental
Edita astro.config.mjs:
export default defineConfig({
output: 'hybrid',
adapter: vercel({
edgeMiddleware: true
})
});
Instala el adaptador:
npm install @astrojs/vercel
Caché de datos
Crea src/lib/cache.ts:
const cache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutos
export function getCached<T>(key: string): T | null {
const cached = cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > CACHE_DURATION) {
cache.delete(key);
return null;
}
return cached.data;
}
export function setCache<T>(key: string, data: T): void {
cache.set(key, {
data,
timestamp: Date.now()
});
}
Añadir sitemap
npm install @astrojs/sitemap
Actualiza astro.config.mjs:
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://tu-sitio.vercel.app',
integrations: [sitemap()]
});
Conclusión
Ahora tienes un blog ultrarrápido que combina lo mejor de tres mundos:
Escribes en el excelente editor de Hashnode
Diseñas tu sitio con total libertad en Astro
Despliegas automáticamente en Vercel con CDN global
Tu flujo de trabajo es simple: escribe en Hashnode, publica, y tu sitio se actualiza automáticamente. ¡Sin preocuparte por la infraestructura!
Recursos adicionales
¿Tienes preguntas? Déjalas en los comentarios o contáctame en mis redes sociales.