SEO Preset

Generator

Your SEO block, done in one pass.

Fill in your project, pick a source, and download a ready-to-use component, every favicon size, and a prompt to finish the wiring.

What you'll get

  • Meta tags, Open Graph & Twitter cards
  • JSON-LD structured data
  • Every favicon size, built from one icon
---
// 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")} />)}
Source
Language

Skip it and the zip only has code. Add one to get favicon.ico and every PNG size too.

Frequently asked questions

What is prompt.md for?

It's not just documentation — it's a task list for an AI coding assistant (Claude, Cursor, etc.) that explains exactly how to wire these files into your real pages, adapt the structured data to what each page actually is, and report back a percentage SEO score.

Which frameworks are supported?

Astro, React, Next.js, Vue, and Svelte. Pick one in the source picker above — every generated file matches that framework's real conventions, not a generic template.

What's actually in the zip?

The SEO integration for your framework (a <Seo> component, or pageMetadata() for Next), consts.ts, structuredData.ts, every favicon size if you uploaded an icon, and a prompt.md written for an AI coding assistant to finish wiring it in.

What happens to the icon I upload?

It's rasterized into every favicon size on the server and never stored — nothing about it persists once the zip (or the live preview) is returned.

Can I use this on an existing project?

Yes. Drop the generated files in, and prompt.md tells the AI (or you) to check for and merge any file that collides with one already in your project, instead of silently overwriting it.

Do I have to support multiple languages?

No — multi-language support is opt-in. Leave it unchecked and you get a single-locale setup with no hreflang or locale code at all.