If you're building or forking a Velocity X site, or migrating an existing codebase to the brand.json pattern, you need to know every field that goes into the file. This reference documents the schema end-to-end — what each key does, whether it's required, what type it is, and what the real-world values look like. Bookmark this for every port you do.
Why Schema Reference Matters
Consistency across AI tools hinges on knowing the schema shape. When Claude or Cursor encounter an ambiguous field, they guess. When they know the exact type (string vs array vs nested object) and whether it's required, they write correct code first time. This reference is your single source of truth.
For teams managing multiple Velocity X forks, a shared schema means every port looks the same. For AI workflows, it means build scripts, validators, and audits all speak the same language.
File Location and Structure
The brand.json file lives at your project root (same level as astro.config.mjs). For a monorepo, each brand gets its own JSON — at that brand's root.
{`velocity9-stock/
├── brand.json # Single source of truth
├── category-pages/ # JSON per category
├── item-pages/ # JSON per item
├── src/
│ └── pages/
│ ├── [category].astro # Reads category-pages/*.json
│ └── [category]/[item].astro
└── astro.config.mjs`}
All paths inside brand.json (logos, images, etc.) are relative to the public/ folder.
Top-Level Structure
brand.json is a single object with eight top-level keys:
{`{
"product": { ... }, // Product details, tiers, features
"business": { ... }, // Company identity, contact, social
"seo": { ... }, // Title suffix, default meta, OG settings
"theme": { ... }, // Colour hue, dark mode, accent
"navigation": { ... }, // Primary nav links, CTA button
"cta": { ... }, // Default form headline and labels
"pricing": { ... }, // Currency, symbol, tier array
"footer": { ... } // Copyright, legal links
}`}
The product Block
Holds all product/service-specific values. Most fields in this block appear on the home hero slider, pricing page, and product index.
{`"product": {
"name": "Velocity X", // string (required) — displayed name
"shortName": "VX", // string (optional) — acronym/abbrev
"version": "X", // string (optional) — version letter/number
"tagline": "The website framework for vibe coders.",
// ^ string (required) — 1-line description, <70 chars
"subtitle": "Every integration I install on every paid site...",
// ^ string (optional) — longer tagline for hero
"price": "$4,995", // string (required) — main display price
"priceFrom": "from $4,995", // string (optional) — with quantifier
"currency": "AUD", // string (required) — 3-letter ISO code
"priceLabel": "one-off · digital download · lifetime updates",
// ^ string (optional) — subtext below price
"purchaseUrl": "https://buy.stripe.com/...",
// ^ string (optional) — Stripe checkout link
"demoUrl": "https://velocity9.aidxn.com",
// ^ string (optional) — live demo site
"creator": "Aidxn Design", // string (optional) — author name
"creatorYears": 12, // number (optional) — years of experience
"creatorTitle": "Full Stack Developer", // string (optional) — role/credential
"tiers": [ // array (optional) — pricing packages
{
"id": "small", // string (required) — unique slug
"name": "Small Website Package", // string (required) — display name
"tagline": "For solo operators...", // string (required) — audience/positioning
"price": "$4,995", // string (required) — cost
"currency": "AUD", // string (required) — ISO code
"stripeUrl": "https://buy.stripe.com/...",
// ^ string (optional) — per-tier checkout
"turnaround": "30-day turnaround", // string (optional) — delivery time
"pages": 32, // number (optional) — page count or feature qty
"audience": "Solo trades, single-location...",
// ^ string (required) — who this is for
"highlights": [ // array (required) — 4-6 bullet points
"32-page website with industry-leading SEO...",
"Built-in admin dashboard...",
"CRM integration (HubSpot, Pipedrive, or Attio)",
// more bullets...
]
}
// repeat for medium, large, or custom tiers
],
"features": [ // array (optional) — 15-20 feature objects
{
"name": "Multi-stop route optimisation",
// ^ string (required) — feature name
"description": "Plan a rep's whole day in seconds...",
// ^ string (required) — 1-2 sentence benefit
"icon": "Route", // string (required) — Lucide icon name
"sourceCompetitor": "Badger Mapping, Optiway, Commerce Vision",
// ^ string (optional) — where this came from / inspiration
"tier": "have-already" // string (required) — "have-already" | "quick-win" | "medium"
}
// repeat for each feature
]
}`}
product — Gotchas
Keep price strings consistent (use "$X,XXX" with comma thousands separator for AUD). The currency field is a label (for display); don't do math on the price string. If you add new tiers, all four keys (id, name, tagline, audience) are mandatory — the renderer expects them. Feature tier values map to UI coloring/grouping on the home features section.
The business Block
Company identity, legal details, contact info, and social media. This data fuels the footer, header branding, and metadata.
{`"business": {
"name": "Aidxn Design", // string (required) — brand name
"shortName": "Aidxn", // string (required) — no-space version for URLs/slugs
"tagline": "Design-led websites, branding and marketing.",
// ^ string (required) — company tagline
"description": "Web design, graphic design and digital marketing...",
// ^ string (required) — 1-2 sentence pitch, used in OG meta
"type": "ProfessionalService", // string (required) — schema.org type
// (use "Organization", "ProfessionalService", "LocalBusiness", etc.)
"founded": 2018, // number (optional) — founding year
"url": "https://aidxn.com", // string (required) — canonical domain
"ogImage": "/opengraph.png", // string (required) — social share image, ~1200x630px
"logos": {
"primary": "/ax-logo-purp.svg", // string (required) — main logo
"white": "/ax-logo-white.svg", // string (required) — on dark backgrounds
"black": "/ax-logo-black.svg", // string (required) — on light backgrounds
"favicon": "/favicon.ico", // string (required) — browser tab icon
"appleTouchIcon": "/apple-touch-icon.png"
// ^ string (optional) — iOS home screen icon
},
"address": {
"locality": "Gold Coast", // string (required) — city
"region": "QLD", // string (required) — state/province
"country": "AU", // string (required) — ISO 2-letter code
"areaServed": "Gold Coast, QLD, Australia",
// ^ string (required) — coverage area description
"wikipedia": "https://en.wikipedia.org/wiki/Gold_Coast,_Queensland"
// ^ string (optional) — location reference
},
"contact": {
"email": "hi@aidxn.com", // string (required) — primary email
"phone": "", // string (optional, can be empty) — phone number
"bookingUrl": "/book", // string (required) — calendar/booking link
"enquireUrl": "/enquire", // string (required) — contact form path
"contactUrl": "/contact" // string (required) — contact page path
},
"social": {
"instagram": "https://instagram.com/aidxn.design",
// ^ string (optional) — full profile URL
"facebook": "https://facebook.com/aidxn.design",
"twitter": "https://twitter.com/aidxn.design",
"github": "https://github.com/aidenwood",
"linkedin": "", // empty string is ok if no account
"youtube": "",
"tiktok": ""
},
"twitterHandle": "@aidxn.design", // string (required) — @ handle (no spaces)
"googleAnalyticsId": "G-MN4T5PXYLD", // string (required) — GA4 ID (G-*)
"web3formsKey": "1e0ffadd-2f05-471b-bbc2-36fa32793837"
// ^ string (required) — Web3Forms API key for contact forms
}`}
business — Gotchas
The address.country field is a 2-letter ISO code (AU, US, GB, CA, etc.). Empty string is valid for social fields you don't use — don't delete the key. Phone can be empty but the key must exist. All URLs must be full (https://...) except contact endpoints (enquireUrl, contactUrl, bookingUrl) which are site-relative (/enquire, /contact, /book).
The seo Block
Default metadata that appears on every page unless overridden at the page level. Mostly used by Layout.astro for head tags and OG cards.
{`"seo": {
"defaultTitleSuffix": "· Aidxn", // string (required) — appended to every page title
"defaultDescription": "Web design, graphic design and digital marketing on the Gold Coast.",
// ^ string (required) — fallback meta description
"ogLocale": "en_AU", // string (required) — Facebook locale code
"ogSiteName": "Aidxn Design" // string (required) — og:site_name tag
}`}
seo — Gotchas
The defaultTitleSuffix should start with a space or separator (" · Aidxn" or " | Aidxn"). ogLocale uses underscore (en_AU, not en-AU). defaultDescription is 150–160 chars — too long and search engines truncate it.
The theme Block
Visual theme settings. primaryHue controls the Tailwind hue used across the site (passed to Alpine's theme store).
{`"theme": {
"primaryHue": "violet", // string (required) — Tailwind hue name
// Options: "slate", "gray", "zinc", "neutral", "stone", "red", "orange",
// "amber", "yellow", "lime", "green", "emerald", "teal", "cyan", "sky",
// "blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose"
"darkModeDefault": false, // boolean (required) — dark mode on by default?
"accentColor": "#a600ff" // string (required) — accent colour hex, for gradients
}`}
theme — Gotchas
primaryHue must match a Tailwind 3.3 colour name. Tailwind 4's new oklch() values aren't supported in this schema. accentColor is a fallback accent used in gradients and highlights; it should contrast well with the primary hue. darkModeDefault sets the initial theme, but Alpine theme store respects user preference.
The navigation Block
Header navigation and primary CTA button.
{`"navigation": {
"primary": [ // array (required) — main nav links
{
"label": "Web Design", // string (required) — display text
"href": "/web-design" // string (required) — link path
},
{
"label": "Digital Marketing",
"href": "/digital-marketing"
},
{ "label": "Blog", "href": "/blog" }
// add 3–7 links total
],
"cta": {
"label": "Enquire", // string (required) — button text
"href": "/enquire" // string (required) — button destination
}
}`}
navigation — Gotchas
href must be a root-relative path (/web-design, not web-design or https://...). The cta is always displayed in the header as a button, even if you repeat it in primary. Keep primary links to 5–7 items for readability on mobile.
The cta Block
Default form messaging used in CTA components throughout the site.
{`"cta": {
"default": {
"headline": "Let us make some quick suggestions?",
// ^ string (required) — form heading
"subtext": "Unique solutions for your brand.",
// ^ string (required) — under-heading subtext
"submitLabel": "Submit" // string (required) — button text
}
}`}
cta — Gotchas
These are default values. Individual CTA components can override them via props. subtext should be 1–2 short lines. submitLabel is often "Submit", "Send", "Let's Go", or "Book Now".
The pricing Block
Currency settings and tier array (currently reserved for future use; tiers live in product.tiers).
{`"pricing": {
"currency": "AUD", // string (required) — 3-letter ISO code
"currencySymbol": "$", // string (required) — display symbol
"tiers": [] // array (empty) — reserved, use product.tiers
}`}
pricing — Gotchas
This block exists for future expansion. For now, define all pricing tiers in product.tiers, not here. The currency and currencySymbol are defaults in case a page needs to reference currency outside of product.
The footer Block
Footer branding and legal links.
{`"footer": {
"copyrightLine": "© Aidxn Design. All rights reserved.",
// ^ string (required) — copyright text
"legal": [ // array (required) — footer link group
{
"label": "Privacy", // string (required) — link text
"href": "/privacy" // string (required) — link path
}
// add "Terms", "Sitemap", "Accessibility", etc. as needed
]
}`}
footer — Gotchas
copyrightLine should include the © symbol. All hrefs must be root-relative (/privacy, not privacy). Keep legal links to 3–5 items; they shrink on mobile.
Migration Tips: Swapping Brands
When you fork Velocity X for a new client or business, here's the checklist:
1. Start with product values. Create the new product.name, tagline, pricing.tiers, and features array. This is the copy-heaviest part; take time here.
2. Business block second. Fill in name, shortName, address, contact URLs. Make sure all URLs are correct (bookingUrl, enquireUrl, contactUrl).
3. Replace logos. Drop four SVG files into public/ (primary, white, black) + favicon. Update paths in logos block. Test in browser to confirm rendering.
4. Update social links. Paste full URLs (https://instagram.com/...). If a platform isn't used, leave the field as empty string ("").
5. Set theme and SEO. Choose a primaryHue from the Tailwind palette. Write a strong defaultDescription (150–160 chars). Set ogSiteName to match business.name.
6. Configure analytics. Get the GA4 ID (starts with G-) and Web3Forms key. Paste into the business block.
7. Test the build. Run npm run build and check for console errors. Watch for missing image paths (ogImage, logo paths). If a path is wrong, the build often still succeeds but images won't load.
Validation: Catching Errors Before Build
A simple JSON schema validator prevents runtime surprises. At the repo root, store a brand.schema.json file that describes types and required fields, then use ajv (npm install ajv) to validate brand.json against it before building.
{`// Minimal schema for brand.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["business", "product", "theme", "seo"],
"properties": {
"business": {
"type": "object",
"required": ["name", "shortName", "url"],
"properties": {
"name": { "type": "string" },
"shortName": { "type": "string" },
"url": { "type": "string", "format": "uri" }
// ... more fields
}
}
// ... other blocks
}
}`}
Add a lint script to package.json: "lint:brand": "ajv validate -s brand.schema.json -d brand.json". Run it before every build.
Frequently Asked Questions
Can I add custom fields to brand.json?
Yes. The schema is a convention, not a restriction. If your site needs fields like "copyrightYear" or "supportPhoneUrgent", add them. The loaders in src/lib/content.ts will pass them through. Just document them in a comment or a companion schema.json so future maintainers know what they're for.
What if a field is blank? Do I delete it or use empty string?
Use empty string ("") for optional string fields, not null or undefined. The loaders expect a string; null can cause template errors. For optional arrays (features, tiers), you can omit the key entirely or pass an empty array ([]).
How do I version brand.json as the brand evolves?
Don't. Keep a single brand.json and version it in git. Each commit is a record. If you need to compare versions, use git diff or git log. Keeping multiple brand-v1.json, brand-v2.json files creates confusion about which is canonical.
Can I reference brand.json from other JSON files (category pages, item pages)?
In Astro, yes. Use the loaders in src/lib/content.ts. They import brand.json and merge it with category/item data. You don't manually reference it in the JSONs. The loaders handle the merge at build time.
What if I'm migrating from a CMS to brand.json?
Write a one-off script that pulls the CMS data (via API) and outputs it as brand.json. Then delete the script. From that point on, edit brand.json directly (via git) or via a simple admin UI that writes back to the file. CMS-as-source and file-as-source don't mix well — pick one.
How do I keep brand.json in sync with the rendered site?
Validation + automated tests. In your CI, run a check that reads brand.json and verifies every value appears somewhere on the site (or at least, the critical ones: business.name in footer, product.price on pricing page, etc.). If the rendered site drifts from the JSON, the test fails. See our previous post on brand.json for a detailed example.
The Bottom Line
A well-structured brand.json is the difference between "editing this site is a mess" and "editing this site is straightforward". Every field in this reference has a purpose. When you're porting to a new brand or onboarding a new developer, this schema is your north star. Bookmark it, share it with your team, and treat it as the single source of truth for every brand value on your site.
The next time you fork Velocity X, fill in brand.json completely, validate it, and trust that the rest of the site will follow. That's the promise of the pattern.