• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 15936d2b846c322fa3b4b91e32ba3f7c002a45a6

13 May 2025 08:35AM UTC coverage: 58.702% (+0.06%) from 58.644%
15936d2b846c322fa3b4b91e32ba3f7c002a45a6

Pull #22258

github

web-flow
Merge pull request #22254 from Yoast/fix/ai-optimize-woo-upsell

Adds an upsell for AI Optimize on products
Pull Request #22258: Merges the feature branch `feature/ai-optimize-classic` to `trunk`

8175 of 14231 branches covered (57.45%)

Branch coverage included in aggregate %.

37 of 68 new or added lines in 11 files covered. (54.41%)

3 existing lines in 2 files now uncovered.

14070 of 23664 relevant lines covered (59.46%)

100886.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/packages/js/src/components/contentAnalysis/SeoAnalysis.js
1
/* global wpseoAdminL10n */
2
import { withSelect } from "@wordpress/data";
3
import { Component, Fragment } from "@wordpress/element";
4
import { __, sprintf } from "@wordpress/i18n";
5
import { addQueryArgs } from "@wordpress/url";
6
import { LocationConsumer, RootContext } from "@yoast/externals/contexts";
7
import PropTypes from "prop-types";
8
import styled from "styled-components";
9
import getIndicatorForScore from "../../analysis/getIndicatorForScore";
10
import Results from "../../containers/Results";
11
import AnalysisUpsell from "../AnalysisUpsell";
12
import MetaboxCollapsible from "../MetaboxCollapsible";
13
import { ModalSmallContainer } from "../modals/Container";
14
import KeywordSynonyms from "../modals/KeywordSynonyms";
15
import { defaultModalClassName } from "../modals/Modal";
16
import MultipleKeywords from "../modals/MultipleKeywords";
17
import Modal from "../modals/SeoAnalysisModal";
18
import ScoreIconPortal from "../portals/ScoreIconPortal";
19
import SidebarCollapsible from "../SidebarCollapsible";
20
import SynonymSlot from "../slots/SynonymSlot";
21
import { getIconForScore } from "./mapResults";
22
import AIOptimizeButton from "../../ai-optimizer/components/ai-optimize-button";
23
import { shouldRenderAIOptimizeButton } from "../../helpers/shouldRenderAIOptimizeButton";
24

25
const AnalysisHeader = styled.span`
×
26
        font-size: 1em;
27
        font-weight: bold;
28
        margin: 1.5em 0 1em;
29
        display: block;
30
`;
31

32
/**
33
 * Redux container for the seo analysis.
34
 */
