ai newsSo., 1. Nov.
EilmeldungOpenAI Cuts Its Funding Target by $10B as Tech Stocks Sell Off

I Wrote a Claude Code SEO Skill, and Testing It Found Two Bugs on Day One

Claude Code skills are just folders. A SKILL.md with a name and a description, plus whatever scripts you want the model to run. When the description matches what you asked for, Claude loads the instructions and follows them. That's the whole mechanism, and it's why SEO is a good fit: most on-page c

dev.toSonntag 06:00 · 01.11.8 Min. Lesezeit
Bild: dev.to

Claude Code skills are just folders. A SKILL.md with a name and a description, plus whatever scripts you want the model to run. When the description matches what you asked for, Claude loads the instructions and follows them. That's the whole mechanism, and it's why SEO is a good fit: most on-page checks are deterministic, and a model is much better at explaining a result than at counting characters in a <title> tag.

So I wrote one. It checks a single URL for the on-page basics and hands Claude a JSON report to turn into a fix list. Then I ran it against five real pages and one fake one before trusting it. It broke twice, on pages I'd have assumed were safe.

.claude/skills/seo-page-check/ ├── SKILL.md └── scripts/ └── check_page.py Enter fullscreen mode Exit fullscreen mode Project-level skills live in.claude/skills/ inside the repo; personal ones go in ~/.claude/skills/. Claude Code reads the frontmatter of every skill at startup and only pulls in the body when the description matches the task, so a skill you're not using costs almost nothing.

name: seo-page-check description: Check one URL for on-page SEO problems (title, meta description, canonical, robots/noindex, H1, JSON-LD, image alt text) and return a prioritized fix list. Use when the user asks to audit, check, or review the SEO of a specific page or URL. # SEO page check 1. Run the checker on the URL the user gave: `python3.claude/skills/seo-page-check/scripts/check_page.py <url>` 2.

Read the JSON it prints. Do not re-fetch the page yourself unless the script failed. 3.

Report in this order, and skip any section with nothing in it: - **Blockers**: anything in `blockers` (noindex, non-200 status, canonical pointing to another host). - **Fix next**: items in `warnings`, one line each, with the current value quoted. - **Fine**: one line listing what passed. 4. For each title or description warning, suggest one rewrite that keeps the page's main keyword.

Do not invent keywords that are not on the page. 5. Never claim a ranking impact.

Say what is wrong and what to change. Enter fullscreen mode Exit fullscreen mode Two choices in there are deliberate.

The description lists the actual checks. Claude decides whether to load a skill from the description alone, so "SEO helper" would be a bad one. It's vague, and it would match half of what you type in a marketing repo. Naming the checks and the trigger ("a specific page or URL") keeps it from firing when you ask for keyword ideas.

Rule 5 is there because models love writing "this will improve your rankings." Nobody knows that from a page check, so the skill forbids it.

Standard library only, so it runs anywhere Claude Code does without a pip install:

#!/usr/bin/env python3 """ Fetch one URL and report on-page SEO issues as JSON. Standard library only. """ import json, sys, urllib. error, urllib. request from html.parser import HTMLParser from urllib.parse import urlparse class Page ( HTMLParser ): def __init__ ( self ): super (). __init__ () self. title, self. in_title, self. in_h1 = "", False, False self. meta, self. canonical, self. h1s, self. jsonld = {}, None, [], 0 self. imgs, self. imgs_no_alt, self. title_done = 0, 0, False def handle_starttag ( self, tag, attrs ): a = dict ( attrs ) if tag == " title " and not self. title_done: self. in_title = True # skip SVG <title>s elif tag == " meta " and a. get ( " name " ): self. meta [ a [ " name " ]. lower ()] = a. get ( " content " ) or "" elif tag == " link " and " canonical " in ( a. get ( " rel " ) or "" ). lower (). split (): self. canonical = a. get ( " href " ) elif tag == " h1 ": self. in_h1 = True; self. h1s. append ( "" ) elif tag == " script " and a. get ( " type " ) == " application/ld+json ": self. jsonld += 1 elif tag == " img ": self. imgs += 1 if not ( a. get ( " alt " ) or "" ). strip (): self. imgs_no_alt += 1 def handle_endtag ( self, tag ): if tag == " title " and self. in_title: self. in_title, self. title_done = False, True elif tag == " h1 ": self. in_h1 = False def handle_data ( self, data ): if self. in_title: self. title += data if self. in_h1: self. h1s [ - 1 ] += data Enter fullscreen mode Exit fullscreen mode The check() function fetches the page, runs the parser, and sorts findings into blockers (things that stop a page from being indexed at all) and warnings (things worth fixing).

