Saltar al contenido

· · SEO  · 8 min read

La Configuración de SEO en Astro que la Mayoría de los Portafolios Omite

Un title tag y una meta description no son una configuración de SEO — son la parte que cualquier template ya te regala. La plomería que se acumula (JSON-LD, canonicals que no dan 404, hreflang que no se te revierte, un sitemap limpio) es lo que los portafolios omiten. Aquí está mi configuración completa de SEO en Astro, con código real incluido.

Un title tag y una meta description no son una configuración de SEO — son la parte que cualquier template ya te regala. La plomería que se acumula (JSON-LD, canonicals que no dan 404, hreflang que no se te revierte, un sitemap limpio) es lo que los portafolios omiten. Aquí está mi configuración completa de SEO en Astro, con código real incluido.

Casi todos los templates de portafolio y blog en Astro vienen con “SEO”. Abres uno y encuentras un <title>, una meta description y una imagen de Open Graph. Eso es real y está bien — pero también es la parte que te dan gratis. No es una configuración; es un valor por defecto.

La configuración de SEO en Astro que de verdad se acumula mes con mes es la plomería de abajo: datos estructurados para que los buscadores entiendan qué es cada página, canonicals que no apuntan a un 404, hreflang que no se te revierte silenciosamente en un sitio multilingüe, un sitemap que solo anuncia las páginas que de verdad quieres indexadas, y noindex en las páginas delgadas que de otro modo te diluyen. Nada de esto es difícil. Todo se omite. Aquí está completo, con código real de este sitio — y dos errores de un solo carácter que yo mismo cometí y que pueden desindexar un post sin un solo error visible.

1. Datos estructurados: dile a los buscadores qué es la página

Un buscador puede leer tu HTML, pero los datos estructurados (JSON-LD siguiendo schema.org) se lo dicen, sin ambigüedad: esto es una Organization, esto es un blog post, este es el autor, esta es la fecha de publicación. Eso es lo que habilita los rich results y lo que hace que tu sitio se entienda como una entidad y no como una bolsa de texto.

La mayoría de los templates no traen nada de esto. Así es como cada post de este sitio emite un nodo BlogPosting:

---
// src/components/common/StructuredData.astro (BlogPosting branch)
structuredData = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.title,
  description: post.excerpt || '',
  image: postImage,
  datePublished: post.publishDate.toISOString(),
  dateModified: post.updateDate?.toISOString() || post.publishDate.toISOString(),
  author: { '@type': 'Person', name: post.author, url: `${siteUrl}/about` },
  publisher: {
    '@type': 'Organization',
    name: siteName,
    logo: { '@type': 'ImageObject', url: `${siteUrl}/favicon.ico` },
  },
  mainEntityOfPage: { '@type': 'WebPage', '@id': postUrl },
  url: postUrl,
};
---
{Object.keys(structuredData).length > 0 && (
  <script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
)}

El home emite un nodo Organization (tu logo, tus perfiles sociales vía sameAs) y un nodo WebSite con un SearchAction — lo que te puede dar un sitelinks search box. Las páginas de post agregan un BreadcrumbList para que el SERP muestre tu jerarquía en vez de una URL cruda.

Dos reglas que evitan que esto se vuelva un problema:

  • dateModified tiene que ser honesto. Volver a datePublished cuando el post no se ha actualizado (como en el código de arriba) es lo correcto. Fingir un dateModified fresco en cada deploy para aparentar “actualizado recientemente” es un patrón que Google lleva años aprendiendo a ignorar.
  • Valídalo una vez. Pega una URL en vivo en el Rich Results Test de Google. Un nodo JSON-LD mal formado es peor que no tener ninguno — puede suprimir el rich result por completo.

2. Canonicals: el typo de un solo carácter que desindexa un post

Una URL canónica le dice a los buscadores “esta es la dirección real de esta página; consolida aquí todas las señales de ranking”. Toda página debería llevar una, y en la mayoría de los casos debería apuntar a sí misma:

---
// src/components/common/Metadata.astro
const {
  canonical = String(getCanonical(String(Astro.url.pathname))),
} = Astro.props;
---
<AstroSeo canonical={canonical} ... />

