Developer guide

SuperBuilder Theme Architecture

How site content and visual themes stay separated — and how Next.js components plug into canonical sections so existing pages keep working when the design changes.

Version 1.4 Canonical source: THEME_ARCHITECTURE.md Live examples: Ultimate HVAC, Ultimate HVAC v4, Comfort Mechanical v1, Azeem Public URL — no login required

1. Overview

SuperBuilder treats a website as three layers. Users edit content once; themes only change how that content looks.

LayerWhat it isChanges when…Edited via
Page config Ordered sections + props (default-config.json) Text, images, section order Builder WYSIWYG or grid save API
Canonical components Stable types: navbar, hero, services, … Platform adds a new section type canonical-components.ts
Theme Colors, fonts, React skins User picks or uploads a design Template picker / ZIP upload
Page Config → Prop Normalizer → Theme Registry → ThemedSiteRenderer
Rendering pipeline: Page Config to ThemedSiteRenderer
Figure 1 — Runtime pipeline from page config to themed output
Key rule: Edit content (section props / WYSIWYG). Do not rewrite theme React components unless you are building or uploading a new theme.

2. Mental model — same content, different skins

A restaurant site and an HVAC site can share the same section types. Only the theme components and design tokens change. Page structure stays in JSON.

Same page config rendered under two different themes
Figure 2 — One page config · two theme skins

What stays the same

  • Section IDs and order
  • Canonical prop fields (brand, CTAs, phone)
  • Widgets / SEO metadata
  • Publish slug and site record

What the theme owns

  • React component implementations
  • CSS / SCSS / Tailwind styling
  • Design tokens (--theme-*)
  • Optional adaptProps() mapping

3. User journey

  1. Browse/websites pick a bundled theme or upload a ZIP
  2. Edit/builder/[slug] WYSIWYG + section tools (owner / edit collaborator only)
  3. Save — grid config and/or content changes
  4. Publish — live site at published slug / subdomain
  5. View — anyone can open the published site (no builder chrome)
Builder canvas mockup with HVAC preview
Figure 3 — Builder canvas: live theme preview with section labels

Site template IDs

SourceSite templateIdRenderer
Bundled themeazeem, hvac, ultimate-hvac, ultimate-hvac-v4, comfort-mechanical-v1, …ThemedSiteRenderer
Next.js ZIP uploadnextjs-theme:{slug}ThemedSiteRenderer + staged files
Single-page HTMLsingle-page:{slug}SinglePageThemeRenderer (iframe)

3b. Builder edit vs public view

Editing is private. Viewing a published site is public. Gate: app/builder/[slug]/layout.tsxrequireSiteAccess(slug, 'edit').

SurfaceWhoPurpose
/builder/[slug] Site owner (or collaborator with edit/full) Design / WYSIWYG / section config
{subdomain}.magicsite.ai Anyone Published live site (middleware → /published/…)
/published/[slug] Anyone Published preview on the main domain
Non-editors who open /builder/[slug] are redirected to the published URL when the site is PUBLISHED. Drafts require sign-in as an editor.

3c. Ultimate HVAC v4

Current Next.js conversion of the Santa Cruz HVAC design. Does not replace production ultimate-hvac. Use templateId: ultimate-hvac-v4 for new sites.

