import html import json import shutil import zipfile from pathlib import Path import requests from django.conf import settings from django.utils.text import slugify from .models import ClientSite from .template_registry import ( COPY_EXCLUDE_DIR_NAMES, get_template_config, resolve_source_path, ) PERSONALIZE_EXTENSIONS = {".html", ".htm", ".css", ".js", ".jsx", ".md", ".txt", ".php"} class SiteGenerator: def __init__(self) -> None: self.ollama_url = settings.OLLAMA_URL.rstrip("/") self.ollama_model = settings.OLLAMA_MODEL def build_all(self, site: ClientSite) -> ClientSite: site_root = Path(settings.MEDIA_ROOT) / "generated" / str(site.id) if site_root.exists(): shutil.rmtree(site_root) site_root.mkdir(parents=True, exist_ok=True) ai_payload = self._generate_ai_payload(site) basic_zip = self._build_basic_site(site, ai_payload, site_root) django_zip = self._build_django_demo(site, ai_payload, site_root) site.ai_payload = ai_payload site.basic_zip_path = str(basic_zip.relative_to(settings.MEDIA_ROOT)) site.django_zip_path = str(django_zip.relative_to(settings.MEDIA_ROOT)) site.status = ClientSite.Status.READY site.last_error = "" site.save( update_fields=[ "ai_payload", "basic_zip_path", "django_zip_path", "status", "last_error", "updated_at", ] ) return site def _generate_ai_payload(self, site: ClientSite) -> dict: fallback = { "tagline": f"{site.business_name} helps local customers with trusted service.", "hero_headline": f"Welcome to {site.business_name}", "hero_subheadline": site.business_description, "services": [ "Professional support tailored to your business goals", "Friendly service with clear communication", "Fast turnaround with quality-focused delivery", ], "testimonials": [ {"name": "Jordan T.", "quote": f"{site.business_name} made process simple and stress-free."}, {"name": "Casey M.", "quote": "Great communication, clear pricing, excellent final result."}, {"name": "Alex R.", "quote": "Reliable team. We will keep using them for future needs."}, ], "contact_prompt": "Tell us what you need and we will respond quickly.", } prompt = ( "You are website copy assistant. Respond in valid JSON only. " "Use keys: tagline, hero_headline, hero_subheadline, services, testimonials, contact_prompt. " "services must be 3 short strings. testimonials must be array of 3 objects with name and quote." f"\nBusiness name: {site.business_name}" f"\nBusiness description: {site.business_description}" f"\nAddress: {site.business_address}" f"\nPhone: {site.business_phone}" f"\nOld website: {site.old_website or 'n/a'}" ) try: response = requests.post( f"{self.ollama_url}/api/generate", json={"model": self.ollama_model, "prompt": prompt, "stream": False}, timeout=20, ) response.raise_for_status() model_text = response.json().get("response", "").strip() parsed = json.loads(model_text) if not isinstance(parsed, dict): return fallback return { "tagline": parsed.get("tagline") or fallback["tagline"], "hero_headline": parsed.get("hero_headline") or fallback["hero_headline"], "hero_subheadline": parsed.get("hero_subheadline") or fallback["hero_subheadline"], "services": parsed.get("services") or fallback["services"], "testimonials": parsed.get("testimonials") or fallback["testimonials"], "contact_prompt": parsed.get("contact_prompt") or fallback["contact_prompt"], } except Exception: return fallback def _build_basic_site(self, site: ClientSite, ai_payload: dict, root: Path) -> Path: basic_dir = root / "basic_site" if basic_dir.exists(): shutil.rmtree(basic_dir) source_path = resolve_source_path(site.template_option.source_folder) if source_path.exists() and source_path.is_dir(): self._copy_client_template(source_path, basic_dir) config = get_template_config(site.template_option.slug) or {} self._personalize_template_tree(basic_dir, site, ai_payload, config) self._write_client_readme(basic_dir, site, ai_payload, config) basic_zip = root / "basic_site.zip" self._zip_dir(basic_dir, basic_zip) return basic_zip basic_dir.mkdir(parents=True, exist_ok=True) safe_name = html.escape(site.business_name) safe_phone = html.escape(site.business_phone) safe_address = html.escape(site.business_address) safe_description = html.escape(site.business_description) safe_tagline = html.escape(ai_payload["tagline"]) hero_title = html.escape(ai_payload["hero_headline"]) hero_sub = html.escape(ai_payload["hero_subheadline"]) services = ai_payload.get("services", []) testimonials = ai_payload.get("testimonials", []) nav = """ """ index_html = f""" {safe_name} | Home {nav}

{hero_title}

{hero_sub}

{safe_tagline}

What We Offer

""" about_html = f""" {safe_name} | About {nav}

About {safe_name}

{safe_description}

Address: {safe_address}

Phone: {safe_phone}

""" t1 = testimonials[0] if len(testimonials) > 0 and isinstance(testimonials[0], dict) else {"name": "Client", "quote": "Great result."} t2 = testimonials[1] if len(testimonials) > 1 and isinstance(testimonials[1], dict) else {"name": "Client", "quote": "Excellent service."} t3 = testimonials[2] if len(testimonials) > 2 and isinstance(testimonials[2], dict) else {"name": "Client", "quote": "Would recommend."} testimonials_html = f""" {safe_name} | Testimonials {nav}

Testimonials

"{html.escape(str(t1.get("quote", "Great result.")))}" - {html.escape(str(t1.get("name", "Client")))}
"{html.escape(str(t2.get("quote", "Excellent service.")))}" - {html.escape(str(t2.get("name", "Client")))}
"{html.escape(str(t3.get("quote", "Would recommend.")))}" - {html.escape(str(t3.get("name", "Client")))}
""" contact_html = f""" {safe_name} | Contact {nav}

Contact Us

{html.escape(ai_payload["contact_prompt"])}

Phone: {safe_phone}

Address: {safe_address}

""" styles = """ body{font-family:Arial,sans-serif;margin:0;background:#f6f7fb;color:#1f2937;} nav{display:flex;gap:1rem;padding:1rem 2rem;background:#111827;} nav a{color:#fff;text-decoration:none;font-weight:600;} .hero{padding:4rem 2rem;background:#2563eb;color:white;} main,section{max-width:900px;margin:2rem auto;padding:0 1rem;} blockquote{background:#fff;padding:1rem;border-left:4px solid #2563eb;border-radius:6px;} .tagline{display:inline-block;margin-top:1rem;background:#1d4ed8;padding:.25rem .75rem;border-radius:999px;} """ (basic_dir / "index.html").write_text(index_html, encoding="utf-8") (basic_dir / "about.html").write_text(about_html, encoding="utf-8") (basic_dir / "testimonials.html").write_text(testimonials_html, encoding="utf-8") (basic_dir / "contact.html").write_text(contact_html, encoding="utf-8") (basic_dir / "styles.css").write_text(styles, encoding="utf-8") basic_zip = root / "basic_site.zip" self._zip_dir(basic_dir, basic_zip) return basic_zip def _copy_client_template(self, source_path: Path, destination: Path) -> None: def ignore(_directory: str, names: list[str]) -> list[str]: ignored: list[str] = [] for name in names: if name in COPY_EXCLUDE_DIR_NAMES: ignored.append(name) elif name.endswith(".zip"): ignored.append(name) return ignored shutil.copytree(source_path, destination, ignore=ignore) def _personalize_template_tree( self, target_dir: Path, site: ClientSite, ai_payload: dict, config: dict, ) -> None: replacements = self._build_replacements(site, ai_payload, config) for file_path in target_dir.rglob("*"): if not file_path.is_file(): continue if file_path.suffix.lower() not in PERSONALIZE_EXTENSIONS: continue if "bootstrap-icons" in file_path.parts and file_path.suffix == ".json": continue try: content = file_path.read_text(encoding="utf-8") except UnicodeDecodeError: continue updated = content for old, new in replacements.items(): if old and new and old in updated: updated = updated.replace(old, new) if updated != content: file_path.write_text(updated, encoding="utf-8") info_path = target_dir / "client-site-info.json" info_path.write_text( json.dumps( { "business_name": site.business_name, "business_address": site.business_address, "business_phone": site.business_phone, "business_description": site.business_description, "old_website": site.old_website, "ai_payload": ai_payload, }, indent=2, ), encoding="utf-8", ) def _build_replacements(self, site: ClientSite, ai_payload: dict, config: dict) -> dict[str, str]: replacements: dict[str, str] = {} business_name = site.business_name hero_headline = ai_payload.get("hero_headline", business_name) hero_subheadline = ai_payload.get("hero_subheadline", site.business_description) for token in config.get("brand_tokens", []): replacements[token] = business_name hero_strings = config.get("hero_strings", []) if hero_strings: replacements[hero_strings[0]] = hero_headline for hero_string in hero_strings[1:]: replacements[hero_string] = hero_subheadline replacements.update( { "555-0100": site.business_phone, "555-0101": site.business_phone, "(555) 010-0100": site.business_phone, "123 Main Street": site.business_address, "San Francisco, California": site.business_address, } ) if site.old_website: replacements["https://example.com"] = site.old_website replacements["http://example.com"] = site.old_website return replacements def _write_client_readme( self, target_dir: Path, site: ClientSite, ai_payload: dict, config: dict, ) -> None: kind = config.get("kind", "static_html") run_hint = "Open index.html in a browser." if kind == "nextjs": run_hint = "Run: npm install && npm run dev" elif kind == "static_html": run_hint = "Open index.html (or another index-*.html page) in a browser." readme = f"""# {site.business_name} — generated site package Template: {site.template_option.name} Type: {kind} ## Business details - Name: {site.business_name} - Phone: {site.business_phone} - Address: {site.business_address} - Description: {site.business_description} """ if site.old_website: readme += f"- Previous website: {site.old_website}\n" readme += f""" ## AI-generated copy - Tagline: {ai_payload.get("tagline", "")} - Headline: {ai_payload.get("hero_headline", "")} - Subheadline: {ai_payload.get("hero_subheadline", "")} ## How to preview {run_hint} See client-site-info.json for full generation payload. """ (target_dir / "README-CLIENT.md").write_text(readme, encoding="utf-8") def _build_django_demo(self, site: ClientSite, ai_payload: dict, root: Path) -> Path: project_slug = slugify(site.business_name)[:30] or "client-site" django_dir = root / "django_demo" django_dir.mkdir(parents=True, exist_ok=True) files = { "manage.py": self._django_manage_py(), "client_demo/__init__.py": "", "client_demo/settings.py": self._django_settings_py(site), "client_demo/urls.py": self._django_urls_py(), "client_demo/wsgi.py": self._django_wsgi_py(), "shop/__init__.py": "", "shop/apps.py": self._shop_apps_py(), "shop/models.py": self._shop_models_py(), "shop/forms.py": self._shop_forms_py(), "shop/urls.py": self._shop_urls_py(), "shop/views.py": self._shop_views_py(ai_payload), "shop/admin.py": self._shop_admin_py(), "shop/migrations/__init__.py": "", "templates/base.html": self._shop_base_template(site), "templates/shop/home.html": self._shop_home_template(), "templates/shop/products.html": self._shop_products_template(), "templates/shop/cart.html": self._shop_cart_template(), "templates/shop/contact.html": self._shop_contact_template(), "README.md": self._django_readme(project_slug), } for relative_path, content in files.items(): destination = django_dir / relative_path destination.parent.mkdir(parents=True, exist_ok=True) destination.write_text(content, encoding="utf-8") django_zip = root / "django_demo.zip" self._zip_dir(django_dir, django_zip) return django_zip def _zip_dir(self, directory: Path, output_zip: Path) -> None: with zipfile.ZipFile(output_zip, "w", zipfile.ZIP_DEFLATED) as zipf: for file_path in directory.rglob("*"): if file_path.is_file(): zipf.write(file_path, file_path.relative_to(directory)) def _django_manage_py(self) -> str: return """#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_demo.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) """ def _django_settings_py(self, site: ClientSite) -> str: return f"""from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = "replace-me" DEBUG = True ALLOWED_HOSTS = ["*"] INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "shop", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "client_demo.urls" TEMPLATES = [{{ "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": {{"context_processors": [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ]}}, }}] WSGI_APPLICATION = "client_demo.wsgi.application" DATABASES = {{"default": {{"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"}}}} LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True STATIC_URL = "static/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" DEFAULT_FROM_EMAIL = "no-reply@example.com" BUSINESS_NAME = {site.business_name!r} BUSINESS_PHONE = {site.business_phone!r} BUSINESS_ADDRESS = {site.business_address!r} """ def _django_urls_py(self) -> str: return """from django.contrib import admin from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), path("", include("shop.urls")), ] """ def _django_wsgi_py(self) -> str: return """import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "client_demo.settings") application = get_wsgi_application() """ def _shop_apps_py(self) -> str: return """from django.apps import AppConfig class ShopConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "shop" """ def _shop_models_py(self) -> str: return """from django.db import models class Product(models.Model): name = models.CharField(max_length=120) description = models.TextField(blank=True) price = models.DecimalField(max_digits=10, decimal_places=2) is_active = models.BooleanField(default=True) def __str__(self): return self.name """ def _shop_forms_py(self) -> str: return """from django import forms class ContactForm(forms.Form): name = forms.CharField(max_length=100) email = forms.EmailField() message = forms.CharField(widget=forms.Textarea(attrs={"rows": 5})) """ def _shop_urls_py(self) -> str: return """from django.urls import path from . import views urlpatterns = [ path("", views.home, name="home"), path("products/", views.products, name="products"), path("cart/add//", views.add_to_cart, name="add_to_cart"), path("cart/", views.cart, name="cart"), path("contact/", views.contact, name="contact"), ] """ def _shop_views_py(self, ai_payload: dict) -> str: headline = ai_payload.get("hero_headline", "Welcome") subheadline = ai_payload.get("hero_subheadline", "") return f"""from django.conf import settings from django.contrib import messages from django.core.mail import send_mail from django.shortcuts import get_object_or_404, redirect, render from .forms import ContactForm from .models import Product def home(request): return render(request, "shop/home.html", {{ "headline": {headline!r}, "subheadline": {subheadline!r}, }}) def products(request): return render(request, "shop/products.html", {{"products": Product.objects.filter(is_active=True)}}) def add_to_cart(request, product_id): product = get_object_or_404(Product, id=product_id, is_active=True) cart_data = request.session.get("cart", {{}}) key = str(product.id) cart_data[key] = cart_data.get(key, 0) + 1 request.session["cart"] = cart_data messages.success(request, f"Added {{product.name}} to cart.") return redirect("products") def cart(request): cart_data = request.session.get("cart", {{}}) items = [] total = 0 for product_id, qty in cart_data.items(): product = Product.objects.filter(id=product_id).first() if not product: continue line_total = product.price * qty items.append({{"product": product, "qty": qty, "line_total": line_total}}) total += line_total return render(request, "shop/cart.html", {{"items": items, "total": total}}) def contact(request): if request.method == "POST": form = ContactForm(request.POST) if form.is_valid(): subject = f"Contact request for {{settings.BUSINESS_NAME}}" body = ( f"Name: {{form.cleaned_data['name']}}\\n" f"Email: {{form.cleaned_data['email']}}\\n\\n" f"Message:\\n{{form.cleaned_data['message']}}" ) send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [settings.DEFAULT_FROM_EMAIL]) messages.success(request, "Message sent.") return redirect("contact") else: form = ContactForm() return render(request, "shop/contact.html", {{"form": form}}) """ def _shop_admin_py(self) -> str: return """from django.contrib import admin from .models import Product admin.site.register(Product) """ def _shop_base_template(self, site: ClientSite) -> str: return f""" {html.escape(site.business_name)} Demo
{{% if messages %}} {{% for message in messages %}}

{{{{ message }}}}

{{% endfor %}} {{% endif %}} {{% block content %}}{{% endblock %}}
""" def _shop_home_template(self) -> str: return """{% extends "base.html" %} {% block content %}

{{ headline }}

{{ subheadline }}

Shop now
{% endblock %} """ def _shop_products_template(self) -> str: return """{% extends "base.html" %} {% block content %}

Products

{% for product in products %}

{{ product.name }}

{{ product.description }}

${{ product.price }}

Add to cart
{% empty %}

No products yet. Add from admin panel.

{% endfor %} {% endblock %} """ def _shop_cart_template(self) -> str: return """{% extends "base.html" %} {% block content %}

Your Cart

{% for item in items %}
{{ item.product.name }} x {{ item.qty }} = ${{ item.line_total }}
{% empty %}

Cart empty.

{% endfor %}

Total: ${{ total }}

{% endblock %} """ def _shop_contact_template(self) -> str: return """{% extends "base.html" %} {% block content %}

Contact

{% csrf_token %} {{ form.as_p }}
{% endblock %} """ def _django_readme(self, project_slug: str) -> str: return f"""# {project_slug} Generated Django demo storefront. ## Run 1. pip install django 2. python manage.py migrate 3. python manage.py createsuperuser 4. python manage.py runserver ## Basic static site Download the basic HTML package from the builder dashboard for the matching template design. """