Routing
Every file in the
pages/
directory automatically becomes a route — there is no routing configuration.
.json
pages are native; other extensions become routable when an enabled extension claims them, which is how jxsuite.com serves
pages/index.md
as its front page (see
Jx Markdown
).
Static routes
The file path determines the URL path:
| File | URL |
|---|---|
pages/index.json |
/ |
pages/about.json |
/about |
pages/about/index.json |
/about |
pages/blog/index.json |
/blog |
pages/blog/first-post.json |
/blog/first-post |
index
files map to their parent directory, so
pages/about.json
and
pages/about/index.json
produce the same URL — use whichever keeps the folder tidy, but not both.
Files and directories whose names start with
_
are excluded from routing entirely. That's the convention for co-locating components next to the pages that use them:
pages/blog/_blog-card.json
is available for
$ref
but never becomes
/blog/_blog-card
.
Dynamic routes
Bracket syntax in filenames creates parameterized routes:
| File | URL pattern | Example match |
|---|---|---|
pages/blog/[slug].json |
/blog/:slug |
/blog/hello-world |
pages/[category]/[id].json |
/:category/:id |
/products/42 |
pages/docs/[...path].json |
/docs/* |
/docs/api/runtime/install |
[param]
matches exactly one path segment.
[...param]
is a catch-all: it matches the whole remaining path, slashes included — one catch-all page can serve an entire subtree.
Generating pages with $paths
A static build has to know every URL in advance, so a dynamic page declares which paths it generates with a top-level
$paths
. The most common shape drives the route from a
content collection
— this is (abbreviated) how the docs page you are reading is generated, from
pages/docs/[...slug].json
:
{
"$layout": "./layouts/docs.json",
"$paths": { "contentType": "docs", "param": "slug" },
"state": {
"page": {
"$prototype": "ContentEntry",
"contentType": "docs",
"id": { "$ref": "#/$params/slug" }
}
},
"children": [{ "tagName": "article", "children": "[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" }]
}
The compiler iterates
$paths
at build time and emits one HTML page per entry, substituting each entry's value into the route pattern. A dynamic page without
$paths
generates nothing (the build warns and skips it).
$paths
takes three shapes:
{ "contentType": "blog", "param": "slug", "field": "id" }
One page per collection entry.
param
names the route parameter (default
slug
);
field
picks which entry field supplies the value (default
id
, the entry id).
{ "values": ["en", "fr", "de"], "param": "lang" }
An explicit list of values.
{ "$ref": "./data/products.json", "param": "id", "field": "sku" }
A JSON array file, one page per item, with
field
selecting the property to use.
For catch-all routes the parameter value may itself contain slashes: nested content entries get path-based ids (like
framework/site/routing
), so a single
[...slug]
page fans out into the whole tree of URLs.
Your project's generated
document.schema.json
validates
$paths
against exactly these shapes plus any your extensions contribute, so a misspelled key or a source belonging to an extension you have not enabled is flagged in the editor and by
jx validate
— rather than building zero pages and warning at the end of the log. Run
jx schema
after changing
extensions
to keep that set current.
Route priority
When several routes could match the same URL, the more specific one wins:
Static routes beat dynamic routes —
/aboutbeats/[slug].Named parameters beat catch-alls —
/blog/[slug]beats/blog/[...path].More specific paths beat less specific ones —
/blog/[slug]beats/[...path].
Params at runtime
Inside a dynamic page, the current route's parameters are addressable as
#/$params/<name>
. The usual pattern binds a parameter into a
ContentEntry
id, as in the example above:
{ "id": { "$ref": "#/$params/slug" } }
Each generated page resolves the reference to its own concrete value —
/blog/hello-world
sees
slug
as
"hello-world"
. The compiler also injects a read-only
$page
state entry carrying
params
,
title
, and
url
, alongside the site-level
$site
context (see
project.json
). In static builds all of this resolves at compile time; nothing route-related ships to the browser.
In Studio, opening a dynamic page shows a params picker in the tab bar so the canvas previews a real entry instead of a placeholder.
Related
Content collections — the entries that drive
$pathsLayouts — the shells dynamic pages render into
Deployment — how generated routes are served