> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fourdos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Block format

> What ?format=blocks serves: a flat array of eight block types, with media as plain fields and text as HTML fragments

`?format=blocks` serves the article body as a flat, ordered array of top-level
blocks instead of one HTML string. Everything you need to render it is on this
page.

## When to choose blocks over HTML

Choose blocks when you want to **render your own components for images and
video**. With `?format=html` an image is our `<img>` (inside a `<figure>` when
the author captioned it) and a video is a `<div>` carrying the address, not a
player; with blocks you get the image's `src`, `alt`, `caption` and `decorative`
and the video's `src` and `provider` as plain fields, and can pass them to your
site's responsive image component, your CDN transform, or your own video player.

**If your articles carry video, you need blocks, or you need to handle that
`<div>` yourself.** We do not put a third-party `<iframe>` in the HTML we serve
you, so a video embed renders as nothing on the page until your site turns the
element into a player. [Articles](/content/articles) describes the element and
what is on it.

Choose HTML for everything else. Blocks are not a richer text model and give
you nothing extra for paragraphs and headings.

You do not have to choose per site. The two formats are two views of the same
stored render, so you can switch by changing one query parameter, and an
article requested in both formats describes the same content: same text, same
image addresses and captions, same video addresses. What differs is what a
browser does with it, which is the video point above.

## The envelope

```json theme={null}
{
  "slug": "match-report",
  "format": "blocks",
  "contract_version": 1,
  "blocks": [
    { "type": "heading", "html": "<h2>The first half</h2>" },
    { "type": "paragraph", "html": "<p>We won, <strong>2-0</strong>.</p>" },
    {
      "type": "image",
      "src": "https://images.example.com/matchday.jpg",
      "alt": "The winning goal going in",
      "caption": "The winner, 88 minutes",
      "decorative": false
    },
    { "type": "divider" },
    {
      "type": "video_embed",
      "src": "https://youtu.be/aqz-KE-bpKQ",
      "provider": "youtube"
    }
  ]
}
```

`blocks` is flat and in document order. Blocks never nest: there is no block
that contains other blocks, so rendering is one pass over the array.

`contract_version` is the version of the block shape this article was rendered
against. It is what a future change to the block shape would key off. Today it
is `1`. Read it if you want to be defensive; you do not need to branch on it.

## The block types

`type` is one of exactly these eight names. They are ours, and they are the
whole set.

| `type`         | Fields                                | Renders as                                               |
| -------------- | ------------------------------------- | -------------------------------------------------------- |
| `paragraph`    | `html`                                | A paragraph                                              |
| `heading`      | `html`                                | A heading. The level is in the fragment (`<h2>`, `<h3>`) |
| `blockquote`   | `html`                                | A pull quote                                             |
| `bullet_list`  | `html`                                | An unordered list                                        |
| `ordered_list` | `html`                                | A numbered list                                          |
| `image`        | `src`, `alt`, `caption`, `decorative` | An image                                                 |
| `video_embed`  | `src`, `provider`                     | An embedded video                                        |
| `divider`      | none                                  | A horizontal rule between sections                       |

### Text-bearing blocks

`paragraph`, `heading`, `blockquote`, `bullet_list` and `ordered_list` each
carry a single field:

| Field  | Type   | Notes                                         |
| ------ | ------ | --------------------------------------------- |
| `html` | string | The block's rendered, sanitised HTML fragment |

The fragment is a complete element, tag included: a `paragraph` is
`"<p>…</p>"`, not the text inside one. Inline formatting (bold, italic, links)
is already in the fragment, and so is the heading's level, so a `heading` block
tells you whether it is an `<h2>` or an `<h3>` by what it contains.

These fragments are the same bytes `?format=html` is joined from, in the same
order. That is why the two formats cannot describe different content: the text
is literally the same HTML, cut at the top-level boundaries.

The fragments are sanitised when the article is published, which is what makes
them safe to inject as HTML. There is no inline-level structure to walk: if you
need one, the `html` string is what you have.

`html` is the only pre-sanitised field on this surface. Every other field, the
media blocks' `src`, `alt` and `caption` and the article's own `title`,
`excerpt` and `feature_image_url`, is plain text an author typed and is not
escaped for you. Set those through the DOM (`textContent`, `img.alt = …`)
instead of building markup out of them: an author writing `Kane's "winner"` in
an alt box breaks a quoted attribute with no malice at all.

### `image`

| Field        | Type           | Notes                                                                      |
| ------------ | -------------- | -------------------------------------------------------------------------- |
| `src`        | string or null | The image address. **Null** when the stored address was rejected as unsafe |
| `alt`        | string         | Alt text. An empty string when the author marked the image decorative      |
| `caption`    | string or null | The caption, when the author wrote one                                     |
| `decorative` | boolean        | True when the author said the picture adds nothing to the words            |

