Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5e7dcb3
Components: add BoundedLayout layout primitive
keoshi May 27, 2026
4a58e53
Jetpack SEO: introduce package foundation and Site visibility Overview
keoshi May 27, 2026
71a062f
SEO foundation: fix typecheck, project structure, and changelog format
May 27, 2026
01fbd45
SEO foundation: address Phan + scope-leftover issues
May 27, 2026
61fc712
SEO foundation: opt into shared admin-page-layout mixin
May 28, 2026
2fb97bb
SEO foundation: ship PHPUnit + jest configs across the CI matrix
May 28, 2026
baa4d8c
SEO foundation: add .gitattributes so build/ ships with the package
May 28, 2026
88243c3
Jetpack SEO: migrate Overview to the shared data-sync layer
May 29, 2026
e1a878f
Jetpack SEO: adopt shared components and design tokens on the Overview
May 29, 2026
801556a
Jetpack SEO: rebuild the admin page on wp-build + script-data
Jun 1, 2026
8ec0504
Jetpack SEO: align wp-build to trunk cohort (@wordpress/build 0.14.0)
Jun 1, 2026
b3c213e
Jetpack SEO: refresh plugins/jetpack composer.lock for the SEO dep ch…
Jun 1, 2026
52b7973
Jetpack SEO: fix Overview page padding and status-dot colors
Jun 1, 2026
b03c210
Jetpack SEO: add content padding to the Overview page
Jun 1, 2026
a0d7bde
Jetpack SEO: remove the broken sitemap "View" link from the Overview
Jun 1, 2026
ec4ff2d
Jetpack SEO: address #49203 review feedback (docs + cleanup)
Jun 2, 2026
13821b1
Jetpack SEO: regenerate jetpack composer.lock after trimming seo pack…
Jun 2, 2026
7190a8b
Jetpack SEO: gate the package behind the rsm_jetpack_seo feature flag
dhasilva Jun 2, 2026
2bdd70c
remove product name translations
dhasilva Jun 2, 2026
84875f2
Jetpack SEO: address review — drop wpds token fallbacks and fragile c…
dhasilva Jun 2, 2026
a78456b
Jetpack SEO: use logical margin-inline-end on the status dot for RTL
dhasilva Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions projects/packages/seo/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Files to include in the mirror repo, but excluded via gitignore
# Remember to end all directories with `/**` to properly tag every file.
/build/** production-include

# Files to exclude from the mirror repo, but included in the monorepo.
# (see also entries in the monorepo root .gitattributes)
# Remember to end all directories with `/**` to properly tag every file.
global.d.ts production-exclude
routes/** production-exclude
_inc/**/*.scss production-exclude
_inc/**/*.tsx production-exclude
_inc/**/*.ts production-exclude
_inc/**/*.jsx production-exclude
src/**/*.scss production-exclude
src/**/*.tsx production-exclude
src/**/*.ts production-exclude
src/**/*.jsx production-exclude
5 changes: 5 additions & 0 deletions projects/packages/seo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
vendor/
node_modules/
build/
.cache/
wordpress
13 changes: 13 additions & 0 deletions projects/packages/seo/.phan/config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
/**
* This configuration will be read and overlaid on top of the
* default configuration. Command-line arguments will be applied
* after this file is read.
*
* @package automattic/jetpack-seo
*/

// Require base config.
require __DIR__ . '/../../../../.phan/config.base.php';

return make_phan_config( dirname( __DIR__ ) );
24 changes: 24 additions & 0 deletions projects/packages/seo/.phpcs.dir.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<ruleset>

<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array">
<element value="jetpack-seo" />
</property>
</properties>
</rule>
<rule ref="Jetpack.Functions.I18n">
<properties>
<property name="text_domain" value="jetpack-seo" />
</properties>
</rule>

<rule ref="WordPress.Utils.I18nTextDomainFixer">
<properties>
<property name="old_text_domain" type="array" />
<property name="new_text_domain" value="jetpack-seo" />
</properties>
</rule>

