Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .claude/skills/seo-audit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
name: seo-audit
description: Audit and improve on-page SEO for web pages, docs sites, and marketing pages. Use whenever the user wants an SEO review or audit, wants to check or fix title tags / meta descriptions / Open Graph / Twitter cards / canonical URLs / structured data (JSON-LD) / robots / sitemap / heading structure / image alt text, asks "why isn't this page ranking / showing a preview card", wants to optimize a page for search or social sharing, or mentions SEO, meta tags, OG tags, rich results, or search snippets — even if they don't say the word "audit". Works on local source files (HTML, JSX/TSX, Markdown, Astro/Next/Vite) or a live URL. Not for off-page SEO (backlinks, ad campaigns) or keyword-volume research.
---

# On-page SEO audit

Review a page (or a set of pages) against on-page SEO best practices, report what's
wrong in priority order, and — when the user wants — fix it in the source files.

The point is not to produce a checklist for its own sake. Search engines and social
platforms read a page's `<head>` and semantic HTML to decide how to index it and how to
render its preview card. When those signals are missing or wrong, the page still "works"
for humans but is invisible or ugly to crawlers. Your job is to find that gap and close it.

## Decide the target first

Two modes — pick based on what the user gives you:

- **Source mode (default when working in a repo):** audit the files that produce the page.
This is what the user usually wants when the pages live in this repo, because the fix
lands in code they can commit. Find where `<head>` / metadata is set — this varies by
stack, so look before assuming:
- Static HTML → the `<head>` in the `.html` file.
- Next.js → `metadata` exports / `<Head>` / `generateMetadata` in `app/` or `pages/`.
- Astro → frontmatter + `<head>` in layout `.astro` files.
- Vite/plain SPA → `index.html` plus any runtime `document.title` / meta injection.
- Markdown docs (MkDocs, Docusaurus, VitePress, Jekyll) → frontmatter + the theme's
head template. Per-page `title`/`description` usually live in frontmatter.
Grep for `og:`, `twitter:`, `canonical`, `application/ld+json`, `<title`, `meta name="description"`
to locate every place metadata is set — there is often more than one.
- **Live mode:** the user gives a URL, or the rendering is dynamic and you need to see the
final DOM. Fetch it with `WebFetch`, or drive Chrome (the `mcp__claude-in-chrome__*`
tools) when the head is populated by client-side JS that `WebFetch` won't execute. Read
the rendered `<head>`. Note in the report that you audited rendered output, not source.

If the user hasn't said which, infer from context (a repo path → source; a URL → live) and
state which mode you chose in one line. Don't stall on the choice.

## What to check

Go through these. For each, report the current value (or "missing"), whether it's OK, and
what to change. Read `references/checklist.md` for the full detail on any item — thresholds,
common mistakes, and exactly what good looks like. The high-signal ones, roughly in order
of how often they matter:

1. **`<title>`** — present, unique per page, ~50–60 chars, front-loads the primary term.
2. **`<meta name="description">`** — present, ~120–160 chars, compelling, not duplicated
across pages. Missing/duplicate descriptions are the single most common real problem.
3. **Open Graph** — `og:title`, `og:description`, `og:image` (absolute URL, ~1200×630),
`og:url`, `og:type`. This is what LinkedIn/Slack/Facebook show. A missing `og:image`
is why a shared link looks blank.
4. **Twitter/X card** — `twitter:card` (usually `summary_large_image`), plus title/desc/image.
5. **Canonical** — `<link rel="canonical">` with an absolute URL, to avoid duplicate-content
splitting between www/non-www, trailing-slash, and query-param variants.
6. **Headings** — exactly one `<h1>`, no skipped levels (h1→h3), headings describe content
not styling.
7. **Image `alt`** — every meaningful `<img>` has descriptive alt text; decorative images
have empty `alt=""`.
8. **Structured data** — JSON-LD (`application/ld+json`) appropriate to the page type
(Article, Product, Organization, BreadcrumbList, FAQPage). Validate the shape.
9. **`robots`** — no accidental `noindex`/`nofollow` on pages that should rank; a sensible
`<meta name="robots">` where needed.
10. **Crawlability** — `robots.txt` and `sitemap.xml` exist and are referenced; internal
links use real `<a href>`; important content isn't hidden behind JS-only rendering.
11. **Technical hygiene** — `<html lang>` set, one viewport meta, HTTPS, no broken canonical
or OG URLs, reasonable page-load weight.

