Content Source Router
Concept of the skill
What it is: The project-specific dispatch contract that decides which content source reads a requested page or post.
Mental model: Treat each source as an adapter behind one explicit router; the router owns selection, not rendering.
Why it exists: A static site with local markdown, MDX, and CMS-synced content needs one place where source precedence is visible and testable.
What it is NOT: It is not the source-specific parser, the CMS sync job, or contributor-facing documentation.
Adjacent concepts: Content source adapters, route matching, fallback policy, routing audit logs.
One-line analogy: It is the switchboard that connects a content request to the right reader.
Common misconception: A router can safely fall back to "markdown" when uncertain; unknown source selection should surface as a coverage gap.
Coverage
- File-extension dispatch —
.md routes to the markdown source, .mdx routes to the MDX source, no extension or .cms.json routes to the CMS source
- Content-path prefix dispatch —
content/posts/** routes to local sources; content/cms-synced/** routes to the CMS source even if the file extension is .md
- Explicit source hints — internal callers (preview tools, manual reconciliation) pass an explicit
source parameter that bypasses inspection
- Coverage-gap surfacing — when no detection rule matches a request, the router returns a structured "unknown source" result; it never silently picks a default
- Adding a new source — the workflow for landing a fourth source (Notion API, Sanity, etc.) without breaking the existing three (registration, routing precedence, fixture test, end-to-end content-fetch sanity)
Philosophy of the skill
A content router is a dispatch surface that has to be exactly right or the rest of the site reads the wrong content. Every misroute is either a 404 (the user sees nothing) or a wrong-content render (the user sees a different post than the URL implies). The discipline is the same anti-default doctrine the skill-router applies to skills: prefer an explicit signal over an inferred one, prefer an unambiguous match over a "best guess," and prefer surfacing a coverage gap loudly over silently routing to a default.
Routing Rules
The router evaluates four signals in priority order. The first signal that produces an unambiguous winner stops the chain.
| Priority |
Signal |
Source |
Match rule |
| 1 |
Explicit source parameter |
Internal callers (preview, manual reconciliation) |
Exact match against the Source enum. Bypasses all subsequent inspection. |
| 2 |
Content-path prefix |
Inbound request path |
First matching prefix wins: content/posts/ → markdown; content/mdx/ → mdx; content/cms-synced/ → cms. |
| 3 |
File extension |
Resolved file path |
.md → markdown; .mdx → mdx; .cms.json → cms. |
| 4 |
Explicit source_hint in query string |
Trusted internal callers |
Last-resort hint; surfaced as a warning in the router's audit log. |
Coverage-gap behavior
If no signal produces a match, the router returns { ok: false, reason: 'unknown_source', evidence: {...} }. It does NOT fall back to a default source. The caller must handle the unknown-source case explicitly — typically by responding HTTP 404 and logging the full request shape for human triage.
Adding a new source
- Add the new source's path prefix and file extension to the priority-2 and priority-3 detection tables
- Add a source implementation in
lib/content/sources/<source>.ts that mirrors the markdown source's interface
- Add the new source to the
Source enum used at priority 1
- Add an end-to-end test that requests a fixture path matched by the new source and asserts the router selects it — without this, the router will silently fall through
Verification
- Run unit cases for every configured source and every unknown-source branch.
- Add a fixture request for each new path prefix, file extension, and explicit source hint.
- Confirm unknown inputs return a structured failure instead of selecting a default source.
Do NOT Use When
| Use instead |
When |
markdown-post-frontmatter-validation |
The task is the actual frontmatter parsing for the markdown source — the router decides which source to read from, the source-specific skill validates and parses |
debugging |
A specific routing decision is wrong in production logs and you need to reproduce |
documentation |
The task is writing a contributor doc about the routing architecture |
1---2# name: stable skill identifier. Match the skill directory name or the final namespace segment.3# Lowercase letters/numbers with hyphen, slash, or colon separators.4name: content-source-router5# description: routing-facing summary of what the skill covers and when it activates.6# Include concrete triggers and an explicit negative boundary; keep routing semantics out of prose-only ambiguity.7description: "Use when dispatching a content-fetch task across the multiple sources the site reads from — local markdown under `content/`, MDX with React components under `content/mdx/`, and a headless-CMS sync under `lib/cms/`. Activate this skill whenever the task says 'render this content' or 'where does this post come from' without naming a specific source, or when adding a new source to the routing surface. Do NOT use for the actual rendering of one source (use the per-source skill — `markdown-post-frontmatter-validation`, an MDX rendering skill, or a CMS-sync skill) or for chasing a specific routing bug (use debugging)."89# === v8 Classification (subject + public; polyhierarchy via subjects[]) — see ADR-0017 ===10# subject: primary browse shelf — what the skill teaches. One of twelve closed values:11# backend-engineering / frontend-engineering / software-architecture / data-engineering / agent-ops / ai-engineering /12# quality-assurance / design / reasoning-strategy / software-engineering-method / knowledge-organization / product-domain.13subject: backend-engineering14# public: publishability/private-data gate. Boolean.15# true = publishable/shareable; false = private and excluded from public export.16# Project anchoring is carried separately by non-empty `project[]` plus `grounding`.17public: false18# taxonomy_domain: optional hierarchical sub-path within `subject`. Slash-delimited19# lowercase kebab-case segments. rename of the original v8 `domain`. Remove when the flat20# `subject` is sufficient.21taxonomy_domain: content/routing22# scope: free-text PRD-style statement of what the skill teaches and what it excludes.23# (v8 required; not an enum). Mirrors Coverage + Do NOT Use When at frontmatter level.24scope: "Content-source dispatch logic for the markdown-static-site example project — routes fetches between local markdown, MDX, and headless-CMS sources."25# stability: lifecycle marker. One of:26# experimental (active development) / stable (production-ready) /27# frozen (no further changes expected) / deprecated.28# When `deprecated`, schema's allOf REQUIRES `superseded_by: <real-skill-name>`.29stability: experimental30# license: SPDX license identifier (e.g., MIT, Apache-2.0).31license: MIT32# compatibility: runtime compatibility object. Prefer structured fields33# (`agent_runtimes`, `node_version`) over free-text `notes`.34compatibility:35 agent_runtimes:36 - node37 node_version: ">=20"38 notes: "Source dispatch by file extension and content-path prefix; assumes a unified content router."39allowed-tools: Read Grep40# keywords: semantic phrases for fuzzy router activation. v8 cap: max 10.41# Keep terms a user would actually type when starting a task in this skill's domain.42keywords:43 - content source router44 - content dispatch45 - which content source46 - markdown vs mdx47 - cms vs local48 - source dispatch49 - content routing50 - source selection51 - content-fetch routing52# triggers: explicit-match activation phrases the router fires on literally.53# Use when label-based routing is intended; usually keywords + examples are enough.54triggers:55 - content-source-router56# paths: glob array of code surfaces this skill governs. Supports gitignore-style57# negation. Each glob should map to ONE canonical skill. Omit if purely conceptual.58paths:59 - "lib/content/router.ts"60 - "lib/content/sources/markdown.ts"61 - "lib/content/sources/mdx.ts"62 - "lib/content/sources/cms.ts"63# examples: 2-5 realistic user prompts the skill SHOULD activate for.64# Written in the user's voice. Improves retrieval recall beyond keywords alone.65examples:66 - "the content fetcher just received a request — which source do I read from?"67 - "add a fourth source (e.g., Notion API) to the routing table"68 - "why did the router pick MDX when the file extension is `.md`?"69 - "design the routing precedence between local files and the CMS sync"70# anti_examples: near-miss prompts that should route ELSEWHERE.71# Pair with relations.suppresses (or legacy boundary alias) to name the confusable territory's owner.72anti_examples:73 - "render this MDX page with React components"74 - "the CMS sync is producing duplicate posts"75 - "write a guide explaining the content routing"76# relations: typed graph edges to sibling skills. Current fields:77# related (adjacency for browse / co-routing expansion) /78# suppresses (exclude listed skills from co-routing when THIS skill wins; write reason79# as "I own this exclusively over X", not "use X instead") /80# boundary (DEPRECATED alias of suppresses, retained for unmigrated skills) /81# verify_with (cross-check; co-loaded as one-hop expansion) /82# depends_on (composition; transitive — A→B→C loads all three) /83# broader / narrower (SKOS-style generalization) /84# disjoint_with (mutual exclusion for incompatible ownership).85relations:86 related:87 - documentation88 - debugging89 - refactor90 verify_with:91 - graph-audit92# grounding: required when `project[]` is non-empty. Declares the truth sources93# the skill anchors to and the failure modes those sources prevent. Omit when the94# skill is universal-knowledge. `subject_matter` replaces v8 `domain_object`.95grounding:96 subject_matter: "Content source dispatch — the table that selects between local markdown, local MDX with React components, and headless-CMS-synced content based on file extension, content-path prefix, or explicit source hint"97 grounding_mode: repo_specific98 truth_sources:99 - lib/content/router.ts100 - lib/content/sources/markdown.ts101 - lib/content/sources/mdx.ts102 - lib/content/sources/cms.ts103 failure_modes:104 - silent_fallback_to_default_source105 - file_extension_check_skipped106 - cms_sync_drift_path_to_source_map_stale107 - new_source_added_without_router_update108 - error_response_from_one_source_silently_retried_via_another109 evidence_priority: repo_code_first110# project: projects this skill is linked to. Array of {handle, role} objects.111# Non-empty project[] anchors the skill to a project and requires `grounding`.112# Suggested role values: source-of-truth, consumer, mirror. Replaces original v8 `workspace_tags`.113project:114 - handle: markdown-static-site115 role: primary116---117118# Content Source Router119120## Concept of the skill121122**What it is:** The project-specific dispatch contract that decides which content source reads a requested page or post.123**Mental model:** Treat each source as an adapter behind one explicit router; the router owns selection, not rendering.124**Why it exists:** A static site with local markdown, MDX, and CMS-synced content needs one place where source precedence is visible and testable.125**What it is NOT:** It is not the source-specific parser, the CMS sync job, or contributor-facing documentation.126**Adjacent concepts:** Content source adapters, route matching, fallback policy, routing audit logs.127**One-line analogy:** It is the switchboard that connects a content request to the right reader.128**Common misconception:** A router can safely fall back to "markdown" when uncertain; unknown source selection should surface as a coverage gap.129130## Coverage131132- File-extension dispatch — `.md` routes to the markdown source, `.mdx` routes to the MDX source, no extension or `.cms.json` routes to the CMS source133- Content-path prefix dispatch — `content/posts/**` routes to local sources; `content/cms-synced/**` routes to the CMS source even if the file extension is `.md`134- Explicit source hints — internal callers (preview tools, manual reconciliation) pass an explicit `source` parameter that bypasses inspection135- Coverage-gap surfacing — when no detection rule matches a request, the router returns a structured "unknown source" result; it never silently picks a default136- Adding a new source — the workflow for landing a fourth source (Notion API, Sanity, etc.) without breaking the existing three (registration, routing precedence, fixture test, end-to-end content-fetch sanity)137138## Philosophy of the skill139140A content router is a dispatch surface that has to be exactly right or the rest of the site reads the wrong content. Every misroute is either a 404 (the user sees nothing) or a wrong-content render (the user sees a different post than the URL implies). The discipline is the same anti-default doctrine the `skill-router` applies to skills: prefer an explicit signal over an inferred one, prefer an unambiguous match over a "best guess," and prefer surfacing a coverage gap loudly over silently routing to a default.141142## Routing Rules143144The router evaluates four signals in priority order. The first signal that produces an unambiguous winner stops the chain.145146| Priority | Signal | Source | Match rule |147|---|---|---|---|148| 1 | Explicit `source` parameter | Internal callers (preview, manual reconciliation) | Exact match against the `Source` enum. Bypasses all subsequent inspection. |149| 2 | Content-path prefix | Inbound request path | First matching prefix wins: `content/posts/` → markdown; `content/mdx/` → mdx; `content/cms-synced/` → cms. |150| 3 | File extension | Resolved file path | `.md` → markdown; `.mdx` → mdx; `.cms.json` → cms. |151| 4 | Explicit `source_hint` in query string | Trusted internal callers | Last-resort hint; surfaced as a warning in the router's audit log. |152153### Coverage-gap behavior154155If no signal produces a match, the router returns `{ ok: false, reason: 'unknown_source', evidence: {...} }`. It does NOT fall back to a default source. The caller must handle the unknown-source case explicitly — typically by responding HTTP 404 and logging the full request shape for human triage.156157### Adding a new source1581591. Add the new source's path prefix and file extension to the priority-2 and priority-3 detection tables1602. Add a source implementation in `lib/content/sources/<source>.ts` that mirrors the markdown source's interface1613. Add the new source to the `Source` enum used at priority 11624. Add an end-to-end test that requests a fixture path matched by the new source and asserts the router selects it — without this, the router will silently fall through163164## Verification165166- Run unit cases for every configured source and every unknown-source branch.167- Add a fixture request for each new path prefix, file extension, and explicit source hint.168- Confirm unknown inputs return a structured failure instead of selecting a default source.169170## Do NOT Use When171172| Use instead | When |173|---|---|174| `markdown-post-frontmatter-validation` | The task is the actual frontmatter parsing for the markdown source — the router decides which source to read from, the source-specific skill validates and parses |175| `debugging` | A specific routing decision is wrong in production logs and you need to reproduce |176| `documentation` | The task is writing a contributor doc about the routing architecture |