Schema reference
Every Site Kit package field, shared reference shape, and supported section content type.
Site Kit uses a strict PortableSiteV1 JSON envelope. Unknown fields are
rejected. A ? below means optional; every other field is required.
Package fields
| Field | Type | Purpose |
|---|---|---|
format | "sajt-site" | Format discriminator. |
version | 1 | Schema version. |
exportedAt | ISO date string | Package creation time. |
site | Site | Business, locale, theme, contact, and site-wide settings. |
fontsAssignment? | { headingTmpId?, bodyTmpId? } | References fonts[].tmpId. |
services? | Service[] | Canonical services referenced by sections. |
folders | Folder[] | Page hierarchy. Use [] when unused. |
contentCollections? | ContentCollection[] | Blog or news collections. |
pages | Page[] | Pages and posts. |
sections | Section[] | Ordered, typed content sections. |
fonts | Font[] | Google, Adobe, or bundled custom fonts. |
assets | Asset[] | Bundled image/logo/favicon/OG/video/document declarations. |
site requires businessName, vertical, goal, language, theme, and
contact. Optional fields are languages, socials, tracking,
bookingConfig, logoAssetId, faviconAssetId, and ogImageAssetId. Use
DEFAULT_THEME as a safe theme starter.
Pages and folders
type Folder = {
tmpId: string; name: string; order: number;
parentTmpId?: string; collapsed?: boolean;
};
type ContentCollection = {
tmpId: string; kind: "blog" | "news";
name: string; slugPrefix: string; order: number;
};
type Page = {
tmpId: string; slug: string; title: string; order: number;
externalKey?: string; // stable id for incremental (merge) imports
folderTmpId?: string; showInNav: boolean;
pageType?: "page" | "post"; collectionTmpId?: string;
excerpt?: string; author?: string; featuredImage?: AssetRef;
firstPublishedAt?: number; contentType?: string; plannedFor?: number;
seo?: { metaTitle?: string; metaDescription?: string; noindex?: boolean;
canonical?: string; sourceUrl?: string };
};An empty slug is the home page. Every tmpId is local to the package and all
references must resolve inside that same package.
Sections
type Section = {
pageTmpId: string;
type: SectionType;
variant: string;
tone?: "light" | "clear" | "dark";
layout?: unknown;
order?: string; // omit it and import assigns keys in array position
hidden?: boolean;
anchorId?: string;
externalKey?: string; // stable id for incremental (merge) imports
content: PortableSectionContent;
};order is optional: leave it out and the import assigns valid fractional keys
in array order. Only set it when you must preserve keys from a real export.
Give every page and section a stable externalKey (for example "home",
"home/hero") if you plan to merge later updates into the same site.
content.type must equal the outer type. Use
SECTION_REGISTRY[type].variants for valid variants and
SECTION_REGISTRY[type].defaultContent as a complete starter. Fields before a
semicolon are required; fields ending in ? are optional.
| Type | Content fields after type |
|---|---|
hero | headline; eyebrow?, subheadline?, media?, primaryCta?, secondaryCta? |
services | heading, items[{ title, description, priceText?, icon?, media?, cta?, serviceId? }]; intro?, source?, footerCta? |
service-detail | title, body; bullets?, media?, cta? |
about | heading, body; media?, signatureName? |
team | heading, members[{ name, role?, photo?, bio? }]; footer and intro fields optional |
testimonials | quotes[{ text, author, role?, rating?, avatar? }]; heading? |
gallery | images[AssetRef]; heading? |
before-after | pairs[{ before, after, label? }]; heading? |
pricing | heading, currency, tiers[{ name, price, features, period?, cta?, highlighted? }]; intro? |
faq | items[{ question, answer }]; heading and footer CTA fields optional |
process | heading, steps[{ title, description, icon? }] |
service-areas | heading, areas[string]; intro? |
contact | heading, fields, submitLabel, successMessage; intro, map, address, and info fields optional |
opening-hours | days; heading?, note? |
location | address; heading?, zoom? |
certifications | items[{ label, logo? }]; heading? |
social-proof | stats[{ value, label }]; heading? |
instagram | images[AssetRef]; heading?, handle? |
cta-band | headline, primaryCta; subtext?, secondaryCta? |
booking | all of heading?, intro?, cta?, source? are optional |
lead-form | heading, fields, submitLabel, successMessage; intro? |
quote-flow | heading, steps, pricing, successMessage, submitLabel; pricing/helper fields optional |
footer | businessName; tagline?, contactLine?, columns?, legalText? |
legal | heading, blocks[{ kind: "h" or "p", text }] |
logos | items[{ label, logo? }]; heading?, intro? |
highlights | heading, items[{ title, description, icon?, media? }]; intro? |
bento | cells[{ title, description?, media?, span? }]; heading?, intro? |
banner | text; cta? |
video | provider: "youtube", "vimeo", or "upload"; heading?, caption?, videoId? (embed providers), video? (AssetRef to a kind: "video" asset for "upload") |
comparison | columns[{ label, highlighted? }], rows[{ label, cells }]; heading?, intro? |
newsletter | heading, placeholder, submitLabel, successMessage; intro?, consentText? |
statement | text; attribution?, cta? |
rich-text | blocks[h, p, or ul]; heading? |
image | image?, caption? |
featured-product | heading?, intro?, siteSlug?, products? |
product-grid | heading?, intro?, siteSlug?, products? |
documents | items[{ title, description?, document? }] (document is an AssetRef to a kind: "document" PDF); heading?, intro? |
scroll-tabs | tabs[{ label, description, title?, media?, video? }]; heading?, intro? |
comparison-slider | minValue, maxValue, defaultValue, columns[{ label, ratePct, note?, highlighted? }]; step?, valuePrefix?, valueSuffix?, valueLabel?, heading?, intro? (restricted type — only advanced-editor users can add it in the app) |
The SDK exports PortableSectionContent, SiteKitSection, SECTION_TYPES, and
the exact runtime sectionContent validator for nested fields.
Shared references
type AssetRef = {
assetId: string; // assets[].exportId, never a database ID
alt: string;
focalX?: number; focalY?: number; // 0..1
};
type CtaRef = {
label: string;
target:
| { kind: "page"; pageSlug: string }
| { kind: "anchor"; anchorId: string }
| { kind: "phone"; value: string }
| { kind: "email"; value: string }
| { kind: "external"; url: string }
| { kind: "booking" };
style?: "primary" | "secondary" | "ghost";
};
type Address = {
street?: string; postalCode?: string; city?: string;
country?: string; lat?: number; lng?: number;
};Service references use services[].tmpId as plain strings. A service requires
tmpId, name, bookable, and order; pricing, duration, category, action,
payment, intake, availability, scheduling, visibility, and archive fields are
optional.
Assets and fonts
type Asset = {
exportId: string; url: string; width: number; height: number;
blurhash?: string; mimeType: string;
kind: "image" | "logo" | "favicon" | "og" | "video" | "document";
alt?: string;
durationSec?: number; // kind: "video" only — best-effort declared duration
};
type Font = {
tmpId: string; source: "upload" | "google" | "adobe"; family: string;
license?: "licensed" | "trial"; // trial families block publish until replaced
googleUrl?: string; adobeKitId?: string;
files?: Array<{ url: string; weight: number;
style: "normal" | "italic"; format: string }>;
};The CLI matches every bundled declaration to exactly one file by ID and
extension. Always run site-kit validate; TypeScript cannot validate parsed
JSON, file bytes, cross-references, or package caps.
Self-hosted video is supported: declare a kind: "video" asset (mp4/webm/mov,
optional durationSec) and reference it from a video section with
provider: "upload" or from a hero's bgVideo slot. Import sniffs the real
container bytes and enforces the target workspace's per-plan size and duration
caps plus storage quota — an oversized clip is skipped and reported, never a
hard failure. kind: "document" assets are downloadable PDFs (import checks
the %PDF- magic bytes) referenced from a documents section.