Portal Studio First-Run
Give a first-time Portal Studio visitor one thing: an account of the portal they were just handed, and the few rules that decide what happens next. It arrives as a single card in the Aero rail, once per user.
Why this shape
Beta feedback says the first landing is overwhelming. The research behind this spec (pattern benchmarks, builder-product teardowns, AI-onboarding analyses) is blunt about the obvious answers: auto-triggered tours are skipped ~70% of the time, slide-carousel modals are closed unread inside four seconds, and a tutorial adds nothing where the UI is already discoverable — it only raises perceived difficulty. What survives that evidence is orientation anchored to the user's own artifact. Portal Studio's entry is already generative, so the gap was never the blank canvas; it is the moment generation ends and five surfaces appear at once with no bridge from "Aero built something" to "I know what it built."
Scope
In: one arrival card in the Aero rail, a Help re-entry, a per-user flag, instrumentation.
Out, deliberately — a coachmark tour. One was built, tried and removed. The research that justifies tours has them beating other tours; against no tour at all, on a UI a CSM can discover, the case collapses. Webflow earns one because its Designer is genuinely alien — selecting a widget, typing in the inspector and clicking a labelled Preview button is not. The beta complaint was never "I can't operate this", it was "I don't know what I'm looking at". The one part of the tour worth keeping — hovering a row to locate what it names — survives in the card as something the vendor pulls rather than something pushed at them.
Also out: a persistent setup checklist (the publish popover's readiness checks already own that job) and a welcome carousel.
The card
Rendered in ORAskAIChat's thread-lead slot, styled as one of Aero's own messages — 28px gradient avatar, a bubble on the assistant's 16px 16px 16px 4px radius, its suggested-reply pills — so it reads as something Aero said, not a panel dropped into the rail.
Heading names the source honestly, because who chose the starting point changes what the vendor should trust: "I built your portal" when Aero authored the pages, "I started you from Supplier onboarding" when Aero answered a description with a catalog template, "You're starting from Supplier onboarding" when the vendor picked it themselves. A generated portal quotes the vendor's own description underneath.
Rows are the map. One per page, carrying Aero's own reason where it authored them and a line of copy from templatePageNotes.js for shipped templates, which cannot state a reason for themselves. Then the brand row — never absent, because a portal always inherits the organization's brand; only its claim changes, so it never says "applied" over stock styling. Then the editor's own three tabs, Brand · Layout · Content, named once so the rail reads as a map. Then where the canvas data comes from, which is the article's own strongest tip: every customer sees a different portal. Hovering or focusing a row spotlights the thing it names.
Rules are what happens when you act, stated as product copy rather than asked of the agent — an approximate answer about irreversible publishing is worse than none: publishing is one-way, and customers reach a portal through their playbook while projects already running keep the one they launched with.
Three chips: two actions (edit the welcome message, preview as the customer) and one question that hands the vendor to Aero — "What can I put on a page?" — scoped to what Aero can actually see, the widget library. A footer links the guide and dismisses the card.
Re-entry
StudioTopBar carries a ghost pi pi-question-circle Help button: the Portal Studio guide, and Ask Aero about this portal where the rail is mounted. An org without Aero gets the guide only — accepted deliberately, because Aero is the upsell this orientation belongs to.
Architecture
Store (portalStudio.store.js)
lastBuild = { source: 'generated' | 'template', description, name, pages: [{ id, navLabel, purpose }] }— session-lifetime, cleared on scope switch.commitGenerationrecords the generated case (it otherwise discards the plan, including the per-pagepurposethat makes the card Aero's own words);recordTemplateStartrecords the template case, fromselect_templatein the rail and frombeginNewScopefor a gallery pick. Page ids are retained so a row can spotlight its own node.descriptionis captured inaskAerowhenhiddenContext.intent === 'start'and must survivebeginNewScope— the entry screen callsaskAerobefore entering the editor, and the rail re-dispatches throughsendMessage, neveraskAero. Whichever path records the build consumes it, so a later build cannot inherit it.- Whether a brand is applied is read live (
hasPortalTheme), never snapshotted:apply_brandandbrand_ingestedcan land long after the portal was recorded.
Frontend (pages/portalStudio/firstRun/)
AeroArrivalRecap.vue— the card.templatePageNotes.js— one line per shipped-template page. Copy, not portal config: it describes a starting point to an author and is never part of a document a customer renders, so it stays clear of the document validator and the frozen contract.firstRunContent.js— where the guide lives.firstRunTelemetry.js— one guarded emitter.- Spotlight + anchors — surfaces declare
data-studio-anchor="pages-tree | page:<id> | brand-tab | canvas", bound from theANCHORSconstant so a rename cannot drift; a small module resolves them and toggles a.studio-spotlitring. No refs threaded through props, and the surfaces stay ignorant of the first run.
Touched components: AeroRail.vue (the card, and routing its chips), StudioTopBar.vue (Help), StudioLeftRail.vue (anchors on the pages tree, each page node and the mode switch), StudioCanvas.vue (canvas anchor), ComposeStep.vue (reads the flag on mount).
Persistence (backend)
ORPortalStudioUserState (or_portal_studio_user_state) — one row per user, user_id unique with ON DELETE CASCADE, plus a JSONB state. All columns Internal, so the SQL tool cannot see per-user UI state. GET/PUT /api/portal-studio/user-state read it and merge-patch it, filtered to first_run_seen_at and first_run_outcome so a stale or hostile client cannot grow the blob. Migration a7c1e93f45d8, downgrade round-trip tested.
The flag is per-user, org-wide — someone oriented once in any portal is not offered it again. localStorage was rejected: per-device, lost on clear, and untargeted repeat audiences was the named failure of the closest analog (Airtable's Interface Designer tour). Read once on Studio mount; a failed read leaves the flag unloaded, which offers nothing rather than nagging.
Behavior matrix
| Context | Arrival card | Help |
|---|---|---|
Aero authored the pages (plan_portal) | "I built your portal" | guide + ask Aero |
| Aero chose a template for a description | "I started you from X" | guide + ask Aero |
| Vendor picked a template from the gallery | "You're starting from X" | guide + ask Aero |
| From-scratch (blank) start | none — nothing to narrate | guide + ask Aero |
| Org without Aero | none — the rail never mounts | guide only |
| A later visit (flag set) | none | guide + ask Aero |
Copy
Sourced from the Portal Studio help article, whose five companion Looms — Getting Started, Branding & Layout, Content, Account Home, Applying a Portal to a Playbook — cover the same ground section for section.
Six ideas carry the orientation; everything else is reference:
- A portal is Brand + Layout + Content — the product's own vocabulary, and literally the rail's three tabs.
- You are editing a draft, and publishing is one-way.
- Portals reach customers through playbooks, and running projects keep the portal they launched with.
- Preview is the truth-teller — each customer sees a different portal.
- Copy personalizes per viewer; an unresolvable field renders as nothing.
- Aero does nearly everything except publish.
Deliberately withheld: the widget catalog, per-widget options, the six style levers, merge-field namespaces, account-home's rules. Front-loading reference is what gets orientation skipped, so those live behind the guide and Aero's answers.
Instrumentation
Portal Studio First Run Shown (source, page count) → Recap Chip Clicked (which) → Recap Dismissed, plus Portal Published carrying first_run_outcome, so a vendor who dismissed the card and shipped a portal reads as a success. Card completion is not the goal and a funnel that treated it as one would reward the wrong thing.
Success is first publish to an audience, with preview-as-customer plus ≥1 edit in the first session as the leading indicator. A spike of sub-four-second dismissals is the regression signal that we rebuilt the thing this design avoids.
Accessibility
Rows that act are buttons and carry the spotlight on focus as well as hover, so keyboard users get the same orientation; rows that only state a fact are not buttons. The spotlight ring meets contrast in both schemes through tokens.
Testing
- Store:
lastBuildretention throughcommitGenerationandrecordTemplateStart, clearing on scope switch, description survivingbeginNewScope, the live brand read, and the preview label derived from the fields the API actually sends. - Card: heading per source, rows from
lastBuild, the rules, the guide link, chip set and the question chip's real prompt, keyboard spotlight, hover fallback. - Anchors: each literal pinned against the component that declares it, so a rename fails a test instead of silently spotlighting nothing.
- Content: a note for every page of every shipped template, short enough to read.
- Backend: the key filter, and a DB round-trip for merge and per-user isolation.
Open questions
- Does the org app want a shared analytics wrapper, rather than each surface guarding
window.heapitself? - Should the card ever return for a materially different portal — a first account-home edit, say — or stay strictly once per user?