Implement first paragraph keyword check for SEO validation

Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-09-08 20:07:41 +00:00
parent d11dbc9d76
commit beef6f36d8
7 changed files with 35 additions and 6 deletions

View File

@@ -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",

View File

@@ -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
};
}

View File

@@ -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%
*/

View File

@@ -11,6 +11,7 @@ export interface IArticleDetailsProps {
internalLinks: number;
externalLinks: number;
images: number;
firstParagraph?: string;
};
}

View File

@@ -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]);

View File

@@ -14,6 +14,7 @@ export interface ISeoKeywordsProps {
content: string;
headings?: string[];
wordCount?: number;
firstParagraph?: string;
}
const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({

View File

@@ -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`}>