Jetpack SEO: introduce package foundation and Site visibility Overview#49203
Conversation
|
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 🤖 🔴 Action required: We would recommend that you add a section to the PR description to specify whether this PR includes any changes to data or privacy, like so: Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! Jetpack plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. |
Code Coverage SummaryCoverage changed in 1 file.
|
| return createElement( | ||
| tagName, | ||
| { | ||
| className: clsx( styles.bounds, styles[ width ], className ), | ||
| }, | ||
| children | ||
| ); |
There was a problem hiding this comment.
Let's rather use JSX which is much more readable.
| .statusOk { | ||
| background: #4ab866; | ||
| } | ||
|
|
||
| .statusWarn { | ||
| background: #dba617; | ||
| } | ||
|
|
||
| .statusErr { | ||
| background: #cc1818; | ||
| } |
There was a problem hiding this comment.
Let's use design system tokens for colours --wpds-*) from @wordpress/theme for ok/warn/err.
| display: flex; | ||
| justify-content: flex-end; | ||
| gap: 8px; |
There was a problem hiding this comment.
You can just use the Stack component from @wordpress/ui for flex layouts to avoid lots of CSS.
|
|
||
| .loading { | ||
| padding: 16px 0; | ||
| color: #757575; |
There was a problem hiding this comment.
Let's use style token colors from @wordpress/theme.
| } | ||
|
|
||
| .loading { | ||
| padding: 16px 0; |
There was a problem hiding this comment.
Let's use spacing tokens from @wordpress/theme.
| .placeholder { | ||
| padding: 48px; | ||
| text-align: center; | ||
| color: #757575; | ||
| } |
There was a problem hiding this comment.
Let's use Stack and style tokens.
| // Override AdminPage's default `--jp-white` background with the admin-ui | ||
| // neutral-surface token (#fcfcfc). Targets the stable `.jp-admin-page__page` | ||
| // hook (passed via `className` from `<AdminPage>`) rather than admin-ui's | ||
| // internal `.admin-ui-page*` classes, which are hashed in the standalone | ||
| // admin-ui build and only literal in some WP core bundles. Double-class on | ||
| // `.neutralBg` for specificity over AdminPage's own `.background` rule. | ||
| .neutralBg.neutralBg { | ||
|
|
||
| &:global(.background), | ||
| :global(.jp-admin-page__page) { | ||
| background-color: var(--wpds-color-bg-surface-neutral, #fcfcfc); | ||
| } | ||
| } |
There was a problem hiding this comment.
This is now an SEO page overriding a Jetpack override over the core colour, flipping back to the core. :-) Doesn't make sense. Let's instead have the design consolidate designs across pages so we don't need to do exceptions, or otherwise avoid this messy way of stacking overrides.
| // Padding around the routed content on Overview and Settings. The admin-ui | ||
| // Page renders children unwrapped (no `.admin-ui-page__content` exists unless | ||
| // `hasPadding` is passed, which jetpack-components' AdminPage doesn't do), so | ||
| // Shell wraps the Outlet in this container and we pad it here. | ||
| .paddedContent { | ||
| padding: var(--wpds-dimension-padding-2xl, 24px); | ||
| } | ||
|
|
There was a problem hiding this comment.
This doesn't make sense 😅
Like note says there's hasPadding prop in Page component, which we should use instead of CSS overrides, which are using CSS selectors which can't be relied upo as stable API.
| // Content tab hosts a full-height DataViews. No extra padding so it can fill | ||
| // the region edge-to-edge. | ||
| .fullBleed { | ||
| padding: 0; | ||
| } |
There was a problem hiding this comment.
Same as above.
When hasPadding is falsy, there's no padding already.
| // The admin-ui `Page` component renders the tabs prop between the header and | ||
| // the content container (not inside the header like Forms' own Page does). | ||
| // Wrap them so they visually continue the header: same white background, same | ||
| // horizontal padding (matching admin-ui's `--wpds-dimension-padding-2xl` = 24px | ||
| // on `.admin-ui-page__header`), and a soft divider at the bottom. This keeps | ||
| // the tabs visually anchored to the header and makes their starting edge line | ||
| // up with the page title. | ||
| .tabsBar { | ||
| background: var(--wpds-color-bg-surface-neutral-strong, #fff); | ||
| padding-inline: var(--wpds-dimension-padding-2xl, 24px); | ||
| border-bottom: | ||
| var(--wpds-border-width-xs, 1px) solid | ||
| var(--wpds-color-stroke-surface-neutral-weak, #e4e4e4); | ||
| } |
There was a problem hiding this comment.
Let's keep Tabs behaviour centralized at AdminPage and not do local modifications; it gets very difficult to maintain otherwise and whole point of centralizing it in first place.
Tabs as a more integrated part of Header is coming up in next versions of Admin UI component so it'll be better.
| @@ -0,0 +1,48 @@ | |||
| .placeholder { | |||
| padding: 48px; | |||
There was a problem hiding this comment.
Let's use sapcing tokens from @wordpress/theme.
| * The chrome inside the HeaderActionsProvider. Split so the AdminPage | ||
| * render can read header actions from context. | ||
| */ | ||
| const ShellChrome: FC = () => { |
There was a problem hiding this comment.
This all seems very complicated; is it worth having as "shell" which override background and padding in AdminPage, which like I noted elsewhere should just be features of AdminPage?
It's a lot of code for very minimal gain.
| __( 'SEO', 'jetpack-seo' ), | ||
| __( 'SEO', 'jetpack-seo' ), |
There was a problem hiding this comment.
We've been avoiding translating product names ("Jetpack SEO"), internal ref for context p1HpG7-wSr-p2
| * @return void | ||
| */ | ||
| public static function add_menu_item() { | ||
| if ( ! self::is_seo_tools_module_active() ) { |
There was a problem hiding this comment.
This module check might make sense somewhere higher up, before we even let the class initialise and run its functions.
For example, we're happily registering routes and other functionality even when the module is disabled.
| } | ||
| /> | ||
| { data.sitemap_active && ( | ||
| <ExternalLink href={ data.sitemap_url }>{ __( 'View', 'jetpack-seo' ) }</ExternalLink> |
There was a problem hiding this comment.
Let's use newer Link component from @wordpress/ui instead.
| @@ -0,0 +1,45 @@ | |||
| import { Notice, Spinner } from '@wordpress/components'; | |||
There was a problem hiding this comment.
Let's use more modern Notice from @wordpress/ui instead.
Add a shared `BoundedLayout` wrapper to `@automattic/jetpack-components` with two MSD-aligned presets — `compact` (660px) and `wide` (1344px) — so products can keep visual alignment across Overview / Settings / Dashboard screens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new `automattic/jetpack-seo` package that mounts a React admin at `wp-admin/admin.php?page=jetpack-seo`. Menu item gates on the `seo-tools` module being active. This PR is the first of a staged series introducing the Jetpack SEO product; it ships only the package scaffold and the Overview screen's Site visibility card. Follow-up PRs add the Settings, Per-post SEO (Content), and AI tabs, along with their REST writers and respective Overview cards. New package - PHP: `Initializer` (admin menu registration + asset enqueue, gated on `seo-tools`), `REST_Controller` exposing `/jetpack-seo/v1/overview` only. - React SPA: `createHashRouter` + `RouterProvider`, `AdminPage` from `@automattic/jetpack-components`, TanStack Query for server state. - Single screen: Overview with a Site visibility card (search-engines allowed, sitemap active/inactive, SEO tools active/inactive). All other Overview cards land with the PRs that own their underlying features. Jetpack plugin wiring - `class.jetpack.php`: load the new package in `late_initialization()`. - `composer.json`: require `automattic/jetpack-seo`. Co-Authored-By: Angela Blake <angela.blake@a8c.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI feedback from the initial push surfaced four classes of issues:
- Typecheck failures from the partial scope:
- Remove the unused "Manage settings" button from SiteVisibilityCard
(referenced the trimmed JetpackSeoRoutes.Settings constant). The button
returns in PR #3 when the Settings tab lands as its deep-link target.
- Update useSimpleMutation onSuccess to TanStack Query v5's 4-arg form
(data, variables, onMutateResult, context) — v5 added onMutateResult
between variables and context.
- Project-structure expectations for a new package:
- Add .phan/config.php so static analysis recognises the package's PHP.
- Add changelog/.gitkeep so the changelog directory survives release.
- Changelog format:
- Reset CHANGELOG.md to the keep-a-changelog header only (no pre-release
`[0.1.0-alpha] - unreleased` block — pending entries live in changelog/).
- Change the Jetpack-plugin changelog Type from `added` to `enhancement`
(plugin/jetpack uses major|enhancement|compat|bugfix|other, not the
keep-a-changelog vocabulary the packages use).
Co-Authored-By: Filipe Varela <filipe@automattic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI surfaced two more issues on the foundation PR: - Initializer still called `LLMS_Txt::init()`, `AI_Crawlers::init()`, and `Schema_Builder::init()` — those classes live in later PRs (#4 and #6). Remove the calls; they return when their classes land. - Initializer registered `maybe_redirect_legacy_surfaces` on `admin_init`, which would have 301-redirected `?page=jetpack&tab=seo|traffic` to the new admin page. But the legacy Traffic page is still live on trunk in this PR's window, so redirecting now strands users mid-task. Remove the hook + the method entirely; the redirect lands in PR #5b alongside the discoverability banner and the actual deletion of the legacy surfaces. - Phan can't see `Jetpack_SEO_Utils` (lives in plugins/jetpack, not this package). The runtime-safety `class_exists` checks already guard the calls; add `@phan-suppress-next-line PhanUndeclaredClassMethod` so static analysis stops flagging them. Co-Authored-By: Filipe Varela <filipe@automattic.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the SEO admin page's reliance on admin-ui's internal `.admin-ui-page*` classes — which are hashed in the standalone `@wordpress/admin-ui` build and only literal in some WP core bundles — in favor of the stable `.jp-admin-page__page` hook that `<AdminPage>` forwards onto admin-ui's `<Page>` and the shared `jetpack-admin-page-layout-wp-build` mixin from `@automattic/jetpack-base-styles`. This also delivers the sticky `JetpackFooter` behavior previously proposed as a one-off in `<AdminPage>`'s own SCSS — the shared mixin already pins the footer to the bottom of the viewport via the stable selector chain, which is the same path Newsletter / Protect / Backup / Search / VideoPress take. See CGastrell's review on #49201. Per-page opt-in lives in a new non-module `_inc/admin-page-layout.scss` (matching the Newsletter pattern), so the mixin's body-class scope stays out of the CSS-modules file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first PR push only included `phpunit.11.xml.dist`, so CI's PHP test matrix (PHP 7.4–8.5) failed at config-read because PHPUnit 8/9/12 had no config to select. Adds `phpunit.8.xml.dist`, `phpunit.9.xml.dist`, and `phpunit.12.xml.dist`, mirrored verbatim from `packages/ip`'s set. Adds a `test` script + a `tests/jest.config.js` so the JS-tests CI job doesn't error with `Missing script: test` either. Uses `--passWithNoTests` so the package can ship before it grows JS tests, and inherits the shared `jetpack-js-tools/jest/config.base.js` for when it does (asset stub for `.scss` imports, ts/jsx transform, JSDOM environment). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without `.gitattributes`, the production-publish pipeline that vendors `automattic/jetpack-seo` into the Jetpack plugin's `jetpack_vendor/` respects `.gitignore` and drops the webpack-built `build/` directory. The PHP source files come along (they're not gitignored), so the SEO admin menu item appears and the mount `<div id="jetpack-seo-root">` renders — but the enqueued `build/index.js` URL 404s, leaving the admin page visually blank. Mirrors Newsletter's `.gitattributes` (the canonical wp-build package): - `/build/** production-include` overrides gitignore for build output - `*.tsx`/`*.ts`/`*.scss` under `_inc/` and `src/` are `production-exclude` since webpack has already compiled them into `build/`. Confirmed against the 404 on `wp-content/plugins/jetpack-dev/jetpack_vendor/automattic/jetpack-seo/build/index.js` in JetpackBeta on `sweetly-partial-gazelle.jurassic.ninja`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review feedback on #49203 — replace hand-rolled data plumbing with Jetpack's established patterns. Data layer: - Register a read-only `overview` data-sync entry (namespace `jetpack_seo`) via Data_Sync_Readonly + Schema, bootstrapped onto the page with attach_to_plugin() so the app hydrates from window.jetpack_seo without a round-trip. - use-overview -> useDataSync; overview-types -> Zod OverviewSchema; providers -> DataSyncProvider. - Delete class-rest-controller.php, use-simple-query.ts and the unused use-simple-mutation.ts; drop the dead jetpackSeoRest bootstrap and the now-unused @wordpress/api-fetch and @tanstack/react-query deps. Module gating: - Move the seo-tools module check up into Initializer::init() so no routes, menu, or assets register when the module is off (matches how other Jetpack modules gate), removing the redundant per-method check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback on #49203 — replace hand-rolled UI with the platform's primitives. - Collapse shell.tsx onto AdminPage (drop the header-actions slot/fill context and the background/padding CSS overrides); delete header-actions-context.tsx and _inc/style.module.scss. - Use @wordpress/ui Notice (compound), Stack, and Link in place of @wordpress/components Notice/ExternalLink and flexbox CSS. - Give StatusDot its own style module using @wordpress/theme color tokens; tokenize remaining colors and spacing. - Extract ternary __() labels to module scope so i18n extraction survives the production minifier. - Remove the unused BoundedLayout from @automattic/jetpack-components. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the SEO foundation page off the custom webpack/React-Router setup onto @wordpress/build (wp-build), matching the live pattern the other recently-modernized Jetpack admin pages use (modeled on the Podcast package). - Build: add the wpPlugin block + wp-build scripts, the routes/index entry (stage/route/package.json), and drop webpack.config.js, babel.config.js, react-router, and zod. - Data layer: replace the data-sync Overview wiring with the shared script-data layer. wp-build pages load as ES modules, so wp_localize_script can't bootstrap them; the server now injects state onto window.JetpackScriptData.seo via the jetpack_admin_js_script_data filter (Podcast/Newsletter pattern), read synchronously in get-overview.ts. - PHP: keep the seo-tools module gate and the Admin_Menu top-level menu; load build/build.php + register WP_Build_Polyfills and alias the screen id on current_screen so wp-build's self-hooked enqueue fires for our slug. - Styles: convert CSS-module SCSS to plain prefixed SCSS (no wp-build package uses CSS modules). - composer: drop wp-js-data-sync + schema, add wp-build-polyfills. Verified: dev + production build, tsgo typecheck, php -l, phpcs, eslint, and i18n string extraction all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sync the branch with trunk and bump @wordpress/build 0.13.0 -> 0.14.0 to match the current wp-build cohort. The branch had fallen ~99 commits behind trunk, which had since bumped @wordpress/build; the stale 0.13.0 pin left the PR-merge lockfile referencing build variants trunk no longer defines, failing CI's frozen install. Regenerated pnpm-lock.yaml on the merged tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ange The SEO package's composer.json changed (dropped wp-js-data-sync + schema, added wp-build-polyfills), so the consuming plugin's lock was stale and failed CI's "Lock files are up to date" check. Regenerated via tools/composer-update-monorepo.sh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two CSS regressions from the wp-build rebuild, caught in JN testing: - Page had no content padding: the layout SCSS included the `jetpack-admin-page-layout-wp-build` mixin, which only carries breadcrumb / anchor resets — not the content layout. Switch to the plain `jetpack-admin-page-layout` mixin (as Scan/Podcast do) and target both the top-level and Jetpack-submenu body classes. - Status dots rendered near-black: the plain `--wpds-color-fg-content-*` tokens are the darkest text shades (#2900 / #470000). Use the `-weak` variants (#007f30 / #cc1818 / amber), the vivid indicator colors the shared StatusIndicator pattern uses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The card sat flush against the header and page sides. `<AdminPage>` renders
its content with `horizontalSpacing={0}` and no `hasPadding`, so only the
header is padded — content padding is the consumer's responsibility (as in
Scan/Podcast). The earlier frontend cleanup removed the old `.paddedContent`
override without replacing it. Wrap the screen in `.jetpack-seo-page-content`
padded at `--wpds-dimension-padding-2xl` (24px) to align with the header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The link pointed at home_url('/sitemap.xml'), which 404s: the "active"
signal comes from a placeholder option (jetpack_seo_sitemap_enabled, which
exists nowhere else in Jetpack) rather than the real `sitemaps` module, and
even when the module is active Jetpack generates the sitemap via cron so the
URL isn't live immediately. Drop the link to avoid shipping a dead link;
correct sitemap detection + URL is tracked as a follow-up. The status row
itself is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README: rewrite to match the shipped wp-build + script-data architecture (drop HashRouter/react-query/useSimpleQuery, REST API, pnpm run build/watch). - composer.json: drop unused requires (jetpack-connection/constants/assets); keep admin-ui/status/wp-build-polyfills, which the Initializer actually uses. - admin-page-layout.scss: use the -wp-build layout mixin variant so the code matches the changelog claim; it's a no-op superset until a tab adds breadcrumbs. - Initializer: guard init() with a private static $initialized flag (Podcast pattern) so the body can't re-run when seo-tools is off; document the pre-wired sitemap_url / front_page_description Overview fields. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…age deps Follow-up to 8bc377e, which dropped the unused jetpack-connection/ constants/assets requires from the jetpack-seo package. The Jetpack plugin's composer.lock still recorded the old require set for the package, so the 'Lock files are up to date' check failed. Regenerated with `composer update automattic/jetpack-seo`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gate the entire SEO surface (admin menu, wp-build bundle, script data) behind the rsm_jetpack_seo filter, default false during roll-out, mirroring how the Scan package gates itself. Consolidate the package changelog into a single entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
497c55d to
2bdd70c
Compare
…ard CSS
- Remove hex fallbacks on --wpds-* design tokens to match @wordpress/theme's
no-token-fallback-values convention (tokens are guaranteed under ThemeProvider).
- Remove the equal-height grid CSS that targeted @wordpress/ui's non-stable
hashed Card internals; defer until a second Overview card exists.
- Use title="SEO" instead of title={ 'SEO' }.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added a very important fix: a feature flag. The module is not enough, people would see an unfinished page in their sidebar without it. |
Part 2 of a stacked PR series extracted from #48154 (Jetpack SEO: introduce new SEO product). Originally authored by @keoshi; driven here by @angelablake. See #48154 for the split plan covering all 8 PRs.
Adds a new
automattic/jetpack-seopackage that mounts a React admin atwp-admin/admin.php?page=jetpack-seo. The entire surface is gated behind thersm_jetpack_seofeature flag (off by default during roll-out); when the flag is on, the menu item additionally requires theseo-toolsmodule to be active.This PR is intentionally the smallest viable foundation: the package scaffold, the
Initializer, and the Overview screen's single Site visibility card. Follow-up PRs add the Settings tab, Per-post SEO (Content) tab, AI tab, module absorption + migration, and the remaining Overview cards as each lands with the feature that owns its data.Proposed changes
New package:
automattic/jetpack-seorsm_jetpack_seofilter (Initializer::FEATURE_FILTER), defaulting tofalse. When the flag is off,Initializer::init()returns early and registers nothing — no admin menu, no assets, no script data — so the package is inert on production until the flag is flipped.Initializerregisters the admin menu (gated behind thersm_jetpack_seoflag, then on theseo-toolsmodule), loads wp-build's generated bundle (build/build.php+WP_Build_Polyfills::register()), and bootstraps the app's read-only initial state. Because the user-facing slug (jetpack-seo) differs from wp-build's page slug (jetpack-seo-dashboard), the screen id is aliased oncurrent_screenso wp-build's auto-generated enqueue callback fires. No REST controller.@wordpress/build(wp-build) dashboard. Routing via@wordpress/route(routes/index/{route,stage}.tsx);_inc/app.tsxwraps the routes inAdminPagechrome from@automattic/jetpack-components. UI uses@wordpress/components,@wordpress/ui,@wordpress/icons. One screen: Overview, rendering a single Site visibility card (search engines allowed, sitemap active, SEO tools active).window.JetpackScriptData.seovia thejetpack_admin_js_script_datafilter (Initializer::inject_script_data()) and read synchronously on the client through@automattic/jetpack-script-data(_inc/data/get-overview.ts). wp-build pages load as ES modules, sowp_localize_scriptcan't bootstrap them — the script-data layer is the supported channel (mirrors Podcast/Newsletter).jetpack-admin-page-layout-wp-buildmixin from@automattic/jetpack-base-styles, scoped tobody.jetpack_page_jetpack-seo/body.toplevel_page_jetpack-seo, via_inc/admin-page-layout.scss..gitattributeswith/build/** production-includeso the wp-build-compiled JS/CSS is included when the package is vendored into the Jetpack plugin distribution (without this, the SEO admin page renders blank on Jetpack Beta — the PHP files vendor through but the built bundle doesn't).tests/jest.config.jsandtestscript, so the CI test matrix has a config to select across PHP 7.4–8.5.Jetpack plugin wiring
class.jetpack.php: load the new package inlate_initialization().composer.json: requireautomattic/jetpack-seo.Related product discussion/links
Screenshots
SEO menu item under Jetpack and the Overview screen with its Site visibility card. The three status rows reflect live data bootstrapped via the script-data layer. (Visually unchanged by the wp-build rebuild.)
Testing instructions
Prerequisites
seo-toolsmodule active (e.g. a JN site running this PR's Jetpack Beta artifact).manage_options).trueforrsm_jetpack_seo(e.g. dropadd_filter( 'rsm_jetpack_seo', '__return_true' );in a mu-plugin or snippet). With the flag off, the package registers nothing and the steps below show no SEO menu.Feature flag gating
wp-admin/admin.php?page=jetpack-seois not reachable.Menu visibility
wp-admin, confirm Jetpack → SEO appears in the admin menu.wp jetpack module deactivate seo-tools). The SEO menu item should disappear — nothing registers when the module is off.wp jetpack module activate seo-tools). The menu item should reappear.Overview screen
wp-admin/admin.php?page=jetpack-seowith the title "SEO" and a Site visibility card.blog_public)jetpack_seo_sitemap_enabled; defaults off — the toggle lands in PR Edit and rename the readme for GitHub #3)seo-toolsmodule state)