# Dynamic Sites An LLM-powered website editing framework. Edit your site via SMS, a web API, or a visual editor — all driven by natural language. ## Architecture ``` ┌─────────────────────────────────────────────────┐ │ Channels │ │ SMS (Vonage) │ POST /api/edit │ /editor │ └───────┬─────────┴────────┬─────────┴─────┬──────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────┐ │ Single Container (entrypoint.sh) │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ Orchestrator (Express, port 3001) │ │ │ │ │ │ │ │ Webhook ──► Idempotency ──► Rate Limit │ │ │ │ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ In-Process FIFO Queue (concurrency 1) │ │ │ │ │ propose: route ► LLM ► proposal │ │ │ │ │ │ apply: validate ► writeContentFile│ │ │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ │ │ │ SQLite: idempotency, proposals, rate │ │ │ │ limits, audit log │ │ │ └──────────────────┬─────────────────────────┘ │ │ │ writes canonical JSON │ │ ▼ │ │ ┌──────────────────┐ │ │ │ content/ (JSON) │ shared filesystem │ │ │ site-context.json│ │ │ └────────┬─────────┘ │ │ │ reads with TTL cache │ │ ▼ │ │ ┌────────────────────────────────────────────┐ │ │ │ Astro SSR (port 4321) │ │ │ │ Homepage + Editor │ │ │ └────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘ ``` ## Quick Start ### Prerequisites - Node.js 22+ - npm - A Vonage API account with a Messages API-enabled application ### Local Development ```bash # Clone and install npm install # Start the Astro dev server (port 4321) npm run dev # In another terminal, start the orchestrator (port 3001) npm run dev:server # Visit http://localhost:4321 — the demo site renders from fixtures # Visit http://localhost:4321/editor — log in with API_EDIT_SECRET ``` ### Environment Variables Copy `.env.example` to `.env` and set at minimum: - `API_EDIT_SECRET` — shared secret for API auth and editor login - `OLLAMA_API_KEY` — required for LLM-powered edits - `VONAGE_API_KEY` — your Vonage API key (from Dashboard) - `VONAGE_API_SECRET` — your Vonage API secret (from Dashboard) - `VONAGE_APPLICATION_ID` — your Vonage application ID - `VONAGE_PRIVATE_KEY_PATH` — path to the `private.key` file on disk, or set `VONAGE_PRIVATE_KEY` to the base64-encoded PEM content - `VONAGE_API_SIGNATURE_SECRET` — webhook signature secret (from Dashboard → API Settings) See `.env.example` for all options. ### Vonage Setup 1. Create a Vonage application in the Dashboard with Messages capability enabled. 2. Set the inbound message webhook URL to `https://dynamicsites.kadil.dev/webhooks/inbound` (POST). 3. Set the status webhook URL to `https://dynamicsites.kadil.dev/webhooks/status` (POST). 4. Under API Settings, ensure Messages API is set as the default for SMS. 5. Copy the generated `private.key` to the project root (or base64-encode it for `VONAGE_PRIVATE_KEY`). 6. Note your signature secret from Dashboard → API Settings for webhook verification. ### Docker ```bash docker compose build docker compose up -d # Site: http://localhost:4321 # Orchestrator: http://localhost:3001/health ``` Both the Astro SSR site and the orchestrator run in a single container, sharing the same `content/` directory. The entrypoint script starts both processes and seeds content from the image into the volume on first deploy. ## Project Structure ``` ├── content/ # Canonical JSON content (the "database") │ ├── sections/ # One JSON file per site section │ ├── events.json # Upcoming events │ └── .backups/ # Pre-apply backups (auto-managed) ├── config/ │ └── sms-sites.json # SMS routing allowlist ├── site-context.json # Brand tone, style, LLM prompt context ├── shared/ # Zod schemas + canonical JSON (workspace pkg) │ └── src/ │ ├── schemas/index.ts # All Zod schemas (the contract) │ ├── canonical-json.ts # Sorted-key JSON serialization │ └── repo-validation.ts# Path → schema mapping ├── server/ # Orchestrator (workspace pkg) │ └── src/ │ ├── index.ts # Entrypoint + graceful shutdown │ ├── app.ts # Express app factory │ ├── db.ts # SQLite (idempotency, proposals, audit) │ ├── logger.ts # Structured logging (pino) │ ├── queue/ # FIFO queue + job processor │ ├── routes/ # API edit, SMS webhook, health │ ├── llm/ # Ollama client with retry/validation │ ├── sms/ # Vonage parse, reply, templates │ └── io/ # Filesystem writer (atomic, with backup) ├── src/ # Astro SSR site │ ├── pages/ │ │ ├── index.astro # Homepage (renders from content/) │ │ └── editor.astro # Editor (auth-gated React island) │ ├── lib/ │ │ ├── site-bundle.ts # Content parser + validator │ │ └── site-data.ts # Disk reader with TTL cache │ ├── layouts/ │ │ └── BaseLayout.astro │ └── components/ │ ├── sections/ # Astro section components │ └── editor/ # React editor island ├── scripts/ # CLI tools ├── Dockerfile # Combined image (Astro SSR + orchestrator) ├── docker-compose.yml # Full stack (single service) └── entrypoint.sh # Starts both processes + seeds content ``` ## Edit Flow 1. **User sends a natural language message** (SMS, HTTP, or editor) 2. **Route**: LLM determines which content file to edit 3. **Propose**: LLM generates new JSON + plain-language summary 4. **Confirm**: User replies YES/NO (SMS) or clicks confirm (editor/HTTP) 5. **Apply**: Validated JSON is written to disk via atomic write 6. **Live**: Astro SSR picks up the change on next request (TTL cache) ## Key Design Decisions - **Single container**: Both Astro SSR and the orchestrator run in one container, sharing the filesystem for content reads and writes - **No Redis, no BullMQ**: Simple in-process FIFO queue with concurrency 1 - **No git**: Content persistence is filesystem-only - **SQLite for everything**: Idempotency, proposals, rate limits, audit log - **Zod is the contract**: Schemas drive validation at every boundary - **Atomic writes**: temp file + rename prevents partial writes - **Pre-write backups**: Last 20 versions per file under `content/.backups/`