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


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:

  1. Validate top-level shape (version, creator.handle, items array)
  2. For each item:
    • Extract slug, title, summary, tags, kinds, publishedAt, url, thumbnail.url
    • Insert/update the items row (see 07_DISCOVERY_LAYER.md)
  3. For items removed from the feed since last pull: mark as soft-deleted in items table
  4. 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:

  1. Bump version to 2
  2. Viewer-template emits both /feed.json (v2) and /feed.v1.json (legacy) during a transition period
  3. The indexer accepts both; new clients use v2
  4. 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:

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.