Mappings
Contents
Bind HTTP functions and filters to specific content types or site-relative URLs
Introduction
Mappings are the first custom processing step in the site service. Mappings execute before the component rendering. You may define mappings for both HTTP functions and filters. The use cases are vast.
Usage
Mappings are defined in the site descriptor (cms/site.yaml). Examples of use cases:
-
Bind an HTTP function to a specific URL pattern (e.g.
/mypath). -
Bind an HTTP function to a specific set of content via content matches (e.g. only the
portal:fragmentcontent type). -
Bind a filter to part or all of the site structure.
kind: "Site"
mappings:
- controller: "/cms/foobar/api.ts" (1) (2)
pattern: '/api/v\d+/.*' (3)
order: 10
- controller: "/cms/pages/default/default.ts"
match: "type:'portal:fragment'" (4)
- filter: "/cms/foobar/filter.ts" (5)
pattern: '/dont-match-me/.*'
invertPattern: true
order: 10
- filter: "/cms/services/image.ts" (6)
service: "image"
| 1 | controller — path to an HTTP function implementation. |
| 2 | order — execution order across apps. Default 50. Lower number means higher priority, with 1 being the pole position. See Execution order. |
| 3 | pattern — regular expression matched against the site-relative URL. Default /.*. See Pattern mappings. |
| 4 | match — property condition evaluated against the contextual content. See Match mappings. |
| 5 | filter — path to an HTTP filter implementation. |
| 6 | service — system service handled by this mapping (e.g. the .../_/image/... endpoint). See Service mappings. |
Each mapping must specify either filter: or controller: — never both — and at least one of pattern: or match:. |
Execution order
An application may contain multiple mappings, and multiple applications can be added to a site. If two apps within a site share the same mapping path or match expression, the mapping with the lower order value wins.
If several mappings have the same order value, the sort order of the applications on the site determines which mapping is used.
Which mappings apply is decided once, before the first filter runs, from the content originally requested. A filter that re-routes rendering to another content therefore does not change the selection — the mappings already chosen keep running, in order, against the re-routed request.
Pattern mappings
Pattern-based mappings can be used to take over the request for a specific URL before content rendering. Common cases include exposing an API endpoint on a contextual URL like /api, or triggering a filter for every request within the site.
The pattern property specifies a regular expression matched against the request URL.
The part of the URL involved is the path relative to the site where the application is configured. For example, if a site is deployed via the virtual host example.com/mysite/, then pattern: '/api/.*' matches requests with URL ending in example.com/mysite/api/.*. Protocol, host, and port are not involved in the pattern matching.
If the pattern contains the question mark ? character, the URL to match also includes query parameters. The query parameters are normalized, so they are always in alphabetical order.
For example, the pattern pattern: '/api\?category=foo&key=\d'+ matches both:
-
/api?category=foo&key=123 -
and
/api?key=123&category=foo
The question mark ? is escaped with a backslash in the pattern because it is a quantifier in regular expressions. Use single-quoted YAML strings for any pattern containing backslashes — YAML’s double-quoted form interprets backslash escapes and would corrupt regex tokens like \d. |
Set invertPattern: true on a mapping to negate the result of evaluating the regular expression.
The pattern string must be a valid regular expression.
Examples:
-
Anything ending with
.jsonwithin the/api/*path:pattern: '/api/.*\.json' -
All items within the site, including the site itself:
pattern: '/.*' -
Regex with query string:
pattern: '/endpoint\?bar=\d&foo=.*'+ -
Inverted via
invertPattern: truetogether withpattern: '/section/.*'
App key substitution
A pattern may include the literal token ${app}, which is expanded at parse time to the owning application’s key. This lets you write site-mapping patterns that reference the app’s own key without hardcoding it.
com.foo.bar
mappings:
- pattern: "/_/static/${app}/.+"
controller: "/site/mappings/static-controller.js"
Match mappings
Match-based mappings can be used to automatically render content types without using page templates or associating content with an implementation directly. A common case is automatic handling of the content type portal:fragment.
The match property specifies a condition related to the content corresponding with the requested URL path.
The condition takes the form of a property path followed by a :, and a value.
The property path can be one of the content properties (_id, _name, _path, type, displayName, hasChildren, language, valid) or any other custom property within the content.
Examples:
-
Match fragment content type:
match: "type:'portal:fragment'" -
Match content within the
/features/path:match: "_path:'/features/.*'" -
Match custom property with string value:
match: "data.employee.type:'developer'" -
Match custom property with a number:
match: "data.product.category:42" -
Match custom property with a boolean:
match: "x.com-enonic-myapp.menuItem.show:true" -
Match any content:
match: "type:'.+'"
The expected value can be either a regular expression matched against the property value, or simply a string, number, or boolean (true | false).
Missing/unavailable content does not match any condition. Omit match if you need to map an HTTP function or filter to a URL that does not correspond to any content. |
Service mappings
Service-based mappings bind service requests directly to HTTP functions or filters.
Supported services:
-
component -
Override the direct component rendering at
/_/component/<component-path>. -
image(deprecated) -
Override the legacy image service at
/_/image/<id>[:<fingerprint>]/<scale>/<name>(e.g./_/image/123456/scale-100-100/photo.jpg). Replaced by themedia:imageUniversal API. -
attachment(deprecated) -
Override the legacy attachment service at
/_/attachment/<mode>/<id>[:<fingerprint>]/<name>(e.g./_/attachment/inline/123456/logo.png). Replaced by themedia:attachmentUniversal API.
Universal APIs mounted as points (/_/<app>:<api>) cannot be remapped through site mappings — they are routed before the site service processes mappings. If you need custom behavior on a service-point URL, implement a custom Universal API instead. |
Re-routing to another content
A filter reached through a mapping can change which content is rendered while the visitor’s URL stays the same. Set contentPath on the request before handing it on, and the site service resolves the request against that content instead:
export function filter(req, next) {
if (req.cookies.experiment === 'b') {
// Visitors in the experiment get /mysite/campaign/variant-b,
// still under the /mysite/campaign address they asked for
req.contentPath = `${req.contentPath}/variant-b`;
}
return next(req);
}
This is a rendering re-route, not a redirect: no Location header, no second request, and path, url and the client’s address bar are untouched. Downstream, the request describes the new content. getContent() returns it, and its page or page template is resolved and rendered as though it had been requested directly. Only the target moves, so a re-route cannot leave the current project or branch: the new path is looked up in the same repository and branch, and the site is re-derived from it.
Rules:
-
Only documented fields are read from the object you pass to
next(). Mutatereqand hand it on. A fresh object works as long as it carriescontentPath, but anything else you put on it is lost. -
The re-routed content’s permissions apply. The lookup itself is privileged, so a filter can target content the visitor cannot read, but the response then fails with
403, as it would on a direct request. A path that resolves to nothing yields404, naming the path the filter set. -
Mappings are already chosen. Which mappings match is decided before any filter runs, from the originally requested content. A re-route does not re-run that matching, so it neither picks up mappings that would match the new content nor cancels the ones already selected. The remaining filters and any controller mapping in the chain receive the re-routed request.
-
Each filter may re-route once, starting from whatever the previous filter passed it. Inside the filter that re-routes, both before and after
next(),getContent()still reports the original content. The change applies to the request handed onwards. -
Generated URLs follow the re-route.
pageUrl()withoutidorpath, relativepathvalues and component URLs resolve against the re-routed content, not the address the visitor asked for. A variant rendered at/mysite/campaignwould link to itself as/mysite/campaign/variant-b. Pass the canonical target explicitly; the filter can hand it down withsetCustomLocalAttribute()before it re-routes.
// the filter, before re-routing
import {setCustomLocalAttribute} from '/lib/xp/context';
setCustomLocalAttribute('com.example.myapp.canonicalPath', req.contentPath);
req.contentPath = `${req.contentPath}/variant-b`;
// the page, when linking to itself
import {get as getContext} from '/lib/xp/context';
import {pageUrl} from '/lib/xp/portal';
const canonical = getContext().attributes['custom.com.example.myapp.canonicalPath'] as string | undefined;
const selfUrl = pageUrl({path: canonical});
Re-routing only maps an incoming address to content. It does not change how URLs are generated, and Content Studio’s built-in preview still renders a content at its real path. A URL scheme of your own therefore also needs a URL function of your own in place of pageUrl(), a decision about whether the real path stays reachable, and, for editors to preview the scheme, a Live view extension that points the preview at your address.
| Build the new path from values you control rather than interpolating a query parameter straight into it. A content path is a lookup key, and path segments have their own validation rules. |