How Routing Works
Routes decide which content is published, and at which URL. They live in the
Routes Manager, and they are the only thing that gives content an address.
A page or a record that no route serves isn't published at all: it doesn't
appear on the deployed site or in sitemap.xml.
A route is one piece of a URL. /team/john/ is two routes: team, and a route
beneath it that spells john. You build URLs by nesting routes, and the full
path is always read off the tree, so renaming team moves everything under it.
Each route has three things:
| Column | What it does |
|---|---|
| Name | The URL segment. Static text like team, or an expression like {this.title.toSlug()}. |
| Content | What the route serves: a single page, records, whole content types, or data sources. |
| Template | The template the served content is rendered through. Optional. |
Kinds of route
The Routes Manager's toolbar starts a route in one of three ways. They differ only in the name the route opens with, and the name alone decides what kind of route it is.
The site root
The / row at the top of the tree is your homepage. There is exactly one, and
every other route hangs beneath it. Assign it a page and that page is served at
the bare domain.
Static route
A fixed name, like team, about or getting-started. Lowercase letters and
numbers, separated by - or _.
A static route serves at most one page. It can also serve nothing: then it
answers with a 404 and simply holds the routes nested under it. That's normal,
and it's how you get /brands/vantara/ without a page at /brands/.
Dynamic route
A name with curly braces, like {this.title.toSlug()}. A dynamic route produces
one page per record it serves: assign it the Employee content type and
every employee gets a page.
The name is resolved once per record, with that record as this. It's the same
dynamic data engine the canvas uses,
so essentially any dynamic data template works in a route name: fields,
relationships, modifiers
and expression functions, mixed
with plain text:
{this.title.toSlug()} → john
{this.fields.year}-{this.title.toSlug()} → 2026-launch-party
{this.fields.brand.title.toSlug()} → northwind
A dynamic route can serve any mix of content: whole content types, records picked one by one, or data sources.
this.title on its own gives you John or Red Fox Motors, with capitals and
spaces. Add .toSlug() so every URL comes out lowercase and URL-safe.
Group
A name in parentheses, like (marketing). A group adds nothing to the URL
and serves nothing. It exists to give a set of routes a shared template without
inventing a path segment for them: /(marketing)/pricing is published at
/pricing/.
Nested dynamic routes
Every expression in a path is resolved against the record the deepest route serves. The routes above it don't pass records down; they are just more of that one record's address.
Take this tree, where only the last route has content assigned:
brands
└── {this.fields.brand.title.toSlug()} no content
└── {this.title.toSlug()} serves: Cars
For the car Breeze, both expressions read the Breeze: this.title is Breeze, and
this.fields.brand.title follows its brand relationship to Northwind. The page
lands at /brands/northwind/breeze/. The route that owns the content owns its whole
path.
That's also why a dynamic route with no content produces no pages of its own:
there's no record to spell its name with. If you want /brands/northwind/ as a page
too, give a route at that level its own content. See
Cars and Brands for the full walkthrough.
A record whose expression resolves to nothing, for example an empty field, is skipped: it has no address, so it isn't published.
Pages from a data source
A dynamic route can also publish pages from a data source, so a product feed or a JSON list gets pages without a Content Hub record per item. Pick it under Data Sources in the Content Picker; only dynamic routes offer that section.
What the source returns decides how many pages it gets:
- An array is a page per element: a list of 20 products is 20 pages.
- Anything else is one page.
- Nothing, such as an API source with no URL, is no page.
The element is this, in the route's name and in its template, exactly as the
source returned it. With a products source returning
[{ "sku": "abc-123", "name": "Desk" }, …], a route {this.sku} under shop
publishes /shop/abc-123/, and the template reads {this.name}.
A few rules:
- It needs a template. A value has no blocks of its own, so a route that serves a data source without a template, its own or inherited, shows an error in the Routes Manager and stops the deploy.
- A failing source fails the deploy. If the API is down, the deploy stops rather than unpublishing every page the source served.
- Pages are a snapshot. The source runs at deploy time; a change upstream shows up on the next deploy.
- The Routes Manager runs the source each time you edit a route, to list the resulting URLs. An API source makes a request per edit.
- A value has no title. Give the template a
<title>, or the route an SEO title. The Routes Manager and deploy errors call a page by its source and position, like Products #3. The sitemap lists the page without a last modified date.
While editing the template, you can preview it with any value a route serves through it.
Templates
A template is a page from a template type, which you create under
Templates in the Content Hub. You design it in the builder like any page.
When a route renders content through it, this is the record being served:
<h1>Meet {this.title}</h1>
While editing a template you can pick which record to preview it with, so you see real data on the canvas.
A few rules:
- Templates are inherited. A route without a template uses the nearest one above it. The Routes Manager shows an inherited template dimmed, marked Inherited. Choose Inherit from parent to clear a route's own template.
- Templates don't stack. The nearest template wins, and only that one. For a shared header and footer, use a component inside each template.
- Templates are never published themselves. They can't be assigned as a route's content, and the content picker doesn't offer them.
- No template means the record renders its own blocks, as a plain page does.
To render the record's own blocks inside a template, add a
Render element and point it at this.content:
<article>
<h1>{this.title}</h1>
{@render this.content}
</article>
Whatever you built on the record itself in the builder shows up there, wrapped in the template around it.
When two routes want the same URL
Routes can overlap, for example a static team/careers next to a dynamic
team/{this.title.toSlug()}. The build settles it:
- The more specific route wins. Static text beats an expression, and a
longer fixed part beats a shorter one.
careersalways wins over a team member that happens to be called Careers. - Two records nothing can tell apart fail the deploy. If two employees are
both called John, both want
/team/john/, and the deploy stops with an error naming both. Make the name more specific, for example{this.title.toSlug()}-{this.fields.role.toSlug()}. - URLs that differ only in case fail the deploy.
/team/John/and/team/john/are the same file on many systems and duplicate pages to search engines. Using.toSlug()avoids this.
A name that resolves to something no URL can hold, such as a /, also stops
the deploy with an error naming the record.
Where a page is served
Every item in the Content Hub shows Served under: the routes that publish
it. A page served by a static route shows its exact path, like /team/. A
record served by a dynamic route currently shows the route's pattern, like
/team/{this.title.toSlug()}, rather than its resolved URL. Nowhere yet
means no route serves it.
Published URLs
- Every page is published with a trailing slash:
/team/john/. The bare form,/team/john, redirects to it. sitemap.xmllists every published URL.- Routes, and new content they pick up, take effect on the next deploy. You don't need to revisit the Routes Manager when you add content: a new employee gets their page as soon as you deploy.
this.slug and this.isHomepage
A record's slug is read from the routes, not typed in: it's the record's
path without the surrounding slashes, taken from the static routes that serve
it.
| Published at | slug | isHomepage |
|---|---|---|
/ | '' | true |
/about/ | about | false |
/docs/intro/ | docs/intro | false |
| nowhere | absent | false |
So https://example.com/{this.slug}/ is always a page's real address. To move a
page, rename its route.
Because the routes decide slug and isHomepage, a route's name can't use
either of them. Use {this.title.toSlug()} or another field instead.
A record served only by a dynamic route has no slug yet. Link to it by
building the same expression the route uses, for example
/team/{member.title.toSlug()}/.