Skip to content

SEO: load the dashboard resiliently (preload + fetch fallback)#50027

Open
angelablake wants to merge 3 commits into
trunkfrom
add/seo-dashboard-resilient-data
Open

SEO: load the dashboard resiliently (preload + fetch fallback)#50027
angelablake wants to merge 3 commits into
trunkfrom
add/seo-dashboard-resilient-data

Conversation

@angelablake

@angelablake angelablake commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

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:

  • Server: read-only REST routes GET /jetpack/v4/seo/{overview,settings,ai} that reuse the existing data builders (manage_options-gated), and preload their responses into the page via rest_preload_api_request() — replacing the raw synchronous data blob.
  • Client: each tab reads its data from the preload synchronously on a normal load (instant, no flash, no request). When a path is missing, it fetches it from its REST route — showing a loading skeleton, retrying silently a couple of times, and surfacing a calm, recoverable "Try again" state only on a genuine repeated failure — rather than the old dead-end.
  • The data stores and the existing save flows are otherwise unchanged. The Content tab already loaded asynchronously and was unaffected.

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
Jetpack SEO loading fallback

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' )):

  • Open the SEO dashboard and confirm the Overview, Settings, AI, and Content tabs all render normally (this exercises the instant preload path — no skeleton should appear).
  • Force the degraded path without needing to reproduce the original timing bug: in the console run 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.
  • Failure state: set DevTools → Network to Offline, run the delete line, 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.
  • Confirm Settings and AI saves still work (toggle something → "Settings saved" snackbar), and that the "enable SEO tools" affordance still shows when the seo-tools module 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.

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>
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the add/seo-dashboard-resilient-data branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/seo-dashboard-resilient-data

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

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:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@jp-launch-control

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/packages/seo/src/class-initializer.php 144/250 (57.60%) -6.53% 26 💔

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

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>
@github-actions github-actions Bot added the Docs label Jun 28, 2026
@angelablake angelablake marked this pull request as ready for review June 28, 2026 03:15
@angelablake angelablake added [Status] Needs Review This PR is ready for review. and removed [Status] In Progress labels Jun 28, 2026
@angelablake angelablake requested a review from Copilot June 28, 2026 03:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +317 to +321
$data[ self::SCRIPT_DATA_KEY ]['preload'] = array_reduce(
self::rest_read_paths(),
'rest_preload_api_request',
array()
);
Comment on lines +315 to +316
// when momentarily absent (the load-error dead-end). See register_rest_reads() and
// the client `preload.ts`.
Comment on lines +1 to +37
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;
}
Comment on lines +48 to +56
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 };
}
Comment on lines +39 to +43
preload: {
[ OVERVIEW_PATH ]: { body: { content_coverage: SEEDED_COVERAGE } },
[ SETTINGS_PATH ]: { body: SEEDED_SETTINGS },
[ AI_PATH ]: { body: SEEDED_AI },
},
Comment on lines +18 to +23
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' } );
} );
@angelablake

Copy link
Copy Markdown
Contributor Author

Re: the Copilot review — I verified its rest_preload_api_request() findings against WordPress core (wp-includes/rest-api.php, rest_preload_api_request()):

  • Map keys are paths, not full URLs. Core does $path = untrailingslashit( $path ); $memo[ $path ] = array( 'body' => …, 'headers' => … ), so the entries are keyed by /jetpack/v4/seo/overview etc. — exactly what get-preloaded.ts reads. (Also confirmed at runtime: a normal load is instant with no skeleton; only deleting the preload triggers a fetch.)
  • No status field in the envelope. It's { body, headers }, and only 200 responses are stored. So adding status: 200 to the fixtures/tests would make them less faithful to core, not more — leaving them as-is.

So those suggestions are non-issues. The one valid catch — a code comment referencing a non-existent preload.ts — is fixed (now points to get-preloaded.ts / use-ensure-tab-data.ts).

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants