10 — Feed Schema
The canonical /feed.json format every creator's site exposes. The indexer consumes it; third-party clients can too. Versioned + typed + designed to be BabyAI-friendly without being heavy.
Design goals
- Human-readable — JSON, not binary, not custom-encoded
- Typed enough for AI — explicit
kindfields, structured tags, summaries that are not just truncated bodies - Stable across viewer-template versions — schema migrations are explicit (bump
version), not implicit - Forward-compatible — unknown fields are allowed (indexer ignores; future fields don't break old indexers)
- Easy to generate — the publish pipeline writes this; no special tooling required
Top-level shape
{
"version": 1,
"generator": "bka-viewer/1.0.0",
"generatedAt": "2026-06-29T20:00:00Z",
"creator": {
"handle": "chris",
"displayName": "Christopher Bender",
"bio": "Short bio for discovery + feed-reader headers.",
"siteUrl": "https://chris-bka-public.pages.dev",
"avatarUrl": "https://chris-bka-public.pages.dev/assets/avatar.jpg",
"language": "en",
"discoverable": true
},
"items": [
{ /* item shape below */ }
]
}
discoverable: false at the top level signals to the indexer: "don't index ANY of my content right now." Useful for a creator going through a quiet period without unregistering.
Item shape
Each entry in items:
{
"slug": "my-first-post",
"title": "My First Post",
"summary": "A short, creator-written or AI-summarized description for feed display and discovery routing.",
"url": "https://chris-bka-public.pages.dev/posts/my-first-post",
"publishedAt": "2026-06-29T20:00:00Z",
"updatedAt": "2026-06-29T20:00:00Z",
"tags": ["intro", "meta"],
"kinds": ["text"],
"discoverable": true,
"thumbnail": {
"url": "https://chris-bka-public.pages.dev/assets/thumbnails/my-first-post.jpg",
"width": 1200,
"height": 630
},
"media": [
{
"kind": "video",
"url": "https://github.com/<owner>/<repo>/releases/download/asset-abc123/intro.mp4",
"mime": "video/mp4",
"durationSec": 245,
"size": 38400000,
"thumbnail": "https://chris-bka-public.pages.dev/assets/thumbnails/intro-poster.jpg"
},
{
"kind": "audio",
"url": "https://github.com/<owner>/<repo>/releases/download/asset-def456/podcast.mp3",
"mime": "audio/mpeg",
"durationSec": 1820
}
],
"language": "en",
"wordCount": 850,
"external": null
}
Field reference
| Field | Required | Notes |
|---|---|---|
slug |
yes | Unique within the creator's feed. Becomes the URL path segment. |
title |
yes | Display title. |
summary |
yes | Short description (1-3 sentences). Used by the indexer for ranking + display. Either creator-written or AI-summarized; the BKA composer can offer to generate one. |
url |
yes | Absolute URL to the post on the creator's site. Visitors land here. |
publishedAt |
yes | ISO 8601 timestamp. The indexer sorts by this. |
updatedAt |
yes | ISO 8601. When the post was last edited. |
tags |
no | Array of lowercase strings. Creator-supplied. The indexer uses these for filters. |
kinds |
yes | Array of `"text" |
discoverable |
no | Default true. Set false to keep out of indexer / discovery without unpublishing. |
thumbnail |
no | Object with url, width, height. Indexer uses for visual feed renders. |
media |
no | Array of media objects (see below). Empty/omitted for text-only posts. |
language |
no | ISO 639-1 code. Defaults to creator-level language. |
wordCount |
no | For text posts. Helps discovery rank long-form vs short-form. |
external |
no | URL string. If set, this is a link-only post (the title points to an external URL, not a hosted post). Lets creators share other people's work. Indexer can show with a "↗ external" hint. |
Media object
{
"kind": "video" | "audio" | "image",
"url": "absolute https URL",
"mime": "video/mp4 | audio/mpeg | image/jpeg | ...",
"durationSec": 245, // video/audio only
"size": 38400000, // bytes; helps client decide how to load
"thumbnail": "...", // optional; video poster image
"width": 1920, // image/video; for layout
"height": 1080, // image/video; for layout
"transcript": "..." // optional; URL to a transcript file for audio/video accessibility
}
url is always the durable public URL (typically https://github.com/.../releases/download/... for files in GitHub Releases). Stable across CF rebuilds.
What the indexer pulls + stores
When the indexer fetches a creator's feed.json:
- Validate top-level shape (version, creator.handle, items array)
- For each item:
- Extract
slug,title,summary,tags,kinds,publishedAt,url,thumbnail.url - Insert/update the
itemsrow (see 07_DISCOVERY_LAYER.md)
- Extract
- For items removed from the feed since last pull: mark as soft-deleted in items table
- Cache the new raw snapshot in KV
Everything in the schema beyond what's listed in step 2 (media URLs, durations, language, wordCount, etc.) is preserved in the cached snapshot for clients that want it via /api/creators/:handle, but isn't directly indexed.
RSS mirror (feed.xml)
The viewer-template also generates /feed.xml as a standard RSS 2.0 feed. This is for backwards-compatibility with RSS readers, not for the BKA indexer (which uses feed.json directly).
The RSS mapping:
| feed.json | RSS 2.0 |
|---|---|
creator.displayName |
<channel><title> |
creator.bio |
<channel><description> |
creator.siteUrl |
<channel><link> |
item.title |
<item><title> |
item.summary |
<item><description> |
item.url |
<item><link>, <guid> |
item.publishedAt |
<pubDate> (converted to RFC 822) |
item.media[] |
<enclosure> per media item |
We don't try to encode the full feed.json shape in RSS; RSS readers get a sensible subset, BKA-aware clients use feed.json.
Versioning + migration
version field at the top level. When schema-breaking changes happen:
- Bump
versionto 2 - Viewer-template emits both
/feed.json(v2) and/feed.v1.json(legacy) during a transition period - The indexer accepts both; new clients use v2
- After transition period (~6 months), v1 generation can drop
Forward-compatibility within a version: unknown fields are allowed and ignored. Old indexers reading a v1 feed with new fields don't break.
Validation
The indexer + the BKA publish pipeline both validate feeds before accepting:
- JSON parses cleanly
- Top-level
versionis a known integer - Required fields present (per the field table above)
- URLs are valid + absolute
- Timestamps parse as ISO 8601
- Arrays have at least one element where required
Validation failures produce a clear error. The indexer's /api/register rejects unparseable feeds at registration time; the BKA publish flow validates before commit so creators see issues before they go live.
Examples
Minimal text-only post
{
"slug": "hello-world",
"title": "Hello, world",
"summary": "First post on the new site.",
"url": "https://chris-bka-public.pages.dev/posts/hello-world",
"publishedAt": "2026-06-29T20:00:00Z",
"updatedAt": "2026-06-29T20:00:00Z",
"kinds": ["text"]
}
Video post with thumbnail
{
"slug": "demo-day-walkthrough",
"title": "Demo Day Walkthrough",
"summary": "Twenty-minute video tour of what we shipped this sprint.",
"url": "https://chris-bka-public.pages.dev/posts/demo-day-walkthrough",
"publishedAt": "2026-06-29T20:00:00Z",
"updatedAt": "2026-06-29T20:00:00Z",
"tags": ["demo", "sprint"],
"kinds": ["video", "text"],
"thumbnail": {
"url": "https://chris-bka-public.pages.dev/assets/thumbnails/demo-day.jpg",
"width": 1280, "height": 720
},
"media": [
{
"kind": "video",
"url": "https://github.com/example/chris-bka-public/releases/download/asset-7f3b/demo-day.mp4",
"mime": "video/mp4",
"durationSec": 1245,
"size": 184320000
}
]
}
Link-only (external) post
{
"slug": "interesting-paper",
"title": "An interesting paper I read",
"summary": "Brief notes on the methodology section.",
"url": "https://chris-bka-public.pages.dev/posts/interesting-paper",
"publishedAt": "2026-06-29T20:00:00Z",
"updatedAt": "2026-06-29T20:00:00Z",
"kinds": ["text"],
"external": "https://arxiv.org/abs/2024.12345"
}
The post page on the creator's site shows the creator's notes + a prominent "↗ Original" link to the external URL. The indexer can display this as a link-share with the appropriate UI hint.