Theme idRoleNotes
ultimate-hvac Production Montserrat / Open Sans; canonical layout
ultimate-hvac-v4 Current conversion Same default copy as production; live HTML hero + menuItems; header/hero fonts hard-coded Montserrat / Open Sans
ultimate-hvac-v1 Removed Composed-hero experiment — do not use
PathPurpose
app/magictemplates/ultimate-hvac-v4/Components + default-config.json
public/images/ultimate-hvac-v4/Assets
data/grid-configs/ultimate-hvac-v4-simple.jsonDev / seed page config
adaptUltimateHvacV4PropsAdapter in lib/themes/service-theme-config.ts
themes/ultimate-heating-cooling-nextjs/Source Next.js package
Content parity with production: menus (incl. Specials → #specials), top bar (24/7 + Financing), hero trust badges, service blurbs, about + Seasonal Tune-Up Special, named reviews, and service areas. CSS scope: .theme-ultimate-hvac-v4.

4. Key files

PathRole
types/theme.types.tsThemeManifest, tokens, CanonicalComponentType
lib/themes/canonical-components.tsShared prop interfaces
lib/themes/prop-normalizer.tsLegacy → canonical props
lib/themes/theme-registry.tsregisterTheme(), bundled loaders
lib/themes/service-theme-config.tsService industry theme list
lib/themes/nextjs-theme-upload.tsZIP extract, validate, stage
lib/themes/uploaded-theme-loader.tsRuntime load for uploads
lib/services/theme.service.tsMongoDB site_themes
app/components/theme/ThemedSiteRenderer.tsxRenders config through a theme
app/components/theme/ThemeProvider.tsxInjects --theme-* CSS vars
app/magictemplates/{slug}/Bundled theme packages
app/magictemplates/_uploaded/Staged user ZIP components
app/websites/components/UploadThemeModal.tsxUser upload UI
app/admin/themes/page.tsxAdmin global themes

5. Theme ownership

UploaderWhereVisibilityWho can use
Admin /admin/themes global Everyone
User /websites → Upload Theme private That account only
Bundled theme-registry.ts / service themes always on Everyone

Private themes return 403 to other users on list/load APIs.

6. Canonical component types

Every theme should implement as many of these as possible. Aliases are auto-normalized.

TypeAliasesTypical industry use
navbarnav, navigation, headerAll sites
heroAll sites
aboutaboutus, about-usAll sites
menumenusection, menu_sectionRestaurants
servicesHVAC, roofing, pest, electric
galleryRestaurants / portfolios
testimonialsreviewsService businesses
contactAll sites
footerAll sites
Filename convention for auto-detection on upload: Navbar.tsx, Hero.tsx, Services.tsx, About.tsx, Menu.tsx, Gallery.tsx, Testimonials.tsx, Contact.tsx, Footer.tsx (case-insensitive).

8. Prop contracts (canonical shapes)

Defined in lib/themes/canonical-components.ts. Themes may use different prop names internally if they provide adaptProps.

Responsive layout required contract

Every theme must work on mobile and desktop. Upload validation (lib/themes/theme-contract-validator.ts) rejects packages that lack viewport breakpoints when they use fixed multi-column grids.

  • Include @media (max-width: …) / @media (min-width: …) in CSS, or Tailwind sm:/md:/lg: classes, or repeat(auto-fit/auto-fill, …)
  • Collapse multi-column grids to 1fr under ~640–768px
  • Navbar should expose a mobile menu (menuToggle / hamburger / aria-expanded)
  • prefers-reduced-motion alone does not count as responsive
/* Example */
.serviceGrid { display: grid; grid-template-columns: repeat(3, 1fr); }
@media (max-width: 768px) {
  .serviceGrid { grid-template-columns: 1fr; }
  .primaryNav { display: none; }
  .primaryNav.open { display: flex; flex-direction: column; }
}

Site business fields data-field contract

Company identity is shared across the site via pageConfig.siteBusiness (lib/themes/business-fields.ts). The builder AI uses update_site_business_info to write once and fan out into navbar / hero / about / contact / footer (including tel: links). At render time, ThemedSiteRenderer overlays these fields onto every section.

Themes must mark live company details in the DOM:

<strong data-field="phone">{phone}</strong>
<a href={phoneHref} data-field="phoneHref">…</a>
<span data-field="address">{address}</span>
<span data-field="brandName">{brandName}</span>
<span data-field="companyName">{companyName}</span>
<span data-field="email">{email}</span>
<span data-field="hours">{hours}</span>
<span data-field="locationLabel">{locationLabel}</span>
siteBusiness?: {
  brandName?: string;
  companyName?: string;
  phone?: string;
  phoneHref?: string; // auto-derived from phone when omitted
  email?: string;
  address?: string;
  hours?: string;
  locationLabel?: string;
}
Do not hard-code demo phone/address only inside one section — accept props and tag them with data-field so a single AI update refreshes the whole site.

NavbarProps required contract

Navigation must be live menuItems props — never baked into hero artwork images. Legacy aliases links, navLinks, and string items are normalized to menuItems. Utility-bar copy may use topItems or the alias announcements.

interface NavbarProps {
  logo?: string;
  brandName?: string;
  /** Required for a usable header */
  menuItems: Array<{ label: string; href: string; target?: string }>;
  /** Optional announcement / utility bar lines */
  topItems?: string[];
  /** Alias for topItems */
  announcements?: string[];
  ctaText?: string;
  ctaHref?: string;
  phone?: string;
}
Example: { "component": "navbar", "props": { "brandName": "Acme", "menuItems": [{ "label": "Home", "href": "#top" }], "phone": "(555) 123-4567" } }
Empty-value pitfalls: menuItems: [] hides all nav links — treat empty arrays as missing and fall back to defaults. Hero title: "" clears the H1 (?? does not help) — use trim() || default. Seeding a site from empty manifest shells (props: {}) shows brand/image fallbacks with blank hero copy — always ship populated default-config.json with component, visible: true, and real props. CSS must target .theme-{manifest.id} (or :is(.theme-slug, .theme-package-id)); a package scoped only to a different class renders unstyled in SuperBuilder.

See §3c Ultimate HVAC v4 for the full bundled HVAC map (ultimate-hvac production vs ultimate-hvac-v4 conversion).

HeroProps

interface HeroProps {
  title: string;
  subtitle?: string;
  description?: string;
  badge?: string;
  highlightedText?: string;
  primaryButton?: { text: string; href: string; target?: string };
  secondaryButton?: { text: string; href: string; target?: string };
  backgroundImage?: string;
  image?: string;
  stats?: Array<{ value: string; label: string }>;
}

ServicesProps

interface ServicesProps {
  title?: string;
  subtitle?: string;
  services: Array<{
    title: string;
    description?: string;
    icon?: string;
    image?: string;
  }>;
}

TestimonialsProps

interface TestimonialsProps {
  title?: string;
  subtitle?: string;
  testimonials: Array<{
    quote: string;
    author: string;
    role?: string;
    avatar?: string;
  }>;
}

ContactProps / FooterProps (abbreviated)

interface ContactProps {
  title?: string; subtitle?: string;
  email?: string; phone?: string; address?: string; hours?: string;
}

interface FooterProps {
  brandName?: string; address?: string; phone?: string; email?: string;
  copyright?: string;
  socialLinks?: Array<{ platform: string; url: string; icon?: string }>;
}
Full interfaces also cover AboutProps, MenuProps, and GalleryProps in canonical-components.ts.

9. Page config example (Ultimate HVAC)

Real shape from data/grid-configs/ultimate-hvac-simple.json. Required SuperBuilder fields per section: component, visible: true, and populated props. type is an accepted alias for component, but do not ship type alone.

{
  "slug": "ultimate-hvac",
  "sections": [
    {
      "id": "section-navbar",
      "component": "navbar",
      "props": {
        "brandName": "ULTIMATE",
        "tagline": "HVAC",
        "phone": "(831) 226-1937",
        "ctaLabel": "Call Now",
        "links": [
          { "label": "Home", "href": "#top" },
          { "label": "Services", "href": "#services" },
          { "label": "Contact", "href": "#contact" }
        ]
      }
    },
    {
      "id": "section-hero",
      "component": "hero",
      "props": {
        "headline": "Heating & Cooling Experts",
        "subheadline": "Reliable comfort. Honest service. Year-round.",
        "eyebrow": "Santa Cruz's Trusted",
        "primaryCtaLabel": "Schedule Service",
        "primaryCtaHref": "#service-form",
        "badges": ["Licensed & Insured", "24/7 Emergency Service"]
      }
    },
    {
      "id": "section-services",
      "component": "services",
      "props": {
        "title": "Complete HVAC Solutions",
        "items": [
          {
            "name": "AC Services",
            "description": "Installation, repair and maintenance.",
            "image": "https://images.unsplash.com/..."
          }
        ]
      }
    }
  ]
}
Ultimate HVAC components use theme-specific names (headline, links, items). That is fine for bundled themes that own both config and components. For maximum swapability, prefer canonical names (title, menuItems, services) or map via adaptProps.

10. Theme folder layout

app/magictemplates/ultimate-hvac/ ├── theme.manifest.json # tokens + supportedComponents ├── theme-registry-snippet.ts # paste into registry or use service-theme-config ├── app/ │ └── globals.css # scoped under .theme-ultimate-hvac when possible └── components/ ├── Navbar.tsx ├── Hero.tsx ├── Services.tsx ├── About.tsx ├── Testimonials.tsx ├── Contact.tsx └── Footer.tsx

Example component (Hero)

From app/magictemplates/ultimate-hvac/components/Hero.tsx:

export default function Hero({
  headline = 'Heating & Cooling Experts',
  subheadline = 'Reliable comfort. Honest service. Year-round.',
  description = 'Professional HVAC service…',
  primaryCtaLabel = 'Schedule Service',
  primaryCtaHref = '#contact',
  eyebrow = "Santa Cruz's Trusted",
  phone = '(831) 226-1937',
  badges = ['Licensed & Insured', 'Honest Pricing'],
}: HeroProps) {
  return (
    <section className="uh-hero">
      <p className="uh-eyebrow">{eyebrow}</p>
      <h1>{headline}</h1>
      <h2>{subheadline}</h2>
      <p>{description}</p>
      {/* CTAs + trust badges */}
    </section>
  );
}

Upload ZIP layout (auto-detected)

my-theme.zip ├── package.json # must list "next" ├── app/ │ └── globals.css # :root tokens extracted └── components/ # or app/components / src/components ├── Navbar.tsx ├── Hero.tsx └── Footer.tsx

11. How rendering works

  1. Builder / published page calls DynamicTemplateRenderer with slug + page config.
  2. If a theme is registered for that slug, ThemedSiteRenderer is used (preferred path).
  3. Each section’s type is resolved (aliases → canonical).
  4. normalizeSectionProps() maps legacy shapes to canonical props.
  5. Optional theme adaptProps(type, props) remaps to component-specific props.
  6. ThemeProvider scopes design tokens as CSS variables on .theme-{slug}.
  7. Each section renders the theme’s React component for that slot.
import { ThemedSiteRenderer } from '@/app/components/theme/ThemedSiteRenderer';

<ThemedSiteRenderer
  config={pageConfig}
  themeId="ultimate-hvac"
  editable
/>
Legacy template wrappers still work as fallback when no theme is registered.

12. Prop normalizer examples

lib/themes/prop-normalizer.ts accepts legacy shapes so old configs keep working.

Navbar — string items → menuItems

Legacy input

{
  "component": "nav",
  "props": {
    "navbar": {
      "restaurantName": "Azeem Restaurant",
      "items": ["Home", "Menu", "About"],
      "action": "Book Table"
    }
  }
}

Canonical output

{
  "brandName": "Azeem",
  "menuItems": [
    { "label": "Home", "href": "#home" },
    { "label": "Menu", "href": "#menu" },
    { "label": "About", "href": "#about" }
  ],
  "ctaText": "Book Table",
  "ctaHref": "#contact"
}

Common renames

LegacyCanonical
restaurantName / companyNamebrandName
items (string[])menuItems
actionctaText
aboutText / textabout body fields
reviews section typetestimonials

13. adaptProps — theme-specific mapping

Use when your components expect different names than the canonical contract (e.g. Ultimate HVAC headline vs canonical title).

adaptProps: (type, props) => {
  if (type === 'hero') {
    return {
      headline: props.title,
      subheadline: props.subtitle,
      description: props.description,
      primaryCtaLabel: props.primaryButton?.text,
      primaryCtaHref: props.primaryButton?.href,
      eyebrow: props.badge,
    };
  }
  if (type === 'navbar') {
    return {
      brandName: props.brandName,
      links: props.menuItems,
      ctaLabel: props.ctaText,
      ctaHref: props.ctaHref,
    };
  }
  if (type === 'services') {
    return {
      title: props.title,
      items: (props.services || []).map((s) => ({
        name: s.title,
        description: s.description,
        image: s.image,
        icon: s.icon,
      })),
    };
  }
  return props;
}

14. CSS design tokens

ThemeProvider exposes these on .theme-{slug}:

CSS variableManifest source
--theme-primarytokens.colors.primary
--theme-secondarytokens.colors.secondary
--theme-accenttokens.colors.accent
--theme-backgroundtokens.colors.background
--theme-texttokens.colors.text
--theme-font-headingtokens.fonts.heading
--theme-font-bodytokens.fonts.body

Ultimate HVAC token swatches

primary#072d5f
secondary#65748a
accent#d92b28
background#ffffff
text#10233f
/* Prefer scoped styles so builder chrome is not affected */
.theme-ultimate-hvac {
  --theme-primary: #072d5f;
  --theme-accent: #d92b28;
  font-family: var(--theme-font-body);
}

.theme-ultimate-hvac .uh-btn-red {
  background: var(--theme-accent);
}
Isolate theme globals under .theme-{slug} (see TemplateIsolationWrapper) so styles do not leak into the editor chrome.

15. Adding a bundled theme

  1. Create app/magictemplates/{slug}/ with components + CSS
  2. Add theme.manifest.json and default page config
  3. Register via registerTheme() or SERVICE_THEME_SLUGS
  4. Register capabilities in lib/templates/template-registry.ts
  5. Add data/grid-configs/{slug}-simple.json
  6. Smoke-test at /builder/{slug}

Registration snippet

registerTheme('ultimate-hvac', async () => {
  const [Navbar, Hero, Services, About, Testimonials, Contact, Footer] =
    await Promise.all([
      import('@/app/magictemplates/ultimate-hvac/components/Navbar').then(m => m.default),
      import('@/app/magictemplates/ultimate-hvac/components/Hero').then(m => m.default),
      import('@/app/magictemplates/ultimate-hvac/components/Services').then(m => m.default),
      import('@/app/magictemplates/ultimate-hvac/components/About').then(m => m.default),
      import('@/app/magictemplates/ultimate-hvac/components/Testimonials').then(m => m.default),
      import('@/app/magictemplates/ultimate-hvac/components/Contact').then(m => m.default),
      import('@/app/magictemplates/ultimate-hvac/components/Footer').then(m => m.default),
    ]);

  await import('@/app/magictemplates/ultimate-hvac/app/globals.css');

  return {
    manifest: { /* from theme.manifest.json */ },
    components: {
      navbar: Navbar,
      hero: Hero,
      services: Services,
      about: About,
      testimonials: Testimonials,
      contact: Contact,
      footer: Footer,
    },
  };
});

16. Uploading themes

Upload Theme modal mockup
Figure 4 — Upload Theme modal (My Websites)

Users — Next.js ZIP (recommended)

  1. Go to My Websites (/websites) → Upload Theme
  2. ZIP the Next.js project (exclude node_modules and .next)
  3. Name the theme; optionally create a website from it

Requirements

  • Format: .zip max 50MB · single-page .html max 5MB
  • Project: package.json with next at root or one folder deep
  • Filenames: Navbar.tsx, Hero.tsx, Footer.tsx, …
  • Locations: components/, app/components/, or src/components/
  • Tokens: tailwind.config or :root in app/globals.css

What happens on upload

Before staging, the uploader validates TypeScript component sources against the prop contracts in this guide (lib/themes/theme-contract-validator.ts + canonical-components.ts). Missing Navbar / Hero / Footer, missing export default, or components that do not accept canonical props (e.g. navbar without menuItems / links / navLinks) cause a 422 rejection with actionable errors.

  1. ZIP extracted and validated as Next.js
  2. Components mapped to canonical slots
  3. Tokens extracted into a theme manifest
  4. Files staged under app/magictemplates/_uploaded/{userId}/{slug}/
  5. Full source kept in data/theme-projects/{userId}/{slug}/source/
  6. MongoDB record with visibility: private
  7. Optional site with templateId: nextjs-theme:{slug}

Admins — global themes

Use /admin/themes with a server folder path (not a browser upload).

POST /api/admin/themes
{
  "projectPath": "html-templates/my-template",
  "themeSlug": "bistro-modern",
  "themeName": "Bistro Modern"
}

17. Theme manifest schema

{
  "id": "ultimate-hvac",
  "slug": "ultimate-hvac",
  "name": "Ultimate HVAC",
  "version": "1.1.0",
  "source": "nextjs",
  "tokens": {
    "colors": {
      "primary": "#072d5f",
      "secondary": "#65748a",
      "accent": "#d92b28",
      "background": "#ffffff",
      "text": "#10233f"
    },
    "fonts": {
      "heading": "'Montserrat', sans-serif",
      "body": "'Open Sans', sans-serif"
    }
  },
  "supportedComponents": [
    "navbar", "hero", "services", "about",
    "testimonials", "contact", "footer"
  ],
  "paths": {
    "components": "components",
    "globalStyles": "app/globals.css"
  }
}

Starter file: app/themes/_theme-starter-kit/theme.json

18. APIs quick reference

POST /api/themes/upload          # multipart ZIP/HTML (private)
GET  /api/themes                 # themes available to current user
GET  /api/themes/{slug}          # manifest (access-controlled)
GET  /api/themes/{slug}/html     # single-page HTML
POST /api/admin/themes           # admin global import via projectPath
GET  /api/admin/themes           # admin list + docsUrl

# Page content (builder)
GET  /api/editor/grid/load?slug=
POST /api/editor/grid/save       # { pageConfig }
GET  /api/editor/wysiwyg/load-changes?slug=&templateId=
POST /api/editor/wysiwyg/save-changes
POST /api/editor/publish

User upload fields

POST /api/themes/upload
Content-Type: multipart/form-data

file:        (ZIP or HTML)
themeName:   Bistro Modern
themeSlug:   bistro-modern
createSite:  true
siteName:    My Restaurant
siteSlug:    my-restaurant

19. Theme author checklist

  • Components named to match canonical slots
  • At least navbar, hero, footer implemented
  • Props documented or mapped with adaptProps
  • Design tokens in manifest + CSS variables
  • Global CSS scoped under .theme-{slug}
  • default-config.json / grid config with real default content
  • Registered in theme registry (+ template capabilities if bundled)
  • Tested in /builder/{slug}: edit text, reorder, save, publish
  • ZIP excludes node_modules / .next (uploads)
  • Mobile layout verified

20. Common mistakes

MistakeFix
Hardcoding all copy inside React Read from props; put defaults in page config
Odd filenames (HeaderBar.tsx) Rename to Navbar.tsx or map in importer
Unscoped global CSS breaks builder Scope under .theme-{slug}
ZIP includes node_modules Exclude it — upload will be huge / fail
Expecting theme swap with theme-only prop names Use canonical props or adaptProps
Components don’t appear after upload in dev Hard refresh — Next must pick up staged files
menuItems: [] (empty array) Header shows logo + CTA but no nav links. Treat empty arrays as missing; fall back to defaults.
Hero title: "" (empty string) H1 disappears because ?? does not treat "" as missing. Use trim() || default.
CSS scoped to wrong class (e.g. .theme-foo vs id foo-v3) Scope under .theme-{manifest.id} — e.g. .theme-ultimate-hvac-v4
Nav baked into hero artwork PNG Always render live HTML menuItems in Navbar
Wrong / missing heading font Load Montserrat + Open Sans (same Google Fonts import as ultimate-hvac). Prefer hard-coded font-family: Montserrat, sans-serif on header/hero so ThemeProvider tokens cannot override.

Changelog

v1.4

  • Bundled theme id ultimate-hvac-v4 (renamed from v3) — production default content + Montserrat/Open Sans on header/hero
  • Removed unused ultimate-hvac-v1
  • Builder edit gate: /builder/[slug] requires owner or edit collaborator
  • Documented empty-value pitfalls and navbar menuItems contract

v1.3

  • Public HTML guide, navbar contract, prop-normalizer aliases

21. Roadmap

  • Theme picker UI in builder (swap skin without changing content)
  • ZIP upload endpoint
  • Auto-wire Next.js components into runtime loader
  • Database storage for user-uploaded themes
  • Admin vs user ownership
  • User upload UI on /websites
  • Builder edit access gate
  • Remove legacy *Template.tsx wrappers