Wire Agent Protocol
IMPORTANT: Read this document completely before taking any action. Skipping sections will cause BUILD REFUSED errors, destroyed content, or wasted API credits on work that gets overwritten. Wire uses the Claude API for all content operations.
Wire is a content pipeline and static site generator for AI-powered content sites.
It is installed as a pip package and operated via CLI commands.
Every Wire site has a wire.yml configuration file at its root.
Wire is opinionated: it refuses to build on missing config rather than silently degrading.
Quick Start
- Install:
pip install https://wire.wise-relations.com/dist/wire-latest.whl - Find wire.yml:
ls wire.yml. If missing, you are not in a site directory - Test build:
python -m wire.build - If build passes: the site is healthy. Read on for operations.
- If build fails: fix the error. See the BUILD REFUSED section below.
- Initialize a new site:
python -m wire.chief init
Requirements:
claude login(Claude Max subscription) ORANTHROPIC_API_KEYin.env(API fallback)- Optional:
BFL_API_KEY(for AI image generation) - Optional:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,token.json(for Google Search Console) - Must run from the site directory (where
wire.ymllives) - Git Bash on Windows: use
/c/Users/...paths, NOTC:\Users\...
All Commands (in correct order)
The standard Wire workflow runs these commands in sequence. Each command depends on the output of previous commands.
| # | Command | Cost | Modifies files? | What it does |
|---|---|---|---|---|
| 0 | init | Free | Yes | Initialize Wire in current directory (generates wire.yml, styleguide, templates) |
| 1 | data | Free | .wire/gsc.db only | Pull Google Search Console metrics into local SQLite database |
| 2 | audit [topic] | Free | No | SEO audit: opportunities, gaps, overlaps, dead pages. Read-only analysis. |
| 3 | deduplicate [topic] | ~$0.10/page | Yes | Merge or differentiate pages that cannibalize each other's keywords |
| 4 | news <topic> | ~$0.02/page | Yes (pending files) | Gather news for all items in a topic via web search |
| 5 | refine <topic> | ~$0.05/page | Yes | Integrate pending news files into page content |
| 6 | reword <topic> | ~$0.05/page | Yes | Rewrite pages to target GSC keyword opportunities |
| 7 | consolidate --topic <topic> | ~$0.10/page | Yes | Create hub pages from comparison pages |
| 8 | crosslink [topic] | ~$0.03/page | Yes | Add internal links to underlinked pages |
| 9 | sanitize [topic] | Free | Yes | Fix broken internal links (no API calls, just re-save) |
| 10 | enrich <topic> | ~$0.06/page | Yes | Analyze locally (free), then improve actionable pages |
| 11 | images | ~$0.04/image | Yes | Generate missing AI images via BFL/FLUX |
| 12 | newsweek | ~$0.30/run | Yes | Generate weekly market intelligence report |
| 13 | translate [topic] | ~$0.01/page | Yes | Translate page bodies to match their language directory |
| 14 | migrate-gsc | Free | .wire/gsc.db only | Rekey GSC database after page restructure via redirects |
| 15 | migrate-lang | Free | Yes | Scaffold multi-language site from single-language |
| 16 | download [topic] | Free | Yes | Download WordPress pages as clean markdown |
| 17 | redirects | Free | No | Analyze GSC coverage gaps, suggest strict redirects |
| 18 | lint-fix [topic] | Free | Yes | Rewrite links pointing through redirects to direct targets |
| - | build | Free | Yes (site/ dir) | Build the static site. Separate module: python -m wire.build |
Command Syntax
All commands use this pattern:
python -m wire.chief <command> [arguments] [--flags]
Build uses a separate module:
python -m wire.build [--dirty] [--serve] [--port N]
Global Flags
| Flag | Effect |
|---|---|
--dry-run | Preview changes without saving (writes .preview files instead) |
--nogsc | Skip GSC database lookups (for new sites without search data) |
--lang <code> | Language code for multi-language sites (e.g., de, en). Required for multi-language content commands. |
--version | Print Wire version and exit |
Per-Command Flags
| Command | Flag | Effect |
|---|---|---|
data | --force | Re-fetch even if data is fresh |
data | --workers N | Concurrent workers (default 1, max 8) |
audit | --min N | Minimum shared keywords to flag (default 3) |
audit | --min-impressions N | Minimum impressions to include (default 100) |
deduplicate | --min N | Minimum shared keywords to act on (default 3) |
news | --resume | Resume interrupted run |
news | --workers N | Concurrent workers (default 1, max 8) |
refine | --resume | Resume interrupted run |
refine | --workers N | Concurrent workers (default 1, max 8) |
reword | --resume | Resume interrupted run |
reword | --workers N | Concurrent workers (default 1, max 8) |
enrich | --resume | Resume interrupted run |
consolidate | --topic <name> | Required. Topic containing items to consolidate. |
consolidate | --min N | Minimum primary comparisons to consolidate (default 3) |
crosslink | --resume | Resume interrupted run |
translate | --resume | Resume interrupted run |
images | --force | Regenerate all images, even cached ones |
images | --prune | Remove orphan images not referenced by any !makeIMG |
newsweek | --from YYYY-MM-DD | Start date (default: 7 days ago) |
newsweek | --to YYYY-MM-DD | End date (default: today) |
newsweek | --skip-review | Skip Phase 3 editorial review |
newsweek | --resynth | Skip Phase 1, reuse cached extracts (for prompt iteration) |
download | --force | Re-download even if page has content |
migrate-lang | --from <code> | Required. Current site language code. |
migrate-lang | --add <code> | Required. Language(s) to add (repeatable). |
What NOT to Do
- Do NOT run commands out of order.
refinebeforenewswastes API credits on empty content. - Do NOT skip
deduplicatewhenauditreports hard overlaps. Build will REFUSE until resolved. - Do NOT run content commands without committing your work first. Wire refuses if
docs/has uncommitted changes. - Do NOT run content generation without a project styleguide at
docs/_styleguide.md. Wire refuses. - Do NOT omit
--langon multi-language sites for content commands. Wire fails loud. - Do NOT run
datafrom outside the site directory. Wire readswire.ymlfrom the current directory.
Frontmatter Schema
Every markdown file MUST have YAML frontmatter delimited by ---.
Wire validates frontmatter at build time. Unknown keys cause BUILD REFUSED.
Required Fields (all pages)
| Key | Type | Purpose |
|---|---|---|
title | string | Page title for <title> tag and H1. Must be non-empty. |
description | string | Meta description for search results. Must be non-empty. |
Required for Content Pages (pages with both topic and slug)
| Key | Type | Purpose |
|---|---|---|
created | string (YYYY-MM-DD) | First publication date. Needed for JSON-LD, RSS, sitemap. |
Required for Topic Pages (pages within a topic directory)
| Key | Type | Purpose |
|---|---|---|
short_title | string (max 20 chars) | Short label for nav tabs and sidebar. No fallback to title. |
Required for Parent Pages (index.md with child content directories)
| Key | Type | Purpose |
|---|---|---|
layout | string | Page layout type. Required when directory has child content dirs. |
Optional Fields
| Key | Type | Purpose |
|---|---|---|
date | string (YYYY-MM-DD) | Last modification date. Updated on every write by stamp(). |
template | string | Jinja2 template name (default: page.html). |
og_type | string | Open Graph type (website, article). |
image | string | Featured image path for OG tags and social sharing. |
tags | list | Content tags for categorization. |
sources | list | External citation URLs (append-only). |
wire_action | string | Last Wire action that modified the page. Set automatically. |
wire_reworded | string (YYYY-MM-DD) | Date of last SEO reword. Set automatically. |
wire_differentiated | string (YYYY-MM-DD) | Date of last differentiation. Set automatically. |
wire_differentiated_from | string | Slug of page this was differentiated from. Set automatically. |
author | string | Author slug (resolves to author page in docs/authors/). |
role | string | Author role/title override for byline display. |
short_title | string | Short label for navigation (max 20 characters). |
layout | string | Page layout. Valid values: page, article, landing, raw, home. |
schema_type | string | JSON-LD schema.org @type override. |
company | string | Company name for vendor/organization pages. |
product | string | Product name for product pages. |
hide_title | any | Hide the page title in rendered output. |
hide_meta | any | Hide the meta info block (author, date) in rendered output. |
extra_css | list | Additional CSS files to load on this page. |
extra_js | list | Additional JS files to load on this page. |
alternate | dict | Cross-language page mappings. Format: {lang_code: /lang/path/}. |
draft | boolean | Mark page as draft (excluded from build). |
summary | string | Short summary for listings and cards. |
linkedin | string | LinkedIn URL for author pages. |
organization | string | Organization name for structured data. |
categories | list | Content categories. |
last_updated | string | Last updated date (separate from date). |
hide | any | Hide page from navigation and listings. |
Complete List of All Known Keys
These are the ONLY keys Wire accepts in frontmatter. Any other key triggers BUILD REFUSED:
alternate, author, categories, company, created, date, description,
draft, extra_css, extra_js, hide, hide_meta, hide_title, image,
last_updated, layout, linkedin, og_type, organization, product,
role, schema_type, short_title, sources, summary, tags, template,
title, wire_action, wire_differentiated, wire_differentiated_from, wire_reworded
Valid Layout Values
| Layout | Use case |
|---|---|
page | Default for topic index pages (nav, no sidebar) |
article | Content pages with TOC, sidebar CTA, reading progress bar |
landing | Marketing pages split into alternating sections at <hr> |
raw | Bare HTML, no chrome (for embeds, widgets) |
home | Homepage with hero section |
Rejected Keys (BUILD REFUSED)
| Key | Why rejected | What to do instead |
|---|---|---|
redirect | Not a Wire feature | Add redirect to wire.yml under redirects: |
Frontmatter Examples
Minimal index page:
---
title: Vendor Directory
description: Compare all IDP vendors in one place
layout: page
short_title: Vendors
---
Content page:
---
title: ABBYY FlexiCapture Review
description: Independent analysis of ABBYY FlexiCapture for document processing
created: 2025-06-15
short_title: ABBYY
---
Landing page:
---
title: Get Started with Our Platform
description: Book a free consultation
layout: landing
short_title: Get Started
---
BUILD REFUSED Gates
Wire refuses to build when it detects configuration problems. Every gate has a specific error message and fix. Gates are checked in this order.
Gate: Missing wire.yml
- Trigger: No
wire.ymlfile in the site directory - Error:
No wire.yml found in {site_dir}. Create one with at least site_name and docs_dir. - Fix: Create
wire.ymlor runpython -m wire.chief init
Gate: Missing Required wire.yml Fields
- Trigger:
site_name,site_url,lang(orlanguages), ornavmissing from wire.yml - Error:
wire.yml missing required field(s): {fields}. These are strategic decisions - Wire does not guess. - Fix: Add the missing fields to wire.yml
Gate: Missing sidebar_cta
- Trigger:
extra.wire.sidebar_ctamissing or incomplete in wire.yml - Error:
wire.yml missing required 'extra.wire.sidebar_cta'. Article pages need a sidebar CTA card. - Fix: Add to wire.yml:
extra:
wire:
sidebar_cta:
title: "Your CTA title"
description: "Short description"
link: /your-page/
link_text: "Learn more"
Gate: Missing article_cta
- Trigger:
extra.wire.article_ctamissing or incomplete in wire.yml - Error:
wire.yml missing required 'extra.wire.article_cta'. Every article needs a bottom CTA banner. - Fix: Add to wire.yml:
extra:
wire:
article_cta:
title: "Ready to get started?"
description: "Book a free consultation."
link: /contact/
link_text: "Get in touch"
Gate: Missing logo
- Trigger:
logokey missing from wire.yml - Error:
wire.yml missing required 'logo'. - Fix: Add
logo: assets/logo.svg(path to logo) orlogo: false(text-only mode)
Gate: Missing favicon
- Trigger:
faviconkey missing from wire.yml - Error:
wire.yml missing required 'favicon'. - Fix: Add
favicon: assets/favicon.svg(path to favicon) orfavicon: false(no favicon)
Gate: Logo/Favicon File Not Found
- Trigger: Logo or favicon path in wire.yml points to a nonexistent file
- Error:
logo file not found: {path}. Expected at: {full_path} - Fix: Create the file at the specified path or update the path in wire.yml
Gate: Language Directories Without Config (single-language sites)
- Trigger:
docs/contains 2+ directories that look like ISO 639-1 language codes (e.g.,de/,en/) but nolanguagesconfig in wire.yml - Error:
docs/ contains directories that look like language codes: {codes}. This creates broken nav tabs instead of a proper multi-language site. - Fix: Configure
languages:in wire.yml with proper language entries
Gate: Missing Required Frontmatter
- Trigger: Page missing
titleordescriptionin frontmatter - Error:
BUILD REFUSED: {count} page(s) missing required frontmatter (title, description) - Fix: Add the missing fields to each page's frontmatter
Gate: Unknown Frontmatter Keys
- Trigger: Frontmatter contains keys not in the known keys list
- Error:
unknown frontmatter key(s): {keys}. Wire only supports: {known_keys} - Fix: Remove the unknown keys or use a known key instead. See the Complete List of All Known Keys above.
Gate: redirect: in Frontmatter
- Trigger: Page uses
redirect:key in frontmatter - Error:
'redirect:' in frontmatter is not supported. Add redirect to wire.yml instead - Fix: Remove
redirect:from frontmatter and add towire.ymlunderredirects:
Gate: MkDocs Extension Syntax
- Trigger: Markdown files use MkDocs extension syntax (
{.class}attr_list,<div markdown>,!!!admonitions,===tabs) - Error:
BUILD REFUSED: {count} MkDocs syntax error(s). Wire does not process MkDocs extensions. - Fix: Replace with Wire equivalents:
{.class}attribute lists: Use:::ctafor buttons, or remove class annotation<div markdown>: Use Wire shortcodes (:::stats,:::cards,:::split,:::banner)!!!admonitions: Use>blockquote instead===tabs: Use##headings to organize content
Gate: Missing Layout on Parent Pages
- Trigger: An
index.mdpage has child content directories but nolayout:in frontmatter - Error:
BUILD REFUSED: {count} page(s) missing required layout - Fix: Add
layout: page(or another valid layout) to the page's frontmatter
Gate: Missing or Invalid short_title on Topic Pages
- Trigger: A topic page (index or content) missing
short_title, or short_title exceeds 20 characters, or short_title ends with a trailing preposition or punctuation - Error:
BUILD REFUSED: {count} topic page(s) missing or invalid short_title - Fix: Add
short_title:to frontmatter (max 20 chars, complete label, no trailing prepositions like "for", "and", "with")
Gate: Numbered Slugs
- Trigger: Content directory name starts with 1-2 digit number prefix (e.g.,
2-adding-chatbot/,7-advanced/) - Error:
BUILD REFUSED: {count} page(s) with numbered slugs - Fix: Rename directories to use descriptive slugs without number prefixes. Date-based slugs (YYYY-MM-DD-*) are allowed for news articles.
Gate: Empty Topic Directories
- Trigger: A topic directory has only an index page, no content child pages (except standalone landing pages)
- Error:
BUILD REFUSED: {count} empty topic(s) - Fix: Add content pages to the topic or remove the empty directory
Gate: Placeholder Created Dates
- Trigger: Content page has
createddate on January 1st (e.g.,2025-01-01) - Error:
created date '{date}' looks like a placeholder (January 1st). Run python -m wire.migrate to backfill from git history. - Fix: Run
python -m wire.migrateto backfill real dates from git history, or set the actual creation date
Gate: Invalid Created Date Format
- Trigger:
createddate is not in YYYY-MM-DD format - Error:
created date '{date}' must be YYYY-MM-DD format. - Fix: Fix the date to use YYYY-MM-DD format
Gate: Pending News Files
- Trigger: Unprocessed YYYY-MM-DD.md news files exist in content directories (not archived)
- Error:
BUILD REFUSED: {count} page(s) have unprocessed pending news - Fix: Run
python -m wire.chief refine <topic>to integrate the pending news
Gate: Migration Artifacts
- Trigger:
.pyfiles at site root (exceptsetup.py,conftest.py) - Error:
BUILD REFUSED: Migration artifact at site root: {filename} - Fix: Delete migration scripts after migration is complete
Gate: Nav Orphans
- Trigger: Pages exist that are not reachable via the
nav:configuration in wire.yml - Error:
BUILD REFUSED: {count} page(s) not reachable via nav in wire.yml - Fix: Add pages to
nav:in wire.yml, or exclude them withexclude:patterns
Gate: Missing header.offerings
- Trigger: Site has topics with child pages but
header.offeringsis not configured - Error:
BUILD REFUSED: header.offerings not configured in wire.yml - Fix: Add to wire.yml:
header:
offerings: <topic-name>
Set to false to explicitly disable.
Gate: Missing header.cta
- Trigger: Site has topics but
header.ctais not configured - Error:
BUILD REFUSED: header.cta not configured in wire.yml - Fix: Add to wire.yml:
header:
cta:
label: Get Started
url: /getting-started/
Set to false to explicitly disable.
Gate: Nav Config Validation
Checked after offerings resolution:
- Alphabetical Nav Order: 3+ nav sections in alphabetical order = uncurated. Reorder by importance.
- Duplicate URLs: Same URL in offerings and content nav. A page cannot be both.
- CTA URL in Nav: CTA URL duplicates a content nav URL. CTA is its own nav element.
- CTA Page Layout: CTA page must have
layout: landing. - Offerings Page Layout: Offerings pages must have
layout: landing,page, orarticle.
Gate: Missing Article Labels
- Trigger:
extra.wire.labelsmissing required keys - Error:
Missing required labels: {keys} - Fix: Add to wire.yml:
extra:
wire:
labels:
on_this_page: "On This Page"
related_articles: "Related Articles"
read_more: "Read More"
Gate: Missing Discovery Labels (non-English sites with discovery steps)
- Trigger: Non-English site has discovery steps but missing
discovery_startordiscovery_readlabels - Error:
Non-English site (lang={lang}) has discovery steps but missing labels: {keys} - Fix: Add to wire.yml:
extra:
wire:
labels:
discovery_start: "Start Reading"
discovery_read: "Continue"
Gate: Content Cannibalization (SEO Audit)
- Trigger: Hard overlaps detected (keyword overlap ratio > 0.4, traffic skew > 0.7, same topic)
- Error:
BUILD REFUSED: {count} content cannibalization issue(s) detected. - Fix: Run
python -m wire.chief audit <topic>thenpython -m wire.chief deduplicate <topic>
Gate: GSC Coverage
- Trigger: Google Search Console URLs with impressions have no corresponding page and no redirect
- Error:
Build blocked: {count} GSC URL(s) have no page and no redirect. - Fix: Add redirects in wire.yml
redirects:section, or runpython -m wire.chief datato refresh GSC URLs
Gate: Redirect Source Conflicts
- Trigger: A redirect source path also has a corresponding
docs/file (ambiguous state) - Error:
BUILD REFUSED: CONFLICT: redirect /{path}/ has a source file - Fix: Delete the docs file or remove the redirect entry
Gate: Alternate Frontmatter Validation (multi-language)
- Trigger:
alternate:frontmatter points to nonexistent pages, or mappings are not reciprocal - Error:
BUILD REFUSED: alternate target /{lang}{path} does not existorNon-reciprocal alternate - Fix: Ensure all alternate targets exist and map back to the source page
Multi-Language Gates
| Gate | Trigger | Error |
|---|---|---|
| Language Asymmetry | Language has less than 20% of default language's page count | Language '{code}' has {n} pages ({pct}% of '{default}') |
| Orphan Language Dirs | Language directory exists with content but is not in wire.yml config | Directory '{code}/' exists with content but is not in languages config |
| Orphan Language Config | Language configured in wire.yml but directory does not exist | Language '{code}' is configured but its docs directory does not exist |
| Duplicate Titles | Same title appears in multiple language directories | Title '{title}' appears in both '{lang1}' and '{lang2}' |
| Untranslated Bodies | Page content detected in wrong language via stopword analysis (min 50 words) | Page '{url}' is in '{expected}' directory but content appears to be '{detected}' |
| Unsupported Language | Language has no stopword set for detection | Must add stopwords to _STOPWORDS in build.py. Supported: de, en, es, fr |
Gate: Uncommitted Changes (content commands only)
- Trigger:
docs/directory has uncommitted git changes when running content-modifying commands - Error:
REFUSED: {count} uncommitted change(s) in docs/ - Fix:
git add docs/ && git commitbefore running content commands
Gate: Missing Styleguide (content generation only)
- Trigger: No
docs/_styleguide.mdfile when running content generation commands - Error:
Content generation refused: no project styleguide found at docs/_styleguide.md - Fix: Create
docs/_styleguide.mdbased onwire/prompts/example_styleguide.md
Gate: Banner Value Too Long
- Trigger:
:::bannershortcode value exceeds 6 characters - Error:
:::banner value '{value}' is {len} chars (max 6). Short values only. - Fix: Shorten the banner value to 6 characters or fewer
Gate: Unsupported Section Variant
- Trigger:
:::section darkor:::section accentused in markdown - Error:
BUILD ERROR: :::section {variant} is not supported. Wire removed dark/accent sections. - Fix: Use
:::section(light) or custom CSS for dark styling
Lint Rules
Wire runs an HTML linter after every build.
Lint failures block deployment. Every rule has a specific, actionable message.
Rules can be filtered with --rules RULE-01,RULE-05.
Group A: Metadata
| Rule | What it checks |
|---|---|
| RULE-01 | H1 present and non-empty. Exactly one required. |
| RULE-02 | Exactly one H1 per page. Multiple H1s = demote extras to H2. |
| RULE-03 | Heading hierarchy not skipped (no H2 followed by H4). |
| RULE-04 | Target query appears in H1 or title (when meta target-query is set). |
| RULE-05 | Title tag present, unique, 20-65 characters. Duplicate titles flagged. |
| RULE-06 | Exactly one title tag per page. |
| RULE-07 | Meta description present, max 158 characters. |
| RULE-08 | Body content has at least 200 words (thin content detection). |
| RULE-09 | Open Graph tags present (og:title, og:description). |
| RULE-10 | Viewport meta tag present. |
| RULE-11 | Charset declared in first 1024 bytes. |
Group B: Canonicalization
| Rule | What it checks |
|---|---|
| RULE-12 | Canonical tag present with href. |
| RULE-13 | Exactly one canonical tag per page. Multiple = Google ignores both. |
| RULE-15 | Canonical URL appears in sitemap.xml. |
| RULE-16 | No duplicate H1 across the entire site. Differentiate or consolidate. |
Group C: URL Structure
| Rule | What it checks |
|---|---|
| RULE-17 | No uppercase characters in URL path. |
| RULE-18 | No spaces or special characters in URL path. Only a-z, 0-9, hyphens, slashes allowed. |
| RULE-19 | No underscores in URL path. Replace with hyphens. |
| RULE-20 | URL length within 115 characters. |
| RULE-21 | No query parameters on canonical page URLs. |
| RULE-22 | Internal links use consistent trailing slash. |
Group D: Sitemap
| Rule | What it checks |
|---|---|
| RULE-23 | sitemap.xml exists at site root. |
| RULE-24 | All indexable pages appear in sitemap.xml. |
| RULE-25 | No noindex pages listed in sitemap. |
| RULE-26 | No robots.txt-blocked pages in sitemap. |
Group E: robots.txt
| Rule | What it checks |
|---|---|
| RULE-28 | robots.txt exists at site root. |
| RULE-29 | robots.txt does not block CSS or JS (Google needs these to render). |
| RULE-30 | robots.txt contains Sitemap directive. |
| RULE-31 | robots.txt does not block all crawlers with Disallow: /. |
| RULE-32 | Site has imprint and privacy links (required by EU/German law). |
Group F: Internal Links
| Rule | What it checks |
|---|---|
| RULE-33 | No internal links returning 404 (target file missing). |
| RULE-34 | No internal links with empty anchor text. |
| RULE-35 | At least one inbound internal link per page (orphan detection). |
| RULE-36 | Internal links must not point to redirect stubs. Update href to target. |
Group G: Security
| Rule | What it checks |
|---|---|
| RULE-37 | No HTTP resources on HTTPS pages (mixed content). |
Group H: Images
| Rule | What it checks |
|---|---|
| RULE-40 | All images have alt text. |
| RULE-41 | No broken local image sources. |
| RULE-42 | Images have explicit width and height (prevents layout shift). |
Group I: Hreflang (multi-language sites)
| Rule | What it checks |
|---|---|
| RULE-43 | Hreflang tags use valid BCP 47 language codes. |
| RULE-44 | Hreflang includes x-default when other hreflang tags exist. |
| RULE-45 | Every hreflang alternate has a reciprocal link back (JSON-LD page also checks for at least one block). |
Group J: Structured Data
| Rule | What it checks |
|---|---|
| RULE-45 | JSON-LD structured data present on every page. |
| RULE-46 | JSON-LD contains valid JSON. |
| RULE-47 | JSON-LD @type is a recognized schema.org type. |
| RULE-48 | No broken local URLs in JSON-LD structured data fields. |
| RULE-55 | JSON-LD includes Google-recommended fields for its @type (headline, datePublished, author). |
Group K: Indexability
| Rule | What it checks |
|---|---|
| RULE-49 | Important pages (with inbound links) not accidentally noindexed. |
| RULE-50 | No nofollow on internal links. |
Group L: Privacy / GDPR
| Rule | What it checks |
|---|---|
| RULE-51 | No raw YouTube/Vimeo iframes. Use !yt[VIDEO_ID] shortcode for GDPR-safe embeds. |
| RULE-52 | No em dashes or double hyphens in body text. Em dashes are the top tell of AI-generated content. |
Group M: Discovery
| Rule | What it checks |
|---|---|
| RULE-53 | Discovery step IDs must match article H2 anchors. |
Group N: Content Quality
| Rule | What it checks |
|---|---|
| RULE-63 | Stats bar must have exactly 4 items (mobile grid is 2-col). |
| RULE-64 | Landing page custom section in hero must declare section-hero class. |
| RULE-65 | og:image present for social sharing previews. |
| RULE-66 | Form element must have a submission handler (action= or form_id). |
| RULE-67 | Headings should not start with numbered prefixes (migration artifact detection). |
| RULE-68 | Progress bar fill width must be 0-100%. Values over 100% break layout. |
| RULE-69 | Internal links must stay in the same language directory (cross-language link detection). |
| RULE-70 | No absolute URLs pointing to own domain. Use relative paths instead. |
Workflow
The standard Wire content workflow runs these commands in sequence:
data -> audit -> deduplicate -> news -> refine -> reword -> consolidate ->
crosslink -> sanitize -> enrich -> images -> newsweek
Phase 1: Data Collection (free)
python -m wire.chief data # Pull GSC metrics into .wire/gsc.db
python -m wire.chief audit # See SEO opportunities
data fetches Google Search Console metrics into a local SQLite database at .wire/gsc.db.
This is the only command that calls the GSC API. All other commands read from the local database.
audit is read-only. It produces a structured report with sections:
- HEALTH: dead pages, redirect gaps, stale GSC data
- ACTION: merge/differentiate recommendations
- SEO: keyword opportunities, position improvements
- INFO: topic summaries, page counts
Phase 2: Deduplication
python -m wire.chief deduplicate vendors
Resolves keyword cannibalization detected by audit.
Hard overlaps (ratio > 0.4, skew > 0.7, same topic) get merged.
Soft overlaps (ratio > 0.15) get differentiated.
Build REFUSES until hard overlaps are resolved.
Phase 3: Content Updates
python -m wire.chief news vendors # Gather news
python -m wire.chief refine vendors # Integrate news into pages
python -m wire.chief reword vendors # Rewrite for GSC keywords
python -m wire.chief consolidate --topic vendors # Hub pages
python -m wire.chief crosslink vendors # Internal links
python -m wire.chief sanitize vendors # Fix broken links (free)
python -m wire.chief enrich vendors # Targeted improvements
Each command modifies pages. Commit after each command.
Phase 4: Assets and Reports
python -m wire.chief images # Generate AI images
python -m wire.chief newsweek # Weekly intelligence report
Phase 5: Build and Deploy
python -m wire.build # Build static site
python -m wire.build --serve # Build and serve locally
python -m wire.build --dirty # Rebuild only modified pages
Resume and Dry Run
All batch commands support --resume to continue after interruption.
Progress is tracked in .wire/progress-*.json files.
Stale progress files from crashes are harmless.
--dry-run writes .preview files instead of modifying content.
Safe for inspection. Skips stamp() (no metadata changes).
Shortcodes
Wire provides markdown shortcodes for common UI components.
Use :::name to open and ::: to close.
:::stats
4-item statistics bar. Must have exactly 4 items (RULE-63 enforces this).
:::stats
500+ | Customers
99.9% | Uptime
24/7 | Support
50ms | Response
:::
:::cards
Feature cards in a responsive grid. Optional links.
:::cards
## Card Title
Card description text.
[Learn more](/link/)
## Another Card
More description.
:::
:::split
Two-column layout separated by ---.
:::split
Left column content with **markdown**.
---
Right column content.
:::
Variant: :::split comparison highlights the second column.
:::banner VALUE
Big number callout. VALUE max 6 characters.
:::banner 223%
Companies using structured content see 223% higher ROI.
:::
:::progress
Horizontal bar chart for comparisons.
:::progress
Content-Engagement | 34%
Webinar-Einladung | 31%
:::
:::cta
Call-to-action buttons. First link auto-primary, rest secondary.
:::cta
[Get Started](/start/)
[Learn More](/about/)
:::
Override with {primary} / {secondary} markers.
:::tabs
Tabbed content sections.
:::tabs
## Tab One
Content for tab one.
## Tab Two
Content for tab two.
:::
:::faq
FAQ section with schema.org FAQPage JSON-LD.
:::faq
## Question one?
Answer to question one.
## Question two?
Answer to question two.
:::
:::badges
Badge/tag list.
:::alert
Alert/notification box.
:::section
Section wrapper. Only :::section (light) or :::section light allowed.
:::section dark and :::section accent are NOT supported (BUILD ERROR).
:::testimonial
Customer testimonial with quote styling.
:::logos
Logo cloud with grayscale hover effect.
:::logos


