REST API
Every content operation is available via REST. All responses are JSON.
CORS is supported on all endpoints. The Access-Control-Allow-Origin header is set to the origin derived from your site.url config value — not a wildcard. This means API requests must originate from the same domain as your configured site URL. Preflight OPTIONS requests return 204 with appropriate CORS headers.
Pages
List all pages
GET /api/pages
Query parameters:
| Param | Type | Description |
|---|---|---|
limit |
number | Maximum pages to return (default: 20) |
offset |
number | Skip N pages (default: 0) |
template |
string | Filter by template name |
published |
boolean | Filter by publish status. Omit to return only published pages. |
order |
string | Sort as field:direction — e.g. date:desc, title:asc, order:asc |
taxonomy.{name} |
string | Filter by taxonomy value — e.g. taxonomy.tag=deno |
Response:
{
"items": [
{
"route": "/blog/hello-world",
"title": "Hello World",
"date": "2025-06-15",
"template": "post",
"format": "md",
"published": true,
"taxonomy": {
"tag": ["deno", "fresh"]
}
}
],
"meta": {
"total": 42,
"page": 1,
"pages": 3,
"limit": 20
}
}
Get a single page
GET /api/pages/{route}
Returns the full page object including rendered HTML and raw content. The {route} segment starts with / — e.g. /api/pages/blog/hello-world returns the page at route /blog/hello-world.
{
"route": "/blog/hello-world",
"title": "Hello World",
"date": "2025-06-15",
"template": "post",
"format": "md",
"rawContent": "---\ntitle: Hello World\n---\n\n# Hello World\n...",
"html": "<h1>Hello World</h1><p>This is my first post...</p>",
"frontmatter": { "title": "Hello World", "date": "2025-06-15" },
"media": [
{ "name": "cover.jpg", "url": "/content-media/02.blog/01.hello-world/cover.jpg", "type": "image/jpeg", "size": 48320 }
]
}
Returns 404 (as { "error": "Not found" }) if no page exists at that route.
Get child pages
GET /api/pages/{route}/children
Returns direct child pages of the given page.
{
"items": [
{
"route": "/blog/hello-world",
"title": "Hello World",
"date": "2025-06-15",
"template": "post",
"format": "md",
"order": 1
}
],
"total": 3
}
Get page media
GET /api/pages/{route}/media
Returns all co-located media files for a page, including sidecar metadata.
{
"items": [
{
"name": "cover.jpg",
"url": "/content-media/02.blog/01.hello-world/cover.jpg",
"type": "image/jpeg",
"size": 48320,
"meta": { "alt": "A sunset", "credit": "Photo by Jane Doe" }
}
],
"total": 1
}
Collections
Query a collection
GET /api/collections
Returns a filtered, ordered, paginated set of pages. Build queries with query parameters rather than frontmatter definitions.
Query parameters:
| Param | Type | Description |
|---|---|---|
source |
string | Collection source (default: @self.children). See below. |
order |
string | Sort field: date (default), title, order |
dir |
string | Sort direction: desc (default), asc |
limit |
number | Items per page (default: 20) |
offset |
number | Skip N items (default: 0) |
template |
string | Filter by template name |
Source values:
@self.children— all direct children (default)@page.children:/blog— children of a specific page@page.descendants:/blog— all descendants of a page@taxonomy.tag:deno— pages with a specific taxonomy value
Response:
{
"items": [
{
"route": "/blog/hello-world",
"title": "Hello World",
"date": "2025-06-15",
"template": "post",
"format": "md"
}
],
"meta": {
"total": 42,
"page": 1,
"pages": 3,
"hasNext": true,
"hasPrev": false
}
}
Taxonomy
List all taxonomies
GET /api/taxonomy
Returns all taxonomy types with their values and page counts.
{
"tag": {
"deno": 12,
"fresh": 8,
"cms": 3
},
"category": {
"tutorials": 5,
"announcements": 2
}
}
List taxonomy values
GET /api/taxonomy/{name}
Returns all values for a taxonomy type with page counts.
{
"name": "tag",
"values": {
"deno": 12,
"fresh": 8,
"cms": 3
}
}
Returns 404 if the taxonomy name is not defined in the site config.
Get pages by taxonomy value
GET /api/taxonomy/{name}/{value}
Returns all pages with a specific taxonomy value.
{
"taxonomy": "tag",
"value": "deno",
"items": [
{
"route": "/blog/hello-world",
"title": "Hello World",
"date": "2025-06-15",
"template": "post",
"format": "md"
}
],
"total": 12
}
Search
Full-text search
GET /api/search
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
q |
string | "" |
Search query |
template |
string | — | Filter by template name |
published |
"true" | "false" |
— | Filter by publish status |
lang |
string | — | Filter by language code |
from |
string | — | Min date (YYYY-MM-DD) |
to |
string | — | Max date (YYYY-MM-DD) |
taxonomy[{name}][] |
string | — | Filter by taxonomy value (repeatable) |
limit |
number | 20 (max 100) | Maximum results |
Response:
{
"query": "deno",
"total": 3,
"items": [
{
"route": "/blog/hello-world",
"title": "Hello World",
"template": "post",
"date": "2025-06-15",
"taxonomy": { "tag": ["deno"] },
"excerpt": "...built with deno and fresh...",
"score": 8.5
}
],
"filters": {
"taxonomy": { "tag": ["deno"] }
}
}
Returns an empty items array (not a 404) if no results are found or q is omitted.
Autocomplete suggestions
GET /api/search/suggest?q={prefix}
| Param | Type | Description |
|---|---|---|
q |
string | Prefix text (minimum 2 characters) |
Response:
{
"suggestions": ["deno", "deploy", "Dune CMS"]
}
Returns up to 10 suggestions matching indexed terms and page titles. Returns an empty array for prefixes shorter than 2 characters.
Site Configuration
Get site config
GET /api/config/site
Returns public site configuration values.
{
"title": "My Site",
"description": "A site built with Dune CMS",
"url": "https://example.com",
"author": { "name": "Jane Doe" },
"metadata": {},
"taxonomies": ["tag", "category"]
}
Navigation
Get navigation tree
GET /api/nav
Returns the ordered navigation tree of all visible pages.
{
"items": [
{
"route": "/",
"title": "Home",
"order": 1,
"depth": 0,
"template": "default"
},
{
"route": "/blog",
"title": "Blog",
"order": 2,
"depth": 0,
"template": "blog"
}
]
}
Content Media
Serve media file
GET /content-media/{source-path}/{filename}
Serves co-located media files. These URLs are generated automatically when resolving image references in Markdown. Responses include a one-hour Cache-Control header.
On-the-fly image processing
Append image processing parameters to any image URL to transform it on demand:
GET /content-media/{source-path}/{filename}?width=800&format=webp
Query parameters:
| Param | Alias | Type | Description |
|---|---|---|---|
width |
w |
number | Target width in pixels. Must be in allowed_sizes. |
height |
h |
number | Target height in pixels. Must be in allowed_sizes. |
quality |
q |
number | Output quality 1–100 (default: 80). |
format |
f |
string | Output format: jpeg, png, webp, avif. |
fit |
— | string | Resize fit mode: cover (default), contain, fill, inside, outside. |
focal |
— | string | Focal point for cover crop as x,y percentages: 50,30 = center-top. |
Processing is activated when at least one of width, height, quality, or format is present.
Supported input formats: .jpg, .jpeg, .png, .webp, .avif, .gif, .tiff.
Constraints:
widthandheightmust be values insystem.images.allowed_sizes— other values return400.- Maximum dimension is capped at 4096px.
- Invalid
focalvalues are silently ignored (falls back to center crop).
Processed images are cached with Cache-Control: public, max-age=31536000, immutable. The response also includes diagnostic headers X-Dune-Image, X-Dune-Image-Width, and X-Dune-Image-Height.
Flex Objects
Flex Objects are schema-driven custom data types managed outside the content tree. See the Flex Objects documentation for schema authoring details.
List records
GET /api/flex/{type}
Returns all records for the given type, sorted newest first.
[
{
"_id": "a3f2c19d0e8b",
"_type": "products",
"_createdAt": 1741234567890,
"_updatedAt": 1741234567890,
"name": "Ceramic Mug",
"price": 24.00,
"published": true
}
]
Returns 404 if the type schema does not exist. Returns an empty array if the type exists but has no records.
Get a single record
GET /api/flex/{type}/{id}
Returns a single record by its ID.
{
"_id": "a3f2c19d0e8b",
"_type": "products",
"_createdAt": 1741234567890,
"_updatedAt": 1741234567890,
"name": "Ceramic Mug",
"price": 24.00,
"published": true
}
Returns 404 if the type or record does not exist.