{safe}
{esc(service_copy(spec, item))}
#!/usr/bin/env python3
"""Website harness: compact spec in, complete HTML out.
This deliberately avoids asking the model to write an entire website. The
model, if used, should only produce a small JSON spec. This renderer owns the
HTML structure, closing tags, responsive CSS, image policy, and validation.
"""
from __future__ import annotations
import html
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
IMAGE_BANK: dict[str, list[str]] = {
"barber": [
"https://images.unsplash.com/photo-1585747860715-2ba37e788b70?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1621605815971-fbc98d665033?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1503951914875-452162b0f3f1?auto=format&fit=crop&w=900&q=80",
],
"bakery": [
"https://images.unsplash.com/photo-1509440159596-0249088772ff?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1517433670267-08bbd4be890f?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1555507036-ab1f4038808a?auto=format&fit=crop&w=900&q=80",
],
"food": [
"https://images.unsplash.com/photo-1562967914-608f82629710?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1604908176997-125f25cc6f3d?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1550547660-d9450f859349?auto=format&fit=crop&w=900&q=80",
],
"contractor": [
"https://images.unsplash.com/photo-1504307651254-35680f356dfd?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1541888946425-d81bb19240f5?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1503387762-592deb58ef4e?auto=format&fit=crop&w=900&q=80",
],
"bookkeeping": [
"https://images.unsplash.com/photo-1554224155-6726b3ff858f?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1554224154-26032ffc0d07?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?auto=format&fit=crop&w=900&q=80",
],
"roofing": [
"https://images.unsplash.com/photo-1632759145351-1d592919f522?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1504307651254-35680f356dfd?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=900&q=80",
],
"cleaning": [
"https://images.unsplash.com/photo-1581578731548-c64695cc6952?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1527515637462-cff94eecc1ac?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1603712725038-e9334ae8f39f?auto=format&fit=crop&w=900&q=80",
],
"fitness": [
"https://images.unsplash.com/photo-1517836357463-d25dfeac3438?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1540497077202-7c8a3999166f?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1534368420009-621bfab424a8?auto=format&fit=crop&w=900&q=80",
],
"salon": [
"https://images.unsplash.com/photo-1522335789203-aabd1fc54bc9?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1560066984-138dadb4c035?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1521590832167-7bcbfaa6381f?auto=format&fit=crop&w=900&q=80",
],
"restaurant": [
"https://images.unsplash.com/photo-1555396273-367ea4eb4db5?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1544148103-0773bf10d330?auto=format&fit=crop&w=900&q=80",
],
"real_estate": [
"https://images.unsplash.com/photo-1560518883-ce09059eeffa?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=900&q=80",
],
"pets": [
"https://images.unsplash.com/photo-1516734212186-a967f81ad0d7?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1525253013412-55c1a69a5738?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1548199973-03cce0bbc87b?auto=format&fit=crop&w=900&q=80",
],
"auto": [
"https://images.unsplash.com/photo-1607860108855-64acf2078ed9?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1503376780353-7e6692767b70?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1525609004556-c46c7d6cf023?auto=format&fit=crop&w=900&q=80",
],
"landscaping": [
"https://images.unsplash.com/photo-1558904541-efa843a96f01?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1416879595882-3373a0480b5b?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1585320806297-9794b3e4eeae?auto=format&fit=crop&w=900&q=80",
],
"legal": [
"https://images.unsplash.com/photo-1589829545856-d10d557cf95f?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1450101499163-c8848c66ca85?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1505664194779-8beaceb93744?auto=format&fit=crop&w=900&q=80",
],
"plumbing": [
"https://images.unsplash.com/photo-1607472586893-edb57bdc0e39?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1585704032915-c3400ca199e7?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1621905252507-b35492cc74b4?auto=format&fit=crop&w=900&q=80",
],
"electrical": [
"https://images.unsplash.com/photo-1621905251189-08b45d6a269e?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1581092160607-ee22621dd758?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1621905252507-b35492cc74b4?auto=format&fit=crop&w=900&q=80",
],
"med_spa": [
"https://images.unsplash.com/photo-1570172619644-dfd03ed5d881?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1512290923902-8a9f81dc236c?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?auto=format&fit=crop&w=900&q=80",
],
"dental": [
"https://images.unsplash.com/photo-1606811971618-4486d14f3f99?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1629909613654-28e377c37b09?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1587314919482-0f8f6f8d9d5d?auto=format&fit=crop&w=900&q=80",
],
"education": [
"https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1513258496099-48168024aec0?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1503676260728-1c00da094a0b?auto=format&fit=crop&w=900&q=80",
],
"default": [
"https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=1400&q=80",
"https://images.unsplash.com/photo-1556761175-b413da4baf72?auto=format&fit=crop&w=900&q=80",
"https://images.unsplash.com/photo-1556761175-4b46a572b786?auto=format&fit=crop&w=900&q=80",
],
}
PALETTES: dict[str, dict[str, str]] = {
"gold": {"ink": "#11100d", "paper": "#fff8ea", "accent": "#f4ad32", "card": "#ffffff"},
"blue": {"ink": "#07111f", "paper": "#f4f9ff", "accent": "#38bdf8", "card": "#ffffff"},
"green": {"ink": "#07180f", "paper": "#f4fff7", "accent": "#22c55e", "card": "#ffffff"},
"rose": {"ink": "#1d1017", "paper": "#fff5f8", "accent": "#fb7185", "card": "#ffffff"},
"slate": {"ink": "#101827", "paper": "#f8fafc", "accent": "#f59e0b", "card": "#ffffff"},
}
LAYOUT_BY_TYPE: dict[str, str] = {
"auto": "kinetic",
"fitness": "kinetic",
"landscaping": "gallery-forward",
"med_spa": "editorial",
"salon": "editorial",
"bakery": "editorial",
"food": "editorial",
"restaurant": "editorial",
"roofing": "proof",
"plumbing": "proof",
"electrical": "proof",
"contractor": "proof",
"bookkeeping": "proof",
"legal": "proof",
"dental": "proof",
}
@dataclass
class WebsiteSpec:
business_name: str
business_type: str
location: str = "Atlanta"
headline: str = "Professional work without the runaround."
subheadline: str = "Clear pricing, polished execution, and a result customers can trust."
cta: str = "Book now"
services: list[str] = field(default_factory=list)
sections: list[str] = field(default_factory=list)
testimonials: list[str] = field(default_factory=list)
hours: str = "Mon-Fri 9am-6pm; Saturday 10am-3pm; Sunday closed"
contact_phone: str = "(404) 555-0199"
contact_email: str = "hello@example.com"
palette: str = "gold"
image_urls: list[str] = field(default_factory=list)
def slugify(value: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") or "site"
def clean_text(value: Any, fallback: str) -> str:
if not isinstance(value, str):
return fallback
value = re.sub(r"\s+", " ", value).strip()
return value or fallback
def title_case_service(value: str) -> str:
return clean_text(value, "Service").strip(" .").title()
def infer_business_name(prompt: str) -> str:
patterns = [
r"(?:website|site|landing page|homepage)\s+for\s+([A-Z][A-Za-z0-9 &'&-]{2,80}?)(?:\.|,|\s+at\s+|\s+with|\s+include|\s+that|\s+put|\s+and\s+put|$)",
r"for\s+([A-Z][A-Za-z0-9 &'&-]{2,80}?)(?:\.|,|\s+at\s+|\s+with|\s+include|\s+that|\s+put|\s+and\s+put|$)",
r"(?:food\s+business|business|company|firm|restaurant|clinic|shop),\s+([A-Z][A-Za-z0-9 &'&-]{2,80}?)(?:\.|,|\s+with|\s+have|\s+include|\s+for|\s+that|$)",
r"(?:business|company|firm|restaurant|clinic|shop)\s+named\s+([A-Z][A-Za-z0-9 &'&-]{2,80}?)(?:\.|,|\s+with|\s+have|\s+include|\s+for|\s+that|$)",
r"(?:business|company|firm|restaurant|clinic|shop)\s+called\s+([A-Z][A-Za-z0-9 &'&-]{2,80}?)(?:\.|,|\s+with|\s+have|\s+include|\s+for|\s+that|$)",
]
for pattern in patterns:
match = re.search(pattern, prompt)
if match:
candidate = clean_text(match.group(1), "Local Business").rstrip(".")
if not re.search(r"\bfolder\b|\bdirectory\b|\bdesktop\b", candidate, re.IGNORECASE):
return candidate
return "Local Business"
def infer_business_type(prompt: str) -> str:
lower = prompt.lower()
choices = [
("food", ["food business", "fried chicken", "chicken", "chickens", "wings", "family meals", "food truck"]),
("restaurant", ["restaurant", "omakase", "dining", "menu", "cuisine", "reserve a table", "reservation"]),
("barber", ["barber", "skin fade", "beard", "haircut"]),
("bakery", ["bakery", "baked", "pastry", "croissant", "cake"]),
("roofing", ["roof", "roofing", "gutter", "storm"]),
("contractor", ["contractor", "remodel", "construction", "hvac", "repair"]),
("bookkeeping", ["bookkeeping", "bookkeeper", "payroll", "accounting", "accountant", "financial reporting"]),
("cleaning", ["cleaning", "maid", "janitorial"]),
("fitness", ["fitness", "gym", "trainer"]),
("med_spa", ["med spa", "medical spa", "botox", "injectables", "facial", "skincare", "skin care"]),
("salon", ["salon", "esthetician", "hair stylist", "hair studio"]),
("real_estate", ["real estate", "realtor", "homes", "property"]),
("pets", ["pet", "dog", "groomer", "veterinary", "vet"]),
("auto", ["auto", "car", "detailer", "mechanic", "repair shop"]),
("landscaping", ["landscaping", "lawn", "garden", "outdoor"]),
("legal", ["law", "lawyer", "attorney", "legal"]),
("plumbing", ["plumber", "plumbing", "pipe", "drain", "water heater"]),
("electrical", ["electrician", "electrical", "breaker", "panel", "wiring"]),
("dental", ["dental", "dentist", "dentistry", "cleanings", "cosmetic dentistry"]),
("education", ["tutor", "tutoring", "school", "course", "teacher", "education"]),
]
for business_type, terms in choices:
if any(term_matches(lower, term) for term in terms):
return business_type
return "default"
def infer_location(prompt: str) -> str:
city_state = re.search(r"\b([A-Z][A-Za-z .'-]{2,40}),\s*([A-Z]{2})(?:\s+\d{5})?\b", prompt)
if city_state:
return f"{clean_text(city_state.group(1), 'Atlanta')}, {city_state.group(2)}"
in_city = re.search(r"\bin\s+([A-Z][A-Za-z .'-]{2,40})(?:\.|,|\s+with|\s+include|\s+for|\s+that|$)", prompt)
if in_city:
candidate = clean_text(in_city.group(1), "Atlanta").rstrip(".")
if not re.search(r"\bfolder\b|\bdesktop\b|\bsection\b", candidate, re.IGNORECASE):
return candidate
return "Atlanta"
def contact_phone_for_location(location: str) -> str:
lower = location.lower()
area_codes = [
("san francisco", "415"),
("austin", "512"),
("boston", "617"),
("charlotte", "704"),
("new york", "212"),
("los angeles", "323"),
("chicago", "312"),
("miami", "305"),
("atlanta", "404"),
]
for city, code in area_codes:
if city in lower:
return f"({code}) 555-0199"
state_codes = {
" ca": "415",
" tx": "512",
" ma": "617",
" nc": "704",
" ny": "212",
" fl": "305",
" ga": "404",
}
for marker, code in state_codes.items():
if lower.endswith(marker) or f",{marker}" in lower:
return f"({code}) 555-0199"
return "(404) 555-0199"
def term_matches(lower_text: str, term: str) -> bool:
if " " in term:
return term in lower_text
return re.search(rf"\b{re.escape(term)}\b", lower_text) is not None
def infer_services(prompt: str, business_type: str) -> list[str]:
defaults = {
"barber": ["Signature Cuts", "Skin Fades", "Beard Trims", "Hot Towel Shaves"],
"bakery": ["Fresh Pastries", "Custom Cakes", "Coffee Bar", "Catering Boxes"],
"food": ["Signature Chicken", "Family Meals", "Catering Trays", "Sides & Drinks"],
"roofing": ["Roof Repair", "Full Replacement", "Storm Inspection", "Gutter Service"],
"contractor": ["Repairs", "Installations", "Project Planning", "Emergency Help"],
"bookkeeping": ["Monthly Bookkeeping", "Payroll Support", "Financial Reporting", "Cleanup Projects"],
"cleaning": ["Deep Cleaning", "Recurring Service", "Move-Out Cleaning", "Office Cleaning"],
"fitness": ["Strength Training", "Nutrition Coaching", "Mobility Work", "Accountability"],
"salon": ["Signature Service", "Treatment Plans", "Consultations", "Aftercare"],
"restaurant": ["Daily Specials", "Catering", "Private Events", "Online Orders"],
"real_estate": ["Home Valuation", "Buyer Guidance", "Listing Strategy", "Relocation Help"],
"pets": ["Full Grooming", "Bath & Brush", "Nail Trim", "Puppy Care"],
"auto": ["Interior Detail", "Exterior Wash", "Paint Protection", "Maintenance Check"],
"landscaping": ["Lawn Care", "Seasonal Cleanup", "Planting Plans", "Hardscape Support"],
"legal": ["Consultation", "Document Review", "Business Counsel", "Estate Planning"],
"plumbing": ["Leak Repair", "Drain Cleaning", "Water Heaters", "Emergency Service"],
"electrical": ["Panel Upgrades", "Lighting Install", "Troubleshooting", "Emergency Repairs"],
"med_spa": ["Facials", "Injectables", "Skin Treatments", "Consultations"],
"dental": ["Preventive Cleanings", "Cosmetic Dentistry", "Emergency Visits", "Insurance Guidance"],
"education": ["One-on-One Tutoring", "Test Prep", "Homework Support", "Progress Plans"],
"default": ["Consultation", "Planning", "Delivery", "Support"],
}
lower = prompt.lower()
package_match = re.search(r"(?:service\s+)?packages?\s+with\s+prices?\s*\(([^)]{20,220})\)", prompt, re.IGNORECASE)
if package_match:
packages = []
for item in re.split(r",|;", package_match.group(1)):
name = re.sub(r"\$[\d,]+(?:/mo)?", "", item, flags=re.IGNORECASE).strip(" -:")
if len(name) > 2:
packages.append(title_case_service(name))
if len(packages) >= 3:
return packages[:4]
if business_type == "restaurant":
simple_menu_match = re.search(
r"menu highlights with\s+(.+?)(?:,\s*omakase|,\s*reservation|,\s*hours|\.|$)",
prompt,
re.IGNORECASE | re.DOTALL,
)
if simple_menu_match:
dishes = []
normalized_menu = re.sub(r"\s+and\s+", ", ", simple_menu_match.group(1))
for item in re.split(r",|;", normalized_menu):
cleaned = re.sub(r"\([^)]*\)|\$[\d,]+", "", item).strip(" -:")
if len(cleaned) > 3:
dishes.append(title_case_service(cleaned))
if len(dishes) >= 3:
return dishes[:4]
parenthetical_dishes = []
for match in re.finditer(r"([A-Z][A-Za-z0-9 '&-]{3,60}?)\s*\(", prompt):
dish = title_case_service(match.group(1))
if len(dish) > 3 and dish.lower() not in {"hours", "location", "phone", "email"}:
parenthetical_dishes.append(dish)
if len(parenthetical_dishes) >= 3:
return parenthetical_dishes[:4]
bullet_dishes = []
for match in re.finditer(r"^\s*-\s*([A-Z][^\n(]+?)(?:\s*\(|$)", prompt, re.MULTILINE):
dish = title_case_service(match.group(1))
if len(dish) > 3 and dish.lower() not in {"hours", "address", "phone", "email"}:
bullet_dishes.append(dish)
if len(bullet_dishes) >= 3:
return bullet_dishes[:4]
menu_match = re.search(r"(?:items\s+like|include\s+items\s+like|signature\s+dishes(?:\s+with\s+prices)?)[^:]*:\s*([^.]*)", prompt, re.IGNORECASE)
if menu_match:
dishes = []
for item in re.split(r",|;", menu_match.group(1)):
name = re.sub(r"\([^)]*\)|\$[\d,]+", "", item).strip(" -:")
if len(name) > 3:
dishes.append(title_case_service(name))
if len(dishes) >= 3:
return dishes[:4]
if business_type == "bakery" and any(term in lower for term in ["weekly specials", "catering", "order-ahead", "order ahead"]):
bakery_services = ["Fresh Pastries"]
if "weekly specials" in lower:
bakery_services.append("Weekly Specials")
if "catering" in lower:
bakery_services.append("Catering Boxes")
if "cake" in lower or "cakes" in lower:
bakery_services.append("Custom Cakes")
if len(bakery_services) < 4:
bakery_services.append("Coffee Bar")
return bakery_services[:4]
if business_type == "contractor" and re.search(r"project sectors?", lower):
return ["Ground-Up Construction", "Tenant Improvements", "Industrial Buildouts", "Preconstruction Planning"]
if "services" in lower:
after = re.split(r"services[:\s]", prompt, flags=re.IGNORECASE, maxsplit=1)
if len(after) == 2:
candidates = re.split(r",|;|\band\b", after[1])[:6]
cleaned = [title_case_service(item) for item in candidates if len(item.strip()) > 3]
if len(cleaned) >= 3:
return cleaned[:4]
return defaults.get(business_type, defaults["default"])
def infer_sections(prompt: str) -> list[str]:
lower = prompt.lower()
sections = ["hero", "services", "pricing", "hours", "testimonials", "contact"]
optional = [
("gallery", ["gallery", "pictures", "photos", "images", "before-and-after"]),
("faq", ["faq", "questions"]),
("service_area", ["service area", "areas served"]),
("trust", ["trust", "badges", "proof", "licensed", "insured"]),
("contact_form", ["contact form", "form"]),
]
for section, terms in optional:
if any(term in lower for term in terms) and section not in sections:
sections.append(section)
return sections
def infer_palette(prompt: str, business_type: str) -> str:
lower = prompt.lower()
if "blue" in lower:
return "blue"
if "green" in lower:
return "green"
if "pink" in lower or "rose" in lower:
return "rose"
if business_type in {"salon", "med_spa"}:
return "rose"
if business_type in {"fitness", "contractor", "roofing", "legal", "real_estate", "plumbing", "electrical", "education", "dental"}:
return "blue"
if business_type in {"cleaning", "landscaping", "pets"}:
return "green"
return "gold"
def infer_cta(prompt: str, business_type: str) -> str:
lower = prompt.lower()
quoted = re.search(r"['\"]([A-Za-z][A-Za-z0-9 &'/-]{2,50})['\"]\s+CTA", prompt)
if quoted:
return clean_text(quoted.group(1), "Get started").rstrip(" .")
if "reserve a table" in lower or "reservation" in lower:
return "Reserve a Table"
if "cleanup-call" in lower or "cleanup call" in lower:
return "Book a Cleanup Call"
if "bid-request" in lower or "bid request" in lower:
return "Request a Bid"
if "order-ahead" in lower or "order ahead" in lower or "online order" in lower:
return "Order Ahead"
match = re.search(
r"\bCTA\s+(?:is\s+|should\s+be\s+|button\s+is\s+)?[\"']?([A-Za-z][A-Za-z0-9 &'/-]{2,50}?)[\"']?(?:[.!?;,]|$)",
prompt,
)
if match:
candidate = clean_text(match.group(1), "Get started").rstrip(" .")
candidate_lower = candidate.lower()
if (
candidate_lower not in {"button", "section", "call", "contact"}
and not candidate_lower.startswith("and ")
and not any(term in candidate_lower for term in ("testimonial", "price section", "hero section"))
):
return candidate
if business_type == "food":
return "Order Now"
if business_type == "barber":
return "Book a chair"
if "emergency" in prompt.lower():
return "Request emergency help"
return "Get started"
def spec_from_prompt(prompt: str) -> WebsiteSpec:
business_type = infer_business_type(prompt)
name = infer_business_name(prompt)
location = infer_location(prompt)
services = infer_services(prompt, business_type)
cta = infer_cta(prompt, business_type)
return WebsiteSpec(
business_name=name,
business_type=business_type,
location=location,
headline=headline_for(name, business_type),
subheadline=subheadline_for(business_type),
cta=cta,
services=services,
sections=infer_sections(prompt),
testimonials=[
"Fast, clear, and professional from the first message.",
"The work looked better than expected and the process was simple.",
"I knew what was happening at every step.",
],
palette=infer_palette(prompt, business_type),
contact_phone=contact_phone_for_location(location),
)
def headline_for(name: str, business_type: str) -> str:
options = {
"barber": "Sharp cuts. Clean fades. Easy booking.",
"bakery": "Fresh pastries, warm coffee, zero guesswork.",
"food": "Hot chicken, clear prices, easy ordering.",
"roofing": "Roofing help when it actually matters.",
"contractor": "Reliable work, clear scope, cleaner results.",
"bookkeeping": "Clean books, clear payroll, better owner decisions.",
"cleaning": "A cleaner home without the scheduling mess.",
"fitness": "Private training built around real consistency.",
"salon": "Skin, style, and care that feels considered.",
"restaurant": "Where tradition meets the harbor.",
"real_estate": "Confident moves for buyers and sellers.",
"pets": "Gentle grooming for pets people love.",
"auto": "Mobile detailing that makes the car feel new.",
"landscaping": "Outdoor spaces that look cared for.",
"legal": "Clear legal guidance without the runaround.",
"plumbing": "Fast plumbing help with clear next steps.",
"electrical": "Safe electrical work without vague estimates.",
"med_spa": "Polished care, clear services, confident booking.",
"dental": "Friendly dental care with clear visits and simple scheduling.",
"education": "Clear tutoring support that moves students forward.",
"default": f"{name} makes the next step simple.",
}
return options.get(business_type, options["default"])
def subheadline_for(business_type: str) -> str:
options = {
"barber": "Premium barbering with visible services, simple pricing, real photos, and a direct chair-booking CTA.",
"bakery": "A warm neighborhood page with menu highlights, catering, hours, and a clear path to order.",
"food": "A simple food-business site with strong photos, clear prices, customer proof, and a direct order CTA.",
"roofing": "A trust-first contractor page with emergency help, service areas, proof, and fast contact.",
"contractor": "A practical local-service site that explains the work, builds trust, and gets customers to act.",
"bookkeeping": "Bookkeeping, payroll, reporting, and cleanup support presented with clear packages and a strong consultation path.",
"cleaning": "Recurring cleaning, deep cleaning, and move-out help presented with clear packages and proof.",
"fitness": "Coaching, nutrition, schedule, and testimonials organized for people ready to start.",
"salon": "A polished service page with treatments, packages, testimonials, and online booking.",
"restaurant": "Refined Japanese dining with omakase, private rooms, signature dishes, and a clear reservation path.",
"real_estate": "Buying, selling, valuation, and local guidance packaged into a clear conversion path.",
"pets": "Friendly grooming services, pricing, hours, and booking for busy pet owners.",
"auto": "Packages, proof, photos, and mobile booking for customers who want the car handled right.",
"landscaping": "Services, photos, service area, and estimate CTA for homeowners who want the yard handled.",
"legal": "Practice areas, trust proof, FAQ, and consultation flow for customers who need clarity.",
"plumbing": "Emergency help, service packages, proof, hours, and a direct request path for homeowners.",
"electrical": "Service calls, safety-focused messaging, clear pricing, and a direct estimate CTA.",
"med_spa": "Treatments, photos, consultation flow, testimonials, and booking in one polished page.",
"dental": "Preventive care, cosmetic options, urgent visits, insurance context, and scheduling in one clear page.",
"education": "Tutoring offers, outcomes, schedule, testimonials, and a simple inquiry path.",
"default": "A polished local business site with clear services, proof, hours, and a direct next step.",
}
return options.get(business_type, options["default"])
def normalize_spec(raw: dict[str, Any] | WebsiteSpec, prompt: str = "") -> WebsiteSpec:
if isinstance(raw, WebsiteSpec):
spec = raw
else:
fallback = spec_from_prompt(prompt)
business_type = clean_text(raw.get("business_type"), fallback.business_type).lower()
if business_type in {"default", "local service business", "business"} and fallback.business_type != "default":
business_type = fallback.business_type
services = raw.get("services") if isinstance(raw.get("services"), list) else fallback.services
sections = raw.get("sections") if isinstance(raw.get("sections"), list) else fallback.sections
testimonials = raw.get("testimonials") if isinstance(raw.get("testimonials"), list) else fallback.testimonials
image_urls = raw.get("image_urls") if isinstance(raw.get("image_urls"), list) else []
spec = WebsiteSpec(
business_name=clean_text(raw.get("business_name"), fallback.business_name),
business_type=business_type,
location=clean_text(raw.get("location"), fallback.location),
headline=clean_text(raw.get("headline"), fallback.headline),
subheadline=clean_text(raw.get("subheadline"), fallback.subheadline),
cta=infer_cta(prompt, fallback.business_type) if re.search(r"\bCTA\b", prompt) else clean_text(raw.get("cta"), fallback.cta),
services=[title_case_service(item) for item in services if isinstance(item, str)][:6] or fallback.services,
sections=[clean_text(item, "").lower().replace(" ", "_") for item in sections if isinstance(item, str)] or fallback.sections,
testimonials=[clean_text(item, "") for item in testimonials if isinstance(item, str)][:3] or fallback.testimonials,
hours=clean_text(raw.get("hours"), fallback.hours),
contact_phone=clean_text(raw.get("contact_phone"), fallback.contact_phone),
contact_email=clean_text(raw.get("contact_email"), fallback.contact_email),
palette=clean_text(raw.get("palette"), fallback.palette).lower(),
image_urls=[clean_text(item, "") for item in image_urls if isinstance(item, str)],
)
if "hero" not in spec.sections:
spec.sections.insert(0, "hero")
for required in ["services", "contact"]:
if required not in spec.sections:
spec.sections.append(required)
for prompt_section in infer_sections(prompt):
if prompt_section not in spec.sections:
spec.sections.append(prompt_section)
if len(spec.services) < 3:
spec.services = infer_services(prompt, spec.business_type)
bank = IMAGE_BANK.get(spec.business_type, IMAGE_BANK["default"])
valid_images = [url for url in spec.image_urls if url.startswith("https://")]
spec.image_urls = (valid_images + bank)[:3]
if spec.palette not in PALETTES:
spec.palette = "gold"
return spec
def esc(value: str) -> str:
return html.escape(value, quote=True)
def layout_for(spec: WebsiteSpec) -> str:
return LAYOUT_BY_TYPE.get(spec.business_type, "classic")
def service_copy(spec: WebsiteSpec, service: str) -> str:
copy_by_type = {
"auto": "A focused package with before-and-after clarity, clean timing, and mobile convenience.",
"landscaping": "Reliable yard care with a clear scope, visible outcomes, and simple estimate follow-up.",
"roofing": "Built for urgent homeowner trust: scope, timing, proof, and the next safe step.",
"plumbing": "Fast help with plain-language diagnosis, clear pricing, and no vague handoff.",
"electrical": "Safety-first service with clear estimates, careful work, and practical scheduling.",
"med_spa": "A polished service path with consultation, expectations, aftercare, and booking clarity.",
"dental": "A patient-ready service path with visit clarity, insurance context, and simple scheduling.",
"bookkeeping": "A business-owner-ready package with clear monthly scope, reporting rhythm, and cleanup support.",
"barber": "A clean chair-ready offer with visible pricing, easy booking, and a premium feel.",
"bakery": "A warm customer offer with photos, order clarity, and an easy way to inquire.",
"food": "A customer-ready menu offer with strong photos, simple pricing, and an easy ordering path.",
"restaurant": "A signature dining experience with seasonal ingredients, careful presentation, and reservation-ready detail.",
"legal": "A plain-English service area with trust, next steps, and consultation clarity.",
}
fallback = "Clear scope, practical guidance, and a clean result without confusing back-and-forth."
return copy_by_type.get(spec.business_type, fallback).replace("service", service.lower(), 1)
def render_cards(spec: WebsiteSpec) -> str:
cards = []
for item in spec.services[:4]:
safe = esc(item)
cards.append(
f" {esc(service_copy(spec, item))}{safe}
Gallery
Service area
We serve local customers who want a simple process, clear communication, and dependable execution.
FAQ
Most requests get a same-day reply during business hours.
Yes. The first step is always scope, timing, and price clarity.
{esc(item)}" for item in spec.testimonials[:3]) optional_sections = render_optional_sections(spec) hero_image = esc(spec.image_urls[0]) secondary_image = esc(spec.image_urls[1]) layout_class = esc(f"layout-{layout_for(spec)}") return f"""
{esc(spec.location)} {esc(spec.business_type)}
{esc(spec.subheadline)}
{esc(spec.cta)}Clear next steps, clean communication, and a professional result.
Services
Simple offers customers can understand quickly.
Pricing
Use this as a clean estimate structure. Final price depends on scope, timing, and details.
Hours
{esc(spec.hours).replace(";", "
")}
Testimonials
Contact
Call {esc(spec.contact_phone)}
Email {esc(spec.contact_email)}
{esc(spec.location)}