Derivar el canonical de la URL real — en vez de dejar que los autores hardcodeen uno por post — es el default seguro, porque el modo de falla de un canonical hardcodeado es brutal. Una vez publiqué un post cuyo canonical en el front-matter tenía un typo de una sola letra: la-mejore-forma en vez de la-mejor-forma. La página estaba viva y funcionaba bien, pero su canonical apuntaba a una URL que no existía. Para Google, ese post estaba declarando “la versión real de mí está allá” — en un 404. Eso es una vía rápida para que te saquen del índice, y nada en el build te avisa.

La regla: deja que el canonical tome por defecto la URL propia de la página, y solo sobrescríbelo deliberadamente — para un cross-post sindicado, apúntalo de vuelta al original. Si vas a hardcodear uno, trátalo como una URL de producción, porque lo es.

3. hreflang: en un sitio multilingüe, la etiqueta equivocada es peor que no tener ninguna

Si sirves más de un idioma, hreflang le dice a Google qué versión mostrarle a qué usuario. Este sitio es principalmente en español con contenido en inglés bajo /en/, así que cada página anuncia sus alternativas más un x-default:

---
// src/components/common/Metadata.astro
const alternateUrls = getAlternateUrls(Astro.url);
---
{alternateUrls.map(({ locale, url }) => (
  <link rel="alternate" hreflang={locale === 'es' ? 'es' : 'en'} href={url} />
))}
<link rel="alternate" hreflang="x-default"
  href={alternateUrls.find((a) => a.locale === 'es')?.url || canonical} />

Aquí está la trampa en la que caí, y es sutil: un post que solo existe en inglés no tiene versión en español. Si su hreflang seguía anunciando una alternativa es, esa alternativa apuntaría a una URL que da 404 — y un cluster de hreflang roto hace que se ignore el conjunto completo. Así que el contenido que existe en un solo idioma tiene que auto-referenciarse únicamente:

// content that exists in exactly one language advertises only itself
if (isEnglishOnly(url.pathname)) {
  return [{ locale: 'en', url: new URL(url.pathname, url.origin).toString() }];
}

La regla general: hreflang debe ser recíproco y cada URL que contiene debe devolver 200. Una alternativa que apunta a un 404 no solo falla para esa página — puede invalidar las anotaciones de todas las páginas a las que hace referencia. Si no puedes garantizar que la URL del otro idioma existe, no la anuncies.

4. Higiene del sitemap: audita lo que realmente estás anunciando

Generar un sitemap es lo mínimo indispensable — en Astro es una sola integración:

// astro.config.mjs
import sitemap from '@astrojs/sitemap';
export default defineConfig({
  site: 'https://heroweb.dev', // required, or URLs come out relative and useless
  integrations: [sitemap()],
});

La parte que la gente se salta es leer su propio sitemap. Un sitemap es una lista de páginas que le estás pidiendo activamente a Google que rastree e indexe — así que cualquier cosa ahí adentro que no quieras indexada es un problema de calidad autoinfligido. Al auditar el mío encontré una página legal de relleno — texto boilerplate de “Nombre de la Empresa” con fecha de 2023 — que nunca había enlazado en la navegación pero que se seguía generando, y por lo tanto seguía apareciendo en sitemap-index.xml, invitando a Google a indexar contenido legal falso en mi dominio.

Abre https://tusitio.com/sitemap-index.xml (y los sitemaps hijos a los que apunta) y lee cada URL. Para todo lo que no debería rankear — páginas de relleno, rutas de utilidad delgadas, paginaciones duplicadas — elimina la página o ponle noindex, que nos lleva a la última pieza.

5. Pon noindex a lo delgado, a propósito

Más páginas indexadas no es mejor. Cincuenta páginas de archivo de tags casi vacías, cada una listando un solo post, son cincuenta páginas delgadas compitiendo con tu contenido real por presupuesto de rastreo y señales de calidad. Por eso este sitio indexa los archivos de categoría pero deliberadamente le pone noindex a los archivos de tags:

