UNPKG

yoastseo

Version:

Yoast client-side content analysis

326 lines (309 loc) 21.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _i18n = require("@wordpress/i18n"); var _lodash = require("lodash"); var _assessment = _interopRequireDefault(require("../assessment")); var _helpers = require("../../../helpers"); var _AssessmentResult = _interopRequireDefault(require("../../../values/AssessmentResult")); var _inRange = require("../../helpers/assessments/inRange"); var _processExactMatchRequest = _interopRequireDefault(require("../../../languageProcessing/helpers/match/processExactMatchRequest")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * @typedef {import("../../../languageProcessing/AbstractResearcher").default } Researcher * @typedef {import("../../../values/").Paper } Paper */ /** * Enumerator for the different types of counting methods for this assessment. * @type {Readonly<{WORDS: string, CONTENT_WORDS: string, CHARACTERS: string}>} */ const COUNT_TEXT_IN = Object.freeze({ WORDS: "words", CONTENT_WORDS: "content words", CHARACTERS: "characters" }); /** * Assessment to check whether the keyphrase has a good length. */ class KeyphraseLengthAssessment extends _assessment.default { /** * Sets the identifier and the config. * * @param {boolean} isProductPage Whether product page scoring is used or not. * @param {Object} [config] The configuration to use. * @param {Object} [config.parameters] The parameters to use for the assessment. * @param {number} [config.parameters.recommendedMinimum] The recommended minimum length of the keyphrase (in words). * @param {number} [config.parameters.acceptableMaximum] The acceptable maximum length of the keyphrase (in words). * @param {Object} [config.scores] The scores to use for the assessment. * @param {number} [config.scores.veryBad] The score to return if the length of the keyphrase is below recommended minimum. * @param {number} [config.scores.consideration] The score to return if the length of the keyphrase is above acceptable maximum. */ constructor(config, isProductPage = false) { super(); this.defaultConfig = { parameters: { recommendedMinimum: 1, recommendedMaximum: 4, acceptableMaximum: 8 }, parametersNoFunctionWordSupport: { recommendedMaximum: 6, acceptableMaximum: 9 }, scores: { veryBad: -999, bad: 3, okay: 6, good: 9 }, countTextIn: COUNT_TEXT_IN.WORDS, urlTitle: (0, _helpers.createAnchorOpeningTag)("https://yoa.st/33i"), urlCallToAction: (0, _helpers.createAnchorOpeningTag)("https://yoa.st/33j"), isRelatedKeyphrase: false }; this.identifier = "keyphraseLength"; this._config = (0, _lodash.merge)(this.defaultConfig, config); this._isProductPage = isProductPage; } /** * Assesses the keyphrase presence and length. * * @param {Paper} paper The paper to use for the assessment. * @param {Researcher} researcher The researcher used for calling research. * * @returns {AssessmentResult} The result of this assessment. */ getResult(paper, researcher) { this._keyphraseLengthData = researcher.getResearch("keyphraseLength"); const assessmentResult = new _AssessmentResult.default(); const countTextInCharacters = researcher.getConfig("countCharacters"); if (countTextInCharacters) { this._config.countTextIn = COUNT_TEXT_IN.CHARACTERS; } /* * Checks whether the keyphrase length is calculated with function words filtered out AND whether the keyphrase doesn't use double quotes. * If both conditions are true, then the feedback string should output 'content words' instead of only 'words'. * */ const keyphrase = paper.getKeyword(); if (this._keyphraseLengthData.functionWords.length > 0 && !(0, _processExactMatchRequest.default)(keyphrase).exactMatchRequested) { this._config.countTextIn = COUNT_TEXT_IN.CONTENT_WORDS; } /* * Checks whether the researcher has custom config for the scoring boundaries and overrides the current config with it. * If no custom config is found, makes boundaries less strict if the language doesn't have function word support. * */ const customConfig = researcher.getConfig("keyphraseLength"); if (customConfig) { this._config = this.getCustomConfig(researcher); } else if (this._keyphraseLengthData.functionWords.length === 0) { this._config.parameters = (0, _lodash.merge)({}, this._config.parameters, this._config.parametersNoFunctionWordSupport); } // Set a variable that contains the scoring boundaries. this._boundaries = this._config.parameters; const calculatedResult = this.calculateResult(); assessmentResult.setScore(calculatedResult.score); assessmentResult.setText(calculatedResult.resultText); if (assessmentResult.getScore() < 9) { assessmentResult.setHasJumps(true); assessmentResult.setEditFieldName("keyphrase"); assessmentResult.setEditFieldAriaLabel((0, _i18n.__)("Edit your keyphrase", "wordpress-seo")); } return assessmentResult; } /** * Merges language-specific configurations for product/regular pages. * * @param {Researcher} researcher The researcher used for calling research. * * @returns {Object} Configuration to use. */ getCustomConfig(researcher) { const customKeyphraseLengthConfig = researcher.getConfig("keyphraseLength"); if (this._isProductPage && Object.hasOwn(customKeyphraseLengthConfig, "productPages")) { // If a language has specific configuration for keyphrase length in product pages, that configuration is used. return (0, _lodash.merge)(this._config, customKeyphraseLengthConfig.productPages); } // If a language has a configuration for keyphrase length for regular pages, that configuration is used. return (0, _lodash.merge)(this._config, customKeyphraseLengthConfig.defaultAnalysis); } /** * Returns the feedback texts for the conditions when the keyphrase is too long or too short. * * @returns {{lessThanMinimum: (function(string, string): string), firstSentence: (function(string): string), moreThanMinimum: (function(string, string): string), wayMoreThanMinimum: (function(string, string): string), wayLessThanMinimum: (function(string, string): string)}} */ getFeedbackTexts() { return { firstSentence: countTextIn => { const wordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of words, %2$s expands to a link on yoast.com, %3$s expands to the anchor end tag. */ (0, _i18n._n)("%2$sKeyphrase length%3$s: The keyphrase contains %1$d word.", "%2$sKeyphrase length%3$s: The keyphrase contains %1$d words.", this._keyphraseLengthData.keyphraseLength, "wordpress-seo"), this._keyphraseLengthData.keyphraseLength, this._config.urlTitle, "</a>"); const contentWordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of content words, %2$s expands to a link on yoast.com, %3$s expands to the anchor end tag. */ (0, _i18n._n)("%2$sKeyphrase length%3$s: The keyphrase contains %1$d content word.", "%2$sKeyphrase length%3$s: The keyphrase contains %1$d content words.", this._keyphraseLengthData.keyphraseLength, "wordpress-seo"), this._keyphraseLengthData.keyphraseLength, this._config.urlTitle, "</a>"); const characterFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of characters, %2$s expands to a link on yoast.com, %3$s expands to the anchor end tag. */ (0, _i18n._n)("%2$sKeyphrase length%3$s: The keyphrase contains %1$d character.", "%2$sKeyphrase length%3$s: The keyphrase contains %1$d characters.", this._keyphraseLengthData.keyphraseLength, "wordpress-seo"), this._keyphraseLengthData.keyphraseLength, this._config.urlTitle, "</a>"); if (countTextIn === COUNT_TEXT_IN.WORDS) { return wordFeedback; } else if (countTextIn === COUNT_TEXT_IN.CONTENT_WORDS) { return contentWordFeedback; } return characterFeedback; }, moreThanMinimum: (countTextIn, keyphraseContains) => { const wordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of words, %2$s expands to the sentence "The keyphrase contains X word(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's more than the recommended maximum of %1$d word. %3$sMake it shorter%4$s!", "%2$s That's more than the recommended maximum of %1$d words. %3$sMake it shorter%4$s!", this._boundaries.recommendedMaximum, "wordpress-seo"), this._boundaries.recommendedMaximum, keyphraseContains, this._config.urlCallToAction, "</a>"); const contentWordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of content words, %2$s expands to the sentence "The keyphrase contains X content word(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's more than the recommended maximum of %1$d content word. %3$sMake it shorter%4$s!", "%2$s That's more than the recommended maximum of %1$d content words. %3$sMake it shorter%4$s!", this._boundaries.recommendedMaximum, "wordpress-seo"), this._boundaries.recommendedMaximum, keyphraseContains, this._config.urlCallToAction, "</a>"); const characterFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of characters, %2$s expands to the sentence "The keyphrase contains X character(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's more than the recommended maximum of %1$d character. %3$sMake it shorter%4$s!", "%2$s That's more than the recommended maximum of %1$d characters. %3$sMake it shorter%4$s!", this._boundaries.recommendedMaximum, "wordpress-seo"), this._boundaries.recommendedMaximum, keyphraseContains, this._config.urlCallToAction, "</a>"); if (countTextIn === COUNT_TEXT_IN.WORDS) { return wordFeedback; } else if (countTextIn === COUNT_TEXT_IN.CONTENT_WORDS) { return contentWordFeedback; } return characterFeedback; }, wayMoreThanMinimum: (countTextIn, keyphraseContains) => { const wordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of words, %2$s expands to the sentence "The keyphrase contains X word(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's way more than the recommended maximum of %1$d word. %3$sMake it shorter%4$s!", "%2$s That's way more than the recommended maximum of %1$d words. %3$sMake it shorter%4$s!", this._boundaries.recommendedMaximum, "wordpress-seo"), this._boundaries.recommendedMaximum, keyphraseContains, this._config.urlCallToAction, "</a>"); const contentWordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of content words, %2$s expands to the sentence "The keyphrase contains X content word(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's way more than the recommended maximum of %1$d content word. %3$sMake it shorter%4$s!", "%2$s That's way more than the recommended maximum of %1$d content words. %3$sMake it shorter%4$s!", this._boundaries.recommendedMaximum, "wordpress-seo"), this._boundaries.recommendedMaximum, keyphraseContains, this._config.urlCallToAction, "</a>"); const characterFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of characters, %2$s expands to the sentence "The keyphrase contains X character(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's way more than the recommended maximum of %1$d character. %3$sMake it shorter%4$s!", "%2$s That's way more than the recommended maximum of %1$d characters. %3$sMake it shorter%4$s!", this._boundaries.recommendedMaximum, "wordpress-seo"), this._boundaries.recommendedMaximum, keyphraseContains, this._config.urlCallToAction, "</a>"); if (countTextIn === COUNT_TEXT_IN.WORDS) { return wordFeedback; } else if (countTextIn === COUNT_TEXT_IN.CONTENT_WORDS) { return contentWordFeedback; } return characterFeedback; }, lessThanMinimum: (countTextIn, keyphraseContains) => { const wordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of words, %2$s expands to the sentence "The keyphrase contains X word(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's less than the recommended minimum of %1$d word. %3$sMake it longer%4$s!", "%2$s That's less than the recommended minimum of %1$d words. %3$sMake it longer%4$s!", this._boundaries.recommendedMinimum, "wordpress-seo"), this._boundaries.recommendedMinimum, keyphraseContains, this._config.urlCallToAction, "</a>"); const contentWordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of content words, %2$s expands to the sentence "The keyphrase contains X content word(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's less than the recommended minimum of %1$d content word. %3$sMake it longer%4$s!", "%2$s That's less than the recommended minimum of %1$d content words. %3$sMake it longer%4$s!", this._boundaries.recommendedMinimum, "wordpress-seo"), this._boundaries.recommendedMinimum, keyphraseContains, this._config.urlCallToAction, "</a>"); const characterFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of characters, %2$s expands to the sentence "The keyphrase contains X character(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's less than the recommended minimum of %1$d character. %3$sMake it longer%4$s!", "%2$s That's less than the recommended minimum of %1$d characters. %3$sMake it longer%4$s!", this._boundaries.recommendedMinimum, "wordpress-seo"), this._boundaries.recommendedMinimum, keyphraseContains, this._config.urlCallToAction, "</a>"); if (countTextIn === COUNT_TEXT_IN.WORDS) { return wordFeedback; } else if (countTextIn === COUNT_TEXT_IN.CONTENT_WORDS) { return contentWordFeedback; } return characterFeedback; }, wayLessThanMinimum: (countTextIn, keyphraseContains) => { const wordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of words, %2$s expands to the sentence "The keyphrase contains X word(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's way less than the recommended minimum of %1$d word. %3$sMake it longer%4$s!", "%2$s That's way less than the recommended minimum of %1$d words. %3$sMake it longer%4$s!", this._boundaries.recommendedMinimum, "wordpress-seo"), this._boundaries.recommendedMinimum, keyphraseContains, this._config.urlCallToAction, "</a>"); const contentWordFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of content words, %2$s expands to the sentence "The keyphrase contains X content word(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's way less than the recommended minimum of %1$d content word. %3$sMake it longer%4$s!", "%2$s That's way less than the recommended minimum of %1$d content words. %3$sMake it longer%4$s!", this._boundaries.recommendedMinimum, "wordpress-seo"), this._boundaries.recommendedMinimum, keyphraseContains, this._config.urlCallToAction, "</a>"); const characterFeedback = (0, _i18n.sprintf)(/* translators: %1$d expands to the number of characters, %2$s expands to the sentence "The keyphrase contains X character(s).", %3$s expands to a link on yoast.com, %4$s expands to the anchor end tag. */ (0, _i18n._n)("%2$s That's way less than the recommended minimum of %1$d character. %3$sMake it longer%4$s!", "%2$s That's way less than the recommended minimum of %1$d characters. %3$sMake it longer%4$s!", this._boundaries.recommendedMinimum, "wordpress-seo"), this._boundaries.recommendedMinimum, keyphraseContains, this._config.urlCallToAction, "</a>"); if (countTextIn === COUNT_TEXT_IN.WORDS) { return wordFeedback; } else if (countTextIn === COUNT_TEXT_IN.CONTENT_WORDS) { return contentWordFeedback; } return characterFeedback; } }; } /** * Calculates the result for product pages based on the keyphraseLength research. * @returns {{score: number, resultText: string}} The score and feedback for a product page. */ calculateResultForProduct() { // Calculates very bad score for product pages. if (this._keyphraseLengthData.keyphraseLength === 0) { return this.getNoKeyphraseFeedback(); } // Calculates good score for product pages. if ((0, _inRange.inRangeStartEndInclusive)(this._keyphraseLengthData.keyphraseLength, this._boundaries.recommendedMinimum, this._boundaries.recommendedMaximum)) { return { score: this._config.scores.good, resultText: (0, _i18n.sprintf)(/* translators: %1$s expands to a link on yoast.com, %2$s expands to the anchor end tag. */ (0, _i18n.__)("%1$sKeyphrase length%2$s: Good job!", "wordpress-seo"), this._config.urlTitle, "</a>") }; } // Gets functions used to create feedback strings for the 'okay' and 'bad' assessment scores. const feedbackTexts = this.getFeedbackTexts(); const firstSentence = feedbackTexts.firstSentence(this._config.countTextIn); // Calculates bad score for product pages. if (this._keyphraseLengthData.keyphraseLength <= this._boundaries.acceptableMinimum) { return { score: this._config.scores.bad, resultText: feedbackTexts.wayLessThanMinimum(this._config.countTextIn, firstSentence) }; } if (this._keyphraseLengthData.keyphraseLength > this._boundaries.acceptableMaximum) { return { score: this._config.scores.bad, resultText: feedbackTexts.wayMoreThanMinimum(this._config.countTextIn, firstSentence) }; } // Calculates okay score for product pages when the keyphrase is too short. if ((0, _lodash.inRange)(this._keyphraseLengthData.keyphraseLength, this._boundaries.acceptableMinimum, this._boundaries.recommendedMinimum)) { return { score: this._config.scores.okay, resultText: feedbackTexts.lessThanMinimum(this._config.countTextIn, firstSentence) }; } // Calculates okay score for product pages when the keyphrase is too long. return { score: this._config.scores.okay, resultText: feedbackTexts.moreThanMinimum(this._config.countTextIn, firstSentence) }; } /** * Returns the feedback when no keyphrase was set. * @returns {{score: number, resultText: string}} The score and feedback for when no keyphrase is set. */ getNoKeyphraseFeedback() { if (this._config.isRelatedKeyphrase) { return { score: this._config.scores.veryBad, resultText: (0, _i18n.sprintf)(/* translators: %1$s and %2$s expand to links on yoast.com, %3$s expands to the anchor end tag */ (0, _i18n.__)("%1$sKeyphrase length%3$s: %2$sSet a keyphrase in order to calculate your SEO score%3$s.", "wordpress-seo"), this._config.urlTitle, this._config.urlCallToAction, "</a>") }; } return { score: this._config.scores.veryBad, resultText: (0, _i18n.sprintf)(/* translators: %1$s and %2$s expand to links on yoast.com, %3$s expands to the anchor end tag */ (0, _i18n.__)("%1$sKeyphrase length%3$s: No focus keyphrase was set for this page. %2$sSet a keyphrase in order to calculate your SEO score%3$s.", "wordpress-seo"), this._config.urlTitle, this._config.urlCallToAction, "</a>") }; } /** * Calculates the result based on the keyphraseLength research. * @returns {{score: number, resultText: string}} The score and feedback for a regular post. */ calculateResult() { if (this._isProductPage) { return this.calculateResultForProduct(); } // Calculates scores for regular pages. if (this._keyphraseLengthData.keyphraseLength < this._boundaries.recommendedMinimum) { return this.getNoKeyphraseFeedback(); } if ((0, _lodash.inRange)(this._keyphraseLengthData.keyphraseLength, this._boundaries.recommendedMinimum, this._boundaries.recommendedMaximum + 1)) { return { score: this._config.scores.good, resultText: (0, _i18n.sprintf)(/* translators: %1$s expands to a link on yoast.com, %2$s expands to the anchor end tag. */ (0, _i18n.__)("%1$sKeyphrase length%2$s: Good job!", "wordpress-seo"), this._config.urlTitle, "</a>") }; } // Gets functions used to create feedback strings for the 'okay' and 'bad' assessment scores. const feedbackTexts = this.getFeedbackTexts(); const firstSentence = feedbackTexts.firstSentence(this._config.countTextIn); if ((0, _lodash.inRange)(this._keyphraseLengthData.keyphraseLength, this._boundaries.recommendedMaximum + 1, this._boundaries.acceptableMaximum + 1)) { return { score: this._config.scores.okay, resultText: feedbackTexts.moreThanMinimum(this._config.countTextIn, firstSentence) }; } return { score: this._config.scores.bad, resultText: feedbackTexts.wayMoreThanMinimum(this._config.countTextIn, firstSentence) }; } } exports.default = KeyphraseLengthAssessment; //# sourceMappingURL=KeyphraseLengthAssessment.js.map