Frontend Specs
Section 15 — Admin Landing Pages (`/admin/landing-pages`)
Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard.
docs/specs/frontend/admin-spec-04e-landing-pages.mdOn this page
- 15.1 Landing Page List
- 1. Purpose
- 2. Visual Layout
- 3. Data Source (tRPC endpoint)
- 4. Zod Schema
- 5. Fetch Frequency
- 6. Data Manipulations
- 7. Rationale
- 8. Interaction Flows
- 9. Error States
- 10. Role-Based Variations
- 15.2 Landing Page Editor
- 1. Purpose
- 2. Visual Layout
- 3. Data Source (tRPC endpoint)
- 4. Zod Schema
- 5. Fetch Frequency
- 6. Data Manipulations
- 7. Rationale
- 8. Interaction Flows
- 9. Error States
- 10. Role-Based Variations
- 15.3 A/B Test Manager
- 1. Purpose
- 2. Visual Layout
- 3. Data Source (tRPC endpoint)
- 4. Zod Schema
- 5. Fetch Frequency
- 6. Data Manipulations
- 7. Rationale
- 8. Interaction Flows
- 9. Error States
- 10. Role-Based Variations
Role access: ADMIN only.
VIEWER/EDITOR/CLIENTredirected to/dashboard. Layout: 3-tab interface: Pages, Editor, A/B Tests.1200pxmax-width. Sidebar:carbon(#12161E) background. Notes: Landing page management is for the marketing website (public-facing pages like homepage, pricing, features, etc.). These are separate from the client dashboard and admin dashboard. Pages are managed via a headless CMS approach or direct HTML/Markdown editing. All changes go through a preview → publish workflow.
15.1 Landing Page List#
1. Purpose#
Manage all public-facing marketing pages. Admins create, edit, publish, unpublish, and organize landing pages. View page performance (traffic, conversions, SEO score). The CMS for the marketing site.
2. Visual Layout#
Section A: Page Stats Bar (top, full width)
- 4 cards in a row:
- Total Pages:
{N}(published + draft + archived). - Published:
{N}live pages. Subtitle: "{N} homepage variants". - Drafts:
{N}unpublished pages. Amber if > 5 (indicates backlog). - Total Traffic (30d):
{N}sessions. Trend vs last 30 days.
- Total Pages:
- Card style: white, shadow,
8pxradius,text-2xlvalue.
Section B: Page Grid (below, full width)
- Card grid (3 columns desktop, 2 tablet, 1 mobile). Each card:
- Thumbnail: Screenshot or preview image of the page (200x150px). If no screenshot, placeholder with page icon.
- Title: Page title.
text-basetext-primary. e.g., "Homepage — V2". - URL:
/homeor/pricingor/features/ai-content.text-smtext-muted. - Status badge:
PUBLISHED(green),DRAFT(amber),ARCHIVED(gray),SCHEDULED(blue + "Publishes {date}"). - SEO score: Mini gauge (0-100). Green if > 80, amber if > 60, red if < 60. Tooltip: "SEO Score: {N}/100 — {issues} issues found".
- Metrics row:
text-smtext-muted. "👁 {N} views · ⏱ {N}s avg · 🎯 {N}% conversion" (last 30 days). Conversion = CTA clicks / page views. - Last edited: "Edited {relative time} by {name}".
- Actions: "Edit" (primary, Ember) + "Preview" (secondary) + "3-dot menu" (Publish, Unpublish, Archive, Duplicate, Delete, View Analytics).
- Homepage indicator: If page is set as homepage, star icon ⭐ next to title + "Homepage" badge.
- A/B test indicator: If page has an active A/B test, beaker icon 🧪 next to title + "A/B Active" badge.
Section C: Create Page Button
- "+ New Landing Page" button (ember, top right) → opens modal:
- Template selector: "Blank", "Homepage", "Pricing", "Features", "About", "Contact", "Blog Post", "Case Study", "Legal (Privacy/Terms)".
- Page name input.
- URL slug input (auto-generated from name, editable). Validates uniqueness.
- "Create Page" button.
Section D: Bulk Actions
- Select multiple pages (checkbox on card) → bulk bar: "{N} selected" + "Publish" + "Unpublish" + "Archive" + "Delete" + "Export".
3. Data Source (tRPC endpoint)#
admin.getLandingPages.useQuery({
status: z.enum(["PUBLISHED", "DRAFT", "ARCHIVED", "SCHEDULED", "ALL"]).default("ALL"),
search: z.string().optional(),
sortBy: z.enum(["DATE", "VIEWS", "CONVERSION", "SEO_SCORE"]).default("DATE"),
sortOrder: z.enum(["ASC", "DESC"]).default("DESC"),
page: z.number().min(1).default(1),
pageSize: z.number().min(1).max(50).default(24),
}, { refetchInterval: 30000 }); // 30s frequent
admin.createLandingPage.useMutation({
name: z.string().min(1).max(100),
slug: z.string().regex(/^[a-z0-9-]+$/),
template: z.enum(["BLANK", "HOMEPAGE", "PRICING", "FEATURES", "ABOUT", "CONTACT", "BLOG_POST", "CASE_STUDY", "LEGAL"]).default("BLANK"),
});
admin.publishPage.useMutation({ pageId: z.string().uuid() });
admin.unpublishPage.useMutation({ pageId: z.string().uuid() });
admin.archivePage.useMutation({ pageId: z.string().uuid() });
admin.deletePage.useMutation({ pageId: z.string().uuid() });
admin.duplicatePage.useMutation({ pageId: z.string().uuid() });
admin.setHomepage.useMutation({ pageId: z.string().uuid() });
4. Zod Schema#
const LandingPageStatusSchema = z.enum(["PUBLISHED", "DRAFT", "ARCHIVED", "SCHEDULED"]);
const LandingPageSchema = z.object({
id: z.string().uuid(),
name: z.string(),
slug: z.string(),
status: LandingPageStatusSchema,
isHomepage: z.boolean().default(false),
hasActiveAbTest: z.boolean().default(false),
abTestId: z.string().uuid().optional(),
seoScore: z.number().min(0).max(100).optional(),
seoIssues: z.number().optional(),
thumbnailUrl: z.string().url().optional(),
template: z.enum(["BLANK", "HOMEPAGE", "PRICING", "FEATURES", "ABOUT", "CONTACT", "BLOG_POST", "CASE_STUDY", "LEGAL"]).optional(),
views30d: z.number().optional(),
avgTimeOnPage: z.number().optional(), // seconds
conversionRate: z.number().optional(), // percentage
lastEditedAt: z.date(),
lastEditedBy: z.string(),
publishedAt: z.date().optional(),
scheduledPublishAt: z.date().optional(),
createdAt: z.date(),
});
const LandingPageListResponseSchema = z.object({
items: z.array(LandingPageSchema),
total: z.number(),
publishedCount: z.number(),
draftCount: z.number(),
archivedCount: z.number(),
totalTraffic30d: z.number(),
trafficTrend: z.enum(["UP", "DOWN", "STABLE"]).optional(),
hasMore: z.boolean(),
});
5. Fetch Frequency#
- Frequent (
30000ms / 30s): Page status changes (publish, unpublish, draft edits). 30s polling. - On-demand: Create, publish, unpublish, archive, delete, duplicate, sort, filter, search, set homepage.
- SSE: When page is published or status changes, push to grid. When new page created, prepend.
6. Data Manipulations#
- Status badges: PUBLISHED = green; DRAFT = amber; ARCHIVED = gray; SCHEDULED = blue + publish date.
- SEO score gauge: Mini circular gauge (40px diameter) on each card. 0-100. Color: green > 80, amber > 60, red < 60. Tooltip lists top 3 issues.
- Thumbnail: Auto-generated from page content on save. If no thumbnail, gray placeholder with page type icon.
- Metrics formatting: Views: "1,247" or "1.2K" or "1.2M". Time: "{N}s" or "{N}min {N}s". Conversion: "{N}%" (1 decimal).
- Homepage indicator: Star icon + "Homepage" badge. Only one page can be homepage. Setting a new homepage unsets the old one.
- A/B test indicator: Beaker icon + "A/B Active" badge. Links to A/B test detail.
- URL slug:
/prefix implied. Slug ishome,pricing,features/ai-content. Full URL shown in preview and on card. - Last edited: Relative time + editor name. "2 hr ago by John".
- Template labels: "Homepage", "Pricing", etc. on card if not blank. Helps organize.
- Traffic trend: ↑ green, ↓ red, — gray. Compares current 30 days to previous 30 days.
- Card hover: Shadow lift (
shadow-md). Click card (not action buttons) → opens editor.
7. Rationale#
- Marketing CMS: The landing page manager is a lightweight CMS for the marketing site. Admins need to update copy, launch new pages, and manage the homepage without engineering support.
- Card grid with thumbnails: Visual browsing. Thumbnails show what the page looks like. Admins can spot the right page at a glance.
- SEO score on card: SEO is critical for landing pages. A low SEO score means the page won't rank. Showing it on the card surface issues before they cost traffic.
- Conversion rate: The ultimate metric for landing pages. "This page gets 1000 views but 0.1% conversion" → the page is broken. "This page gets 500 views but 5% conversion" → double down on it.
- 30s polling: Page status changes during publishing workflows. 30s catches PUBLISHED transitions. Draft edits are less time-sensitive.
- Templates: Pre-built page structures. "Pricing" template has pricing table blocks. "Features" template has feature grid blocks. Reduces setup time from 30 minutes to 2 minutes.
- Homepage management: Only one homepage. Setting a new version is a common operation ("launch new homepage design"). One-click set homepage.
- A/B test integration: Landing pages are the primary subject of A/B tests. Showing which pages have active tests prevents accidental edits to test variants.
- Bulk actions: If a campaign ends, admin might need to unpublish 10 landing pages. Bulk unpublish handles this. Or if migrating to a new template, bulk archive old pages.
- Archive vs Delete: Archive hides the page but keeps it in the system. Delete is permanent. Archive is the safe default. Delete requires confirmation.
- Traffic stats: Marketing team needs to know which pages are performing. Views, time on page, and conversion rate are the core metrics. Integrated from Google Analytics or internal tracking.
- Draft backlog: If draft count > 5, amber stat card warns admin that pages are pending publication. Marketing team might have a backlog.
8. Interaction Flows#
- Create page: Click "+ New Landing Page" → modal → select template → type name → slug auto-generates (editable) → "Create" → toast "Page created." → navigates to editor with new page.
- Edit page: Click "Edit" on card → navigates to editor (
/admin/landing-pages/editor/{id}). - Preview: Click "Preview" → opens page in new tab at preview URL (e.g.,
/?preview={token}). Preview is live but not indexed by search engines. - Publish: Click "Publish" in menu → confirmation: "Publish '{name}'? It will be live at /{slug}." → "Publish" → status changes to PUBLISHED → toast "Page published. Live at /{slug}." → if first publish, "Set as homepage?" prompt appears (if homepage-type page).
- Unpublish: Click "Unpublish" → confirmation → status changes to DRAFT → toast "Page unpublished. No longer accessible publicly."
- Archive: Click "Archive" → confirmation → status changes to ARCHIVED → card moves to grayed state → toast "Page archived."
- Delete: Click "Delete" → confirmation: "Permanently delete '{name}'? This cannot be undone." → "Delete" → card removed → toast "Page deleted."
- Duplicate: Click "Duplicate" → new page created with " (Copy)" suffix → status DRAFT → toast "Page duplicated. Edit as needed." → card appears in grid.
- Set homepage: Click "Set as Homepage" in menu → confirmation: "Set '{name}' as the homepage? Current homepage will be demoted." → "Set" → star moves to this card → toast "Homepage updated."
- Bulk select: Checkbox on cards → bulk bar → "Publish 3 pages" → confirmation → all published → toast "3 pages published."
- SEO tooltip: Hover SEO gauge → tooltip: "SEO Score: 72/100. Issues: Missing meta description, H1 too long, Image alt text missing."
- Impersonation: Read-only. Pages viewable. Editor, preview, publish, unpublish, archive, delete, duplicate, set homepage all disabled. Can view SEO scores and metrics. Cannot modify anything.
9. Error States#
- Loading: Skeleton card grid (9 cards) + skeleton stats.
- Empty: "No landing pages yet. Create your first page to start building the marketing site."
- Slug conflict: If slug already exists, inline red error: "URL /{slug} is already used by '{pageName}'. Choose a different slug."
- Publish fail: Toast "Publish failed. Page content may have validation errors. Check editor."
- Homepage fail: Toast "Cannot set homepage. Page must be published first."
- Delete fail: Toast "Delete failed. Page may be referenced by A/B tests or navigation menus."
- SEO score unavailable: Gauge shows "—" in gray. Tooltip: "SEO analysis pending. Save page to trigger analysis."
- Metrics unavailable: "—" for views, time, conversion. Tooltip: "Analytics not configured. Connect Google Analytics in settings."
- No permission: Non-ADMIN redirected.
10. Role-Based Variations#
- ADMIN: Full access — create, edit, publish, unpublish, archive, delete, duplicate, set homepage, bulk actions, view analytics.
- Other roles: No access.
- Impersonation: Read-only. Grid viewable. SEO scores and metrics visible. Editor, preview, publish, all modifications disabled. Cannot create, edit, or delete pages.
15.2 Landing Page Editor#
1. Purpose#
Visual editor for building and editing landing pages. Block-based editor (like Notion, Webflow, or a lightweight page builder). Admins add sections, edit text, upload images, and configure SEO without writing code.
2. Visual Layout#
Section A: Editor Header (sticky top)
- Left: Page name (editable inline) + status badge (PUBLISHED/DRAFT) + "Last saved {relative time}".
- Center: Device preview toggle — Desktop 🖥️ | Tablet 📱 | Mobile 📱. Changes editor canvas width.
- Right: "SEO" button (opens SEO panel) + "Preview" button (secondary) + "Publish" button (ember, if DRAFT) or "Update" button (ember, if PUBLISHED).
- Unsaved changes: "Unsaved changes" badge in amber if content modified since last save. Auto-save every 30 seconds (if enabled in settings).
Section B: Editor Canvas (center, 70% width)
- Block-based editor. Each block is a section of the page:
- Hero block: Title, subtitle, CTA button, background image/color. Editable inline (click text → type). CTA button configurable (text, link, color).
- Feature grid: 2-4 column grid of feature cards. Each card: icon, title, description. Add/remove cards. Reorder via drag.
- Pricing table: Plan cards with features list. Toggle features on/off per plan. Edit prices inline.
- Testimonial: Quote + author name + photo. Upload photo or select from gallery.
- CTA block: Background + heading + button. Simple.
- FAQ: Accordion-style Q&A. Add Q&A pairs. Reorder via drag.
- Image gallery: Grid or carousel of images. Upload or select from media library.
- Text block: Rich text (Bold, Italic, Links, Lists). No headings (H1/H2 reserved for SEO panel).
- Embed block: YouTube, Loom, or custom iframe.
- Form block: Lead capture form. Fields: name, email, phone, company, message. Connect to CRM or lead dashboard.
- Navigation block: Menu items. Edit links, add/remove items, reorder.
- Footer block: Links, social icons, copyright, legal links.
- Block actions: Each block has a toolbar on hover: ↑ (move up), ↓ (move down), ⚙️ (settings), 🗑️ (delete), ➕ (add block below).
- Add block: "+ Add Block" button at bottom of canvas. Opens block picker modal (icons + names for all block types).
- Canvas background:
bg-whitefor page content. Outer editor area isbg-gray-50to distinguish canvas from chrome.
Section C: Block Settings Panel (right sidebar, 30% width)
- When a block is selected, settings panel shows block-specific options:
- Hero: Background color/image, text alignment, padding, CTA button style, text color (dark/light).
- Feature grid: Number of columns (2-4), gap size, card style (flat/elevated), icon color.
- Pricing: Plan names, prices, billing cycle, feature list, highlight "Most Popular" plan.
- Testimonial: Quote style, photo shape (circle/square), background color.
- Image gallery: Layout (grid/masonry/carousel), columns, gap, image size.
- Text: Font size, color, alignment, max-width.
- Form: Submit button text, success message, redirect URL, field validation rules, connect to lead source.
- Navigation: Style (horizontal/vertical/hamburger), sticky toggle, background color.
- Global settings tab: Page-level settings (SEO, meta tags, social images, analytics, custom CSS, custom JS).
Section D: SEO Panel (slide-in from right)
- Click "SEO" button → panel slides in:
- Meta title: Input (max 60 chars). Character counter. Preview of Google SERP snippet.
- Meta description: Textarea (max 160 chars). Character counter. SERP preview.
- Canonical URL: Input. Auto-filled with page slug.
- OG title: Input (defaults to meta title).
- OG description: Textarea (defaults to meta description).
- OG image: Image upload. Recommended 1200x630px. Preview shows how it looks on Facebook/LinkedIn.
- Twitter card: Same as OG but for Twitter (summary_large_image).
- Schema markup: Select type — "WebPage", "Product", "Organization", "FAQPage", "HowTo". Auto-generates JSON-LD based on page content.
- Keywords: Tags input. Add/remove keywords. SEO score updates live based on these fields.
- SEO score: Live gauge (0-100). Updates as fields are filled. Green > 80, amber > 60, red < 60. Issues list below: "Meta title missing", "Description too short", "No OG image", etc. Each issue has a "Fix" link that focuses the relevant field.
- SERP preview: Live mockup of how the page will appear in Google search results. Title, URL, description.
3. Data Source (tRPC endpoint)#
admin.getLandingPage.useQuery({ pageId: z.string().uuid() }, { refetchInterval: 0 }); // on-demand only
admin.saveLandingPage.useMutation({
pageId: z.string().uuid(),
content: z.record(z.any()), // block-based JSON structure
seo: z.object({
metaTitle: z.string().max(60).optional(),
metaDescription: z.string().max(160).optional(),
canonicalUrl: z.string().url().optional(),
ogTitle: z.string().optional(),
ogDescription: z.string().optional(),
ogImage: z.string().url().optional(),
twitterCard: z.enum(["summary", "summary_large_image"]).default("summary_large_image"),
schemaType: z.enum(["WebPage", "Product", "Organization", "FAQPage", "HowTo"]).optional(),
keywords: z.array(z.string()).optional(),
}),
settings: z.object({
customCss: z.string().optional(),
customJs: z.string().optional(),
analyticsId: z.string().optional(),
}).optional(),
});
admin.uploadImage.useMutation({
file: z.instanceof(File), // multipart upload
pageId: z.string().uuid().optional(),
});
admin.analyzeSeo.useQuery({
pageId: z.string().uuid(),
content: z.record(z.any()),
seo: z.record(z.any()),
}, { enabled: false }); // manual trigger
4. Zod Schema#
const BlockTypeSchema = z.enum([
"HERO", "FEATURE_GRID", "PRICING_TABLE", "TESTIMONIAL",
"CTA", "FAQ", "IMAGE_GALLERY", "TEXT", "EMBED", "FORM",
"NAVIGATION", "FOOTER",
]);
const BlockSchema = z.object({
id: z.string().uuid(),
type: BlockTypeSchema,
order: z.number(),
settings: z.record(z.any()), // block-specific settings
content: z.record(z.any()), // block-specific content
});
const LandingPageContentSchema = z.object({
blocks: z.array(BlockSchema),
global: z.object({
backgroundColor: z.string().optional(),
fontFamily: z.string().optional(),
maxWidth: z.string().optional(),
}).optional(),
});
const LandingPageSeoSchema = z.object({
metaTitle: z.string().max(60).optional(),
metaDescription: z.string().max(160).optional(),
canonicalUrl: z.string().url().optional(),
ogTitle: z.string().optional(),
ogDescription: z.string().optional(),
ogImage: z.string().url().optional(),
twitterCard: z.enum(["summary", "summary_large_image"]).default("summary_large_image"),
schemaType: z.enum(["WebPage", "Product", "Organization", "FAQPage", "HowTo"]).optional(),
keywords: z.array(z.string()).optional(),
});
const LandingPageEditorSchema = z.object({
id: z.string().uuid(),
name: z.string(),
slug: z.string(),
status: LandingPageStatusSchema,
content: LandingPageContentSchema,
seo: LandingPageSeoSchema,
settings: z.object({
customCss: z.string().optional(),
customJs: z.string().optional(),
analyticsId: z.string().optional(),
}).optional(),
lastSavedAt: z.date().optional(),
seoScore: z.number().min(0).max(100).optional(),
seoIssues: z.array(z.object({
field: z.string(),
message: z.string(),
severity: z.enum(["ERROR", "WARNING", "INFO"]),
})).optional(),
});
5. Fetch Frequency#
- On-demand: Page loaded once. Auto-save triggers saves without refetching. Publish/update triggers refetch.
- SSE: If another admin is editing the same page, show banner: "{name} is also editing this page." with "Reload" button. Prevents concurrent edit conflicts.
6. Data Manipulations#
- Block JSON: Each block is a JSON object with
id,type,order,settings,content. Editor maintains this structure in React state. Save sends full JSON to server. - Block reordering: Drag and drop via
react-beautiful-dndor similar. Order numbers recalculated on drop. - Inline editing: Click text in block → contentEditable or textarea overlay → type → blur → auto-save trigger (if enabled) or manual save.
- Image upload: Drag and drop onto image block or upload button. Multipart upload to S3/CDN. Returns URL. Progress bar during upload.
- Device preview: Desktop = 1200px canvas. Tablet = 768px. Mobile = 375px. Canvas width animates with
transition-width. - SEO score: Computed client-side from SEO fields + content analysis. Formula: meta title (20 pts), meta description (20 pts), OG image (15 pts), schema markup (15 pts), keywords (10 pts), H1 present (10 pts), image alt text (10 pts). Total 100. Issues list updates live.
- SERP preview: Live Google snippet mockup. Title blue, URL green, description gray. Updates as meta title/description change. Truncates at Google's actual cutoff (~60 chars title, ~160 chars description).
- OG image preview: Shows Facebook/LinkedIn card mockup. If no image, shows placeholder with warning.
- Auto-save: Every 30 seconds if changes detected. Shows "Saving..." → "Saved" → "Auto-saved at {time}". If auto-save fails, amber badge "Unsaved changes — save manually".
- Custom CSS/JS: Textarea in global settings. Syntax highlighted (basic). Validated for syntax errors on save (client-side only, not full CSS parser). Warning: "Custom CSS may break page layout. Test in preview."
7. Rationale#
- Block-based editor: No-code page building. Admins create professional landing pages without engineering. Blocks are the right abstraction — each block is a self-contained section (hero, features, pricing, etc.).
- Inline editing: Click and type. No form fields. The page looks like the final output. WYSIWYG. Reduces cognitive load — admin sees exactly what visitors will see.
- SEO panel: SEO is critical for landing pages. The panel is a guided checklist. "Fill in meta title" → "Add description" → "Upload OG image." Score gamifies the process. 100/100 = green = satisfaction.
- SERP preview: Shows exactly how the page will appear in Google. If the title is cut off, admin sees it and fixes it. Prevents "oops, my title is truncated in search results."
- Device preview: Mobile traffic is 50%+ for most sites. Admins need to see how the page looks on phone. One-click toggle between desktop, tablet, and mobile.
- Block templates: Pre-built blocks (hero, pricing, etc.) save time. Admin doesn't need to design a pricing table from scratch. Select "Pricing" block → 3 plans auto-generated → edit prices and features.
- Auto-save: Prevents data loss. If admin closes the tab accidentally, auto-save has their back. 30s interval is frequent enough to be safe but not so frequent it creates server load.
- Schema markup: Structured data helps Google understand the page. FAQ schema → rich results in SERP. Product schema → product snippets. Organization schema → knowledge panel. Automated generation reduces errors.
- Custom CSS/JS: Power-user feature. If admin needs a custom animation or a tracking script, they can add it without engineering. But it's behind a warning because it can break things.
- Concurrent edit detection: If two admins edit the same page, the second one sees a banner. Prevents "last save wins" data loss. Admin can choose to reload (and see the other admin's changes) or continue (risking conflict).
- Form block lead capture: Landing pages need lead capture forms. The form block connects directly to the Leads dashboard. Submissions appear in
/dashboard/leadsimmediately. No third-party form builder needed. - Image CDN: Uploaded images go to S3/CloudFront/CDN. Optimized and served fast. No need for admin to optimize images manually.
8. Interaction Flows#
- Add block: Click "+ Add Block" at bottom → modal with block icons → click "Pricing Table" → block inserted at bottom → pricing table with 3 default plans appears → edit prices and features inline.
- Edit text: Click "Pro Plan" in pricing block → text becomes editable → type "Enterprise Plan" → click outside → text updates → auto-save triggers.
- Move block: Hover block → drag handle appears → drag up/down → drop → order updates → auto-save.
- Delete block: Hover block → click trash icon → confirmation: "Delete this block?" → "Delete" → block removed with slide-out animation → auto-save.
- Block settings: Click block → right sidebar shows settings → change background color to
#F6F6F8→ block background updates live → auto-save. - Upload image: Click image placeholder in hero block → file picker → select image → upload progress → image appears in block → auto-save.
- SEO panel: Click "SEO" → panel slides in → type meta title → SERP preview updates live → SEO score increases from 45 to 65 → issues list updates: "Meta description missing" remains.
- Preview: Click "Preview" → new tab opens at preview URL → admin sees exact page as visitor would.
- Publish: Click "Publish" → confirmation: "Publish changes to '{name}'? Current version will be replaced." → "Publish" → status changes to PUBLISHED → toast "Page published. Live at /{slug}." → navigates back to page list.
- Save draft: Click "Save" (if no publish button, e.g., already published) → toast "Draft saved." → last saved time updates.
- Concurrent edit: If another admin saves while you're editing, banner appears: "This page was updated by {name} at {time}. Reload to see latest version." → "Reload" → page reloads with latest version. If you continue, your save might overwrite their changes.
- Impersonation: Read-only. Editor loads but all blocks are non-editable. "Preview" works. "SEO" panel viewable but fields disabled. No publish, no save. Banner: "Impersonation — Read Only. End session to edit."
9. Error States#
- Loading: Skeleton editor with placeholder blocks (5 blocks with shimmer).
- Save fail: Toast "Save failed. {error}." → "Unsaved changes" badge persists. "Retry Save" button appears.
- Upload fail: Toast "Image upload failed. Max size: 5MB. Supported formats: JPG, PNG, WebP." → image placeholder returns.
- SEO score fail: If content analysis fails, SEO panel shows "SEO analysis unavailable. Check content structure."
- Invalid slug: If slug is changed to invalid format (spaces, special chars), inline error: "URL slug can only contain lowercase letters, numbers, and hyphens."
- Slug conflict: If slug already exists, error: "URL /{slug} is already in use."
- Concurrent edit save: If you try to save after another admin saved, error: "Page was modified by another user. Reload to see latest version." → your changes are preserved in a temp state → "Reload and Merge" or "Overwrite".
- Custom CSS error: If custom CSS has obvious syntax errors (unclosed braces), warning: "Custom CSS may have syntax errors. Preview before publishing."
- No permission: Non-ADMIN redirected.
10. Role-Based Variations#
- ADMIN: Full access — edit all blocks, upload images, configure SEO, publish, preview, save, device preview, custom CSS/JS.
- Other roles: No access.
- Impersonation: Read-only. Editor loads but all blocks are non-editable. Preview works. SEO panel viewable but disabled. No publish, no save. Cannot create or modify pages.
15.3 A/B Test Manager#
1. Purpose#
Create and manage A/B tests for landing pages. Admins test page variants (different headlines, CTAs, layouts) against each other to optimize conversion rates. Track statistical significance and auto-declare winners.
2. Visual Layout#
Section A: Active Tests List (top, full width)
- Table: Test Name | Page | Variants | Status | Duration | Traffic Split | Conversion | Winner | Actions
- Test Name: "Homepage CTA Test" or "Pricing Page Headline Test".
- Page: Link to the parent page. Click → navigates to page editor.
- Variants: "A (control) + B (new headline)" or "A + B + C". Control always labeled "A".
- Status:
RUNNING(green + spinner),PAUSED(amber),COMPLETED(blue + winner declared),DRAFT(gray). - Duration: "Running for {N} days" or "Scheduled: {N} days left" or "Completed on {date}".
- Traffic split: "50/50" or "33/33/34" or "70/30" (custom). Visual bar showing split proportions.
- Conversion: Conversion rate per variant. "A: 2.1% · B: 3.4%". If B is winning, green arrow next to B.
- Winner: If COMPLETED, shows winning variant name + uplift. "B wins (+62% uplift)". Confidence level: "95% confidence".
- Actions: 3-dot menu:
- "View Results" → detailed results modal
- "Pause" / "Resume" (for RUNNING)
- "Declare Winner" (for RUNNING, manual override)
- "Duplicate" → create new test with same config
- "Archive" → move to archived tests
- "Delete" → permanent deletion
Section B: Test Results Modal (triggered by "View Results")
- Header: Test name + status + page + duration.
- Variant comparison table: Variant | Traffic | Conversions | Conversion Rate | Relative Uplift | Confidence | Status
- Status:
LEADING(green),LAGGING(red),CONTROL(blue),WINNER(gold trophy icon).
- Status:
- Conversion trend chart: Line chart. Conversion rate per variant over time. 1 line per variant (different colors). X-axis: days since start. Y-axis: conversion rate %.
- Traffic distribution chart: Bar chart. Visitors per variant. Should match the configured split. If not, there's a traffic allocation bug.
- Statistical significance: Large number display. "Confidence: {N}%". Green if > 95%, amber if > 80%, red if < 80%. "{N} more visitors needed to reach 95% confidence" if not there yet.
- Segment breakdown: Tabs for device (desktop/mobile), traffic source (organic/paid/social), geography (country). Each segment shows variant performance.
- Winner declaration: If confidence >= 95% and one variant is clearly better, "Auto-declare winner" button. If manual override, "Declare {variant} as winner" with confirmation.
Section C: Create Test Form (below list, full width)
- Click "+ New A/B Test" → form:
- Page selector: Dropdown of all published landing pages.
- Test name: Input.
- Hypothesis: Textarea. "We believe that changing the CTA from 'Sign Up' to 'Start Free Trial' will increase conversion by 20% because it reduces perceived commitment."
- Variants: Dynamic section. Start with Control (A) pre-filled with current page content. "Add Variant" button creates Variant B, C, etc. Each variant:
- Name: "B — New Headline" or "C — Shorter Form".
- Description: What changed.
- Content: Link to edit variant page (opens editor with variant loaded).
- Traffic split: Number input (%). Total must = 100%. Auto-balances if not manually set.
- Goal: Select conversion event — "Form submission", "Button click", "Page scroll to 50%", "Time on page > 2min".
- Duration: Number input (days). Default:
14. Minimum:7. Maximum:90. - Minimum visitors: Number input. "Run until {N} visitors per variant." Default:
1000. - Auto-declare winner: Toggle. If ON, automatically declares winner when confidence >= 95% and minimum visitors reached. Default: ON.
- "Start Test" button (ember, disabled until form valid).
Section D: Archived Tests (below, collapsible)
- Accordion: "Archived Tests ({N})". Click → expands showing completed/archived tests in a compact table:
- Test name, page, winner, uplift, completed date. No actions except "View Results" and "Restore" (unarchive).
3. Data Source (tRPC endpoint)#
admin.getAbTests.useQuery({
status: z.enum(["RUNNING", "PAUSED", "COMPLETED", "DRAFT", "ARCHIVED", "ALL"]).default("ALL"),
page: z.number().min(1).default(1),
pageSize: z.number().min(1).max(50).default(20),
}, { refetchInterval: 30000 }); // 30s frequent
admin.getAbTestResults.useQuery({
testId: z.string().uuid(),
}, { enabled: false }); // manual trigger
admin.createAbTest.useMutation({
pageId: z.string().uuid(),
name: z.string().min(1).max(100),
hypothesis: z.string().max(500).optional(),
variants: z.array(z.object({
name: z.string().min(1),
description: z.string().optional(),
pageVariantId: z.string().uuid(), // reference to variant page
trafficSplit: z.number().min(1).max(99),
})),
goal: z.enum(["FORM_SUBMISSION", "BUTTON_CLICK", "SCROLL_50", "TIME_ON_PAGE_2MIN"]),
durationDays: z.number().min(7).max(90).default(14),
minVisitorsPerVariant: z.number().min(100).default(1000),
autoDeclareWinner: z.boolean().default(true),
});
admin.pauseAbTest.useMutation({ testId: z.string().uuid() });
admin.resumeAbTest.useMutation({ testId: z.string().uuid() });
admin.declareWinner.useMutation({ testId: z.string().uuid(), variantId: z.string().uuid() });
admin.archiveAbTest.useMutation({ testId: z.string().uuid() });
admin.deleteAbTest.useMutation({ testId: z.string().uuid() });
4. Zod Schema#
const AbTestStatusSchema = z.enum(["RUNNING", "PAUSED", "COMPLETED", "DRAFT", "ARCHIVED"]);
const AbTestGoalSchema = z.enum(["FORM_SUBMISSION", "BUTTON_CLICK", "SCROLL_50", "TIME_ON_PAGE_2MIN"]);
const AbTestVariantSchema = z.object({
id: z.string().uuid(),
name: z.string(), // "A — Control", "B — New Headline"
description: z.string().optional(),
pageVariantId: z.string().uuid(),
trafficSplit: z.number().min(1).max(99),
visitors: z.number(),
conversions: z.number(),
conversionRate: z.number(),
relativeUplift: z.number().optional(), // vs control
confidence: z.number().optional(), // vs control
status: z.enum(["CONTROL", "LEADING", "LAGGING", "WINNER"]).optional(),
});
const AbTestSchema = z.object({
id: z.string().uuid(),
pageId: z.string().uuid(),
pageName: z.string(),
name: z.string(),
hypothesis: z.string().optional(),
status: AbTestStatusSchema,
variants: z.array(AbTestVariantSchema),
goal: AbTestGoalSchema,
durationDays: z.number(),
minVisitorsPerVariant: z.number(),
autoDeclareWinner: z.boolean().default(true),
startedAt: z.date().optional(),
completedAt: z.date().optional(),
winnerVariantId: z.string().uuid().optional(),
winnerDeclaredBy: z.string().optional(), // "AUTO" or admin name
overallConfidence: z.number().optional(),
visitorsNeededForConfidence: z.number().optional(), // remaining visitors needed
});
const AbTestResultsSchema = z.object({
test: AbTestSchema,
conversionTrend: z.array(z.object({
date: z.date(),
variantRates: z.record(z.number()), // { "A": 2.1, "B": 3.4 }
})),
trafficDistribution: z.array(z.object({
variantId: z.string(),
visitors: z.number(),
percentage: z.number(),
})),
segmentBreakdown: z.record(z.array(z.object({
segment: z.string(),
variantId: z.string(),
conversionRate: z.number(),
}))), // { "device": [...], "source": [...], "country": [...] }
});
5. Fetch Frequency#
- Frequent (
30000ms / 30s): A/B test status and conversion rates change as visitors interact. 30s polling. - On-demand: Create, pause, resume, declare winner, archive, delete, view results.
- SSE: When a test reaches statistical significance, push "Confidence reached" notification. When winner is auto-declared, push update. When new visitor data arrives, push conversion rate updates.
6. Data Manipulations#
- Traffic split bar: Visual bar divided by variant proportions. If 50/50, two equal halves. If 70/30, left segment 70% wide, right 30%. Color per variant.
- Conversion rate formatting: "{N}%" (1 decimal). "2.1%" or "3.4%".
- Relative uplift:
((variant_rate - control_rate) / control_rate) * 100. "+62%" in green if positive, "-15%" in red if negative. - Confidence formatting: "{N}%". Green if >= 95%, amber if >= 80%, red if < 80%. If < 95%, show "{N} more visitors needed" below.
- Status badges: RUNNING = green + spinner; PAUSED = amber; COMPLETED = blue; DRAFT = gray; ARCHIVED = gray.
- Winner display: Gold trophy icon 🏆 next to winning variant. "Winner" badge in gold. Uplift shown in large green text: "+62% uplift".
- Conversion trend chart: Line chart with 1 line per variant. Control = blue. Variants = other colors (green, orange, purple). X-axis: days. Y-axis: conversion rate. Tooltip: exact rate per variant on that day.
- Traffic distribution: Bar chart showing actual visitors vs configured split. If actual != configured, amber warning: "Traffic split is off by {N}%. Check allocation logic."
- Segment breakdown: Tabs for device, source, country. Each tab shows a table or mini bar chart of variant performance per segment. "Mobile users prefer Variant B (+80% uplift) while desktop users prefer A." → insight for responsive design.
- Hypothesis display: Test results modal shows the original hypothesis. After completion, admin can compare hypothesis to actual results. "We predicted +20% but got +62%."
- Duration countdown: For RUNNING tests, "Day {N} of {total}" or "{N} days remaining". If minimum visitors not reached by end of duration, test extends or shows warning.
- Auto-declare: When confidence >= 95% and min visitors reached, system automatically marks winner. Email notification to admin. Test status changes to COMPLETED. Winning variant becomes the new default page.
7. Rationale#
- A/B testing is essential for conversion optimization: Landing pages are investments. A/B testing ensures the best version is live. Without it, you're guessing.
- Hypothesis-driven: Every test starts with a hypothesis. "We believe X will improve Y because Z." This prevents random testing and focuses on learnings. After the test, hypothesis vs results = learning.
- Statistical significance: "Variant B has 3.4% conversion vs A's 2.1%" sounds good. But with 100 visitors, it's noise. With 10,000 visitors, it's real. Confidence level tells you if the result is meaningful. 95% is the industry standard.
- Auto-declare winner: Removes manual work. When the data is conclusive, the system acts. No admin needed to check the dashboard daily. Winner is promoted automatically. Loser is archived.
- Segment breakdown: Aggregate results can hide segment differences. "B wins overall (+20%)" but "B loses on mobile (-10%)." Mobile users need a different variant. Segment data reveals this.
- Traffic split visualization: Admins need to verify the split is working. If configured 50/50 but actual is 60/40, there's a bug in the allocation logic. Visual bar makes this obvious.
- 30s polling: Conversion rates change with every visitor. 30s keeps the data fresh. For a test with 1000 visitors/day, that's ~1 visitor per minute. 30s catches the trend.
- Variant editor integration: Each variant is a separate landing page. Admin edits variant B in the page editor, then the test runs it against the control. No special "variant editor" needed — reuses the landing page editor.
- Minimum visitors: Prevents premature conclusions. "100 visitors per variant" is the minimum for any statistical validity. Default 1000 gives good confidence.
- Duration limits: 7-day minimum (1-day tests are meaningless). 90-day maximum (prevents tests running forever). If a test hasn't reached significance in 90 days, the difference is probably too small to matter.
- Archive vs Delete: Completed tests are archived for reference. "What did we test in Q2?" → archived tests. Delete is for mistakes (e.g., created wrong test).
- Goal selection: Different pages have different goals. Homepage = button click or form submission. Pricing page = plan selection. Blog post = scroll depth or time on page. Flexible goal selection makes the tool work for any page type.
- Conversion trend chart: Visualizes when the variant pulled ahead. "B was tied for 3 days, then surged on day 4." → maybe a traffic source changed on day 4. Charts tell stories that tables don't.
8. Interaction Flows#
- Create test: Click "+ New A/B Test" → form → select page → name test → write hypothesis → add Variant B (links to editor to create variant) → configure traffic split (50/50 auto-balanced) → select goal → set duration → "Start Test" → confirmation: "Start test for '{pageName}'? Control and variants will be served to visitors." → "Start" → test status changes to RUNNING → toast "A/B test started. Results will appear in 24 hours."
- View results: Click "View Results" on RUNNING test → modal opens with live data → conversion rates update every 30s → watch trend chart fill in over time.
- Pause test: Click "Pause" → status changes to PAUSED → traffic routes 100% to control → toast "Test paused. All traffic going to control."
- Resume test: Click "Resume" → status changes to RUNNING → toast "Test resumed."
- Declare winner: If confidence >= 95%, "Declare Winner" button active → click → select winning variant → confirmation: "Declare {variant} as winner? It will become the default page. Other variants will be archived." → "Declare" → status changes to COMPLETED → toast "Winner declared. {variant} is now live."
- Auto-declare: If enabled, system auto-declares when criteria met → admin gets email notification → test status changes to COMPLETED → winning variant promoted.
- Archive: Click "Archive" on completed test → moves to archived section → toast "Test archived."
- Duplicate: Click "Duplicate" → new test created with same config + " (Copy)" suffix → status DRAFT → toast "Test duplicated. Edit variants and start when ready."
- Segment drill: In results modal, click "Mobile" tab → shows mobile conversion rates per variant → "Mobile users prefer {variant} by +{N}%".
- Impersonation: Read-only. Test list viewable. Results modal viewable. Create, pause, resume, declare winner, archive all disabled. No modifications.
9. Error States#
- Loading: Skeleton table (8 rows) + skeleton form.
- Empty: "No A/B tests yet. Create your first test to start optimizing conversions."
- Traffic split invalid: If variants don't sum to 100%, inline error: "Traffic split must total 100%. Current: {N}%."
- No variants: If only control (A) exists, "Add at least 1 variant to start a test."
- Declare winner fail: If confidence < 80%, "Cannot declare winner. Confidence too low ({N}%). Collect more data."
- Test duration exceeded: If test reaches max duration without significance, amber banner: "Test duration exceeded ({N} days). Difference may be too small to detect. Consider declaring a winner or extending."
- Variant editor fail: If variant page can't be loaded, toast "Variant page unavailable. It may have been deleted."
- Segment data unavailable: If not enough data for segment, "Not enough data for {segment} breakdown. Minimum 100 visitors per segment required."
- No permission: Non-ADMIN redirected.
10. Role-Based Variations#
- ADMIN: Full access — create, edit, start, pause, resume, declare winner, archive, delete, duplicate, view results, segment analysis.
- Other roles: No access.
- Impersonation: Read-only. Test list viewable. Results modal viewable. All actions disabled. Cannot create, modify, or control tests.