# Set Up SEO

> Use when a public page must be found — search and answer-engine discoverability for landing, marketing, and content pages. The crawlability gate (content present in served HTML), per-page metadata (title, description, canonical, Open Graph), JSON-LD structured data by page type, sitemap.xml + robots.txt, and answer-engine-readable content structure. Framework-agnostic — audits built HTML from any stack.

- Skill: `velimirmueller/set-up-seo` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add velimirmueller/set-up-seo`
- Raw SKILL.md: https://api.skillmd.com/api/skills/velimirmueller/set-up-seo/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: VelimirMueller (https://skillmd.com/u/velimirmueller)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/velimirmueller/set-up-seo

---


# Set Up SEO

Discoverability as a checked property, not a vibe. Audit the *served* HTML, fix in
order: crawlability → metadata → structured data → sitemap/robots → answerability.

## 1. Gate — is this a public page?

Run the gate from `../_shared/page-types.md`. App surfaces behind auth: stop, wrong
catalogue. For this skill the gate doubles as the first audit: if the main content fails
the view-source test, **that is finding #1** and everything else waits —
see `./crawlability.md`.

## 2. Audit current state

```bash
curl -s "$URL" -o /tmp/page.html
grep -oiE "<title>[^<]*</title>" /tmp/page.html
grep -ciE "<meta name=\"description\"" /tmp/page.html
grep -ciE "rel=\"canonical\"" /tmp/page.html
grep -ciE "property=\"og:(title|description|image)\"" /tmp/page.html
grep -ciE "application/ld\+json" /tmp/page.html
curl -s "$ORIGIN/robots.txt"; curl -sI "$ORIGIN/sitemap.xml" | head -1
```

Audit each *template* (home, article, product), not just one URL. Apply only what's
missing; a conforming page is a no-op.
(The `-c` counters count matching *lines*, not tags — minified heads collapse to 1 — and
the patterns assume double-quoted attributes; treat them as presence checks, not totals.)

## 3. Metadata per page

- **`<title>`** — unique per page, the page's promise, ~50–60 chars, brand last
  ("Outcome the visitor gets — Brand").
- **`<meta name="description">`** — the snippet pitch, ~150–160 chars, contains the
  page's one job.
- **`<link rel="canonical">`** — absolute URL, on every public page (self-referencing is
  correct); one canonical per duplicate-reachable content.
- **Open Graph / Twitter** — `og:title`, `og:description`, `og:image` (1200×630),
  `twitter:card` — this is what the link looks like when shared.
- All of it must be in the **served** HTML (step 1), not injected client-side.

## 4. Structured data (JSON-LD)

Pick types from the table in `./structured-data.md`; the hard rule is **schema mirrors
visible content, never invents it**. Example for an article page:

```html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Exactly the visible h1",
  "description": "Matches the meta description",
  "datePublished": "2026-05-01",
  "dateModified": "2026-06-10",
  "author": { "@type": "Person", "name": "The visible byline" }
}
</script>
```

`FAQPage` only for real, visible FAQ sections — its rich result is gone (May 2026), the
markup still serves answer engines (`./structured-data.md`). Never `HowTo` (deprecated
rich result).

## 5. sitemap.xml + robots.txt

```
# robots.txt — at the origin root
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
```

- Sitemap: every *canonical, public* URL — no redirects, no noindexed pages, no params.
  Generate at build; `lastmod` only if it's real.
- Submit once via Search Console (the old ping endpoints are retired); after that the
  `Sitemap:` line is discovery enough.
- **`Disallow` is not `noindex`** — to de-index, serve `<meta name="robots"
  content="noindex">` on a crawlable page (`./crawlability.md`).

## 6. Answer-engine readability

Answer engines quote pages that answer cleanly. The moves are content structure, not
tags:

- A section heading phrased as the question users ask, followed **immediately** by a
  direct answer in 2–3 sentences — elaboration after, never before.
- Visible **author**, **published/updated dates**, and (for health/finance/legal topics)
  a named reviewer — trust signals machines and humans share.
- Enumerable facts as lists or tables — extractable beats prose.
- One idea per paragraph; the page answers its `<title>`'s promise.

This is the generic core of GEO/AEO; project-specific scoring belongs in a rubric
(`../audit-content-quality/SKILL.md`).

## 7. Verify

```bash
curl -s "$URL" | grep -c "ld+json"        # ≥1
curl -s "$ORIGIN/robots.txt" | grep -ci "^Sitemap:"   # 1
```

Paste the URL into `validator.schema.org` (0 errors) and the Rich Results Test. Fetch
with JS disabled: title, description, content all present. Share the URL in a chat
client: the OG preview renders.

## References
- ./crawlability.md — the view-source test, the empty-shell symptom, Disallow vs noindex, remediation pointers.
- ./structured-data.md — type table, the mirror rule, the FAQ caveat, validation.
- ../_shared/page-types.md — the gate and the priority inversion.
- ../audit-content-quality/SKILL.md — rubric-driven scoring on top of this baseline.
- ../../frontend/set-up-document-head/SKILL.md — in-app (SPA) head management; it handles tabs and a11y, not crawlability.

