Skip to content

Show data on a page

A page is a route, an access rule, and a list of regions. A region reads one table and renders it in one shape. This guide covers the four regions that only read.

report is the region for a screen somebody works in. It gives you search, filters, sortable columns, pagination, and an empty state.

page members "Members" {
route: /members
access: board
report of members "All members" {
columns: last_name, first_name, email, joined_on
search: last_name, first_name, email
filter: fee_group, active
sort: last_name
page_size: 25
empty: "No members yet."
}
}

Every column in columns is sortable. The reader sorts by clicking a heading, and the link works with JavaScript turned off.

Search is case-insensitive and folds letters beyond ASCII, so a search for öztürk finds Öztürk. The engine compares the text itself rather than leaving it to the database’s byte order.

Add page_size: for any table that grows. Without it, a report renders every row it can read.

row -> names the page to open when somebody clicks a row.

report of members "All members" {
columns: last_name, first_name, email
row -> member
}

The target page carries the row in its route:

page member "Member" {
route: /members/{member}
new_route: /members/new
access: board
form of members {
fields: first_name, last_name, email, fee_group
actions: save, delete
}
}

new_route: gives the same page a second address for creating a row. The engine cannot guess a URL segment, so you write both.

For a second link per row, use row_action:

row_action "Signups" -> event_signups

detail renders one row as a record rather than as a table row. Use it for an article, a member file, or any page that is about one row.

A detail can show a multi or a list, because a metadata line has room for a set of links. A report cannot, because a cell holds one value.

page post "{:post.title}" {
route: /news/{slug}
access: public
data post of posts where status.public
detail of posts {
fields: published_at, author, tags
body: body
}
}

Two things are happening here.

data post of posts binds the row that the URL names. The name post is then available to the rest of the page.

{:post.title} interpolates the bound row into the page title. Any text an author writes can interpolate a value this way.

list renders the same rows as a report, in a layout for reading. It has no search box and no filters.

Use a list for a page a visitor reads and a report for a screen a member works in. They make different promises, which is why they are two regions and not one region with a flag.

stats groups a table and renders the totals as tiles.

page dashboard "Dashboard" {
route: /
access: board
stats of members "Members by status" {
group_by: active
}
stats of posts "Posts by state" {
access: editor
group_by: status
}
}

The second region carries its own access:. An editor sees both tiles. A board member without the editor role sees only the first, and the second is absent rather than refused.

where: adds a fixed condition on top of the access rule.

report of events "Coming up" {
columns: name, starts_at, place
where: starts_at >= now
sort: starts_at
}

A condition can compare columns, use now and today, walk a reference, and ask whether a related set is empty. It cannot call a function you write, because a condition has to become SQL that the engine can reason about.

NOTE: On a page that binds a row, a region over a different table must reference that row in its where:, such as where: event = :event.id. The compiler refuses a region that does not, because nothing else relates the two.