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---2name: content-source-router3description: 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).4license: MIT5---67# Content Source Router89## Concept of the skill1011**What it is:** The project-specific dispatch contract that decides which content source reads a requested page or post.12**Mental model:** Treat each source as an adapter behind one explicit router; the router owns selection, not rendering.13**Why it exists:** A static site with local markdown, MDX, and CMS-synced content needs one place where source precedence is visible and testable.14**What it is NOT:** It is not the source-specific parser, the CMS sync job, or contributor-facing documentation.15**Adjacent concepts:** Content source adapters, route matching, fallback policy, routing audit logs.16**One-line analogy:** It is the switchboard that connects a content request to the right reader.17**Common misconception:** A router can safely fall back to "markdown" when uncertain; unknown source selection should surface as a coverage gap.1819## Coverage2021- File-extension dispatch — `.md` routes to the markdown source, `.mdx` routes to the MDX source, no extension or `.cms.json` routes to the CMS source22- 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`23- Explicit source hints — internal callers (preview tools, manual reconciliation) pass an explicit `source` parameter that bypasses inspection24- Coverage-gap surfacing — when no detection rule matches a request, the router returns a structured "unknown source" result; it never silently picks a default25- 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)2627## Philosophy of the skill2829A 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.3031## Routing Rules3233The router evaluates four signals in priority order. The first signal that produces an unambiguous winner stops the chain.3435| Priority | Signal | Source | Match rule |36|---|---|---|---|37| 1 | Explicit `source` parameter | Internal callers (preview, manual reconciliation) | Exact match against the `Source` enum. Bypasses all subsequent inspection. |38| 2 | Content-path prefix | Inbound request path | First matching prefix wins: `content/posts/` → markdown; `content/mdx/` → mdx; `content/cms-synced/` → cms. |39| 3 | File extension | Resolved file path | `.md` → markdown; `.mdx` → mdx; `.cms.json` → cms. |40| 4 | Explicit `source_hint` in query string | Trusted internal callers | Last-resort hint; surfaced as a warning in the router's audit log. |4142### Coverage-gap behavior4344If 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.4546### Adding a new source47481. Add the new source's path prefix and file extension to the priority-2 and priority-3 detection tables492. Add a source implementation in `lib/content/sources/<source>.ts` that mirrors the markdown source's interface503. Add the new source to the `Source` enum used at priority 1514. 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 through5253## Verification5455- Run unit cases for every configured source and every unknown-source branch.56- Add a fixture request for each new path prefix, file extension, and explicit source hint.57- Confirm unknown inputs return a structured failure instead of selecting a default source.5859## Do NOT Use When6061| Use instead | When |62|---|---|63| `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 |64| `debugging` | A specific routing decision is wrong in production logs and you need to reproduce |65| `documentation` | The task is writing a contributor doc about the routing architecture |