Skip to content

Commit e8d04a7

Browse files
authored
SEO: Warn when the front-page description nears its limit (#53875)
Turn the count amber past the 155 characters the help text says search displays, and red with "Maximum characters reached." at the 300 cap, as the legacy SEO screen did.
1 parent 0bf75d4 commit e8d04a7

5 files changed

Lines changed: 63 additions & 15 deletions

File tree

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

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { __, _n, sprintf } from '@wordpress/i18n';
77
import { home, link, seen } from '@wordpress/icons';
88
import { useSearch } from '@wordpress/route';
99
import { Button, Card, CollapsibleCard, Link, Notice, Stack, Text } from '@wordpress/ui';
10+
import clsx from 'clsx';
1011
import CardTitleIcon from '../../components/card-title-icon';
1112
import StatusIndicator from '../../components/status-indicator';
1213
import UpsellBanner from '../../components/upsell-banner';
@@ -54,9 +55,9 @@ const platformManagedHelp = __(
5455
'This setting is managed by your hosting platform and cannot be changed here.',
5556
'jetpack-seo'
5657
);
57-
// Figures are what search *displays*, not a limit we enforce — the field is
58-
// deliberately uncapped. Google measures a pixel width (920px desktop / 680px
59-
// mobile), which works out around 155 and 120 characters.
58+
// Figures are what search *displays*, not a limit we enforce. Google measures a
59+
// pixel width (920px desktop / 680px mobile), which works out around 155 and 120
60+
// characters.
6061
//
6162
// No social figure is quoted on purpose. `functions.opengraph.php` truncates
6263
// `og:description` at 197, but that happens *before* the `jetpack_open_graph_tags`
@@ -67,6 +68,9 @@ const frontPageHelp = __(
6768
'Shown under your site name in search results and social shares. About the first 155 characters display in search, or 120 on mobile.',
6869
'jetpack-seo'
6970
);
71+
// Keep in step with the 155 quoted in `frontPageHelp`.
72+
const descriptionSearchLength = 155;
73+
const descriptionMaxLength = 300;
7074

7175
interface Props {
7276
form: SettingsForm;
@@ -159,6 +163,13 @@ const SettingsScreen: FC< Props > = ( { form } ) => {
159163
// Count code points, not UTF-16 units, so an emoji or an astral character
160164
// counts once rather than twice.
161165
const descriptionLength = [ ...local.front_page_description ].length;
166+
// `maxLength` caps UTF-16 units, so test the cap in those: an emoji spends two.
167+
const descriptionAtMax = local.front_page_description.length >= descriptionMaxLength;
168+
const descriptionCount = sprintf(
169+
/* translators: %d: number of characters currently in the front-page description. */
170+
_n( '%d character', '%d characters', descriptionLength, 'jetpack-seo' ),
171+
descriptionLength
172+
);
162173

163174
// Single toggles and single fields are binary — they have no partial state,
164175
// so they move straight from not-started to complete.
@@ -200,20 +211,28 @@ const SettingsScreen: FC< Props > = ( { form } ) => {
200211
help={ frontPageHelp }
201212
value={ local.front_page_description }
202213
onChange={ next => setField( { front_page_description: next } ) }
203-
maxLength={ 300 }
214+
maxLength={ descriptionMaxLength }
204215
rows={ 3 }
205216
disabled={ isSaving }
206217
__nextHasNoMarginBottom
207218
/>
208-
{ /* Count and Save share a row, matching the title-structure footer. The
209-
count is informational only — nothing here is capped. */ }
219+
{ /* Count and Save share a row, matching the title-structure footer. */ }
210220
<Stack direction="row" justify="space-between" align="center" gap="sm">
211-
<Text variant="body-sm" className={ styles.charCount }>
212-
{ sprintf(
213-
/* translators: %d: number of characters currently in the front-page description. */
214-
_n( '%d character', '%d characters', descriptionLength, 'jetpack-seo' ),
215-
descriptionLength
216-
) }
221+
<Text
222+
variant="body-sm"
223+
className={ clsx( styles.charCount, {
224+
[ styles.charCountWarn ]:
225+
descriptionLength > descriptionSearchLength && ! descriptionAtMax,
226+
[ styles.charCountMax ]: descriptionAtMax,
227+
} ) }
228+
>
229+
{ descriptionAtMax
230+
? sprintf(
231+
/* translators: %s: the character count, e.g. "300 characters". */
232+
__( '%s — Maximum characters reached.', 'jetpack-seo' ),
233+
descriptionCount
234+
)
235+
: descriptionCount }
217236
</Text>
218237
<Button
219238
onClick={ () => commitFields( [ 'front_page_description' ] ) }

‎projects/packages/seo/_inc/screens/settings/style.module.scss‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
font-variant-numeric: tabular-nums;
2828
}
2929

30+
// Past what search displays, then at the field's cap.
31+
.charCountWarn {
32+
color: var(--wpds-color-foreground-content-warning-weak);
33+
}
34+
35+
.charCountMax {
36+
color: var(--wpds-color-foreground-content-error-weak);
37+
}
38+
3039
// The "while it's off" list in the Advanced module. Body text, not the muted
3140
// explainer style — these are consequences to read, not hints about a field.
3241
// wp-admin's `ul { list-style: none; }` (common.css) strips the markers. It's

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,6 @@ describe( 'Settings module completion status', () => {
218218
} );
219219
} );
220220

221-
// The count is purely informational — the field is deliberately uncapped,
222-
// because the real limits differ per surface (search truncates for display,
223-
// Jetpack's own og:description cuts at 197) and none is enforced on this save path.
224221
describe( 'Front-page description character count', () => {
225222
it( 'counts an empty description as zero, pluralized', () => {
226223
render( <SettingsScreen form={ buildForm() } /> );
@@ -237,6 +234,21 @@ describe( 'Settings module completion status', () => {
237234
render( <SettingsScreen form={ buildForm( { front_page_description: description } ) } /> );
238235
expect( screen.getByText( `${ description.length } characters` ) ).toBeInTheDocument();
239236
} );
237+
238+
it.each( [
239+
{ name: '299 letters', description: 'x'.repeat( 299 ), count: 299, atMax: false },
240+
{ name: '300 letters', description: 'x'.repeat( 300 ), count: 300, atMax: true },
241+
{
242+
name: '10 emoji and 280 letters',
243+
description: '😀'.repeat( 10 ) + 'x'.repeat( 280 ),
244+
count: 290,
245+
atMax: true,
246+
},
247+
] )( 'says whether the maximum is reached with $name', ( { description, count, atMax } ) => {
248+
render( <SettingsScreen form={ buildForm( { front_page_description: description } ) } /> );
249+
const text = screen.getByText( new RegExp( `^${ count } characters` ) );
250+
expect( text.textContent?.includes( 'Maximum characters reached.' ) ).toBe( atMax );
251+
} );
240252
} );
241253

242254
describe( 'Site verification — one service is enough', () => {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: added
3+
4+
Settings: Warn when the front-page description runs longer than search results display, and say when it reaches the maximum length.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: enhancement
3+
4+
SEO: Warn when the front-page description runs longer than search results display, and say when it reaches the maximum length.

0 commit comments

Comments
 (0)