Today Page V2
Minimal native-facing render and refresh contract for the Today Page V2 WebView surface.
Today Page V2 uses the common todayWebViewBridge transport for web → native
events. Native → web visibility lifecycle is common bridge behavior; see
Life Cycle Events.
Unknown todayPage.* events still follow the bridge rule: resolve undefined,
never reject.
Surface
Native loads:
/embed/feeds/v1?language=<language>language accepts en, zh, zh-Hant, and ja. Common regional aliases
such as zh-CN, zh-TW, zh-HK, and ja-JP are normalized by Web; missing
or unsupported values fall back to English.
When a native refresh performs a hard WebView reload, native adds or replaces
requestId:
/embed/feeds/v1?requestId=<request-id>requestId belongs to native. Web echoes it on render events and sends exactly
one refreshResult for that request.
Events
| Direction | Type | Meaning |
|---|---|---|
| native → web | todayPage.refreshRequested | Native starts a user- or system-initiated refresh without inventing extra refresh modes. Native creates a fresh requestId and sends it to Web; Web reloads the current embed URL with that requestId, then echoes it on the render events and final refreshResult. |
| native → web | todayPage.scrollToSection | Native asks Web to bring a rendered Today section into view and optionally flash a short highlight. The command targets batchId, because each rendered batch owns one section. Missing or not-yet-rendered sections are ignored. |
| web → native | todayPage.firstBatchRendered | The first contiguous widget batch has finished fetching its widgets, React has committed those widgets to the DOM, and one animation frame has passed. For a non-empty feed, this is the earliest point where Native can remove the initial loading UI. |
| web → native | todayPage.allBatchesRendered | Every initial batch in the page has reached a terminal render state. status: 'succeeded' means all widgets rendered; status: 'partial' means at least one widget fell back to a placeholder, but the page is still usable and visible. |
| web → native | todayPage.loadFailed | The Web surface failed before any batch became visible. Native should treat this as a pre-content failure for the current load or refresh request; widget-level failures after content is visible are reported through partial rendering, not this event. |
| web → native | todayPage.refreshResult | The terminal answer for the current native requestId. Native should stop pull-to-refresh or refresh spinners here, and should ignore results whose requestId is no longer current. Success usually follows firstBatchRendered; empty feeds complete on allBatchesRendered. |
Analytics Ownership
For /embed/feeds/v1 inside a native WebView:
- Native reports
app_sess_start,page_view, andpage_end. - Web reports Today Page/Card render results, card impressions, and card clicks
by sending
trackmessages through the bridge. Native enriches those events with its ownuid,did,sid,client_plat, andctxbefore forwarding them to PostHog. - For chat prefill actions, Web appends Today attribution query parameters to
the action link itself before navigation. Native routes the trusted in-app
deeplink normally and reports
chat_query_submitlater, when the user actually sends.
If /embed/feeds/v1 is opened in a normal browser with no native bridge, Web
uses posthog-js directly and owns the full event set for that browser session.
Native → Web
Native dispatches native → web events through the common DOM event described in Life Cycle Events.
todayPage.refreshRequested
interface TodayPageRefreshRequestedEvent {
type: 'todayPage.refreshRequested'
schemaVersion: 1
requestId: string
}When Web receives this event, it reloads the current embed URL with the same
requestId in the query. Native may do the same reload itself instead of
dispatching the event. In both cases, the next Web boot answers with
refreshResult for that requestId.
todayPage.scrollToSection
interface TodayPageScrollToSectionEvent {
type: 'todayPage.scrollToSection'
schemaVersion: 1
batchId: string
behavior?: 'auto' | 'smooth'
highlight?: boolean
}Web looks for the rendered section with data-batch-id === batchId, calls
scrollIntoView({ block: 'start', behavior }), and briefly highlights that
section when highlight !== false.
Defaults:
behavior: 'smooth'highlight: true
If the batch does not exist, is stale, or has not rendered yet, Web silently
ignores the command. Native can resend the same batchId later; Web treats each
native event as a fresh command.
Web → Native
Web sends these events with:
bridge.emit(type, payload)The native host parses known todayPage.* events, routes them to the Today Page
state owner, then resolves undefined. Unknown fields should be ignored.
todayPage.firstBatchRendered
interface TodayPageFirstBatchRenderedEvent {
type: 'todayPage.firstBatchRendered'
schemaVersion: 1
requestId?: string
batchId: string
elapsedMs: number
}This is the loading gate for non-empty feeds. Web emits it after the first contiguous widget batch has committed to the DOM and one animation frame has passed. Native should hide the initial skeleton here.
Empty feeds do not emit firstBatchRendered; they complete through
allBatchesRendered.
todayPage.allBatchesRendered
interface TodayPageAllBatchesRenderedEvent {
type: 'todayPage.allBatchesRendered'
schemaVersion: 1
requestId?: string
status: 'succeeded' | 'partial'
empty: boolean
elapsedMs: number
}This is the completion/telemetry point. Native should not wait for this event to
hide loading when firstBatchRendered already fired.
status: 'partial' means at least one widget rendered a fallback placeholder,
but the page is still user-visible.
todayPage.loadFailed
interface TodayPageLoadFailedEvent {
type: 'todayPage.loadFailed'
schemaVersion: 1
requestId?: string
error: string
elapsedMs: number
}Web emits this only if the page fails before any batch becomes visible. Widget
fallbacks after content is visible are not loadFailed.
todayPage.refreshResult
interface TodayPageRefreshResultEvent {
type: 'todayPage.refreshResult'
schemaVersion: 1
requestId: string
status: 'succeeded' | 'failed'
terminalEvent: 'firstBatchRendered' | 'allBatchesRendered' | 'loadFailed'
elapsedMs?: number
error?: string
}refreshResult answers refreshRequested or a hard reload with
?requestId=.... Native stops pull-to-refresh on this event.
Relationship:
- Native creates
requestId. - Native either dispatches
todayPage.refreshRequested, or hard-reloads the URL withrequestId. - Web renders.
- Web emits one terminal
refreshResultfor thatrequestId.
Success rules:
- Non-empty feed:
refreshResult(status: 'succeeded')followsfirstBatchRendered. - Empty feed:
refreshResult(status: 'succeeded')followsallBatchesRendered(empty: true). - Pre-visible failure:
refreshResult(status: 'failed')followsloadFailed.
Native must ignore a refreshResult whose requestId is not the current
refresh request.
Card Action Deeplinks
Web does not send a separate bridge message for card action clicks. On user
click inside a Today card, Web reports today_card_click through track and
then appends attribution directly to Today chat deeplinks before the WebView
navigates:
today://chat/send?text=...&today_source=today_page_v2&today_card_id=...&today_bundle_hash=...&today_batch_id=...&today_source_trace_id=...&msg_trigger_ele_id=...Native should route the resulting today://chat... URL as a trusted in-app
deeplink. today_card_id and msg_trigger_ele_id are the stable wgt_ widget
row id, while today_bundle_hash carries the content-addressed bundle hash.
The row id should be carried into the next manual chat_query_submit. Editing
the prefilled draft must not clear that id; sending must consume and clear it; a
later card click overwrites it.
Implementation Checklist
Native:
- Load
/embed/feeds/v1for Today Page V2. - Implement the common bridge channel and resolve known/unknown
todayPage.*events withundefined. - Report
app_sess_start,page_view, andpage_endfrom native for the embed page. - Forward Web
trackmessages to the native analytics pipeline so native base fields are used. - On native refresh, create
requestIdand pass it to Web. - Hide initial loading on
firstBatchRendered, or onallBatchesRendered(empty: true)for an empty feed. - Stop pull-to-refresh on
refreshResult. - Use
todayPage.scrollToSectionwhen native needs to focus a specific Today section and show a short visual highlight. - Use common
webView.visibilityWillChange/webView.visibilityChangedfor visibility and impression gating. - Route trusted
today://chat...action links withmsg_trigger_ele_idinto the nextchat_query_submitwhen chat is prefilled.
Web source files:
apps/web/src/canvas-boot/boot.tsapps/web/src/canvas-boot/feed-canvas.tsxapps/web/src/canvas-boot/today-page-lifecycle.tsapps/web/src/canvas-boot/today-page-refresh-request.tsapps/web/src/app/embed/_feed-shell.tsx