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")} />)}// Global site metadata. Every page's SEO defaults derive from here —
// change these once and the whole template stays consistent.
export const SITE_NAME = "Acme Widgets";
export const SITE_URL = "https://acmewidgets.com";
// 120-160 character summary used as the meta description and social
// preview fallback for pages that don't provide their own.
export const DEFAULT_DESCRIPTION = "Hand-built widgets, shipped worldwide.";
// Fallback image used for Open Graph / Twitter cards when a page doesn't
// provide its own. Must be an absolute-from-root path in `public/`,
// 1200x630 PNG/JPG — SVG is not reliably rendered by social crawlers.
export const DEFAULT_OG_IMAGE = "/og-default.png";
// Used for Twitter/X card attribution. Set to "" to omit the tag.
export const TWITTER_HANDLE = "@acmewidgets";
export const THEME_COLOR = "#f97316";
// Supported languages. The first locale in the URL path (e.g. /en/, /es/)
// determines the page's translations.
export const DEFAULT_LOCALE = "en";
export const LOCALES = ["en", "es"] as const;
export type Locale = (typeof LOCALES)[number];
// Per-locale metadata used for the <html lang>, hreflang, and og:locale tags.
// og:locale entries without a region in the source code (e.g. "en" rather than
// "en-US") are guessed as "<lang>_<LANG>" — adjust any that don't match your
// actual target region.
export const LOCALE_DETAILS: Record<Locale, { label: string; htmlLang: string; ogLocale: string }> =
{
"en": { label: "English", htmlLang: "en", ogLocale: "en_EN" },
"es": { label: "Español", htmlLang: "es", ogLocale: "es_ES" },
};// Default Organization + WebSite JSON-LD, generated from your site's name
// and URL. Import this on your homepage and pass it to <Seo structuredData={defaultStructuredData} />.
// Add page-specific schema (Article, Product, FAQ...) the same way on other pages.
import { SITE_NAME, SITE_URL } from "./consts";
export const defaultStructuredData = [
{
"@context": "https://schema.org",
"@type": "Organization",
name: SITE_NAME,
url: SITE_URL,
},
{
"@context": "https://schema.org",
"@type": "WebSite",
name: SITE_NAME,
url: SITE_URL,
},
];# Finish integrating this SEO preset — and take it to a perfect SEO score
These files were generated by SEO Preset for **Acme Widgets** (`https://acmewidgets.com`), targeting
**Astro**, and have already been placed into this project — that happened before this
prompt was run, so don't go looking for a zip or copy anything in from elsewhere. You have three
jobs here: (1) wire these files in correctly (render the component, link the favicons, none of
which happens automatically just because the files exist), (2) adapt the structured data to what
this project's pages actually are — the integration checklist below has a type-by-type table for
this, and it's the step that actually finishes the SEO rather than just installing it — and
(3) use the checklist at the end to close any remaining gaps elsewhere in the project so this site
is genuinely optimized for search, not just "has some meta tags".
## How each file works
### `src/components/Seo.astro`
A `<head>`-only component — no `<html>`/`<body>`, so it drops into any layout. Render it once per page and forward page-specific props. It emits, in order: the `<title>` and meta description; the canonical `<link>` (defaults to `Astro.site` + the current path, or pass `canonical` to override); the `robots` meta (`noindex` prop to exclude a page from search entirely); hreflang `<link rel="alternate">` tags for every locale in `LOCALES`, plus an `x-default`, so Google serves the right language variant instead of guessing; the full Open Graph set (`og:type`, `og:site_name`, `og:title`, `og:description`, `og:url` (the same canonical URL), `og:image` resolved to an absolute URL, `og:image:alt`, `og:locale`); the Twitter Card tags; and, when a page passes `structuredData`, one `<script type="application/ld+json">` per schema object. `type="article"` unlocks `article:published_time` / `article:modified_time` / `article:author` — use it for blog posts.
### `src/consts.ts`
The single source of truth every site-wide SEO default across this integration reads from (not all of these are used by `Seo.astro` itself — some feed the favicon `<meta name="theme-color">` tag or `astro.config.mjs`'s `site` instead): `SITE_NAME`, `SITE_URL`, `DEFAULT_DESCRIPTION`, `DEFAULT_OG_IMAGE`, `TWITTER_HANDLE`, `THEME_COLOR`, plus `DEFAULT_LOCALE`, `LOCALES`, and `LOCALE_DETAILS` (label/htmlLang/ogLocale per locale). Changing site-wide metadata means editing this one file, not hunting through every page.
### `src/structuredData.ts`
Exports `defaultStructuredData`: an `Organization` + `WebSite` JSON-LD pair built from `SITE_NAME`/`SITE_URL`. This is what makes your brand name, logo, and sitelinks search box eligible in Google results — it has to load on the homepage to count. Every other page needs its own schema matching its real content instead of reusing this pair — see the integration checklist below for exactly which type to add where. Schema that doesn't match visible content violates Google's structured data guidelines and can trigger a manual action.
### `public/` (favicons)
`favicon.ico` (multi-resolution, legacy fallback), `favicon.svg` (modern browsers), `favicon-16x16.png` / `favicon-32x32.png` (browser chrome/bookmarks), `apple-touch-icon.png` (180x180, iOS home screen), `icon-192.png` / `icon-512.png` (Android/PWA via the manifest), and `site.webmanifest` referencing the two PWA icons plus `theme_color`/`name`. A missing or generic favicon is a small but real trust signal search results are missing without it.
## Integration checklist
1. `src/components/Seo.astro`, `src/consts.ts`, and `src/structuredData.ts` are already in this project — if any of them collided with a file of the same name that already existed here, confirm the two got merged rather than one silently overwriting the other before continuing.
2. The favicon files are already under `public/` — they just aren't linked from `<head>` yet. In the `<head>` of the main layout, add:
```html
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#f97316" />
```
3. Render `<Seo {...props} />` inside that same `<head>`, forwarding a `title` (required) and any of the optional props documented in `Seo.astro`.
4. On the homepage, import `defaultStructuredData` from `src/structuredData.ts` and pass it as the `structuredData` prop: `<Seo title="..." structuredData={defaultStructuredData} />`.
5. **Adapt the structured data to what this project actually is — this is the step that finishes the SEO, don't skip it.** `defaultStructuredData` (Organization + WebSite) belongs on the homepage, but every other page needs JSON-LD that matches its real content, built from data already on the page — never invent prices, dates, ratings, or addresses. Go through the project's actual pages and match each one against the closest fit:
- **Event** (concert, webinar, meetup, conference, ticketed page): `Event` — `startDate`, `endDate`, `location` (a `Place` with an `address`), `offers` (price/availability), `performer`/`organizer`.
- **Product / e-commerce listing**: `Product` — `image`, `brand`, `offers` (`price`, `priceCurrency`, `availability`), plus `aggregateRating`/`review` if real reviews exist on the page.
- **Blog post / news article**: `Article` (or `NewsArticle`/`BlogPosting`) — `headline`, `image`, `datePublished`, `dateModified`, `author`. Also pass `type="article"` to `<Seo>` on these pages so the article-specific Open Graph tags emit alongside it.
- **Physical business location** (store, restaurant, clinic, office you can visit): `LocalBusiness` (or a more specific subtype) — `address`, `telephone`, `openingHoursSpecification`, `geo`.
- **Recipe**: `Recipe` — `recipeIngredient`, `recipeInstructions`, `cookTime`, `nutrition`.
- **FAQ content**: `FAQPage` — `mainEntity` as a list of `Question`/`acceptedAnswer` pairs.
- **Job listing**: `JobPosting` — `hiringOrganization`, `jobLocation`, `datePosted`, `validThrough`.
- **Software product / app**: `SoftwareApplication` — `applicationCategory`, `offers`, `operatingSystem`.
- Any page with visible breadcrumb navigation: add a `BreadcrumbList` alongside its main type.
- None of these fit? Check https://schema.org/docs/full.html for the closest real type instead of leaving Organization/WebSite on a page it doesn't describe.
`<Seo>`'s `type` prop only distinguishes `website`/`article` for Open Graph purposes — that's expected, since rich results for Event/Product/etc. come from the JSON-LD above, not from `og:type`.
6. Set `site` in `astro.config.mjs` to the real production URL — `Seo.astro` needs `Astro.site` to resolve absolute canonical/OG/Twitter image URLs.
7. **Read every page in this project and write its real `<Seo>`'s props — don't leave pages on the defaults.** Every page currently renders with `DEFAULT_DESCRIPTION` and `DEFAULT_OG_IMAGE` unless you override them, which is a safe fallback, not a finished result. For each page/route:
- Open the page and read its actual heading, copy, and any product/event/article data it renders — then write a `title` (50-60 characters) and `description` (120-160 characters) specific to that content. **Hard limits: title ≤70 characters, description ≤160 characters** — count the actual characters before moving on, don't estimate. Google truncates past these lengths, and titles like "Home" or descriptions copy-pasted between pages are one of the most common real-world SEO issues.
- Pass a page-specific `image`/`imageAlt` whenever the page has its own hero, product photo, or cover image — reuse `DEFAULT_OG_IMAGE` only for pages that genuinely don't have one of their own, not as a shortcut.
- Set `noindex` on pages that shouldn't rank (thank-you/confirmation screens, internal search results, auth/admin pages, duplicate filtered views of the same content).
- On article/blog pages, set `publishedTime` (and `modifiedTime` when the page shows an updated date) and `author` from the page's real byline/date, not placeholder values.
- Do this per locale, not just for the default one — a translated page that keeps the source language's title/description is worse for that audience than no translation.
8. This project uses multiple locales (en, es). Make sure your Astro config's `i18n.locales` matches the `LOCALES` array in `src/consts.ts`, and that `Astro.currentLocale` resolves correctly on every page — `Seo.astro` reads it to build hreflang tags and pick `og:locale`.
## SEO checklist for a 100/100 score
Go through this against the actual project, not just the files above — most of it lives outside
this package (page content, layout, images, build config):
- **Semantic structure**: exactly one `<h1>` per page, headings nest in order (no skipping from `h2` to `h4`), and landmarks are real (`<header>`, `<nav>`, `<main>`, `<footer>`) — not `<div>` soup. This is what both crawlers and screen readers rely on to understand the page.
- **Crawlability**: every important page is reachable through a real `<a href>` (not a JS-only `onClick`), anchor text is descriptive (not "click here"), and there's a `sitemap.xml` referenced from `robots.txt` — `@astrojs/sitemap` handles this in Astro with near-zero config.
- **Images**: every `<img>` has meaningful `alt` text (empty `alt=""` only for purely decorative images), explicit `width`/`height` (or `aspect-ratio`) to avoid layout shift, and modern formats (WebP/AVIF) with `loading="lazy"` on anything below the fold.
- **Core Web Vitals**: minimize render-blocking CSS/JS, avoid layout shift from late-loading fonts/ads/embeds (`font-display: swap`, reserved space), and keep the largest above-the-fold element (LCP) loading fast.
- **One canonical URL per piece of content**: no duplicate-content traps from trailing slashes, `?query` params, or `www` vs. non-`www` all resolving separately without a canonical tag pointing at one true URL.
- **HTTPS everywhere**, no mixed-content warnings, and no broken internal links (404s) or redirect chains longer than one hop.
- **Structured data actually validates** — run the final pages through Google's Rich Results Test and Schema.org validator, not just "it compiles".
- **Mobile-first**: verify tap targets, font sizes, and layout actually work on a real small screen, since Google indexes the mobile version of the page.
- **`lang` and hreflang match reality**: the `<html lang>` attribute and any hreflang tags describe the language actually rendered on that page, not just the URL prefix.
- **Structured data matches content**: every page's JSON-LD reflects what it actually is — Event, Product, Article, etc. from the table above — not `defaultStructuredData` copy-pasted onto pages it doesn't describe.
- **Meta tags are page-specific**: every page has its own `title`, `description`, and (where relevant) `image` derived from its real content, not left on `DEFAULT_DESCRIPTION` / `DEFAULT_OG_IMAGE` by default.
## Final self-check — report a score, not just "done"
Once every step above is finished, re-open the actual pages you changed — don't grade from memory
of what you intended to do — and audit them against every item in the checklist directly above.
Then report back, in this order:
1. **A percentage score**: (checks that are true right now) ÷ (total checks in the list above) ×
100, rounded to the nearest whole number. A check either passes or it doesn't — no partial
credit for "did it on some pages."
2. **A table of every failing or partial check, sorted worst first**, colored by how much it
actually costs you in search — plain markdown can't color table cells, so use these emoji as the
color signal instead of inventing your own scale:
- 🔴 **Critical** — blocks indexing or rich results outright: missing/duplicate `title`, no
canonical, a page that should be indexed carrying `noindex` (or vice versa), structured data
missing a field Google requires for that type, a page that isn't crawlable at all.
- 🟠 **Moderate** — page still gets indexed but under-performs: generic/duplicate `description`,
missing OG image, structured data present but incomplete on optional-but-valuable fields.
- 🟡 **Minor** — polish, not a visibility blocker: missing `modifiedTime`, a title a few
characters outside the ideal range, non-blocking accessibility nits.
Skip the table entirely only if the score is 100%:
| Severity | Check | Status | Where | What's missing |
|---|---|---|---|---|
| 🔴 Critical | Unique title/description per page | Fail | `/about`, `/contact` | Still on `DEFAULT_DESCRIPTION` |
| 🟠 Moderate | Structured data matches content | Partial | `/events/*` | `Event` schema added, missing `offers` |
| 🟡 Minor | Article dates present | Partial | `/blog/*` | Missing `modifiedTime` |
(That table is the shape to use, not a preview of the answer — fill it with what you actually
found on this project, sorted 🔴 before 🟠 before 🟡.)
3. Some checks — Lighthouse/PageSpeed score, Google's Rich Results Test, real mobile rendering —
only make sense once this is deployed. List those separately as "can't verify pre-deploy" and
leave them out of the percentage rather than guessing at a pass.
Don't round up and don't call it done at anything less than 100% — a lower number here means real
work is still open, not that the number needs softening.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.