# Prompt-to-PR: Build a Static SEO Site with Astro

> Complete SOP for scaffolding a content-driven static SEO site with Astro, Tailwind, MDX content collections, sitemap, and canonical tags from scratch.

**Type:** Playbook  
**Tools:** Cursor, Claude Code, Codex, Windsurf  
**Stack:** Astro, TypeScript, Tailwind  
**Difficulty:** medium  
**Updated:** 2026-06-08

---

Scaffold a production-ready, fully static Astro 5 site optimized for organic search: content collections, Tailwind typography, `@astrojs/sitemap`, and correct `<head>` metadata wired up from day one.

## 1. Requirement

Build a static marketing/content site that scores 100 on Lighthouse SEO and Accessibility. Content is authored in MDX files managed by Astro content collections. No JavaScript framework needed — output is pure HTML+CSS with opt-in islands.

## 2. First Prompt

```txt title="First Prompt"
Scaffold a new Astro 5 static SEO site from scratch in the current directory.

Requirements:
1. Init with: `bunx create astro@latest . --template minimal --typescript strict --no-git`
2. Add integrations:
   - `@astrojs/tailwind` with `@tailwindcss/typography`
   - `@astrojs/sitemap`
   - `@astrojs/mdx`
3. Create a content collection `blog` in `src/content/blog/` with this Zod schema:
   title, description, pubDate (Date), updatedDate (Date, optional),
   author (string, default "Admin"), tags (string[], default []), draft (bool, default false).
4. Create a BaseLayout.astro with:
   - A `<head>` block: charset, viewport, canonical (`Astro.url.href`),
     og:title, og:description, og:url, og:type, twitter:card.
   - Accept `title`, `description`, `image` props.
5. Create pages:
   - `/` — hero + last 6 non-draft posts
   - `/blog` — paginated list (10 per page) using `paginate()`
   - `/blog/[slug]` — single post rendered with `<Content />`
   - `/tags/[tag]` — posts filtered by tag
6. Create `src/content/blog/hello-world.mdx` as a real sample post.
7. Configure `astro.config.ts`:
   - `site: process.env.SITE_URL ?? "http://localhost:4321"`
   - `integrations: [tailwind(), sitemap(), mdx()]`
   - `output: "static"`
8. Add a `.env.example` with `SITE_URL=https://example.com`.
```

## 3. Expected File Changes

```txt
astro.config.ts
tailwind.config.ts
src/content.config.ts                    (blog collection schema)
src/layouts/BaseLayout.astro             (head + canonical + OG tags)
src/pages/index.astro
src/pages/blog/index.astro               (paginated)
src/pages/blog/[slug].astro
src/pages/tags/[tag].astro
src/content/blog/hello-world.mdx
.env.example
package.json                             (updated with all integrations)
```

## 4. Review Checklist

- `astro.config.ts` sets `site` — required for `@astrojs/sitemap` to emit absolute URLs.
- `BaseLayout.astro` outputs a `<link rel="canonical">` using `Astro.url.href`.
- Blog list page uses `Astro.props.page.data` from `paginate()` — not a raw `getCollection()` call.
- Draft posts (`draft: true`) are excluded in production via `import.meta.env.PROD` guard.
- `<html lang="en">` is set on the root element.
- `tailwind.config.ts` includes the `typography` plugin and targets `src/**/*.{astro,mdx}`.
- `sitemap()` integration is present — verify `dist/sitemap-index.xml` exists after `bun run build`.

## 5. Test Commands

```bash
bun install
bun run dev
# visit http://localhost:4321 and confirm hero + posts render

bun run build
# expect zero errors

bun run preview
# check /sitemap-index.xml and /sitemap-0.xml exist
# check /blog and /blog/hello-world render with correct <title> and canonical

# Lighthouse CLI smoke test (optional)
bunx lighthouse http://localhost:4321 --output json --quiet | jq '.categories.seo.score'
# expect 1 (100%)
```

## 6. Common Failures

- **Sitemap generates relative URLs** — `site` missing from `astro.config.ts`. Add it.
- **`getCollection` returns drafts in production** — filter: `posts.filter(p => !p.data.draft || !import.meta.env.PROD)`.
- **Pagination 404 on `/blog`** — first page must be `/blog/` (index), not `/blog/1`. Astro's `paginate()` outputs `/blog/`, `/blog/2/`, etc. by default.
- **MDX content not styled** — `@tailwindcss/typography` `prose` class missing on the article wrapper in `[slug].astro`.
- **OG tags use relative image path** — must be an absolute URL. Prefix with `Astro.site`.

## 7. Fix Prompt

```txt title="Fix Prompt"
The sitemap at /sitemap-0.xml contains relative paths like "/blog/hello-world"
instead of absolute URLs like "https://example.com/blog/hello-world".

Fix: add `site: "https://example.com"` (or `process.env.SITE_URL`) to the
top-level astro.config.ts object. The sitemap integration reads this value
to prefix all URLs.
```

## 8. PR Description

```md title="PR description"
## Init: Static Astro 5 SEO site

- Astro 5 + TypeScript strict + Tailwind (with `@tailwindcss/typography`)
- `@astrojs/sitemap` and `@astrojs/mdx` integrations
- Content collection `blog` with Zod schema (title, description, pubDate, tags, draft)
- BaseLayout with canonical, OG, and Twitter Card meta tags
- Pages: home, paginated blog list, single post, tag archive
- Sample `hello-world.mdx` post
- `bun run build` emits `sitemap-index.xml` and all static pages
```