Don't invent problems to pad the report. If a page is in good shape, say so — a short
"these 9 things are correct, here are the 2 that aren't" is more useful than a wall of green
checkmarks.

## Report format

Lead with the fixes that matter. Use this shape:

```
# SEO audit — <page or URL> (<source|live> mode)

## Critical (breaks indexing or sharing)
- <issue> — <why it matters> — <exact fix, with file:line if source mode>

## Recommended (real improvement, not urgent)
- ...

## Passing
- <one line each for the checks that are already correct>
```

- **Critical** = the page won't be indexed correctly or its share card is broken (missing
title, `noindex` by accident, no og:image, broken canonical).
- **Recommended** = genuine improvements (thin description, missing JSON-LD, h1 issues).
- Every fix must be **specific**: the actual tag to add and where, not "improve your meta
description". In source mode, cite `file:line`. Prefer showing the exact snippet to paste.

## Applying fixes

Only edit files when the user asks you to fix things (or approves the audit's fixes). Then:

- Make the smallest change that resolves each issue; match the file's existing style and the
framework's idiom (e.g. Next's `metadata` export, not a raw `<head>` tag, in an app-router
project).
- Use **absolute** URLs for `og:image`, `og:url`, and `canonical` — relative URLs silently
break when the page is scraped off-domain. If you don't know the production origin, find it
(existing tags, `package.json` homepage, CNAME, config) or ask rather than guessing.
- After editing, re-run the relevant checks so the report reflects reality, and show a diff.

## Verifying

Recommend the user confirm share cards with the platform validators (these re-scrape the
live URL, so they only work after deploy): opengraph.xyz, LinkedIn Post Inspector,
X/Twitter Card Validator, and Google's Rich Results Test for structured data. For local
verification, re-read the rendered `<head>` (live mode) or re-grep the source (source mode).
```
```

Keep the audit proportional to the ask: one page → tight focused report; "audit the whole
site" → sample the templates that generate the pages (layout, per-type page components)
since fixing a template fixes every page it renders, and call out which pages inherit each fix.
140 changes: 140 additions & 0 deletions .claude/skills/seo-audit/references/checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# On-page SEO checklist — full detail

Read this when you need the precise threshold, the common failure mode, or a copy-paste
example for a given item. The SKILL.md has the summary; this has the specifics.

## Table of contents
1. Title tag
2. Meta description
3. Open Graph
4. Twitter / X card
5. Canonical URL
6. Headings
7. Image alt text
8. Structured data (JSON-LD)
9. Robots directives
10. Crawlability (robots.txt, sitemap, links)
11. Technical hygiene
12. Copy-paste starter block

---

## 1. Title tag
- **Good:** unique per page, ~50–60 characters (Google truncates around 580px ≈ 60 chars),
primary keyword/topic near the front, brand at the end (`Primary Topic — Brand`).
- **Bad:** same title on every page, empty, >70 chars (truncated in results), keyword-stuffed,
or set only via JS so crawlers that don't run JS miss it.
- Only one `<title>` per page. In SPAs, make sure the title is set before first paint or
server-rendered, not just updated on route change.

## 2. Meta description
- **Good:** 120–160 characters, reads like ad copy, describes what the page delivers, unique
per page, includes the term a searcher would use.
- **Bad:** missing (Google auto-generates a worse one), duplicated across pages (the most
common real SEO defect), truncated mid-word, or just the first sentence of body text.
- Not a ranking factor directly, but drives click-through from the results page, which is.

