Project sections and settings
A project section is a top-level
project.json
key owned by an extension class:
content
(parser),
connections
and
data
(connector),
auth
(auth). Owning a section means three things: your fragment defines its schema, your capabilities load it and expand its
$paths
, and your
$studio
block gives it a settings page. A class claims a section iff its descriptor has a top-level
project
object.
The
project
block
The parser's
Content.class.json
declares, verbatim:
"project": {
"key": "content",
"title": "Content Types",
"description": "File-based content collections loaded at build and dev-serve time",
"referenceable": true
}
| Key | Type | Default | Meaning |
|---|---|---|---|
key |
string |
— | The project.json top-level property this class owns (single word by convention; exclusive across extensions). |
title |
string |
— | Studio label. |
description |
string |
— | Studio help text. |
referenceable |
boolean |
false |
Opts the section's named entries into the relationships vocabulary ( Relationships ). |
The section's
value schema is not duplicated here
. It lives in the package's project fragment (the manifest's
schemas.project
, see
Schema composition
). Hosts that need the entry shape (Studio settings forms, reference pickers) read
properties[<key>]
from the fragment via the registry.
Loading section data:
projectData
Behavior attaches through
capability methods
on the same class. The
projectData
capability turns the raw section value into loaded data:
projectData(sectionValue, { projectConfig, root, registry, io, lazy? }) → unknown
The compiler's site build and the dev server's resolve path both call it, storing the result as
_project[<key>]
in resolved scope, so the parser's
projectData
loads content types through the format registry and pages see the entries as
config._project.content
. The auth extension's
projectData
is nearly a passthrough: it exposes the section's identifiers under
_project.auth
(never secrets).
Loading on demand
A build reads every part of every section, so it calls
projectData
without
lazy
and gets everything at once. The dev server and Studio serve one page at a time, so they pass
lazy: true
. An owner may then return data that loads each part the first time a class asks for it, with the same shape a whole load has, plus a
complete()
method that loads the rest. An owner that ignores
lazy
returns whole data, and every host reads it unchanged.
The parser's
content
section uses this. With
lazy
,
_project.content
starts empty: a collection loads the first time it is asked for, with only its frontmatter, and an entry's Markdown body is parsed only when that entry is returned. A page that shows four case studies parses four bodies instead of every collection in the project.
A class written against a whole section still gets one. Unless its implementation declares
static projectOnDemand = true
, the server calls each section's
complete()
before constructing it, so a synchronous
_project.content.get("posts")
sees every entry with its body, as in a build. To get the on-demand speed in your own class, read the section the way
ContentCollection
does and declare the flag:
export class LatestPosts {
static projectOnDemand = true;
constructor(config) {
this.config = config;
}
async resolve() {
const content = this.config._project?.content;
const posts = (await content?.load?.("posts")) ?? content?.get("posts") ?? [];
const latest = posts.slice(0, 3);
await content?.materialize?.(latest);
return latest;
}
}
load
and
materialize
are optional, so the same code reads a whole section (a build, or a plain
Map
in a test) unchanged.
Expanding routes:
resolvePaths
and discriminators
A dynamic route's
$paths
value is dispatched to the section class whose
resolvePaths
capability declares the matching
discriminator
, the key that routes to it. The parser's discriminator is
contentType
, so this page head:
{ "$paths": { "contentType": "blog", "param": "slug" } }
dispatches to
Content.resolvePaths(pathsDef, { data, projectConfig, root })
, which returns one route-param object per entry (
[{ "slug": "hello-world" }, …]
). Hosts dispatch purely on which discriminator key is present: no central switch statement, no extension aware of any other. The contributed
$paths
shape is also what your document fragment unions into the paths schema resource, so editors validate it. See
Routing
for the authoring side.
Publishing files:
assets
If your section reads from directories, the
assets
capability publishes them at a site URL so the files beside its sources are reachable:
assets(sectionValue, { projectConfig, root }) → [{ urlPrefix: "/content/blog", dir: "/abs/content/blog" }]
The parser returns one mount per content type with a directory source. The site build resolves those URLs for image optimization and copies the referenced files into
dist/
; the dev server serves them from the source. That is what makes a collection's co-located images work, including when the source lives outside the project. Details in
Capability methods
.
Studio settings:
$studio.settings
The section class's
$studio
block may declare a settings section, rendered generically by Studio in its Project settings dialog. The auth extension's, verbatim:
"$studio": {
"settings": {
"icon": "lock-simple",
"label": "Authentication",
"order": 58,
"layout": "form",
"entry": {
"ui": {
"connection": { "enum": { "$ref": "#/$context/connections" } },
"secretEnv": { "control": "secret" }
}
}
}
}
| Key | Meaning |
|---|---|
icon |
Section icon in the settings nav. Names a glyph from the Jx UI kit icon set (
@jxsuite/ui/icons
), never an element tag. A name the kit does not ship draws nothing. |
label |
Section label (defaults to
project.title
). |
group |
Which group of the settings nav lists the section:
"general"
,
"integrations"
(default) or
"advanced"
(Studio marks it
For developers
). Any other value reads as the default. |
order |
Sort position within its group. |
layout |
"form"
(default) renders one form over the whole section value.
"map"
gives master-detail for
type: object
+
additionalProperties
sections: key list left, entry form right. |
entry.ui |
Per-field control overrides for the entry form:
{ "<field>": { "control": "<name>" } }
. |
entry.newEntry |
Template for freshly created entries, with
${key}
substitution. |
renderer |
Escape hatch: names a Studio-registered custom section renderer. First-party extensions use the generic path. |
Everything else (field labels, types, required marks) comes from the project fragment's schema for the section, so the form stays in sync with validation for free.
The parser's
Content
class shows the
map
layout: its section is a map of content types, so Studio renders a key list (add/rename/delete, slugified) beside an entry form, seeds new entries from
entry.newEntry
(
"source": "./content/${key}/"
, where
${key}
becomes the new entry's name), and swaps the
schema
field for the visual
schema-builder
control. That is the entire implementation of the
Content types
surface. No parser-specific Studio code exists.
Controls and dynamic enums
Built-in controls you can name in
entry.ui
:
"schema-builder": the visual JSON-Schema field editor."secret": the value is committed via the platform's secret store, never project.json."binding": signal/route-param binding.Implicit defaults per type, enum, and format cover everything else.
Enum choices may be dynamic via
{ "$ref": "#/$context/<pointer>" }
, a JSON-pointer walk over the project config (with
{@param}
segment substitution and the
$formats
virtual root). Auth's
connection
field above lists the keys of the project's
connections
section;
#/$context/auth/roles
would list configured roles.
Secrets never enter
project.json
. Committed config carries identifiers and env-var
names
only (
"secretEnv": "BETTER_AUTH_SECRET"
); the
secret
control writes actual values to the platform's secret store, and locally they live in the git-ignored
.dev.vars
. See
Security and secrets
.
Related
Schema composition : where the section's value schema lives
Capability methods :
projectDataandresolvePathscontractsServer mounts : giving a section a route subtree
Project settings in Studio : what users see