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
- match route -- "which page row does this URL belong to?" The worker looks
at the URL the visitor typed and finds the descriptor whose
routespattern matches it. - fetch sources -- "go get the data this page needs." The worker reads the
sourceslist and makes the API calls it names. - build -- "assemble the content." The fetched data is arranged into the
page body using the
layoutand any transforms. - wrap layout -- "put the header and footer around it." The worker's
renderLayoutstep 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 eachsourcesentry) names the whole answer of one call.bindings(the separatepage_types.bindingscolumn) 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=celebritiesslug=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:
- Route match. The URL matches the article descriptor's
"/{category}/{slug}". The captures arecategory = celebritiesandslug = taylor-swift. - Read the one source. There is a single entry in
sources. call: "pages"-- ask thepagesAPI call (fetch one article).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.)
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.bind: "page"-- store the returned article under the namepage. Thearticlelayout element now reads the headline, body, and related items frompage.- Build + wrap. The
articlebuilder shapespageinto the body; therenderLayoutwrapper adds header and footer. Cached forttl= 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.
- Route match.
/matches the homepage descriptor. There are no{...}captures in this route, so nothing is interpolated. call: "lists"-- ask thelistsAPI call (it returns a list of content).params--kind: "homepage"selects which list (the homepage feed), andlimit: 30asks for up to 30 items. Both are literals -- no braces, no substitution.requiredis omitted. The homepage feed is the page's reason to exist, so it is treated as essential.bind: "main"-- store the returned feed under the namemain. Thehomepagelayout element renders the cards frommain.- 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.
- Route match. Captures
slug = jane-doe. - First source --
call: "lists", withkind: "author"(the author-feed list) andauthor_slug: "{slug}"→author_slug: "jane-doe". The answer is stored underbind: "list"(the author's articles). - Second source --
call: "authors", withslug: "{slug}"→slug: "jane-doe". The answer is stored underbind: "author"(the profile). - Two names, one page. The
authorlayout element now has bothlist(the articles) andauthor(the bio/headshot) available, and renders them together. Cached forttl= 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 withrequiredomitted) fails the page if its call fails -- correct for the article'spageand the homepage'smain, because the page is pointless without them. - A source with
required: falsedegrades tonullon 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.