Skip to Content
DocumentationFeaturesPlatformContent-types package

Content-types package

What it is

The bridge between the mesh and whatever consumes it. gtmesh types emits a self-contained TypeScript workspace package (default packages/content-types/) that your static-site generator — or any TypeScript consumer — compiles against. It is the content-types package.

Why it matters

The mesh’s contract is its page schemas and its committed graph. Hand-writing types to match them drifts the moment a schema changes. Generating them keeps the consumer honest: the SSG imports the same shapes the engine guarantees, and gtmesh types --check fails CI the instant the committed package and the schemas disagree.

How it works

gtmesh types reads schemas/ and the page types declared in config.page_types, then writes a buildable package containing:

  • per-type page interfaces — one TypeScript interface per page type, derived from its schema;
  • a PageType union — the closed set of page types, derived from config.page_types;
  • the graph-native readerloadGraph() (the indexed GraphView over graph/{nodes.jsonl,edges.tsv}, with derived roleOf/byRole/entityOf/edgesOf/membersOf accessors — edgesOf(id, type?) is a node’s whole relationship set, both layers, mirroring Page.edges in the engine) and loadManifest() (the projected site.manifest.json link tree). This is the reader — the old row-shaped loadRegistry()/RegistryEntry reader is removed (§6.2); see the crosswalk if your SSG still uses it;
  • page-bundle loaders — to load a page’s content/<id>/index.yaml bundle;
  • a redirect reader (loadRedirects()) — reads the url/ALIAS_OF graph layer, returns { from, to } to emit 301s. See Redirects;
  • an entities/facet reader + mesh-feed helpersloadEntities / entitiesByFacet, and breadcrumbs / hubMembers / relatedLinks + the pageSummaries card index, so navigation is a call, not a re-implemented lib/;
  • named projectionsgetProjection(name) returns a typed, cached list of schema-shaped cards (label, thumbnail, tags) from a config mapping;
  • co-located asset helpersassetUrl resolves a bare asset filename to a served URL; copyMeshAssets mirrors the files to your public dir;
  • config-derived constants (site.ts) — the section list, the per-environment render set, the site base URL, the facet axes, the thumbnail projection, and the asset base, so the SSG reads engine-owned policy instead of re-declaring it.

The package is self-contained, so the consumer depends on it like any workspace package — no reaching into the mesh internals.

Wire gtmesh types --check into CI. It writes nothing and exits non-zero on drift, so the consumer’s types can never fall out of sync with the content contract.

Library API

Everything the package exports, grouped by what it reads. For how these compose into a site build, see Build a website.

Pages — load a page’s content bundle

