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.
<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.
<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"
| Syntax | Meaning | Example |
|---|---|---|
field=value | Equals | status=published |
& separator | AND (all must match) | status=published&featured=true |
, separator | OR (any can match) | category=tech,category=design |
Full-Text Search
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.
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:
{
"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.
[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
| Pattern | Backend | Effect |
|---|---|---|
dsn:name.collection | DB types | Query a collection (table) |
dsn:name/path | API type | GET base_url + path |
dsn:name | Any | Root: all collections (DB) or base URL (API) |
<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
| Type | Required Fields | Description |
|---|---|---|
d1 | account_id, api_token, d1_database_id | Cloudflare D1 (serverless SQLite) |
supabase | project_url, api_key | Supabase (PostgreSQL via PostgREST) |
sqlite | path | Local SQLite file |
api | url | REST API (optional headers) |
${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 backends | One per app | Unlimited |
| Supports form actions | Yes (w-store) | Read-only (fetch) |
| Best for | Primary app data | External services, multi-DB setups |