:::
:::pricing
Pricing grid with plan cards.
:::pricing
:::plan
## Basic
$9/mo
- Feature one
- Feature two
[Start Free](/signup/)
:::plan
:::plan featured
## Pro
$29/mo
- Everything in Basic
- Priority support
[Get Pro](/signup/)
:::plan
:::pricing
:::steps
Step-by-step guide.
:::visual
Site visual component (logo + site name).
!makeIMG[slug]{preset}
AI image shortcode. Generates image via BFL/FLUX.
!makeIMG[document-processing]{wide}
Presets: default (1024x576), wide (1440x480), square (512x512).
Image stored at docs/assets/images/{slug}.png.
Missing image at build time = FileNotFoundError (build error).
Inside backticks or code fences, !makeIMG is skipped by the parser (safe in docs).
!yt[VIDEO_ID]
YouTube embed shortcode. GDPR-safe (no iframe until user clicks).
!yt[dQw4w9WgXcQ]
Gotchas
These are common pitfalls that cause confusing errors. Read before operating.
Prompt and API Gotchas
topicis auto-injected inload_prompt(). Passingtopic=in kwargs causesTypeError: got multiple values for argument.- Prompts larger than 50KB auto-stream. Non-streaming times out at 10 minutes.
claude_text()is CLI-first: triesclaude -pfirst, falls back to API. Requiresclaude loginfor CLI path.- API model (fallback only):
claude-sonnet-4-6. - Rate limit retries: 15s then 30s, max 2 retries.
- Markdown fences are stripped from output automatically (both CLI and API paths).
Filesystem Gotchas
frontmatter.load()crashes on missing files. Always check.exists()first.{#slug}in prompts must be escaped as{{#slug}}(Python.format()KeyError).- Every markdown file needs YAML frontmatter with
title:. Content.from_path()requirestitlein frontmatter. No fallback.- Content item body via
item.read_index()notitem.content(Content has no content attribute). - Git Bash on Windows:
/c/Users/...notC:\Users\....
Command Gotchas
- Must run from site directory (where
wire.ymllives). comparesyntax:vendors/X vendors/Y(no "vs" between paths).consolidaterequires--topicCLI argument (no default).--langis required for Chief commands on multi-language sites. Exception:dataruns all languages automatically when--langis omitted.refine()saves content first, then archives news. Archive failure is safe..wire/progress-*.jsoncleaned on completion. Stale ones from crashes are harmless.
Build Gotchas
site_name,site_url,langare required in wire.yml. Build refuses without them.logo,faviconare required in wire.yml (path to file orfalse).extra.wire.labelsis required:on_this_page,related_articles,read_more(all sites);discovery_start,discovery_read(sites with steps.md).extra.wire.sidebar_ctais required. Build refuses withouttitle,description,link,link_text.extra.wire.article_ctais required. Build refuses withouttitle,description,link,link_text.header.offeringsrequired on sites with topics. String resolves from topic name.falseto disable.header.ctarequired on sites with topics. Build refuses without it.falseto disable.header.cta.urlmust not duplicate a content nav URL. CTA is its own nav element.- CTA page must have
layout: landing. Build refuses otherwise. - Offerings child pages must have
layout: landing,page, orarticle. navis required. No auto-discovery. Site structure is a strategic decision.- Nav sections in alphabetical order (3+) = BUILD REFUSED (uncurated). Skipped when offerings configured.
resolve_offerings()runs AFTERcheck_nav_orphans(). Do not reorder.validate_nav_config()runs AFTERresolve_offerings(). Nav is already filtered when checked.redirect:in frontmatter causes BUILD REFUSED (use wire.ymlredirects:instead).- Unknown frontmatter keys cause BUILD REFUSED.
short_titletrailing preposition/punctuation causes BUILD REFUSED (incomplete nav label).- Numbered slugs (e.g.,
2-foo/) cause BUILD REFUSED (use descriptive slugs; date-slugs YYYY-MM-DD allowed). createddate on January 1st causes BUILD REFUSED (placeholder detection).- Migration artifacts (*.py at site root) cause BUILD REFUSED.
- Empty topic dir (layout: page, no content children) causes BUILD REFUSED.
- Search page + icon are auto-generated. Disable with
search: false.
Multi-Language Gotchas
- Language asymmetry (<20% of default) causes BUILD REFUSED (ghost-town translation).
- Duplicate titles across languages causes BUILD REFUSED (untranslated content detection).
- Untranslated page body in wrong language dir causes BUILD REFUSED (stopword detection, min 50 words, supports: de, en, es, fr).
- Unsupported language in Gate 14 causes BUILD REFUSED (must add stopwords to
_STOPWORDSin build.py). - Stale per-language DBs (
.wire/{lang}/gsc.db) cause BUILD REFUSED (delete old DBs, re-rundata). - Unified GSC at
.wire/gsc.dbwithlangcolumn. - Progress at
.wire/{lang}/progress-*.jsonfor multi-language.
Image Gotchas
- Missing
BFL_API_KEY=RuntimeError(hard crash, not silent). - Missing image at build time =
FileNotFoundError(build error). - Images stored as normal files at
docs/assets/images/{slug}.png. No cache layer. - BFL API: POST to
api.bfl.ai/v1/{model}, poll until Ready, download signed URL.
Wire Metadata
stamp(**fields)setsdate=today,createdfrom git, merges kwargs.- Workflow gates: MERGE then DIFFERENTIATE then REFINE then REWORD (cascade blocking).
_overlap_slugs(topic)pre-computes merge/diff slug sets for gate checks._maybe_rediscover()auto-regeneratessteps.mdafter refine/seo/merge/differentiate/improve.
Multi-Language
Wire supports multi-language sites with per-language directories and shared configuration.
Setup
Add to wire.yml:
languages:
- code: en
name: English
docs_dir: docs/en
default: true
- code: de
name: Deutsch
docs_dir: docs/de
Each language gets its own docs_dir. Pages are matched across languages by URL path convention (same relative path = same content in different language).
Alternate Frontmatter
For pages with different slugs across languages, use alternate: in frontmatter:
---
title: About Us
alternate:
de: /de/ueber-uns/
---
Alternate mappings must be reciprocal. If EN points to DE, DE must point back to EN. Non-reciprocal mappings cause BUILD REFUSED.
Language Switcher
Wire auto-generates hreflang tags and a language switcher component.
Pages without explicit alternate: get homepage links for other languages.
Content Commands with --lang
Content commands require --lang on multi-language sites:
python -m wire.chief refine topics --lang de
python -m wire.chief reword topics --lang en
Exception: data runs all languages automatically when --lang is omitted.
Translate Command
For pages that need translation:
python -m wire.chief translate --lang de
Translates page bodies to match their language directory at roughly $0.01/page.
Multi-Language Scaffold
To convert a single-language site to multi-language:
python -m wire.chief migrate-lang --from en --add de --add fr
This moves docs, generates redirects, and updates wire.yml.
Multi-Language Build Output
- Root
/index.htmlredirects to default language - Each language builds to
/{code}/(e.g.,/en/,/de/) - Per-language sitemap, RSS, search index, and llms.txt
- Shared assets go to root output directory
Multi-Language Validation Gates
- Language asymmetry: every language must have at least 20% of the default language's page count
- Orphan language dirs: language directories with content must be in wire.yml config
- Orphan language config: configured languages must have existing directories
- Duplicate titles: same title in multiple languages = likely untranslated
- Untranslated bodies: stopword analysis detects wrong language in wrong directory (supports de, en, es, fr)
- Alternate validation: targets must exist, mappings must be reciprocal
Configuration (wire.yml)
wire.yml is the central configuration file. It must exist in the site root directory.
Required Top-Level Keys
| Key | Type | Purpose |
|---|---|---|
site_name | string | Site name (used in title, header, JSON-LD) |
site_url | string | Full production URL (e.g., https://example.com) |
lang | string | Site language code (e.g., en, de). Not needed if languages is set. |
nav | list | Navigation structure. No auto-discovery. |
logo | string or false | Logo image path (relative to docs_dir) or false for text-only |
favicon | string or false | Favicon path (relative to docs_dir) or false for none |
Optional Top-Level Keys
| Key | Type | Default | Purpose |
|---|---|---|---|
description | string | "" | Site description for meta tags |
docs_dir | string | "docs" | Path to markdown source directory |
templates_dir | string | "templates" | Path to custom templates directory |
output_dir | string | "site" | Path to build output directory |
theme | dict | {} | Theme configuration |
exclude | list | [] | File patterns to exclude from build |
footer | dict | {} | Footer configuration |
search | bool | true | Enable/disable search page generation |
extra_css | list | [] | Additional CSS files to include |
header | dict | {} | Header configuration (offerings, CTA, links) |
languages | list | none | Multi-language configuration |
redirects | list | [] | URL redirects |
copy_dirs | list | [] | Directories to copy verbatim to output |
deploy | dict | {} | Deployment configuration |
exclude_gsc_data_for_path | list | [] | Path prefixes skipped by GSC coverage check |
Header Configuration
header:
offerings: vendors # Topic name for offerings bar (Row 1)
cta: # CTA button in sticky nav (Row 2)
label: Get Started
url: /getting-started/
links: # Custom links in Row 1 (external auto-open in new tab)
- title: Blog
url: https://blog.example.com
extra.wire Configuration
extra:
wire:
sidebar_cta:
title: "Your CTA title"
description: "Short description"
link: /your-page/
link_text: "Learn more"
article_cta:
title: "Ready to get started?"
description: "Book a free consultation."
link: /contact/
link_text: "Get in touch"
labels:
on_this_page: "On This Page"
related_articles: "Related Articles"
read_more: "Read More"
discovery_start: "Start" # Only needed with discovery steps
discovery_read: "Continue" # Only needed with discovery steps
refresh_days: # Per-topic news refresh interval
vendors: 21
default_refresh_days: 21 # Default news refresh interval
reword_tiers:
full: 20 # Top 20% get full reword
light: 30 # Next 30% get light reword
max_articles: 20 # Max articles per news search
rate_limit_delay: 1 # Seconds between API calls
min_opportunity_score: 15.0 # Minimum score for SEO opportunities
newsweek_batch_chars: 150000 # Batch size for newsweek extraction
newsweek_max_news_age_days: 14 # Max age of news for newsweek
image_style: "Your image style" # Style prompt for AI image generation
image_sizes: # Custom image presets
default: [1024, 576]
wide: [1440, 480]
square: [512, 512]
issues_repo: "org/repo" # GitHub repo for filing issues via skills
form_id: "abc123" # Wire form submission handler ID
Multi-Language wire.yml Configuration
extra:
wire:
sidebar_cta:
en:
title: "Your CTA"
description: "Description"
link: /en/your-page/
link_text: "Learn more"
de:
title: "Ihr CTA"
description: "Beschreibung"
link: /de/ihre-seite/
link_text: "Mehr erfahren"
article_cta:
en:
title: "Ready?"
description: "Book a consultation."
link: /en/contact/
link_text: "Get in touch"
de:
title: "Bereit?"
description: "Beratung buchen."
link: /de/kontakt/
link_text: "Kontakt"
Nav Configuration
nav:
- Vendors:
- vendors/vendor-a/
- vendors/vendor-b/
- Guides:
- guides/getting-started/
- guides/configuration/
- About: about/
Navigation is explicit. Wire does NOT auto-discover pages. Pages not in nav cause BUILD REFUSED (nav orphan check). Wire auto-discovers children within declared topic sections. Topic sections with 3+ items in alphabetical order cause BUILD REFUSED (uncurated).
Redirects Configuration
redirects:
- from: /old/path/
to: /new/path/
status: 301 # Default: 301 (permanent redirect)
- from: /removed/page/
to: ""
status: 410 # 410 Gone
Redirects are also read from .wire/redirects.yml (internal Wire moves).
Both sources are merged. wire.yml wins on conflict.
Exclude Patterns
exclude:
- '*/comparisons/*' # Auto-generated comparisons
- '*/archive/*' # Archived content
Footer Configuration
footer:
copyright: "2025 Company Name"
links:
- title: Imprint
url: /imprint/
- title: Privacy
url: /privacy/
Dataclasses and Key Objects
Wire uses these core data structures throughout:
Site
@dataclass
class Site:
title: str # From wire.yml site_name
url: str # From wire.yml site_url
description: str # From wire.yml description
Auto-injected as {site} in all prompts.
Content
@dataclass
class Content:
slug: str # "abbyy" -- folder name
title: str # "ABBYY" -- H1 from index.md
path: str # "/vendors/abbyy/" -- absolute URL path
summary: str # First paragraph / description
Key methods:
item.read_index(): Read the page's index.md contentitem.save_index(content, preview=False): Save content (with validation, sanitization, quality warnings)item.index_path: Path to index.md fileitem.fs_path: Filesystem path to item directoryitem.topic: Topic directory name (e.g., "vendors")Content.from_path(directory, slug): Create from pathContent.from_location("vendors/abbyy"): Create from location string
Topic
class Topic:
directory: str # "vendors"
title: str # From index.md frontmatter
description: str # From index.md frontmatter
Key methods:
topic.list(): All Content items in this topictopic.get(slug): Single Content item by slugtopic.needing_news(days=21): Items needing news refreshtopic.find_news(date_str): News files for a date
Module-Level Globals
These are set at import time from wire.yml:
SITE: Site object with title, url, descriptionDOCS_DIR: Path to docs directorySITE_DIR: Path to site root (where wire.yml lives, alwaysPath.cwd())WIRE_CONFIG: Dict fromextra.wiresection of wire.ymlTOPIC_LANGUAGES: Per-topic language mappingSITE_LANGUAGE: Site default language name
Prompt System
Wire uses a 3-layer prompt system for AI content generation.
How Prompts Load
load_prompt(topic, action, output="markdown_file", **variables) does three things:
- Prepends the styleguide (
_style.md). Rules apply to every prompt - Loads the topic prompt from
{docs_dir}/{topic}/_{action}.mdor falls back towire/prompts/{action}.md - Calls
.format(**variables)with auto-injected{topic}and{site}
Auto-Injected Variables
NEVER pass these manually (causes TypeError: got multiple values):
{topic}: Topic object (has.title,.description){site}: Site object (has.title,.url,.description)
Variable Patterns
{item.title}: When variable is a dataclass (Content object){item_name}: When variable is a string- Never mix patterns in the same prompt
Escaping
Python .format() treats { and } as variable delimiters:
{{#slug}}for HTML anchor IDs like{#slug}{{Brand}}for literal braces in output examples
Output Formats
"markdown_file": Outputs frontmatter + body only (for content pages)"decision": Prefixes withWHY: RELEVANTorWHY: NOT_RELEVANT"none": No format instructions appended
Content Quality System (3 layers)
- PREVENT (
_style.md): Styleguide teaches Claude rules (title format, linking, citations) - FIX (
_sanitize_content()): 9 auto-fixes (pipe to dash, H1 dedup, dedup links, broken links) - DETECT (
_warn_content_quality()+_audit_content_quality()): Returns dict of issues
Prompt Structure
Every prompt follows this skeleton:
## Your Role: [Specific Role] for [Context]
[1-2 sentences: what this prompt accomplishes]
## [Instructions / Rules]
[Numbered rules with BAD/GOOD examples]
## [DATA SECTION] [Label: {variable}]
{variable_content}
Prompt Authoring Rules
- Start with
## Your Role:and a specific role title - Reference
{site.title}or{topic.title}in the role line - Use consistent variable patterns
- Escape literal braces as
{{and}} - Data sections use
## LABEL [Context: {variable}]headers - Wrap page content in markdown code fences
- Do NOT duplicate rules from
_style.md - No hardcoded site names or domains
- Include BAD/GOOD examples for non-obvious rules
Newsweek Prompts
Newsweek prompts use _load_newsweek_prompt() which:
- Injects
{site}only (no{topic}) - Does NOT prepend the styleguide automatically
- 3-phase map-reduce: EXTRACT, SYNTHESIZE, REVIEW
GSC Database
Wire stores Google Search Console data in a local SQLite database.
Location
- Single-language:
.wire/gsc.db - Multi-language:
.wire/gsc.db(unified, withlangcolumn on Content table) - Stale per-language DBs (
.wire/{lang}/gsc.db) cause BUILD REFUSED
Tables
| Table | Purpose |
|---|---|
| Content | Page-level metrics (impressions, clicks, position) |
| Keyword | Per-keyword metrics per page |
| Snapshot | Data freshness tracking |
| GscUrl | Bulk URL discovery data (dimensions=['page']) |
Key Operations
fetch_and_store(): ONLY called bydatacommand. All other commands are DB-read-only.discover_urls(): Bulk GSC fetch, stores in GscUrl table, 28-day caching.find_overlaps(): Detect keyword cannibalization between pages.find_dead_pages(): Pages with very low impressions (acceptsredirects=parameter).find_missing_redirects(): GSC paths with no page and no redirect.trending_keywords(): Keywords gaining position/impressions.
Overlap Classification
- Hard overlap: ratio > 0.4, skew > 0.7, same topic = MERGE
- Soft overlap: ratio > 0.15 = DIFFERENTIATE
- Monitor: everything else
Redirect-Aware Operations
load_redirects(site_dir): Reads both.wire/redirects.ymlANDwire.yml redirects:key (merged, wire.yml wins)rekey_from_redirects(redirects, lang=): Rekey Content rows to match redirect targets after restructurefind_dead_pages()acceptsredirects=: Excludes pages with known redirect sources
Build Guard: GSC Coverage
Every GscUrl with impressions must have a page, redirect, or 410 entry.
Build is blocked otherwise.
Configure exclude_gsc_data_for_path in wire.yml for web apps on the same domain that are not Wire content.
Audit Output
The audit command produces a structured report with cascade filtering.
Report Structure
- HEALTH: Dead pages, redirect gaps, stale data
- ACTION: Merge/differentiate recommendations (cascade: merge then diff then news then refine then reword)
- SEO: Keyword opportunities, position improvements
- INFO: Topic summaries, page counts
Three-State Classification
- Dead: Truly low impressions, no meaningful traffic
- Missing redirect: Has impressions but no page (needs redirect or 410)
- Untracked: No GSC data at all
Templates and Assets
Template Fallback
Jinja2 FileSystemLoader chain:
- Customer
templates/directory (first priority) wire/templates/defaults shipped with pip (fallback)
Customers do NOT need their own templates or CSS. Defaults ship with pip.
Asset Fallback
- Copy
wire/assets/(defaults) first - Overlay customer
docs/assets/on top
Customer assets override defaults for individual files.
Template Types
| Template | Used for |
|---|---|
page.html | Default page layout |
article.html | Content pages (auto-selected for pages with topic+slug). IBM Think-style 3-column (TOC, Content, Sidebar). |
landing.html | Landing pages. Full customer freedom (no Wire H1, no width constraints). |
home.html | Homepage with hero section. |
base.html | Base template with header, footer, scripts. |
Landing Page Behavior
_landing_sections()splits at<hr>into alternating.section-*bands (hero/light/default)- No dark/accent sections. Customers own their own dark styling via custom CSS.
- Customer owns the H1. Wire does NOT inject one.
- If first landing section contains
<section class="section-hero ...">, Wire skips its wrapper
JavaScript Files Shipped
scroll-title, toc-spy, back-to-top, reading-progress, code-copy, discovery, yt-embed, tabs, forms
Two-Row Header
- Row 1: Offerings bar (scrolls away) + optional custom links
- Row 2: Content nav (sticky) + CTA button
Build Output Files
Wire auto-generates these files during build:
sitemap.xmlrobots.txtfeed.xml(RSS)feed_rss_created.xml(RSS sorted by creation date)search_index.json(search page data)llms.txtandllms-full.txt(AI-readable site summaries).htaccess(Apache redirect rules)404.htmlCNAME(if configured)bot/directory (agent-facing site context)
Content Quality System
Layer 1: PREVENT (styleguide)
docs/_styleguide.md is prepended to every prompt via _style.md.
Teaches Claude: title format, linking style, citation style, editorial voice, prose over lists.
Layer 2: FIX (sanitize)
_sanitize_content() runs 9 auto-fixes on every save:
- Pipe characters to dashes
- H1 dedup (remove extra H1s)
- Dedup links (same URL linked multiple times in one paragraph)
- Broken internal link repair
- And more
Layer 3: DETECT (warn + audit)
_warn_content_quality(): Returns dict of quality issues (warnings, does not block).
_audit_content_quality(): Returns dict of issues for audit reporting.
Merge Guard
Merge output must be at least 80% of keeper body length. Below that = preview only (no save).
Source Diversity
analyze_source_diversity(): Flags dominant sources.
Threshold: (>3 citations AND >20% from one source) OR (>40% from one source AND >=5 total citations).
File Layout
Site Root
site-root/
wire.yml # Required. Central configuration.
docs/ # Markdown source (configurable via docs_dir)
_styleguide.md # Required for content generation
index.md # Homepage
topic-a/ # Topic directory
index.md # Topic index page (needs layout: page, short_title)
item-1/ # Content item directory
index.md # Content page (needs created, short_title)
2025-03-15.md # Pending news file (integrate with refine)
news/ # Archived news (after refine)
item-2/
index.md
assets/ # Customer assets (override defaults)
images/ # Images including AI-generated ones
css/ # Custom CSS
templates/ # Customer template overrides (optional)
site/ # Build output (configurable via output_dir)
.wire/ # Wire internal data (gitignored)
gsc.db # GSC SQLite database
redirects.yml # Wire-internal redirect moves
progress-*.json # Batch progress tracking (cleaned on completion)
Multi-Language Layout
site-root/
wire.yml
docs/
en/ # English docs directory
index.md
topic-a/
index.md
de/ # German docs directory
index.md
topic-a/
index.md
site/
en/ # English build output
de/ # German build output
index.html # Root redirect to default language
Naming Conventions
- Directories: lowercase, hyphens (e.g.,
getting-started/) - No numbered prefixes on directories (e.g., NOT
2-setup/) - Date-based slugs allowed for news:
YYYY-MM-DD-title/ - Underscore-prefixed files are excluded from build:
_style.md,_styleguide.md - Pending news files:
YYYY-MM-DD.md(processed byrefinecommand)
Newsweek (Weekly Market Intelligence Report)
3-Phase Pipeline
- EXTRACT (Phase 1): Evaluate news articles for each topic. Cache at
.wire/newsweek_extracts_{date}.md. - SYNTHESIZE (Phase 2): Map-reduce extracts into weekly report.
- REVIEW (Phase 3): Editorial review and polish.
Output
docs/news/YYYY-MM-DD-news.md
Shortcuts
python -m wire.chief newsweek --resynth # Skip Phase 1, reuse cached extracts
python -m wire.chief newsweek --skip-review # Skip Phase 3 editorial review
python -m wire.chief newsweek --resynth --skip-review # Fastest iteration on Phase 2 prompt
Redirects
Sources
Wire reads redirects from two locations (merged, wire.yml wins on conflict):
.wire/redirects.yml: Internal Wire moves (auto-generated by merge, restructure commands)wire.ymlredirects:key: User-configured redirects
Format
# List format (preferred):
redirects:
- from: /old/path/
to: /new/path/
status: 301
- from: /removed/
to: ""
status: 410
# Dict format (legacy):
redirects:
/old/path/: /new/path/
Status Codes
| Status | Meaning | When to use |
|---|---|---|
| 301 | Permanent redirect (default) | Page moved to new URL |
| 410 | Gone | Page permanently removed, no replacement |
Build Guard
Every GSC URL with impressions must be covered by a page, redirect, or 410 entry.
Use exclude_gsc_data_for_path for web apps that share the domain but are not Wire content.
Redirect Conflict Detection
If a redirect source path also has a docs/ file, build REFUSES (ambiguous state).
Delete the file or remove the redirect.
Redirect Chain/Dead-End Detection
Audit detects redirect chains (A redirects to B, which redirects to C) and dead-end redirects (redirect target does not exist).
Migrate GSC After Restructure
After restructuring pages with redirects:
python -m wire.chief migrate-gsc
This rekeys Content rows in the GSC database to match redirect targets.
Enrich Command
The enrich command is a 4-phase content improvement pipeline:
- Analyze (free): Local keyword presence, BM25, keyword routing
- Filter: Select pages with actionable improvement potential
- Web Research (~$0.01/page): Research specific gaps
- Improve (~$0.06/page): Apply improvements to content
Position threshold: > 5 (not > 3, due to ads and YouTube results pushing organic down).
python -m wire.chief enrich vendors
Images Command (BFL/FLUX)
Shortcode
!makeIMG[document-processing]{wide}
Presets
| Preset | Size |
|---|---|
default | 1024x576 |
wide | 1440x480 |
square | 512x512 |
Configurable via extra.wire.image_sizes in wire.yml.
Two-Layer Prompt
image_stylefrom wire.yml (site-wide style guidance)- Slug (hyphens converted to spaces) as subject
Storage
Images at docs/assets/images/{slug}.png. No cache layer.
CSS class: .wire-img.
CLI
python -m wire.chief images # Generate missing images
python -m wire.chief images --force # Regenerate all
python -m wire.chief images --prune # Remove orphan images
Requirements
BFL_API_KEYenvironment variable (hard crash without it)- Missing image at build time =
FileNotFoundError
Build Command
Separate module from Chief:
python -m wire.build # Full build
python -m wire.build --dirty # Only modified pages
python -m wire.build --serve # Build + dev server
python -m wire.build --serve --port 8080 # Custom port
Pipeline
markdown -> frontmatter parse -> mistune render -> Jinja2 template -> HTML
Features:
- Parallel rendering (8 workers)
- HTML minification (strips comments, collapses whitespace)
- CSS minification
- Auto-lint after build (no opt-out)
- Auto-QA after build
- Link sculpting (absolute to relative URL conversion)
- H1 dedup (strips first H1 from rendered HTML on page/article layouts)
- TOC extraction (h2/h3, inline details, requires 3+ headings)
- Reading time calculation (220 words/minute)
- Image dimension injection (auto width/height attributes)
- Code copy buttons
- Back-to-top button
- Reading progress bar
- Smooth scroll with header offset
- External link indicator (CSS-only arrow)
base_path
Auto-derived from site_url via urlparse().path.
Enables GitHub Pages subpath deployments (e.g., https://user.github.io/repo/).
Dirty Build
Only rebuilds pages modified since last build.
Timestamp tracked via .build_timestamp marker file in output directory.
Excluded Files
These patterns are automatically excluded from build:
*/YYYY-MM-DD.md(pending news files)_*.md(underscore-prefixed files like_style.md)*/comparisons/*(auto-generated comparisons)*.steps.md/steps.md(discovery step files)
Development
Install
pip install -e ".[dev]" # Development install with test deps
Test
pytest tests/ # Run all tests
Build This Site
python -m wire.build # Build from repo root (this IS a Wire site)
Test Patterns
tmp_path+patch('wire.tools.DOCS_DIR')for filesystem isolationcontent.pytests: patch BOTHwire.tools.DOCS_DIRandwire.content.DOCS_DIRtest_db.py:seeded_dbfixture with 5 pages for overlap scenariosTestLoadPrompt: autouse fixture creatingdocs/vendors/index.mdintmp_path- Tests that assert
claude_textcall count needpatch.object(ContentEditor, "_maybe_rediscover")
Visual QA
from wire.qa import capture_screenshots
capture_screenshots(site_dir, output_dir, pages=['guides/foo'])
Requires: pip install playwright && playwright install chromium
Claude AI Integration
claude_text(prompt, description, max_tokens=2000, temperature=0.3): CLI-first dispatcher_run_cli(prompt, description, tools): Single entry point for CLI subprocess callsweb_search(query, max_results, context, exclude_domains): CLI-first with--tools WebSearch- CLI requires
claude login(Max subscription) - API fallback: auto-streams for prompts >50KB or max_tokens >16K
- API model:
claude-sonnet-4-6
Fixing Lint Issues in Content
When wire.build reports lint issues, fix the content, not the rules. The rules are evidence-based. Every error message tells you what is wrong and how to fix it.
Workflow
# Build once, capture all issues to a file
python -m wire.build 2>&1 | grep -E '\[RULE-' -A1 | grep -v "^--$" > lint.txt
# Read the file. Fix content. Rebuild.
# Do NOT rebuild after every file. Batch your fixes.
Common patterns
Page titles: use : not - as separator. Dashes in titles are best practice for SERPs (Zyppy study), but Wire renders page titles as link text in sidebar and related articles. The spaced hyphen - triggers RULE-52 inside that link text. Colons don't.
BAD: title: SEO Reference - Thresholds and Evidence
GOOD: title: "SEO Reference: Thresholds and Evidence"
Every article page needs links: 2 sibling links (pages in the same topic), 1 link to the topic index page, and 1 external citation. Add them on first mention of the concept, not as a list at the bottom.
Wire's prompt system determines what Claude sees for every content
operation. See [Writing for Wire](/guides/writing-for-wire/) for
editing guidance and the [Guides overview](/guides/) for all docs.
External citations: use real, authoritative sources relevant to the page content. Google, NNG, SearchPilot, W3C, MDN, GitHub. Not generic links.
Math formulas: no spaces around operators inside backticks. (1-CTR) not (1 - CTR). The spaced minus triggers RULE-52.
Card shortcodes: each card in a :::cards block should link to a DIFFERENT page. Multiple cards pointing to the same URL triggers RULE-81 (conflicting anchor text).
Code examples: use placeholder URLs like /your-topic/slug/ not real site URLs. Real URLs in code fences render as links and trigger lint rules.
PDF links: add (PDF) to the anchor text so visitors know they are downloading. [Title (PDF)](url.pdf) not [Title](url.pdf).
Headings: use parentheses not dashes. ## Module (Description) not ## Module - Description. Same RULE-52 reason as titles.
Troubleshooting: if X fails, try Y
RULE-52 keeps firing after you fixed the markdown: the - is in a page title that appears as link text on OTHER pages (sidebar, related articles). Fix the title in frontmatter, not the body text. Use : instead of -.
RULE-52 fires on a math formula in backticks: the HTML parser splits text nodes around <code> tags. The spaced minus leaks into adjacent text. Fix: remove spaces around minus (1-CTR) not (1 - CTR).
RULE-81 fires on card shortcodes: you have multiple :::cards entries linking to the same URL with different card descriptions. Each card MUST link to a different page. If you're demoing cards, use real different destinations, not the same page 3 times.
RULE-81 fires on pricing CTAs: "Get started" and "Start free trial" both point to the same page. Point the secondary CTA to a different page (e.g., managed service, workflow, about).
RULE-71 says "needs topic index link" but you added one: check if the link renders with the correct href. Links in the last line of a file sometimes don't render if there's no trailing newline. Ensure the file ends with \n.
RULE-85 says "no external links" but you added one: if the link points to a PDF, it may count for RULE-85 but trigger RULE-102 (missing PDF indicator). Add (PDF) to anchor text.
Lint count goes UP after fixing: you probably introduced broken links (RULE-33) or trailing slash issues (RULE-22) with anchor fragment links like /page/#section. Wire checks that the target page exists but anchor fragments on pages with trailing slashes cause false positives. Point to the page URL without fragments.
Build says "0 lint issues" locally but CI fails: check for !makeIMG shortcodes in headings. ### !makeIMG[slug] is parsed as an image reference. Wrap in backticks: ### `!makeIMG[slug]`.
You fixed 30 pages but lint count barely changed: many RULE-52 hits come from the SAME source (a page title with - that appears as link text on 20 other pages). Fix the title once, all 20 hits disappear.
What to do when the rule is wrong for your content
If a rule fires but the content is genuinely correct (a page about "prompts" repeating "prompt", a reference page with many short sections), that is a rule-level issue, not a content issue. Do not change correct content to satisfy a rule. File a ticket, document the issue, and move on. Rule changes get their own ticket with evidence research.