# Dynamic data

Your sitemd site isn't limited to static content. Connect a database or API and display live, auto-updating data as cards, lists, tables, or full detail pages — without writing any JavaScript. Combine it with [user auth](/docs/user-auth) and gated pages to build client portals, member dashboards, order histories, and personalized experiences that would normally require a custom web app.

{#what-you-can-build}
## What you can build

Dynamic data turns a markdown site into something much closer to a full application. A few examples:

- **Product catalog** — display inventory from Supabase or Airtable as cards, link each to a detail page with full specs, pricing, and stock status
- **Client portal** — gate an "orders" or "invoices" page behind login, filter by `{{currentUser.id}}`, and give each customer a private order history with detail views
- **Member directory** — pull team or community profiles from a database, display as a list with thumbnails, link to individual profile pages
- **Course platform** — show enrolled courses filtered by user, track progress per lesson, gate content by subscription tier
- **Job board** — list open positions from an API, link each to a detail page with full description, requirements, and an apply button
- **Knowledge base** — feed articles from a headless CMS, display as a searchable list, render each article on its own detail page
- **Event listings** — show upcoming events as cards, link to detail pages with schedule, speakers, and registration info
- **Support ticket tracker** — let logged-in users see their open tickets, click through to individual ticket detail with status and history

All of these are built with the same building blocks: a data source definition, a display mode, and optionally auth gating and detail pages.

{#setup}
## Setup

1. Configure your provider in `settings/data.md` — set the `provider` field
2. Add credentials via `sitemd config setup data`
3. Define named data sources in the `sources` array
4. Use `data:` blocks in any page to display data

```yaml
---
provider: supabase
cacheTTL: 300
sources:
  - name: products
    table: products
    select: id, name, description, photo_url, price, slug
    filter: active = true
    sort: name asc
---
```

{#providers}
## Providers

### Supabase

- Uses PostgREST API directly
- Shares credentials with auth if both use Supabase
- Supports Row Level Security for user-scoped data

### Firebase

- Uses Firestore REST API
- Shares credentials with auth if both use Firebase

### Airtable

- Connects to any Airtable base
- Great for non-technical content management

### REST API

- Generic adapter for any backend
- Configurable base URL and headers
- Auto-detects response format (array, data.data, data.items, data.results)

{#display-modes}
## Display modes

### Cards

```
data: products
data-display: cards
data-detail: modal
data-title: name
data-text: {{price}} — {{description}}
data-image: photo_url
data-link: View Details: #
data-detail-field: Name: name
data-detail-field: Price: {{price}}
data-detail-field: Description: description
data-detail-field: In Stock: stock_status
```

Map fields: `data-title`, `data-text`, `data-image`, `data-link` — same slots as static cards. With `data-detail: modal`, clicking "View Details" opens a modal instead of navigating.

<div class="sitemd-data" style="margin:var(--space-lg) 0">
<div class="sitemd-data-content">
<div class="card-grid">
<div class="card">
<img src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=600&h=400&fit=crop" alt="Wireless Headphones" loading="lazy" class="card-image">
<div class="card-body">
<h3 class="card-title">Wireless Headphones</h3>
<p class="card-text">$79 — Premium over-ear headphones with active noise cancelling and 30-hour battery life</p>
<span class="card-link"><a href="#" class="data-detail-trigger" data-demo-modal="demo-product-1">View Details &#8594;</a></span>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1626958390898-162d3577f293?w=600&h=400&fit=crop" alt="Mechanical Keyboard" loading="lazy" class="card-image">
<div class="card-body">
<h3 class="card-title">Mechanical Keyboard</h3>
<p class="card-text">$129 — Compact 75% layout with hot-swappable switches and RGB backlighting</p>
<span class="card-link"><a href="#" class="data-detail-trigger" data-demo-modal="demo-product-2">View Details &#8594;</a></span>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1616578273577-5d54546f4dec?w=600&h=400&fit=crop" alt="USB-C Hub" loading="lazy" class="card-image">
<div class="card-body">
<h3 class="card-title">USB-C Hub</h3>
<p class="card-text">$45 — 7-in-1 adapter with HDMI, USB-A, SD card reader, and 100W passthrough charging</p>
<span class="card-link"><a href="#" class="data-detail-trigger" data-demo-modal="demo-product-3">View Details &#8594;</a></span>
</div>
</div>
</div>
</div>
</div>

<div class="modal-overlay" data-modal-id="demo-product-1" role="dialog" aria-modal="true" hidden>
<div class="modal-wrap">
<button class="modal-close" type="button" aria-label="Close">&times;</button>
<div class="modal-dialog"><div class="modal-body content">
<dl class="sitemd-data-detail">
<div class="data-detail-row"><dt class="data-detail-label">Name</dt><dd class="data-detail-value">Wireless Headphones</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Price</dt><dd class="data-detail-value">$79</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Description</dt><dd class="data-detail-value">Premium over-ear headphones with active noise cancelling and 30-hour battery life</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">In Stock</dt><dd class="data-detail-value">Available</dd></div>
</dl>
</div></div>
</div>
</div>

<div class="modal-overlay" data-modal-id="demo-product-2" role="dialog" aria-modal="true" hidden>
<div class="modal-wrap">
<button class="modal-close" type="button" aria-label="Close">&times;</button>
<div class="modal-dialog"><div class="modal-body content">
<dl class="sitemd-data-detail">
<div class="data-detail-row"><dt class="data-detail-label">Name</dt><dd class="data-detail-value">Mechanical Keyboard</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Price</dt><dd class="data-detail-value">$129</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Description</dt><dd class="data-detail-value">Compact 75% layout with hot-swappable switches and RGB backlighting</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">In Stock</dt><dd class="data-detail-value">Available</dd></div>
</dl>
</div></div>
</div>
</div>

<div class="modal-overlay" data-modal-id="demo-product-3" role="dialog" aria-modal="true" hidden>
<div class="modal-wrap">
<button class="modal-close" type="button" aria-label="Close">&times;</button>
<div class="modal-dialog"><div class="modal-body content">
<dl class="sitemd-data-detail">
<div class="data-detail-row"><dt class="data-detail-label">Name</dt><dd class="data-detail-value">USB-C Hub</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Price</dt><dd class="data-detail-value">$45</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Description</dt><dd class="data-detail-value">7-in-1 adapter with HDMI, USB-A, SD card reader, and 100W passthrough charging</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">In Stock</dt><dd class="data-detail-value">Available</dd></div>
</dl>
</div></div>
</div>
</div>

<script>
(function(){
  document.addEventListener('click',function(e){
    var trigger=e.target.closest('[data-demo-modal]');
    if(trigger){e.preventDefault();var id=trigger.dataset.demoModal;var overlay=document.querySelector('[data-modal-id="'+id+'"]');if(overlay){overlay.hidden=false;document.body.style.overflow='hidden';overlay.querySelector('.modal-close').focus();}return;}
    var close=e.target.closest('.modal-close');
    if(close){var ov=close.closest('.modal-overlay');if(ov){ov.hidden=true;document.body.style.overflow='';}return;}
    if(e.target.classList&&e.target.classList.contains('modal-overlay')){e.target.hidden=true;document.body.style.overflow='';}
  });
  document.addEventListener('keydown',function(e){if(e.key==='Escape'){var open=document.querySelector('.modal-overlay:not([hidden])');if(open){open.hidden=true;document.body.style.overflow='';}}});
})();
</script>

### List

```
data: recent-posts
data-display: list
data-title: title
data-text: {{excerpt}}
data-image: cover_image
data-link: Read More: /blog/{{slug}}
```

Clean list with optional thumbnails. Use it for blog feeds, article indexes, or any collection where a linear layout fits better than a grid.

<div class="sitemd-data" style="margin:var(--space-lg) 0">
<div class="sitemd-data-content">
<ul class="sitemd-data-list">
<li class="data-list-item">
<img src="https://images.unsplash.com/photo-1761322572550-967ea8c0bfd9?w=112&h=112&fit=crop" alt="" loading="lazy" class="data-list-thumb">
<div class="data-list-body">
<a href="/docs/dynamic-data/post" class="data-list-title" target="_blank" rel="noopener noreferrer">Why Markdown Still Wins in 2026</a>
<p class="data-list-text">After decades of WYSIWYG editors and visual builders, plain text markdown remains the fastest way to create structured content.</p>
</div>
</li>
<li class="data-list-item">
<img src="https://images.unsplash.com/photo-1754039984985-ef607d80113a?w=112&h=112&fit=crop" alt="" loading="lazy" class="data-list-thumb">
<div class="data-list-body">
<a href="/docs/dynamic-data/post" class="data-list-title" target="_blank" rel="noopener noreferrer">Building Documentation Sites with Claude Code</a>
<p class="data-list-text">A walkthrough of how we built the sitemd docs using the same tool that powers the product.</p>
</div>
</li>
<li class="data-list-item">
<img src="https://images.unsplash.com/photo-1635602739175-bab409a6e94c?w=112&h=112&fit=crop" alt="" loading="lazy" class="data-list-thumb">
<div class="data-list-body">
<a href="/docs/dynamic-data/post" class="data-list-title" target="_blank" rel="noopener noreferrer">Shipping User Auth in a Static Site</a>
<p class="data-list-text">How sitemd adds client-side authentication with five provider adapters while keeping the site fully static.</p>
</div>
</li>
</ul>
</div>
</div>

### Table

```
data: my-orders
data-display: table
data-detail: modal
data-field: Order: order_number
data-field: Status: status
data-field: Total: {{currency}}{{amount}}
data-field: Date: created_at
data-link: View: #
data-detail-field: Order Number: order_number
data-detail-field: Status: status
data-detail-field: Total: {{currency}}{{amount}}
data-detail-field: Shipping: {{street}}, {{city}}, {{state}} {{zip}}
data-detail-field: Items: items_description
```

Uses `data-field: Label: template` for columns. With `data-detail: modal`, clicking "View" opens the full record in a modal.

<div class="sitemd-data" style="margin:var(--space-lg) 0">
<div class="sitemd-data-content">
<div class="sitemd-data-table-wrap">
<table class="sitemd-data-table">
<thead><tr><th>Order</th><th>Status</th><th>Total</th><th>Date</th><th></th></tr></thead>
<tbody>
<tr><td>ORD-4821</td><td>Shipped</td><td>$129.00</td><td>2026-03-15</td><td><a href="#" class="data-detail-trigger" data-demo-modal="demo-order-1">View</a></td></tr>
<tr><td>ORD-4807</td><td>Delivered</td><td>$45.00</td><td>2026-03-12</td><td><a href="#" class="data-detail-trigger" data-demo-modal="demo-order-2">View</a></td></tr>
<tr><td>ORD-4793</td><td>Delivered</td><td>$238.50</td><td>2026-03-08</td><td><a href="#" class="data-detail-trigger" data-demo-modal="demo-order-3">View</a></td></tr>
<tr><td>ORD-4756</td><td>Delivered</td><td>$79.00</td><td>2026-02-28</td><td><a href="#" class="data-detail-trigger" data-demo-modal="demo-order-4">View</a></td></tr>
</tbody>
</table>
</div>
</div>
</div>

<div class="modal-overlay" data-modal-id="demo-order-1" role="dialog" aria-modal="true" hidden>
<div class="modal-wrap">
<button class="modal-close" type="button" aria-label="Close">&times;</button>
<div class="modal-dialog"><div class="modal-body content">
<dl class="sitemd-data-detail">
<div class="data-detail-row"><dt class="data-detail-label">Order Number</dt><dd class="data-detail-value">ORD-4821</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Status</dt><dd class="data-detail-value">Shipped</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Total</dt><dd class="data-detail-value">$129.00</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Shipping</dt><dd class="data-detail-value">742 Evergreen Terrace, Springfield, IL 62704</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Items</dt><dd class="data-detail-value">1x Mechanical Keyboard (Cherry MX Brown)</dd></div>
</dl>
</div></div>
</div>
</div>

<div class="modal-overlay" data-modal-id="demo-order-2" role="dialog" aria-modal="true" hidden>
<div class="modal-wrap">
<button class="modal-close" type="button" aria-label="Close">&times;</button>
<div class="modal-dialog"><div class="modal-body content">
<dl class="sitemd-data-detail">
<div class="data-detail-row"><dt class="data-detail-label">Order Number</dt><dd class="data-detail-value">ORD-4807</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Status</dt><dd class="data-detail-value">Delivered</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Total</dt><dd class="data-detail-value">$45.00</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Shipping</dt><dd class="data-detail-value">221B Baker Street, London, NW1 6XE</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Items</dt><dd class="data-detail-value">1x USB-C Hub (7-in-1)</dd></div>
</dl>
</div></div>
</div>
</div>

<div class="modal-overlay" data-modal-id="demo-order-3" role="dialog" aria-modal="true" hidden>
<div class="modal-wrap">
<button class="modal-close" type="button" aria-label="Close">&times;</button>
<div class="modal-dialog"><div class="modal-body content">
<dl class="sitemd-data-detail">
<div class="data-detail-row"><dt class="data-detail-label">Order Number</dt><dd class="data-detail-value">ORD-4793</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Status</dt><dd class="data-detail-value">Delivered</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Total</dt><dd class="data-detail-value">$238.50</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Shipping</dt><dd class="data-detail-value">1600 Pennsylvania Ave, Washington, DC 20500</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Items</dt><dd class="data-detail-value">1x Wireless Headphones, 1x Mechanical Keyboard</dd></div>
</dl>
</div></div>
</div>
</div>

<div class="modal-overlay" data-modal-id="demo-order-4" role="dialog" aria-modal="true" hidden>
<div class="modal-wrap">
<button class="modal-close" type="button" aria-label="Close">&times;</button>
<div class="modal-dialog"><div class="modal-body content">
<dl class="sitemd-data-detail">
<div class="data-detail-row"><dt class="data-detail-label">Order Number</dt><dd class="data-detail-value">ORD-4756</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Status</dt><dd class="data-detail-value">Delivered</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Total</dt><dd class="data-detail-value">$79.00</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Shipping</dt><dd class="data-detail-value">742 Evergreen Terrace, Springfield, IL 62704</dd></div>
<div class="data-detail-row"><dt class="data-detail-label">Items</dt><dd class="data-detail-value">1x Wireless Headphones</dd></div>
</dl>
</div></div>
</div>
</div>

{#detail-pages}
## Detail pages

Detail pages are where dynamic data becomes a real application. Instead of just listing records, each row in your data source gets its own page with a unique URL — a product page, an order receipt, a member profile, a job posting.

There are two ways to show record details: as a **standalone page** or in a **modal**.

### Standalone detail pages

A standalone detail page has its own URL, making it linkable, shareable, and bookmarkable. Create a separate markdown file and use `data-display: detail` with a URL parameter to identify which record to show.

**Product detail** (`pages/products/detail.md`):

```
data: products
data-display: detail
data-param: slug
data-key: slug
data-field: Name: name
data-field: Price: {{price}}
data-field: Description: description
data-field: In Stock: stock_status
data-field: Category: category
```

- `data-param` — which URL query parameter to read (e.g., `slug` reads `?slug=wireless-headphones`)
- `data-key` — which data field to match against the param value
- `data-field` — label/value pairs rendered as a definition list

Link to it from a card or list: `data-link: View: /products/detail?slug={{slug}}`

This is what makes dynamic data a full application pattern. A product catalog links each card to `/products/detail?slug=wireless-headphones`. A member directory links each name to `/members/profile?id=42`. A job board links each listing to `/jobs/detail?id=senior-engineer`. The list page and detail page share the same data source — the detail page just filters to one record.

**Blog post detail** (`pages/blog/post.md`):

```
data: recent-posts
data-display: detail
data-param: slug
data-key: slug
data-field: Title: title
data-field: Author: author
data-field: Published: created_at
data-field: Body: body
```

<div class="sitemd-data" style="margin:var(--space-lg) 0">
<div class="sitemd-data-content">
<dl class="sitemd-data-detail">
<div class="data-detail-row">
<dt class="data-detail-label">Title</dt>
<dd class="data-detail-value">Why Markdown Still Wins in 2026</dd>
</div>
<div class="data-detail-row">
<dt class="data-detail-label">Author</dt>
<dd class="data-detail-value">Sarah Chen</dd>
</div>
<div class="data-detail-row">
<dt class="data-detail-label">Published</dt>
<dd class="data-detail-value">2026-03-14</dd>
</div>
<div class="data-detail-row">
<dt class="data-detail-label">Body</dt>
<dd class="data-detail-value">After decades of WYSIWYG editors and visual builders, plain text markdown remains the fastest way to create structured content...</dd>
</div>
</dl>
</div>
</div>

### Modal details

Open detail inline without leaving the page. Add `data-detail: modal` and `data-detail-field:` lines to any cards, list, or table block. No separate page needed:

```
data: products
data-display: cards
data-detail: modal
data-title: name
data-text: {{price}} — {{description}}
data-image: photo_url
data-link: View Details: #
data-detail-field: Name: name
data-detail-field: Price: {{price}}
data-detail-field: Description: description
data-detail-field: In Stock: stock_status
```

When `data-detail: modal` is set, clicking the link opens a modal overlay with the detail fields rendered inside — using the same modal system as [Tooltips & Modals](/docs/tooltips-modals). The `data-link` label is used as the trigger text; the URL is ignored.

Works with all collection display modes (cards, list, table). Use modals for quick-peek details (a product spec summary, an order snapshot) and standalone pages when you need a permanent, shareable URL (a receipt a customer can bookmark, a profile link they can share).

{#personalized-portals}
## Building personalized portals

Dynamic data, [user auth](/docs/user-auth), and gated pages are three features that work together to create something much bigger than any of them alone: personalized, logged-in experiences. This is how you turn a markdown site into a client portal, a student dashboard, or a team workspace.

The pattern is straightforward:

1. **Auth** handles login/signup and gives you `{{currentUser.id}}`
2. **Data sources** filter by that user ID so each person sees only their own records
3. **Gated pages** restrict access to logged-in users (or specific user types)
4. **Detail pages** let users drill into individual records

### Example: customer order portal

A customer logs in and sees their order history. They click any order to see full details including shipping and line items. The entire experience is four things: a data source, two pages, and an auth setting.

**Data source** (`settings/data.md`):

```yaml
sources:
  - name: my-orders
    table: orders
    select: id, order_number, status, currency, amount, created_at, street, city, state, zip, items_description
    filter: user_id = {{currentUser.id}}
    sort: created_at desc
```

The `{{currentUser.id}}` filter means each user only sees their own orders. This is resolved at runtime after the user logs in.

**Orders list** (`gated-pages/orders.md`):

```
data: my-orders
data-display: table
data-auth: required
data-field: Order: order_number
data-field: Status: status
data-field: Total: {{currency}}{{amount}}
data-field: Date: created_at
data-link: View: /account/orders/detail?id={{id}}
data-sort: created_at desc
```

**Order detail** (`gated-pages/orders/detail.md`):

```
data: my-orders
data-display: detail
data-param: id
data-key: id
data-auth: required
data-field: Order Number: order_number
data-field: Status: status
data-field: Total: {{currency}}{{amount}}
data-field: Shipping: {{street}}, {{city}}, {{state}} {{zip}}
data-field: Items: items_description
```

Because the detail page is in `gated-pages/`, it requires login automatically. And because the data source filters by `{{currentUser.id}}`, a user can't view someone else's order even if they guess the ID. Direct link for email: `https://yoursite.com/account/orders/detail?id=abc123`

### Example: student course dashboard

A learning platform where students see their enrolled courses and track lesson progress.

**Data source** (`settings/data.md`):

```yaml
sources:
  - name: my-courses
    table: enrollments
    select: id, course_name, course_image, progress, total_lessons, completed_lessons, next_lesson_slug
    filter: student_id = {{currentUser.id}}
    sort: course_name asc
  - name: course-lessons
    table: lessons
    select: id, title, duration, completed, video_url, content
    sort: position asc
```

**My courses** (`gated-pages/courses.md`):

```
data: my-courses
data-display: cards
data-auth: required
data-title: course_name
data-text: {{completed_lessons}}/{{total_lessons}} lessons complete
data-image: course_image
data-link: Continue: /account/courses/detail?id={{id}}
```

**Course detail** (`gated-pages/courses/detail.md`):

```
data: course-lessons
data-display: list
data-param: id
data-auth: required
data-title: title
data-text: {{duration}} min
data-link: Watch: /account/lessons/watch?id={{id}}
```

### Example: team member workspace

An internal tool where team members see assigned tasks and project updates, with different views for different roles.

**Data source** (`settings/data.md`):

```yaml
sources:
  - name: my-tasks
    table: tasks
    select: id, title, status, priority, due_date, project_name
    filter: assignee_id = {{currentUser.id}}
    sort: due_date asc
```

**My tasks** (`gated-pages/dashboard.md`):

```
data: my-tasks
data-display: table
data-auth: required
data-field: Task: title
data-field: Project: project_name
data-field: Priority: priority
data-field: Due: due_date
data-field: Status: status
data-link: Open: /account/tasks/detail?id={{id}}
data-filter: status != done
```

Gate different pages by user type to create role-specific views — managers see all team tasks, individual contributors see only their own. See [User Auth & Gating](/docs/user-auth) for user type configuration.

{#auth-integration}
## Auth integration

- `data-auth: required` — hides the data block until the user is logged in, then sends the user's Bearer token with every request
- Use `{{currentUser.id}}` in source filters for user-scoped data:
  ```
  filter: user_id = {{currentUser.id}}
  ```
- Use `{{currentUser.email}}`, `{{currentUser.name}}`, or any field returned by your auth provider's `userDataUrl` webhook
- Combine with Supabase Row Level Security for defense-in-depth — even if the client-side filter were bypassed, the database enforces access

{#query-options}
## Query options

- `data-filter: field = value` — filter results (supports =, !=, >, <, >=, <=)
- `data-sort: field desc` — sort results
- `data-limit: 12` — limit number of results
- `data-paginate: true` — show prev/next pagination

{#template-syntax}
## Template syntax

Use `{{fieldName}}` anywhere in map values to interpolate data fields:

```
data-text: {{price}} — {{description}}
data-link: View: /products/{{slug}}
data-field: Address: {{street}}, {{city}}, {{state}} {{zip}}
```

{#caching}
## Caching

Data is cached in the browser session (sessionStorage) with a configurable TTL (default: 5 minutes). Set `cacheTTL` in `settings/data.md` to control this. Set to `0` to disable caching.

## Related

- [Portals](/docs/portals) — complete, copy-pasteable portal builds: freelancer portals, SaaS admin panels, real estate listings, membership directories, and e-commerce accounts
- [User Auth & Gating](/docs/user-auth) — authentication, gated pages, and user types
- [Cards](/docs/cards) — static card components that dynamic cards mirror
- [CLI Config](/docs/cli-config) — manage data provider credentials and other service config
- [Tooltips & Modals](/docs/tooltips-modals) — the modal system used by `data-detail: modal`