35
class SeoAnalysis extends Component {
36
        /**
37
         * Renders the keyword synonyms upsell modal.
38
         *
39
         * @param {string} location The location of the upsell component. Used to determine the shortlinks in the component.
40
         * @param {string} locationContext In which editor this component is rendered.
41
         *
42
         * @returns {JSX.Element} A modalButtonContainer component with the modal for a keyword synonyms upsell.
43
         */
44
        renderSynonymsUpsell( location, locationContext ) {
45
                const modalProps = {
×
46
                        className: `${ defaultModalClassName } yoast-gutenberg-modal__box yoast-gutenberg-modal__no-padding`,
47
                        classes: {
48
                                openButton: "wpseo-keyword-synonyms button-link",
49
                        },
50
                        labels: {
51
                                open: "+ " + __( "Add synonyms", "wordpress-seo" ),
52
                                modalAriaLabel: __( "Add synonyms", "wordpress-seo" ),
53
                                heading: __( "Add synonyms", "wordpress-seo" ),
54
                        },
55
                };
56

57
                const buyLink = wpseoAdminL10n[
×
58
                        location.toLowerCase() === "sidebar"
×
59
                                ? "shortlinks.upsell.sidebar.focus_keyword_synonyms_button"
60
                                : "shortlinks.upsell.metabox.focus_keyword_synonyms_button"
61
                ];
62

63
                return (
×
64
                        <Modal { ...modalProps }>
65
                                <ModalSmallContainer>
66
                                        <KeywordSynonyms buyLink={ addQueryArgs( buyLink, { context: locationContext } ) } />
67
                                </ModalSmallContainer>
68
                        </Modal>
69
                );
70
        }
71

72
        /**
73
         * Renders the multiple keywords upsell modal.
74
         *
75
         * @param {string} location The location of the upsell component. Used to determine the shortlinks in the component.
76
         * @param {string} locationContext In which editor this component is rendered.
77
         *
78
         * @returns {JSX.Element} A modalButtonContainer component with the modal for a multiple keywords upsell.
79
         */
80
        renderMultipleKeywordsUpsell( location, locationContext ) {
81
                const modalProps = {
×
82
                        className: `${ defaultModalClassName } yoast-gutenberg-modal__box yoast-gutenberg-modal__no-padding`,
83
                        classes: {
84
                                openButton: "wpseo-multiple-keywords button-link",
85
                        },
86
                        labels: {
87
                                open: "+ " + __( "Add related keyphrase", "wordpress-seo" ),
88
                                modalAriaLabel: __( "Add related keyphrases", "wordpress-seo" ),
89
                                heading: __( "Add related keyphrases", "wordpress-seo" ),
90
                        },
91
                };
92

93
                const buyLink = wpseoAdminL10n[
×
94
                        location.toLowerCase() === "sidebar"
×
95
                                ? "shortlinks.upsell.sidebar.focus_keyword_additional_button"
96
                                : "shortlinks.upsell.metabox.focus_keyword_additional_button"
97
                ];
98

99
                return (
×
100
                        <Modal { ...modalProps }>
101
                                <ModalSmallContainer>
102
                                        <MultipleKeywords buyLink={ addQueryArgs( buyLink, { context: locationContext } ) } />
103
                                </ModalSmallContainer>
104
                        </Modal>
105
                );
106
        }
107

108
        /**
109
         * Renders the AnalysisUpsell component.
110
         *
111
         * @param {string} location The location of the upsell component. Used to determine the shortlink in the component.
112
         * @param {string} locationContext In which editor this component is rendered.
113
         *
114
         * @returns {JSX.Element} The AnalysisUpsell component.
115
         */
116
        renderWordFormsUpsell( location, locationContext ) {
117
                let url = location === "sidebar"
×
118
                        ? wpseoAdminL10n[ "shortlinks.upsell.sidebar.morphology_upsell_sidebar" ]
119
                        : wpseoAdminL10n[ "shortlinks.upsell.sidebar.morphology_upsell_metabox" ];
120
                url = addQueryArgs( url, { context: locationContext } );
×
121
                return (
×
122
                        <AnalysisUpsell
123
                                url={ url }
124
                                alignment={ location === "sidebar" ? "vertical" : "horizontal" }
×
125
                        />
126
                );
127
        }
128

129
        /**
130
         * Renders the ScoreIconPortal component, which displays a score indication icon in the SEO metabox tab.
131
         *
132
         * @param {string} location       Where this component is rendered.
133
         * @param {string} scoreIndicator String indicating the score.
134
         *
135
         * @returns {JSX.Element} The rendered score icon portal element.
136
         */
137
        renderTabIcon( location, scoreIndicator ) {
138
                // The tab icon should only be rendered for the metabox.
139
                if ( location !== "metabox" ) {
×
140
                        return null;
×
141
                }
142

143
                return (
×
144
                        <ScoreIconPortal
145
                                target="wpseo-seo-score-icon"
146
                                scoreIndicator={ scoreIndicator }
147
                        />
148
                );
149
        }
150

151
        /**
152
         * Returns the list of results used to upsell the user to Premium.
153
         *
154
         * @param {string} location                 Where this component is rendered (metabox or sidebar).
155
         * @param {string} locationContext         In which editor this component is rendered.
156
         *
157
         * @returns {Array} The upsell results.
158
         */
159
        getUpsellResults( location, locationContext ) {
160
                let link = wpseoAdminL10n[ "shortlinks.upsell.metabox.keyphrase_distribution" ];
×
161
                if ( location === "sidebar" ) {
×
162
                        link = wpseoAdminL10n[ "shortlinks.upsell.sidebar.keyphrase_distribution" ];
×
163
                }
164
                link = addQueryArgs( link, { context: locationContext } );
×
165

166
                const keyphraseDistributionUpsellText = sprintf(
×
167
                        /* Translators: %1$s is a span tag that adds styling to 'Keyphrase distribution', %2$s is a closing span tag.
168
                         %3%s is an anchor tag with a link to yoast.com, %4$s is a closing anchor tag.*/
169
                        __(
170
                                "%1$sKeyphrase distribution%2$s: Have you evenly distributed your focus keyphrase throughout the whole text? " +
171
                                "%3$sYoast SEO Premium will tell you!%4$s",
172
                                "wordpress-seo"
173
                        ),
174
                        "<span style='text-decoration: underline'>",
175
                        "</span>",
176
                        `<a href="${ link }" data-action="load-nfd-ctb" data-ctb-id="f6a84663-465f-4cb5-8ba5-f7a6d72224b2" target="_blank">`,
177
                        "</a>"
178
                );
179

180
                return [
×
181
                        {
182
                                score: 0,
183
                                rating: "upsell",
184
                                hasMarks: false,
185
                                hasJumps: false,
186
                                id: "keyphraseDistribution",
187
                                text: keyphraseDistributionUpsellText,
188
                                markerId: "keyphraseDistribution",
189
                        },
190
                ];
191
        }
192

193

194
        /**
195
         * Renders the Yoast AI Optimize button.
196
         * The button is shown when:
197
         * - The assessment can be fixed through Yoast AI Optimize.
198
         * - The AI feature is enabled (for Yoast SEO Premium users; for Free users, the button is shown with an upsell).
199
         * - We are in the block editor.
200
         * - We are not in the Elementor editor, nor in the Elementor in-between screen.
201
         *
202
         * @param {boolean} hasAIFixes Whether the assessment can be fixed through Yoast AI Optimize.
203
         * @param {string} id The assessment ID.
204
         *
205
         * @returns {void|JSX.Element} The AI Optimize button, or nothing if the button should not be shown.
206
         */
NEW
207
        renderAIOptimizeButton = ( hasAIFixes, id ) => {
×
NEW
208
                const { isElementor, isAiFeatureEnabled, isPremium, isTerm } = this.props;
×
209

210
                // Don't show the button if the AI feature is not enabled for Yoast SEO Premium users.
211
                if ( isPremium && ! isAiFeatureEnabled ) {
×
212
                        return;
×
213
                }
214

NEW
215
                const shouldRenderAIButton = shouldRenderAIOptimizeButton( hasAIFixes, isElementor, isTerm );
×
216
                // Show the button if the assessment can be fixed through Yoast AI Optimize, and we are not in the Elementor editor,
217
                // WooCommerce Product pages or Taxonomy
NEW
218
                return shouldRenderAIButton && ( <AIOptimizeButton id={ id } isPremium={ isPremium } /> );
×
219
        };
220

221

222
        /**
223
         * Renders the SEO Analysis component.
224
         *
225
         * @returns {JSX.Element} The SEO Analysis component.
226
         */
227
        render() {
228
                const score = getIndicatorForScore( this.props.overallScore );
×
229
                const { isPremium } = this.props;
×
230
                const highlightingUpsellLink = "shortlinks.upsell.sidebar.highlighting_seo_analysis";
×
231

232
                if ( score.className !== "loading" && this.props.keyword === "" ) {
×
233
                        score.className = "na";
×
234
                        score.screenReaderReadabilityText = __( "Enter a focus keyphrase to calculate the SEO score", "wordpress-seo" );
×
235
                }
236

237
                return (
×
238
                        <LocationConsumer>
239
                                { location => {
240
                                        return (
×
241
                                                <RootContext.Consumer>
242
                                                        { ( { locationContext } ) => {
243
                                                                const Collapsible = location === "metabox" ? MetaboxCollapsible : SidebarCollapsible;
×
244

245
                                                                let upsellResults = [];
×
246
                                                                if ( this.props.shouldUpsell ) {
×
247
                                                                        upsellResults = this.getUpsellResults( location, locationContext );
×
248
                                                                }
249

250
                                                                return (
×
251
                                                                        <Fragment>
252
                                                                                <Collapsible
253
                                                                                        title={ isPremium
×
254
                                                                                                ? __( "Premium SEO analysis", "wordpress-seo" )
255
                                                                                                : __( "SEO analysis", "wordpress-seo" ) }
256
                                                                                        titleScreenReaderText={ score.screenReaderReadabilityText }
257
                                                                                        prefixIcon={ getIconForScore( score.className ) }
258
                                                                                        prefixIconCollapsed={ getIconForScore( score.className ) }
259
                                                                                        subTitle={ this.props.keyword }
260
                                                                                        id={ `yoast-seo-analysis-collapsible-${ location }` }
261
                                                                                >
262
                                                                                        <SynonymSlot location={ location } />
263
                                                                                        { this.props.shouldUpsell && <Fragment>
×
264
                                                                                                { this.renderSynonymsUpsell( location, locationContext ) }
265
                                                                                                { this.renderMultipleKeywordsUpsell( location, locationContext ) }
266
                                                                                        </Fragment> }
267
                                                                                        { this.props.shouldUpsellWordFormRecognition && this.renderWordFormsUpsell( location, locationContext ) }
×
268
                                                                                        <AnalysisHeader>
269
                                                                                                { __( "Analysis results", "wordpress-seo" ) }
270
                                                                                        </AnalysisHeader>
271
                                                                                        <Results
272
                                                                                                results={ this.props.results }
273
                                                                                                upsellResults={ upsellResults }
274
                                                                                                marksButtonClassName="yoast-tooltip yoast-tooltip-w"
275
                                                                                                editButtonClassName="yoast-tooltip yoast-tooltip-w"
276
                                                                                                marksButtonStatus={ this.props.marksButtonStatus }
277
                                                                                                location={ location }
278
                                                                                                shouldUpsellHighlighting={ this.props.shouldUpsellHighlighting }
279
                                                                                                highlightingUpsellLink={ highlightingUpsellLink }
280
                                                                                                renderAIOptimizeButton={ this.renderAIOptimizeButton }
281
                                                                                        />
282
                                                                                </Collapsible>
283
                                                                                { this.renderTabIcon( location, score.className ) }
284
                                                                        </Fragment>
285
                                                                );
286
                                                        } }
287
                                                </RootContext.Consumer>
288
                                        );
289
                                } }
290
                        </LocationConsumer>
291
                );
292
        }
293
}
294