FunctionWhat it does
loadPage<T>(slug, opts?)Load + parse one bundle content/<slug>/index.yaml, typed as T (pass a generated <Type>Page). null if absent.
loadPagesByType<T>(type, opts?)Every page of a page_type, graph-driven — { slug, node, page }[] (each node is a GraphNode; its derived views come from the GraphView accessors).
loadPagesBySection<T>(section, opts?)Every member page in a section, same shape. Excludes the section’s own root (the index/home structural pages) so an index never self-lists — pass { excludeIndex: false } to include the landing.
pageSlugs(opts?)The slugs for a type/section/status — no bodies read. For generateStaticParams, sitemaps.
authorOf<T>(page, opts?)The author bundle a page references via meta.author (#179 E2) — content/people/<slug>/, typed via PersonPage. Null if unset.
loadOrganization<T>(opts?)The organization identity (foundation/organization.yaml), typed via Organization. Null if absent.
loadPageBrief(slug, opts?)The engine-owned assignment brief (apply’s _brief block — the writer’s instructions) as a typed camelCase RegistryBrief. Excluded from the generated <Type>Page types by design (it isn’t render content); this is the explicit accessor for tooling/debug UIs. Null when there’s no bundle or no _brief.

All accept a PageQuery: { status?, graph?, root?, path? }. Pass status: renderableStatuses(env) to list only the pages an environment renders, and graph to reuse an already-loaded GraphView instead of re-reading it.

Feeds — navigation as cards

Traversal over the mesh link tree, returning ready-to-render PageSummary[]. Page identity + status come from the GraphView (pass the loadGraph() handle as the first argument); the up-link/sibling arrays are a site.manifest.json read. Each accepts opts.status (prune to the render set) and opts.thumbnails (fill card images). See Navigation & relationships.

FunctionWhat it does
breadcrumbs(g, slug, opts?)Root → page trail (primary up-link chain), each hop a card.
hubMembers(g, hubSlug, opts?)The pages that link up to a hub (the derived down edge), sorted.
relatedLinks(g, slug, opts?){ siblings, across } — peer links as cards.

Projections — schema-shaped cards (projections.ts)

Named projections: a page → a shaped list-item whose shape is a schema (card.schema.yaml → the Card type).

ExportWhat it is
getProjection(name)Returns a runnable (opts?) => Card[] that projects the selected pages, memoised after first run. Typed per projection name.
PROJECTIONSThe baked projection specs (name → schema + select + field mapping).
Card (+ CardThumbnail, CardTag)The generated result type for the card schema.
getProjection("productCard")({ status: renderableStatuses(env) }) // → Card[] .map(c => ({ href: c.slug, label: c.label, img: c.thumbnail, tags: c.tags }));

Summaries — the card index (summary.ts)

The build-once slug → PageSummary projection: a graph-node + bundle-meta join (label, description, optional thumbnail), scanned once and memoised on the GraphView, so labelling related slugs is O(1) instead of re-parsing bundles. Pass the loadGraph() handle as the first argument.

FunctionWhat it does
pageSummaries(g, opts?)The full slug → PageSummary index (restrict with opts.status).
pageSummary(g, slug, opts?)One page’s card (reads just that bundle, cached).
labelForSlug(g, slug, opts?)The display name — navTitle ?? title ?? humanised slug.

PageSummary = { slug, type, section, entity, title?, navTitle?, description?, label, thumbnail? }. Pass { thumbnails: THUMBNAILS } to populate thumbnail ({ asset, alt?, caption?, ratio? }); asset is the bare co-located filename (resolve content/<slug>/<asset>).

Graph — the graph-native accessor (graph.ts)

loadGraph(opts?) returns an indexed, read-only GraphView over the committed graph (graph/nodes.jsonl + graph/edges.tsv) — parsed and indexed once per build (no per-call re-parse), source-hidden (you never pass arrays around). It’s a generic accessor: it exposes the raw graph — nodes by id/property + the typed edges — and your SSG builds its own domain lists from the edge relationships it knows. The lib re-encodes no config vocabulary (no cluster/facet/topic groupers), so nothing in it can drift from your gtmesh.config.yaml.

MethodWhat it does
getById(id) / node(id)One node by its stable id (the frozen-first-slug handle — the same value written into each page’s _brief.id).
findByProperty(prop, value)Every node whose top-level prop equals value (indexed per prop) — e.g. findByProperty("status", "published").
bySlug(slug)The page node serving slug (indexed).
byLabel(label)Enumerate every node carrying labelbyLabel("product") (all products), byLabel("manufacturer") (pages on the manufacturer axis), byLabel("topic"), byLabel("url"), byLabel("page"). This is how you build index / listing pages (§A.3 “all manufacturer pages = a label query”). A hub page carries its axis label (its page_type minus -hub), so “all category pages” is byLabel("category").
nodes() / edges()The full node / edge set (a fresh, stable-ordered copy) — for a sitemap or an all-pages sweep.
out(id, type?) / in(id, type?)The typed edges from/to id (optionally one edge type) — the raw relations, exposed directly.
outNodes(id, type?) / inNodes(id, type?)The resolved endpoint nodes of those edges — this is how you build a domain list: outNodes(id, "HAS_APPLICATION") for a product’s applications, outNodes(id, "HAS_COLLECTION") for its collections, inNodes(hubId, "HAS_CATEGORY") for a hub’s members.

Derived accessors (sugar). The mesh’s authority derivations, computed once per view and reused — so you never hand-roll the apex-edge role scan or the pluralised-hub entity trap. Each wraps the same canonical engine derivation, so roles/entities stay in one place.

MethodWhat it does
roleOf(id)The page’s DERIVED role — pillar (PILLAR_OF), hub (HUB_OF), sub-hub (a HUB_OF also MEMBER_OF a topic it doesn’t own), spoke (no apex), or a structural kind (home/index). A view of the apex edges, never a stored column.
byRole(role)Every page node whose roleOf equals role, id-ordered (indexed) — the role filter (byRole("hub")) without a per-call edge scan.
entityOf(id)The page’s DERIVED entity slug — the apex value for an owner (so a pluralised /categories/rotary-lobe-pumpsrotary-lobe-pump), else the id minus its section prefix.
edgesOf(id, type?)The node’s out-edges as { type, to }, sorted + deduped — both layers in one accessor. Filter to read one: edgesOf(id, "HAS_INDUSTRY") for a domain axis (every value on it), edgesOf(id, "MEMBER_OF") for topical membership. Targets are node ids — the stable token, and what you link with. Mirrors Page.edges in the engine.
membersOf(id)The members of a topic — pass a topic id (topics/<value>) or a hub/pillar page id (resolved via HUB_OF/PILLAR_OF), owner excluded.
loadRedirects(opts?)Standalone (not a GraphView method): the redirect list (old slug → current) from the url/ALIAS_OF graph layer, for emitting 301s. Auto-detects the store; not deprecated.

Coming from clustersOf / topicsOf? Those per-layer accessors are goneedgesOf replaces both. clustersOf typed a page’s memberships as axis → ONE value, so a page on four industries reported one and the rest were dropped; topicsOf returned bare values rather than node ids. Rewrite each call, then run gtmesh types:

g.clustersOf(id).industry → g.edgesOf(id, "HAS_INDUSTRY").map(e => e.to) // every value, as node ids g.topicsOf(id) → g.edgesOf(id, "MEMBER_OF").map(e => e.to) // the same memberships, as node ids

A target id is the stable token: link with it, or resolve it to a node with getById(e.to) (its display name is on the node) — or to the value it stands for with entityOf(e.to), which unwraps a pluralised hub (categories/rotary-lobe-pumpsrotary-lobe-pump).

membershipsFromGraph is gone too, for the same reason: it pre-bucketed memberships as axis → one value, so a many-valued axis silently lost everything after the first. Group edgesOf yourself if you want a map, and each axis keeps its real cardinality. topicAxesFromGraph retires with it — once nothing in the package consumed a topic’s axis, keeping a second copy of the engine’s derivation only invited drift. Need a topic’s axis? Read its apex’s labels off edgesOf(topic, "HUB_OF").

Edge-type knowledge (HAS_APPLICATION, HAS_COLLECTION, …) is domain-specific and lives in your SSG — which is exactly where it belongs. Each page bundle also carries its stable node id at _brief.id — use it as the lookup key into getById (it survives renames; a better cache key than the slug).

import { loadGraph, loadPageBrief } from "@acme/content-types"; const g = loadGraph(); // a listing / index page — enumerate by label const allProducts = g.byLabel("product"); // every product page // a detail page — build domain lists by the edge types you know const id = loadPageBrief("/products/alfa-laval-sru")!.id; // the stable handle const applications = g.outNodes(id, "HAS_APPLICATION"); // build your own list, by edge type const collections = g.outNodes(id, "HAS_COLLECTION"); // …and another — no entities.csv, no array passing

Migrating off the row reader (loadRegistryloadGraph)

The row-reconstruction reader (RegistryEntry / loadRegistry* and the slug-keyed helpers) is removed in the graph cleanup (§6.2) — running gtmesh types regenerates the package without it. If your SSG still calls it, move each call to its loadGraph() equivalent. The graph is loaded once and indexed, so these are lookups, not array scans:

Removed row readerGraph-native replacementBefore → after
loadRegistry() (enumerate pages)loadGraph().byLabel("page") / .nodes()loadRegistry({ root })loadGraph({ root }).byLabel("page")
a status filter.byLabel("page") + a node.props.status filterloadRegistry().filter(r => r.status === "published")g.byLabel("page").filter(n => n.props.status === "published")
the role filter (r.role === "hub")byRole(role) — role is derived from the apex edges, not a stored columnloadRegistry().filter(r => r.role === "hub")g.byRole("hub")
entryBySlug(entries, slug)bySlug(slug)entryBySlug(loadRegistry(), slug)g.bySlug(slug)
childrenBySlug(entries) (the down edge)hubMembers(g, hubSlug), or membersOf for topical memberschildrenBySlug(entries).get(hubSlug)hubMembers(g, hubSlug)
pagesByTopic(entries) / allTopics(entries)membersOf(topicId) / byLabel("topic")pagesByTopic(entries).get("rotary-lobe-pump")g.membersOf("topics/rotary-lobe-pump")
breadcrumbs / hubMembers / relatedLinks (feeds)same functions, now (g, …) — pass the GraphView instead of the entries arrayhubMembers(entries, hubSlug)hubMembers(g, hubSlug)
pageSummary / pageSummaries / labelForSlug (cards)same functions, now (g, …)pageSummary(entries, slug)pageSummary(g, slug)

The RegistryEntry fields that were derived at reconstruction move to the same edge reads — read them off the graph rather than a row:

RegistryEntry fieldGraph-native read
.clusters (axis → value)edgesOf(id, "HAS_<AXIS>") — the page’s relationships on that axis, every value, as target node ids. One axis per call, so a many-valued axis reads the same way a single-valued one does.
.topics (MEMBER_OF targets)edgesOf(id, "MEMBER_OF") — the same memberships, as topics/<value> node ids
.entityentityOf(id) — the apex value for an owner (a pluralised hub slug still resolves to the singular entity), else the node id minus its section prefix
.roleroleOf(id) — derived from the apex edge (HUB_OF ⇒ hub, PILLAR_OF ⇒ pillar, a hub also MEMBER_OF a topic it doesn’t own ⇒ sub-hub, else spoke) — not a stored prop

Redirects need no change: loadRedirects() returns the same { from, to } list from the url/ALIAS_OF graph layer. It is not deprecated.

Entities — the entity table & facets (entities.ts) — deprecated

Deprecated (graph-native migration). These slug-keyed table readers reconstruct a CSV-shaped array and ask you to scan it (entityBySlug(entries, …)). They still work and are not removed in this release — but new code should read the graph directly via loadGraph. Migration path (every capability has a concrete port):

Old (table)New (loadGraph())
loadEntities() / loadEntitiesFromGraph() (enumerate)byLabel("<label>") (e.g. byLabel("product")) or nodes(); read fields off node.props
entityBySlug(entities, slug)bySlug(slug) / getById(id)
entityFacets(entity, axes)per-axis outNodes(id, "HAS_<AXIS>") (e.g. HAS_INDUSTRY)
entitiesByFacet(entities, axis, value)inNodes("<axis>/<value>", "HAS_<AXIS>") (or the owner hub id)
Entity typeGraphNode (fields on node.props)
facetValues(cell)obsolete — edges are discrete, nothing to split

The helpers will be removed in a future release once meshes have migrated.

These deprecated readers reconstruct a slug-keyed table of entity relationships and their config-declared facet columns (taxonomy.facets); the same data now lives on entity nodes in the committed graph, read via loadGraph() above.

FunctionWhat it does
loadEntities(opts?)Read the committed graph into typed Entity[] — reconstructed from folded page nodes + HAS_<AXIS> facet edges, memoised per graph mtime (a thin alias for loadEntitiesFromGraph). There is no entities.csv.
entityBySlug(entities, key, keyColumn?)One entity by key (pass ENTITY_KEY if the key column isn’t slug).
entitiesByFacet(entities, axis, value)Facet-hub membership — every entity whose axis column contains value.
entityFacets(entity, facetAxes)An entity’s facet memberships (pass FACET_AXES).

Assets — co-located images & documents (assets.ts + site.ts)

Resolve a bare asset filename to a URL, and ship the files. See Assets.

ExportWhat it is
assetUrl(slug, asset, base?)The served URL for a bare asset — ${base}${slug}/${asset} (base defaults to ASSET_BASE). Used for card thumbnails and body images alike.
ASSET_BASEThe URL prefix assets serve under, from site.assets.base (default /content).
pageAssets(slug, opts?)One page’s co-located assets — { slug, name, path }[].
meshAssets(opts?)Every renderable page’s assets (status-filterable) — the copy inventory.
copyMeshAssets(dest, opts?)Mirror content/<slug>/<asset>dest/<slug>/<asset>; returns the manifest.

Site — config-derived constants (site.ts)

Projected from gtmesh.config.yaml by gtmesh types, so the SSG never re-declares engine policy.

ExportWhat it is
SECTIONSThe top-level URL sections, from taxonomy.sections (a readonly tuple).
sectionLabel(section)Humanise a section slug for nav (best-forBest For).
RENDER_ENVSThe full env → renderable statuses map, from config.environments.
renderableStatuses(env)The statuses visible in env — the same policy render-manifest applies. Throws on an unconfigured env.
siteBaseUrl(env?)The canonical origin for env (its base_url override, else the committed site.base_url; null if none).
SITE_BASE_URL / SITE_BASE_URL_BY_ENVThe committed default origin and the per-env overrides, if you need them directly.
ENTITY_KEY / CLUSTER_AXES / FACET_AXESThe entities key column + cluster/facet axis names, from taxonomy — pass FACET_AXES to entityFacets.
THUMBNAILSThe card-thumbnail projection config (paths + register ratios), from site.thumbnail + images — pass to the summary builders.

Types

Generated per-schema interfaces (GuidePage, ProductPage, …), the PageType union, GraphNode / GraphEdge / GraphView, Entity, RegistryBrief, Redirect, Status, LoadOptions, PageBundle, Section, and RenderEnv — all re-exported from the package entry. (RegistryEntry / RegistryRow are removed — §6.2.)

Key files & flags

WhereWhat it controls
gtmesh typesGenerate the package
gtmesh types --checkVerify the committed package matches the schemas (CI gate)
gtmesh types --dir <path>Override the package directory
schemas/ + config.page_typesThe source of the generated interfaces and union
  • types — the command in full
  • Lifecycle — how pages reach the committed graph the package reads
Last updated on