On this page
- Module Map
- wire/build.py (Static Site Generator)
- wire/lint.py (Build-Time Content Linter)
- wire/qa.py (Automated Quality Assurance)
- wire/discovery.py (Discovery Reading System)
- wire/content.py (Content Operations)
- wire/news.py (News Intelligence)
- wire/chief.py (Batch Orchestrator)
- wire/analyze.py (Local Analysis)
- wire/gsc.py + wire/db.py (Data Layer)
- wire/wp.py (WordPress Migration)
- Why Twelve Modules, Not One
Wire consists of fourteen modules. Each has a single responsibility. They share data through function calls and dataclasses, not through global state or message passing. For the architectural overview and design philosophy, start there. The data model explains the dataclasses, and the self-assessment documents where the architecture falls short. Wire's rendering engine uses mistune for Markdown and Jinja2 for templates.
Module Map
wire/
tools.py # Shared infrastructure (Claude API, I/O, dataclasses)
build.py # Static site generator (replaces MkDocs)
lint.py # Build-time HTML content linter (91 rules)
qa.py # Automated QA (SEO, links, accessibility, reports)
discovery.py # Discovery reading system (guided content)
content.py # Content operations (create, refine, seo, merge)
news.py # News gathering and evaluation (junior/senior)
chief.py # Batch orchestrator (data, audit, reword, etc.)
analyze.py # Local analysis (zero AI calls)
gsc.py # Google Search Console integration
db.py # SQLite database operations
schema.py # Frontmatter validation contract
wp.py # WordPress migration tool
migrate.py # Backfill created dates from git history
build_dev.py # Dev server with file watching
wire/build.py (Static Site Generator)
Single-pass markdown-to-HTML pipeline. Replaces MkDocs entirely.
python -m wire.build --site . --serve
# Builds, lints, and starts dev server at localhost:8000
Pipeline: wire.yml config → collect pages → parse frontmatter → render markdown (mistune) → inject discovery layer → apply Jinja2 template → write HTML → copy assets → generate sitemap.xml + robots.txt + feed.xml → auto-lint → optional QA.
Custom renderer: WireRenderer(mistune.HTMLRenderer) handles link sculpting (first 3 external links dofollow, rest nofollow), heading anchors (auto-generated id attributes), code block language classes, and raw HTML passthrough. The base_path parameter (auto-derived from site_url via urlparse().path) prefixes all internal links. This is how GitHub Pages subpath deployments work without manual URL rewriting.
Template fallback: build_jinja_env() creates a Jinja2 FileSystemLoader chain: customer templates/ directory first, then wire/templates/ as fallback. Override one template without touching the rest. See adding a site for how Wire's own docs use this.
Asset fallback: copy_assets() copies wire/assets/ first (defaults), then overlays customer docs/assets/ on top. Customer files with the same name replace defaults.
Generated outputs beyond HTML:
sitemap.xml: all pages with lastmod datesrobots.txt: references sitemap, allows all crawlersfeed.xml: RSS 2.0 feed with the 20 most recent pages (sorted bycreated), RFC 822 dates, compatible with follow.it and other feed readerssearch_index.json: full-text search index for client-side searchllms.txt: LLM-readable site summary- JSON-LD:
build_jsonld()generates Article, WebSite, or CollectionPage schema based on page type
Automatic page features (zero config, no template changes needed):
- Table of contents: extracted from h2/h3, shown when 3+ headings, collapsed on mobile
- Reading progress bar: thin primary-color bar at top tracking scroll position
- Back-to-top button: appears after 400px scroll
- Code copy buttons: "Copy" on hover over
<pre>blocks - Scroll title: header shows page H1 after scrolling past it
- External link indicator:
↗after links withtarget="_blank"(CSS-only) - Smooth scrolling:
scroll-behavior: smoothwith header offset for anchor links
Theming: CSS custom properties injected from wire.yml theme: section. Clients change colors and fonts through config, with no CSS editing.
theme:
primary_color: "#059669"
accent_color: "#f59e0b"
font_family: "'Inter', sans-serif"
Performance: ThreadPoolExecutor with 8 workers. Dirty builds skip unchanged files. Large sites (500+ pages) build in seconds.
CLI flags: --dirty (incremental), --serve (dev server), --strict (warnings also block), --port N. Lint and QA always run. There is no opt-out.
wire/lint.py (Build-Time Content Linter)
58 rules across 15 groups. Every rule is a hard failure with a specific fix. Runs automatically after every build. See the full rule reference for what each check does and why it exists.
python -m wire.lint --site site/ --rules RULE-01,RULE-05
Groups: Metadata (RULE-01 to RULE-11), Canonicalization (RULE-12, 13, 15, 16), URL Structure (RULE-17 to 22), Sitemap (RULE-23 to 26), robots.txt (RULE-28 to 31), Internal Links (RULE-33 to 35), Security (RULE-37), Images (RULE-40 to 42), Hreflang (RULE-43, 44, 45), Structured Data (RULE-46 to 48), Indexability (RULE-49, 50).
Key design: _real_tags(soup, tag_name) filters out tags inside <code> and <pre> blocks. Without this, code examples trigger false positives for missing H1, heading hierarchy violations, and image alt text.
Cross-page checks: Duplicate title detection, duplicate H1 detection, orphan page detection (no inbound links), sitemap coverage. These require the full site context, not just a single page.
wire/qa.py (Automated Quality Assurance)
HTML-based QA that replaces manual browser review. Checks every page for SEO metadata, broken links, accessibility basics, and HTML quality.
python -m wire.qa --site site/ --full
# QA: 17 pages checked. All passed.
# Report: qa/report.html
Checks: check_seo() (title, description, H1, canonical, viewport), check_links() (broken internal links, masked link validation, empty anchors), check_accessibility() (image alt, target=_blank without noopener, missing lang), check_html_quality() (DOM node count, inline styles).
Report: generate_report_html() produces a visual HTML report with color-coded pass/fail. Engineers open one file. If green, ship it.
Sampling: Full mode checks every page. Default mode samples 20 random pages plus the homepage. Keeps QA fast for large sites.
wire/discovery.py (Discovery Reading System)
Interactive guided reading layer for long-form content. Increases dwell time without changing the article itself.
docs/guides/getting-started/
index.md # Full article (unchanged, always in DOM)
steps.md # Discovery layer (optional companion file)
Parser: parse_steps() converts :::hook/:::step/:::choices markdown syntax into a JSON structure. render_discovery_html() generates DOM elements with data-* attributes. extract_h2_ids() extracts H2 anchor IDs from markdown using the same slugification as WireRenderer.heading() in build.py.
Behavior: Hook text and two buttons appear above the article. "Guide me through this" fades in interactive step cards. "Read the full article on this" scrolls to the matching section with a highlight. All content is in the DOM on load. Works without JavaScript.
Integration: wire.build auto-detects steps.md files and injects the discovery HTML before the article content. The discovery.js script handles transitions. See the discovery system guide for authoring steps.
wire/content.py (Content Operations)
Every operation that creates or modifies a single page.
Commands: create(), refine(), expand(), compare(), consolidate(), seo(), seo_light(), crosslink(), merge(), differentiate(), improve().
Each method follows the same pattern: gather context → build prompt → call Claude → validate output → save through the sanitize pipeline. The improve() method combines multiple operations into one Claude call based on a pre-built amendment brief from analyze.py. After saving, refine(), seo(), merge(), differentiate(), and improve() call _maybe_rediscover() to auto-regenerate discovery steps when H2 anchors change and steps.md already exists.
wire/news.py (News Intelligence)
Junior-senior pattern for evaluating article relevance.
analyze_article() performs junior evaluation. Claude reads one article and decides if it is relevant. Returns ArticleEvaluation or None. Evaluations run in parallel via ThreadPoolExecutor.
combine() handles senior synthesis. All relevant junior reports combined into an executive summary.
gather_news() orchestrates the pipeline: web search, fetch articles, junior evaluation, senior synthesis, save as pending news file.
wire/chief.py (Batch Orchestrator)
Coordinates multi-page operations across entire topics.
Commands: data, audit, deduplicate, news, refine, reword, consolidate, crosslink, sanitize, enrich, newsweek.
Chief handles progress tracking (resume on interruption), cascade filtering (merge blocks differentiate blocks refine blocks reword), input validation, and reporting. It calls content.py, news.py, analyze.py, and db.py but never accesses Claude directly.
wire/analyze.py (Local Analysis)
Zero API calls. All functions use GSC database queries and local math.
keyword_routing() decides how to handle a keyword: add section, add substantial content, mention with link, or create new page. Uses three signals: impression ratio, page breadth, and BM25 semantic fit.
build_amendment_brief() produces a complete improvement brief for one page. Checks gates (recently touched, no data, overlap-blocked) before analysis runs.
wire/gsc.py + wire/db.py (Data Layer)
GSC: get_search_terms() reads from database only. fetch_and_store() hits the API. Clean separation: reading is free, fetching is rate-limited.
Database: SQLite at .wire/gsc.db. Three tables (Content, Keyword, Snapshot). Key functions: find_overlaps(), find_content_gaps(), find_dead_pages(), keeper_score(), trending_keywords().
wire/wp.py (WordPress Migration)
Downloads WordPress pages and converts to Wire-compatible markdown. Extracts metadata from JSON-LD/Yoast/OG tags. Converts HTML to markdown with markdownify. Downloads images to local assets. See the migration guide.
Why Twelve Modules, Not One
Wire's module count is a design choice, not accidental complexity. Each module has exactly one reason to change.
analyze.py changes when keyword routing logic improves. content.py changes when a new content operation is needed. chief.py changes when batch orchestration logic evolves. db.py changes when the database schema changes. None of these changes affect each other.
This separation has a concrete benefit: the analysis module (analyze.py) makes zero API calls. Keyword presence analysis, BM25 scoring, keyword routing, and amendment brief generation all run locally. When Wire builds an amendment brief for 500 pages, it costs nothing. Only the content generation step in content.py hits the Claude API. Using your AI subscription, the generation step is precise. Claude receives a pre-built brief with specific instructions, not an open-ended prompt.
The module boundaries also enforce Wire's core principle: analyze first, act second. chief.py calls analyze.py before calling content.py. It calls db.py before calling gsc.py. The dependency graph flows from free operations to paid ones. No module can accidentally reverse this flow because the imports are unidirectional.
The static site generator (build.py) is deliberately separate from the content pipeline (content.py). Content operations produce markdown. Build operations produce HTML. Mixing these, as many CMS platforms do, creates situations where build failures corrupt content and content errors break builds. Wire's separation means a build failure never affects your markdown source files.
The linter (lint.py) runs after the build, not during content creation. This is intentional. Content creation uses Claude with styleguide rules (Layer 1 prevention). The linter checks the rendered HTML output (Layer 3 detection). Running the linter during content creation would be redundant; the styleguide already prevents the issues the linter checks for. Running it after build catches problems introduced by template rendering, markdown edge cases, or manual edits that bypassed Wire's pipeline.