what

Data Sources

Fetch data from local collections or named external datasources with sorting, filtering, search, and pagination — all from your template's <what> block.

Local Collections

Use the local: prefix in a fetch directive to read from a database collection. The collection name follows the prefix.

site/blog/index.html
<what>
fetch.posts = "local:posts"
</what>

<loop data="#posts#" as="post">
  <article>
    <h2>#post.title#</h2>
    <p>#post.excerpt#</p>
    <small>By #post.author# on #post.created_at#</small>
  </article>
</loop>

The fetch.posts directive loads the "posts" collection from the database and makes it available as the #posts# template variable.

Query Parameters

Append .sort, .filter, .search, .search_fields, .limit, and .offset to any fetch directive to control the query.

Full example with all options
<what>
fetch.posts = "local:posts"
fetch.posts.sort = "created_at:desc"
fetch.posts.filter = "status=published"
fetch.posts.search = "#query.q#"
fetch.posts.search_fields = "title,content"
fetch.posts.limit = "10"
fetch.posts.offset = "#query.offset|default:0#"
</what>

Sorting

Sort by any field in ascending or descending order:

fetch.posts.sort = "created_at:desc"
fetch.posts.sort = "title:asc"
fetch.posts.sort = "price:desc"

The format is field:direction where direction is asc or desc.

Filtering

Filter results by field values. The filter expression supports equality and comparison operators.

<!-- Exact match -->
fetch.posts.filter = "status=published"

<!-- Multiple AND conditions (use &) -->
fetch.posts.filter = "status=published&category=tech"

<!-- OR conditions (use comma) -->
fetch.posts.filter = "status=published,status=draft"
SyntaxMeaningExample
field=valueEqualsstatus=published
& separatorAND (all must match)status=published&featured=true
, separatorOR (any can match)category=tech,category=design

Search across one or more fields with a text query. The search is case-insensitive and matches partial strings.

<what>
fetch.posts = "local:posts"
fetch.posts.search = "#query.q#"
fetch.posts.search_fields = "title,content"
</what>

<form>
  <input type="search" name="q" value="#query.q#" placeholder="Search posts...">
  <button type="submit">Search</button>
</form>

<loop data="#posts#" as="post">
  <div>#post.title#</div>
</loop>

The #query.q# variable reads the q parameter from the URL query string, so /blog?q=rust searches for "rust" in the title and content fields.

Tip: If search_fields is not specified, the search applies to all text fields in the collection.

Pagination

Use limit and offset to paginate results. Combine with query parameters for page navigation.

<what>
fetch.posts = "local:posts"
fetch.posts.sort = "created_at:desc"
fetch.posts.limit = "10"
fetch.posts.offset = "#query.offset|default:0#"
</what>

<loop data="#posts#" as="post">
  <div>#post.title#</div>
</loop>

For built-in pagination controls, use the paginate attribute on the loop instead. See Conditionals & Loops: Pagination.

JSON DataStore

By default, collections are stored in a JSON file at data/store.json. The format is:

data/store.json
{
  "collections": {
    "posts": [
      {
        "id": "abc123",
        "title": "Hello World",
        "content": "My first post.",
        "status": "published",
        "created_at": "2025-01-15T10:30:00Z"
      }
    ],
    "users": [
      { "id": "u1", "name": "Jorge", "email": "[email protected]" }
    ]
  },
  "values": {}
}

When a SQLite database is configured, collections are stored as tables instead. The fetch syntax remains the same.

Looping Over Data

Once fetched, data is used with the <loop> tag. Access fields with #alias.field# syntax.

<what>
fetch.products = "local:products"
fetch.products.sort = "price:asc"
fetch.products.filter = "in_stock=true"
</what>

<if products>
  <div class="grid grid-cols-3 gap-4">
    <loop data="#products#" as="product">
      <div class="card p-4">
        <h3>#product.name#</h3>
        <p>#product.price|currency#</p>
        <p class="text-muted">#product.description|truncate:80#</p>
      </div>
    </loop>
  </div>
</if>
<else/>
  <p class="text-muted">No products found.</p>
</else>

Named Datasources (dsn:)

Named datasources let you connect to multiple external backends simultaneously. Each datasource is configured in what.toml and accessed via the dsn: prefix.

what.toml
[datasources.cloud]
type = "d1"
account_id = "${CF_ACCOUNT_ID}"
api_token = "${CF_API_TOKEN}"
d1_database_id = "${CF_D1_DATABASE_ID}"

[datasources.cms]
type = "supabase"
project_url = "${SUPABASE_URL}"
api_key = "${SUPABASE_KEY}"

[datasources.inventory]
type = "api"
url = "https://api.example.com"
headers = { Authorization = "Bearer ${API_TOKEN}" }

Fetch Syntax

PatternBackendEffect
dsn:name.collectionDB typesQuery a collection (table)
dsn:name/pathAPI typeGET base_url + path
dsn:nameAnyRoot: all collections (DB) or base URL (API)
site/products/index.html
<what>
[data]
fetch.todos = "dsn:cloud.todos"
fetch.todos.sort = "created_at:desc"
fetch.todos.limit = "20"
</what>

<loop data="#todos#" as="todo">
  <p>#todo.title#</p>
</loop>

All query parameters (sort, filter, search, limit, offset) work identically with dsn: and local: prefixes.

Datasource Types

TypeRequired FieldsDescription
d1account_id, api_token, d1_database_idCloudflare D1 (serverless SQLite)
supabaseproject_url, api_keySupabase (PostgreSQL via PostgREST)
sqlitepathLocal SQLite file
apiurlREST API (optional headers)
Tip: Use ${VAR_NAME} syntax in config values for environment variable expansion. Store credentials in a .env file (never committed to source control).

When to use dsn: vs local:

local:dsn:
Configuration[database] section[datasources.name] sections
Number of backendsOne per appUnlimited
Supports form actionsYes (w-store)Read-only (fetch)
Best forPrimary app dataExternal services, multi-DB setups