# src/config.yaml
apps:
  blog:
    tag:
      isEnabled: true      # tags still work for navigation
      robots:
        index: false       # but they don't get indexed

Los tags siguen siendo útiles para los lectores que navegan el sitio; simplemente no le piden a Google que rankee una docena de listas de un solo elemento. La misma lógica aplica a los resultados de búsqueda interna, a los archivos paginados /page/2 y a cualquier cosa autogenerada y delgada. Indexa lo que te daría orgullo que alguien encontrara en frío. Ponle noindex a lo demás.

6. RSS que apunta a casa

RSS no está muerto — alimenta lectores, newsletters y plataformas de sindicación, y cada una de esas es un lugar por donde viaja tu contenido. Lo único que importa para el SEO: cada ítem enlaza a su URL canónica, para que dondequiera que el feed se republique, la señal de ranking regrese a ti:

// src/pages/rss.xml.ts
const postUrl = getCanonical(addLocaleToPath(getPermalink(post.permalink, 'post'), locale));
return { link: postUrl, title: post.title, description: post.excerpt || '', pubDate: post.publishDate };

La lección real: el SEO de un sitio pequeño es plomería, no trucos

Nada de esto es un growth hack. No hay keyword-stuffing, ni esquema de enlaces, ni exploit ingenioso. Son cinco o seis piezas de plomería que cada una toma una tarde en conectarse correctamente — y después todo el juego se vuelve mantenerlas correctas. Los modos de falla no son dramáticos; son un canonical con typo, una página huérfana en el sitemap, un hreflang apuntando a un 404. Silenciosos, individualmente pequeños, y colectivamente la diferencia entre un sitio que se acumula y uno que se filtra en silencio.

Si estás en un theme estilo AstroWind, mucho de esto está a medias y vale la pena auditarlo en vez de asumirlo. Mi análisis a fondo del template AstroWind recorre cómo encajan sus capas de configuración, y si quieres dos historias concretas de un default del theme fallando en silencio, mira la trampa de Partytown que mató mis analytics y los breaking changes que me mordieron en la migración de Astro 5 a 6. Mismo theme, misma lección siempre: los defaults son un punto de partida, no una línea de meta.

Ve a leer tu propio sitemap. Toma cinco minutos, y podrías encontrar una página de términos de relleno invitando a Google a indexar texto que ni siquiera escribiste.

¿Te sirvió este artículo?

Recibe nuevos artículos sobre desarrollo web, IA y SEO directo en tu correo. Sin spam, cancela cuando quieras.

Sin spam. Al suscribirte aceptas la política de privacidad .

Volver al blog

Related Posts

View All Posts »
Cómo Valido un Post de Astro Antes de Publicarlo

Cómo Valido un Post de Astro Antes de Publicarlo

Publicar un post estático en Astro está a un merge de estar en vivo — justo por eso los bugs silenciosos son peligrosos. Un lang incorrecto, un canonical que da 404, un link entre idiomas que se filtró: nada de eso se ve en tu editor, solo en el HTML compilado. Aquí está el checklist que corro sobre dist/ antes de cada post, scripts incluidos.

Migración de Astro 5 a 6: los 3 cambios que me rompieron el build

Migración de Astro 5 a 6: los 3 cambios que me rompieron el build

La migración de Astro 5 a 6 se veía como un bump de seguridad de diez minutos. En vez de eso choqué con tres builds rotos, uno tras otro — todos en código del theme que nunca escribí. Aquí están los errores exactos, los fixes, y por qué un sitio con theme se rompe donde tu propio código no se rompería.

Cómo reduje 88% el peso de las imágenes de mi sitio (Astro + Sharp y WebP)

Cómo reduje 88% el peso de las imágenes de mi sitio (Astro + Sharp y WebP)

Un sitio estático en Astro con 15KB de JavaScript estaba enviando 3.4MB de imágenes. Estos son los tres modos de fallo que se colaron por el pipeline de imágenes — la regla de que el original siempre se publica, la captura que el optimizador nunca tocó y el material de plantilla que nadie referenciaba — y las 15 líneas de Sharp que lo arreglaron todo.