SEO Preset

Generador

Tu bloque de SEO, listo en un solo paso.

Rellena tu proyecto, elige una fuente, y descarga un componente listo para usar, todos los tamaños de favicon, y un prompt para terminar de integrarlo.

Qué vas a obtener

  • Meta tags, Open Graph y Twitter cards
  • Datos estructurados JSON-LD
  • Todos los tamaños de favicon, desde un solo icono
---
// Drop this in the <head> of any layout/page and pass per-page props.
// It covers the meta tags search engines and social platforms actually
// read: title, description, canonical, robots, Open Graph, Twitter Card,
// and JSON-LD structured data.

import { getAbsoluteLocaleUrl } from "astro:i18n";
import {
	DEFAULT_DESCRIPTION,
	DEFAULT_LOCALE,
	DEFAULT_OG_IMAGE,
	LOCALE_DETAILS,
	LOCALES,
	SITE_NAME,
	TWITTER_HANDLE,
	type Locale,
} from "../consts";

interface Props {
	/** Page title. Rendered as-is in <title> and og:title — compose the "| Site Name" suffix here if you want it. */
	title: string;
	/** 120-160 character summary used for the meta description and social previews. */
	description?: string;
	/** Overrides the auto-derived canonical URL (Astro.site + Astro.url.pathname). */
	canonical?: string | URL;
	/** Absolute-from-root or absolute URL. Falls back to DEFAULT_OG_IMAGE. */
	image?: string;
	/** Alt text for the social preview image. */
	imageAlt?: string;
	/** og:type. Use "article" for blog posts. */
	type?: "website" | "article";
	/** Excludes the page from search indexes (staging, thank-you pages, etc). */
	noindex?: boolean;
	/** ISO 8601 date strings, only emitted when type="article". */
	publishedTime?: string;
	modifiedTime?: string;
	/** Article author name, only emitted when type="article". */
	author?: string;
	/** Raw JSON-LD object(s) to embed as structured data. */
	structuredData?: Record<string, unknown> | Record<string, unknown>[];
}

const locale = (Astro.currentLocale as Locale | undefined) ?? DEFAULT_LOCALE;

const {
	title,
	description = DEFAULT_DESCRIPTION,
	canonical,
	image = DEFAULT_OG_IMAGE,
	imageAlt = `${SITE_NAME} preview image`,
	type = "website",
	noindex = false,
	publishedTime,
	modifiedTime,
	author,
	structuredData,
} = Astro.props;

const canonicalURL = canonical
	? new URL(canonical, Astro.site)
	: new URL(Astro.url.pathname, Astro.site);

const imageURL = new URL(image, Astro.site);

const schemas = structuredData
	? Array.isArray(structuredData)
		? structuredData
		: [structuredData]
	: [];

// hreflang alternates: the same path with each supported locale's prefix
// swapped in, plus an x-default pointing at the default locale.
const pathWithoutLocale = Astro.url.pathname.replace(new RegExp(`^/${locale}/?`), "/");
const alternateLocales = LOCALES.map((altLocale) => ({
	htmlLang: LOCALE_DETAILS[altLocale].htmlLang,
	href: getAbsoluteLocaleUrl(altLocale, pathWithoutLocale),
}));
---

<!-- Primary SEO -->
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<meta name="robots" content={noindex ? "noindex, nofollow" : "index, follow"} />

<!-- Language alternates -->
{
	alternateLocales.map(({ htmlLang, href }) => (
		<link rel="alternate" hreflang={htmlLang} href={href} />
	))
}
<link
	rel="alternate"
	hreflang="x-default"
	href={getAbsoluteLocaleUrl(DEFAULT_LOCALE, pathWithoutLocale)}
/>

<!-- Open Graph -->
<meta property="og:type" content={type} />
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:image" content={imageURL} />
<meta property="og:image:alt" content={imageAlt} />
<meta property="og:locale" content={LOCALE_DETAILS[locale].ogLocale} />

{
	type === "article" && publishedTime && (
		<meta property="article:published_time" content={publishedTime} />
	)
}
{
	type === "article" && modifiedTime && (
		<meta property="article:modified_time" content={modifiedTime} />
	)
}
{type === "article" && author && <meta property="article:author" content={author} />}

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageURL} />
{TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}

<!-- Structured data (JSON-LD) -->
{schemas.map((schema) => <script type="application/ld+json" set:html={JSON.stringify(schema).replace(/</g, "\\u003c")} />)}
Fuente
Lenguaje

Sin icono, el zip solo lleva el código. Añade uno para incluir también el favicon.ico y todos los tamaños PNG.

Preguntas frecuentes

¿Para qué sirve prompt.md?

No es solo documentación — es una lista de tareas para un asistente de IA (Claude, Cursor, etc.) que explica exactamente cómo integrar estos archivos en tus páginas reales, adaptar los datos estructurados a lo que cada página realmente es, y devolver un porcentaje de puntuación SEO.

¿Qué frameworks están soportados?

Astro, React, Next.js, Vue y Svelte. Elige uno en el selector de fuente de arriba — cada archivo generado sigue las convenciones reales de ese framework, no una plantilla genérica.

¿Qué lleva exactamente el zip?

La integración SEO para tu framework (un componente <Seo>, o pageMetadata() en Next), consts.ts, structuredData.ts, todos los tamaños de favicon si subiste un icono, y un prompt.md pensado para que un asistente de IA termine de integrarlo.

¿Qué pasa con el icono que subo?

Se rasteriza a todos los tamaños de favicon en el servidor y nunca se guarda — nada de él persiste una vez que se devuelve el zip (o la vista previa).

¿Puedo usarlo en un proyecto ya existente?

Sí. Añade los archivos generados y prompt.md le indica a la IA (o a ti) que compruebe y fusione cualquier archivo que coincida con uno que ya exista en tu proyecto, en vez de sobrescribirlo sin más.

¿Tengo que dar soporte a varios idiomas?

No — el soporte multi-idioma es opcional. Déjalo sin marcar y obtienes un montaje de un solo idioma, sin código de hreflang ni de locale.