Skip to content

Commit d3a4c96

Browse files
committed
Merge branch 'trunk' of https://github.com/Yoast/wordpress-seo into 2972-trivial-ai-features-displayed-two-times-at-user-settings
2 parents 9b4297a + b05660c commit d3a4c96

37 files changed

Lines changed: 1151 additions & 445 deletions

admin/metabox/class-metabox.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,13 @@ protected function render_hidden_fields() {
358358
echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'social' );
359359
}
360360

361+
$screen = WP_Screen::get();
362+
$is_block_editor = $screen && $screen->is_block_editor();
363+
if ( $is_block_editor && $this->get_metabox_post()->post_type === 'post' ) {
364+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in class.
365+
echo new Meta_Fields_Presenter( $this->get_metabox_post(), 'content_planner' );
366+
}
367+
361368
/**
362369
* Filter: 'wpseo_content_meta_section_content' - Allow filtering the metabox content before outputting.
363370
*
@@ -729,6 +736,11 @@ public function save_postdata( $post_id ) {
729736
WPSEO_Meta::get_meta_field_defs( 'schema', $post->post_type ),
730737
);
731738

739+
// We can't detect in save_postdata whether the request is coming from the block editor, so we gate the content_planner fields on post type only.
740+
if ( $post->post_type === 'post' ) {
741+
$meta_boxes = array_merge( $meta_boxes, WPSEO_Meta::get_meta_field_defs( 'content_planner' ) );
742+
}
743+
732744
foreach ( $meta_boxes as $key => $meta_box ) {
733745

734746
// If analysis is disabled remove that analysis score value from the DB.

images/black-friday-2025.gif

-209 KB
Binary file not shown.

inc/class-wpseo-meta.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ class WPSEO_Meta {
190190
'default_value' => '0',
191191
],
192192
],
193+
'content_planner' => [
194+
'is_content_planner_banner_rendered' => [
195+
'type' => 'hidden',
196+
'default_value' => '0',
197+
],
198+
'is_content_planner_banner_dismissed' => [
199+
'type' => 'hidden',
200+
'default_value' => '0',
201+
],
202+
],
193203
];
194204

195205
/**

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"webpack-bundle-analyzer": "^4.9.1"
7777
},
7878
"yoast": {
79-
"pluginVersion": "27.6-RC2"
79+
"pluginVersion": "27.6-RC5"
8080
},
8181
"version": "0.0.0"
8282
}

packages/js/src/ai-content-planner/block.json

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/js/src/ai-content-planner/blocks/banner-block.js

Lines changed: 0 additions & 94 deletions
This file was deleted.

packages/js/src/ai-content-planner/components/approve-modal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const ApproveModal = ( { isEmptyPost, isPremium, isUpsell, onClick, upsel
8282
<GradientSparklesIcon className="yst-h-6 yst-w-6" { ...svgAriaProps } />
8383
</div>
8484
<Modal.Title className="yst-text-slate-900 yst-font-medium yst-text-lg yst-mb-2">{ title }</Modal.Title>
85-
<Modal.Description className="yst-text-slate-600 yst-text-sm yst-mb-6 yst-mx-10">{ description }</Modal.Description>
85+
<Modal.Description as="div" className="yst-text-slate-600 yst-text-sm yst-mb-6 yst-mx-10">{ description }</Modal.Description>
8686
{ ! isPremium && ! isUpsell && <OneSparkNote className="yst-mb-2" /> }
8787
{ isUpsell ? <Button
8888
variant="upsell" as="a" href={ upsellLink } target="_blank" className="yst-w-full" rel="noopener noreferrer"

packages/js/src/ai-content-planner/components/content-planner-error.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable complexity */
2+
import { useSelect } from "@wordpress/data";
23
import { __ } from "@wordpress/i18n";
34
import { Button, useModalContext } from "@yoast/ui-library";
45
import { noop } from "lodash";
@@ -7,6 +8,7 @@ import {
78
BadWPRequestAlert,
89
GenericAlert,
910
RateLimitAlert,
11+
SubscriptionError,
1012
TimeoutAlert,
1113
} from "../../ai-generator/components/errors";
1214
import { SiteUnreachableAlert } from "../../ai-generator/components/errors/site-unreachable-alert";
@@ -15,16 +17,35 @@ import { SiteUnreachableAlert } from "../../ai-generator/components/errors/site-
1517
* @param {number} errorCode The error code.
1618
* @param {string} [errorIdentifier=""] The error identifier.
1719
* @param {string} [errorMessage=""] The error message.
20+
* @param {string[]} [missingLicenses=[]] Products with an invalid subscription.
1821
* @param {function} [onRetry=noop] Called to retry.
1922
* @returns {JSX.Element} The element.
2023
*/
2124
export const ContentPlannerError = ( {
2225
errorCode,
2326
errorIdentifier = "",
2427
errorMessage = "",
28+
missingLicenses = [],
2529
onRetry = noop,
2630
} ) => {
2731
const { onClose } = useModalContext();
32+
const isPremium = useSelect( ( select ) => select( "yoast-seo/editor" ).getIsPremium(), [] );
33+
34+
// Premium installed but licence missing/expired: surface the renew/activate flow.
35+
// The backend returns 402 when the licence is invalid, and 429 with USAGE_LIMIT_REACHED
36+
// once a no-licence Premium user exhausts the free sparks (Too_Many_Requests_Exception
37+
// extends Payment_Required_Exception in the PHP layer).
38+
// Free-only sites are handled earlier by the Approve modal upsell, so they fall through here.
39+
const isSubscriptionRequired = isPremium && (
40+
errorCode === 402 ||
41+
( errorCode === 429 && errorIdentifier === "USAGE_LIMIT_REACHED" )
42+
);
43+
if ( isSubscriptionRequired ) {
44+
// If the backend response has no `missingLicenses` we fall back to "Yoast SEO Premium" so the alert
45+
// always renders with a product name, instead of "active undefined subscription".
46+
const invalidSubscriptions = missingLicenses.length > 0 ? missingLicenses : [ "Yoast SEO Premium" ];
47+
return <SubscriptionError invalidSubscriptions={ invalidSubscriptions } />;
48+
}
2849

2950
let alert;
3051
switch ( errorCode ) {
@@ -69,5 +90,6 @@ ContentPlannerError.propTypes = {
6990
errorCode: PropTypes.number.isRequired,
7091
errorIdentifier: PropTypes.string,
7192
errorMessage: PropTypes.string,
93+
missingLicenses: PropTypes.arrayOf( PropTypes.string ),
7294
onRetry: PropTypes.func,
7395
};

packages/js/src/ai-content-planner/components/outline-modal-content.js

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,28 @@ import { getDescriptionProgress, getProgressColor } from "@yoast/search-metadata
77
import { ContentPlannerError } from "./content-planner-error";
88
import classNames from "classnames";
99
import { IntentCallout } from "./intent-callout";
10-
import { StructureRow } from "./structure-row";
10+
import { StructureRow, StructureRowSkeleton } from "./structure-row";
1111
import { CategorySection } from "./category-section";
1212
import { ASYNC_ACTION_STATUS } from "../../shared-admin/constants";
1313
import { useDraggableStructure, useFetchContentOutline } from "../hooks";
1414
import { Transition } from "@headlessui/react";
15+
import { SKELETON_ROW_COUNT } from "../constants";
16+
17+
/**
18+
* Section header for the Blog post structure list, shared by the loading and loaded states.
19+
*
20+
* @returns {JSX.Element} The StructureSectionHeader component.
21+
*/
22+
const StructureSectionHeader = () => (
23+
<div className="yst-flex yst-items-end yst-justify-between yst-mb-2">
24+
<span className="yst-font-medium yst-text-sm yst-text-slate-800">
25+
{ __( "Blog post structure", "wordpress-seo" ) }
26+
</span>
27+
<span className="yst-text-xs yst-text-slate-500">
28+
{ __( "Drag to reorder", "wordpress-seo" ) }
29+
</span>
30+
</div>
31+
);
1532

1633
/**
1734
* @typedef {import( "../constants" ).Suggestion} Suggestion
@@ -82,10 +99,23 @@ const LoadingOutlineModalContent = () => {
8299
<CategorySection
83100
isLoading={ true }
84101
/>
85-
<div className="yst-flex yst-flex-col yst-gap-4">
86-
<SkeletonFormField label={ __( "Focus Keyphrase", "wordpress-seo" ) } />
87-
<SkeletonFormField label={ __( "Title", "wordpress-seo" ) } />
88-
<SkeletonFormField label={ __( "Meta description", "wordpress-seo" ) } multiline={ true } />
102+
<div className="yst-flex yst-flex-col">
103+
<div className="yst-flex yst-flex-col yst-gap-4">
104+
<SkeletonFormField label={ __( "Focus Keyphrase", "wordpress-seo" ) } />
105+
<SkeletonFormField label={ __( "Title", "wordpress-seo" ) } />
106+
<SkeletonFormField label={ __( "Meta description", "wordpress-seo" ) } multiline={ true } />
107+
</div>
108+
<hr className="yst-border-slate-200 yst-my-6" />
109+
<StructureSectionHeader />
110+
<ul
111+
aria-label={ __( "Blog post structure", "wordpress-seo" ) }
112+
aria-busy={ true }
113+
className="yst-list-none yst-p-0 yst-m-0 yst-flex yst-flex-col yst-gap-2"
114+
>
115+
{ Array.from( { length: SKELETON_ROW_COUNT } ).map( ( _, index ) => (
116+
<StructureRowSkeleton key={ `structure-row-skeleton-${ index }` } />
117+
) ) }
118+
</ul>
89119
</div>
90120
</>;
91121
};
@@ -128,6 +158,8 @@ export const OutlineModalContent = ( {
128158
const {
129159
structure,
130160
dragOverIndex,
161+
reorderMessage,
162+
handleAnnounce,
131163
handleDragStart,
132164
handleDragOver,
133165
handleDrop,
@@ -136,6 +168,9 @@ export const OutlineModalContent = ( {
136168
handleMoveDown,
137169
} = useDraggableStructure();
138170

171+
const handleSentinelDragOver = useCallback( ( e ) => handleDragOver( e, structure.length ), [ handleDragOver, structure.length ] );
172+
const handleSentinelDrop = useCallback( ( e ) => handleDrop( e, structure.length ), [ handleDrop, structure.length ] );
173+
139174
useEffect( () => {
140175
setFocusKeyphrase( suggestion.keyphrase );
141176
setTitle( suggestion.title );
@@ -180,6 +215,7 @@ export const OutlineModalContent = ( {
180215
errorCode={ error.errorCode }
181216
errorIdentifier={ error.errorIdentifier }
182217
errorMessage={ error.errorMessage }
218+
missingLicenses={ error.missingLicenses }
183219
onRetry={ handleRetry }
184220
/>
185221
</Modal.Container.Content>
@@ -189,7 +225,7 @@ export const OutlineModalContent = ( {
189225
return (
190226
<>
191227
<Modal.Container.Content className="yst-overflow-y-auto yst-pt-6 yst-px-6 yst-pb-0 yst-m-0 yst-relative" aria-busy={ isLoading }>
192-
<div className="yst-flex yst-flex-col yst-gap-6 yst-pb-4">
228+
<div className="yst-flex yst-flex-col yst-gap-6">
193229
<IntentCallout
194230
intent={ suggestion.intent }
195231
description={ suggestion.explanation }
@@ -228,7 +264,7 @@ export const OutlineModalContent = ( {
228264
onToggle={ handleCategoryToggle }
229265
/>
230266
) }
231-
<div className="yst-flex yst-flex-col yst-gap-6">
267+
<div className="yst-flex yst-flex-col">
232268
<div className="yst-flex yst-flex-col yst-gap-4">
233269
<TextField
234270
id="content-outline-focus-keyphrase"
@@ -259,16 +295,11 @@ export const OutlineModalContent = ( {
259295

260296
</div>
261297
</div>
262-
<hr className="yst-border-slate-200" />
263-
<div className="yst-flex yst-items-end yst-justify-between" style={ { marginBottom: "-16px" } }>
264-
<span className="yst-font-medium yst-text-sm yst-text-slate-800">
265-
{ __( "Blog post structure", "wordpress-seo" ) }
266-
</span>
267-
<span className="yst-text-xs yst-text-slate-500">
268-
{ __( "Drag to reorder", "wordpress-seo" ) }
269-
</span>
270-
</div>
271-
<div role="listbox" aria-label={ __( "Blog post structure", "wordpress-seo" ) } className="yst-flex yst-flex-col yst-gap-2">
298+
<hr className="yst-border-slate-200 yst-my-6" />
299+
<StructureSectionHeader />
300+
{ /* Live region announces keyboard reorder results to screen readers. */ }
301+
<div aria-live="assertive" aria-atomic="true" className="yst-sr-only">{ reorderMessage }</div>
302+
<ul aria-label={ __( "Blog post structure", "wordpress-seo" ) } className="yst-flex yst-flex-col yst-gap-2 yst-list-none yst-p-0 yst-m-0">
272303
{ structure.map( ( item, index ) => (
273304
<StructureRow
274305
key={ item.id }
@@ -282,14 +313,24 @@ export const OutlineModalContent = ( {
282313
onMoveUp={ handleMoveUp }
283314
onMoveDown={ handleMoveDown }
284315
totalItems={ structure.length }
316+
onAnnounce={ handleAnnounce }
285317
/>
286318
) ) }
287-
</div>
319+
</ul>
320+
{ /* Sentinel drop zone: allows dropping an item into the last position. */ }
321+
<div
322+
className="yst-h-8"
323+
onDragOver={ handleSentinelDragOver }
324+
onDrop={ handleSentinelDrop }
325+
/>
288326
</div>
289327
</Transition>
290328
</div>
291329
<div
292-
className="yst-sticky -yst-left-6 -yst-right-6 yst-bottom-0 yst-h-10 yst-pointer-events-none yst-bg-gradient-to-t yst-from-white yst-to-transparent yst-transition-opacity"
330+
className={ classNames(
331+
isLoading ? "yst-sticky" : "yst-hidden",
332+
"-yst-left-6 -yst-right-6 yst-bottom-0 yst-h-10 yst-pointer-events-none yst-bg-gradient-to-t yst-from-white yst-to-transparent yst-transition-opacity"
333+
) }
293334
aria-hidden="true"
294335
/>
295336
</Modal.Container.Content>

0 commit comments

Comments
 (0)