Detail pages

A detail page — a "drill-down" or sub page — is a single template that serves a whole collection: one file renders the focused view for every record, keyed by a value in the URL. You write pages/things/[id].md once and it answers /things/1, /things/42, … — the captured segment lands in your SQL as ${id}.

Pair it with a list page whose rows link into the detail page and you get the classic analytics flow: an overview table → click a row → a page focused on that one record.

1. The dynamic route #

A file or directory named [param] captures one URL segment as a route parameter (the mechanics live in Writing pages → Dynamic pages). The captured value flows into queries as ${param}, through the same context-aware, injection-safe substitution as every other parameter (see Queries):

<!-- pages/channels/[channel].md  →  /channels/<channel> -->
:::query name=channel_months connector=main
SELECT month, downloads
FROM downloads
WHERE channel = '${channel}'
ORDER BY month
:::

<LineChart data={channel_months} x="month" y="downloads" title="Monthly downloads" />

Note

Page prose is not templated — only the SQL inside :::query blocks sees ${param}. To show the current record's name in the body, read it from a query with a <Value /> (as the demo's detail page does), rather than typing the placeholder into Markdown.

How a route value reaches your data #

The captured value is substituted into the page's queries server-side, per data request — the browser sends it to /_dashdown/api/data/<query> and the server fills ${param} before running the SQL (injection-safe). So a detail page needs either the live server or a static_paths build (see Static export below) that pre-renders one snapshot per record; a plain dashdown build skips [param] pages, so a deployed export without static_paths has no data for them.

Note

The framework merges route params into every data/WebSocket request (at lowest precedence — an explicit filter still wins), which is what makes each record's request URL unique. Without that, two slugs of one template would produce byte-identical, cacheable data URLs (/api/data/q?_connector=main) and the browser would serve the first record's response for the second (and the server-side result cache would collide too). A built-in <Table>/chart gets this for free; a custom data-driven component should fetch through core.js's fetchQueryData so it inherits the same merge.

Make a list table's rows clickable with row_link, a URL pattern whose {column} placeholders are filled from each row:

<Table data={channel_totals} row_link="/detail-pages/{channel}" />

The whole row becomes a link, and its first column also renders as a real anchor so the drill-down stays reachable by keyboard and screen reader. Prefer a single clickable column instead of the whole row? Use link_column + link_pattern, or the detail_slug shorthand that links one column to {current page}/{value}:

Attribute Effect
row_link Whole row clickable; pattern with {column} placeholders.
link_column + link_pattern Make one column a link, e.g. link_pattern="/channels/{channel}".
detail_slug Shorthand: links the named column to {current path}/{value}.

Live demo #

The table below is a real list page. Each row links to /detail-pages/<channel>, served by the single template pages/detail-pages/[channel].mdclick any row to drill into that channel's detail page.

Note

Dynamic [param] pages are left out of the sidebar (no single enumerable URL). They still work under dashdown serve, and dashdown build can pre-render one static page per record when the template opts in — see Static export below.

3. Static export (dashdown build) #

By default a [param] template can't be exported — the build doesn't know which values exist. Tell it by adding a static_paths block to the page's frontmatter: a query whose rows enumerate the slug values (the same idea as Next.js / Astro getStaticPaths). Each row becomes one pre-rendered page, with its own data snapshot.

---
title: Channel detail
static_paths:
  connector: main
  query: SELECT DISTINCT channel FROM downloads ORDER BY channel
---

The query runs once at build time; each row supplies the route params by column name — a [channel] route needs a channel column (extra columns are ignored, so a richer list query works too). dashdown build then emits /detail-pages/pip, /detail-pages/docker, … each as a standalone index.html with that record's data baked in.

Note

Without static_paths, a [param] page is skipped by the build (live server only). Rows whose value is empty or contains a / (can't be one URL segment) are skipped with a warning. See Exporting. This demo's detail page ships a static_paths block, so dashdown build docs exports all three channels.

Generated