SnabbSajt · Docs
Site Kit SDK

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

FieldTypePurpose
format"sajt-site"Format discriminator.
version1Schema version.
exportedAtISO date stringPackage creation time.
siteSiteBusiness, locale, theme, contact, and site-wide settings.
fontsAssignment?{ headingTmpId?, bodyTmpId? }References fonts[].tmpId.
services?Service[]Canonical services referenced by sections.
foldersFolder[]Page hierarchy. Use [] when unused.
contentCollections?ContentCollection[]Blog or news collections.
pagesPage[]Pages and posts.
sectionsSection[]Ordered, typed content sections.
fontsFont[]Google, Adobe, or bundled custom fonts.
assetsAsset[]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.

TypeContent fields after type
heroheadline; eyebrow?, subheadline?, media?, primaryCta?, secondaryCta?
servicesheading, items[{ title, description, priceText?, icon?, media?, cta?, serviceId? }]; intro?, source?, footerCta?
service-detailtitle, body; bullets?, media?, cta?
aboutheading, body; media?, signatureName?
teamheading, members[{ name, role?, photo?, bio? }]; footer and intro fields optional
testimonialsquotes[{ text, author, role?, rating?, avatar? }]; heading?
galleryimages[AssetRef]; heading?
before-afterpairs[{ before, after, label? }]; heading?
pricingheading, currency, tiers[{ name, price, features, period?, cta?, highlighted? }]; intro?
faqitems[{ question, answer }]; heading and footer CTA fields optional
processheading, steps[{ title, description, icon? }]
service-areasheading, areas[string]; intro?
contactheading, fields, submitLabel, successMessage; intro, map, address, and info fields optional
opening-hoursdays; heading?, note?
locationaddress; heading?, zoom?
certificationsitems[{ label, logo? }]; heading?
social-proofstats[{ value, label }]; heading?
instagramimages[AssetRef]; heading?, handle?
cta-bandheadline, primaryCta; subtext?, secondaryCta?
bookingall of heading?, intro?, cta?, source? are optional
lead-formheading, fields, submitLabel, successMessage; intro?
quote-flowheading, steps, pricing, successMessage, submitLabel; pricing/helper fields optional
footerbusinessName; tagline?, contactLine?, columns?, legalText?
legalheading, blocks[{ kind: "h" or "p", text }]
logositems[{ label, logo? }]; heading?, intro?
highlightsheading, items[{ title, description, icon?, media? }]; intro?
bentocells[{ title, description?, media?, span? }]; heading?, intro?
bannertext; cta?
videoprovider: "youtube", "vimeo", or "upload"; heading?, caption?, videoId? (embed providers), video? (AssetRef to a kind: "video" asset for "upload")
comparisoncolumns[{ label, highlighted? }], rows[{ label, cells }]; heading?, intro?
newsletterheading, placeholder, submitLabel, successMessage; intro?, consentText?
statementtext; attribution?, cta?
rich-textblocks[h, p, or ul]; heading?
imageimage?, caption?
featured-productheading?, intro?, siteSlug?, products?
product-gridheading?, intro?, siteSlug?, products?
documentsitems[{ title, description?, document? }] (document is an AssetRef to a kind: "document" PDF); heading?, intro?
scroll-tabstabs[{ label, description, title?, media?, video? }]; heading?, intro?
comparison-sliderminValue, 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.

On this page