Meridian handbook
Templates

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 the event type in the request’s locale, whatever segment its route gives it in that locale.
  • article.uri is the article’s own path; a variable in href goes 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()

CallReturns
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>
ArgumentDefaultMeaning
first/A path starting with /, or a type key for that type’s index page on the target site
sitethis siteAnother site’s key; an unknown key is an error at render
localethe target site’s default localeA 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

HelperReturns
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
localesThe site’s enabled locales, for a switcher you build yourself
entry.translationsEach 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

PropertyValue
entry.uriThe path on this site, with the locale prefix (/whats-on/film-club)
entry.urlThe same, absolute
entry.translations.<code>.path and .urlThe document in another locale on this site; both empty when it does not exist there
page.uriThe current page’s path
page.canonicalThe page’s canonical URL, absolute on the site’s configured domain
page.alternatesThe 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 %}

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

HelperPrints
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

PropertyValue
urlThe resolved address with the editor’s suffix (query or fragment); empty when broken
textThe editor’s text, else the target document’s title, the asset’s title, the URL’s host and path, the address or the number
kindThe kind’s key as the field declares it
ofThe engine kind: document, asset, url, email, phone, site or anchor
title, ariaLabelThe editor’s title and accessible name
newTab, targetWhether the editor asked for a new tab; _blank or empty
relThe chosen rel tokens, with noopener for a new tab
style, classThe chosen style’s key and class
isExternalWhether the link leaves the site
brokenWhether the target is not live in the locale chain
documentThe target document as an entry, one level deep
assetThe target asset: url, title, filename, mime, extension, bytes, size, type
fieldsThe link’s sub-field values, by name
isAffiliate, affiliateWith 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.

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.

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

HelperReturns
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|icsUrlAn event’s .ics address, when its route has a calendar

Feeds and calendars are configured on the route (routes reference).