Claude Skill

audit-seo

Per-route SEO audit: `head()` coverage, title and description presence, Open Graph and Twitter cards, canonicals, robots rules, plus a `sitemap.xml` generated from the route manifest. Use for "audit SEO", "check meta tags", "generate a sitemap", "are my OG cards set", "review rob

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Download jovidecroock-pracht-skills_audit-seo-8bcdd52.zip · 3 KB

Install

skills CLI npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/audit-seo
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart
Git git clone https://github.com/JoviDeCroock/pracht.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jovidecroock/pracht collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Pracht Audit SEO

Pracht owns the document. Per-route SEO lives in the head() export returning { title?, lang?, meta?, link?, script? }. This skill audits coverage and generates the static SEO artifacts.

Step 1: Inventory

MCP: when the pracht MCP server is registered (pracht dev-mcp), prefer its inspect_routes/inspect_api/inspect_build/doctor/verify tools over shelling out.

pracht inspect routes --json

Prerequisite: pracht inspect needs a vite config with the pracht plugin wired up; run from the app root.

For every route file, read the head() export (and the shell's head() for inherited values).

Step 2: Per-route checklist

For each route, capture presence and quality:

Field Expected
title 30-60 chars, unique per route
meta description 70-160 chars, unique per route
meta og:title Present (often = title)
meta og:description Present
meta og:image Absolute URL, ≥ 1200×630
meta og:url Absolute, canonical
meta twitter:card summary_large_image for content
link canonical Absolute URL
script (JSON-LD) Optional; see Step 6
lang Set on root or via shell

Skip SPA-only / non-indexable routes (admin, dashboard) — flag as "noindex candidate" if they don't already declare it.

Step 3: Cross-route checks

  • Duplicate titles across routes — list collisions.
  • Duplicate descriptions — same.
  • Missing canonical on any route that has any ?query variants.
  • Duplicate trailing-slash URLs. With the default pracht({ trailingSlash: "ignore" }) every page answers at /about and /about/. Recommend "always" or "never", which redirects the other spelling with a 308, or a canonical link on every page. Canonicals, og:url, and sitemap entries must use the spelling in dist/server/routing.json.
  • Missing OG image — most common issue; recommend a default image at shell level so every route inherits one.

Step 4: robots.txt

robots.txt MUST be served at the origin root (/robots.txt) — crawlers never look anywhere else. Pracht API routes are always mounted under /api/ (a src/api/robots.ts handler serves /api/robots, never /robots.txt), so an API handler cannot provide it. The workable option is the static asset:

  • public/robots.txt (Vite static asset, served at /robots.txt).

If absent, recommend creating public/robots.txt:

User-agent: *
Allow: /
Disallow: /api/
Disallow: /admin/
Sitemap: https://<domain>/sitemap.xml

If present, validate:

  • A Sitemap: line referencing an existing endpoint (see Step 5 — if the sitemap is an API route, this must point at /api/sitemap).
  • No accidental Disallow: / (full-site block).
  • Path patterns match real routes (cross-reference with the manifest).
  • Flag any src/api/robots.ts found in the repo as warn: it is dead weight at /api/robots and does not serve /robots.txt.

Step 5: sitemap

Generate (or recommend generating) a sitemap from the route manifest:

  • Include routes by indexability, not prefetch strategy: public render: "ssg"/"isg" routes (and public SSR routes worth indexing), minus routes under auth middleware, minus routes declaring a robots/noindex meta. (prefetch is a client navigation hint — orthogonal to indexability; do not use it as a criterion.)
  • Skip dynamic-segment routes unless getStaticPaths resolved them at build time — pull resolved paths from the prerender output, which is laid out as dist/client/<route>/index.html (clean URLs; e.g. /about → dist/client/about/index.html).
  • Write each URL in the app's canonical spelling: /about/ under trailingSlash: "always", /about otherwise (the root is always /).
  • Default <changefreq> from the route's revalidate policy: timeRevalidate → weekly if > 86400s, daily if > 3600s, hourly otherwise.

Offer two output forms:

  1. Static public/sitemap.xml regenerated at build time via a small script (served at /sitemap.xml).
  2. A pracht API route at src/api/sitemap.ts that emits XML on each request from the inspected manifest. It is served at /api/sitemap (API routes always mount under /api/), so the robots Sitemap: line must point at https://<domain>/api/sitemap.

Step 6: Structured data (optional)

If the user asks, scaffold JSON-LD via the head() export's native script field — HeadMetadata supports script?: HeadScriptDescriptor[], where each descriptor takes attributes plus children for the inline body:

export function head() {
  return {
    script: [
      {
        type: "application/ld+json",
        children: JSON.stringify({ "@context": "https://schema.org", ... }),
      },
    ],
  };
}

No shell-level custom <script> JSX is needed. This is opt-in — do not push it on every audit.

Step 7: Report

Route Severity Title Description OG image Canonical Verdict

Primary severity per finding: error (site blocked by robots, auth-gated route in sitemap), warn (missing title/description/OG image/canonical on an indexable route), info (nice-to-haves like JSON-LD). Keep the complete/partial/missing verdict as a secondary per-route rollup, grouped by verdict.

Rules

  1. Use the resolved manifest — shell head() inheritance matters.
  2. Never auto-write sitemap.xml to the deployed site without user confirmation; offer the file as a draft.
  3. Recommend a default OG image at the shell level — single highest-leverage fix.
  4. Auth-gated routes should NOT appear in sitemaps.
  5. Cross-reference with tune-render-mode — SSG routes are the sitemap candidates; SSR routes need decision per case.

$ARGUMENTS

Files (pracht)
  • SKILL.md 6.6 KB
    ---
    name: audit-seo
    version: 1.1.1
    description: |
      Per-route SEO audit: `head()` coverage, title and description presence, Open
      Graph and Twitter cards, canonicals, robots rules, plus a `sitemap.xml`
      generated from the route manifest.
      Use for "audit SEO", "check meta tags", "generate a sitemap", "are my OG cards
      set", "review robots.txt".
    allowed-tools:
      - Bash
      - Read
      - Write
      - Grep
      - Glob
    ---
    
    # Pracht Audit SEO
    
    Pracht owns the document. Per-route SEO lives in the `head()` export
    returning `{ title?, lang?, meta?, link?, script? }`. This skill audits
    coverage and generates the static SEO artifacts.
    
    ## Step 1: Inventory
    
    MCP: when the pracht MCP server is registered (`pracht dev-mcp`), prefer its
    `inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
    shelling out.
    
    ```bash
    pracht inspect routes --json
    ```
    
    Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
    wired up; run from the app root.
    
    For every route file, read the `head()` export (and the shell's `head()` for
    inherited values).
    
    ## Step 2: Per-route checklist
    
    For each route, capture presence and quality:
    
    | Field                                    | Expected                          |
    | ---------------------------------------- | --------------------------------- |
    | `title`                                  | 30-60 chars, unique per route     |
    | `meta` `description`                     | 70-160 chars, unique per route    |
    | `meta` `og:title`                        | Present (often = `title`)         |
    | `meta` `og:description`                  | Present                           |
    | `meta` `og:image`                        | Absolute URL, ≥ 1200×630          |
    | `meta` `og:url`                          | Absolute, canonical               |
    | `meta` `twitter:card`                    | `summary_large_image` for content |
    | `link` `canonical`                       | Absolute URL                      |
    | `script` (JSON-LD)                       | Optional; see Step 6              |
    | `lang`                                   | Set on root or via shell          |
    
    Skip SPA-only / non-indexable routes (admin, dashboard) — flag as "noindex
    candidate" if they don't already declare it.
    
    ## Step 3: Cross-route checks
    
    - **Duplicate titles** across routes — list collisions.
    - **Duplicate descriptions** — same.
    - **Missing canonical** on any route that has any `?query` variants.
    - **Duplicate trailing-slash URLs.** With the default
      `pracht({ trailingSlash: "ignore" })` every page answers at `/about` and
      `/about/`. Recommend `"always"` or `"never"`, which redirects the other
      spelling with a 308, or a canonical link on every page. Canonicals, `og:url`,
      and sitemap entries must use the spelling in `dist/server/routing.json`.
    - **Missing OG image** — most common issue; recommend a default image at
      shell level so every route inherits one.
    
    ## Step 4: `robots.txt`
    
    `robots.txt` MUST be served at the origin root (`/robots.txt`) — crawlers
    never look anywhere else. Pracht API routes are always mounted under `/api/`
    (a `src/api/robots.ts` handler serves `/api/robots`, never `/robots.txt`), so
    an API handler cannot provide it. The workable option is the static asset:
    
    - `public/robots.txt` (Vite static asset, served at `/robots.txt`).
    
    If absent, recommend creating `public/robots.txt`:
    
    ```
    User-agent: *
    Allow: /
    Disallow: /api/
    Disallow: /admin/
    Sitemap: https://<domain>/sitemap.xml
    ```
    
    If present, validate:
    - A `Sitemap:` line referencing an existing endpoint (see Step 5 — if the
      sitemap is an API route, this must point at `/api/sitemap`).
    - No accidental `Disallow: /` (full-site block).
    - Path patterns match real routes (cross-reference with the manifest).
    - Flag any `src/api/robots.ts` found in the repo as `warn`: it is dead
      weight at `/api/robots` and does not serve `/robots.txt`.
    
    ## Step 5: sitemap
    
    Generate (or recommend generating) a sitemap from the route manifest:
    
    - Include routes by **indexability**, not prefetch strategy: public
      `render: "ssg"`/`"isg"` routes (and public SSR routes worth indexing),
      minus routes under auth middleware, minus routes declaring a
      `robots`/`noindex` meta. (`prefetch` is a client navigation hint —
      orthogonal to indexability; do not use it as a criterion.)
    - Skip dynamic-segment routes unless `getStaticPaths` resolved them at build
      time — pull resolved paths from the prerender output, which is laid out as
      `dist/client/<route>/index.html` (clean URLs; e.g. `/about` →
      `dist/client/about/index.html`).
    - Write each URL in the app's canonical spelling: `/about/` under
      `trailingSlash: "always"`, `/about` otherwise (the root is always `/`).
    - Default `<changefreq>` from the route's revalidate policy: `timeRevalidate`
      → `weekly` if `> 86400s`, `daily` if `> 3600s`, `hourly` otherwise.
    
    Offer two output forms:
    
    1. Static `public/sitemap.xml` regenerated at build time via a small script
       (served at `/sitemap.xml`).
    2. A pracht API route at `src/api/sitemap.ts` that emits XML on each request
       from the inspected manifest. It is served at `/api/sitemap` (API routes
       always mount under `/api/`), so the robots `Sitemap:` line must point at
       `https://<domain>/api/sitemap`.
    
    ## Step 6: Structured data (optional)
    
    If the user asks, scaffold JSON-LD via the `head()` export's native `script`
    field — `HeadMetadata` supports `script?: HeadScriptDescriptor[]`, where each
    descriptor takes attributes plus `children` for the inline body:
    
    ```ts
    export function head() {
      return {
        script: [
          {
            type: "application/ld+json",
            children: JSON.stringify({ "@context": "https://schema.org", ... }),
          },
        ],
      };
    }
    ```
    
    No shell-level custom `<script>` JSX is needed. This is opt-in — do not push
    it on every audit.
    
    ## Step 7: Report
    
    | Route | Severity | Title | Description | OG image | Canonical | Verdict |
    | ----- | -------- | ----- | ----------- | -------- | --------- | ------- |
    
    Primary severity per finding: `error` (site blocked by robots, auth-gated
    route in sitemap), `warn` (missing title/description/OG image/canonical on an
    indexable route), `info` (nice-to-haves like JSON-LD). Keep the
    `complete`/`partial`/`missing` verdict as a secondary per-route rollup,
    grouped by verdict.
    
    ## Rules
    
    1. Use the resolved manifest — shell `head()` inheritance matters.
    2. Never auto-write `sitemap.xml` to the deployed site without user
       confirmation; offer the file as a draft.
    3. Recommend a default OG image at the shell level — single highest-leverage
       fix.
    4. Auth-gated routes should NOT appear in sitemaps.
    5. Cross-reference with `tune-render-mode` — SSG routes are the sitemap
       candidates; SSR routes need decision per case.
    
    $ARGUMENTS
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.