WiseAI Realtor — Site Guide Editor Expected Output Spec
§0 Why this exists, and the incident behind it
The guide library (guides.ts) is hardcoded per site slug. Team Beckett — a
real paying customer — sent their own revisions to the cost-to-sell guide
four times; each application was a code deploy, each was mis-verified, and
the page kept opening in the wrong voice with an invented statistic ("roughly
90% of that is commission") published under their name. Their own edit would
have taken minutes.
The Content Studio can author guide-type items into re_content_items, but
the public /s/{slug}/guides/* pages never read that table. Two halves that
never met. This feature connects them for the built-in guides specifically.
Founder constraint (2026-08-31, verbatim intent): "our editor is crappy and how can we keep the nice charts, fonts etc?" — therefore structured editing ONLY. The charts, tables and typography live in the template; the editor must make it impossible to break them. No rich-text surface, no HTML paste.
§1 Expected output — dashboard
/realtor/app/website/guideslists every built-in guide for the account's site slug with: title, hub chip, state badge (Default|Edited|Hidden), and actions Edit / Hide (or Show) / Revert to default (only when edited/hidden).- Edit opens a structured form generated from the guide's actual shape:
- lede → one textarea
- each section: heading → input; each paragraph → textarea; each bullet → input; each table cell → input (table headers editable, row/column shape FIXED — no add/remove of columns in MVP);
- each FAQ: question input + answer textarea.
- No field accepts HTML; values render through the existing template exactly as static guide text does.
- Save persists the override and the public page serves it (after ISR revalidation, §4). Revert deletes the override row; the static default returns. Hide removes the guide from the public index AND 404s its detail page.
- Structure adds/removals (new sections, new guides, images) are OUT of scope for MVP — that is Content Studio territory.
§2 Expected output — public site
/s/{slug}/guidesand/s/{slug}/guides/{guide}render, per guide:- the OVERRIDE when one exists and is not hidden;
- NOTHING (index omits; detail 404s) when hidden;
- the static default otherwise.
- Overridden guides keep identical layout/JSON-LD structure — only text
differs.
datePublished,heroImage,sourcesand slugs are NOT editable in MVP and always come from the static guide. - If every guide is hidden the index renders its (existing) empty state; the
hub link disappearing from site nav is a KNOWN non-goal for MVP
(
site-pages.ts:150checks the static list).
§3 Storage — and the blog-leak trap, named
One re_content_items row per overridden guide:
| column | value |
|---|---|
type | buyer_guide when the guide's hub is buy, else seller_guide (satisfies the CHECK constraint) |
status | 'draft' FOREVER — the public blog surface lists PUBLIC_SURFACE_TYPES with status='published' (content-items.ts:350-353); a published guide-type row would appear on /blog. The merge reads metadata, never status. A contract test pins that no code path publishes a site-guide row. |
slug | site-guide--{guideSlug} (namespaced so it can never collide with a Studio-authored item) |
metadata | { kind: 'site_guide_override', site_guide_slug, hidden: boolean, guide: <Guide JSON minus non-editable fields> } |
No migration. Rejected alternative: local_businesses.metadata.website.guides
— that blob is concurrently rewritten by the PropTx/DDF sync path
(read-modify-write on the same row), and a guide save racing a sync could
clobber either side.
§4 API
/api/real-estate/site-guides?account_id= (query param REQUIRED — middleware
strips x-realtor-account-id on /api/real-estate/*):
GET— merged list: every built-in guide + state + override content. Capability:content:item:read(or the nearest existing content cap — whatevercontent-itemsroutes use; never invent a new cap).PUT{ guideSlug, guide }— validate against the static guide's shape (same section count, same table dimensions, string fields only, length caps); UPSERT the override row. Capability:content:item:create.POST{ guideSlug, action: 'hide' | 'show' | 'revert' }. Same capability.- Every write triggers revalidation of
/s/{slug}/guidesand the guide's detail path (same mechanism Studio publish uses). ISRrevalidate = 3600means without explicit revalidation an edit could take up to an hour — unacceptable after this week; explicit revalidate is REQUIRED, not nice.
§5 Acceptance tests (the checklist that gates "done")
- Editing one paragraph in the dashboard changes exactly that paragraph on the live detail page; every other element byte-identical.
- Table cell edit renders inside the SAME styled table (the template's classes, not new markup).
- Hide removes the guide from the live index and 404s the detail URL; Show restores both.
- Revert restores the static text exactly.
- The override row NEVER appears on
/s/{slug}/blog(the §3 trap, asserted against the live page with an override saved). - A request without the capability is denied at the API, not just hidden in the UI.
- Positive control: with no override rows, list + detail pages are byte-identical to before this feature shipped.
- Contract test: no code path sets
status='published'on asite_guide_overriderow.
§6 Out of scope (MVP)
AI generation for these guides; structural edits (add/remove sections, rows,
columns, guides); image upload; per-language variants; migrating the static
library into the DB; editing sources/datePublished/slug/SEO fields;
surfacing merged state in website-builder-data (dashboard preview shows static
until a follow-up).