Skip to content

sources / bind / call / params explained

This is the plain-English heart of the docs. If you read only one page, read this one. No prior knowledge of the codebase is assumed.

The big idea

A web page needs two things: a shape (where the header, the article, the footer go) and data (the actual article, the list of related stories). In the old system, a programmer wrote a code file for each kind of page that fetched its data and assembled it.

In the new system, a row in a database table describes the page instead. The worker -- the program that turns a web address into a finished page -- reads that row and follows the same four steps for every page:

match route → fetch sources → build → wrap layout

  1. match route -- "which page row does this URL belong to?" The worker looks at the URL the visitor typed and finds the descriptor whose routes pattern matches it.
  2. fetch sources -- "go get the data this page needs." The worker reads the sources list and makes the API calls it names.
  3. build -- "assemble the content." The fetched data is arranged into the page body using the layout and any transforms.
  4. wrap layout -- "put the header and footer around it." The worker's renderLayout step adds the site header and footer, links the stylesheet, and returns the finished HTML.

Because the description is data, adding or changing a page is an edit to a table row, not a code change and not a deploy.

The glossary (in one breath)

A sources entry looks like this:

{"bind": "page", "call": "pages", "params": {"slug": "{slug}"}, "required": true}

Read it as a sentence: "Make the pages API call, filling in its parameters from the URL, and store the answer under the name page; this page cannot be built without it."

Word Plain meaning
call Which data source to ask. It names one of a fixed set of API calls (see the call vocabulary below). Think of it as choosing which drawer to open.
params The question you ask that source. A small set of key/value pairs, e.g. "the slug is {slug}, give me 6 related items". Values written like {slug} are placeholders filled from the URL.
bind The name the answer is stored under. After the call returns, its result is placed in the page's data box under this name. If bind is "page", the template refers to the fetched data as page.
required Is this data essential? true (or omitted on a must-have source) means: if the call fails, the page fails. false means: if the call fails, store null instead and carry on -- a nice-to-have that should not break the page.
bindings A separate column (not inside a sources entry) that renames a piece inside a fetched answer into its own top-level name, using a dotted path. Example: {"related": "related.items"} means "take the items field out of the related source and also expose it directly as related".

bind vs bindings -- not the same thing

  • bind (inside each sources entry) names the whole answer of one call.
  • bindings (the separate page_types.bindings column) reaches inside an already-fetched answer and lifts a nested field up to its own name. It is optional and is for convenience when a template wants a deep value at the top level.

The call vocabulary

call must be one of these named API calls -- the fixed set the worker knows how to make:

call Returns
lists A list of content (homepage feed, a category's articles, an author's articles). The params.kind picks which list.
pages A single page/article by its slug, optionally with related items.
authors An author's profile (bio, headshot, ...) by slug.
sitemap The data for the XML sitemap.
seo SEO data.

{param} interpolation

When a visitor opens /celebrities/taylor-swift, the matched descriptor's route "/{category}/{slug}" captures two values:

  • category = celebrities
  • slug = taylor-swift

Anywhere a params value is written as {slug} or {category}, the worker substitutes the captured value before making the call. That is the whole mechanic: routes capture, params interpolate.


Worked example 1 -- the article page

This is the descriptor the spec singles out. From the seed:

[{"bind": "page", "call": "pages", "params": {"slug": "{slug}", "related_limit": 6}, "required": true}]

Route: "/{category}/{slug}". Suppose a visitor opens /celebrities/taylor-swift.

Step by step:

  1. Route match. The URL matches the article descriptor's "/{category}/{slug}". The captures are category = celebrities and slug = taylor-swift.
  2. Read the one source. There is a single entry in sources.
  3. call: "pages" -- ask the pages API call (fetch one article).
  4. params -- fill them in:
    • slug: "{slug}"slug: "taylor-swift" (from the route capture).
    • related_limit: 6 → a plain literal: "also bring back up to 6 related articles." (No braces, so nothing is substituted.)
  5. required: true -- an article page with no article is meaningless, so if this call fails the whole page fails (returns an error / 404), rather than rendering an empty article.
  6. bind: "page" -- store the returned article under the name page. The article layout element now reads the headline, body, and related items from page.
  7. Build + wrap. The article builder shapes page into the body; the renderLayout wrapper adds header and footer. Cached for ttl = 1800s.

If this descriptor also wanted the related items at the top level, it could set the bindings column to {"related": "page.related"} -- lifting the nested related list out of page and exposing it as related. (The seeded article keeps everything under page; this is just how bindings would be used.)

Worked example 2 -- the homepage

[{"bind": "main", "call": "lists", "params": {"kind": "homepage", "limit": 30}}]

Route: "/". A visitor opens the site root.

  1. Route match. / matches the homepage descriptor. There are no {...} captures in this route, so nothing is interpolated.
  2. call: "lists" -- ask the lists API call (it returns a list of content).
  3. params -- kind: "homepage" selects which list (the homepage feed), and limit: 30 asks for up to 30 items. Both are literals -- no braces, no substitution.
  4. required is omitted. The homepage feed is the page's reason to exist, so it is treated as essential.
  5. bind: "main" -- store the returned feed under the name main. The homepage layout element renders the cards from main.
  6. Build + wrap, cached for ttl = 120s (the homepage changes often).

This same shape is reused by the search descriptor (route /search), which the seed notes is a placeholder to refine later.

Worked example 3 -- the author page (two sources)

The author page needs two different things: the list of the author's articles, and the author's own profile (bio, headshot). So its sources has two entries:

[
  {"bind": "list",   "call": "lists",   "params": {"kind": "author", "author_slug": "{slug}"}},
  {"bind": "author", "call": "authors", "params": {"slug": "{slug}"}}
]

Route: "/author/{slug}". A visitor opens /author/jane-doe, so slug = jane-doe.

  1. Route match. Captures slug = jane-doe.
  2. First source -- call: "lists", with kind: "author" (the author-feed list) and author_slug: "{slug}"author_slug: "jane-doe". The answer is stored under bind: "list" (the author's articles).
  3. Second source -- call: "authors", with slug: "{slug}"slug: "jane-doe". The answer is stored under bind: "author" (the profile).
  4. Two names, one page. The author layout element now has both list (the articles) and author (the bio/headshot) available, and renders them together. Cached for ttl = 900s.

This is the pattern repeating: one entry per piece of data the page needs, each with its own bind name. A page can have zero sources (the static legal pages), one (homepage, article, category), or several (author).

Required vs optional, restated

  • A source with required: true (or a must-have source with required omitted) fails the page if its call fails -- correct for the article's page and the homepage's main, because the page is pointless without them.
  • A source with required: false degrades to null on failure and the page still renders -- correct for a nice-to-have widget (e.g. a trending sidebar) that should never take the whole page down.

How this maps back to the table

Everything above is stored in one page_types row:

  • routes -- the URL patterns and their {captures}.
  • sources -- the list of {bind, call, params, required} entries walked here.
  • bindings -- optional dotted-path lifts into a fetched source.
  • layout -- which content elements render the bound data.
  • ttl -- how long the finished page is cached.

No code file per page. One generic loop, driven by data.