</ruleset>
6 changes: 6 additions & 0 deletions projects/packages/seo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
36 changes: 36 additions & 0 deletions projects/packages/seo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Jetpack SEO

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 being built up across a stacked series of PRs (see #48154 for the split plan). This foundation ships the package scaffold, the admin page, and the Overview screen's Site visibility card; the Settings, Content, and AI tabs and the remaining Overview cards land in follow-up PRs.

## What this foundation provides

- A standalone wp-admin page registered at `admin.php?page=jetpack-seo`, gated behind the `rsm_jetpack_seo` feature flag (off by default during roll-out) and, when on, the `seo-tools` module being active.
- The **Overview** screen with a single **Site visibility** card (search engines allowed, sitemap active, SEO tools active).

## Architecture

Built as a [`@wordpress/build`](https://www.npmjs.com/package/@wordpress/build) (wp-build) dashboard, the pattern shared by recently-shipped Jetpack admin pages (Podcast, Scan, Forms, Newsletter):

- **PHP:** `Automattic\Jetpack\SEO\Initializer` 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:** the page is an ES-module bundle. Routing uses [`@wordpress/route`](https://www.npmjs.com/package/@wordpress/route); each route is a `routes/<name>/{route,stage}.tsx` pair. `_inc/app.tsx` wraps the routes in the `AdminPage` chrome from `@automattic/jetpack-components`. UI uses `@wordpress/components`, `@wordpress/ui`, and `@wordpress/icons`.
- **Data:** read-only initial state is bootstrapped server-side onto `window.JetpackScriptData.seo` via the `jetpack_admin_js_script_data` filter (`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, so `wp_localize_script` can't bootstrap them — the script-data layer is the supported channel. There is no REST controller in this foundation.

## Development

```bash
# 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`).
22 changes: 22 additions & 0 deletions projects/packages/seo/_inc/admin-page-layout.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@use "@automattic/jetpack-base-styles/admin-page-layout" as *;

// Apply the shared Jetpack admin-page layout so `<AdminPage>` owns the page
// chrome: fixed header, scrollable middle, pinned footer. We use the
// `-wp-build` variant because this is a wp-build dashboard: it's a superset
// of the base mixin that adds `<Breadcrumbs>` resets — a no-op until a tab
// renders breadcrumbs, so it's forward-compatible for the follow-up tabs.
// Both menu placements are targeted because Admin_Menu may register the page as
// a top-level menu or as a Jetpack submenu depending on the environment.
body.jetpack_page_jetpack-seo,
body.toplevel_page_jetpack-seo {

@include jetpack-admin-page-layout-wp-build;
}

// `<AdminPage>` renders content with `horizontalSpacing={0}` and no
// `hasPadding`, so the content area gets no padding from admin-ui or the layout
// mixin — only the header does (at `padding-inline: 2xl`). Pad our content to
// align with the header and clear it (the consumer-side job Scan/Podcast own).
.jetpack-seo-page-content {
padding: var(--wpds-dimension-padding-2xl);
}
35 changes: 35 additions & 0 deletions projects/packages/seo/_inc/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AdminPage, ThemeProvider } from '@automattic/jetpack-components';
import { __ } from '@wordpress/i18n';
import OverviewScreen from './screens/overview';
import './admin-page-layout.scss';
import type { FC } from 'react';

/**
* Root of the Jetpack SEO admin app.
*
* `@wordpress/build` mounts this as the route's `stage`. It renders the shared
* `AdminPage` chrome (header + footer) and the Overview screen. The screen
* reads its data synchronously from the page bootstrap (`window.JetpackScriptData`),
* so there's no router or async provider to set up here yet — tabs arrive in
* later PRs.
*
* @return The Jetpack SEO admin page.
*/
const App: FC = () => (
<ThemeProvider>
<AdminPage
title="SEO"
subTitle={ __(
'Visibility tools for your site — sitemaps, canonical URLs, and search-engine settings, in one place.',
'jetpack-seo'
) }
showFooter
>
<div className="jetpack-seo-page-content">
<OverviewScreen />
</div>
</AdminPage>
</ThemeProvider>
);

export default App;
23 changes: 23 additions & 0 deletions projects/packages/seo/_inc/data/get-overview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getScriptData } from '@automattic/jetpack-script-data';
import type { OverviewResponse } from './overview-types';

type SeoScriptData = {
seo?: {
overview?: OverviewResponse;
};
};

/**
* Read the aggregated Overview state.
*
* The server bootstraps it onto `window.JetpackScriptData.seo.overview` via the
* `jetpack_admin_js_script_data` filter (see `Initializer::inject_script_data()`),
* so it's on the page at first paint — the Overview reads it synchronously with
* no request and no loading state. Returns `null` if the bootstrap is missing.
*
* @return The Overview state, or `null` when unavailable.
*/
export default function getOverview(): OverviewResponse | null {
const scriptData = getScriptData() as SeoScriptData | undefined;
return scriptData?.seo?.overview ?? null;
}
18 changes: 18 additions & 0 deletions projects/packages/seo/_inc/data/overview-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Shape of the aggregated Overview state the server bootstraps onto
// `window.JetpackScriptData.seo.overview` (see `Initializer::get_overview_data()`).
// Plain TypeScript — the server owns the payload, so no runtime schema is needed.

export interface SiteVisibility {
search_engines_visible: boolean;
sitemap_active: boolean;
sitemap_url: string;
seo_tools_active: boolean;
front_page_description: string;
}

export interface OverviewResponse {
site_visibility: SiteVisibility;
plan: {
seo_enabled_for_site: boolean;
};
}
38 changes: 38 additions & 0 deletions projects/packages/seo/_inc/screens/overview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { __ } from '@wordpress/i18n';
import { Notice } from '@wordpress/ui';
import getOverview from '../../data/get-overview';
import SiteVisibilityCard from './site-visibility-card';
import './style.scss';
import type { FC } from 'react';

const OverviewScreen: FC = () => {
const data = getOverview();

if ( ! data ) {
return (
<Notice.Root intent="error">
<Notice.Description>{ __( 'Unable to load overview.', 'jetpack-seo' ) }</Notice.Description>
</Notice.Root>
);
}

return (
<>
{ ! data.plan.seo_enabled_for_site && (
<Notice.Root intent="warning">
<Notice.Description>
{ __(
'SEO tools are not enabled on this site. Some cards reflect the underlying WordPress options only.',
'jetpack-seo'
) }
</Notice.Description>
</Notice.Root>
) }
<div className="jetpack-seo-overview__grid">
<SiteVisibilityCard data={ data.site_visibility } />
</div>
</>
);
};

export default OverviewScreen;
Loading
Loading