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 }) → 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 every content type 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).
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 under
Settings
. The auth extension's, verbatim:
"$studio": {
"settings": {
"icon": "sp-icon-lock-closed",
"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. |
label |
Section label (defaults to
project.title
). |
order |
Sort position among contributed sections. |
layout |
"form"
(default) — one form over the whole section value.
"map"
— 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
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
(
—
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