## 3. Open Graph (`og:`)
Required for a rich share card on LinkedIn, Facebook, Slack, Discord, iMessage:
- `og:title` — can differ from `<title>`; optimize for the share context.
- `og:description` — 2–4 sentences.
- `og:image` — **absolute** URL, ideally 1200×630 (1.91:1), under ~5MB, PNG/JPG. This is the
single most impactful tag for social — a missing or relative `og:image` is why a shared link
renders as a blank/text-only card.
- `og:url` — the canonical absolute URL of the page.
- `og:type` — `website` for home/landing, `article` for posts.
- Optional but nice: `og:site_name`, `og:image:alt`, `og:locale`.

## 4. Twitter / X card
- `twitter:card` — `summary_large_image` for a big image, `summary` for a small one.
- `twitter:title`, `twitter:description`, `twitter:image` — X falls back to OG tags for
most fields, so you often only need `twitter:card` if OG is complete. Verify rather than
assuming; add `twitter:image` explicitly if the OG image isn't picked up.
- Optional: `twitter:site` / `twitter:creator` (@handles).

## 5. Canonical URL
- `<link rel="canonical" href="https://example.com/page">` — absolute, self-referential on
the primary version of the page.
- Prevents duplicate-content dilution across `http/https`, `www/non-www`, trailing slash,
and tracking-param variants (`?utm_...`).
- **Bad:** canonical pointing to the wrong page, to a relative URL, to a `noindex` page, or
every page canonicalizing to the homepage (a classic accidental de-indexing).

## 6. Headings
- Exactly one `<h1>`, describing the page's main topic.
- Don't skip levels (h1 → h2 → h3, not h1 → h4). Screen readers and crawlers use the outline.
- Headings should describe content, not be chosen for their font size — use CSS for styling.

## 7. Image alt text
- Every meaningful `<img>` needs `alt` describing the image's content/function.
- Purely decorative images: `alt=""` (empty, not missing) so assistive tech skips them.
- **Bad:** `alt="image"`, `alt="logo logo logo"`, filename dumped as alt, or no alt attribute
at all (fails accessibility and loses image-search traffic).

## 8. Structured data (JSON-LD)
- Embed as `<script type="application/ld+json">` in the head or body.
- Match the schema.org type to the page: `Article`/`BlogPosting`, `Product` + `Offer`,
`Organization`, `WebSite` (+ `SearchAction` for a sitelinks search box), `BreadcrumbList`,
`FAQPage`, `HowTo`.
- Required fields vary by type — validate the shape (all required properties present, correct
nesting) and confirm it matches visible page content (Google penalizes mismatched markup).
- Verify with Google's Rich Results Test after deploy.

## 9. Robots directives
- Check no page that should rank carries `<meta name="robots" content="noindex">` or
`nofollow` by accident — a staging default that shipped to prod is a common cause of "my
pages vanished from Google."
- Use `noindex` deliberately on thank-you pages, filtered/faceted URLs, internal search results.
- `X-Robots-Tag` HTTP header can also set this — in live mode check response headers too.

## 10. Crawlability
- **robots.txt** at the domain root: doesn't accidentally `Disallow: /`, and references the
sitemap (`Sitemap: https://example.com/sitemap.xml`).
- **sitemap.xml**: exists, lists canonical URLs, valid XML, submitted in Search Console.
- **Internal links** use real `<a href>` (crawlers follow those, not `onclick` divs).
- **Content** important for ranking should be in the initial HTML, not injected only after a
client-side fetch — if it's live mode, compare `WebFetch` output (no JS) against the
Chrome-rendered DOM to see what a non-JS crawler misses.

## 11. Technical hygiene
- `<html lang="en">` (or correct locale) set.
- Exactly one `<meta name="viewport" content="width=device-width, initial-scale=1">`.
- Served over HTTPS; no mixed content.
- No duplicate/conflicting meta tags (two descriptions, two canonicals).
- Reasonable performance — Core Web Vitals (LCP, CLS, INP) influence ranking; flag obviously
heavy pages, but deep perf work is its own task.

## 12. Copy-paste starter block
A complete, correct head to diff a page against (replace values; keep URLs absolute):

