SEO: load the dashboard resiliently (preload + fetch fallback)#50027
SEO: load the dashboard resiliently (preload + fetch fallback)#50027angelablake wants to merge 3 commits into
Conversation
The Overview, Settings, and AI tabs read their state from a one-shot
synchronous snapshot of the page bootstrap. When that snapshot was missing or
stale at first paint — most often a stale bundle after a plugin update, seen on
Atomic — the tab dead-ended on a permanent "Unable to load…" error even though
the data was usually available a moment later.
Adopt the established wp-build preload pattern (as Podcast and Forms do):
- Server: read-only REST routes /jetpack/v4/seo/{overview,settings,ai} reusing
the existing data builders, preloaded into the page via
rest_preload_api_request().
- Client: each tab reads its data from the preload synchronously (instant, no
flash on a normal load); when a path is missing it fetches it from its REST
route — showing a skeleton, retrying silently, and surfacing a recoverable
"Try again" state only on genuine failure — instead of dead-ending. The data
stores and save flows are otherwise unchanged.
JETPACK-1777
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
Code Coverage SummaryCoverage changed in 1 file.
Full summary · PHP report · JS report If appropriate, add one of these labels to override the failing coverage check:
Covered by non-unit tests
|
Unit tests for get-preloaded (read/write the page preload) and use-ensure-tab-data (ready when preloaded; fetch + seed on miss; error after silent retries, recoverable via retry). Update the README's data section to describe the preload + fetch-fallback architecture and the new read routes. JETPACK-1777 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR makes the Jetpack SEO dashboard’s Overview/Settings/AI tabs resilient to missing or stale bootstrapped data by switching from a one-shot synchronous payload to a REST-preload-first approach with a fetch+retry fallback.
Changes:
- Add read-only REST endpoints for the dashboard’s initial state and preload their responses into script data.
- Add a client hook (
useEnsureTabData) to synchronously use the preload when present, otherwise fetch+retry and surface a recoverable error state. - Add loading/error UI components plus unit tests, and update package docs + changelog.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| projects/packages/seo/src/class-initializer.php | Registers read-only REST routes and injects preloaded REST responses into script data. |
| projects/packages/seo/routes/settings/stage.tsx | Gates Settings rendering on ensured preload/fetch status; seeds the settings store on recovery. |
| projects/packages/seo/routes/overview/stage.tsx | Adds preload/fetch gating with skeleton + recoverable error UI. |
| projects/packages/seo/routes/ai/stage.tsx | Gates AI rendering on ensured preload/fetch status; seeds the AI store on recovery. |
| projects/packages/seo/README.md | Updates architecture/data-loading documentation for preload + fallback approach. |
| projects/packages/seo/changelog/add-seo-dashboard-resilient-data | Adds changelog entry describing improved dashboard loading resilience. |
| projects/packages/seo/_inc/data/use-ensure-tab-data.ts | New hook implementing preload-first, fetch+retry fallback, and retry handler. |
| projects/packages/seo/_inc/data/test/use-ensure-tab-data.test.ts | Unit tests for the hook’s ready/fetch/error+retry behavior. |
| projects/packages/seo/_inc/data/test/get-preloaded.test.ts | Unit tests for reading/writing the preload map. |
| projects/packages/seo/_inc/data/test/fixtures/store-fixtures.ts | Updates store fixtures to seed state from the preload map. |
| projects/packages/seo/_inc/data/settings-store.ts | Seeds Settings store from preloaded REST body instead of direct script data payload. |
| projects/packages/seo/_inc/data/get-preloaded.ts | Adds preload reader/writer and REST path constants for the dashboard slices. |
| projects/packages/seo/_inc/data/get-overview.ts | Switches Overview slice reads to getPreloaded. |
| projects/packages/seo/_inc/data/ai-store.ts | Seeds AI store from preloaded REST body instead of direct script data payload. |
| projects/packages/seo/_inc/components/dashboard-skeleton.tsx | Adds a shared skeleton UI for degraded (fetching) loads. |
| projects/packages/seo/_inc/components/dashboard-skeleton.scss | Styles for the skeleton UI. |
| projects/packages/seo/_inc/components/dashboard-load-error.tsx | Adds a shared recoverable “Try again” error state UI. |
| projects/packages/seo/_inc/components/dashboard-load-error.scss | Styles for the load error UI. |
| $data[ self::SCRIPT_DATA_KEY ]['preload'] = array_reduce( | ||
| self::rest_read_paths(), | ||
| 'rest_preload_api_request', | ||
| array() | ||
| ); |
| // when momentarily absent (the load-error dead-end). See register_rest_reads() and | ||
| // the client `preload.ts`. |
| import { getScriptData } from '@automattic/jetpack-script-data'; | ||
|
|
||
| /** | ||
| * REST paths the dashboard hydrates its initial state from. Must match the | ||
| * routes registered in `Initializer::register_rest_reads()` (and the paths | ||
| * preloaded onto the page by `Initializer::inject_script_data()`). | ||
| */ | ||
| export const OVERVIEW_PATH = '/jetpack/v4/seo/overview'; | ||
| export const SETTINGS_PATH = '/jetpack/v4/seo/settings'; | ||
| export const AI_PATH = '/jetpack/v4/seo/ai'; | ||
|
|
||
| /** A single preloaded REST response, as emitted by `rest_preload_api_request()`. */ | ||
| interface PreloadedResponse { | ||
| body?: unknown; | ||
| } | ||
|
|
||
| type SeoScriptData = { | ||
| seo?: { | ||
| preload?: Record< string, PreloadedResponse | undefined >; | ||
| }; | ||
| }; | ||
|
|
||
| /** | ||
| * Read a REST response the server preloaded onto the page, with no request. On a | ||
| * normal load every dashboard path is present here at first paint, so the screens | ||
| * read their state synchronously. Returns `undefined` when a path wasn't preloaded | ||
| * — a stale or incomplete page snapshot — which is the dashboard's signal to fetch | ||
| * it instead of dead-ending. See [use-ensure-tab-data]. | ||
| * | ||
| * @param path - The REST path; one of the `*_PATH` constants. | ||
| * @return The preloaded response body, or `undefined` when absent. | ||
| */ | ||
| export function getPreloaded< T >( path: string ): T | undefined { | ||
| return ( getScriptData() as SeoScriptData | undefined )?.seo?.preload?.[ path ]?.body as | ||
| | T | ||
| | undefined; | ||
| } |
| export function writePreloaded( path: string, body: unknown ): void { | ||
| const scriptData = getScriptData() as SeoScriptData | undefined; | ||
| if ( ! scriptData ) { | ||
| return; | ||
| } | ||
| scriptData.seo = scriptData.seo ?? {}; | ||
| scriptData.seo.preload = scriptData.seo.preload ?? {}; | ||
| scriptData.seo.preload[ path ] = { body }; | ||
| } |
| preload: { | ||
| [ OVERVIEW_PATH ]: { body: { content_coverage: SEEDED_COVERAGE } }, | ||
| [ SETTINGS_PATH ]: { body: SEEDED_SETTINGS }, | ||
| [ AI_PATH ]: { body: SEEDED_AI }, | ||
| }, |
| it( 'reads a preloaded response body for a path', () => { | ||
| mockGetScriptData.mockReturnValue( { | ||
| seo: { preload: { [ OVERVIEW_PATH ]: { body: { hello: 'world' } } } }, | ||
| } ); | ||
| expect( getPreloaded( OVERVIEW_PATH ) ).toEqual( { hello: 'world' } ); | ||
| } ); |
|
Re: the Copilot review — I verified its
So those suggestions are non-issues. The one valid catch — a code comment referencing a non-existent |
The inject_script_data() comment pointed at a client file `preload.ts` that doesn't exist; the readers are `_inc/data/get-preloaded.ts` and `_inc/data/use-ensure-tab-data.ts`. Caught in review. JETPACK-1777 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes #
Resolves JETPACK-1777.
Proposed changes
The SEO dashboard's Overview, Settings, and AI tabs read their state from a one-shot, synchronous snapshot of the page bootstrap. When that snapshot was missing or stale at first paint — most often a stale/mismatched script bundle after a plugin update (seen on Atomic) — the tab dead-ended on a permanent-looking "Unable to load…" error, even though the data was usually available a moment later and a hard refresh always cleared it.
This adopts the established wp-build preload pattern (the same approach Podcast and Forms use) so the tabs load their data properly and recover on their own instead of dead-ending:
GET /jetpack/v4/seo/{overview,settings,ai}that reuse the existing data builders (manage_options-gated), and preload their responses into the page viarest_preload_api_request()— replacing the raw synchronous data blob.New unit tests cover the preload reader/writer and the per-tab loading hook (ready when preloaded; fetch + seed on miss; error after retries, recoverable). README's data section updated.
Screenshots
Jetpack.SEO.skeleton.load.mp4
Related product discussion/links
Does this pull request change what data or activity we track or use?
No.
Testing instructions
On a site with the Jetpack SEO product enabled (
add_filter( 'rsm_jetpack_seo', '__return_true' )):delete window.JetpackScriptData.seo.preload, then click into another tab. It should show the loading skeleton, fetch its data, and render the content — no "Unable to load" dead-end.deleteline, and switch tabs; after the silent retries you'll get a friendly message with a Try again button that re-fetches in place once you're back online.seo-toolsmodule is off.Verified on a WordPress.com Atomic site, where the original "Unable to load" error reproduced regularly on build switches and no longer occurs; the forced loading and failure states behave as described.