# Feeds and containment

This guide is for app builders rendering **other people's content in their own looks** — a feed, a reader, an embed. It builds on [Render themes in your app](https://standard.mode.place/guides/render-themes/): the functions used here (`getTheme`, `fontFaces`, `themeToCss`) are defined there, and the layer order `@layer app, author, viewer` is already declared. What's new is the problem the **author** source brings with it: many authors on one page, every look needing to stay inside its own content — the [Mode Contract, Rule 7](https://standard.mode.place/contract/#rule-7--keeping-a-foreign-look-contained) job.

## One marker, many authors

The real multi-author case is a feed: many authors' posts, each dressed in **its author's baseline** — every author's own `default`. So every post carries the *same* marker, `data-mode="default"`, and every author's `[data-mode="default"]` rule, all sharing the `author` layer, would match *every* post. The layer separates the author tier from app and viewer — **not the authors from each other**: peers in one layer fall through to source order, so the last rule would win for all of them. Namespacing only helps when authors ship genuinely *distinct* concepts (a `com.alice.dark` and a `com.bob.dark` get distinct markers for free); here the marker is the same `default` for everyone, so containment is required. This is the [Rule 7](https://standard.mode.place/contract/) job for `@scope`.

## Contain by position with `@scope`

The app writes each author's CSS **in place** — inside the post, at the moment it renders it. That's the author lifecycle from the sources table in [Render themes in your app](https://standard.mode.place/guides/render-themes/): an author's CSS is produced in the same step that writes the author's post, because that's exactly when the app has the author's DID and the post's container in hand. And because the CSS lands inside the post, the `@scope` needs no selector at all: a **prelude-less** `@scope { … }` roots at the `<style>`'s parent element, so the post *is* the boundary. `themeToCss` stays placement-agnostic; the app wraps its output in `@scope { … }` and drops that `<style>` inside the `<article>`. (An implementer who only renders author content could fold the wrap into their own `themeToCss`; the reference leaves it out so one function still serves the global app and viewer tiers.)

```ts
// Called as the app renders each feed item. Everything for the post lives in one <style>
// written INSIDE it: the @font-face rules at the top (global wherever they sit, as long as
// they're outside @scope), and a prelude-less @scope { … } — which roots at the <article> on
// its own, no id or selector — containing the scoped variable rules.
// Themes change rarely — cache per author DID (like the agentFor cache in the render
// guide) so a feed doesn't re-fetch the same author's theme on every post and every render.
const themes = new Map<string, Promise<ThemeRecord | null>>();
function themeFor(did: string): Promise<ThemeRecord | null> {
  let theme = themes.get(did);
  if (!theme) themes.set(did, (theme = getTheme(did, "default")));
  return theme;
}

async function renderPost(post: { authorDid: string; body: string }) {
  const theme = await themeFor(post.authorDid); // the author's default look, cached
  if (!theme) return `<article><div data-mode="default">${post.body}</div></article>`;

  const style = `<style>${fontFaces(theme)} @scope { ${themeToCss(theme, "default", "author")} }</style>`;
  // The marker sits on a CHILD of the <article>, not on the <article> itself: a scoped
  // style rule only matches descendants of its @scope root (a selector with no :scope in
  // it never matches the root element), so a marker on the <article> would sit just
  // outside the reach of the very look written for it.
  return `<article>${style}<div data-mode="default">${post.body}</div></article>`;
}
```

## What it renders

For a feed of Alice and Bob that yields (each post's `<style>` sits *inside* its `<article>` — any `@font-face` at the top of the block, the `@scope` beneath it; these are color-only, so there's no font to show):

```html
<style>@layer app, author, viewer;</style>

<article>
  <style>
    /* fontFaces(theme) would go here — global, outside @scope (empty for a color-only look) */
    @scope {                       /* prelude-less → scopes to THIS <article>, its parent */
      @layer author {
        :where([data-mode="default"], [data-mode~="default/color"]) {
          --color-surface-primary-background: #0a0e14; /* …Alice's default… */
        }
      }
    }
  </style>
  <div data-mode="default">…Alice's content…</div>
</article>

<article>
  <style>
    @scope {
      @layer author {
        :where([data-mode="default"], [data-mode~="default/color"]) {
          --color-surface-primary-background: #241018; /* …Bob's default… */
        }
      }
    }
  </style>
  <div data-mode="default">…Bob's content…</div>
</article>
```

Each `<style>` lives inside its own `<article>`, so the prelude-less `@scope` roots at that article and the author's rule can't escape it — no id, no selector: the position *is* the boundary. Both posts still carry the honest `data-mode="default"` marker. And the **viewer still wins**: a viewer's page-global `@layer viewer { :where([data-mode="default"]) { … } }` matches inside each article too, and because layers sort above scope-proximity, the `viewer` layer beats the scoped `author` rule on every post. Contain the author by position, keep the marker; the override survives. (Keep the app's own chrome outside these article subtrees, per [Rule 7](https://standard.mode.place/contract/), so an author's colors don't repaint the feed's toolbar.)

:::note[Authors: the viewer still wins here — by design]
If you publish and expect your look to ride your posts everywhere, the above may surprise you: a viewer who has saved their own `default` sees your posts in *their* look, not yours. That's the [Mode Contract, Rule 5](https://standard.mode.place/contract/#rule-5--whose-look-wins-fill-through-not-conflict) precedence and it's deliberate — the viewer's guaranteed override is the standard's accessibility valve (nobody can be trapped in a look that fails them, which is also what makes [author-owns-accessibility](https://standard.mode.place/contract/#whos-responsible) fair). And it's per variable: a viewer with a color-only baseline still reads your typography.

What a **publication** can do is serve a *specific* expression instead of the generic baseline: publish its theme at a namespaced key (`…/theme/com.alice.zine`), and mark its content `data-mode="com.alice.zine"`. A viewer's generic `default` doesn't match that marker — it only inherits from the scope above, and values your look sets *directly* on the publication's scope win by locality — so the look holds through a casual viewer baseline, exactly like a brand concept does. It stays **reserved, not locked**: a viewer who deliberately saves a theme at `com.alice.zine` supersedes it wherever it's placed (see [Generic invites, specific reserves](https://standard.mode.place/reference/expressions/#generic-invites-specific-reserves)). The [live demo](https://standard.mode.place/demo/)'s third feed post shows both halves: the zine's colors hold through the viewer toggle while the `default` posts yield.
:::

:::note[At scale: an indexer, not a fan-out]
The code above fetches each author's theme from that author's own PDS — fine for a page, but a busy feed would fan `getRecord`s out across the network on every load (and meet rate limits doing it). The at-scale shape is the standard AT Protocol one: an **indexer** subscribes to the firehose (`com.atproto.sync.subscribeRepos`) for `place.mode.standard.theme` commits — bootstrapping with `listReposByCollection` — and maintains a DID → theme table your app reads locally. The per-DID cache above is the miniature of it, and the [Colophon](https://standard.mode.place/colophon/) describes the same discovery path; the [shared gateway](https://standard.mode.place/guides/render-themes/#optional-a-shared-gateway) is the serving half of the idea.
:::

:::note[A future: posts that name their theme]
Here the app uses each author's `default` **by convention** — that's the only "which theme?" signal it has. A richer world lets a *post* name the look it wants: the content record carries a theme key (an expression), and the app fetches `getTheme(authorDid, thatKey)`, falling back to `"default"` when absent. That reference is a **content-format** concern — a field in the post/publication lexicon, not in `place.mode.standard.*` — so it's an ecosystem extension, not a schema change here. The containment above is identical either way; only the key you fetch changes.
:::

## Next steps

- **[The live demo](https://standard.mode.place/demo/)** — this pattern running in your browser: two authors' posts, contained, with the viewer still winning.
- **[The Mode Contract, Rule 7](https://standard.mode.place/contract/#rule-7--keeping-a-foreign-look-contained)** — the containment rule this guide implements.
- **[Render themes in your app](https://standard.mode.place/guides/render-themes/)** — the functions and layer setup this guide builds on.
- **[Colophon](https://standard.mode.place/colophon/)** — the firehose discovery path behind the at-scale indexer.