Projections (cards & lists)
Almost every page in a mesh-backed site also appears as a list item somewhere else — a related tile, a hub member, a search hit, a nav card. A projection defines how a page becomes that item, once, in config — so the SSG doesn’t re-derive labels, thumbnails, and tags by hand at every call site.
The two parts
A projection splits cleanly into a shape and a mapping:
- The result shape is a schema —
schemas/card.schema.yaml(shipped). It’s the same schema system as page types, sogtmesh typesgenerates aCardTypeScript type from it (and it can be validated). One genericcardschema serves most list items. - A projection is a named config that binds that schema + which pages + a
field mapping. Many projections reuse the one
cardschema; they differ only by filter and mapping.
# gtmesh.config.yaml — projections are website config, so they nest under `site`
site:
projections:
- name: defaultCard # the generic fallback — any page
schema: card
map:
label: [meta.navTitle, meta.title] # first non-empty → else humanised slug
description: meta.metaDescription
thumbnail: content.thumbnail
- name: productCard # a product list-item
schema: card
select: { type: product } # which pages this runs over
map:
label: [meta.navTitle, meta.title]
description: meta.metaDescription
thumbnail: content.hero # products keep the hero elsewhere
tags: [manufacturer, category, industry]Per-page-type overrides are just another projection — productCard is the
product override, with its own thumbnail path and tags. There’s no special
override syntax; you name a projection and give it a select.
How map is read
Each map key is a field on the result schema, and the engine interprets the
value by that field’s type — so the schema is the single source of truth for
the shape, and the mapping stays terse:
| Field type (in the schema) | Mapping value | Resolution |
|---|---|---|
string (label, description) | a bundle path, or a list | first non-empty path; label falls back to the humanised slug |
image (thumbnail) | a bundle path | normalize to { asset, alt?, caption?, ratio? } |
Tag[] (tags) | a list of axis names | resolve each axis to tag(s) |
slug, type, section, and entity are identity — the engine fills them
from the page node automatically; you never map them.
Tags: one per value, on any axis
Tag axes are mesh-native — you just name the axis, and the engine reads the
page’s own relationships on it (its HAS_<AXIS> edges), emitting one tag per
value, each linked to that value’s hub page.
Cardinality belongs to the axis, not to a second kind of mapping: a cluster
axis (e.g. manufacturer, category) carries one value per page in practice, so
it yields one tag; a facet axis (e.g. industry, application) carries several,
so it yields several. Declaring an axis under taxonomy.axes or
taxonomy.facets is what makes it nameable here (an axis in neither is rejected
when the projection is baked) — it never trims what the page actually carries.
tags: [manufacturer, category, industry] # two single-valued axes + a many-valued oneresolves — flattened, in order — to:
tags: [
{ name: "Acme Pumps", link: "/manufacturers/acme", axis: "manufacturer" },
{ name: "Rotary Lobe Pumps", link: "/categories/rotary-lobe-pumps", axis: "category" },
{ name: "Dewatering", link: "/industries/dewatering", axis: "industry" },
{ name: "HVAC", link: "/industries/hvac", axis: "industry" },
]Each tag carries the axis it came from, so the SSG can group or style them
(“Manufacturer: … / Industries: … …”). A value with no hub page becomes a
name-only tag (no link). For a free-text tag a page carries itself, map
{ field: <bundle-path> } instead of an axis name.
Running a projection
gtmesh types exposes getProjection(name), which returns a runnable that
projects the selected pages and caches after the first run:
import { getProjection, type Card } from "@your-project/content-types";
const products: Card[] = getProjection("productCard")();
products.map((c) => ({ href: c.slug, label: c.label, img: c.thumbnail, tags: c.tags }));It accepts the usual options — { status: renderableStatuses(env) } to restrict
to the render set, { root } to point at the mesh:
getProjection("productCard")({ status: renderableStatuses(env) });A Card’s thumbnail.asset is the bare co-located filename — resolve its URL with
assetUrl(card.slug, card.thumbnail.asset) (the card
carries slug). ratio is render-ready:
image.ratio ?? images.registers[register].ratio ?? images.default_ratio.
Related
- Navigation & relationships — the feeds that return these cards
- Page types & schemas — the schema system projections reuse
- Content-types package —
getProjectionin the API reference