Skip to Content
DocumentationBuild a websiteProjections (cards & lists)

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 schemaschemas/card.schema.yaml (shipped). It’s the same schema system as page types, so gtmesh types generates a Card TypeScript type from it (and it can be validated). One generic card schema serves most list items.
  • A projection is a named config that binds that schema + which pages + a field mapping. Many projections reuse the one card schema; 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 projectionproductCard 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 valueResolution
string (label, description)a bundle path, or a listfirst non-empty path; label falls back to the humanised slug
image (thumbnail)a bundle pathnormalize to { asset, alt?, caption?, ratio? }
Tag[] (tags)a list of axis namesresolve 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 one

resolves — 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.

Last updated on