project.json
Studio writes this file for you — Project settings edits the identity, defaults, and build keys; the content-type builder writes
content; the design-token editor writesstyle.
project.json
at the project root is the only required configuration file in a Jx site. A minimal real one (abbreviated from jxsuite.com):
{
"$schema": "./project.schema.json",
"name": "Jx Suite",
"url": "https://jxsuite.com",
"defaults": { "layout": "./layouts/base.json", "lang": "en" },
"build": { "outDir": "./dist", "trailingSlash": "always" },
"extensions": ["@jxsuite/parser"]
}
Key reference
| Key | Type | Purpose |
|---|---|---|
name |
string |
Site name — the default
<title>
and
$site.name |
url |
string |
Production URL, used for canonical URLs and the sitemap |
defaults |
object |
layout
,
lang
, and
charset
applied to every page |
$head |
array |
Global
<head>
entries injected into every page |
$media |
object |
Named breakpoints available in every style object |
style |
object |
Design tokens (
--
keys) and global element rules |
state |
object |
Site-wide state, merged read-only into every page |
$defs |
object |
Shared JSON Schema type definitions for the whole project |
content |
object |
Content types — see Content collections |
imports |
object |
$prototype
name → class path, cascaded to all pages |
extensions |
array |
Extension packages providing formats, sections, and classes |
redirects |
object |
Source path → destination — see Redirects |
copy |
object |
Extra source → destination file mappings copied into the build output |
build |
object |
outDir
,
trailingSlash
,
adapter
— see
Deployment |
images |
object |
Image optimization settings — see Images |
Enabled extensions add sections of their own alongside these. The first-party ones:
| Key | Type | Contributed by | Purpose |
|---|---|---|---|
connections |
object |
@jxsuite/connector |
Named database connections — see Connections |
data |
object |
@jxsuite/connector |
Dynamic tables served over
/_jx/data
— see
Data tables |
auth |
object |
@jxsuite/auth |
Visitor accounts and sessions — see Auth and secrets |
search |
object |
@jxsuite/search |
Build-time search index — see Search |
Identity and defaults
name
and
url
identify the site:
name
is the fallback
<title>
and is exposed to every page as
$site.name
;
url
anchors canonical URLs and sitemap entries.
defaults.layout
wraps every page that doesn't declare its own
$layout
(a page can opt out with
"$layout": false
), and
defaults.lang
sets
<html lang>
site-wide.
Global head entries
$head
entries are injected into every page's
<head>
, before layout- and page-level entries:
{
"$head": [
{ "tagName": "link", "attributes": { "rel": "icon", "href": "/favicon.svg" } },
{
"tagName": "meta",
"attributes": { "name": "viewport", "content": "width=device-width, initial-scale=1" }
}
]
}
A page's own
$head
appends to (and, for singletons like
<title>
, overrides) the site-level entries. The full merge order is covered in
SEO and metadata
.
Breakpoints and style tokens
$media
names media queries once so every component can respond to
"@--md"
without redeclaring the query.
style
holds the global stylesheet:
--
-prefixed keys compile to
:root {}
custom properties, plain camelCase properties style the page root, and nested objects are element selectors applied site-wide. Declaring a scheme query like
"--dark": "(prefers-color-scheme: dark)"
additionally enables the visitor-forced
color-scheme contract
—
@--dark
token overrides in
style
become the dark variant of the design.
{
"$media": { "--md": "(max-width: 768px)", "--sm": "(max-width: 640px)" },
"style": {
"--color-accent": "#3b82f6",
"--radius": "8px",
"fontFamily": "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
"backgroundColor": "var(--color-bg-primary)",
"table": { "width": "100%", "borderCollapse": "collapse" }
}
}
Every component in the project can reference
var(--color-accent)
and use
"@--md": { … }
overrides without importing anything. See
Styling
for the style object format and
Design tokens
for the Studio editor.
Site state and shared types
state
declares site-wide data. Every page receives it two ways: merged directly into the page's own
state
(the page wins when it declares the same key), and under the read-only
$site
entry —
$site.name
,
$site.url
, plus each site state key.
{
"state": {
"siteName": "My Site",
"socialLinks": [{ "label": "GitHub", "url": "https://github.com/example" }]
}
}
$defs
holds reusable
JSON Schema type definitions
— shapes for API responses, CMS payloads, or shared value types — that any document in the project can reference, the project-wide counterpart of a document's own
$defs
.
Content types
The
content
section defines content collections — folders of Markdown, JSON, or CSV entries with a schema (abbreviated from jxsuite.com, which publishes these docs from the repo's
docs/
folder):
{
"content": {
"docs": {
"source": "../../docs",
"format": "Markdown",
"$elements": [{ "$ref": "./components/doc-note.json" }],
"schema": { "type": "object", "required": ["title"] }
}
}
}
The keys and query prototypes are covered in Content collections .
Redirects
redirects
maps old paths to new ones — a plain string for a 301, or an object to pick the status:
{
"redirects": {
"/docs/get-studio": "/docs/start/install/",
"/legacy/post/:slug": { "destination": "/blog/:slug", "status": 301 }
}
}
Patterns and status codes are covered in Redirects .
Copying extra files
copy
maps files from anywhere in the repository into the build output — useful for publishing artifacts that live outside
public/
:
{
"copy": {
"../../packages/schema/project-schema.json": "schema/project/v1/index.json"
}
}
Sources resolve relative to the project root; destinations are paths inside
dist/
. The copies run after
public/
assets, so a mapping can overwrite a public file of the same name.
Build
build
controls output:
outDir
(default
./dist
),
trailingSlash
, an optional deployment
adapter
, and sitemap generation. See
the build pipeline
and
Deployment
.
Extensions and imports
extensions
lists extension packages (bare package names or relative paths) that contribute format classes such as
Markdown
and
Csv
, project sections such as
content
, and
$prototype
classes.
imports
maps a
$prototype
name to a class file for all pages at once; a page-level
imports
entry wins on collision. See
Data prototypes
and
Dependencies and imports
.
Extension-contributed sections
An extension can own a whole top-level key. Listing the package in
extensions
is what turns that key on:
jx schema
composes each enabled extension's schema fragment into
project.schema.json
, so an unlisted extension's section is an unknown key that
jx validate
rejects. Re-run
jx schema
whenever the list changes.
{
"extensions": ["@jxsuite/parser", "@jxsuite/connector", "@jxsuite/auth"],
"connections": {
"main": { "provider": "sqlite" }
},
"data": {
"comments": {
"connection": "main",
"permissions": { "read": "public", "insert": "authenticated", "update": "owner" },
"ownerField": "author_id",
"schema": {
"type": "object",
"properties": { "message": { "type": "string" }, "author_id": { "type": "string" } },
"required": ["message"]
}
}
},
"auth": {
"connection": "main",
"methods": { "emailPassword": true },
"redirects": { "afterSignIn": "/", "afterSignOut": "/" }
},
"build": { "adapter": "cloudflare-pages" }
}
Two rules run through all of them:
Names, never values. Sections carry identifiers and env-var names (
urlEnv,secretEnv,clientIdEnv); the secret values live in the git-ignored.dev.varslocally and your host's secret store in production. See Auth and secrets .A section served at runtime needs an adapter.
connections/dataandauthare served by extension mounts under/_jx/, sobuild.adaptermust name a server-capable target — the build fails on static. See Build output and adapters .
Studio writes these sections for you from Settings > Connections , Data Tables , and Authentication — see Databases .
What documents inherit
Site-level declarations cascade into every page automatically — no imports needed:
$headentries are prepended to every page's<head>.$mediabreakpoints andstyletokens are available in every component.defaults.langsets<html lang>everywhere.statemerges into page state (page wins) and is exposed as$site.importsand$elementsmerge with page-level entries (page wins on collision).
Everything else is deliberate: files in
data/
load via an explicit
$ref
, collection data requires
ContentCollection
or
ContentEntry
declarations, and components receive outside data only through
$props
— scope never leaks across a component boundary.