On this page
- BUILD REFUSED Gates
- Configuration Gates
- Navigation Gates
- Frontmatter Gates
- Content Structure Gates
- Shortcode Gates
- Redirect Gates
- GSC and SEO Gates
- Multi-Language Gates
- Content Command Gates
- How Lint Works
- Build Exit Codes
- Fixing Lint Issues
- Common Lint Patterns
- Per-Page Lint Suppression
- What Wire Checks
- Page Structure (16 rules)
- Canonicalization (4 rules)
- URL Structure (6 rules)
- Sitemap Integrity (4 rules)
- Crawl Access (4 rules)
- Links (14 rules)
- Security (1 rule)
- Images (4 rules)
- Multilingual (3 rules)
- Structured Data (5 rules)
- Discovery (1 rule)
- Components (5 rules)
- Content Quality (22 rules)
- Legal (1 rule)
- Indexability (2 rules)
- Where This Fits in Wire's Quality System
- What Wire Does Not Check at Build Time
- Typical Results
- Running Selectively
Build verification is the core of Wire's Content Build System. Wire does not trust what it builds. Two systems enforce quality: BUILD REFUSED gates block the build before rendering starts, and lint rules check the finished HTML after rendering. Both produce specific, actionable messages. Pages that fail do not ship silently.
This is the final layer of Wire's quality system. The content quality layers work on markdown before and during writing. Build verification works on the finished HTML after rendering. It catches problems that only become visible in the final output: broken links between rendered pages, missing meta tags that templates should have inserted, images without dimensions, structured data with invalid JSON.
BUILD REFUSED Gates
Gates run before or during the build. When a gate fails, Wire stops and tells you exactly what is wrong and how to fix it. Gates are checked in order. Fix the first failure, rebuild, and the next gate runs.
Configuration Gates
These check wire.yml before anything else happens.
| Gate | Trigger | Fix |
|---|---|---|
| Missing wire.yml | No wire.yml in site directory | Create wire.yml or run python -m wire.chief init |
| Missing required fields | site_name, site_url, lang (or languages), or nav missing | Add the missing fields. These are strategic decisions. Wire does not guess. |
| Missing sidebar_cta | extra.wire.sidebar_cta missing or incomplete | Add sidebar_cta: with title, description, link, link_text under extra.wire: |
| Missing article_cta | extra.wire.article_cta missing or incomplete | Add article_cta: with title, description, link, link_text under extra.wire: |
| Missing logo | logo key missing from wire.yml | Add logo: assets/logo.svg or logo: false for text-only |
| Missing favicon | favicon key missing from wire.yml | Add favicon: assets/favicon.svg or favicon: false for none |
| Logo/favicon not found | Path in wire.yml points to nonexistent file | Create the file or fix the path |
| Missing labels | extra.wire.labels missing on_this_page, related_articles, or read_more | Add all three labels under extra.wire.labels: |
| Missing discovery labels | Non-English site with discovery steps but no discovery_start or discovery_read | Add both labels under extra.wire.labels: |
| Invalid og_image | og_image in wire.yml is a URL or missing file | Use a local file path relative to docs_dir |
| Invalid BotPoison key | extra.wire.form_botpoison starts with sk_ (secret key) | Replace with your public key (pk_...). Secret keys must never be client-side. |
| Invalid footer.links | footer.links is a flat list instead of grouped dict | Restructure as footer: links: Company: - About: /about/ |
Navigation Gates
These validate the nav structure in wire.yml. Checked after offerings resolution.
| Gate | Trigger | Fix |
|---|---|---|
| Missing nav | No nav: key in wire.yml | Add nav: with your site structure. No auto-discovery. |
| Nav orphans | Pages exist but are not reachable via nav: | Add pages to nav: or exclude with exclude: patterns |
| Legal pages in nav | Legal pages (imprint, privacy, contact, etc.) in nav: | Remove from nav:. Wire auto-adds them via footer.legal. |
| Alphabetical nav order | 3+ sections in alphabetical order | Reorder by importance. Alphabetical looks uncurated. Skipped when offerings configured. |
| Duplicate URLs | Same URL in offerings and content nav | A page cannot be both. Remove from one. |
| Missing header.offerings | Site has topics but no header.offerings | Add header: offerings: <topic-name> or false to disable |
| Too many offerings | header.offerings list has more than 4 items | Consolidate. The header layout breaks with more than 4. |
| Missing header.cta | Site has topics but no header.cta | Add header: cta: label: ..., url: ... or false to disable |
| CTA URL in nav | CTA URL duplicates a content nav URL | CTA is its own nav element. Remove from content nav. |
| CTA page layout | CTA page missing layout: landing | Add layout: landing to the CTA page's frontmatter |
| Offerings page layout | Offerings child page missing valid layout | Add layout: landing, page, or article to frontmatter |
Frontmatter Gates
These validate every markdown file's YAML frontmatter. See Data Model: Frontmatter Contract for the full schema.
| Gate | Trigger | Fix |
|---|---|---|
| Missing title or description | Page missing title or description | Add both to frontmatter. Every page needs them. |
| Unknown keys | Frontmatter key not in known keys list | Remove the key or use a known key. |
redirect: in frontmatter | Page uses redirect: key | Remove it. Add redirect to wire.yml under redirects: instead. |
Missing created | Content page (topic + slug) without created date | Add created: YYYY-MM-DD. Run python -m wire.migrate to backfill from git. |
| Invalid date format | created not in YYYY-MM-DD format | Fix to YYYY-MM-DD. |
| Placeholder date | created on January 1st (e.g. 2025-01-01) | Run python -m wire.migrate to backfill real dates from git history. |
| Future date | created or date is in the future | Set to today or an actual past date. |
| Missing short_title | Topic page without short_title | Add short_title: (max 20 chars). Used for nav tabs and sidebar. |
| Invalid short_title | Ends with preposition (for, and, with) or punctuation | Complete the label. "Solutions for" is incomplete; "Solutions" works. |
| Missing layout | Parent page (index.md with child dirs) without layout | Add layout: page or another valid layout. |
| Missing author | Content page on site with authors/ directory | Add author: <slug> matching a page in docs/authors/. |
| Invalid author/reviewer | Author or reviewer slug not found in docs/authors/ | Fix the slug. Error message lists available slugs. |
Content Structure Gates
These catch structural problems in your docs/ directory.
| Gate | Trigger | Fix |
|---|---|---|
| Numbered slugs | Directory name starts with digits (e.g. 2-foo/) | Rename to descriptive slug. Date slugs (YYYY-MM-DD-*) are allowed. |
| Empty topic | Topic directory with only index.md, no content pages | Add content pages or remove the empty directory. |
| Pending news | Unprocessed YYYY-MM-DD.md files in content dirs | Run python -m wire.chief refine <topic> to integrate them. |
| Migration artifacts | .py files at site root (except setup.py, conftest.py) | Delete migration scripts after migration is complete. |
| MkDocs syntax | Files use {.class}, <div markdown>, !!!, or === "Tab" | Replace: {.class} with :::cta, !!! with > blockquote, === with ## headings. |
| Content cannibalization | Hard overlaps detected (ratio > 0.4, skew > 0.7) | Run python -m wire.chief audit <topic> then deduplicate <topic>. |
| Root page missing layout | Homepage index.md without explicit layout | Add layout: landing or layout: home to frontmatter. |
Shortcode Gates
These validate Wire shortcode syntax in markdown.
| Gate | Trigger | Fix |
|---|---|---|
| Cards list syntax | :::cards uses - [Title](/url/) instead of headings | Use ### Title with a CTA link below each heading. |
| Links in shortcode headings | ### [Link](/url/) inside cards, tabs, faq, or pricing | Use plain text headings. Links belong in the body. |
| Banner value too long | :::banner value exceeds 6 characters | Shorten to 6 characters or fewer. |
| Unsupported section variant | :::section dark or :::section accent | Use :::section (light) or custom CSS for dark styling. |
| Missing !makeIMG images | !makeIMG[slug] references but image file missing | Run python -m wire.chief images to generate them. |
Redirect Gates
| Gate | Trigger | Fix |
|---|---|---|
| Redirect source conflict | Redirect source path also has a docs/ file | Delete the docs file or remove the redirect entry. |
| Self-redirect | Redirect source equals destination | Remove the circular redirect from wire.yml. |
| Query params in redirect | Redirect source contains ? or = | Redirects must be clean paths, not query strings. |
GSC and SEO Gates
| Gate | Trigger | Fix |
|---|---|---|
| GSC coverage gap | GSC URLs with impressions have no page and no redirect | Add redirects in wire.yml redirects: or run python -m wire.chief data to refresh. |
| Stale per-language databases | Old .wire/{lang}/gsc.db files from pre-migration | Delete old DBs, run python -m wire.chief data. Wire uses single .wire/gsc.db now. |
Multi-Language Gates
| Gate | Trigger | Fix |
|---|---|---|
| Language dirs without config | docs/ has 2+ language-code dirs but no languages: in wire.yml | Configure languages: in wire.yml. |
| Language asymmetry | Language has < 20% of default language's page count | Translate more pages or remove the language. Ghost-town translations hurt. |
| Orphan language dirs | Language directory exists but not in wire.yml config | Delete the directory or add it to languages: config. |
| Orphan language config | Language in wire.yml but directory does not exist | Create the directory or remove from config. |
| Duplicate titles | Same title in multiple language directories | Translate the title. Identical titles = untranslated content. |
| Untranslated bodies | Page content in wrong language (stopword detection, min 50 words) | Translate the content. Supported: de, en, es, fr. |
| Unsupported language | Language has no stopword set for Gate 14 detection | Add stopwords to _STOPWORDS in build.py. |
| Invalid alternate targets | alternate: frontmatter points to nonexistent pages | Ensure all alternate targets exist. |
| Non-reciprocal alternates | A maps to B but B does not map back to A | Add reciprocal alternate: to both pages. |
Content Command Gates
These block content-modifying CLI commands (not the build itself).
| Gate | Trigger | Fix |
|---|---|---|
| Uncommitted changes | docs/ has uncommitted git changes | git add docs/ && git commit before running content commands. |
| Missing styleguide | No docs/_styleguide.md when running content generation | Run python -m wire.chief init to generate a starter. See Writing for Wire. |
| translate without --lang | translate command missing --lang flag | Add --lang <code>, e.g. --lang de. |
| Styleguide language mismatch | Styleguide language differs from --lang target | Translate _styleguide.md to target language first. |
| relink without --lang | relink command missing --lang flag | Add --lang <code>. |
| migrate-lang: no wire.yml | Running outside a Wire site | Run from a directory with wire.yml. |
| migrate-lang: already multi | Site already has languages: configured | Only for converting single-language sites. |
| migrate-lang: missing docs | docs_dir in wire.yml does not exist | Create the directory or fix the path. |
| migrate-lang: lang conflict | --from language also in --add list | Remove the duplicate. |
How Lint Works
After wire.build renders your site to HTML, run the verification:
python -m wire.lint --site /path/to/site
Wire parses every HTML file, builds a cross-page index (for duplicate detection and orphan checks), and runs all 91 rules. The output lists every failure with the rule ID, the affected URL, and what to fix.
You can also run specific checks:
python -m wire.lint --site /path/to/site --rules RULE-01,RULE-05,RULE-33
On a typical 200-page site, the full scan takes seconds. Zero API calls, zero cost.
Build Exit Codes
| Level | Example | Site built? | Exit code | Can deploy? |
|---|---|---|---|---|
| BUILD REFUSED | Missing frontmatter, invalid cards syntax, bad nav config | No, build aborts | 1 | No |
| Lint errors | RULE-05 title too long, RULE-33 broken link | Yes, full site in site/ | 1 | No |
| Lint warnings | RULE-52 dashes, RULE-42 image dimensions | Yes, full site in site/ | 0 | Yes |
Use --strict to treat warnings as errors (exit 1 even when the site was fully built).
Fixing Lint Issues
Fix free issues first, then use AI for the rest:
python -m wire.build # See all issues (also saved to .wire/build-errors.txt)
python -m wire.chief sanitize # Fix broken links for free (no AI)
python -m wire.chief lint-fix # Redirect links free, then AI for remaining
python -m wire.build # Rebuild to verify
lint-fix runs two steps: (1) redirect link rewriting (free, mechanical), then (2) AI content fixing via Haiku (low token usage, sequential per page).
Common Lint Patterns
- Page titles: use
:not-as separator. Dashes in titles trigger RULE-52 when rendered as link text in sidebar and related articles. - Math formulas: no spaces around operators inside backticks.
(1-CTR)not(1 - CTR). - Card shortcodes: each card must link to a different page. Same URL triggers RULE-81.
- PDF links: add
(PDF)to anchor text:[Title (PDF)](url.pdf). - Headings: use parentheses not dashes.
## Module (Description)not## Module - Description. - RULE-52 fires after fix: the dash is in a page title that appears as link text on other pages. Fix the title in frontmatter.
- Lint count goes UP after fixing: anchor fragment links like
/page/#sectioncan introduce RULE-33. Point to the page URL without fragments. - Fixed 30 pages, count barely changed: many RULE-52 hits come from one title. Fix the title once, all hits disappear.
Per-Page Lint Suppression
If a rule fires but the content is genuinely correct, suppress per-page with _overruled: in frontmatter:
---
title: My Page
_overruled:
- RULE-85
---
The warning still appears in output but does not block. Error-level rules (RULE-22, RULE-33, RULE-36, RULE-37, RULE-41, RULE-48, RULE-51) cannot be overruled.
What Wire Checks
The 91 rules fall into 16 categories. Each rule exists because independent research, not Google's public statements, shows it affects search performance or site health.
Page Structure (16 rules)
These checks verify that every page has the HTML elements search engines need to understand and rank it.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-01 | Missing or empty H1 | SearchPilot A/B test: pages with keyword-aligned H1 saw 28% more traffic. The H1 tells Google the page's primary topic |
| RULE-02 | Multiple H1 tags on one page | The 2024 Google API leak confirmed entity extraction uses heading structure. Multiple H1s dilute the primary topic signal |
| RULE-03 | Skipped heading levels (H1 then H3, no H2) | Heading hierarchy feeds semantic structure signals. Skipped levels break the logical outline Google uses for entity extraction |
| RULE-04 | Target search query missing from H1 or title | Zyppy's 81K title study: Google rewrites titles 61.6% of the time when they do not match the page's primary heading |
| RULE-05 | Title tag missing, empty, too short (<20), too long (>65), or duplicate | Backlinko's analysis of 11.8M search results: title tag optimization correlates with higher rankings. Google truncates titles over ~60 characters in search results |
| RULE-06 | Multiple <title> tags | Browsers use the first one. Search engines may use either. Remove the duplicate |
| RULE-07 | Meta description missing or over 158 characters | Google truncates long descriptions in search results. A missing description means Google writes one for you, and it rarely matches your intent |
| RULE-08 | Thin content (fewer than 200 words) | Multiple case studies show thin pages hurt when they cannibalize stronger siblings. 201Creative deleted thin ecommerce pages and saw +867% traffic. This rule flags stubs that need expanding |
| RULE-09 | Missing Open Graph tags (og:title, og:description) | These control how your pages appear when shared on LinkedIn, Twitter, Slack, and other platforms. Missing tags mean the platform guesses, usually poorly |
| RULE-65 | Missing og:image tag | Social shares without an image show a blank preview. Wire resolves og:image from frontmatter image:, first body <img>, or theme.og_image in wire.yml. Set a site-wide fallback to cover text-only pages |
| RULE-66 | <form> without submission handler | A form without action= and no form_id configured renders but cannot submit. Either add form_id to wire.yml for Formspark handling, or add action= to point the form at your own endpoint |
| RULE-10 | Missing viewport meta tag | Without it, mobile browsers render pages at desktop width and scale down. Google's mobile-first indexing penalizes pages that are not mobile-friendly |
| RULE-11 | No character encoding in first 1024 bytes | Browsers need to know the encoding before parsing. Missing charset can cause garbled text, especially for non-English content |
| RULE-103 | Root index page without explicit layout | Wire needs to know how to render the homepage. Add layout: landing or layout: home to frontmatter |
| RULE-104 | Landing page with no Wire components | A landing page without :::stats, :::cards, or other components is just a regular page. Use components or switch to a different layout |
| RULE-107 | Landing page hero text exceeds 40 words | Long hero text before the first component reduces visual impact. Move details below the hero |
Canonicalization (4 rules)
Canonical tags tell search engines which version of a page is the "real" one. Getting this wrong splits your ranking signals across multiple URLs.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-12 | Missing canonical tag | Without a canonical tag, Google decides which URL to index. If your page is accessible at multiple URLs (with/without trailing slash, with/without www), Google may split your ranking signals |
| RULE-13 | Multiple canonical tags | When Google finds two canonical tags pointing to different URLs, it ignores both. Your page has no canonical signal at all |
| RULE-15 | Canonical URL not found in sitemap | If the canonical URL and sitemap URL disagree, Google sees conflicting signals about which page matters. Both should point to the same URL |
| RULE-16 | Duplicate H1 across different pages | Two pages with the same H1 create a cannibalization signal. Google cannot tell which page should rank for that heading's topic. Differentiate or consolidate |
URL Structure (6 rules)
Clean URLs help search engines and users. These rules enforce the URL patterns that every major SEO study recommends.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-17 | Uppercase characters in URL path | URLs are case-sensitive on most servers. /About/ and /about/ are different pages. Uppercase creates duplicate content risk |
| RULE-18 | Special characters in URL path | Spaces, brackets, and non-ASCII characters in URLs cause encoding issues. Some crawlers fail to follow these links |
| RULE-19 | Underscores in URL path | Google treats hyphens as word separators but underscores as joiners. content-strategy reads as two words; content_strategy reads as one |
| RULE-20 | URL longer than 115 characters | Long URLs get truncated in search results and are harder to share. Backlinko found shorter URLs correlate with higher rankings |
| RULE-21 | Query parameters on page URL | Parameters like ?utm_source=... on canonical pages create duplicate content. Strip parameters from canonical URLs |
| RULE-22 | Internal links missing trailing slash | [ERROR] Inconsistent trailing slashes create duplicate URLs. If /guides/ and /guides both work, Google may index both and split ranking signals |
Sitemap Integrity (4 rules)
Your sitemap tells Google which pages exist and matter. These rules catch contradictions between your sitemap and your actual pages.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-23 | No sitemap.xml at site root | Without a sitemap, Google relies entirely on crawling to discover pages. Large sites can have pages that Google never finds |
| RULE-24 | Published pages missing from sitemap | If a page exists but is not in the sitemap, Google may deprioritize crawling it. Every indexable page should appear |
| RULE-25 | noindex pages listed in sitemap | Contradictory signals: the sitemap says "index this" while the page says "do not index." Google sees confusion and may ignore both directives |
| RULE-26 | robots.txt-blocked pages listed in sitemap | The sitemap includes a page that robots.txt blocks from crawling. Google cannot crawl it but sees it in the sitemap, another contradictory signal |
Crawl Access (4 rules)
robots.txt controls which pages search engines can access. Mistakes here can accidentally hide your entire site.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-28 | No robots.txt file | Every site should have one. Without it, crawlers assume everything is allowed, including admin pages, staging content, and asset directories you may not want indexed |
| RULE-29 | robots.txt blocking CSS or JavaScript files | Google needs CSS and JS to render your pages. Blocking them means Google sees your site without styling and cannot evaluate mobile-friendliness, layout, or dynamic content |
| RULE-30 | No Sitemap directive in robots.txt | The Sitemap line in robots.txt is how crawlers discover your sitemap. Without it, they have to guess the location |
| RULE-31 | robots.txt blocking all crawlers (Disallow: /) | This makes your entire site invisible to search engines. Usually a leftover from staging that was never removed |
Links (14 rules)
Internal links distribute ranking power across your site. Broken links waste it. Orphan pages never receive it. External links and special link types (tel:, PDF) need correct formatting to work across devices and to set user expectations.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-33 | Broken internal links (target page does not exist) | The 2024 Google API leak lists badBacklinks as a negative ranking signal. Broken internal links waste crawl budget and pass zero ranking power. Wire checks every link against the rendered site directory |
| RULE-34 | Internal links with empty anchor text | Google uses anchor text to understand what the target page is about. Empty anchor text wastes this signal. Screen readers also cannot describe the link to visually impaired users |
| RULE-35 | Orphan pages (zero inbound internal links) | SearchPilot's controlled experiment showed that adding internal links to orphan pages produces a statistically significant traffic increase. Pages with no inbound links are invisible to navigation and receive minimal crawl attention |
| RULE-36 | Links pointing to redirect stubs instead of final URLs | [ERROR] Linking to a redirect wastes a hop and passes less ranking power. Link to the final destination |
| RULE-66 | Form without submission handler | A <form> without action= and no AJAX handler renders but cannot submit. Add action= or JavaScript handling |
| RULE-69 | Cross-language internal links | A German page linking to /en/ content confuses language signals. Keep internal links within the same language directory |
| RULE-70 | Absolute self-links to own domain | Using https://yourdomain.com/path/ instead of /path/ creates unnecessary coupling. Use relative paths |
| RULE-71 | Articles below minimum link thresholds | Articles need sibling links, a topic index link, and at least one external link for source credibility |
| RULE-72 | Generic anchor text like "click here" or "read more" | Google uses anchor text to understand the target page. Generic text wastes this signal |
| RULE-78 | Pages with zero outbound internal links | Dead-end pages trap visitors and waste crawl budget. Add at least one internal link |
| RULE-81 | Same URL linked with different anchor text | Conflicting anchor text for the same target confuses Google about what that page is about |
| RULE-92 | Article with only one outbound internal link | Weakly integrated content. Add more internal links to related pages |
| RULE-101 | tel: links not in E.164 format | Phone links should use international format (+49...) for consistent handling across devices |
| RULE-102 | Links to PDF files without file type indicator | Users should know they are downloading a PDF before they click |
Security (1 rule)
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-37 | HTTP resources loaded on HTTPS pages (mixed content) | Browsers block or warn about mixed content. Images loaded over HTTP on an HTTPS page may not display. Scripts may be blocked entirely. Google flags mixed content as a security issue |
Images (4 rules)
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-40 | Images missing alt text | Alt text serves accessibility (screen readers) and gives Google context for image search. While Moz found alt text is a ranking factor for Google Images specifically, accessibility compliance is reason enough |
| RULE-41 | Broken image sources (local file missing) | A broken image leaves a blank space on the page. Users see it immediately. Google's page quality ratings penalize pages with broken media |
| RULE-42 | Images without explicit width and height | Wire auto-injects dimensions for  markdown images at build time. This rule fires when raw <img> tags bypass that injection or when the image file is unreadable (0-byte/corrupt). Use standard markdown image syntax |
| RULE-77 | Images larger than 200KB | Large images hurt page speed and Core Web Vitals. Compress or resize before deploy |
Multilingual (3 rules)
These rules only activate when your pages use hreflang tags for multilingual content. If your site is single-language, they are skipped automatically.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-43 | Invalid language codes in hreflang tags | Hreflang uses BCP 47 language tags (en, de, fr, etc.). A typo like "eng" instead of "en" means Google ignores the tag and may show the wrong language version to searchers |
| RULE-44 | Hreflang tags present but no x-default | The x-default tag tells Google which page to show when no language matches the searcher's region. Without it, Google picks one and may pick wrong |
| RULE-45 | Hreflang links without reciprocal confirmation | If page A says "my German version is page B" but page B does not link back to page A, Google treats the hreflang as unconfirmed and may ignore it |
Structured Data (5 rules)
JSON-LD structured data helps Google understand your content type, author, dates, and relationships. Invalid structured data is worse than none at all.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-45 | Page has no JSON-LD structured data | Every page should have at least one JSON-LD block declaring its type. Wire generates these automatically |
| RULE-46 | Invalid JSON in JSON-LD script blocks | Malformed JSON means Google cannot parse any of your structured data. A single missing comma or bracket breaks the entire block |
| RULE-47 | Unrecognized schema.org type | Using an @type that schema.org does not define means Google ignores the entire structured data block. Wire checks against 18 recognized types including Article, WebPage, Organization, Product, FAQPage, and BreadcrumbList |
| RULE-48 | Broken URLs in structured data fields | If your JSON-LD references a URL (for images, logos, or page links) that does not exist on your site, Google may flag the structured data as misleading |
| RULE-55 | JSON-LD missing recommended fields for its type | Google recommends specific fields per schema type (e.g. Article needs author, datePublished). Missing fields reduce rich result eligibility |
Discovery (1 rule)
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-53 | Discovery step IDs that do not match any H2 anchor in the article | Step IDs reference article sections. When a rewrite renames or removes an H2, the step's "Read the full article on this" button scrolls to nothing. Users see a broken experience. Wire catches this before deploy |
Components (5 rules)
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-63 | Stats bar (:::stats) without exactly 4 items | Desktop renders 4 across, mobile renders 2+2. Any other count (especially 3) creates an orphan row on mobile. Add or remove stats to hit exactly 4 |
| RULE-64 | Custom <section> in landing hero without section-hero class | Wire wraps the first landing section with section-hero (light background, padding, max-width). If you provide your own <section>, add section-hero to its class list so Wire passes it through without double-wrapping. Otherwise your custom styling fights Wire's hero padding |
| RULE-68 | Progress bar fill exceeds 100% | Progress bars visually break when the fill width is greater than the container |
| RULE-105 | Card links to external URL | Cards are navigation elements. External links in cards confuse the internal linking structure |
| RULE-106 | Mixed linked and unlinked cards in one block | All cards in a :::cards block should be consistently linked or all unlinked |
| RULE-108 | Markdown link in :::stats, :::banner, or :::progress | These shortcodes render values as plain text, no markdown processing. [text](url) renders literally. Use :::badges for linked items |
Content Quality (22 rules)
Content quality rules detect AI writing patterns, thin structure, and metadata problems that hurt rankings.
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-52 | Em dashes in body text | Em dashes are a strong AI writing signal. Human writers use commas, periods, or semicolons. Rewrite the sentence |
| RULE-67 | Headings with numbered prefixes (## 1. Introduction) | Numbered headings are a mechanical writing pattern. Use descriptive headings |
| RULE-73 | Duplicate meta descriptions across pages | Each page needs a unique description. Duplicates dilute click-through signals |
| RULE-74 | First image uses loading=lazy | Lazy-loading the first image delays Largest Contentful Paint. Remove lazy from above-the-fold images |
| RULE-75 | Pages more than 3 clicks from homepage | Deep pages get less crawl attention. Keep important content within 3 clicks of the homepage |
| RULE-76 | Article pages without date metadata | E-E-A-T freshness signals require publication dates. Add created: to frontmatter |
| RULE-80 | Identical opening paragraphs on different pages | Template content that was not customized. Each page needs a unique introduction |
| RULE-83 | More than 10% of body text is bold | Excessive bold text is an AI writing pattern. Bold should highlight key terms, not entire paragraphs |
| RULE-84 | List items exceeding 50 words | Long list items are paragraphs hiding as bullets. Use actual paragraphs for detailed explanations |
| RULE-85 | Pages over 500 words with no external links | Longer content without external citations lacks source credibility. Link to evidence |
| RULE-86 | Meta description starts with the title text | Lazy metadata. The description should expand on the title, not repeat it |
| RULE-87 | Many headings with very few words between them | Outline-style content with no depth. Expand thin sections or consolidate headings |
| RULE-88 | Single word exceeds 5% of body text | Keyword stuffing. Reduce repetition and use natural language variation |
| RULE-89 | Pages over 300 words with no H2 subheadings | Long unstructured text. Add H2s to break content into scannable sections |
| RULE-91 | Excessive H5/H6 heading depth | Over-structured content. Most pages only need H2 and H3 |
| RULE-93 | Tables without thead | Tables need <thead> for accessibility and search engine parsing |
| RULE-94 | Meta description exceeds 160 characters | Google truncates long descriptions. Keep under 160 characters |
| RULE-95 | Title exceeds 60 characters | Google truncates long titles in search results. Keep under 60 characters |
| RULE-96 | No semantic HTML elements | Pages should use <article> or <main> for content structure |
| RULE-97 | Article body has no paragraph text | Non-prose content in an article layout. Check if the layout is correct |
| RULE-99 | Meta description under 70 characters | Short descriptions underutilize the SERP snippet. Aim for 120-155 characters |
| RULE-100 | Title and H1 share less than 30% keyword overlap | Title about one topic, H1 about another. Align them |
Legal (1 rule)
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-32 | Missing imprint/privacy links | EU/German law requires imprint and privacy policy links. Wire checks for both |
Indexability (2 rules)
| Rule | What it catches | Why it matters |
|---|---|---|
| RULE-49 | noindex on pages that other pages link to | If pages link to a noindexed page, those links pass ranking power to a page Google will not show in search results. The noindex is usually accidental, a leftover from staging or a template error |
| RULE-50 | nofollow on internal links | rel="nofollow" tells Google not to follow a link or pass ranking power through it. On internal links, this wastes your own site's link equity. Internal nofollow is almost always a mistake |
Where This Fits in Wire's Quality System
Wire enforces content quality at four levels. Each catches what the previous layers missed.
| Layer | When it runs | What it checks | Cost |
|---|---|---|---|
| Prevent | Before the AI writes | Styleguide rules taught to the AI before every prompt | Free |
| Fix | On every save | 9 auto-corrections applied before content hits disk | Free |
| Detect | On demand (audit) | 13 content-level checks across all pages | Free |
| Verify | After build (lint) | 91 HTML-level checks across rendered site | Free |
The first three layers work on markdown, the raw content. Build verification works on the finished HTML, what search engines and visitors actually see. A page can have perfect markdown and still fail verification if a template forgets to insert a canonical tag, or if a Jinja2 partial omits the viewport meta tag.
This is why Wire runs verification after rendering, not before. The template layer can introduce problems that do not exist in the content layer. Wire catches them before they reach production.
What Wire Does Not Check at Build Time
Four checks from the original specification require a live HTTP server and are deliberately excluded from build verification:
- Redirect chains (RULE-14): requires following HTTP redirects, which only work on a running server
- 4xx/5xx status codes (RULE-27): requires HTTP requests to external URLs
- Page load speed (RULE-38): requires a browser rendering engine (Lighthouse)
- Mobile rendering (RULE-39): requires device emulation
These are server-level and network-level checks. Wire is a build tool. It works with files on disk. For live-server checks, use your search console's URL Inspection tool or a monitoring service like Pingdom or UptimeRobot.
Typical Results
On a real 316-page production site, the first build verification run found:
- 4 pages with duplicate H1 tags across different sections
- 12 internal links pointing to pages that had been renamed
- 2 pages where the template failed to insert Open Graph tags
- 1 page with a JSON-LD block containing a trailing comma (invalid JSON)
Total cost: zero. Every issue included the exact URL and a specific fix instruction. The same issues would have taken a manual review hours to find, if they were found at all.
Running Selectively
Not every check matters for every site. A single-language site does not need hreflang rules. A site without structured data does not need JSON-LD rules. Run only what applies:
# Only check page structure and internal links
python -m wire.lint --site site/ --rules RULE-01,RULE-02,RULE-03,RULE-05,RULE-07,RULE-33,RULE-35
# Only check sitemap consistency
python -m wire.lint --site site/ --rules RULE-23,RULE-24,RULE-25,RULE-26
Every rule ID is stable. You can save your preferred rule set and run the same checks on every build.