Links and URLs
How a template builds links with path(), url(), siteUrl() and localeUrl(), prints an entry's address, renders link and asset fields, and why every variable in href or src goes through the url filter.
Templates never assemble an address from strings. A document’s address comes from its route, so it is read from the entry; a literal path, a type’s index and another site are built by helpers that add the site’s path prefix and the locale prefix; and an editor’s link comes from a link field that resolves its target, its text and its attributes. If a route or a locale prefix changes, every link follows.
A minimal example
<nav>
<a href="{{ path('/') }}">{{ t("nav.home") }}</a>
<a href="{{ path('event') }}">{{ t("nav.whats_on") }}</a>
<a href="{{ path('/visit/accessibility') }}">{{ t("nav.access") }}</a>
</nav>
{% for article in items %}
<a href="{{ article.uri|url }}">{{ article.title }}</a>
{% endfor %}
{% if entry.bookingUrl %}{{ entry.bookingUrl.link(text=t("event.book"), class="button") }}{% endif %} path('/')is the home page with the locale prefix the site’s strategy needs.path('event')is the index page of theeventtype in the request’s locale, whatever segment its route gives it in that locale.article.uriis the article’s own path; a variable inhrefgoes through|url.bookingUrl.link()prints the editor’s link with the text,rel, new-tab announcement and class the editor chose.
The url filter rule
engine-dmo check refuses a variable printed into href, src, action, formaction, srcset or poster unless it goes through the url filter:
<a href="{{ entry.uri|url }}"> {# accepted #}
<a href="{{ entry.uri }}"> {# error: a variable inside href must go through |url #} The filter validates the scheme. Relative paths, #fragment, ?query, http, https, mailto, tel, sms and otpauth pass; anything else (javascript:, data:, a protocol-relative //host) becomes #. It also escapes the value for the attribute.
Values built by the engine’s own helpers are already safe, so these need no filter: path(...), url(...), siteUrl(...), assets.url(...), feedUrl(...), calendarUrl(...), the icsUrl filter, and a field value’s method called with parentheses, such as entry.heroImage.url('hero'). A property read without parentheses (entry.uri, entry.url, item.url, pagination.next, localeUrl('fr')) needs |url.
Helpers
path()
| Call | Returns |
|---|---|
path() | The site’s home in the request locale: /, or /fr/ under a prefix strategy |
path('/visit') | The literal path with the site’s path prefix and the request locale’s prefix |
path('event') | The event type’s index page in the request locale; the argument itself when the type has no index route on the site |
path('event', entry) | The entry’s own path (entry.uri); empty when the second argument is not an entry |
path('/visit', locale='fr') | The path in another locale the site enables; empty when it does not enable it |
path(locale='fr') | The current page in that locale, as localeUrl('fr') |
A path starts with /. Anything without a leading slash is read as a type key.
url()
url(...) takes the same arguments as path() and returns an absolute URL on the origin the visitor is on (scheme, host and port of the request). Use it where an absolute address is needed and the visitor’s own host is right, such as a share link. For an address search engines read (canonical, hreflang, sitemaps, structured data), the engine uses the site’s configured domain itself; see SEO and structured data.
siteUrl()
An absolute URL on any site of the project, for a link from one site to another:
<a href="{{ siteUrl('/', site='county') }}">{{ t("nav.county_site") }}</a>
<a href="{{ siteUrl('listing', site='county', locale=locale.code) }}">{{ t("nav.places") }}</a> | Argument | Default | Meaning |
|---|---|---|
| first | / | A path starting with /, or a type key for that type’s index page on the target site |
site | this site | Another site’s key; an unknown key is an error at render |
locale | the target site’s default locale | A locale the target site enables; ignored when it does not enable it |
The host is the target site’s domain. When the visitor came in on a port (in development, town.localhost:8080), the link keeps the port. An absolute URL as the first argument is an error.
localeUrl() and the language switcher
| Helper | Returns |
|---|---|
localeUrl('fr') | The current page in another locale; empty where the page has no translation published on this site |
localeSwitcher() | A ready-made <nav> of the locales this page exists in, the current one marked |
locales | The site’s enabled locales, for a switcher you build yourself |
entry.translations | Each enabled locale’s { locale, exists, stale, url, path } for this document |
<ul>
{% for code, tr in entry.translations %}
{% if tr.exists %}<li><a href="{{ tr.path|url }}" hreflang="{{ code }}" lang="{{ code }}">{{ code }}</a></li>{% endif %}
{% endfor %}
</ul> An entry’s address
| Property | Value |
|---|---|
entry.uri | The path on this site, with the locale prefix (/whats-on/film-club) |
entry.url | The same, absolute |
entry.translations.<code>.path and .url | The document in another locale on this site; both empty when it does not exist there |
page.uri | The current page’s path |
page.canonical | The page’s canonical URL, absolute on the site’s configured domain |
page.alternates | The locales the page exists in on this site, each locale, hreflang, url (absolute) and path |
Use uri for a link on the page, {{ item.uri|url }}: it is a path, so the link stays on whatever host the visitor is on. Use url only where an absolute address is needed, such as a share link or an email. Both are correct in href; the examples in this handbook use uri.
A document with no route on the site has no uri or url. Relations, list items, children and ancestors are entries too, so item.uri works everywhere. Navigation items carry url, isExternal, isCurrent and isActive (Navigation).
Index pages give pagination.prev and pagination.next as paths that keep the selected facets, and each facet value its own url:
{% if pagination.next %}<a rel="next" href="{{ pagination.next|url }}">{{ t("pager.next") }}</a>{% endif %} Link fields
A link field stores an editor’s link to a document, an asset, a web address, an email address, a phone number, another site or an anchor. Its value resolves the target at render, so a link to a document follows the document when its address changes, and a link to a document that is not live in the locale is reported as broken. How kinds, styles and editor attributes are configured is in the link field.
{# the whole <a>, with everything the editor set #}
{{ entry.cta.link() }}
{{ entry.cta.link(text=t("more"), class="button button--primary", data-track="cta") }}
{# your own element around the editor's attributes #}
<a {{ entry.card.attributes(class="card__link") }}>
<span class="card__title">{{ entry.card.text }}</span>
</a>
{# a list of links #}
{% for l in entry.links %}<li>{{ l.link() }}</li>{% endfor %} Helpers
| Helper | Prints |
|---|---|
link(text=, ...attributes) | The <a> with href, the editor’s title, accessible name, rel, target, the chosen style’s class merged with class=, and the new-tab and file-type announcements. Nothing when the link is broken or empty |
attributes(...attributes) | The same attributes alone, href included, for an element you write yourself |
href is the link’s own and is refused as a keyword. style= and inline event handlers are refused. engine-dmo check warns when a literal aria-label= does not contain the literal text=, since the visible text must be part of the accessible name.
Properties
| Property | Value |
|---|---|
url | The resolved address with the editor’s suffix (query or fragment); empty when broken |
text | The editor’s text, else the target document’s title, the asset’s title, the URL’s host and path, the address or the number |
kind | The kind’s key as the field declares it |
of | The engine kind: document, asset, url, email, phone, site or anchor |
title, ariaLabel | The editor’s title and accessible name |
newTab, target | Whether the editor asked for a new tab; _blank or empty |
rel | The chosen rel tokens, with noopener for a new tab |
style, class | The chosen style’s key and class |
isExternal | Whether the link leaves the site |
broken | Whether the target is not live in the locale chain |
document | The target document as an entry, one level deep |
asset | The target asset: url, title, filename, mime, extension, bytes, size, type |
fields | The link’s sub-field values, by name |
isAffiliate, affiliate | With the affiliates module (engine-dmo): whether the link is tracked through an affiliate programme, rewritten to the site’s /go/ redirect and marked rel="sponsored", and the programme’s names |
Printing the value itself, {{ entry.cta }}, prints its url. With multiple: true the value is a list.
Links to other websites
Every link to another website, in a link field or in rich text, takes the site’s outbound rules on top of the editor’s own choices:
# config/sites/<key>/site.yaml
links:
outbound:
new_tab: true # open in a new tab, announced, with noopener
rel: [noopener, noreferrer] # tokens every outbound link carries A link counts as outbound when its host is not one of the install’s sites. The new-tab announcement text is the string link.new_tab in the string catalogue.
Asset links
Any asset value has link(), a download link that names the file’s type and size in the request locale:
{{ entry.report.link() }} {# Annual report (PDF, 2.1 MB) #}
{{ entry.report.link(text=t("download.report")) }} {{ entry.report }} prints the original file’s URL. For images, see Images.
Static files
assets.url(path) is a file under the site’s public directories with a version query from its modification time, so a changed file is fetched again and an unchanged one stays cached for a year:
<link rel="icon" href="{{ assets.url('favicon.svg') }}" type="image/svg+xml"> Files resolve along public/<site>/, public/_shared/, then public/. See Frontend build.
Feeds and calendars
| Helper | Returns |
|---|---|
feedLinks() | The <link rel="alternate"> tags of the site’s feeds in this locale; feedLinks('article') one type’s |
feedUrl('article', format='atom') | A type’s feed path in this locale (rss, the default, atom or json); empty when it has none |
calendarUrl('event') | A type’s calendar feed path in this locale; empty when it has none |
entry|icsUrl | An event’s .ics address, when its route has a calendar |
Feeds and calendars are configured on the route (routes reference).
Related
- Sites, locales and routes: how a route and a locale strategy make an address.
- Redirects and URLs: what happens to an old address.
- Multisite templates: linking between sister sites.
Images
How to print responsive images from asset fields and theme files with img(), picture() and figure(), configure named transforms, breakpoints and art direction in config/images.yaml, and check what a page downloads.
SEO and structured data
What head() and jsonld() print, how titles, descriptions, canonical URLs, hreflang, robots and Open Graph are decided, how to map a content type to schema.org, and how the sitemap and robots.txt are configured.