automattic/jetpack-seo

Jetpack SEO — the visibility command center for WordPress sites in the agentic web.

Maintainers

Package info

github.com/Automattic/jetpack-seo

Type:jetpack-library

pkg:composer/automattic/jetpack-seo

Transparency log

Statistics

Installs: 940

Dependents: 1

Suggesters: 0

Stars: 0

v0.8.0 2026-08-03 23:26 UTC

This package is auto-updated.

Last update: 2026-08-07 05:34:42 UTC


README

The visibility command center for WordPress sites in the agentic web — a unified wp-admin screen that consolidates SEO, sitemaps, AI discoverability, and site verification settings across all site types (self-hosted, Atomic/WoW, Simple).

This package is built up across a stacked series of PRs (see #48154 for the split plan). It currently provides, on a wp-admin page registered at admin.php?page=jetpack-seo (gated on the seo-tools module being active):

  • Overview — a dashboard with Site visibility, Site verification, AI access, and Content SEO cards. Visibility reports indexing directly and the sitemap by its effective state; verification lists Google, Bing and Facebook plus any other service the site has verified with. AI access summarizes the crawler groups and llms.txt, or the one reason per-crawler settings can't apply. Content SEO shows factual coverage rings (schema, SEO title, meta description, search visibility) with literal counts, each deep-linking to the content still missing that field. The visibility/verification cards deep-link into the matching Settings sections, AI access into the GEO tab.
  • Settings — search-engine indexing, the XML sitemap (with a "View sitemap" link whenever it's on, and turning it off removes the site's sitemap entirely — including WordPress core's own — for a proper 404; the toggle is disabled while indexing is blocked, since a sitemap can't be served then), canonical URLs, the title structure for all page types (front page, posts, pages, tags, archives), the front-page description, site verification codes, and a single site-level Schema module (a BreadcrumbList toggle plus the site's Organization details, with LocalBusiness nested as a refinement of the Organization and one Save for both), plus the signed-in user's Author profile card, read-only search & social previews of the home page (Google / Facebook / X), and an Advanced module at the foot of the tab holding the turn-off-SEO-tools control — hidden on WordPress.com Simple, where Modules::is_active() reports every module active so it couldn't take effect.
  • Content — a DataViews list of published content across every supported post type, showing each item's SEO state (schema type, meta description set, search visibility). Rows link to the Gutenberg editor and open a per-post SEO inspector (custom title, description, schema type, noindex, SERP preview) in the dashboard sidebar. Filters for post type, schema type, description state, and search visibility run client-side over the merged set. The Schema type control and SEO columns also appear in the block editor and edit.php post-list tables respectively; the post-list columns are hidden by default and opted into per user via Screen Options. JSON-LD (Article / FAQ) is emitted on wp_head.
  • GEO — llms.txt generation, the plan-gated AI SEO Enhancer, and per-crawler allow/block controls for answer engines and training crawlers (written to the virtual robots.txt). (Generative Engine Optimization; internal route/id ai.)

Architecture

Built as a @wordpress/build (wp-build) dashboard, the pattern shared by recently-shipped Jetpack admin pages (Podcast, Scan, Forms, Newsletter):

  • PHP: Automattic\Jetpack\SEO\Initializer boots the package and gates the surface behind the legacy rsm_jetpack_seo filter or per-site seo-admin-ui feature, then its discoverability cohort; Admin_Page registers the admin menu via Admin_Menu::add_menu(), loads wp-build's generated bundle (build/build.php + WP_Build_Polyfills::register()), and bootstraps the app's initial state. Because the user-facing slug (jetpack-seo) differs from wp-build's page slug (jetpack-seo-dashboard), the screen id is aliased on current_screen so wp-build's auto-generated enqueue callback fires.
  • React: an ES-module bundle. Each top-level view (Overview, Settings, Content, GEO) is its own @wordpress/route route under routes/, exporting a stage — the per-route pattern shared with Forms. The Content route also exports an inspector (the per-post SEO editor), rendered in wp-build's native Stage/Inspector sidebar when a post is selected (?postId=). A shared shell (_inc/dashboard/dashboard-page + dashboard-nav) renders the AdminPage chrome from @automattic/jetpack-components and the route-based tab navigation, so every route gets the same header, tabs and footer. UI uses @wordpress/components, @wordpress/ui, and @wordpress/icons.
  • Data: the Overview, Settings, and GEO routes are backed by read-only REST routes GET /jetpack/v4/seo/{overview,settings,ai} (Dashboard_Data::register_rest_reads(), manage_options-gated), whose responses are preloaded into the page server-side via rest_preload_api_request() (Admin_Page::inject_script_data(), on the jetpack_admin_js_script_data filter — wp-build pages load as ES modules, so wp_localize_script can't bootstrap them and the script-data layer is the supported channel). On a normal load each route reads its slice from the preload synchronously through @automattic/jetpack-script-data (_inc/data/get-preloaded.ts) — instant, no request. When a slice is missing or stale (e.g. a mismatched bundle after an update), the route's stage fetches it from its REST route instead of dead-ending: _inc/data/use-ensure-tab-data.ts shows a loading skeleton, retries silently, and surfaces a recoverable "Try again" state only on genuine failure. google_verify and site stay synchronous on window.JetpackScriptData.seo. The Content route fetches posts and pages live from core /wp/v2/posts and /wp/v2/pages (no custom endpoint), with SEO meta returned via registered show_in_rest post meta; the inspector writes through the same core post endpoint. Most Settings writes reuse /jetpack/v4/settings (and core /wp/v2/settings for blog_public); nested site-level schema settings use the package-owned GET/POST /jetpack/v4/seo/schema-settings route so each schema section can be preserved across partial updates.
  • Plan gating (WordPress.com): on WordPress.com sites below Premium (lacking the advanced-seo entitlement after the March 2026 rebundling) the dashboard reduces to a free subset and surfaces an upsell. The gate is Host::is_wpcom_platform() && ! Current_Plan::supports( 'advanced-seo' ) (Initializer::is_gated()): advanced-seo is in the free plan's supports list, so supports() returns true on self-hosted (never gated) and hijacks to wpcom_site_has_feature() on WordPress.com, false below Premium. It's surfaced to the client as the seo.gating script-data slice (is_gated, upsell_url), read via _inc/data/is-gated.ts, which defaults to ungated when the slice is absent so a stale snapshot never hides a paid feature. When gated: the Content and GEO tabs are removed and their route stages redirect to Overview; Overview and Settings drop their paid cards/sections and pin the upsell banner; and — importantly for anyone debugging a missing /llms.txt or absent AI-crawler robots.txt rules — init() skips Llms_Txt::init() and Ai_Crawlers::init(), so those services stop emitting, not just their UI. Gating is evaluated live per request (no cached option), so an upgrade restores everything on the next load and a downgrade takes effect immediately.

UI conventions

Text hierarchy

@wordpress/ui's Text variants are easy to pick wrong, because the heading and body ramps don't share a scaleheading-sm is smaller than body-md. Resolved from the WPDS tokens (@wordpress/theme):

Variant Size Weight Transform Use it for
heading-lg 15px medium card title
heading-md 13px medium sub-heading inside a module
heading-sm 11px medium UPPERCASE field label
body-md 13px regular body copy
body-sm 12px regular field explainer

"Small heading" is a field label, not a small sub-heading. Reach for heading-md when you want a sub-heading.

The rule for prose, which the modules drifted away from once already:

  • body-md — a module's or section's own prose. Things the reader is meant to read.
  • body-sm + .muted — explainer text attached to a field: hints under an input, help under a group label. Not general copy.
  • body-md + .muted — the third case, and deliberately narrow: chrome inside the SERP and social preview mockups (social-previews-card, serp-preview). Those imitate how Google and Facebook render a link, so their type follows someone else's design, not ours. Don't extend this case to real UI.

Field labels should be uppercase across the board. Where a TextControl renders the label, that's automatic; where a label is drawn by hand, use heading-sm so it matches (same size and casing — the control label is weight 600 against heading-sm's medium, so they read alike without being pixel-identical).

Element rendering

Text renders a <span> by default, which is rarely what you want:

  • Prose → render={ <p /> }.
  • A real sub-heading → render={ <h3 /> }. Card headers are h2 (the page <h1> is in Admin_Page), so a module's own sub-headings are h3. Without this a screen-reader user navigating by heading finds the card title and nothing beneath it.
  • A hand-drawn field label heading a group of controls rather than one input → keep it a span, but give the wrapper role="group" and aria-labelledby pointing at it (see google-verification-field), or it announces as a label attached to nothing.

Rendering as <p> or a heading is safe: Text always applies both of @wordpress/ui's global-CSS defenses, and every variant defines --_gcd-heading-* alongside --_gcd-p-*, so the element change doesn't alter the type.

Development

# Build once (from the repo root)
jetpack build packages/seo

# Watch mode
pnpm --filter='@automattic/jetpack-seo' run watch

# Typecheck
pnpm --filter='@automattic/jetpack-seo' run typecheck

# Tests
pnpm --filter='@automattic/jetpack-seo' run test

The built JS/CSS lives in build/ and is included in the vendored Jetpack plugin distribution via .gitattributes (/build/** production-include).