Envisioning LogoEnvisioning App (6.3.0)
Configuration

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:

  1. A compact manifest (one entity, one cluster, N tags, ≥1 metrics, plus project labels and locales).
  2. 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 scope types, self metrics, or sub-metric groupings (metricResults ranges 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:

KeyRequiredDescription
allEntitiesyesArray of entity records.
allClustersyesArray of cluster records.
allTags_<key>per tagOne per declared tag in the manifest. e.g. allTags_industry.
allMetrics_<key>per metricOne per declared metric. e.g. allMetrics_trl.
projectnoOptional 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 }
}
  • id and title are required.
  • cluster: { id } is always named cluster, regardless of the cluster's manifest key. The key only 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 a value number.
  • image is 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:

FieldEffect
project.homeIf present, generates a Home page that renders this markdown. Adds a Home menu item.
project.aboutIf present, generates an About page that renders this markdown. Adds an About menu item.
project.coverImageObject { 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 list allTags_<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.

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), metrics
  • entities (list with list/cards modes) + 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.default is false if no second metric exists.
  • bar: [ metric[2], metric[3], ... ]preferences.showBar.default is false if no third metric exists.

Settings

  • projectMainLabel / projectExtraLabel / locales / seo from 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 scope types.
  • No self-source metrics (i.e., per-metric metricResults value buckets).
  • No sub-metric groupings (the trl_ticks / trlGroup pattern 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:

index.ts

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:

  1. 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.
  2. Data shape: a Zod schema derived from the manifest at call time validates the data structurally. Missing allTags_<key>, missing metric_<key> on an entity, wrong cluster shape, etc. produce { success: false, error } with the field path in error. 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

ExportDescription
configFromManifest(manifest, data)The function. Server-only.
zManifestZod schema for the manifest. Use for runtime validation in external repos.
ManifestTypeScript input type for the manifest.

On this page