Frontend Map
The Frontend Map is the single source of truth for how each site’s content is structured into URLs. It declares, per site, the rule that maps a content type to a frontend pathname.
It lives in the shared workspace package @nelioscom/site-structure (source: src/packages/site-structure/src/index.ts) and exports the frontendMap data together with the builder/matcher helpers that consume it.
Where it is used
The same data and builders are consumed in several places, so URL logic never drifts between them:
| Consumer | What it uses it for |
|---|---|
config/admin.ts | The Strapi Preview button (buildPreviewPath) |
src/api/frontendmap/... | Sitemap generation + inbound path matching |
config/plugins.ts | nelios-popup plugin config (frontendMap) |
Because it is a compiled workspace package, after editing the source you must rebuild it (
pnpm run build:sharedfrom the backend root, ornpm run buildinside the package) before consumers pick up the change.
How a rule works
Each content type has one rule string, and that single rule is used for both directions:
- Outbound — building a URL for a document (Preview, sitemap).
- Inbound — matching an incoming pathname back to a content type.
A rule is a /-separated template of literal segments and tokens, e.g.:
{locale}/{hotel}/accommodation/{?category}/{slug}Keep each rule in sync with the actual frontend route structure.
Token reference
Tokens appear inside a rule. Wrapping a token as {?token} makes it optional — the segment is dropped when its value is empty.
| Token | Expands to |
|---|---|
{locale} | The locale segment. Omitted when it equals the site defaultLocale. |
{slug} | The document’s Slug. |
{hotel} | prefixHotels + the document’s direct Hotel slug. |
{parents} | The recursive Parent-page slug chain (root → parent). |
{parents_h} | {parents} plus the Hotel of the root ancestor (hotelGroup sites). |
{category} | The document’s Category field, lowercased for the URL segment. |
{?token} | Optional variant of any token — dropped when the value is empty. |
Data shape
The map is an array of site configs, each holding an array of rules.
export type FrontendMapRule = {
collectionType: string; // content-type UID, e.g. plugin::uzenie.uzenie-page
rule: string; // URL template (see tokens above)
prefix?: string; // optional path prefix used for inbound matching
};
export type FrontendMapSite = {
site: string; // site key, e.g. "uzenie"
prefixHotels: string; // auto-prefix applied when {hotel} is used (e.g. "/hotels")
hotelGroup: boolean; // true for multi-hotel sites; enables {parents_h}
defaultLocale: string; // locale that is omitted from the URL (e.g. "en")
defaultCollectionType: string; // fallback content type for the site
rules: FrontendMapRule[];
};
export const frontendMap: FrontendMapSite[] = [ /* ...one entry per site... */ ];Site fields
| Field | Description |
|---|---|
site | Unique site key. |
prefixHotels | Segment(s) automatically inserted before a hotel slug whenever {hotel} (or {parents_h}) resolves. Empty string for single-property sites. |
hotelGroup | true for hotel-group sites. Enables {parents_h}, which appends the root ancestor’s hotel. |
defaultLocale | The locale that is not rendered as a URL segment. |
defaultCollectionType | Default content type for the site (used as a fallback). |
rules | The per-content-type URL rules. |
Example: site config
export const uzenie = {
site: 'uzenie',
defaultLocale: 'en',
prefixHotels: '',
hotelGroup: false,
defaultCollectionType: 'plugin::uzenie.uzenie-page',
rules: [
{ collectionType: 'plugin::uzenie.uzenie-homepage', rule: '{locale}' },
{ collectionType: 'plugin::uzenie.uzenie-page', rule: '{locale}/{?parents}/{slug}' },
{ collectionType: 'plugin::uzenie.uzenie-room', rule: '{locale}/accommodation/{slug}', prefix: 'accommodation' },
{ collectionType: 'plugin::uzenie.uzenie-offer', rule: '{locale}/offers/{slug}', prefix: 'offers' },
{ collectionType: 'plugin::uzenie.uzenie-restaurant', rule: '{locale}/gastronomy/{slug}', prefix: 'gastronomy' },
{ collectionType: 'plugin::uzenie.uzenie-experience', rule: '{locale}/experiences/{slug}', prefix: 'experiences' },
],
};A hotel-group site such as andronis (hotelGroup: true, prefixHotels: '/hotels') uses the hotel tokens:
export const andronisRules = [
{ collectionType: 'plugin::andronis.andronis-page', rule: '{locale}/{?hotel}/{?parents}/{slug}' },
{ collectionType: 'plugin::andronis.andronis-room', rule: '{locale}/{hotel}/accommodation/{?category}/{slug}', prefix: 'accommodation' },
];Example resolutions
| Site | Rule | Document | Result |
|---|---|---|---|
uzenie | {locale}/{?parents}/{slug} | slug: "wellness", locale en (default) | /wellness |
uzenie | {locale}/{?parents}/{slug} | slug: "wellness", locale el | /el/wellness |
uzenie | {locale}/offers/{slug} | slug: "early-bird", locale en | /offers/early-bird |
andronis | {locale}/{hotel}/accommodation/{?category}/{slug} | hotel arts, category Suites, slug caldera, locale en | /hotels/arts/accommodation/suites/caldera |
Exported helpers
Resolution
findSiteRule(uid)— returns the{ siteCfg, ruleEntry }that owns a content-type UID, ornull.buildPreviewPath({ strapi, uid, documentId, locale?, status? })— async; resolves a single document to a frontend pathname using the rules. Returnsnullwhen no rule exists for the UID (caller falls back to its own logic) or the document can’t be found.buildUrlForDoc({ doc, ruleEntry, siteCfg })— builds the pathname from an already-fetched document.buildFindManyQuery({ strapi, uid, rule, parentDepth? })— derives the Strapifields/populateneeded to satisfy a rule’s tokens (e.g. recursively populatingParentandHotel).frontendBaseUrl(uid)— resolves a site’s base URL from its UID, e.g.plugin::andronis.andronis-page→FRONT_END_URL_ANDRONIS.
Sitemap
buildSitemapXml({ urls, lastmodByUrl? })— renders a<urlset>sitemap document from a list of URLs (with optionallastmodper URL).
Path utilities
normalizePathname(p)— trims query/hash, collapses duplicate slashes, removes trailing slash.splitSegments(p)— normalizes and splits a pathname into decoded segments.extractRuleTokens(rule)— returns the unique token names inside a rule.isLocaleSeg(s)— tests whether a segment looks like a locale (en,el,en-US).joinPath(...parts)— joins parts into a normalized pathname.
Adding or changing a route
- Add or edit the relevant
FrontendMapRuleunder the correct site infrontendMap. - Make sure the
rulematches the actual frontend route shape for that content type. - Set
prefixwhen the route has a fixed leading segment used for inbound matching (e.g.offers,accommodation). - For hotel-group sites, use
{hotel}/{parents_h}and setprefixHotelsaccordingly. - Rebuild the package (
pnpm run build:shared) so all consumers pick up the change.