Cómo crear un Blog de Alto Rendimiento con Astro y Hygraph
Guía Definitiva: Cómo crear un Blog de Alto Rendimiento con Astro y Hygraph (Headless CMS)
En el mundo del desarrollo web actual, la velocidad no es una característica opcional; es el factor principal de posicionamiento SEO. Astro ha revolucionado el mercado al entregar "0 JavaScript" por defecto al navegador, y Hygraph (anteriormente GraphCMS) ofrece una de las APIs GraphQL más robustas para gestionar contenido.
En este artículo, construiremos un blog profesional, totalmente funcional, con rutas dinámicas, renderizado de texto enriquecido y optimización de metadatos para buscadores.
¿Por qué esta combinación (Stack)?
- Astro (Frontend): Genera HTML estático puro (SSG). Esto significa que Google puede leer tu sitio instantáneamente sin ejecutar JavaScript.
- Hygraph (Backend): Te permite modelar datos complejos (autores, SEO tags, posts relacionados) y obtenerlos con una sola petición precisa gracias a GraphQL.
- Resultado: Un sitio que carga en milisegundos y obtiene puntuaciones de 100/100 en Lighthouse.
Fase 1: Arquitectura de Datos en Hygraph
Antes de tocar código, necesitamos diseñar el "cerebro" del blog. Hygraph fuerza una estructura estricta, lo cual es excelente para mantener la integridad de los datos.
- Entra en Hygraph y crea un proyecto nuevo.
- Ve a la sección Schema (Esquema).
- Crea un modelo llamado Post con los siguientes campos (esenciales para SEO y funcionalidad):
| Campo (Field) | Tipo | Configuración |
|---|---|---|
title |
Single Line Text | Obligatorio. Úsalo como "Title field". |
slug |
Slug | Obligatorio. Configúralo para generarse desde title. |
publishedAt |
Date | Opcional (para ordenar posts). |
excerpt |
Multi Line Text | Esto será la meta-description de Google. |
coverImage |
Asset | La imagen destacada y para redes sociales (OG Image). |
content |
Rich Text | El cuerpo del artículo. Asegúrate de marcar "Embeds" si quieres subir imágenes dentro del texto. |
tags |
Multi-value String | Para categorizar contenido. |
⚠️ Paso Crítico: Permisos de API
Por defecto, la API de Hygraph es privada.
- Ve a Project Settings > API Access.
- En "Content API", activa los permisos.
- Si es un blog público, crea un permiso para el rol Public y marca todas las casillas de Read (Leer).
- Copia la Content API Endpoint (la URL que empieza por
https://api-REGION.hygraph.com/...).
Fase 2: Inicialización del Proyecto Astro
Vamos a configurar un entorno profesional con TypeScript y Tailwind CSS.
# Crear proyecto (elige la plantilla "Empty" o "Blog")
npm create astro@latest blog-astro-hygraph
# Entra en la carpeta
cd blog-astro-hygraph
# Instalar Tailwind CSS (Estándar de industria para estilos)
npx astro add tailwind
# Instalar soporte para Markdown/MDX (opcional, pero útil si mezclas contenido)
npx astro add mdx
Necesitaremos una librería ligera para hacer peticiones GraphQL. Aunque fetch nativo sirve, graphql-request es más limpio y tipado.
npm install graphql-request graphql
Fase 3: La Capa de Servicio (Conexión GraphQL)
Para mantener el código limpio y mantenible, no haremos las peticiones dentro de los componentes .astro. Crearemos un servicio dedicado.
Crea el archivo src/lib/hygraph.ts:
import { GraphQLClient } from 'graphql-request';
// Reemplaza con tu URL copiada en la Fase 1
const HYGRAPH_ENDPOINT = 'TU_URL_DE_HYGRAPH_AQUI';
const client = new GraphQLClient(HYGRAPH_ENDPOINT);
// Interfaz TypeScript para tus Posts (Tipado fuerte = menos errores)
export interface Post {
slug: string;
title: string;
publishedAt: string;
excerpt: string;
coverImage: {
url: string;
width: number;
height: number;
};
content: {
html: string; // Hygraph puede devolver HTML directo del Rich Text
};
tags: string[];
}
export const hygraph = client;
Fase 4: Página Principal (Home) y Grid de Posts
En Astro, el JavaScript que corre en el servidor (build time) va entre guiones ---.
Edita src/pages/index.astro. Observa cómo solicitamos solo los datos necesarios para la tarjeta, no el contenido completo (optimización de carga).
---
import Layout from '../layouts/Layout.astro';
import { hygraph, type Post } from '../lib/hygraph';
import { gql } from 'graphql-request';
// Query GraphQL optimizada
const query = gql`
query GetPosts {
posts(orderBy: publishedAt_DESC) {
slug
title
excerpt
publishedAt
coverImage {
url
}
tags
}
}
`;
// Ejecutamos la petición en tiempo de compilación
const { posts } = await hygraph.request<{ posts: Post[] }>(query);
---
<Layout title="Mi Blog Tech | Desarrollo y Tutoriales">
<main class="max-w-4xl mx-auto px-4 py-12">
<header class="text-center mb-16">
<h1 class="text-5xl font-extrabold tracking-tight text-slate-900 mb-4">
Ingeniería & <span class="text-indigo-600">Código</span>
</h1>
<p class="text-xl text-slate-600 max-w-2xl mx-auto">
Tutoriales avanzados sobre desarrollo web, headless CMS y rendimiento.
</p>
</header>
<div class="grid md:grid-cols-2 gap-8">
{posts.map((post) => (
<article class="group bg-white rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 border border-slate-100 overflow-hidden">
<a href={`/blog/${post.slug}`} class="block">
<div class="aspect-video relative overflow-hidden">
<img
src={post.coverImage.url}
alt={post.title}
class="object-cover w-full h-full group-hover:scale-105 transition-transform duration-500"
loading="lazy"
/>
</div>
<div class="p-6">
<div class="flex gap-2 mb-3">
{post.tags.map(tag => (
<span class="text-xs font-semibold bg-indigo-50 text-indigo-600 px-2 py-1 rounded-full uppercase tracking-wider">
{tag}
</span>
))}
</div>
<h2 class="text-2xl font-bold text-slate-800 mb-2 group-hover:text-indigo-600 transition-colors">
{post.title}
</h2>
<p class="text-slate-600 line-clamp-3">
{post.excerpt}
</p>
<div class="mt-4 text-sm text-slate-400 font-medium">
{new Date(post.publishedAt).toLocaleDateString('es-ES', {
year: 'numeric', month: 'long', day: 'numeric'
})}
</div>
</div>
</a>
</article>
))}
</div>
</main>
</Layout>
Fase 5: Rutas Dinámicas (SSG) y Renderizado de Contenido
Aquí es donde Astro brilla. Usaremos getStaticPaths para decirle a Astro que genere un archivo HTML físico para cada post que exista en Hygraph.
Crea el archivo src/pages/blog/[slug].astro.
Nota importante sobre Rich Text: El campo Rich Text de Hygraph devuelve JSON por defecto, pero podemos pedirle html directamente en la query para simplificar la vida, o usar una librería procesadora. Pediremos html para este tutorial.
---
import Layout from '../../layouts/Layout.astro';
import { hygraph, type Post } from '../../lib/hygraph';
import { gql } from 'graphql-request';
// 1. getStaticPaths: Genera las rutas estáticas
export async function getStaticPaths() {
const query = gql`
query GetAllSlugs {
posts {
slug
}
}
`;
const { posts } = await hygraph.request<{ posts: Post[] }>(query);
return posts.map((post) => ({
params: { slug: post.slug },
}));
}
// 2. Obtener los datos del post específico
const { slug } = Astro.params;
const query = gql`
query GetPostBySlug($slug: String!) {
post(where: { slug: $slug }) {
title
publishedAt
excerpt
coverImage {
url
}
content {
html
}
tags
}
}
`;
const { post } = await hygraph.request<{ post: Post }>(query, { slug });
if (!post) {
return Astro.redirect('/404');
}
---
<!--
SEO DINÁMICO:
Pasamos el título, la descripción (excerpt) y la imagen
al Layout para que rellene los metatags <head>.
-->
<Layout
title={post.title}
description={post.excerpt}
image={post.coverImage.url}
>
<article class="max-w-3xl mx-auto px-4 py-12">
<!-- Cabecera del Artículo -->
<header class="mb-10 text-center">
<div class="mb-4">
{post.tags.map(tag => (
<span class="inline-block bg-slate-100 rounded-full px-3 py-1 text-sm font-semibold text-slate-700 mr-2 mb-2">
#{tag}
</span>
))}
</div>
<h1 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-6 leading-tight">
{post.title}
</h1>
<time class="text-slate-500 font-medium">
{new Date(post.publishedAt).toLocaleDateString('es-ES', { dateStyle: 'long' })}
</time>
</header>
<!-- Imagen destacada -->
<div class="relative aspect-video mb-12 rounded-xl overflow-hidden shadow-lg">
<img
src={post.coverImage.url}
alt={`Portada de ${post.title}`}
class="absolute inset-0 w-full h-full object-cover"
/>
</div>
<!--
CONTENIDO DEL POST (HTML INYECTADO)
Usamos la clase 'prose' de Tailwind Typography para
que los H2, P, Lists, y Codes se vean perfectos sin CSS extra.
-->
<div class="prose prose-lg prose-indigo mx-auto prose-imgs:rounded-lg">
<div set:html={post.content.html} />
</div>
</article>
</Layout>
Fase 6: Optimización SEO Técnica (Layout)
Para que el blog sea "fully functional" y posicione, necesitas controlar el <head> de cada página. Modifiquemos src/layouts/Layout.astro.
---
interface Props {
title: string;
description?: string;
image?: string;
}
const {
title,
description = "Blog de desarrollo web y tecnología.",
image = "/default-og-image.jpg" // Imagen por defecto
} = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- SEO Básico -->
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph / Facebook / LinkedIn -->
<meta property="og:type" content="article" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={image} />
<meta name="generator" content={Astro.generator} />
</head>
<body class="bg-slate-50 text-slate-900 font-sans antialiased">
<!-- Navbar simple -->
<nav class="border-b bg-white/80 backdrop-blur sticky top-0 z-10">
<div class="max-w-5xl mx-auto px-4 h-16 flex items-center justify-between">
<a href="/" class="font-bold text-xl tracking-tight">MiBlog<span class="text-indigo-600">.</span></a>
<!-- Aquí podrías añadir enlaces a Github/Twitter -->
</div>
</nav>
<slot />
<footer class="py-10 text-center text-slate-400 text-sm">
© {new Date().getFullYear()} Construido con Astro & Hygraph.
</footer>
</body>
</html>
Configuración Final para Producción
Para que todo funcione al desplegar, debes configurar tu dominio en astro.config.mjs. Esto es necesario para que las Canonical URLs se generen correctamente (crucial para SEO).
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
export default defineConfig({
site: 'https://mi-super-blog.com', // <--- TU DOMINIO REAL
integrations: [tailwind()],
});
Resumen de Beneficios de esta Arquitectura
- Seguridad: Tu API Key y la lógica de conexión a Hygraph nunca llegan al navegador del usuario. Todo ocurre en el servidor durante la compilación.
- Rendimiento Extremo: El usuario solo descarga HTML y CSS. No hay hidratación de React pesada ni tiempos de espera de carga (Spinners).
- SEO Impecable: Al usar
getStaticPaths, cada post es una página real. Google la indexa perfectamente. Las etiquetasmetason dinámicas y específicas para cada post. - Escalabilidad: Si mañana tienes 1000 posts, Astro los generará todos. Hygraph soporta mucho tráfico.
Solo te queda hacer npm run build y subir la carpeta dist/ a cualquier hosting (Vercel, Netlify o un FTP clásico).
Escrito por
Joaquin Sáez