295
SeoAnalysis.propTypes = {
×
296
        results: PropTypes.array,
297
        marksButtonStatus: PropTypes.string,
298
        keyword: PropTypes.string,
299
        shouldUpsell: PropTypes.bool,
300
        shouldUpsellWordFormRecognition: PropTypes.bool,
301
        overallScore: PropTypes.number,
302
        shouldUpsellHighlighting: PropTypes.bool,
303
        isElementor: PropTypes.bool,
304
        isAiFeatureEnabled: PropTypes.bool,
305
        isPremium: PropTypes.bool,
306
        isTerm: PropTypes.bool,
307
};
308

309
SeoAnalysis.defaultProps = {
×
310
        results: [],
311
        marksButtonStatus: null,
312
        keyword: "",
313
        shouldUpsell: false,
314
        shouldUpsellWordFormRecognition: false,
315
        overallScore: null,
316
        shouldUpsellHighlighting: false,
317
        isElementor: false,
318
        isAiFeatureEnabled: false,
319
        isPremium: false,
320
        isTerm: false,
321
};
322

323
export default withSelect( ( select, ownProps ) => {
324
        const {
325
                getFocusKeyphrase,
326
                getMarksButtonStatus,
327
                getResultsForKeyword,
328
                getIsElementorEditor,
329
                getIsPremium,
330
                getIsAiFeatureEnabled,
331
                getIsTerm,
UNCOV
332
        } = select( "yoast-seo/editor" );
×
333

334
        const keyword = getFocusKeyphrase();
×
335

336
        return {
×
337
                ...getResultsForKeyword( keyword ),
338
                marksButtonStatus: ownProps.hideMarksButtons ? "disabled" : getMarksButtonStatus(),
×
339
                keyword,
340
                isElementor: getIsElementorEditor(),
341
                isPremium: getIsPremium(),
342
                isAiFeatureEnabled: getIsAiFeatureEnabled(),
343
                isTerm: getIsTerm(),
344
        };
345
} )( SeoAnalysis );
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc