Config reference
Every field of the three-section config — designSystem, organization and site — plus the full design-token schema. Browsable, filterable, and generated from the published JSON Schema.
Generated from config.schema.json and schema.json, the same schemasvitops validate checks against — so they always match validation. Every field is linkable by its dotted path: #site.analytics.clarityId, #colors.palette.
For what belongs in each section and when you need a config at all, start withYour config. Agents should read vitops docs config and vitops docs authoringinstead of this page.
Project config
The three sections of a project config. Token fields live under designSystem and are listed in full below, so this tree stops at the wrapper.
designSystemobjectrequiredThe design system: named themes plus the system-wide facts (which theme, which appearance). A bare design system, or a bare theme map, is accepted as shorthand for
{ themes: … }.themesmaprequiredNamed themes.
defaultis the base; others mayextendsanother and supply a partial patch. Light/dark is NOT a theme — the functional tokens flip per appearance within each one, sodefaultalready has both. Only the default theme is built today; multi-theme output and a picker are not wired yet.defaultThemestringWhich
themesentry to use (convention: "default"). Validated against the map; selecting a non-default theme is not wired yet.defaultColorSchemelight | dark | systemInitial appearance.
"system"follows the OS viaprefers-color-schemeand is what makes<wc-color-scheme-toggle>'s "System" position resolve to anything — it removes the theme attribute, so without this the page falls through to light. It also gives a no-JS page the OS appearance. Defaults to"light", because switching an existing site to"system"visibly flips it dark for dark-OS visitors.
organizationobjectThe company: schema.org Organization details for JSON-LD (name, legalName, logo, tax IDs, sameAs), plus its contact, physical locations, services and public profiles. These stay true across sites — several sites can share one
organizationand differ only insite.nameone ofTrading name, as a reader would recognise it (e.g. "Acme"). Localisable.
- one ofstring
- one ofmap
legalNamestringRegistered legal name (e.g. "Acme Widgets Inc."). The generated legal documents prefer this over
name— a policy is a statement by the legal entity.foundingDatestringDate the organization was founded (YYYY-MM-DD).
logoone ofOrganization logo → JSON-LD
logo.- one ofstring
one ofobject
srcstringrequiredaltstringrequiredwidthnumberheightnumber
emailstringGeneral contact address. Also the last fallback for privacy requests — see
contact.phonestringGeneral contact telephone number.
addressobjectRegistered/mailing address. Its
addressRegionandaddressCountryare what the governing-law clause of the generated terms of service defaults to.streetAddressstringrequiredaddressLocalitystringrequiredCity.
addressRegionstringState / province.
postalCodestringaddressCountrystringrequiredCountry (ISO 3166-1 alpha-2 preferred).
taxIDstringTax identification number.
vatIDstringVAT identification number.
sameAsarray of stringProfile URLs → JSON-LD
sameAs(mirrorslinks).contactone ofPrimary contact: either a
locationskey (string reference) or an inline name/email/phone/address object.- one ofstring
one ofobject
namestringemailstringphonestringaddressobjectschema.org
PostalAddressfield names, so JSON-LD generation is lossless.streetAddressstringrequiredaddressLocalitystringrequiredCity.
addressRegionstringState / province.
postalCodestringaddressCountrystringrequiredCountry (ISO 3166-1 alpha-2 preferred).
primaryLocationstringThe main
locationskey (for JSON-LD and defaults).locationsmapPhysical locations (schema.org LocalBusiness): address, geo, opening hours, service area.
<name>objectslugone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
nameone ofrequiredA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
emailstringphonestringaddressobjectschema.org
PostalAddressfield names, so JSON-LD generation is lossless.streetAddressstringrequiredaddressLocalitystringrequiredCity.
addressRegionstringState / province.
postalCodestringaddressCountrystringrequiredCountry (ISO 3166-1 alpha-2 preferred).
geoobjectlatitudenumberrequiredlongitudenumberrequired
hoursarray[items]objectdayOfWeekarray of Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sundayrequiredopensstringrequiredclosesstringrequired
hoursSpecialarrayHoliday/temporary hours, as dated deviations from the recurring
hours— distinct from it because these carry a date range rather than a weekday.[items]objectvalidFromstringrequiredvalidThroughstringopensstringclosesstring
typeLocalBusiness | Store | Restaurant | CafeOrCoffeeShop | ProfessionalService | MedicalBusiness | HealthAndBeautyBusiness | HomeAndConstructionBusiness | AutomotiveBusiness | FinancialService | LegalService | RealEstateAgent | Organizationdescriptionone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
areaServedarray of stringpriceRangestringpaymentAcceptedarray of stringcurrenciesAcceptedstringknowsLanguagearray of stringphotosarrayLocation photos (logo, storefront, interior, …) → JSON-LD
image. Every listing platform (GBP, Bing Places, Apple Business Connect) wants these; none can derive them.[items]one of- one ofstring
one ofobject
srcstringrequiredaltstringrequiredwidthnumberheightnumber
sameAsarray of stringThis location's own profile URLs (its Google Business Profile, Bing Places listing, Apple Business Connect location, Facebook page, …) → JSON-LD
sameAs. Distinct fromorganization.sameAs, which is the company overall — a multi-location org's locations do not share one GBP listing.listingsmapExternal listing IDs for this location, keyed by platform — the Google Business Profile location id, the Bing Places business id, the Apple Business Connect location id. Not consumed by the generator today; recorded here so a future sync has a stable id to match against instead of risking a duplicate listing.
servicesmapServices offered (schema.org Service/Offer): name, description, slug, price offers.
<name>objectnameone ofrequiredA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
descriptionone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
slugone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
offersarray[items]objectnameone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
pricestring | numberpriceCurrencystringavailabilitystringurlstringvalidFromstringpriceValidUntilstring
linksobjectPublic profile URLs (also feed JSON-LD
sameAsviaorganization.sameAs).googleMapsstringinstagramstringfacebookstringxstringlinkedinstringyoutubestringgithubstring
siteobjectrequiredThis published site: locales, domains, environments, content templates, SEO, analytics, legal documents, icons, favicon and deployment. Facts about a presentation of the organization rather than the organization itself.
defaultLocalestringrequiredThe locale used when none is specified; must be a
localeskey.localesmaprequiredLocales the site is published in, keyed by BCP 47 tag (e.g. "en", "fr").
<name>objectnamestringrequiredDisplay name of the locale (e.g. "English").
taglinestringSite tagline in this locale.
basePathstringURL prefix for this locale (e.g. "/fr").
domainsobjectCanonical domain + redirecting aliases.
canonicalstringrequiredThe canonical origin used for absolute URLs and SEO.
aliasesarrayAlias domains and how they redirect to the canonical one.
[items]objectdomainstringrequiredredirectTypenumber | number | number | numberrequiredredirectTostringrequiredenvironmentstring
dnsmapDesired DNS state per domain (provider, nameservers, records) — declarative reference, not applied automatically.
<name>objectproviderstringnameserversarray of stringrecordsarray[items]objecttypeA | AAAA | CNAME | TXT | MX | NS | SRV | CAA | PTR | SOArequirednamestringvaluestringrequiredttlintegerproxiedboolean
cloudflaremapDeliberate escape hatch for provider-specific Cloudflare settings — out of scope to model here.
googleobjectThis site's Google Cloud project. Not a secret — the credentials stay in the environment.
projectstringrequiredGoogle Cloud project id that API usage is attributed to (quota and billing). One project per site keeps usage separate — Search Console today, Maps and anything else later. Sent as
x-goog-user-project, which is required when the credential is a user one (an ADC login fromgcloud auth application-default login, orVITOPS_GOOGLE_*): those use a shared OAuth client that carries no project of its own. A service-account credential belongs to a project already, so this is not needed for it. The identity must holdserviceusage.services.useon the project, and the APIs being called must be enabled there.
searchConsolemapDomains to onboard as Google Search Console domain properties via
vitops search setup, keyed by bare hostname (mirrorsdns). Credentials come from the environment (CLOUDFLARE_API_TOKEN + the Google OAuth vars) — never in this file. Setgoogle.projectwhen using a user credential.<name>objectdelegatedOwnersarray of stringEmails added as verification owners on the Site Verification web resource. Automated via the API — additive and idempotent, never removes an existing owner.
fullUserGroupstringA Google Group to grant Full-User access in Search Console. SURFACED AS A REMINDER only: Search Console exposes no user/permission API, so this step stays manual by design.
environmentsmaprequiredDeploy environments (production, dev, …): URL, API origin, analytics toggle, robots policy, active A/B variant.
<name>objecturlstringrequiredPublic origin of this environment.
apistringAPI origin, when different from
url.analyticsbooleanWhether analytics fire in this environment.
adsbooleanWhether
site.adspixels fire in this environment. Defaults toanalytics, then true — a preview deployment sending pageviews is survivable, one firing conversion pixels is not, so this can be turned off on its own.robotsstringRobots policy (e.g. "noindex,nofollow" for dev).
variantstringActive
abTesting.variantskey for this environment.
abTestingobjectA/B testing: cookie-based split plus named variants whose
overridespatch the config per environment.enabledbooleanrequiredcookieNamestringcookieMaxAgeintegerSeconds.
splitRationumberFraction of traffic sent to the variant (0–1).
variantsmapNamed variants; each targets an environment and may deep-merge config
overrides.<name>objectenvironmentstringrequireddescriptionstringoverridesmap
fontsarrayFont LOADING (a serialisable projection of the Astro Fonts API: provider, weights, subsets, preload). Font TOKENS live in
designSystem.themes.<theme>.fontsand reference the samecssVariable.[items]objectnamestringrequiredprovidergoogle | fontsource | adobe | bunny | fontshare | googleicons | npm | localrequiredcssVariablestringrequiredweightsarray[items]number | string
stylesarray of normal | italicsubsetsarray of stringfallbacksarray of stringoptimizedFallbacksbooleandisplayauto | block | swap | fallback | optionalformatsarray of stringunicodeRangearray of stringvariantsarray[items]objectsrcarray of stringrequiredweightnumber | stringstylenormal | italicunicodeRangearray of stringdisplayauto | block | swap | fallback | optional
preloadone ofEmit
<Font preload />for this family (true, or a list of weight/style faces).- one ofboolean
one ofarray
[items]objectweightnumber | stringrequiredstylenormal | italic
tagsmapFree-form content-tag taxonomy (site-specific; not interpreted by the generator).
postTypesmapFree-form content-type definitions (site-specific; not interpreted by the generator).
galleriesmapNamed image galleries (title, tags, images with localisable alt/caption).
<name>objecttitleone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
tagsarray of stringimagesarrayrequired[items]objectsrcstringrequiredaltone ofrequiredA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
captionone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
testimonialsmapNamed testimonial groups (quote, name, role, rating, date).
<name>objecttitleone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
itemsarrayrequired[items]objectquoteone ofrequiredA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
namestringrequiredrolestringratingnumberdatestring
templatesmapNamed content templates rendered at SSR time:
nav(menu trees),form(field lists + submit), ornodes(raw element trees).<name>one ofone ofobject
typestringrequiredtagsarray of stringpresentmapitemsarray
one ofobject
typestringrequiredtagsarray of stringpresentmapactionstringmethodget | postfieldsarray[items]objectnamestringrequiredtypetext | email | tel | url | number | password | textarea | select | checkbox | radio | date | file | hiddenrequiredlabelone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
requiredbooleanvalidationstringoptionsone of- one ofstring
one ofarray
[items]objectlabelstringrequiredvaluestringrequired
attrsmap<name>string | number | boolean
submitmap<name>string | number | boolean
honeypotboolean
one ofobject
typestringrequiredtagsarray of stringnodesarray
seoobjectSEO defaults: title/description templates, robots policy, verification tokens, Open Graph + Twitter cards, and post-deploy indexing notification.
titleTemplatestringPage-title pattern (e.g. "%s · Acme").
descriptionTemplatestringrobotsstringe.g. "index,follow" / "noindex,nofollow".
googleSiteVerificationstringbingSiteVerificationstringopenGraphobjecttypestringsiteNamestringtitleone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
descriptionone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
imageone of- one ofstring
one ofobject
srcstringrequiredaltstringrequiredwidthnumberheightnumber
localestring
twitterobjectcardsummary | summary_large_image | app | playersitestringcreatorstringtitleone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
descriptionone ofA string, or a per-locale map of strings (
{ en: "…", fr: "…" }).- one ofstring
- one ofmap
imageone of- one ofstring
one ofobject
srcstringrequiredaltstringrequiredwidthnumberheightnumber
indexingobjectHow
vitops search notifytells search engines about a deploy: which sitemap, IndexNow key, Search Console property, and which pages to verify afterwards.sitemapUrlstringThe sitemap to submit and to diff for changed URLs. Defaults to
<canonical>/sitemap-index.xml.indexNowobjectIndexNow submission (Bing, Yandex, Naver, Seznam, Yep — not Google). Omit to skip the channel.
keystringrequiredIndexNow key (8–128 chars, hex is conventional). NOT a secret — it is served publicly at
keyLocationso the engine can verify you own the host. Generate one withvitops search notify --new-key.keyLocationstringAbsolute URL of the key file. Defaults to
<canonical>/<key>.txt; set it only when the file lives elsewhere.endpointstringIndexNow endpoint (default
https://api.indexnow.org/indexnow). Any participating engine shares submissions with the rest, so one is normally enough.
searchConsoleobjectGoogle Search Console property. Needs a credential at run time — either a service account in
VITOPS_GSC_SERVICE_ACCOUNT/GOOGLE_APPLICATION_CREDENTIALS, or the user OAuth credentialvitops search setupuses (VITOPS_GOOGLE_CLIENT_ID/_CLIENT_SECRET/_REFRESH_TOKEN). The service account wins when both are set. Never put a key in this file.siteUrlstringrequiredThe property exactly as Search Console identifies it —
sc-domain:acme.cafor a domain property, or the URL-prefix formhttps://acme.ca/. A mismatch here is a 403, not a "not found".resubmitSitemapbooleanRe-submit the sitemap through the Search Console API on each notify (default true when
searchConsoleis set). This is the automated equivalent of the manual resubmit in the UI.
priorityUrlsarray of stringThe pages whose indexing actually matters.
vitops search notify --checkinspects these and exits non-zero if Google has not indexed one. Kept explicit because URL Inspection is quota-bound (2000/day), so checking every page is neither affordable nor informative.
analyticsobjectAnalytics provider IDs (gated per environment via
environments.<env>.analytics). Which provider is set is also what the generated privacy policy and cookie notice disclose — seelegal.googleAnalyticsIdstringplausibleDomainstringgoogleTagManagerIdstringclarityIdstringMicrosoft Clarity project ID.
matomoobjectMatomo instance.
urlstringrequiredInstance base URL (self-hosted, or
*.matomo.cloud).siteIdstringrequiredSite ID within that instance.
cookiesbooleanDefault false — Matomo runs cookieless (
disableCookies), which is what lets the cookie notice state positively that it sets none.
notificationsobjectWhere site notifications (form submissions, tracked calls) are sent. TODO:
smsandpersistchannels — onlyemailis implemented.emailone ofWhere conversion notifications are e-mailed. A bare address is shorthand for
{ provider: "cloudflare", to }.- one ofstring
one ofobject
providerstringrequiredDelivery provider. Only Cloudflare Email Sending is implemented; the field is stated so adding another is additive.
tostringRecipient. Defaults to the primary location’s
email, then the first location that has one.fromstringSender. Defaults to
noreply@<domains.canonical>. Its domain must be onboarded (wrangler email sending enable <domain>) or every send fails.fromNamestringDisplay name on the From header. Defaults to
organization.name.replyTostringReply-To header.
bindingstringWorkers
send_emailbinding name (defaultEMAIL). Set it when using a restricted binding.
trackingobjectAd-click attribution for conversion tracking.
enabledbooleanCapture ad click IDs and UTMs from the landing URL into the
_accookie, for attributing conversions.categorymarketing | analyticsConsent category the
_accookie waits on (defaultmarketing). It is a 90-day identifier tying a visitor to the ad that brought them, so it is only written once this category is granted — and asking for it is what raises the banner.platformsarray of stringInformational list of ad platforms in use. Capture recognises every known click-ID parameter regardless.
adsmapAd properties this site is linked to, keyed by platform. Read by
vitops ads setup(ensure the DNS verification record),vitops ads tags(emit the consent-gated pixel), and the generated cookie notice (disclose what each pixel stores). Credentials never live here — the DNS write uses CLOUDFLARE_API_TOKEN from the environment.<name>objectaccountIdstringThe advertising account as the platform shows it — Google Ads customer ID, Meta ad account, LinkedIn partner ID, Reddit advertiser ID. For LinkedIn this is also the Insight Tag id; everywhere else the tag id is
pixelId.pixelIdstringThe tag/pixel ID the browser snippet initialises — Meta pixel, TikTok pixel, Reddit pixel, Microsoft UET tag ID, Pinterest tag ID, Snap pixel, or the Google Ads conversion ID (
AW-…, which is NOT the customer ID).conversionLabelstringGoogle Ads only: the conversion action label paired with the
AW-…conversion ID.domainstringBare hostname to verify with this platform. Defaults to the host of
domains.canonical— set it only when the ad account is verified against a different domain.domainVerificationstringThe verification token from the platform UI, for the platforms that verify by DNS TXT (Meta, TikTok, Pinterest, Snapchat).
vitops ads setupprompts for it and writes it here on first run. NOT a secret — it is published in DNS, and the platform fetching it back is the ownership proof, exactly like the IndexNow key. A value containing=is used as the whole record, which is the escape hatch if a platform changes its prefix.categorymarketing | analyticsConsent category the tag waits on (default
marketing). An ad pixel is advertising by default;analyticsis for a site using one purely for its own measurement.enabledbooleanSet false to keep the property on record without emitting its tag.
securityobjectSecurity integrations (bot protection).
turnstileobjectsiteKeystringCloudflare Turnstile site key (public).
legalobjectLegal pages: which documents exist, where they live, and the facts the generated prose asserts.
jurisdictioncaWhich legal template set the generated documents use. Default
ca.privacyPolicyobjectenabledbooleanrequiredurlstringlastUpdatedstringprivacyOfficerone ofWho privacy requests go to. Falls back to
contact, thenprimaryLocation.- one ofstring
one ofobject
namestringemailstringphonestringaddressobjectschema.org
PostalAddressfield names, so JSON-LD generation is lossless.streetAddressstringrequiredaddressLocalitystringrequiredCity.
addressRegionstringState / province.
postalCodestringaddressCountrystringrequiredCountry (ISO 3166-1 alpha-2 preferred).
retentionstringHow long personal information is kept, in prose (e.g. "24 months after last contact").
processorsarrayThird parties that receive personal information and cannot be inferred from the rest of the config (payment processors, CRMs, mail senders). Known providers implied by
analytics,securityanddeploymentare added automatically — list only the rest.[items]objectnamestringrequiredThe provider, as a reader would recognise it (e.g. "Stripe").
purposestringrequiredWhy they receive it, as a noun phrase that reads after "for" (e.g. "payment processing").
countrystringShorthand for the common case where one country is both where they store it and whose laws reach it: asserts BOTH
storage: [{ country }]ANDoperatorCountry. Reads inside a sentence (e.g. "the United States"). When the two differ — a Canadian region operated by a US company — statestorageandoperatorCountryinstead; setting this alongside either is rejected.storagearrayWhere the information actually rests. Feeds the "stored or processed outside of <jurisdiction>" disclosure and nothing else. Several entries are allowed, each optionally scoped to a category of information — which is what makes a Canadian-region tenant holding identity data in the US expressible.
[items]objectcountrystringrequiredA country or bloc where this provider stores or processes the information, as it should read in a sentence (e.g. "Canada", "the United States", "the European Union"). Naming the jurisdiction the policy is written for is meaningful: it says this information does not leave it.
scopestringWhich information is held there, as a noun phrase that reads after "in the case of" (e.g. "account and sign-in information", "mailbox contents"). Omit when this location holds everything the provider receives — a scoped entry claims LESS than an unscoped one, so omitting it is the safe default.
operatorCountrystringThe jurisdiction that can compel this provider to hand the information over — where it is established, or from which it is controlled. Reads after "the laws of" (e.g. "the United States"); a bloc is acceptable. A SEPARATE fact from
storage, because privacy law cares about foreign access, not only foreign storage: a Canadian-region service run by a US company isstorage: [{ country: "Canada" }]withoperatorCountry: "the United States".privacyUrlstring
termsOfServiceobjectenabledbooleanrequiredurlstringlastUpdatedstring | nullgoverningLawstringGoverning-law clause, in prose (e.g. "the Province of Ontario"). Defaults from the contact address.
cookieConsentobjectenabledbooleanrequiredtypeopt-in | opt-outConsent model.
positiontop | bottom | centercategoriesarray of string
iconsmapIcon sets and the specific icons to bundle from each (keys are iconify collection names).
uistringIcon set used for UI chrome (e.g. "lucide", "ph").
brandstringIcon set used for brand marks.
weightstringWeight for suffix-weighted sets like Phosphor ("regular" | "bold" | "duotone" | "fill" | "light" | "thin"). Ignored by sets that split weights across collections, e.g. Font Awesome.
spritebooleanEmit an SVG sprite (icons.svg) alongside the stylesheet, for consumers that cannot run an icon integration (Bricks/WordPress, EmDash renderers).
semanticarray of stringNamed semantic icons to include.
fa7-solidarray of stringfa7-regulararray of stringfa7-lightarray of stringfa7-thinarray of stringfa7-brandsarray of stringsimple-iconsarray of stringmaterial-symbolsarray of stringlucidearray of stringpharray of string
faviconobjectFavicon/PWA asset generation (consumed by
@getvitops/utilsfavicon tooling).sourcestringrequiredSource image (SVG/PNG) the favicon set is generated from.
lowResSourcestringAlternate source for small raster sizes (16/32px) when the main source scales down poorly.
namestringApp name for the generated web manifest. Setting this AND
themeColoris what makes the site installable: together they emitsite.webmanifest(withdisplay: "standalone") and link it. Neither alone does anything. A marketing site that does not want a browser offering to install it should leave one of them unset.themeColorstringBrowser UI colour, emitted as
<meta name="theme-color">on every page — so it applies with or without a manifest. Note it is also half the manifest switch: seename.backgroundColorstringBackground composited under the OPAQUE generated icons —
apple-touch-icon.pngand the maskableicon-mask.png— which are produced whether or not there is a manifest. Defaults to white, so a dark logo on a transparent source needs this set. It is also the manifestbackground_colorwhen one is emitted, but it is not manifest-only.
deploymentobjectHow the site is built and deployed (platform, commands, output directory) — informational for tooling.
platformstringbuildCommandstringdeployCommandstringoutputDirectorystringtypestring
Design tokens
Every field of a design system — what designSystem.themes.<name> holds, and what a standalone design-system.json contains on its own.
metaobjectBrand identity for agent-facing output. Consumed only by the
designformat (DESIGN.md); it emits no CSS and no tokens.namestringBrand/system name. Used as the
namefield and<h1>of thedesignformat'sDESIGN.md. Defaults to "Design System".descriptionstringOne or two sentences on the brand personality and the feeling the UI should evoke — what an agent needs when no token answers the question. Becomes the DESIGN.md
descriptionfield and opens its Overview section; if omitted, a generic description of the system's mechanics is used instead.
colorsobjectrequiredThe colour system (the only required section):
palettehues become generated OKLCH scales;rolesmap semantic roles onto those hues, from which all role tokens and dark mode derive.palettemaprequiredPalette hues by name. Each becomes an 11-step numeric OKLCH scale (
--color-<hue>-50…950). Every ramp shares one fixed lightness ladder, so a step means the same lightness in every hue; only chroma and hue vary. Beyond the outermost authored colour the chroma decays towards a small endpoint value (0.008 light / 0.015 dark) so the near-white and near-black ends keep a whisper of the hue — but that is a ceiling, not a target, so a low-chroma seed stays low and a chroma-0 seed gives a true neutral rather than a tinted one. The ladder is also enforced: if pinned colours leave a ramp non-monotonic (some step darker than the one below it), the build fails rather than shipping a scale whose hover states run backwards.<name>one ofA palette hue, authored one of two ways:
{ seed, anchors? }generates an 11-step numeric OKLCH scale (50…950) from the seed, or{ tones }supplies a fixed brand kit used verbatim.one ofobject
Seeded hue: the 11-step scale is GENERATED in OKLCH from
seed(anchors pin specific steps).seedstringrequiredSeed colour (hex or oklch()). An 11-step numeric scale (50…950, tinted near-white → tinted near-black) is generated in OKLCH from it; the seed is preserved at its natural step.
anchorsmapStep → colour overrides (hex or oklch()) pinned VERBATIM at those steps. Every other step takes its lightness from the shared ladder, with chroma and hue interpolated between the anchors — so an anchor is reproduced exactly and is the only step allowed off the ladder. An explicit anchor overrides the seed at that step; two anchors that resolve to the same step are an error.
one ofobject
Fixed hue: authored brand tones used verbatim; no generation.
tonesone ofrequiredFixed brand kit: authored tones placed verbatim at their nearest steps plus tinted off-white/off-black endpoints; no interpolation. Either an ordered light → dark array or a step → colour map.
- one ofarray of string
- one ofmap
rolesmaprequiredMaps semantic role names onto palette hues. Role names are ARBITRARY — add a key and the generator emits that role's token set (
--color-<target>-<role>[-<variant>]for target bg/text/icon/border), its dark-mode flip and its utility classes. A value is either a hue name ("danger": "rust") or{ "hue": …, "kind": "surface" | "chromatic" }. The kind decides the shape of the token set.chromatic(the default, and what the bare-string form means) is a signal colour: its backgrounds split into tints (bg-<role>-x-muted/-muted) and solids (bg-<role>-solid[-bold|-x-bold]), with deliberately no barebg-<role>— "how loud?" is a question the author answers.surfaceis a page/panel colour: it has a barebg-<role>plus the full emphasis range and text scale. Dark mode flips automatically; there is no per-appearance scheme grammar. The solid family and its computedtext-on-<role>foreground stay mode-stable so a filled button keeps its identity. Six roles are a required core, because the shipped framework CSS references them with no fallback: brand-primary, danger, neutral, surface, ui-primary, warning. Conventional additions are ui-secondary/accent, brand-secondary, info and success.<name>one of- one ofstring
A palette hue name. Shorthand for
{ hue, kind: "chromatic" }. one ofobject
huestringrequiredThe palette hue this role resolves to.
kindsurface | chromaticsurface— a page/panel colour: gets a barebg-<role>plus the full emphasis range and text scale.chromatic(default) — a signal colour: tints and solids only, no barebg-<role>.
- one ofstring
utilitiesarray of bg | text | icon | border | outline | fill | strokeWhich colour utility-class families to emit (
bg-*,text-*,icon-*,border-*,outline-*,fill-*,stroke-*). Defaults to bg, text, icon, border.iconis a separate non-text tier (a glyph may run more vivid than text);outline/fill/strokehave no tokens of their own and alias the border and icon tiers.
shadowsmapNamed shadows →
--shadow-<name>tokens and.drop-shadow-<name>utilities. Values are shadow parameter lists (offset/blur/colour). Each token feeds two consumers with different grammars —box-shadow(pattern geometry, via the--ds-*group aliases) andfilter: drop-shadow(…)(the utilities and theshadow:state shortcut) — so values must stay in the intersection: one layer, no spread radius, noinset.drop-shadow()rejects all three, and rejecting them invalidates the whole filter, so the shadow vanishes rather than degrading.fontsmapRaw font stacks by name, emitted as
--font-<name>tokens (referenced bytypography.families). Stacks only — vitops does not load webfonts. A value here is afont-familylist and nothing more: it emits no@font-face, no preload, and no metrics-matched fallback. If a family needs loading, declare it in Astro'sfonts:config (astro.config, or the site config'sfontsarray) and point the token at the family'scssVariable—"display": "var(--font-league-spartan), sans-serif". Installing a@fontsource*package and importing its CSS also works but gives up subsetting, preload andsize-adjust/ascent-overridefallbacks, so it regresses CLS.typeScaleobjectFluid modular TYPE scale →
--text-<name>tokens, consumed by typography roles and text-size utilities.basestringrequiredAnchor size (a CSS length, e.g. "1rem") — the value at
baseStep.rationumberrequiredModular ratio between adjacent steps at large viewports.
stepsnumberToken count when
namesis absent (steps are then named 1..steps).namesarray of stringStep names, smallest → largest (e.g. ["xs","sm","md",…]); each becomes a token suffix.
baseStepnumber1-based index of the step whose value is
base.baselinestringNamed step used as the fluid pivot / GUI scale centre (defaults to
baseStep).fluidobjectMakes the scale fluid: each step compiles to a clamp() that interpolates from
minRatioatminVwtoratioatmaxVw.minVwstringrequiredViewport width (CSS length) where fluid scaling bottoms out.
maxVwstringrequiredViewport width (CSS length) where fluid scaling tops out.
minRationumberrequiredModular ratio at/below
minVw(usually <ratio).
spaceScaleobjectFluid modular SPACE scale →
--space-<name>tokens, consumed by spacing/gap utilities and vertical rhythm.basestringrequiredAnchor size (a CSS length, e.g. "1rem") — the value at
baseStep.rationumberrequiredModular ratio between adjacent steps at large viewports.
stepsnumberToken count when
namesis absent (steps are then named 1..steps).namesarray of stringStep names, smallest → largest (e.g. ["xs","sm","md",…]); each becomes a token suffix.
baseStepnumber1-based index of the step whose value is
base.baselinestringNamed step used as the fluid pivot / GUI scale centre (defaults to
baseStep).fluidobjectMakes the scale fluid: each step compiles to a clamp() that interpolates from
minRatioatminVwtoratioatmaxVw.minVwstringrequiredViewport width (CSS length) where fluid scaling bottoms out.
maxVwstringrequiredViewport width (CSS length) where fluid scaling tops out.
minRationumberrequiredModular ratio at/below
minVw(usually <ratio).
patternsobjectComponent patterns and their token cascade:
defaults→groups→ per-patternoverrides, plus shape (radii) and z-index primitives.defaultsmapCascade-wide fallback tokens, emitted as
--<prop>-default.radiimapShape primitives, emitted as
--br-<name>(referenced by pattern bases).groupsmapGroup-level tokens, emitted as
--<prop>-<group>; patterns opt in via theirgroupkey.<name>mapA CSS declaration block: property → value. Values stay strings (they can be hex, var(), clamp(), keywords, …); the generator, not the schema, interprets them.
zmapZ-index tiers →
--z-tier-<name>.itemsmapThe component patterns to emit, keyed by name.
<name>objectOne component pattern (button, link, badge, card, …): base declarations + interaction states + semantic role variants, resolved through the pattern token cascade.
groupstringToken-cascade group this pattern belongs to (e.g. tag / control / panel); base declarations resolve through
--<prop>-<group>before--<prop>-default.overridesmapPer-pattern token overrides, emitted as
--<prop>-<name>-groupvalues.elementstringStyle at element level via zero-specificity
:where(<element>)(instead of, or alongside, a class).classstringClass name to emit (defaults to the pattern's key when no
elementis set). Combined withelement, the pattern emits one zero-specificity:where(<element>, .<class>)rule so the class works on any tag and any explicit class overrides it.fillbooleanWhether this pattern is colour-filled (states/roles drive
background-color+on-solidtext) or text-coloured (they drivecolor). Defaults to true whenbasedeclares a background.default_rolestringSemantic colour role applied to the bare/default variant.
basemapBase CSS declarations. Geometry properties (padding, border-radius, border, box-shadow, font-size) are wrapped in per-pattern override hooks (
--p-<name>,--br-<name>,--b-<name>,--ds-<name>,--fs-<name>) so consumers can restyle one pattern by setting one variable.statesmapInteraction states (hover / active / focus-visible), each a map of shortcuts:
step(intensify the fill or text by n rungs —bg-<role>-solid→-solid-bold,text-<role>→-bold),scale(transform scale),lift(translateY + shadow),shadow(a shadow name → drop-shadow(var(--shadow-<name>)), or true → lift shadow),ring(focus ring), or rawcssdeclarations. Hover rules are wrapped in@media (hover: hover).rolesarray of stringSemantic colour role variants to emit as
<pattern>-<role>classes (fills use the role solid / on-solid tokens).
typographyobjectTypography: family aliases, semantic type roles (→
font-<role>classes), and the bare-element → role mapping.familiesmapRole-facing family aliases → CSS font values, usually referencing the top-level
fontstokens (e.g. "var(--font-display)").rolesmapSemantic type roles (display, title, heading, body, quote, caption, eyebrow, code, lead, footnote, tag, …), each emitted as a
font-<role>class.<name>mapA bag of CSS-ish keys, each mapped to a declaration plus a
--<role>-<sfx>override hook. The recognised set is closed:family,size,weight,style,line-height,tracking(→letter-spacing),text-transform,text-decoration,text-wrap,color. Note the last four are spelled with their full CSS property names —transformanddecorationare NOT accepted. Anything unrecognised is ignored, not passed through, so the generator warns rather than emitting it: a silently-droppedtransform: uppercaseis how title-case navigation reaches production. Note also thatstyle,text-transform,text-decorationandtext-wrapare emitted on every role at their identity value (normal/none/none/wrap) whether declared or not, so applying one role class over another fully resets it — which means omittingtext-wrapis not "inherit": it emitstext-wrap: wrapand cancels theprettythe role would otherwise inherit from aprettyancestor such as abody-mapped role. Declare it on every role —balancefor heading-like roles,prettyfor copy,wrapfor short single-line labels.<name>string | number
headingsmapMaps bare elements to type roles so unclassed markup picks up role styling —
{ "h1": "display", "h2": "heading" }. The key is used verbatim as a selector, so it is not limited to h1…h6: map"body"to your prose role to bind base page typography to the role rather than hand-writing it. That binding is what makes the role editable — a stylesheet that re-statesfont-family/line-heightas literals onbodyshadows--<role>-ff/--<role>-lh, and the live theme editor then appears to do nothing.
animationsobjectAnimation effect + journey classes (pure value layers). The animation engine itself — keyframes, drivers, floats, utilities — is static framework CSS, not configured here.
effectsmapEffect classes to emit (
.fade-in,.reveal-left, …), keyed by class name.<name>objectA named animation effect class — a pure value layer (
--_anim+--<prop>-from/-to) over the static keyframe engine.kfstringrequiredKeyframe family driving the effect: composite (transform/opacity), paint, or layout.
cssmapExtra literal declarations merged into the effect class as-is.
<name>string | number
varsmapEffect endpoint variables (
--<key>: <value>, e.g. opacity-from, translate-y-to) that override the keyframe defaults.<name>string | number
journeysobjectMulti-part journey classes composed from
basebuilding blocks.basemapNamed journey building blocks: part name → var map.
composearray of arrayCombinations of base parts, each emitted as a
.<parts>-journeyclass.