Visual Landing Page Builder (M20)
What It Does#
The Visual Landing Page Builder lets non-technical users create branded landing pages through a two-panel interface: an AI chat agent on the left assembles and edits page blocks in response to natural-language instructions; a drag-and-drop canvas on the right shows a live preview of the block composition. Pages can be delivered via a hosted public URL (with secure server-side event tracking) or exported as TSX, HTML, or JSON for self-hosting.
The system builds on the M19b GeneratedPage foundation and adds a full block-based composition model, brand design system management, a reusable page template library, and external product feed ingestion.
Builder UI#
Route: /ai-reach/pages/[id]/builder
The builder is a two-panel layout:
| Panel | Contents |
|---|---|
| Left — AI Chat | Streaming chat with the page builder agent. Natural-language instructions are translated into block operations on the canvas. |
| Right — Canvas | Live block preview. Blocks can be reordered via drag-and-drop (@dnd-kit). Clicking a block opens its property editor. |
Additional panels accessible from the builder toolbar:
- Design System — preview and apply the active brand kit (colors, fonts, spacing tokens)
- Export — download the page as TSX, HTML with inline CSS, or raw JSON block data
Block operations from chat#
The AI agent can:
- Add blocks to the page (
emit_block) - Update block content or properties (
update_block) - Suggest a template based on the page's query context (
suggest_template) - Set the page title (
set_page_title) - Pull products from the PIM or an external feed (
fetch_products)
New Page Wizard#
Route: /ai-reach/pages/new
A 3-step wizard for creating a new page:
- Choose a starting point — blank canvas, a saved template, or a query-driven auto-generate
- Select product source — PIM catalog, product segment, or external product feed
- Configure delivery — hosted (delivery app) or API / self-hosted
AI Agent Workflow#
The builder agent is a LangGraph ReAct loop running at POST /api/ai-pages/pages/<id>/build/. It streams results as Server-Sent Events.
Tools#
| Tool | Description |
|---|---|
fetch_products | Retrieve products from the tenant PIM or a named external feed |
fetch_template | Load a saved page template by ID |
list_templates | List available templates for the tenant |
fetch_design_system | Load the active brand design system |
fetch_page_context | Load the current page state (existing blocks, title, query) |
emit_block | Append a new block to the page |
update_block | Modify an existing block's content or configuration |
suggest_template | Pick the best matching template for the current query context |
set_page_title | Set or update the page title |
SSE event types#
Block Types#
The block registry defines 6 canonical block types. Each block has typed props enforced by the registry.
| Block Type | Purpose |
|---|---|
hero | Full-width hero with headline, subheadline, CTA button, and optional background image |
product_grid | Grid of product cards with image, name, price, and link. Supports 2–4 column layouts. |
text_section | Rich text block with heading and body copy |
cta_banner | Call-to-action strip with headline and button |
feature_list | Icon + label + description list for product or brand features |
testimonial | Single testimonial with quote, author name, and optional avatar |
Design System#
Route: /ai-reach/pages/design-system
The design system stores a tenant's brand kit as CSS design tokens. Tokens are injected at render time in both the builder preview and the hosted delivery app.
| Field | Description |
|---|---|
primary_color | Primary brand color (hex) |
secondary_color | Secondary/accent color (hex) |
background_color | Default page background (hex) |
text_color | Body text color (hex) |
heading_font | Heading font family name |
body_font | Body font family name |
border_radius | Button and card border radius (CSS value) |
spacing_unit | Base spacing unit in px |
custom_css | Free-form CSS appended after generated tokens |
Vision extraction#
POST /api/ai-pages/design-system/extract/ accepts an image URL (screenshot, brand asset, or homepage URL). A Claude vision call extracts color palette, font suggestions, and style notes from the image and streams the results as SSE events. The extracted values are pre-populated into the design system form for review before saving.
Page Templates#
Route: /ai-reach/pages/templates
Templates capture a block composition as a reusable starting point. A template stores:
- Template name and description
- Ordered block structure (block types + default props)
- Suggested use case tags
Templates are created from existing pages ("Save as template" in the builder) or via the template library UI. The AI agent's suggest_template tool ranks available templates by cosine similarity to the current page query.
External Product Feeds#
Route: /ai-reach/pages/feeds (admin); API at /api/ai-pages/feeds/
External product feeds let you pull product data from outside the PIM — useful for partner catalogs, staging environments, or Shopify storefronts.
| Field | Description |
|---|---|
name | Display name for the feed |
url | Feed endpoint URL (JSON or XML) |
format | json or xml |
field_map | JSON mapping from source fields to canonical product fields |
last_synced_at | Timestamp of last successful sync |
Syncing fetches the remote feed, applies the field map, and upserts products into a feed-scoped staging area (separate from the PIM catalog). The builder's fetch_products tool can target a specific feed by name.
Sync can be triggered manually via POST /api/ai-pages/feeds/<id>/sync/ or on a scheduled Celery beat task (fetch_external_feed).
Export Formats#
POST /api/ai-pages/pages/<id>/export/ queues an async export_page Celery task. Poll status via GET /api/ai-pages/pages/<id>/export/.
| Format | Description |
|---|---|
tsx | React component file. Each block renders as a typed component. A <script> tracking snippet is injected automatically. |
html | Static HTML with inline CSS from design tokens. Tracking snippet auto-injected before </body>. |
json | Raw block array as JSON. No tracking injection — use for headless rendering pipelines. |
The tracking snippet in TSX/HTML exports calls the /api/track proxy endpoint using a short-lived JWT page token. The tenant API key is never embedded in the export.
Delivery Methods#
Hosted (delivery app)#
Pages configured with delivery_method: hosted are served by the standalone apps/landing-page-delivery/ Next.js app (port 3002). This app is separate from the control plane.
Public URL pattern: /<tenant_slug>/<page_slug>
The delivery app:
- Renders blocks server-side using public block renderer components
- Injects CSS design tokens from the page's
design_snapshot - Generates a 5-minute JWT page token (signed with
DELIVERY_SIGNING_SECRET) - Embeds JSON-LD structured data
- Serves a
PageTrackerclient island for event tracking
API / self-hosted#
Pages with delivery_method: api are fetched via GET /api/ai-pages/public/pages/<tenant>/<slug>/ and rendered by the client. The response includes the blocks array and design_snapshot for client-side rendering.
Secure Event Tracking (Delivery App)#
The delivery app tracks user behavior without exposing the tenant API key to the browser.
Architecture#
Events tracked#
| Event | When fired | Extra fields |
|---|---|---|
page_viewed | On page mount | page_id, tenant_slug, page_slug |
block_viewed | Intersection Observer ≥50% viewport overlap maintained for dwell threshold | block_id, block_type, dwell_ms |
block_clicked | Click on any element inside a block | block_id, block_type, element |
JWT page token#
Generated server-side at render time. Payload:
The /api/track proxy verifies the token signature and expiry before forwarding. Expired tokens return 401. The signing secret (DELIVERY_SIGNING_SECRET) is never sent to the browser.
Billing Gate#
The WithinPageLimit permission class blocks page creation when the tenant's published page count reaches the plan limit. Attempts to create beyond the limit return 403 with:
Plan limits are defined in the billing configuration and checked on every POST /api/ai-pages/pages/.
Bulk Job Tracking#
Bulk page generation operations (e.g., generating all pages from active queries) run as BulkGenerationJob records.
GET /api/ai-pages/jobs/ — list all bulk jobs for the tenant.
GET /api/ai-pages/jobs/<id>/ — detail with total, completed, failed, and per-page status.
The landing pages list view (/ai-reach/pages) shows a progress bar for any active bulk job.
Data Model Extensions#
GeneratedPage fields added in M20:
| Field | Type | Notes |
|---|---|---|
blocks | JSONField | Ordered array of block objects [{id, type, props}] |
builder_status | CharField | draft, building, ready, error |
template | FK PageTemplate | Nullable — template this page was created from |
product_source | CharField | pim, segment, or feed |
delivery_method | CharField | hosted or api |
design_snapshot | JSONField | Snapshot of the design system at publish time |
bulk_job | FK BulkGenerationJob | Nullable — job that created this page |
Celery Tasks#
| Task | Description |
|---|---|
apps.ai_pages.tasks.generate_page(page_id) | Original M19b task — semantic search + Claude Haiku intro copy. Still used for query-driven pages without a builder session. |
apps.ai_pages.tasks.bulk_generate_pages_v2(tenant_id, job_id) | Creates pages for all active queries under a BulkGenerationJob, updates progress. |
apps.ai_pages.tasks.export_page(page_id, format) | Renders page to TSX / HTML / JSON, injects tracking snippet, stores result on PageExport. |
apps.ai_pages.tasks.fetch_external_feed(feed_id) | Fetches and syncs a single ExternalProductFeed. |
Evals#
The builder agent is evaluated with an LLM-as-judge pipeline.
- Dataset:
apps/evals/page_builder_golden_dataset.json— 10 test cases covering hero-only, product grid, full page, and brand extraction scenarios - Scoring dimensions:
helpfulness,accuracy,tool_use,conciseness, plusblock_quality(structural pass/fail: correct block types, valid props, no duplicate block IDs)
Run evals locally:
Dependencies#
- M19a — embedding pipeline must be running for
fetch_productssemantic search - M19b —
GeneratedPagemodel baseline - M13 — PIM product catalog (primary product source)
- Active AIQueries for query-driven page generation