The thresholds are plain: title 30–60 characters, description 70–160, exactly one H1, a canonical on the same host, at least one JSON-LD block. You'll want to tune them; I picked them because they're common and easy to argue about.

I ran the first version against example.com, MDN's HTML docs, Wikipedia's SEO article, Hacker News, and an httpbin URL that returns a 404.

Most of it was fine. Example.com and Hacker News got exactly the warnings you'd expect: short titles, no description, no canonical, no H1. Wikipedia came back with no meta description and "8 of 16 images have no alt text," which is real.

"title": "HTML: HyperText Markup Language | MDNMDNMDNMozilla", Enter fullscreen mode Exit fullscreen mode The page's title is HTML: HyperText Markup Language | MDN. The extra "MDNMDNMozilla" came from the inline SVG logos in the body, because SVG has its own <title> element for accessibility, and my parser was appending every <title> it saw. Any site with inline SVG icons would get a wrong title length and possibly a wrong "title too long" warning. The fix is to stop collecting after the first </title> closes, which is what the title_done flag above does.

{ "url": "https://httpbin.org/status/404", "error": "<HTTPError 404: 'Not Found'>" } Enter fullscreen mode Exit fullscreen mode urllib raises on any 4xx or 5xx, so the most important blocker, "this page doesn't load," came back as a crash instead of a finding. Following rule 2 of the skill, Claude would then fall back to fetching the page itself, which is exactly what I wanted to avoid. HTTPError is also a response object, so the fix is to catch it and keep going:

try: r = urllib. request. urlopen ( req, timeout = 20 ) except urllib. error. HTTPError as e: # 4xx/5xx still has a status worth reporting r = e Enter fullscreen mode Exit fullscreen mode Test 2: a page built to fail None of the real pages had a noindex or a cross-domain canonical, and those are the two checks I care about most. So I wrote a fake staging page and served it with python3 -m http.server:

<title> Staging copy of our pricing page </title> <meta name= "robots" content= "noindex, nofollow" > <meta name= "description" content= "short" > <link rel= "canonical" href= "https://other-domain.example/pricing" > <h1> Pricing </h1><h1> Plans </h1><img src= "a.png" > Enter fullscreen mode Exit fullscreen mode After the fixes, the three test pages came back like this:

Every planted problem showed up, and the MDN title now reads correctly. The checker also looks at the X-Robots-Tag response header, not only the meta tag, because a noindex set at the server level never appears in the HTML.

There are two halves, and they need different tests.

The script is ordinary code. Give it known-good pages, known-bad pages and a broken URL, and compare the JSON with what you expect. Both bugs above were in this half, and neither would have shown up if I'd only tried it on my own site.

The trigger is the description. The way to test it is to open a fresh Claude Code session in the repo and try prompts that should and shouldn't load it: "check the SEO of https://…" should; "give me blog post ideas about SEO" shouldn't. If it fires too often, narrow the description; if it never fires, the description is probably too abstract. I'd keep a short list of those prompts next to the skill and rerun them whenever you edit the frontmatter.

A single-page checker is the easy part of SEO. It won't tell you which pages lost clicks last month, who you're competing with for a query, or whether a page is even worth fixing. That needs Search Console data and competitor research, and both are harder to make deterministic.

If you want something along those lines without writing it, Tracetify publishes free claude seo skills on GitHub. One compares Search Console periods and ranks the pages worth working on; another traces a competitor's launch history from public sources. Each installs with a single npx skills add command, works in Codex as well as Claude Code, and doesn't need an account. Reading how they structure their SKILL.md files is also a decent way to see how someone else handles the description-and-trigger problem.

For my own checker, the next step is a non-zero exit code whenever blockers is non-empty, so the same script can fail a CI run before a noindex ships to production.

Quelle: dev.to (dev.to). Original öffnen