forked from iarv/vscode-front-matter
Implement first paragraph keyword check for SEO validation
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
This commit is contained in:
@@ -504,6 +504,7 @@
|
||||
"panel.seoKeywords.density": "Keyword density",
|
||||
"panel.seoKeywordInfo.validInfo.label": "Heading(s)",
|
||||
"panel.seoKeywordInfo.validInfo.content": "Content",
|
||||
"panel.seoKeywordInfo.validInfo.firstParagraph": "First paragraph",
|
||||
"panel.seoKeywordInfo.density.tooltip": "Recommended frequency: 0.75% - 1.5%",
|
||||
|
||||
"panel.seoKeywords.title": "Keywords",
|
||||
|
||||
@@ -807,7 +807,8 @@ export class ArticleHelper {
|
||||
const elms: Parent[] | Link[] = this.getAllElms(mdTree);
|
||||
|
||||
const headings = elms.filter((node) => node.type === 'heading');
|
||||
const paragraphs = elms.filter((node) => node.type === 'paragraph').length;
|
||||
const paragraphNodes = elms.filter((node) => node.type === 'paragraph');
|
||||
const paragraphs = paragraphNodes.length;
|
||||
const images = elms.filter((node) => node.type === 'image').length;
|
||||
const links: string[] = elms
|
||||
.filter((node) => node.type === 'link')
|
||||
@@ -836,6 +837,21 @@ export class ArticleHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract first paragraph text for SEO keyword checking
|
||||
let firstParagraph = '';
|
||||
if (paragraphNodes.length > 0) {
|
||||
const firstParagraphNode = paragraphNodes[0];
|
||||
const extractTextFromNode = (node: any): string => {
|
||||
if (node.type === 'text') {
|
||||
return node.value || '';
|
||||
} else if (node.children && Array.isArray(node.children)) {
|
||||
return node.children.map(extractTextFromNode).join('');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
firstParagraph = extractTextFromNode(firstParagraphNode);
|
||||
}
|
||||
|
||||
const wordCount = this.wordCount(0, mdTree);
|
||||
|
||||
return {
|
||||
@@ -846,7 +862,8 @@ export class ArticleHelper {
|
||||
internalLinks,
|
||||
externalLinks: externalLinks.length,
|
||||
wordCount,
|
||||
content: article.content
|
||||
content: article.content,
|
||||
firstParagraph
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1632,6 +1632,10 @@ export enum LocalizationKey {
|
||||
* Content
|
||||
*/
|
||||
panelSeoKeywordInfoValidInfoContent = 'panel.seoKeywordInfo.validInfo.content',
|
||||
/**
|
||||
* First paragraph
|
||||
*/
|
||||
panelSeoKeywordInfoValidInfoFirstParagraph = 'panel.seoKeywordInfo.validInfo.firstParagraph',
|
||||
/**
|
||||
* Recommended frequency: 0.75% - 1.5%
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface IArticleDetailsProps {
|
||||
internalLinks: number;
|
||||
externalLinks: number;
|
||||
images: number;
|
||||
firstParagraph?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface ISeoKeywordInfoProps {
|
||||
content: string;
|
||||
wordCount?: number;
|
||||
headings?: string[];
|
||||
firstParagraph?: string;
|
||||
}
|
||||
|
||||
const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
|
||||
@@ -26,7 +27,8 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
|
||||
slug,
|
||||
content,
|
||||
wordCount,
|
||||
headings
|
||||
headings,
|
||||
firstParagraph
|
||||
}: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
|
||||
|
||||
const density = () => {
|
||||
@@ -90,9 +92,10 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
|
||||
(slug.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase())),
|
||||
content: !!content && content.toLowerCase().includes(keyword.toLowerCase()),
|
||||
heading: checkHeadings()
|
||||
heading: checkHeadings(),
|
||||
firstParagraph: !!firstParagraph && firstParagraph.toLowerCase().includes(keyword.toLowerCase())
|
||||
};
|
||||
}, [title, description, slug, content, headings, wordCount]);
|
||||
}, [title, description, slug, content, headings, wordCount, firstParagraph]);
|
||||
|
||||
const tooltipContent = React.useMemo(() => {
|
||||
return (
|
||||
@@ -102,7 +105,8 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
|
||||
<span className='inline-flex items-center gap-1'><ValidInfo isValid={checks.description} /> {localize(LocalizationKey.commonDescription)}</span><br />
|
||||
<span className='inline-flex items-center gap-1'><ValidInfo isValid={checks.slug} /> {localize(LocalizationKey.commonSlug)}</span><br />
|
||||
<span className='inline-flex items-center gap-1'><ValidInfo isValid={checks.content} /> {localize(LocalizationKey.panelSeoKeywordInfoValidInfoContent)}</span><br />
|
||||
<span className='inline-flex items-center gap-1'><ValidInfo isValid={!!checks.heading} /> {localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)}</span>
|
||||
<span className='inline-flex items-center gap-1'><ValidInfo isValid={!!checks.heading} /> {localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)}</span><br />
|
||||
<span className='inline-flex items-center gap-1'><ValidInfo isValid={checks.firstParagraph} /> {localize(LocalizationKey.panelSeoKeywordInfoValidInfoFirstParagraph)}</span>
|
||||
</>
|
||||
)
|
||||
}, [checks]);
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface ISeoKeywordsProps {
|
||||
content: string;
|
||||
headings?: string[];
|
||||
wordCount?: number;
|
||||
firstParagraph?: string;
|
||||
}
|
||||
|
||||
const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
|
||||
|
||||
@@ -96,6 +96,7 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
|
||||
headings={metadata?.articleDetails?.headingsText}
|
||||
wordCount={metadata?.articleDetails?.wordCount}
|
||||
content={metadata?.articleDetails?.content}
|
||||
firstParagraph={metadata?.articleDetails?.firstParagraph}
|
||||
/>
|
||||
|
||||
<FieldBoundary fieldName={`Keywords`}>
|
||||
|
||||
Reference in New Issue
Block a user