Skip to content

Commit 536135d

Browse files
authored
SEO: Lock the front-page description while another SEO plugin owns the output (#53876)
* SEO: Report which conflicting SEO plugin owns the output The SEO module now names the plugin it steps aside for through a new jetpack_seo_conflicting_plugin filter, and the dashboard's Overview and Settings payloads carry that name so the screens can say who owns the output. * SEO: Name the conflicting plugin only while Jetpack steps aside Report the name only when jetpack_disable_seo_tools is on, so it can't contradict live output, and list paid editions before the free plugins they run alongside so the first match names the right one. * SEO: Lock the front-page description while another SEO plugin owns the output Settings now names the plugin at the top (or says another SEO plugin when it's unnamed), makes the front-page description read-only, and hides the search and social previews, which show Jetpack's tags rather than that plugin's. The lock follows Jetpack's output being off, not the plugin name, so it matches the title structure lock. The locked field shows the description Jetpack saved instead of the empty legacy fallback. * Changelog: Make the Settings entries comment-only, as the SEO page is gated
1 parent 0eb289a commit 536135d

6 files changed

Lines changed: 94 additions & 4 deletions

File tree

‎projects/packages/seo/_inc/screens/settings/index.tsx‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ const frontPageHelp = __(
7676
'Shown under your site name in search results and social shares. About the first 155 characters display in search, or 120 on mobile.',
7777
'jetpack-seo'
7878
);
79+
const frontPageLockedHelp = __(
80+
'Locked while another SEO plugin manages your SEO. Jetpack uses this again once it’s deactivated.',
81+
'jetpack-seo'
82+
);
7983
// Keep in step with the 155 quoted in `frontPageHelp`.
8084
const descriptionSearchLength = 155;
8185
const descriptionMaxLength = 300;
@@ -200,6 +204,23 @@ const SettingsScreen: FC< Props > = ( { form } ) => {
200204
// gated (the value stays live), so its card is shown below in that case too.
201205
const gated = isGated();
202206

207+
// Another SEO plugin owns the output these fields feed, so they're locked. Gated sites
208+
// report the output off too, but keep their grandfathered description editable.
209+
const outputLocked = ! gated && ! local.title_formats_editable;
210+
const lockedNotice = local.conflicting_seo_plugin
211+
? sprintf(
212+
/* translators: %s: name of the SEO plugin that controls the site's SEO output, e.g. Yoast SEO. */
213+
__(
214+
'%s is managing your SEO titles and descriptions, so Jetpack’s settings for them are locked while it’s active.',
215+
'jetpack-seo'
216+
),
217+
local.conflicting_seo_plugin
218+
)
219+
: __(
220+
'Another SEO plugin is managing your SEO titles and descriptions, so Jetpack’s settings for them are locked while it’s active.',
221+
'jetpack-seo'
222+
);
223+
203224
const frontPageDescriptionCard = (
204225
<CollapsibleCard.Root defaultOpen={ false }>
205226
<CollapsibleCard.Header render={ <h2 /> }>
@@ -223,12 +244,12 @@ const SettingsScreen: FC< Props > = ( { form } ) => {
223244
// also on this tab and leave two controls indistinguishable to a
224245
// screen reader.
225246
label={ __( 'Home page description', 'jetpack-seo' ) }
226-
help={ frontPageHelp }
247+
help={ outputLocked ? frontPageLockedHelp : frontPageHelp }
227248
value={ local.front_page_description }
228249
onChange={ next => setField( { front_page_description: next } ) }
229250
maxLength={ descriptionMaxLength }
230251
rows={ 3 }
231-
disabled={ isSaving }
252+
disabled={ isSaving || outputLocked }
232253
__nextHasNoMarginBottom
233254
/>
234255
{ /* Count and Save share a row, matching the title-structure footer. */ }
@@ -251,7 +272,7 @@ const SettingsScreen: FC< Props > = ( { form } ) => {
251272
</Text>
252273
<Button
253274
onClick={ () => commitFields( [ 'front_page_description' ] ) }
254-
disabled={ isSaving || ! isDirty( [ 'front_page_description' ] ) }
275+
disabled={ isSaving || outputLocked || ! isDirty( [ 'front_page_description' ] ) }
255276
>
256277
{ saveLabel }
257278
</Button>
@@ -263,6 +284,11 @@ const SettingsScreen: FC< Props > = ( { form } ) => {
263284

264285
return (
265286
<Stack direction="column" gap="lg" className={ styles.root }>
287+
{ outputLocked && (
288+
<Notice.Root intent="warning">
289+
<Notice.Description>{ lockedNotice }</Notice.Description>
290+
</Notice.Root>
291+
) }
266292
<div id="visibility" className={ styles.section }>
267293
<CollapsibleCard.Root defaultOpen>
268294
<CollapsibleCard.Header render={ <h2 /> }>
@@ -398,7 +424,8 @@ const SettingsScreen: FC< Props > = ( { form } ) => {
398424

399425
{ frontPageDescriptionCard }
400426

401-
<SocialPreviewsCard description={ local.front_page_description } />
427+
{ /* It previews Jetpack's tags, which aren't the ones a conflicting plugin outputs. */ }
428+
{ ! outputLocked && <SocialPreviewsCard description={ local.front_page_description } /> }
402429
</>
403430
) }
404431

‎projects/packages/seo/_inc/screens/settings/test/settings-screen.test.tsx‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,42 @@ const statusFor = ( moduleTitle: string ): string | undefined => {
120120
return undefined;
121121
};
122122

123+
describe( 'Another SEO plugin owning the output', () => {
124+
const openDescription = () => {
125+
// eslint-disable-next-line testing-library/prefer-user-event -- Match the package's existing disclosure tests without adding a dependency.
126+
fireEvent.click( screen.getByRole( 'button', { name: 'Front-page description' } ) );
127+
return screen.findByRole( 'textbox', { name: 'Home page description' } );
128+
};
129+
130+
// A plugin can switch Jetpack's output off without naming itself; the lock can't depend on the name.
131+
it.each( [
132+
[ 'Yoast SEO', /^Yoast SEO is managing your SEO/ ],
133+
[ '', /^Another SEO plugin is managing your SEO/ ],
134+
] )(
135+
'with "%s" in charge, locks the description and hides the previews',
136+
async ( plugin, notice ) => {
137+
render(
138+
<SettingsScreen
139+
form={ buildForm( { conflicting_seo_plugin: plugin, title_formats_editable: false } ) }
140+
/>
141+
);
142+
143+
expect( screen.getByText( notice ) ).toBeInTheDocument();
144+
expect( screen.queryByText( 'social previews' ) ).not.toBeInTheDocument();
145+
await expect( openDescription() ).resolves.toBeDisabled();
146+
expect( screen.getByText( /^Locked while another SEO plugin/ ) ).toBeInTheDocument();
147+
}
148+
);
149+
150+
it( 'leaves both alone while Jetpack owns the output', async () => {
151+
render( <SettingsScreen form={ buildForm() } /> );
152+
153+
expect( screen.queryByText( /is managing your SEO/ ) ).not.toBeInTheDocument();
154+
expect( screen.getByText( 'social previews' ) ).toBeInTheDocument();
155+
await expect( openDescription() ).resolves.toBeEnabled();
156+
} );
157+
} );
158+
123159
describe( 'Indexing toggle on an unpublished site', () => {
124160
const indexingToggle = () =>
125161
screen.getByRole( 'checkbox', { name: /Allow search engines to index this site/i } );
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: changed
3+
Comment: Settings: Name the conflicting SEO plugin, lock the front-page description, and hide the previews while it's active. The new SEO page is gated.
4+

‎projects/packages/seo/src/class-dashboard-data.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,14 @@ public static function get_settings_data() {
10951095
$title_formats_editable = self::is_jetpack_seo_enabled();
10961096
$front_page_desc = self::get_front_page_meta_description();
10971097

1098+
/** This filter is documented in projects/plugins/jetpack/modules/seo-tools/class-jetpack-seo-utils.php */
1099+
if ( apply_filters( 'jetpack_disable_seo_tools', false ) ) {
1100+
// Same for the description, which the helper swaps for the legacy fallback while
1101+
// another plugin owns the output. Show the one Jetpack resumes with; it's locked.
1102+
$modern = get_option( self::FRONT_PAGE_META_OPTION );
1103+
$front_page_desc = $modern ? $modern : get_option( self::LEGACY_FRONT_PAGE_META_OPTION, '' );
1104+
}
1105+
10981106
// A site that set a front-page description back when it was free for all
10991107
// WordPress.com Simple sites keeps editing it, even when otherwise plan-gated:
11001108
// the value stays live in the legacy option. The gated Settings uses this to keep

‎projects/packages/seo/tests/php/DashboardDataTest.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected function tearDown(): void {
3030
\Jetpack_AI_Settings::$is_ai_seo_enabled = true;
3131
\Jetpack_AI_Settings::$is_ai_enabled = true;
3232
delete_option( 'advanced_seo_title_formats' );
33+
delete_option( Dashboard_Data::FRONT_PAGE_META_OPTION );
3334
delete_option( 'jetpack_ai_seo_enabled' );
3435
delete_option( 'jetpack_seo_sitemap_enabled' );
3536
delete_option( 'jetpack_seo_canonical_urls_enabled' );
@@ -355,6 +356,16 @@ public function test_get_settings_data_preserves_title_formats_while_output_is_d
355356
$this->assertFalse( $settings['title_formats_editable'] );
356357
}
357358

359+
/**
360+
* The locked front-page description shows what Jetpack saved, not the helper's legacy fallback.
361+
*/
362+
public function test_get_settings_data_shows_the_saved_description_while_output_is_disabled() {
363+
update_option( Dashboard_Data::FRONT_PAGE_META_OPTION, 'Saved description.' );
364+
add_filter( 'jetpack_disable_seo_tools', '__return_true' );
365+
366+
$this->assertSame( 'Saved description.', Dashboard_Data::get_settings_data()['front_page_description'] );
367+
}
368+
358369
/**
359370
* @dataProvider provide_conflicting_seo_plugin_states
360371
* @param bool $disabled Whether `jetpack_disable_seo_tools` is on.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: other
3+
Comment: SEO: Lock the gated SEO settings that a conflicting SEO plugin overrides.
4+

0 commit comments

Comments
 (0)