restokes92's picture
Upload Kaiju Coder 7 OpenCode helper package
89ef4db verified
#!/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"<article class=\"card\"><h3>{safe}</h3><p>{esc(service_copy(spec, item))}</p></article>"
)
return "\n".join(cards)
def render_pricing(services: list[str]) -> str:
rows = []
explicit_prices = {
"starter": "$299/mo",
"growth": "$599/mo",
"enterprise": "$999/mo",
"omakase": "$120",
"wagyu": "$85",
"black cod": "$42",
"ramen": "$28",
"signature chicken": "$14+",
"family meals": "$38+",
"catering trays": "$120+",
"sides": "$5+",
}
for index, item in enumerate(services[:4]):
lower = item.lower()
price = next((value for key, value in explicit_prices.items() if key in lower), f"${45 + index * 25}+")
rows.append(f"<li><span>{esc(item)}</span><strong>{esc(price)}</strong></li>")
return "\n".join(rows)
def render_gallery(spec: WebsiteSpec) -> str:
images = "\n".join(
f"<figure><img src=\"{esc(url)}\" alt=\"{esc(spec.business_name)} work sample {index + 1}\"><figcaption>Recent customer work</figcaption></figure>"
for index, url in enumerate(spec.image_urls[:3])
)
return f"""<section id="gallery" class="section">
<div class="section-head"><p class="eyebrow">Gallery</p><h2>Real work, clean presentation.</h2></div>
<div class="gallery">{images}</div>
</section>"""
def render_optional_sections(spec: WebsiteSpec) -> str:
chunks: list[str] = []
if "trust" in spec.sections:
chunks.append(
"""<section id="trust" class="trust-strip">
<span>Licensed where required</span><span>Clear estimates</span><span>Local service</span><span>Fast follow-up</span>
</section>"""
)
if "gallery" in spec.sections:
chunks.append(render_gallery(spec))
if "service_area" in spec.sections:
chunks.append(
f"""<section id="service-area" class="section split">
<div><p class="eyebrow">Service area</p><h2>Built for {esc(spec.location)} and nearby customers.</h2></div>
<p>We serve local customers who want a simple process, clear communication, and dependable execution.</p>
</section>"""
)
if "faq" in spec.sections:
chunks.append(
"""<section id="faq" class="section split">
<div><p class="eyebrow">FAQ</p><h2>Quick answers.</h2></div>
<div class="faq"><details open><summary>How fast do you reply?</summary><p>Most requests get a same-day reply during business hours.</p></details><details><summary>Can I get a clear estimate?</summary><p>Yes. The first step is always scope, timing, and price clarity.</p></details></div>
</section>"""
)
return "\n".join(chunks)
def render_html(raw_spec: dict[str, Any] | WebsiteSpec, prompt: str = "") -> str:
spec = normalize_spec(raw_spec, prompt)
palette = PALETTES[spec.palette]
service_cards = render_cards(spec)
pricing = render_pricing(spec.services)
testimonials = "\n".join(f"<blockquote>{esc(item)}</blockquote>" 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"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{esc(spec.business_name)} | {esc(spec.business_type.title())}</title>
<meta name="description" content="{esc(spec.business_name)} is a polished local {esc(spec.business_type)} serving {esc(spec.location)}.">
<style>
:root{{--ink:{palette["ink"]};--paper:{palette["paper"]};--accent:{palette["accent"]};--card:{palette["card"]};--muted:#667085;--line:rgba(16,24,40,.14)}}*{{box-sizing:border-box}}html{{scroll-behavior:smooth}}body{{margin:0;font-family:Avenir Next,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:radial-gradient(circle at top left,rgba(244,173,50,.22),transparent 30%),var(--paper);color:var(--ink)}}a{{color:inherit;text-decoration:none}}img{{max-width:100%;display:block}}.wrap{{max-width:1160px;margin:auto;padding:24px}}nav{{display:flex;align-items:center;justify-content:space-between;gap:18px}}.brand{{font-size:25px;font-weight:950;letter-spacing:-.025em}}.links{{display:flex;gap:18px;color:var(--muted);font-weight:850}}.btn{{display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:999px;background:var(--accent);color:#111;padding:13px 20px;font-weight:950;box-shadow:0 16px 42px rgba(0,0,0,.16)}}.hero{{display:grid;grid-template-columns:1.02fr .98fr;gap:34px;align-items:center;padding:60px 0 42px}}.eyebrow{{color:var(--accent);font-size:12px;font-weight:950;letter-spacing:.18em;text-transform:uppercase}}h1{{font-size:clamp(44px,6.4vw,76px);line-height:.95;letter-spacing:-.045em;margin:12px 0 16px;max-width:10ch}}h2{{font-size:clamp(30px,4vw,48px);line-height:1;letter-spacing:-.035em;margin:0 0 12px}}h3{{margin:0 0 8px;font-size:20px}}p{{font-size:18px;line-height:1.6;color:var(--muted)}}.hero-media{{position:relative}}.hero-media img{{width:100%;height:490px;object-fit:cover;border-radius:32px;box-shadow:0 30px 80px rgba(0,0,0,.24)}}.floating{{position:absolute;left:24px;bottom:24px;background:rgba(255,255,255,.92);backdrop-filter:blur(14px);border-radius:22px;padding:18px;max-width:280px;border:1px solid var(--line);color:var(--ink)}}.section{{padding:44px 0;border-top:1px solid var(--line)}}.section-head{{display:flex;align-items:end;justify-content:space-between;gap:22px;margin-bottom:18px}}.grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}}.card,.panel,blockquote,figure{{background:var(--card);border:1px solid var(--line);border-radius:24px;padding:20px;box-shadow:0 14px 45px rgba(15,23,42,.08)}}.split{{display:grid;grid-template-columns:1fr 1fr;gap:22px;align-items:start}}ul{{list-style:none;margin:0;padding:0}}li{{display:flex;justify-content:space-between;gap:20px;padding:14px 0;border-bottom:1px solid var(--line);font-size:18px}}blockquote{{margin:0;font-size:18px;line-height:1.55;color:var(--ink)}}.gallery{{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}}figure{{margin:0;padding:10px}}figure img{{width:100%;height:230px;object-fit:cover;border-radius:18px}}figcaption{{padding:10px 4px 2px;color:var(--muted);font-weight:800}}.trust-strip{{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;padding:14px 0}}.trust-strip span{{background:var(--ink);color:white;border-radius:999px;text-align:center;padding:12px 14px;font-weight:900}}.contact{{background:var(--ink);color:white;border-radius:30px;padding:32px;margin:44px 0 0}}.contact p{{color:rgba(255,255,255,.78)}}input,textarea{{width:100%;border:1px solid rgba(255,255,255,.18);border-radius:16px;background:rgba(255,255,255,.08);color:white;padding:14px;margin:0 0 12px;font:inherit}}footer{{padding:28px 0;color:var(--muted)}}.layout-proof .hero{{background:linear-gradient(135deg,var(--ink),#1f2937);border-radius:34px;color:white;margin-top:26px;padding:44px}}.layout-proof .hero p{{color:rgba(255,255,255,.76)}}.layout-proof .hero-media img{{height:430px}}.layout-proof .floating{{right:22px;left:auto;color:var(--ink)}}.layout-proof .floating p{{color:var(--muted)}}.layout-editorial .hero{{grid-template-columns:.92fr 1.08fr;padding-top:78px}}.layout-editorial h1{{font-size:clamp(52px,7vw,88px)}}.layout-editorial .hero-media img{{border-radius:999px 999px 34px 34px}}.layout-gallery-forward #gallery .section-head{{display:grid;grid-template-columns:.32fr 1fr;align-items:start}}.layout-gallery-forward #gallery h2{{font-size:clamp(38px,5vw,64px)}}.layout-kinetic .hero-media img{{border-radius:36px 36px 110px 36px}}.layout-kinetic .card:nth-child(even){{transform:translateY(18px)}}@media(max-width:880px){{.hero,.split,.layout-editorial .hero{{grid-template-columns:1fr}}.layout-proof .hero{{padding:28px}}.grid,.gallery,.trust-strip{{grid-template-columns:1fr 1fr}}.links{{display:none}}.hero-media img,.layout-proof .hero-media img{{height:360px}}h1{{max-width:12ch}}}}@media(max-width:560px){{.wrap{{padding:18px}}.grid,.gallery,.trust-strip{{grid-template-columns:1fr}}h1,.layout-editorial h1{{font-size:44px;max-width:none}}.layout-kinetic .card:nth-child(even){{transform:none}}}}
</style>
</head>
<body class="{layout_class}">
<div class="wrap">
<nav><a class="brand" href="#">{esc(spec.business_name)}</a><div class="links"><a href="#services">Services</a><a href="#pricing">Pricing</a><a href="#hours">Hours</a><a href="#contact">Contact</a></div><a class="btn" href="#contact">{esc(spec.cta)}</a></nav>
<main>
<header class="hero" id="top"><div><p class="eyebrow">{esc(spec.location)} {esc(spec.business_type)}</p><h1>{esc(spec.headline)}</h1><p>{esc(spec.subheadline)}</p><a class="btn" href="#contact">{esc(spec.cta)}</a></div><div class="hero-media"><img src="{hero_image}" alt="{esc(spec.business_name)} customer experience"><div class="floating"><strong>Same-week availability</strong><p>Clear next steps, clean communication, and a professional result.</p></div></div></header>
{optional_sections}
<section id="services" class="section"><div class="section-head"><div><p class="eyebrow">Services</p><h2>What we handle.</h2></div><p>Simple offers customers can understand quickly.</p></div><div class="grid">{service_cards}</div></section>
<section id="pricing" class="section split"><div><p class="eyebrow">Pricing</p><h2>Clear starting points.</h2><p>Use this as a clean estimate structure. Final price depends on scope, timing, and details.</p><img src="{secondary_image}" alt="{esc(spec.business_name)} service detail" style="height:220px;width:100%;object-fit:cover;border-radius:22px"></div><div class="panel"><ul>{pricing}</ul></div></section>
<section id="hours" class="section split"><div><p class="eyebrow">Hours</p><h2>Open this week.</h2><p>{esc(spec.hours).replace(";", "<br>")}</p></div><div><p class="eyebrow">Testimonials</p><div class="grid" style="grid-template-columns:1fr">{testimonials}</div></div></section>
<section id="contact" class="contact split"><div><p class="eyebrow">Contact</p><h2>{esc(spec.cta)}</h2><p>Call {esc(spec.contact_phone)}<br>Email {esc(spec.contact_email)}<br>{esc(spec.location)}</p></div><form><input aria-label="Name" placeholder="Name"><input aria-label="Email" placeholder="Email"><textarea aria-label="Project details" placeholder="What do you need?"></textarea><button class="btn" type="button">Send request</button></form></section>
</main>
<footer>Copyright 2026 {esc(spec.business_name)}. Built as a complete one-file website.</footer>
</div>
</body>
</html>"""
def validate_html(rendered: str, spec: WebsiteSpec | None = None) -> list[str]:
lower = rendered.lower()
readable_lower = html.unescape(rendered).lower()
errors: list[str] = []
required = ["<!doctype html", "<html", "<head", "</head>", "<body", "</body>", "</html>"]
for token in required:
if token not in lower:
errors.append(f"missing {token}")
if not lower.strip().endswith("</html>"):
errors.append("document does not end with </html>")
if "```" in rendered:
errors.append("markdown fence found")
if re.search(r"opacity\s*:\s*0", rendered, flags=re.IGNORECASE):
errors.append("hidden opacity:0 content found")
if "viewport" not in lower:
errors.append("missing mobile viewport")
if "@media" not in lower:
errors.append("missing responsive media query")
if "<img " not in lower or "https://images.unsplash.com/" not in lower:
errors.append("missing verified external image")
for section in ["services", "pricing", "hours", "contact"]:
if f'id="{section}"' not in lower:
errors.append(f"missing #{section} section")
if spec:
for service in spec.services[:3]:
if service.lower() not in readable_lower:
errors.append(f"missing service: {service}")
return errors
def render_from_prompt(prompt: str) -> tuple[WebsiteSpec, str, list[str]]:
spec = spec_from_prompt(prompt)
rendered = render_html(spec, prompt)
return spec, rendered, validate_html(rendered, spec)
def write_html(path: Path, rendered: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(rendered, encoding="utf-8")
def spec_to_json(spec: WebsiteSpec) -> str:
return json.dumps(spec.__dict__, indent=2, ensure_ascii=False)