Config from manifest
Derive the full config from a compact manifest and a typed JSON payload
What it is
configFromManifest is a server-only runtime helper that derives the full Config object — schema, navigation, pages, blocks, radar and settings — from two inputs:
- A compact manifest (one entity, one cluster, N tags, ≥1 metrics, plus project labels and locales).
- A data object that follows a fixed naming convention.
It is an opt-in alternative to writing the seven config files (schema.ts, settings.ts, navigation.ts, pages.ts, blocks.ts, radar.ts, extras.ts) by hand. The multi-file approach still works exactly as before — configFromManifest simply gives you a shortcut for projects whose shape matches the standard blueprint.
import { configFromManifest, type Manifest } from "@envisioning/app/server";
import exampleData from "../data/example-data.json";
export const manifest: Manifest = {
project: { mainLabel: "Cities", extraLabel: "ENVISIONING", locales: ["en", "pt"] },
entity: { label: { en: "Technology" } },
cluster: { key: "trend", label: { en: "Trend" } },
tags: [{ key: "industry", label: { en: "Industry" } }],
metrics: [{ key: "trl", label: { en: "TRL" } }],
};
const result = configFromManifest(manifest, exampleData.data);
if (!result.success) throw new Error(result.error ?? "invalid data");
export const config = result.config;When to use it
Use configFromManifest when all of the following hold:
- Your project has exactly one entity type, exactly one cluster type and at least one metric.
- You don't need
scopetypes,selfmetrics, or sub-metric groupings (metricResultsranges on metrics). - The default
_dev-style layout (entities list with list/cards toggle, entity detail with spider chart + per-metric charts + tag/cluster sections + sources, cluster/tag/metrics pages, conditional home/about) is what you want. - You're OK with the data shape conventions described below.
If any of those don't hold, drop configFromManifest and write the multi-file configuration directly. The two paths coexist; no migration is required either way.
Data shape conventions
configFromManifest validates the data structurally against the manifest. The shape is non-negotiable — names must match exactly.
Top-level keys:
| Key | Required | Description |
|---|---|---|
allEntities | yes | Array of entity records. |
allClusters | yes | Array of cluster records. |
allTags_<key> | per tag | One per declared tag in the manifest. e.g. allTags_industry. |
allMetrics_<key> | per metric | One per declared metric. e.g. allMetrics_trl. |
project | no | Optional project-level fields (see below). |
Entity record:
{
"id": "solar-microgrids",
"title": "Solar Microgrids",
"summary": "Optional one-line summary.",
"description": "Optional long-form description (markdown).",
"image": { "url": "https://example.com/cover.jpg" },
"sources": [{ "title": "...", "summary": "...", "url": "..." }],
"cluster": { "id": "energy" },
"tag_industry": [{ "id": "energy" }],
"metric_trl": { "value": 8 }
}idandtitleare required.cluster: { id }is always namedcluster, regardless of the cluster's manifestkey. Thekeyonly affects derived schema and URL slugs.- Each declared tag has a field
tag_<key>containing an array of{ id }objects. Optional — if omitted on an entity, it defaults to[]. - Each declared metric has a field
metric_<key>containing an object with avaluenumber. imageis optional. When present it is an object of shape{ url }; templates read it as{image.url}.
Cluster / tag record:
{ "id": "energy", "label": "Energy", "color": { "hex": "#FFA500" }, "summary": "...", "description": "..." }Metric record (in allMetrics_<key>):
{ "value": 8, "label": "System qualified", "summary": "..." }value can also be a [min, max] tuple to define range buckets.
Optional project block:
| Field | Effect |
|---|---|
project.home | If present, generates a Home page that renders this markdown. Adds a Home menu item. |
project.about | If present, generates an About page that renders this markdown. Adds an About menu item. |
project.coverImage | Object { url } used as the header image for Home and About. If absent, a random cover from allEntities is used. |
Referential integrity (an entity's tag_industry[].id actually existing in allTags_industry) is not checked — that's downstream's job.
Manifest reference
import { configFromManifest, type Manifest, zManifest } from "@envisioning/app/server";zManifest is the Zod schema; every field is annotated with .meta({ description }) so documentation can be generated from it. Use zManifest.parse(myManifest) to double-validate manifests in external repos.
project
project: {
mainLabel: "Cities", // shown in the app header
extraLabel: "ENVISIONING", // shown above the main label
locales: ["en", "pt"], // first locale is the default
seo: { // optional
title: "Cities", // defaults to mainLabel
description: "...", // defaults to "Envisioning"
image: "https://...", // optional preview image
},
}entity (singleton, required)
entity: {
label: { en: "Technology", pt: "Tecnologia" },
singular: { en: "Technology" }, // optional, defaults to label
plural: { en: "Technologies" }, // optional, defaults to label
icon: "circleSmall", // optional, default "circleSmall"
aiTemplate: "{summary}\n...", // optional; auto-generated from metrics if omitted
}The entity's schema key is fixed as entities. Page IDs are entities (list) and entity (detail); URLs are /entities and /entity/[id].
cluster (singleton, required)
cluster: {
key: "trend", // optional, default "cluster"
label: { en: "Trends" },
singular: { en: "Trend" },
plural: { en: "Trends" },
icon: "box", // optional, default "box"
}key drives block IDs (trendsList, trendListItem, divisionTrend, ...) and page IDs (trends, trend). The data field on entities is always cluster.id regardless of key.
tags (0..N)
tags: [
{ key: "industry", label: { en: "Industry" } },
{ key: "stage", label: { en: "Adoption Stage" } },
]Each tag declares a key, which drives:
- the entity field
tag_<key>and the top-level listallTags_<key>; - block IDs (
<key>sList,<key>ListItem,context<Key>List,<key>ListItemSm); - the list page
<key>s, the detail page<key>; - a SELECTION filter in the toolbar.
icon is optional. If omitted, an icon is assigned from the cycle:
triangle, diamond, pentagon, hexagon, octagon, square, circle, tag, tag, ...Position-based: first tag gets triangle, second diamond, and so on. From the eighth tag onward all remaining tags receive tag.
metrics (≥1, required)
metrics: [
{
key: "trl",
label: { en: "Technology Readiness" },
description: { en: "..." }, // optional
domain: [1, 9], // optional — see below
origin: 1, // optional, defaults to domain[0]
helper: { /* MetricHelper settings */ }, // optional, pass-through
},
]domain is optional. If omitted, the function derives [min, max] from allMetrics_<key>.value (numbers or [min, max] range tuples). An empty allMetrics_<key> with no explicit domain throws with a clear error.
The first declared metric drives the radar's distance axis. The second populates bubble. The third populates bar. See Radar below.
What gets generated
Schema (schema.objects)
entities(entity type) with explicit joins to the cluster, every tag and every metric.<clusterKey>(cluster type).- One object per declared tag and per declared metric.
All joins are explicit — auto-join is not relied on, because the data field names (tag_<key>, metric_<key>) don't match the schema keys.
Navigation
Menu — in order:
[ home? , entities , <clusterPlural> , ...<tagPlural> , metrics , about? ]Home and About appear only if project.home / project.about are present in the data.
Pages:
home(conditional),about(conditional),metricsentities(list withlist/cardsmodes) +entity(detail)<clusterPlural>(list) +<clusterKey>(detail)- For each tag:
<key>s(list) +<key>(detail)
Blocks
A curated, structural subset of the _dev blueprint:
- Shared:
groupTitle,dash,hr,spacerXs/Sm/Md/Lg,divisionRelatedEntities,divisionSources,panelSearch. - Project media (conditional):
coverImage/randomCoverImage,homeIntro,aboutText. - Entity:
entitiesList,entityListItemLg,entitiesListToolbar,entitiesCards,entityCardItem,entityPreviewItem,relatedEntitiesList,entityListItemMd,entityImage,entityTitle,entitySummary,entityDescription,entitySources,entitiesSpiderChart,entitiesPagination. - Cluster:
<clusterPlural>List,<clusterKey>ListItem,<clusterKey>Mosaic,entity<ClusterKey>,<clusterPlural>Pagination,division<ClusterKey>. - Per tag:
<key>sList,<key>ListItem,context<Key>List,<key>ListItemSm,<key>sPagination,division<Key>. - Per metric:
<key>Chart(full chart on entity detail),<key>ChartMinimal(card-sized snippet),<key>Content(MetricHelperBlock on the metrics page).
Radar
- distance:
[ firstMetric ]. - sort:
[ Title, ...all metrics ]. - group:
[ cluster, ...all metrics, Favorites, None ]. - bubble:
[ metric[1], metric[2], ... ]—preferences.showBubble.defaultisfalseif no second metric exists. - bar:
[ metric[2], metric[3], ... ]—preferences.showBar.defaultisfalseif no third metric exists.
Settings
projectMainLabel/projectExtraLabel/locales/seofrom the manifest.- One RANGE filter ("Metrics") with one slider per declared metric.
- One SELECTION filter per declared tag.
headerSettings: a Language switcher and a Fullscreen button. Themes are not exposed.
What it does not do
- No
scopetypes. - No
self-source metrics (i.e., per-metricmetricResultsvalue buckets). - No sub-metric groupings (the
trl_ticks/trlGrouppattern in_dev). - No assistant configuration in the manifest.
- No theme selection.
- No custom blocks or pages.
If you need any of these, fall back to the multi-file configuration.
Project layout
A project using configFromManifest has a much smaller config/ folder:
config/index.ts declares the manifest and exports the built config:
import { configFromManifest, type Manifest } from "@envisioning/app/server";
import exampleData from "../data/example-data.json";
export const manifest: Manifest = {
project: { mainLabel: "Template", extraLabel: "ENVISIONING", locales: ["en", "pt"] },
entity: { label: { en: "Technology" }, plural: { en: "Technologies" } },
cluster: {
key: "category",
label: { en: "Category" },
plural: { en: "Categories" },
},
tags: [
{ key: "industry", label: { en: "Industry" } },
{ key: "stage", label: { en: "Adoption Stage" } },
],
metrics: [
{ key: "readiness", label: { en: "Technology Readiness" } },
{ key: "impact", label: { en: "Impact" } },
{ key: "complexity",label: { en: "Complexity" } },
],
};
const result = configFromManifest(manifest, exampleData.data);
if (!result.success) throw new Error(result.error ?? "invalid data");
export const config = result.config;layout.tsx, sitemap.ts and [[...seo]]/page.tsx import config from ./config exactly as before — the only change in the project is the destructure-and-bail at the call site.
See _projects/_template/ for a working example.
Validation behavior
configFromManifest returns a discriminated result:
type Result =
| { success: true; config: ConfigNotYetValidated; error?: never }
| { success: false; error: string | null | undefined; config?: never };Two layers of validation:
- Manifest:
zManifest.safeParse(manifest)— an invalid manifest throws with a prettified error. Manifest mistakes are caller bugs, not runtime data issues, so they short-circuit the call. - Data shape: a Zod schema derived from the manifest at call time validates the data structurally. Missing
allTags_<key>, missingmetric_<key>on an entity, wrongclustershape, etc. produce{ success: false, error }with the field path inerror. Handle the failure at the call site — typically by throwing during config bootstrap or returning a 500 from the data route.
One residual throw: a metric declared without an explicit domain whose allMetrics_<key> is empty cannot derive a domain, so configFromManifest throws with a clear message.
Errors are prefixed with configFromManifest: so they stand out in logs.
Exports
| Export | Description |
|---|---|
configFromManifest(manifest, data) | The function. Server-only. |
zManifest | Zod schema for the manifest. Use for runtime validation in external repos. |
Manifest | TypeScript input type for the manifest. |