Comparing version 0.0.2 to 0.0.3
@@ -10,2 +10,3 @@ import { type CheerioAPI } from "cheerio"; | ||
siteDomainName: string | null; | ||
bodyText: string; | ||
constructor(htmlContent: string, siteDomainName?: string | null); | ||
@@ -19,3 +20,4 @@ getAllLinks(): Link[]; | ||
getInternalLinks(): LinksGroup; | ||
getPureText(stringContent: string): string; | ||
getWordCount(stringContent?: string | null): number; | ||
} |
@@ -13,2 +13,3 @@ "use strict"; | ||
this.siteDomainName = siteDomainName; | ||
this.bodyText = this.htmlDom.text().toLowerCase(); | ||
} | ||
@@ -82,10 +83,9 @@ getAllLinks() { | ||
} | ||
getPureText(stringContent) { | ||
let gapSpaceRegex = /\s+/gi; | ||
return stringContent.trim().replace(gapSpaceRegex, ' '); | ||
} | ||
getWordCount(stringContent = null) { | ||
if (!stringContent) { | ||
stringContent = this.htmlDom.text().toLowerCase(); | ||
} | ||
else { | ||
stringContent = stringContent.toLowerCase(); | ||
} | ||
return stringContent.split(' ').length; | ||
stringContent = stringContent ? stringContent.toLowerCase() : this.htmlDom.text().toLowerCase(); | ||
return this.getPureText(stringContent).split(' ').length; | ||
} | ||
@@ -92,0 +92,0 @@ } |
@@ -26,6 +26,9 @@ interface Link { | ||
seoScore: number; | ||
wordCount: number; | ||
keywordSeoScore: number; | ||
keywordFrequency: number; | ||
messages: { | ||
warnings: string[]; | ||
goodPoints: string[]; | ||
minorWarnings: string[]; | ||
}; | ||
@@ -40,4 +43,5 @@ keywordDensity: number; | ||
keywordWithTitle: KeywordDensity; | ||
wordCount: number; | ||
}; | ||
} | ||
export { Link, LinksGroup, KeywordDensity, ContentJson, SeoData }; |
@@ -6,22 +6,61 @@ import type { HtmlAnalyzer } from "./html-analyzer"; | ||
MAXIMUM_KEYWORD_DENSITY: number; | ||
MAXIMUM_SUB_KEYWORD_DENSITY: number; | ||
MINIMUM_SUB_KEYWORD_DENSITY: number; | ||
EXTREME_LOW_SUB_KEYWORD_DENSITY: number; | ||
MAXIMUM_META_DESCRIPTION_LENGTH: number; | ||
MAXIMUM_META_DESCRIPTION_DENSITY: number; | ||
MINIMUM_META_DESCRIPTION_DENSITY: number; | ||
MAXIMUM_TITLE_LENGTH: number; | ||
MINIMUM_TITLE_LENGTH: number; | ||
MAXIMUM_SUB_KEYWORD_IN_META_DESCRIPTION_DENSITY: number; | ||
MINIMUM_SUB_KEYWORD_IN_META_DESCRIPTION_DENSITY: number; | ||
content: ContentJson; | ||
htmlAnalyzer: HtmlAnalyzer; | ||
htmlDom: HtmlAnalyzer['htmlDom']; | ||
bodyText: string; | ||
keywordDensity: number; | ||
messages: { | ||
warnings: string[]; | ||
minorWarnings: string[]; | ||
goodPoints: string[]; | ||
}; | ||
constructor(content: ContentJson, htmlAnalyzer: HtmlAnalyzer); | ||
getSubKeywordsDensity(): KeywordDensity[]; | ||
calculateDensity(keyword: string, bodyText?: string | null): number; | ||
getKeywordDensity(): number; | ||
totalUniqueInternalLinksCount(): number; | ||
totalUniqueExternalLinksCount(): number; | ||
getMessages(): { | ||
warnings: string[]; | ||
goodPoints: string[]; | ||
}; | ||
getKeywordInTitle(keyword?: string | null): KeywordDensity; | ||
getSubKeywordsInTitle(): KeywordDensity[]; | ||
countOccurrencesInString(keyword: string, stringContent: string): number; | ||
getKeywordInMetaDescription(keyword?: string | null): KeywordDensity; | ||
getSubKeywordsInMetaDescription(): KeywordDensity[]; | ||
getSeoScore(): number; | ||
getKeywordSeoScore(): number; | ||
getTitleWordCount(): number; | ||
private assignMessagesForKeyword; | ||
private assignMessagesForSubKeywords; | ||
private assignMessagesForTitle; | ||
private assignMessagesForLinks; | ||
private assignMessagesForMetaDescription; | ||
/** | ||
* Returns the messages object. | ||
* @return object The messages object. | ||
* @example | ||
* { | ||
* goodPoints: [], | ||
* warnings: [], | ||
* minorWarnings: [], | ||
* } | ||
* @see SeoAnalyzer.messages | ||
*/ | ||
private assignMessages; | ||
/** | ||
* Calculates the density of a keyword in the given string of body text. | ||
* @param keyword Should not be null. | ||
* @param bodyText If null, it will use the default value, i.e. `this.htmlAnalyzer.bodyText` | ||
*/ | ||
calculateDensity(keyword: string, bodyText?: string | null): number; | ||
/** | ||
* Returns the number of occurrences of a keyword in a string. Or you can say, it returns the keyword count in the given string. | ||
* @param keyword If null, it will use the default value, i.e. `this.content.keyword` | ||
* @param stringContent If null, it will use the default value, i.e. `this.htmlAnalyzer.bodyText` | ||
* @return number The number of occurrences of the keyword in the string. | ||
*/ | ||
countOccurrencesInString(keyword?: string | null, stringContent?: string | null): number; | ||
} |
@@ -6,8 +6,23 @@ "use strict"; | ||
constructor(content, htmlAnalyzer) { | ||
this.MINIMUM_KEYWORD_DENSITY = 0.5; | ||
this.MAXIMUM_KEYWORD_DENSITY = 5; | ||
this.MINIMUM_KEYWORD_DENSITY = 1; | ||
this.MAXIMUM_KEYWORD_DENSITY = 3; | ||
this.MAXIMUM_SUB_KEYWORD_DENSITY = 1; | ||
this.MINIMUM_SUB_KEYWORD_DENSITY = 0.12; | ||
this.EXTREME_LOW_SUB_KEYWORD_DENSITY = 0.09; | ||
this.MAXIMUM_META_DESCRIPTION_LENGTH = 160; | ||
this.MAXIMUM_META_DESCRIPTION_DENSITY = 5; | ||
this.MINIMUM_META_DESCRIPTION_DENSITY = 2; | ||
this.MAXIMUM_TITLE_LENGTH = 70; | ||
this.MINIMUM_TITLE_LENGTH = 40; | ||
this.MAXIMUM_SUB_KEYWORD_IN_META_DESCRIPTION_DENSITY = 5; | ||
this.MINIMUM_SUB_KEYWORD_IN_META_DESCRIPTION_DENSITY = 2; | ||
this.messages = { | ||
warnings: [], | ||
minorWarnings: [], | ||
goodPoints: [] | ||
}; | ||
this.content = content; | ||
this.htmlAnalyzer = htmlAnalyzer; | ||
this.htmlDom = htmlAnalyzer.htmlDom; | ||
this.bodyText = this.htmlDom.text().toLowerCase(); | ||
this.keywordDensity = this.calculateDensity(this.content.keyword); | ||
this.assignMessages(); | ||
} | ||
@@ -25,12 +40,2 @@ getSubKeywordsDensity() { | ||
} | ||
calculateDensity(keyword, bodyText = null) { | ||
if (!bodyText) { | ||
bodyText = this.bodyText; | ||
} | ||
let keywordCount = this.countOccurrencesInString(keyword, bodyText); | ||
return (keywordCount / bodyText.split(' ').length) * 100; | ||
} | ||
getKeywordDensity() { | ||
return this.calculateDensity(this.content.keyword); | ||
} | ||
totalUniqueInternalLinksCount() { | ||
@@ -42,136 +47,257 @@ return this.htmlAnalyzer.getInternalLinks().unique.length; | ||
} | ||
getMessages() { | ||
const warnings = []; | ||
const goodPoints = []; | ||
let keywordDensity = this.getKeywordDensity(); | ||
const wordCount = this.htmlAnalyzer.getWordCount(); | ||
getKeywordInTitle(keyword = null) { | ||
var _a; | ||
keyword = keyword !== null && keyword !== void 0 ? keyword : this.content.keyword; | ||
const density = this.calculateDensity(keyword, this.content.title); | ||
return { | ||
keyword, | ||
density, | ||
position: (_a = this.content.title) === null || _a === void 0 ? void 0 : _a.indexOf(keyword) | ||
}; | ||
} | ||
getSubKeywordsInTitle() { | ||
let subKeywordsInTitle = []; | ||
this.content.subKeywords.forEach((sub_keyword) => { | ||
subKeywordsInTitle.push(this.getKeywordInTitle(sub_keyword)); | ||
}); | ||
return subKeywordsInTitle; | ||
} | ||
getKeywordInMetaDescription(keyword = null) { | ||
var _a; | ||
if (keyword === null) { | ||
keyword = this.content.keyword; | ||
} | ||
const density = this.calculateDensity(keyword, this.content.metaDescription); | ||
return { | ||
keyword, | ||
density, | ||
position: (_a = this.content.metaDescription) === null || _a === void 0 ? void 0 : _a.indexOf(keyword) | ||
}; | ||
} | ||
getSubKeywordsInMetaDescription() { | ||
let subKeywordsInTitle = []; | ||
this.content.subKeywords.forEach((sub_keyword) => { | ||
subKeywordsInTitle.push(this.getKeywordInMetaDescription(sub_keyword)); | ||
}); | ||
return subKeywordsInTitle; | ||
} | ||
getSeoScore() { | ||
const MAX_SCORE = 100; | ||
const { warnings, goodPoints } = this.messages; | ||
const messagesScore = ((goodPoints.length) / (warnings.length + goodPoints.length)) * 100; | ||
return Math.min(messagesScore, MAX_SCORE); // SEO score should never go above 100 | ||
} | ||
getKeywordSeoScore() { | ||
const MAX_SCORE = 100; | ||
const keywordInTitle = this.getKeywordInTitle(); | ||
const subKeywordsInTitle = this.getSubKeywordsInTitle(); | ||
const subKeywordsDensity = this.getSubKeywordsDensity(); | ||
const keywordInTitleScore = keywordInTitle.density * 10; | ||
const subKeywordsInTitleScore = subKeywordsInTitle.length * 10; | ||
const subKeywordsDensityScore = subKeywordsDensity.reduce((total, subKeywordDensity) => { | ||
return total + (subKeywordDensity.density * 10); | ||
}, 0); | ||
const keywordDensityScore = this.keywordDensity * 10; | ||
const totalScore = keywordInTitleScore + subKeywordsInTitleScore + subKeywordsDensityScore + keywordDensityScore; | ||
return Math.min(totalScore, MAX_SCORE); // SEO score should never go above 100 | ||
} | ||
getTitleWordCount() { | ||
return this.htmlAnalyzer.getWordCount(this.content.title); | ||
} | ||
assignMessagesForKeyword() { | ||
// warning for keyword not in content | ||
if (this.content.keyword) { | ||
goodPoints.push(`Your main keyword is "${this.content.keyword}".`); | ||
this.messages.goodPoints.push(`Good, your content has a keyword "${this.content.keyword}".`); | ||
// warning for keyword overstuffing | ||
if (this.keywordDensity > 5) { | ||
this.messages.warnings.push('Serious keyword overstuffing.'); | ||
} | ||
// warning for keyword density too high or too low based on content length | ||
if (this.keywordDensity < this.MINIMUM_KEYWORD_DENSITY) { | ||
this.messages.warnings.push(`Keyword density is too low. It is ${this.keywordDensity.toFixed(2)}%, try increasing it.`); | ||
} | ||
else if (this.keywordDensity > this.MAXIMUM_KEYWORD_DENSITY) { | ||
this.messages.warnings.push(`Keyword density is too high. It is ${this.keywordDensity.toFixed(2)}%, try decreasing it.`); | ||
} | ||
else { | ||
this.messages.goodPoints.push(`Keyword density is ${this.keywordDensity.toFixed(2)}%.`); | ||
} | ||
} | ||
else { | ||
warnings.push('Missing main keyword.'); | ||
this.messages.warnings.push('Missing main keyword, please add one.'); | ||
} | ||
} | ||
assignMessagesForSubKeywords() { | ||
// warning for sub keywords in content | ||
if (this.content.subKeywords.length > 0) { | ||
goodPoints.push(`Your sub keywords are "${this.content.subKeywords.join('", "')}".`); | ||
this.messages.goodPoints.push(`Good, your content has sub keywords "${this.content.subKeywords.join(', ')}".`); | ||
// warning for sub keywords not in title | ||
const subKeywordsDensity = this.getSubKeywordsDensity(); | ||
subKeywordsDensity.forEach((subKeywordDensity) => { | ||
if (subKeywordDensity.density > this.MAXIMUM_SUB_KEYWORD_DENSITY) { | ||
this.messages.warnings.push(`The density of sub keyword "${subKeywordDensity.keyword}" is too high in the content, i.e. ${subKeywordDensity.density.toFixed(2)}%.`); | ||
} | ||
else if (subKeywordDensity.density < this.MINIMUM_SUB_KEYWORD_DENSITY) { | ||
let densityBeingLowString = subKeywordDensity.density < this.EXTREME_LOW_SUB_KEYWORD_DENSITY ? 'too low' : 'low'; | ||
this.messages.warnings.push(`The density of sub keyword "${subKeywordDensity.keyword}" is ${densityBeingLowString} in the content, i.e. ${subKeywordDensity.density.toFixed(2)}%.`); | ||
} | ||
else { | ||
this.messages.goodPoints.push(`The density of sub keyword "${subKeywordDensity.keyword}" is ${subKeywordDensity.density.toFixed(2)}% in the content, which is good.`); | ||
} | ||
}); | ||
} | ||
else { | ||
warnings.push('Missing sub keywords.'); | ||
this.messages.minorWarnings.push('Missing sub keywords, please add some.'); | ||
} | ||
// warning for keyword density too high or too low based on content length | ||
if (keywordDensity < this.MINIMUM_KEYWORD_DENSITY) { | ||
warnings.push(`Keyword density is too low. It is ${keywordDensity.toFixed(2)}%, try increasing it.`); | ||
} | ||
else if (keywordDensity > this.MAXIMUM_KEYWORD_DENSITY) { | ||
warnings.push(`Keyword density is too high. It is ${keywordDensity.toFixed(2)}%, try decreasing it.`); | ||
} | ||
else { | ||
goodPoints.push(`Keyword density is ${keywordDensity.toFixed(2)}%.`); | ||
} | ||
// checking keyword density for subKeywords | ||
const subKeywordsDensity = this.getSubKeywordsDensity(); | ||
subKeywordsDensity.forEach((subKeywordDensity) => { | ||
if (subKeywordDensity.density > 3) { | ||
warnings.push(`The density of sub keyword "${subKeywordDensity.keyword}" is too high, i.e. ${subKeywordDensity.density.toFixed(2)}%.`); | ||
} | ||
assignMessagesForTitle() { | ||
// warning for content title and its length | ||
if (this.content.title) { | ||
if (this.content.title.length > this.MAXIMUM_TITLE_LENGTH) { | ||
this.messages.warnings.push('Title tag is too long.'); | ||
} | ||
else if (subKeywordDensity.density < 0.3) { | ||
let densityBeingLowString = subKeywordDensity.density < 0.2 ? 'too low' : 'low'; | ||
warnings.push(`The density of sub keyword "${subKeywordDensity.keyword}" is ${densityBeingLowString}, i.e. ${subKeywordDensity.density.toFixed(2)}%.`); | ||
else if (this.content.title.length < this.MINIMUM_TITLE_LENGTH) { | ||
this.messages.warnings.push('Title tag is too short.'); | ||
} | ||
else { | ||
goodPoints.push(`The density of sub keyword "${subKeywordDensity.keyword}" is ${subKeywordDensity.density.toFixed(2)}%.`); | ||
this.messages.goodPoints.push(`Title tag is ${this.content.title.length} characters long.`); | ||
} | ||
}); | ||
if (this.getKeywordInTitle()) { | ||
goodPoints.push(`You have your main keyword in question.`); | ||
const keywordInTitle = this.getKeywordInTitle(); | ||
if (keywordInTitle.density) { | ||
this.messages.goodPoints.push(`Keyword density in title is ${keywordInTitle.density.toFixed(2)}%, which is good.`); | ||
} | ||
else { | ||
this.messages.warnings.push('No main keyword in title.'); | ||
} | ||
if (this.content.title) { | ||
if (this.getSubKeywordsInTitle().length > 0) { | ||
this.messages.goodPoints.push(`You have ${this.getSubKeywordsInTitle().length} sub keywords in title.`); | ||
} | ||
else { | ||
this.messages.minorWarnings.push('No sub keywords in the title.'); | ||
} | ||
} | ||
} | ||
else { | ||
warnings.push('No main keyword in question.'); | ||
this.messages.warnings.push('Missing title tag, please add one.'); | ||
} | ||
if (this.getSubKeywordsInTitle().length > 0) { | ||
goodPoints.push(`You have ${this.getSubKeywordsInTitle().length} sub keywords in question.`); | ||
} | ||
else { | ||
warnings.push('No sub keywords in question.'); | ||
} | ||
} | ||
assignMessagesForLinks() { | ||
let wordCount = this.htmlAnalyzer.getWordCount(); | ||
// warning for less internal links based on content length | ||
if (this.totalUniqueInternalLinksCount() < (wordCount / 100)) { | ||
warnings.push(`Not enough internal links. You only have ${this.totalUniqueInternalLinksCount()} unique internal links, try increasing it.`); | ||
if (this.totalUniqueInternalLinksCount() < (wordCount / 300)) { | ||
this.messages.warnings.push(`Not enough internal links. You only have ${this.totalUniqueInternalLinksCount()} unique internal links, try increasing it.`); | ||
} | ||
else { | ||
goodPoints.push(`You have ${this.totalUniqueInternalLinksCount()} internal links.`); | ||
this.messages.goodPoints.push(`You have ${this.totalUniqueInternalLinksCount()} internal links.`); | ||
} | ||
// warning for less outbound links based on content length | ||
if (this.totalUniqueExternalLinksCount() < (wordCount / 200)) { | ||
warnings.push(`Not enough outbound links. You only have ${this.totalUniqueExternalLinksCount()}, try increasing it.`); | ||
if (this.totalUniqueExternalLinksCount() < (wordCount / 400)) { | ||
this.messages.warnings.push(`Not enough outbound links. You only have ${this.totalUniqueExternalLinksCount()}, try increasing it.`); | ||
} | ||
// warning for duplicate internal links | ||
if (this.htmlAnalyzer.getInternalLinks().duplicate.length > 1) { | ||
warnings.push(`You have ${this.htmlAnalyzer.getInternalLinks().duplicate.length} duplicate internal links.`); | ||
this.messages.minorWarnings.push(`You have ${this.htmlAnalyzer.getInternalLinks().duplicate.length} duplicate internal links.`); | ||
} | ||
else { | ||
goodPoints.push('No duplicate internal links.'); | ||
this.messages.goodPoints.push('No duplicate internal links.'); | ||
} | ||
// warning for duplicate external links | ||
if (this.htmlAnalyzer.getOutboundLinks().duplicate.length > 1) { | ||
warnings.push(`You have ${this.htmlAnalyzer.getOutboundLinks().duplicate.length} duplicate outbound links.`); | ||
this.messages.minorWarnings.push(`You have ${this.htmlAnalyzer.getOutboundLinks().duplicate.length} duplicate outbound links.`); | ||
} | ||
else { | ||
goodPoints.push('No duplicate outbound links.'); | ||
this.messages.goodPoints.push('No duplicate outbound links.'); | ||
} | ||
if (this.getKeywordDensity() > 10) | ||
warnings.push('Serious keyword overstuffing.'); | ||
if (this.htmlDom('title').text().length > 60) | ||
warnings.push('Title tag is too long.'); | ||
if (!this.content.metaDescription) | ||
warnings.push('Missing meta description.'); | ||
return { warnings, goodPoints }; | ||
} | ||
getKeywordInTitle(keyword = null) { | ||
var _a; | ||
if (keyword === null) { | ||
keyword = this.content.keyword; | ||
assignMessagesForMetaDescription() { | ||
if (this.content.metaDescription) { | ||
let keywordInMetaDescription = this.getKeywordInMetaDescription(); | ||
// warning for meta description length | ||
if (this.content.metaDescription.length > this.MAXIMUM_META_DESCRIPTION_LENGTH) { | ||
this.messages.warnings.push(`Meta description is too long. It is ${this.content.metaDescription.length} characters long, try reducing it.`); | ||
} | ||
else if (this.content.metaDescription.length < 100) { | ||
this.messages.warnings.push(`Meta description is too short. It is ${this.content.metaDescription.length} characters long, try increasing it.`); | ||
} | ||
else { | ||
this.messages.goodPoints.push(`Meta description is ${this.content.metaDescription.length} characters long.`); | ||
// warning for meta description keyword density | ||
if (keywordInMetaDescription.density > this.MAXIMUM_META_DESCRIPTION_DENSITY) { | ||
this.messages.warnings.push(`Keyword density of meta description is too high. It is ${keywordInMetaDescription.density.toFixed(2)}%, try decreasing it.`); | ||
} | ||
else if (keywordInMetaDescription.density < this.MINIMUM_META_DESCRIPTION_DENSITY) { | ||
this.messages.warnings.push(`Keyword density of meta description is too low. It is ${keywordInMetaDescription.density.toFixed(2)}%, try increasing it.`); | ||
} | ||
else { | ||
this.messages.goodPoints.push(`Keyword density of meta description is ${keywordInMetaDescription.density.toFixed(2)}%, which is good.`); | ||
} | ||
} | ||
// warning for meta description not starting with keyword | ||
if (keywordInMetaDescription.position > 1) { | ||
this.messages.minorWarnings.push(`Meta description does not start with keyword. It starts with "${this.content.metaDescription.substring(0, 20)}", try starting with keyword. Not starting with keyword is not a big issue, but it is recommended to start with keyword.`); | ||
} | ||
else { | ||
this.messages.goodPoints.push(`Meta description starts with keyword, i.e. "${this.content.metaDescription.substring(0, 20)}".`); | ||
} | ||
// warning for meta description not ending with keyword | ||
let subKeywordsInMetaDescription = this.getSubKeywordsInMetaDescription(); | ||
subKeywordsInMetaDescription.forEach((subKeyword) => { | ||
if (subKeyword.density > this.MAXIMUM_SUB_KEYWORD_IN_META_DESCRIPTION_DENSITY) { | ||
this.messages.warnings.push(`The density of sub keyword "${subKeyword.keyword}" in meta description is too high, i.e. ${subKeyword.density.toFixed(2)}%.`); | ||
} | ||
else if (subKeyword.density < this.MINIMUM_SUB_KEYWORD_IN_META_DESCRIPTION_DENSITY) { | ||
let densityBeingLowString = subKeyword.density < 0.2 ? 'too low' : 'low'; | ||
this.messages.warnings.push(`The density of sub keyword "${subKeyword.keyword}" in meta description is ${densityBeingLowString}, i.e. ${subKeyword.density.toFixed(2)}%.`); | ||
} | ||
else { | ||
this.messages.goodPoints.push(`The density of sub keyword "${subKeyword.keyword}" in meta description is ${subKeyword.density.toFixed(2)}%.`); | ||
} | ||
}); | ||
} | ||
const density = this.calculateDensity(keyword, this.content.title); | ||
return { | ||
keyword, | ||
density, | ||
position: (_a = this.content.title) === null || _a === void 0 ? void 0 : _a.indexOf(keyword) | ||
}; | ||
else { | ||
this.messages.warnings.push('Missing meta description.'); | ||
} | ||
} | ||
getSubKeywordsInTitle() { | ||
let subKeywordsInQuestion = []; | ||
this.content.subKeywords.forEach((sub_keyword) => { | ||
subKeywordsInQuestion.push(this.getKeywordInTitle(sub_keyword)); | ||
}); | ||
return subKeywordsInQuestion; | ||
/** | ||
* Returns the messages object. | ||
* @return object The messages object. | ||
* @example | ||
* { | ||
* goodPoints: [], | ||
* warnings: [], | ||
* minorWarnings: [], | ||
* } | ||
* @see SeoAnalyzer.messages | ||
*/ | ||
assignMessages() { | ||
this.assignMessagesForKeyword(); | ||
this.assignMessagesForSubKeywords(); | ||
this.assignMessagesForTitle(); | ||
this.assignMessagesForLinks(); | ||
this.assignMessagesForMetaDescription(); | ||
return this.messages; | ||
} | ||
countOccurrencesInString(keyword, stringContent) { | ||
return stringContent.split(keyword).length - 1; | ||
/** | ||
* Calculates the density of a keyword in the given string of body text. | ||
* @param keyword Should not be null. | ||
* @param bodyText If null, it will use the default value, i.e. `this.htmlAnalyzer.bodyText` | ||
*/ | ||
calculateDensity(keyword, bodyText = null) { | ||
bodyText = bodyText !== null && bodyText !== void 0 ? bodyText : this.htmlAnalyzer.bodyText; | ||
return (this.countOccurrencesInString(keyword, bodyText) / this.htmlAnalyzer.getWordCount(bodyText)) * 100; | ||
} | ||
getSeoScore() { | ||
const MAX_SCORE = 100; | ||
const { warnings, goodPoints } = this.getMessages(); | ||
const messagesScore = ((goodPoints.length) / (warnings.length + goodPoints.length)) * 100; | ||
return Math.min(messagesScore, MAX_SCORE); // SEO score should never go above 100 | ||
/** | ||
* Returns the number of occurrences of a keyword in a string. Or you can say, it returns the keyword count in the given string. | ||
* @param keyword If null, it will use the default value, i.e. `this.content.keyword` | ||
* @param stringContent If null, it will use the default value, i.e. `this.htmlAnalyzer.bodyText` | ||
* @return number The number of occurrences of the keyword in the string. | ||
*/ | ||
countOccurrencesInString(keyword = null, stringContent = null) { | ||
keyword = keyword !== null && keyword !== void 0 ? keyword : this.content.keyword; | ||
stringContent = stringContent !== null && stringContent !== void 0 ? stringContent : this.htmlAnalyzer.bodyText; | ||
return stringContent.split(keyword).length - 1; // -1 because the split function will always return one more than the actual occurrences | ||
} | ||
getKeywordSeoScore() { | ||
const MAX_SCORE = 100; | ||
const keywordDensity = this.getKeywordDensity(); | ||
const keywordInQuestion = this.getKeywordInTitle(); | ||
const subKeywordsInQuestion = this.getSubKeywordsInTitle(); | ||
const subKeywordsDensity = this.getSubKeywordsDensity(); | ||
const keywordInQuestionScore = keywordInQuestion.density * 10; | ||
const subKeywordsInQuestionScore = subKeywordsInQuestion.length * 10; | ||
const subKeywordsDensityScore = subKeywordsDensity.reduce((total, subKeywordDensity) => { | ||
return total + (subKeywordDensity.density * 10); | ||
}, 0); | ||
const keywordDensityScore = keywordDensity * 10; | ||
const totalScore = keywordInQuestionScore + subKeywordsInQuestionScore + subKeywordsDensityScore + keywordDensityScore; | ||
return Math.min(totalScore, MAX_SCORE); // SEO score should never go above 100 | ||
} | ||
getTitleWordCount() { | ||
return this.htmlAnalyzer.getWordCount(this.content.title); | ||
} | ||
} | ||
exports.SeoAnalyzer = SeoAnalyzer; | ||
//# sourceMappingURL=seo-analyzer.js.map |
@@ -10,7 +10,11 @@ import { SeoAnalyzer } from './seo-analyzer'; | ||
constructor(contentJson: ContentJson, siteDomainName?: string | null); | ||
private makeContentLowerCase; | ||
analyzeSeo(): { | ||
seoScore: number; | ||
wordCount: number; | ||
keywordSeoScore: number; | ||
keywordFrequency: number; | ||
messages: { | ||
warnings: string[]; | ||
minorWarnings: string[]; | ||
goodPoints: string[]; | ||
@@ -26,4 +30,5 @@ }; | ||
keywordWithTitle: import("./interfaces").KeywordDensity; | ||
wordCount: number; | ||
}; | ||
}; | ||
} |
@@ -9,2 +9,3 @@ "use strict"; | ||
this.content = contentJson; | ||
this.makeContentLowerCase(); | ||
this.siteDomainName = siteDomainName; | ||
@@ -14,8 +15,18 @@ this.htmlAnalyzer = new html_analyzer_1.HtmlAnalyzer(this.content.htmlText, this.siteDomainName); | ||
} | ||
makeContentLowerCase() { | ||
this.content.title = this.content.title.toLowerCase(); | ||
this.content.metaDescription = this.content.metaDescription.toLowerCase(); | ||
this.content.keyword = this.content.keyword.toLowerCase(); | ||
this.content.subKeywords = this.content.subKeywords.map((subKeyword) => { | ||
return subKeyword.toLowerCase(); | ||
}); | ||
} | ||
analyzeSeo() { | ||
return { | ||
seoScore: this.seoAnalyzer.getSeoScore(), | ||
wordCount: this.htmlAnalyzer.getWordCount(), | ||
keywordSeoScore: this.seoAnalyzer.getKeywordSeoScore(), | ||
messages: this.seoAnalyzer.getMessages(), | ||
keywordDensity: this.seoAnalyzer.getKeywordDensity(), | ||
keywordFrequency: this.seoAnalyzer.countOccurrencesInString(), | ||
messages: this.seoAnalyzer.messages, | ||
keywordDensity: this.seoAnalyzer.keywordDensity, | ||
subKeywordDensity: this.seoAnalyzer.getSubKeywordsDensity(), | ||
@@ -28,2 +39,3 @@ totalLinks: this.htmlAnalyzer.getAllLinks().length, | ||
keywordWithTitle: this.seoAnalyzer.getKeywordInTitle(), | ||
wordCount: this.seoAnalyzer.getTitleWordCount(), | ||
} | ||
@@ -30,0 +42,0 @@ }; |
{ | ||
"name": "seord", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Advanced SEO Analyzer", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
147
README.md
# SEO Analyzer - SEOrd | ||
SEOrd `(pronounced "sword")` is an advanced SEO Analyzer library that allows you to perform an SEO analysis | ||
on HTML content. SEOrd helps check the SEO friendliness of a website by looking at different factors such | ||
SEOrd `(pronounced "sword")` is an advanced content SEO Analyzer library that allows you to perform a rapid SEO analysis | ||
on your rich text-ed content. SEOrd helps check the SEO friendliness of a website by looking at different factors such | ||
as keyword density, meta description, and link analysis `(internal and external link)`. | ||
@@ -10,2 +10,6 @@ | ||
> **Note:** This library analyzes the SEO of the given content on the basis of the standard on-page SEO rules. | ||
> This library is just a tool to help you improve your SEO score or use it as a reference or a supplement to | ||
> your webapps. | ||
## Install | ||
@@ -19,3 +23,3 @@ | ||
- Keyword Density Analysis | ||
- Keyword Density and Frequency Analysis | ||
- Sub Keywords Density Analysis | ||
@@ -27,3 +31,5 @@ - SEO Messages: Get warnings and good points related to SEO. | ||
- SEO Score Analysis | ||
- Key SEO Score Analysis | ||
- Keyword SEO Score | ||
- Word Count Analysis | ||
- Checks for meta description length, placement, keyword density, and keyword frequency | ||
@@ -43,41 +49,45 @@ ## Usage | ||
import {SeoCheck} from "seord"; | ||
import {readFile} from 'fs'; // Only for this example | ||
const htmlContent = `<h1>Does Progressive Raise Your Rates After 6 Months?</h1><p>When it comes to car insurance, | ||
finding the right provider... | ||
` | ||
const contentJson = { | ||
title: 'Does Progressive raise your rates after 6 months?', | ||
htmlText: htmlContent, | ||
keyword: 'progressive', | ||
subKeywords: ['car insurance', 'rates', 'premiums', 'save money', 'US'], | ||
metaDescription: 'Find out if Progressive raises your rates after 6 months and what factors can impact your insurance premiums. Learn how to save money on car insurance in the US.', | ||
languageCode: 'en', | ||
countryCode: 'us' | ||
}; | ||
readFile('test.html', 'utf8' , (err, htmlContent) => { | ||
if (err) { | ||
console.error(err) | ||
return | ||
} | ||
// Initialize SeoCheck with html content, main keyword and sub keywords | ||
const seoCheck = new SeoCheck(contentJson, 'liveinabroad.com'); | ||
// Initialize SeoCheck with html content, main keyword and sub keywords | ||
const seoCheck = new SeoCheck( | ||
{ | ||
question: 'What\'s the best insurance cover to get?', | ||
htmlText: htmlContent, | ||
keyword: 'best insurance cover', | ||
subKeywords: ['types of insurance', 'insurance coverage', 'insurance options'], | ||
metaDescription: 'Discover the best insurance cover to protect yourself and your loved ones. Explore different types of insurance and find the right coverage for your needs.', | ||
languageCode: 'en', | ||
countryCode: 'us' | ||
}, | ||
'liveinabroad.com' | ||
); | ||
// Perform analysis | ||
const result = seoCheck.analyzeSeo(); | ||
// Print the result | ||
console.log(`Warnings: ${result.messages.warnings.length}`); | ||
result.messages.warnings.forEach((warning) => { | ||
console.log(` - ${warning}`); | ||
}); | ||
console.log(`\nGood Points: ${result.messages.goodPoints.length}`); | ||
result.messages.goodPoints.forEach((error) => { | ||
console.log(` - ${error}`); | ||
}); | ||
// Perform analysis | ||
const result = seoCheck.analyzeSeo(); | ||
// Print the result | ||
console.log("Warnings: " + result.messages.warnings.length); | ||
result.messages.warnings.forEach((warning) => { | ||
console.log(' - ' + warning); | ||
}); | ||
console.log(`\nMinor Warnings: ${result.messages.minorWarnings.length}`); | ||
result.messages.minorWarnings.forEach((error) => { | ||
console.log(` - ${error}`); | ||
}); | ||
console.log("\nGood Points: " + result.messages.goodPoints.length); | ||
result.messages.goodPoints.forEach((error) => { | ||
console.log(' - ' + error); | ||
}); | ||
console.log("\nSEO Score: " + result.seoScore); | ||
console.log("Keyword SEO Score: " + result.keywordSeoScore); | ||
}) | ||
console.log("\nSEO Score: " + result.seoScore); | ||
console.log(`Keyword SEO Score: ${result.keywordSeoScore}`); | ||
console.log(`Keyword Density: ${result.keywordDensity}`); | ||
console.log(`Sub Keyword Density: ${result.subKeywordDensity.map((subKeywordDensity) => { | ||
return `(${subKeywordDensity.keyword} ${subKeywordDensity.density})`; | ||
})}`); | ||
console.log(`Keyword Frequency: ${result.keywordFrequency}`); | ||
console.log(`Word Count: ${result.wordCount}`); | ||
console.log(`Total Links: ${result.totalLinks}`); | ||
``` | ||
@@ -91,20 +101,37 @@ | ||
```text | ||
Warnings: 3 | ||
- The density of sub keyword "insurance options" is too low, i.e. 0.08%. | ||
- Not enough internal links. You only have 0 unique internal links, try increasing it. | ||
Warnings: 7 | ||
- The density of sub keyword "car insurance" is too high in the content, i.e. 2.34%. | ||
- The density of sub keyword "rates" is too high in the content, i.e. 4.28%. | ||
- The density of sub keyword "premiums" is too high in the content, i.e. 1.38%. | ||
- The density of sub keyword "us" is too high in the content, i.e. 1.38%. | ||
- Not enough internal links. You only have 1 unique internal links, try increasing it. | ||
- Not enough outbound links. You only have 0, try increasing it. | ||
- Meta description is too long. It is 161 characters long, try reducing it. | ||
Good Points: 9 | ||
- Your main keyword is "best insurance cover". | ||
- Your sub keywords are "types of insurance", "insurance coverage", "insurance options". | ||
- Keyword density is 0.62%. | ||
- The density of sub keyword "types of insurance" is 0.47%. | ||
- The density of sub keyword "insurance coverage" is 0.62%. | ||
- You have your main keyword in question. | ||
- You have 3 sub keywords in question. | ||
Good Points: 14 | ||
- Good, your content has a keyword "progressive". | ||
- Keyword density is 1.52%. | ||
- Good, your content has sub keywords "car insurance, rates, premiums, save money, us". | ||
- The density of sub keyword "save money" is 0.55% in the content, which is good. | ||
- Title tag is 49 characters long. | ||
- Keyword density in title is 12.50%, which is good. | ||
- You have 5 sub keywords in title. | ||
- No duplicate internal links. | ||
- No duplicate outbound links. | ||
- The density of sub keyword "car insurance" in meta description is 3.45%. | ||
- The density of sub keyword "rates" in meta description is 3.45%. | ||
- The density of sub keyword "premiums" in meta description is 3.45%. | ||
- The density of sub keyword "save money" in meta description is 3.45%. | ||
- The density of sub keyword "us" in meta description is 3.45%. | ||
SEO Score: 75 | ||
Minor Warnings: 1 | ||
- Meta description does not start with keyword. It starts with "find out if progress", try starting with keyword. Not starting with keyword is not a big issue, but it is recommended to start with keyword. | ||
SEO Score: 66.66666666666666 | ||
Keyword SEO Score: 100 | ||
Keyword Density: 1.5172413793103448 | ||
Sub Keyword Density: (car insurance 2.344827586206897),(rates 4.275862068965517),(premiums 1.3793103448275863),(save money 0.5517241379310345),(us 1.3793103448275863) | ||
Keyword Frequency: 11 | ||
Word Count: 725 | ||
Total Links: 1 | ||
``` | ||
@@ -119,4 +146,6 @@ | ||
seoScore: number, | ||
wordCount: number, | ||
keywordSeoScore: number, | ||
messages: { warnings: string[], goodPoints: string[] }, | ||
keywordFrequency: number, | ||
messages: { warnings: string[], goodPoints: string[], minorWarnings: string[] }, | ||
keywordDensity: number, | ||
@@ -127,5 +156,6 @@ subKeywordDensity: KeywordDensity[], | ||
outboundLinks: LinksGroup, | ||
questionSEO: { | ||
subKeywordsWithQuestion: KeywordDensity[], | ||
keywordWithQuestion: KeywordDensity | ||
titleSEO: { | ||
subKeywordsWithTitle: KeywordDensity[], | ||
keywordWithTitle: KeywordDensity, | ||
wordCount: number | ||
} | ||
@@ -146,2 +176,9 @@ } | ||
This project is licensed under the MIT license. Please see the [LICENSE](LICENSE) file for more information. | ||
This project is licensed under the MIT license. Please see the [LICENSE](LICENSE) file for more information. | ||
> **Note:** This library is still in development. I will be adding more features in the future. | ||
> If you have any suggestions, please let me know. | ||
Please note that this library is not affiliated with Google or any other search engine. | ||
> It does not guarantee the SEO score calculated by this library will be the same as the SEO score | ||
> calculated by Google or any other search engine. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
51352
614
177