A decorative image is one the author declared purely visual: a divider, a
flourish, a repeat of the caption. `alt` is an empty string on one, which is
what tells a screen reader to skip it, so binding `alt` straight onto your
`<img>` already does the right thing and you can ignore the flag. Read it if you
do something else with the field, like falling back to the caption when `alt` is
blank: that fallback is wrong on a decorative image.

Handle `src: null` by rendering nothing for that block. It means the address
did not pass our URL rules and was blanked at publish, so there is no image to
fetch.

### `video_embed`

| Field      | Type           | Notes                                                                                   |
| ---------- | -------------- | --------------------------------------------------------------------------------------- |
| `src`      | string or null | The address the author pasted, unchanged. **Null** when the stored address was rejected |
| `provider` | string or null | `"youtube"` or `"vimeo"`                                                                |

`src` is stored exactly as the author pasted it and is not normalised, so it can
be any address the editor accepted for that provider: a watch page, a share
link, a `/shorts/` or `/live/` page, a player or embed URL. The block carries no
video id, so if you want to build your own player you have to read the id out of
`src`. These are the shapes the editor accepts, and the whole set of them:

| Provider  | Hosts (with or without `www.`)                         | Paths carrying the id                                                   |
| --------- | ------------------------------------------------------ | ----------------------------------------------------------------------- |
| `youtube` | `youtube.com`, `m.youtube.com`, `youtube-nocookie.com` | `/watch?v=<id>`, `/embed/<id>`, `/shorts/<id>`, `/live/<id>`, `/v/<id>` |
| `youtube` | `youtu.be`                                             | `/<id>`                                                                 |
| `vimeo`   | `vimeo.com`                                            | `/<id>`, `/<id>/<unlisted-hash>`, `/channels/<name>/<id>`               |
| `vimeo`   | `player.vimeo.com`                                     | `/video/<id>`                                                           |

A YouTube id is eleven characters of `A-Z a-z 0-9 _ -`; a Vimeo id is digits.
Reading it back out covers every row above:

```js theme={null}
const YOUTUBE_HOSTS = ["youtube.com", "m.youtube.com", "youtube-nocookie.com", "youtu.be"];
const VIMEO_HOSTS = ["vimeo.com", "player.vimeo.com"];

function videoId(block) {
  if (block.src === null) return null;
  const url = new URL(block.src);
  const host = url.hostname.replace(/^www\./, "");
  const path = url.pathname.split("/").filter((segment) => segment.length > 0);

  if (YOUTUBE_HOSTS.includes(host)) {
    if (host === "youtu.be") return path[0] ?? null;
    if (path[0] === "watch") return url.searchParams.get("v");
    if (["embed", "shorts", "live", "v"].includes(path[0])) return path[1] ?? null;
    return null;
  }
  if (VIMEO_HOSTS.includes(host)) {
    // The id is the first all-digits segment, which is what keeps an unlisted
    // video's hash out of it.
    return path.find((segment) => /^\d+$/.test(segment)) ?? null;
  }
  return null;
}
```

Treat `provider` as an open set even though two values ship today, and fall back
to a plain link to `src` for a value you do not recognise, or when `videoId`
comes back null. Render nothing for a `src` of null.

### `divider`

Carries no fields at all:

```json theme={null}
{ "type": "divider" }
```

## Rendering it

A minimal renderer is a switch with eight arms and a default:

```js theme={null}
function renderBlock(block) {
  switch (block.type) {
    case "image":
      return block.src === null ? "" : myImageComponent(block.src, block.alt, block.caption);
    case "video_embed":
      return block.src === null ? "" : myVideoComponent(block.src, block.provider);
    case "divider":
      return "<hr>";
    case "paragraph":
    case "heading":
    case "blockquote":
    case "bullet_list":
    case "ordered_list":
      return block.html;
    default:
      // A block type this code predates: skip it rather than break the article.
      return "";
  }
}

const body = article.blocks.map(renderBlock).join("");
```

Keep that `default` arm. The eight types above are the whole set today, and a
type you do not recognise can only mean a block type added after your code was
written. Skipping it costs a reader one block; throwing costs them the article.

The two media arms hand `src`, `alt` and `caption` to your own components, which
is where the escaping rule above applies: build those nodes rather than
concatenating the fields into markup.
[Render a club's articles](/walkthroughs/render-club-articles) has the same
renderer written that way.

A short renderer that only wants its own media components is shorter still:
intercept `image` and `video_embed`, and pass everything else through as
`block.html ?? ""`.

## Where to go next

* [Articles](/content/articles) for the endpoints and their parameters.
* [Render a club's articles](/walkthroughs/render-club-articles) for a working
  page.
