Build a flexible theme system for MyBiz.Fit tenant landing pages so each SMB gets a unique look (colors, palette) while sharing the same component structure. The system must be:
- YAML-driven β no web admin, just edit files
- Preset + override β tenants pick a theme preset, can override specific tokens
- Agent-friendly β a prompt template lets coding agents generate tenant configs
- Light + dark β support both modes across 8 curated presets
mybiz/
βββ data/
β βββ themes/ β 8 theme presets (NEW)
β β βββ dark-amber.yaml
β β βββ dark-ocean.yaml
β β βββ dark-forest.yaml
β β βββ dark-rose.yaml
β β βββ light-minimal.yaml
β β βββ light-ocean.yaml
β β βββ light-warm.yaml
β β βββ light-lavender.yaml
β βββ templates/ β 8 business-type presets (NEW)
β β βββ restaurant.yaml
β β βββ salon.yaml
β β βββ contractor.yaml
β β βββ dental.yaml
β β βββ real-estate.yaml
β β βββ fitness.yaml
β β βββ retail.yaml
β β βββ professional.yaml
β βββ tenants/ β per-tenant configs (EXISTING)
β βββ rgresidences.yaml
βββ prompts/
β βββ tenant-config.yaml β coding agent prompt template (NEW)
βββ layouts/
β βββ _default/
β β βββ baseof.html β MODIFY: inject CSS vars from theme
β β βββ list.html β MODIFY: load theme, resolve accent
β βββ index.html β NO CHANGE (homepage uses brand theme)
βββ static/css/
β βββ landing.css β MODIFY: replace hardcoded colors
βββ hugo.yaml
Each theme YAML defines a complete color palette:
name: "Dark Amber"
mode: "dark"
colors:
bg: "#06070b"
bg2: "#0a0c13"
panel: "rgba(255,255,255,0.03)"
line: "rgba(255,255,255,0.09)"
text: "#eceef5"
muted: "#8d93a6"
dim: "#5d6273"
accent: "#ffb000"
accent2: "#ffcf57"
accentRGB: "255,176,0"The accentRGB field is needed because CSS rgba() cannot accept hex values. Hugo has no built-in hex-to-RGB conversion, so we store the RGB triplet as a string for use in rgba(var(--accent-rgb), 0.x) patterns.
| Preset | Mode | Accent | Mood |
|---|---|---|---|
dark-amber |
dark | #ffb000 |
Warm, premium (current default) |
dark-ocean |
dark | #00b4d8 |
Tech, modern, cool |
dark-forest |
dark | #2ee6a6 |
Natural, fresh, eco |
dark-rose |
dark | #ff6b9d |
Elegant, feminine, beauty |
light-minimal |
light | #1a1a2e |
Clean, minimal, corporate |
light-ocean |
light | #0077b6 |
Bright, professional, trust |
light-warm |
light | #e07a5f |
Friendly, approachable, local |
light-lavender |
light | #7b2cbf |
Creative, artistic, boutique |
Light themes swap the dark/light values:
# Example: light-ocean
colors:
bg: "#f8f9fa" # was dark, now light
bg2: "#ffffff" # was darker, now white
panel: "rgba(0,0,0,0.03)" # was white alpha, now black alpha
line: "rgba(0,0,0,0.09)" # was white alpha, now black alpha
text: "#1a1a2e" # was light, now dark
muted: "#4a5568" # was light-muted, now dark-muted
dim: "#a0aec0" # was dark-dim, now light-dim
accent: "#0077b6"
accent2: "#00a8e8"
accentRGB: "0,119,182"Each template extends the tenant YAML schema with sensible defaults for a specific business type. Templates are NOT code β they're reference YAML files that get copied and customized.
# Template metadata
_template: true
_templateName: "Restaurant"
_templateDescription: "For restaurants, cafes, food trucks, and catering businesses"
# ββ Defaults for this business type ββ
theme: "dark-amber"
accent: null
business:
name: null
tagline: "Fresh Food, Made with Love"
description: "Tell customers about your restaurant..."
heroImage: null
sections:
hero: true
services: true # β mapped to "Menu Categories"
video: false
gallery: true
about: true
contact: true
testimonials: true
branches: false
products: false
cta: true
services:
- name: "Dine-In"
description: "Enjoy our cozy atmosphere"
icon: "π½οΈ"
- name: "Takeout"
description: "Quick pickup, same great taste"
icon: "π₯‘"
- name: "Catering"
description: "Events and parties"
icon: "π"
cta:
text: "Reserve Your Table"
button: "Chat with Us"
action: "chat"| Template | Default Sections | Unique Defaults |
|---|---|---|
restaurant |
hero, services, gallery, about, contact, testimonials, cta | Services β menu categories, CTA β "Reserve Your Table" |
salon |
hero, services, gallery, about, contact, testimonials, cta | Services β hair/nail/spa, CTA β "Book an Appointment" |
contractor |
hero, services, about, contact, testimonials, cta | Services β plumbing/electrical/remodel, CTA β "Get a Free Quote" |
dental |
hero, services, about, contact, testimonials, cta | Services β cleanings/whitening/ortho, CTA β "Schedule a Visit" |
real-estate |
hero, services, gallery, about, contact, branches, cta | Services β buying/selling/renting, branches β office locations |
fitness |
hero, services, video, about, contact, testimonials, cta | Services β classes/training/equipment, CTA β "Start Free Trial" |
retail |
hero, products, gallery, about, contact, cta | products β product catalog, gallery β product photos |
professional |
hero, services, about, contact, testimonials, cta | Services β consulting/auditing/strategy, CTA β "Book a Consultation" |
Single YAML file per tenant. References a theme by name, can override specific tokens.
# ββ Theme ββ
theme: "dark-amber"
accent: null # null = use theme default, or hex to override
# ββ Business Info ββ
business:
name: "string"
tagline: "string"
description: "string"
heroImage: "/path/to/image.jpg"
# ββ Section Toggles ββ
sections:
hero: true
services: true
video: false
gallery: false
about: true
contact: true
testimonials: false
branches: false
products: false
cta: true
# ββ Section Data (only needed if section is true) ββ
services: [...]
video: {...}
gallery: [...]
about: {...}
branches: [...]
products: [...]
cta: {...}
contact: {...}
testimonials: [...]
# ββ Chat Config ββ
chat:
welcome: "Hi! How can I help you today?"
companyName: null # null = use business.nameThe existing rgresidences.yaml uses the old schema (top-level accent, no theme: key). The template update will:
- Add
theme: "dark-amber"field - Add
chat:block withwelcomeandcompanyName - Keep all existing data fields unchanged
The current CSS hardcodes rgba(255,176,0,...) in 15 places. When a tenant picks a different accent color, these won't update.
- Add
accentRGBto each theme preset (e.g.,"255,176,0") - In
baseof.html: inject an inline<style>block that sets all CSS variables from the resolved theme - In
landing.css: replace all 15 hardcodedrgba(255,176,0,...)withrgba(var(--accent-rgb), ...)
{{ if or (eq .Params.layout "landing") (eq .Params.layout "home") }}
<style>
:root {
--bg: {{ $theme.colors.bg }};
--bg2: {{ $theme.colors.bg2 }};
--panel: {{ $theme.colors.panel }};
--line: {{ $theme.colors.line }};
--text: {{ $theme.colors.text }};
--muted: {{ $theme.colors.muted }};
--dim: {{ $theme.colors.dim }};
--accent: {{ $accent }};
--accent2: {{ $accent2 }};
--accent-rgb: {{ $accentRGB }};
}
body { background: var(--bg); color: var(--text); }
</style>
{{ end }}Note: The inline <style> must appear BEFORE the <link rel="stylesheet" href="/css/landing.css"> so that the CSS file can use the variables. Actually β CSS custom properties are live, so the <style> can appear anywhere in <head> and the CSS file will pick them up. But placing it before the stylesheet is cleaner.
All 15 occurrences of rgba(255,176,0,...) become rgba(var(--accent-rgb), ...):
| Line | Current | Replacement |
|---|---|---|
| 48 | rgba(255,176,0,0.08) |
rgba(var(--accent-rgb),0.08) |
| 49 | rgba(255,176,0,0.2) |
rgba(var(--accent-rgb),0.2) |
| 115 | rgba(255,176,0,0.3) |
rgba(var(--accent-rgb),0.3) |
| 194 | rgba(255,176,0,0.35) |
rgba(var(--accent-rgb),0.35) |
| 253 | rgba(255,176,0,0.1) |
rgba(var(--accent-rgb),0.1) |
| 253 | rgba(255,176,0,0.02) |
rgba(var(--accent-rgb),0.02) |
| 254 | rgba(255,176,0,0.15) |
rgba(var(--accent-rgb),0.15) |
| 255 | rgba(255,176,0,0.15) |
rgba(var(--accent-rgb),0.15) |
| 619 | rgba(255,176,0,0.06) |
rgba(var(--accent-rgb),0.06) |
| 775 | rgba(255,176,0,0.4) |
rgba(var(--accent-rgb),0.4) |
| 776 | rgba(255,176,0,0.2) |
rgba(var(--accent-rgb),0.2) |
| 874 | rgba(255,176,0,0.2) |
rgba(var(--accent-rgb),0.2) |
| 1013 | rgba(255,176,0,0.12) |
rgba(var(--accent-rgb),0.12) |
| 1013 | rgba(255,176,0,0.02) |
rgba(var(--accent-rgb),0.02) |
| 1014 | rgba(255,176,0,0.15) |
rgba(var(--accent-rgb),0.15) |
| 1015 | rgba(255,176,0,0.15) |
rgba(var(--accent-rgb),0.15) |
| 1122 | rgba(255,176,0,0.2) |
rgba(var(--accent-rgb),0.2) |
Also remove the hardcoded :root variable defaults from landing.css (lines 1-14) since they'll be injected by the template. Keep them as fallback for development/testing only.
At the top of the landing block (after line 3), add theme loading:
{{/* ββ Theme Resolution ββ */}}
{{ $themeName := $tenant.theme | default "dark-amber" }}
{{ $theme := index site.Data.themes $themeName }}
{{ $accent := $tenant.accent | default $theme.colors.accent }}
{{ $accent2 := $tenant.accent2 | default $theme.colors.accent2 }}
{{ $accentRGB := $theme.colors.accentRGB | default "255,176,0" }}Replace the old $accent resolution (line 4):
{{/* OLD */}}
{{ $accent := $tenant.accent | default "#ff5733" }}
{{/* NEW β removed, replaced above */}}Also update the chat widget config to use $tenant.chat.welcome with fallback:
{{ $chatWelcome := $tenant.chat.welcome | default "Hi! How can I help you today?" }}
{{ $chatCompany := $tenant.chat.companyName | default $tenant.business.name }}Add the inline <style> block inside the <head> for landing/home layouts, BEFORE the CSS link. The variables $theme, $accent, $accent2, $accentRGB must be passed from the content page context β but baseof.html doesn't have access to tenant data.
Solution: Use Hugo's .Scratch or define blocks. The cleanest approach: define the variables in list.html using block "head" and override the head in baseof.html.
Actually, the simpler approach: inject the CSS variables in list.html right after the <body> tag, not in baseof.html. Since list.html defines the "main" block, we can add an inline style at the start of the landing layout:
{{ define "main" }}
{{ if eq .Params.layout "landing" }}
{{/* ... theme resolution ... */}}
<style>:root { --accent: {{ $accent }}; ... }</style>
{{/* ... rest of template ... */}}
{{ end }}
{{ end }}This is cleaner because all theme logic stays in list.html where tenant data is available.
A YAML template that coding agents fill in to generate tenant configs.
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# MyBiz.Fit Tenant Configuration Generator
# Instructions: Fill in each field. Replace null with actual values.
# Agents: Read this file as your template, output a valid tenant YAML.
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# ββ STEP 1: Business Type ββ
# Choose one: restaurant | salon | contractor | dental | real-estate | fitness | retail | professional
# This determines which sections are enabled by default.
businessType: null
# ββ STEP 2: Theme ββ
# Choose one:
# Dark: dark-amber | dark-ocean | dark-forest | dark-rose
# Light: light-minimal | light-ocean | light-warm | light-lavender
theme: null
# ββ STEP 3: Accent Override (optional) ββ
# null = use theme default. Set a hex color to override.
# Example: "#e63946"
accent: null
# ββ STEP 4: Business Information ββ
business:
name: null # Business name (required)
tagline: null # Short headline for hero (e.g., "Fresh Pizza, Delivered Fast")
description: null # 1-2 sentence description of the business
heroImage: null # Path to hero image in /static/ (e.g., "/joes-pizza/images/hero.jpg")
# ββ STEP 5: Section Toggles ββ
# Set each to true/false based on what the business needs.
sections:
hero: true
services: true # Menu items, service offerings, etc.
video: false # YouTube embed
gallery: false # Photo gallery
about: true # About section
contact: true # Contact info cards
testimonials: false # Customer reviews
branches: false # Multiple locations
products: false # Product catalog with images
cta: true # Call-to-action banner
# ββ STEP 6: Section Data ββ
# Only fill in sections that are set to true above.
# Services / Menu Items / Offerings
services: []
# - name: "Service Name"
# description: "Short description"
# icon: "π§"
# Video (YouTube)
# video:
# youtubeId: "dQw4w9WgXcQ" # 11-character YouTube video ID
# title: "Video Title"
# Photo Gallery (array of image paths)
gallery: []
# - "/business-name/images/photo1.jpg"
# About Section
# about:
# title: "About Our Business"
# description: "Longer description of the business..."
# image: "/business-name/images/about.jpg"
# Branches / Locations
branches: []
# - name: "Main Location"
# address: "123 Main St, City, State ZIP"
# phone: "(555) 123-4567"
# mapUrl: "https://maps.google.com/?q=123+Main+St"
# Products (with images)
products: []
# - name: "Product Name"
# price: "$29.99"
# description: "Product description"
# image: "/business-name/images/product1.jpg"
# Call-to-Action
cta:
text: null # CTA headline (e.g., "Ready to Order?")
button: "Chat with Us" # Button label
action: "chat" # "chat" = opens widget, or a URL
# Contact Information
contact:
address: null
phone: null
email: null
hours: null # e.g., "Mon-Fri 9am-5pm, Sat 10am-2pm"
# Customer Reviews
testimonials: []
# - name: "Customer Name"
# text: "What they said..."
# rating: 5
# ββ STEP 7: Chat Widget Config ββ
chat:
welcome: "Hi! How can I help you today?"
companyName: null # null = use business.name
# ββ STEP 8: Special Requests ββ
# Free-form narrative for anything not covered above.
# Use this for: custom fonts, specific layout changes, unique sections,
# brand guidelines, or any other special requirements.
specialRequests: |
null| # | Task | Files | Effort | Dependencies |
|---|---|---|---|---|
| 1 | Create 8 theme presets | data/themes/*.yaml |
Small | None |
| 2 | Create 8 business templates | data/templates/*.yaml |
Medium | None |
| 3 | Refactor landing.css β replace hardcoded colors |
static/css/landing.css |
Small | None |
| 4 | Update list.html β theme loading + CSS var injection |
layouts/_default/list.html |
Medium | Steps 1, 3 |
| 5 | Update baseof.html β no changes needed (list.html handles it) |
β | Skip | β |
| 6 | Update rgresidences.yaml β add theme field |
data/tenants/rgresidences.yaml |
Tiny | Step 1 |
| 7 | Create prompt template | prompts/tenant-config.yaml |
Small | Steps 1, 2 |
| 8 | Test build | β | Medium | All |
- Run
hugo serverinmybiz/ - Verify homepage renders with default brand theme (unchanged)
- Verify
rgresidencestenant page renders withdark-ambertheme - Temporarily change
rgresidences.yamltotheme: "dark-ocean"β verify accent changes - Temporarily change to
theme: "light-ocean"β verify light mode renders correctly - Test accent override: set
accent: "#ff0000"β verify it overrides the theme - Verify all 8 theme presets render without CSS errors
- Verify chat widget picks up the tenant accent color
- Pick a business template:
cp data/templates/restaurant.yaml data/tenants/joes-pizza.yaml - Edit the YAML: fill in business name, description, services, contact info
- Pick a theme:
theme: "dark-amber"(or any preset) - Run
hugoβ page is live atmybiz.fit/joes-pizza
- Receive user's business description
- Read
prompts/tenant-config.yamlas instructions - Auto-fill all fields based on business type + user preferences
- Write tenant YAML + content
_index.md - Done β page is live
- Chat widget collects business info
- You (or an agent) generate the tenant config from the prompt template
- Tenant gets their link:
mybiz.fit/<their-business>