DEV Community

Cover image for Cómo usar la API de Hashnode con Astro y desplegarlo en Vercel
Joaquin Sáez
Joaquin Sáez

Posted on • Originally published at code.webgae.com

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

  1. Crea una cuenta en Hashnode
  2. Configura tu blog en Hashnode (puedes usar un subdominio gratuito)
  3. Escribe algunos artículos de prueba
  4. 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
Enter fullscreen mode Exit fullscreen mode

Selecciona las siguientes opciones:

  • Template: Empty
  • TypeScript: Yes
  • Install dependencies: Yes
  • Git repository: Yes

Paso 3: Instalar dependencias necesarias

npm install graphql-request graphql
Enter fullscreen mode Exit fullscreen mode

Paso 4: Configurar variables de entorno

Crea un archivo .env en la raíz del proyecto:

HASHNODE_PUBLICATION_ID=tu-publication-id-aqui
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Instala marked para procesar Markdown:

npm install marked
Enter fullscreen mode Exit fullscreen mode

Paso 8: Configurar Astro para producción

Edita astro.config.mjs:

import { defineConfig } from 'astro/config';

export default defineConfig({
  output: 'static',
  build: {
    inlineStylesheets: 'auto'
  }
});
Enter fullscreen mode Exit fullscreen mode

Paso 9: Desplegar en Vercel

Opción 1: Desde la interfaz de Vercel

  1. Sube tu código a GitHub
  2. Ve a Vercel
  3. Haz clic en "Add New Project"
  4. Importa tu repositorio de GitHub
  5. Vercel detectará automáticamente que es un proyecto Astro
  6. Añade las variables de entorno:
    • HASHNODE_PUBLICATION_ID: Tu ID de publicación
  7. Haz clic en "Deploy"

Opción 2: Desde la CLI de Vercel

npm install -g vercel
vercel login
vercel
Enter fullscreen mode Exit fullscreen mode

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:

  1. En Vercel, ve a tu proyecto → Settings → Git
  2. Copia el "Deploy Hook URL"
  3. En Hashnode, ve a tu blog → Settings → Webhooks
  4. Añade el Deploy Hook URL de Vercel
  5. 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
  })
});
Enter fullscreen mode Exit fullscreen mode

Instala el adaptador:

npm install @astrojs/vercel
Enter fullscreen mode Exit fullscreen mode

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()
  });
}
Enter fullscreen mode Exit fullscreen mode

Añadir sitemap

npm install @astrojs/sitemap
Enter fullscreen mode Exit fullscreen mode

Actualiza astro.config.mjs:

import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://tu-sitio.vercel.app',
  integrations: [sitemap()]
});
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)