· Jose Antonio López · 6 min lectura
SEO en Astro - Guía para Configurar Metadatos, Open Graph y JSON-LD
Aprende a implementar una estrategia de SEO técnico en Astro. Configura metadatos dinámicos, etiquetas Open Graph, Twitter Cards, datos estructurados con JSON-LD y tipado con schema-dts para mejorar la indexación y visibilidad en Google.

Objetivo
Proporcionar una guía técnica real sobre cómo configurar el SEO en un proyecto web con Astro y enfocado a blog. Técnicamente se explica lo siguiente:
¿Por qué escribo este artículo?
La mayoría de sitios añade etiquetas <title>, <meta name="description"> y poco más. Sin embargo, en aplicaciones reales se necesita estructurar las páginas con datos enriquecidos.
En este artículo, detallo cómo construir un sistema que permita reusar metadatos globales, sobrescribirlos en páginas y generar datos estructurados (JSON-LD) sin ensuciar la lógica de los componentes.
Prerequisitos
Para seguir este artículo necesitas tener instalado:
Dependencias
Trabajaremos con las siguientes librerías:
{
"dependencies": {
"@astrolib/seo": "1.0.0-beta.8",
"schema-dts": "^1.1.2",
"lodash.merge": "^4.6.2"
}
}Decisiones de diseño
Estructura del proyecto
A continuación puedes ver los archivos de la capa de SEO y datos estructurados:
src
├── config.yaml // Configuración global del sitio
├── components
│ └── common
│ ├── CommonMeta.astro // i18n y etiquetas base
│ └── Metadata.astro // Lógica con @astrolib/seo
├── ld-json
│ ├── pages
│ │ └── blog.ts // Esquemas de página de blog
│ ├── types.ts // Definiciones de tipos
│ └── utils.ts // Generadores de JSON-LD
├── layouts
│ ├── Layout.astro // Layout genérico
│ └── BlogEntryLayout.astro // Layout especializado para posts
└── pages
└── [...blog]
└── index.astro // Implementación final del postDatos Estructurados Tipados
En lugar de escribir JSON plano se usa schema-dts y se garantiza que no se cometan errores en los nombres de las propiedades (por ejemplo, escribir headline en lugar de name donde no corresponde).
Configuración del sitio cargada con integración de Astrowind
Para evitar el hardcoding de valores se usa un archivo de configuración llmado src/config.yaml. Mediante una integración de AstroWind, los valores se exponen en un módulo virtual astrowind:config y el componente de metadatos accede a la configuración global.
Esta es la forma más fácil que he encontrado para mi sitio web ya que carga la configuración y los datos vienen tipados en un solo lugar dentro de las variables SITE y METADATA.
Si necsitas algo más customizado y quieres una referencia puedes visitar el repositorio oficial de AstroWind en la sección “integration”.
SEO Global
Configuración
La configuración del sitio web está almacenada en el archivo src/config.yaml:
site:
name: josealopez.dev
site: https://josealopez.dev
metadata:
title:
default: Blog sobre desarrollo web
template: '%s | Jose López'
description: Blog de desarrollo web, tutoriales, guías y recursos sobre backend y frontend.
robots:
index: true
follow: true
openGraph:
site_name: josealopez.dev
images:
- url: '~/assets/images/whatever.png'
width: 1200
height: 628
type: website
twitter:
handle: '@josealopez_dev'
site: '@josealopez_dev'
cardType: summary_large_image
Metadatos
La pieza central es el componente Metadata.astro. El componente recibe las propiedades de SEO y las inyecta en el <head>. Este componente viene ya de Astrowind en un principio y ha sido adaptado a mis necesidades.
import merge from 'lodash.merge';
import { AstroSeo } from '@astrolib/seo';
import { SITE, METADATA } from 'astrowind:config';
import type { MetaData } from '~/types';
export interface Props extends MetaData {}
const {
title,
canonical,
description,
openGraph = {},
} = Astro.props;
const seoProps = merge(
{
titleTemplate: METADATA?.title?.template || '%s',
description: METADATA?.description,
canonical: canonical,
openGraph: {
site_name: SITE?.name,
type: 'website',
},
},
{
title: title,
description: description,
openGraph: { url: canonical, ...openGraph },
}
);
---
<AstroSeo {...{ ...seoProps, openGraph: await adaptOpenGraphImages(seoProps?.openGraph, Astro.site) }} />Explicación del código
merge: Combinaastrowind:configcon las props del componente. Los valores de la derecha sobrescriben a los de la izquierda.titleTemplate: Permite que todas las páginas tengan un formato coherente (ej: “Mi Post | Jose Lopez”) sin tener que escribir el nombre del sitio cada vez.AstroSeo: Componente de la librería que se encarga de las etiquetas<title>,<meta name="description">,og:ytwitter:.
Tip
El uso de un titleTemplate con %s es una buena práctica de SEO para mantener la consistencia de marca en los resultados de búsqueda de Google.
SEO para Datos Estructurados
Para las entradas de blog, necesitaba un esquema para generar JSON-LD. El objetivo es que los buscadores puedan entender la estructura y contenido del sitio sin necesidad de leer todo el HTML. Mejor poner las cosas fáciles a los bots que indexan antes de que se vayan sin haber indexado lo importante.
Generador de JSON-LD
Se crea una utilidad que transforma un objeto post en un objeto BlogPosting válido dentro de schema.org.
import type { BlogPosting, WithContext } from 'schema-dts';
import type { Post } from '~/types';
export function createArticleJsonLd(post: Post): WithContext<BlogPosting> {
return {
'@context': 'https://schema.org',p
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
datePublished: post.publishDate.toISOString(),
dateModified: post.updateDate?.toISOString() || post.publishDate.toISOString(),
image: post.image,
author: {
'@type': 'Person',
name: 'Jose Antonio López',
url: 'https://josealopez.dev'
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': post.metadata?.canonical
}
};
}Explicación del código
WithContext<BlogPosting>: Proporciona el tipado necesario deschema-dts, incluyendo el campo@context.headlineydescription: Son campos críticos para que Google entienda el resumen del artículo.datePublished/dateModified: Ayudan a Google a saber si el contenido es fresco o ha sido actualizado recientemente.
Post type
Para referencia puedes ver las variables dentro del tipo Post.
export interface Post {
id: string;
publishDate: Date;
updateDate?: Date;
title: string;
excerpt?: string;
image?: string;
metadata?: MetaData;
}Integración Final en la Página
Finalmente, juntamos todo en la página del blog:
---
import type { InferGetStaticPropsType, GetStaticPaths } from 'astro';
import merge from 'lodash.merge';
import type { ImageMetadata } from 'astro';
import { createArticleJsonLd } from '~/ld-json/utils';
export const getStaticPaths = (async () => {
return await getStaticPathsBlogPost();
}) satisfies GetStaticPaths;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { post } = Astro.props as Props;
const postPath = getBlogPermalink(post.permalink, language);
const url = getCanonical(getPermalink(postPath, 'post'));
const image = (await findImage(post.image)) as ImageMetadata | string | undefined;
const metadata = merge(
{
title: post.title,
description: post.excerpt,
robots: {
index: blogPostRobots?.index,
follow: blogPostRobots?.follow,
},
openGraph: {
type: 'article',
...(image
? { images: [{ url: image, width: (image as ImageMetadata)?.width, height: (image as ImageMetadata)?.height }] }
: {}),
},
},
{ ...(post?.metadata ? { ...post.metadata, canonical: post.metadata?.canonical || url } : {}) }
) as MetaData;
const structuredData = createArticleJsonLd(post, language);
---
<PageLayout metadata={metadata} structuredData={structuredData}, >
<SinglePost post={{ ...post, image: image }} url={url}>
{post.Content ? <post.Content /> : <Fragment set:html={post.content || ''} />}
</SinglePost>
</PageLayout>Explicación del código
Astro tiene en este momento el archivo del post cargado para construirlo mediante SSG(Server Side Generation). El objetivo es usar los datos que necesites en ese momento. Este es el momento adecuado para construir toda el contenido SEO y pasarlo a otro Layout con la estructura del documento.
metadata: Se pasa al Layout que estés usando.structuredData: Se pasa al Layout que estés usando.
Importante
Nunca olvides que el JSON-LD debe ser inyectado como un string JSON válido dentro de una etiqueta script. Astro permite hacer esto de forma segura con la directiva set:html.
Resumen de la Arquitectura SEO
- Layout: Recibe metadatos y datos estructurados.
- Metadata.astro: Procesa y renderiza las etiquetas meta tradicionales.
- JSON-LD: Mejora la visibilidad con fragmentos enriquecidos (Rich Snippets).
- TypeScript: Asegura que toda la cadena de datos sea válida y coherente.
Fuentes consultadas
- Astro
- SEO
- TypeScript