Skip to content
Back to skills

Website Seo Geo

ASecurity

The on-page SEO + GEO contract for a new site: exact metadata limits (title 50–60 chars, meta description 140–160 with a 120 floor), og:title/og:description == title/desc, twitter inherits OG, clean canonical == og:url, required JSON-LD schema, and llms.txt for AI answer engines. Use when building or reviewing a page's <head> / metadata (step 5 of the new-website pipeline), or when a title/description is too long or tags are out of sync. Enforced by the seo.spec.ts check in the new-website te...

  • 6 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 19, 2026
ai-agentspythongobashnodeapi

Works with

  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 19, 2026

npx -y skills add karero/website-builder --skill website-seo-geo --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Website Seo Geo?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Website Seo Geo
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/karero-website-seo-geo/badge)](https://www.skillsdirectory.com/skills/karero-website-seo-geo)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: website-seo-geo
description: >
  The on-page SEO + GEO contract for a new site: exact metadata limits (title
  50–60 chars, meta description 140–160 with a 120 floor), og:title/og:description == title/desc,
  twitter inherits OG, clean canonical == og:url, required JSON-LD schema, and
  llms.txt for AI answer engines. Use when building or reviewing a page's <head>
  / metadata (step 5 of the new-website pipeline), or when a title/description is
  too long or tags are out of sync. Enforced by the seo.spec.ts check in the
  new-website test suite. Delegates depth to ai-seo (GEO) and schema-markup.
  Trigger phrases: "SEO metadata", "title too long", "meta description length",
  "og tags", "twitter card", "canonical", "is my SEO set up", "llms.txt", "GEO",
  "head tags", "structured data on this page".
---

# Website SEO & GEO

The metadata contract every page must satisfy. This skill owns the **exact
limits and consistency rules**; it hands depth off to the existing global skills:
`ai-seo` (AI answer-engine / GEO strategy), `schema-markup` (rich JSON-LD),
`seo-audit` (technical SEO / crawl / Core Web Vitals — see also `website-qa`).

## The limit table (enforced)

| Tag | Ideal | Hard cap | Why |
|---|---|---|---|
| `<title>` | 50–60 chars | ~60 | Google truncates ~580–600px; >60 risks a cut, >70 almost always cut |
| `<meta description>` | 140–160 chars | ~160 | Truncates ~920px; >160 cut; <120 wastes space / keyword room |
| `og:title` / `og:description` | = `<title>` / = description | — | Test fails on any divergence |
| `twitter:title` / `twitter:description` | inherit OG (same strings) | — | `twitter:card = summary_large_image` |

The test counts characters (em-dash, en-dash, curly apostrophe each = 1 char but
render wider — stay clear of the cap). `<meta name="description">` is **separate
from** `og:description`; one does not replace the other — ship both, in sync.

### German (and other compounding languages) eat the budget faster

The limits above are pixel-render caps, not word counts — they don't change per
language, and the numbers in the table stay exactly as specified. But German
compound nouns (`Suchmaschinenoptimierung`, `Barrierefreiheit`,
`Barrierefreiheitsprüfung`) pack far more meaning per character into a single
unbroken word than English does, so the same 50–60 / 140–160 char cap holds
noticeably less content once translated. Write for it deliberately:

- **Front-load the key term.** German syntax tends to push the important noun
  toward the end of a compound or clause; if you translate an English title
  1:1 the keyword can land past where truncation cuts. Put the primary
  keyword/term first, then qualify it, rather than burying it in a trailing
  compound.
- **When one natural compound blows the budget, don't force it — restructure.**
  If `Barrierefreiheitsprüfung` (24 chars alone) doesn't leave room for
  anything else, reach for a shorter near-synonym or split brand/category from
  descriptor with a colon or dash: `Website-Builder: barrierefrei &
  SEO-optimiert` reads better and fits, where one giant compound noun forces
  an awkward truncation or abbreviation.
- **Budget for ~10–15% fewer effective words of meaning** than an English
  title/description of the same character count. Plan the German copy's
  content around that from the start — don't write the English version, translate
  it, and then trim words until the character count fits; that's how you end
  up with a mangled abbreviation instead of a deliberate, readable title.

- **Target the LOWER half of the title range (~50–55 chars) for German.**
  Google truncates by pixel width (~580px), not characters — German capitalizes
  every noun, and capitals plus umlauts render wide, so a passing 60-char
  German title can still be cut in the SERP. The 60-char cap is a ceiling,
  not a safe harbor.

This is a **writing tactic**, not a test change: `seo.spec.ts` enforces the same
character counts regardless of language, and it should — the fix here is how you
write within that budget, not the budget itself.

## Consistency rules

- One pair of inputs `(title, description)` drives `<title>`, OG, Twitter,
  canonical and the WebPage schema — wired once in the new-website skill's
  `templates/astro/src/layouts/Base.astro`, so pages only pass `title`/`description`
  and the tags can never drift.
- **Canonical** is clean (no `.html`), absolute, and equals `og:url` and the
  page's `sitemap.xml` `<loc>`.
- **No trailing slash (convention):** URLs are `/about`, not `/about/` (home stays `/`).
  The starter sets `trailingSlash: 'never'` + `build.format: 'file'`; write internal links
  without a trailing slash. `navigation.spec.ts` flags any `/about/` link (404/redirect hop)
  and `seo.spec.ts` pins `canonical == SITE.url + path`, so the whole site stays consistent.
- Exactly one `<h1>` per page (its text MAY differ from the keyword-tuned title).
- **ASCII slugs only:** transliterate German umlauts/ß to digraphs (`/ueber-uns`,
  not `/über-uns`) — machine-serialized URLs (canonical, sitemap) percent-encode
  non-ASCII while hand-written strings (PAGES, llms.txt, authored links) stay
  raw, a mismatch factory (site-architecture has the full rule).
- **og:locale derives from `<html lang>`** (de → de_DE; regioned tags → base+region;
  one shared `ogLocaleFor()` in `src/config.ts`, emitted by Base.astro, asserted
  by `seo.spec.ts` whenever a value derives — mapped bases and all regioned
  tags) — a German page silently carrying `og:locale en_US` is exactly the
  drift this row exists to stop.

## Schema (JSON-LD) — minimum per page

- Every page: `WebSite` + `Organization` (once, site-wide) and a `WebPage` node.
- Add page-type schema via `schema-markup`: `BreadcrumbList` on nested pages,
  `FAQPage` on FAQs, `Article`/`TechArticle` with author `@id` + dates on posts,
  `Service`/`Product` as relevant. Author/Organization `@id`s and `sameAs` feed
  EEAT (see `website-content-guide`); keep `sameAs` URLs that actually resolve.
- **Datetimes in JSON-LD carry an explicit timezone.** `VideoObject.uploadDate`,
  `Event.startDate/endDate`, `Offer.validFrom`, `Article` dates etc. must be full
  ISO-8601 with an offset (`2026-03-11T00:00:00+00:00`, not a bare `2026-03-11`).
  Google's video parser rejects a date-only `uploadDate` as "missing a timezone" /
  "invalid datetime value" — a GSC error that only surfaces weeks after launch.
- Validate with Google Rich Results Test / schema.org validator before launch.

## GEO (generative engine optimization)

- Ship `/llms.txt` (template in the starters): a curated, accurate index of key
  pages + facts for AI answer engines. **Write it in the site's primary content
  language** (German site → German descriptions; multilingual site → each
  entry's description in that page's language) — same precedent as the
  privacy page: an answer engine quoting your English blurb to a German
  searcher is the wrong-language failure again. Keep it in sync with the site — route
  *coverage* is enforced both ways in the new-website starter suite:
  `tests/llms-coverage.spec.ts` fails if any `PAGES` route is missing from it,
  or if a same-site entry goes stale (page removed/renamed). Add the page's
  `- [Title](URL): description` line in the same commit as the page;
  deliberately-hidden pages (paid-ad landers) go in its `LLMS_EXEMPT` with a
  reason. On a site that predates the guard, copy the spec in from the starter
  before relying on it.
- For deeper GEO/AEO (being cited by ChatGPT/Perplexity/AI Overviews, answer
  formatting, entity coverage), run **`ai-seo`** — do not duplicate it here.

## PDFs you host (the doc-title IS the search-result title)

A PDF has no `<title>` — Google and **Bing** print its embedded **doc-title** as
the search-result title. Authoring tools ship a placeholder
("PowerPoint-Präsentation", "Microsoft Word - report.docx") or a working-filename
slug, which Bing flags as *"page title too short"* (seen in production on a real
site built with this kit, 2026-06-19). So any PDF you put in `public/` needs a
descriptive doc-title set **before** it ships:

- **Title convention:** the same human title the page uses for that document —
  e.g. `Talk Title — Speaker | Brand`. ≥15 chars, never the authoring placeholder.
- **Set it in BOTH places crawlers read** — the Info dict `/Title` **and** the XMP
  `dc:title`. Fixing only one leaves the stale title visible. Use the helper:
  ```bash
  python3 scripts/set_pdf_title.py public/<file>.pdf "Descriptive Title | Brand" --author "Author"
  ```
  (needs `pip install pypdf`; local authoring tool only — the test below is dep-free.)
- **Enforced by `seo.spec.ts`:** the PDF guard scans `public/**/*.pdf` and fails the
  build on a placeholder, a title under 15 chars, or no doc-title at all. No-op for
  sites that host no PDFs.
- After fixing a live PDF, **re-submit its URL in Bing/Google** so it recrawls.

## Done means

`npm test` green — specifically `seo.spec.ts`: every title/description within
limits, og/twitter/canonical in sync, JSON-LD parses, a sitemap exists, and every
hosted PDF carries a descriptive doc-title (not an authoring placeholder).

## Judgment pass (what the tests cannot check)

`seo.spec.ts` proves the metadata is *well-formed*; it cannot tell you it is *good*.
Once it is green, do a one-page judgment read — this is a model call (Rule 5: use the
model only where code can't answer):
- **Title matches search intent**, not just ≤60 chars — would a searcher click it
  over its SERP neighbours?
- **Description earns the click** — answers the query, not keyword-stuffed.
- **Schema is semantically right** — correct `@type` and real relationships, not
  merely valid JSON (a valid-but-wrong `@type` still passes the test).
- **`llms.txt` is accurate vs the live site** — facts and wording current, no
  drift (route *coverage* is the mechanical half, pinned by
  `tests/llms-coverage.spec.ts`; this read is about the wording half).
- **EEAT signals resolve** — `sameAs` / author URLs actually load and corroborate.

Intent and semantics only. Structural correctness is the test's job; cross-file
route/URL/config/SEO consistency is the `website-review` (Double-Knuth) skill's job.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.