```html
<title>Primary Topic — Brand</title>
<meta name="description" content="120–160 chars of compelling, page-specific copy." />
<link rel="canonical" href="https://example.com/page" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://example.com/page" />
<meta property="og:title" content="Share-optimized title" />
<meta property="og:description" content="2–4 sentence summary." />
<meta property="og:image" content="https://example.com/og-image.png" />
<meta property="og:image:alt" content="Describe the image" />
<meta property="og:site_name" content="Brand" />

<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Share-optimized title" />
<meta name="twitter:description" content="2–4 sentence summary." />
<meta name="twitter:image" content="https://example.com/og-image.png" />

<!-- Structured data (swap type/fields to match the page) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Brand",
"url": "https://example.com"
}
</script>
```
14 changes: 14 additions & 0 deletions docs/game.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
<link rel="icon" type="image/svg+xml" href="/gyro-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LoopFlow Lab — learn loops by building them</title>
<meta name="description" content="LoopFlow Lab is an interactive, browser-based way to learn loop engineering: write .loop files, watch them run, and level up through hands-on challenges. No install." />
<meta property="og:type" content="website" />
<meta property="og:title" content="LoopFlow Lab — learn loops by building them" />
<meta property="og:description" content="Learn loop engineering hands-on: write .loop files, watch them run, and level up through interactive challenges in your browser." />
<meta property="og:url" content="https://loopflow.live/game.html" />
<meta property="og:image" content="https://loopflow.live/assets/og-card.png" />
<meta property="og:site_name" content="LoopFlow" />
<link rel="canonical" href="https://loopflow.live/game.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="LoopFlow Lab — learn loops by building them" />
<meta name="twitter:description" content="Learn loop engineering hands-on: write .loop files, watch them run, and level up through interactive challenges in your browser." />
<meta name="twitter:image" content="https://loopflow.live/assets/og-card.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,400;1,500&family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
Expand Down Expand Up @@ -221,8 +233,10 @@
.startbtn:hover{filter:brightness(1.07);}
@media(max-width:640px){ .pblock{min-width:0; flex:1 1 44%;} .brand{font-size:21px;} .card.lvl h2{font-size:23px;} }
</style>
<script defer data-domain="loopflow.live" src="https://plausible.io/js/script.js"></script>
</head>
<body>
<h1 style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0">LoopFlow Lab — learn loop engineering by building loops</h1>
<div class="wrap" id="app">
<div class="topbar">
<div class="wordmark"><span class="logo"><svg class="lpmark" viewBox="0 0 24 24" fill="none" aria-label="LoopFlow"><circle cx="12" cy="12" r="8.5" stroke="#34e0c4" stroke-width="3.6" stroke-linecap="round" stroke-dasharray="42.2 11.2"/></svg></span><span class="brand" id="brand">LoopFlow Studio</span></div>
Expand Down
6 changes: 6 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@
<meta property="og:description" content="Write the goal once — the loop plans, acts, reflects on red, and stops only when the check is green. An open, natural-language DSL for AI coding workflows." />
<meta property="og:url" content="https://loopflow.live/" />
<meta property="og:image" content="https://loopflow.live/assets/og-card.png" />
<meta property="og:site_name" content="LoopFlow" />
<link rel="canonical" href="https://loopflow.live/" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="LoopFlow — stop babysitting the agent" />
<meta name="twitter:description" content="Write the goal once — the loop runs until the check is green. Open-source DSL for AI coding workflows." />
<meta name="twitter:image" content="https://loopflow.live/assets/og-card.png" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"WebSite","name":"LoopFlow","url":"https://loopflow.live/","description":"An open, natural-language DSL for loop engineering — describe a self-correcting, human-gated AI coding workflow in plain English and run it until the check is green."}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,400;1,500&family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
Expand Down Expand Up @@ -195,6 +200,7 @@
.heroduo .duo-arrow{transform:rotate(90deg);padding:0}
}
</style>
<script defer data-domain="loopflow.live" src="https://plausible.io/js/script.js"></script>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
Expand Down
Loading
Loading