From bb0308f9b235a991b9d0a85e3e39757da228b6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel=20Mialdea=20Monzo=20Dev?= Date: Fri, 24 Jul 2026 23:26:38 +0200 Subject: [PATCH] Add references/seo.md: head, semantics and structured-data rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hallmark already decides the , heading levels and image attributes on every full-page build; this makes those decisions explicit. Conditional load — component-scope builds skip it. --- skills/hallmark/SKILL.md | 1 + skills/hallmark/references/seo.md | 121 ++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 skills/hallmark/references/seo.md diff --git a/skills/hallmark/SKILL.md b/skills/hallmark/SKILL.md index e0715e9..77b5060 100644 --- a/skills/hallmark/SKILL.md +++ b/skills/hallmark/SKILL.md @@ -369,6 +369,7 @@ The non-negotiables live in [`references/`](references/). **Be precise about wha - [`microinteractions.md`](references/microinteractions.md) — load whenever the output has *any* interactive element (buttons, inputs, modals, tabs, dropdowns, toasts, drag handles, copy buttons). That is most pages. - [`interaction-and-states.md`](references/interaction-and-states.md) — load when the page has stateful UI (forms, command palettes, optimistic updates). - [`responsive.md`](references/responsive.md) — load when mobile is in scope. +- [`seo.md`](references/seo.md) — load when the build emits a full page (i.e. it owns a ``). Carries the head block, semantic skeleton, image attributes, and structured-data rules. Skip entirely on component-scope builds. - [`structure.md`](references/structure.md) — load only when deviating from a named macrostructure. - [`hero-enrichment.md`](references/hero-enrichment.md) — **do NOT load at Step 4 unless the image-need check in the next paragraph returns YES.** Most builds are typography-only and never touch this file. The decision is one quick read of the brief, not a defensive auto-load. - [`custom-craft.md`](references/custom-craft.md) — load only when an enrichment archetype requires construction (CSS art, SVG, declarative animation, etc.). diff --git a/skills/hallmark/references/seo.md b/skills/hallmark/references/seo.md new file mode 100644 index 0000000..9c9570d --- /dev/null +++ b/skills/hallmark/references/seo.md @@ -0,0 +1,121 @@ +# Discoverability + +Most AI-generated pages are invisible. They ship a flawless hero and forget the ``. They stack six `<div>`s where a `<main>` belonged. They size a line at 6rem and call it an `h1` because it *looks* like one. They lazy-load the hero image — the one thing that should load first. None of this shows up in a screenshot, which is exactly why it survives. + +Discoverability is not a marketing bolt-on. Every rule below is a decision Hallmark already makes while it builds; this file just makes it make them correctly. + +## Principles + +- **Structure carries meaning, size carries emphasis.** Heading *level* describes the document's shape. Type *scale* describes how loud a line is. They are independent. A `--text-display` line is not automatically an `h1`, and an `h2` is free to be small. +- **One `h1`, no skipped levels.** Exactly one per page. `h2` follows `h1`; `h3` follows `h2`. Never jump a level to reach a size you liked. +- **Landmarks before divs.** `header` · `nav` · `main` · `footer` are not optional semantics. Exactly one `main` per page, wrapping the page's actual content — not the whole body. +- **Mark up only what is on the page.** Structured data describes visible content. If the rating isn't rendered, it isn't marked up. This is the same rule as [`anti-patterns.md` § Invented metrics](anti-patterns.md), enforced one layer deeper — invented structured data is a fabricated metric that only machines read. + +## The head + +Every emitted page carries these. No exceptions, no placeholders left as `TODO`. + +```html +<html lang="en"> <!-- match the copy's language --> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Cold-brew kits for small kitchens · Ratio + + + + + + + + + + +``` + +| Field | Length | Rule | +| --- | --- | --- | +| `` | 50–60 chars | Page subject first, brand last, separated by ` · ` or ` — `. Unique per page. | +| `description` | 140–160 chars | A sentence a human would read. Not a keyword list. | +| `og:image` | 1200×630 | Real image or omit the tag. Never point at a missing file. | +| `lang` | — | Matches the copy actually on the page, not the developer's locale. | + +A `<title>` that is only the brand name is a wasted line — it's the largest text a search result will ever show for the page. + +## Semantic skeleton + +```html +<body> + <header> <!-- masthead; site-level, not the hero --> + <nav aria-label="Primary">…</nav> + </header> + + <main> <!-- exactly one, no id required --> + <h1>…</h1> + <section aria-labelledby="kits"> + <h2 id="kits">…</h2> + </section> + </main> + + <footer>…</footer> +</body> +``` + +- A `section` with no accessible name is a `div` with extra steps. Give it `aria-labelledby` pointing at its own heading, or use `div`. +- Multiple `nav` elements need distinguishing labels (`aria-label="Primary"`, `aria-label="Footer"`). +- Decorative wrappers stay `div`. Reaching for `section` or `article` to look tidy is noise. + +## Images + +- **`alt` describes function, not appearance.** What would you say to someone who can't see it and needs to keep reading? *"Brewer disassembled into six parts"* — not *"image of a coffee product on a white background"*. +- **Decorative images take `alt=""`.** Empty, not missing. Missing `alt` makes a screen reader read the filename. +- **Always set `width` and `height`.** They reserve the box and stop the page from jumping as images arrive. CSS still controls the rendered size. +- **The hero image loads eagerly.** It is almost always the largest element on screen; lazy-loading it delays the one paint the visitor is waiting for. + +```html +<!-- hero: the visitor is waiting for this one --> +<img src="/brewer.avif" width="1600" height="900" alt="Brewer disassembled into six parts" + loading="eager" fetchpriority="high" decoding="async"> + +<!-- below the fold --> +<img src="/kitchen.avif" width="1200" height="800" alt="" loading="lazy" decoding="async"> +``` + +## Structured data + +Emit one JSON-LD block in the head, only for what the page genuinely is, and only describing content a visitor can see. One `@type` per page is normal; two is the ceiling. + +| Page | `@type` | +| --- | --- | +| Company / product site | `Organization`, or `LocalBusiness` when there's a real address | +| Single product | `Product` | +| Written piece | `Article` (`author` and `datePublished` required) | +| Q&A block already on the page | `FAQPage` | +| Any page below the root | `BreadcrumbList` | + +```html +<script type="application/ld+json"> +{ + "@context": "https://schema.org", + "@type": "Organization", + "name": "Ratio", + "url": "https://example.com", + "logo": "https://example.com/logo.png" +} +</script> +``` + +Ratings are the trap. `aggregateRating` is only legal when the reviews it summarises are rendered on that page, with real counts. If the brief didn't supply reviews, the page has no rating — the same way it has no *"+47 % conversion"*. Marking up a rating that nobody wrote is not an optimisation; it's a manual action waiting to happen. + +## Bans + +- **No missing `<title>` or `description`.** Shipping either as `TODO`, `Untitled`, or the brand name alone. +- **More than one `h1`**, or a heading level skipped to reach a size. +- **Headings chosen by size.** `h3` for a small caption is a structural lie; use a `p` with a label class. +- **`div` where a landmark belonged.** No `main`, or several. +- **`alt="image of…"`, `alt="photo"`, or keyword-stuffed alt.** Also: missing `alt` on a decorative image — use `alt=""`. +- **Lazy-loading the hero image.** Delays the largest paint on purpose. +- **`<img>` without `width`/`height`.** Guarantees layout shift. +- **Structured data for content that isn't on the page** — reviews, ratings, FAQs, breadcrumbs to routes that don't exist. Machine-readable fabrication is still fabrication. +- **Keyword-stuffed copy.** Copy is written for the reader. See [`copy.md`](copy.md). +- **`og:image` pointing at a file that was never generated.**