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
- Documentación de Hashnode API
- Documentación de Astro
- Documentación de Vercel
- GraphQL Playground de Hashnode
¿Tienes preguntas? Déjalas en los comentarios o contáctame en mis redes sociales.
Top comments (0)