Skip to main content

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/guides lists 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}/guides and /s/{slug}/guides/{guide} render, per guide:
    1. the OVERRIDE when one exists and is not hidden;
    2. NOTHING (index omits; detail 404s) when hidden;
    3. the static default otherwise.
  • Overridden guides keep identical layout/JSON-LD structure — only text differs. datePublished, heroImage, sources and 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:150 checks the static list).

§3 Storage — and the blog-leak trap, named

One re_content_items row per overridden guide:

columnvalue
typebuyer_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.
slugsite-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 — whatever content-items routes 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}/guides and the guide's detail path (same mechanism Studio publish uses). ISR revalidate = 3600 means 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")

  1. Editing one paragraph in the dashboard changes exactly that paragraph on the live detail page; every other element byte-identical.
  2. Table cell edit renders inside the SAME styled table (the template's classes, not new markup).
  3. Hide removes the guide from the live index and 404s the detail URL; Show restores both.
  4. Revert restores the static text exactly.
  5. The override row NEVER appears on /s/{slug}/blog (the §3 trap, asserted against the live page with an override saved).
  6. A request without the capability is denied at the API, not just hidden in the UI.
  7. Positive control: with no override rows, list + detail pages are byte-identical to before this feature shipped.
  8. Contract test: no code path sets status='published' on a site_guide_override row.

§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).