diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index bd58e0fd..e09ea624 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -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", diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index 9f96db7a..bb592b8d 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -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 }; } diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 4845ee59..d7ba0350 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -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% */ diff --git a/src/panelWebView/components/ArticleDetails.tsx b/src/panelWebView/components/ArticleDetails.tsx index 747d100a..8fd7ddb2 100644 --- a/src/panelWebView/components/ArticleDetails.tsx +++ b/src/panelWebView/components/ArticleDetails.tsx @@ -11,6 +11,7 @@ export interface IArticleDetailsProps { internalLinks: number; externalLinks: number; images: number; + firstParagraph?: string; }; } diff --git a/src/panelWebView/components/SeoKeywordInfo.tsx b/src/panelWebView/components/SeoKeywordInfo.tsx index bb3b78d1..094fc442 100644 --- a/src/panelWebView/components/SeoKeywordInfo.tsx +++ b/src/panelWebView/components/SeoKeywordInfo.tsx @@ -16,6 +16,7 @@ export interface ISeoKeywordInfoProps { content: string; wordCount?: number; headings?: string[]; + firstParagraph?: string; } const SeoKeywordInfo: React.FunctionComponent = ({ @@ -26,7 +27,8 @@ const SeoKeywordInfo: React.FunctionComponent = ({ slug, content, wordCount, - headings + headings, + firstParagraph }: React.PropsWithChildren) => { const density = () => { @@ -90,9 +92,10 @@ const SeoKeywordInfo: React.FunctionComponent = ({ (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 = ({ {localize(LocalizationKey.commonDescription)}
{localize(LocalizationKey.commonSlug)}
{localize(LocalizationKey.panelSeoKeywordInfoValidInfoContent)}
- {localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)} + {localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)}
+ {localize(LocalizationKey.panelSeoKeywordInfoValidInfoFirstParagraph)} ) }, [checks]); diff --git a/src/panelWebView/components/SeoKeywords.tsx b/src/panelWebView/components/SeoKeywords.tsx index 15fb3ad7..fc459ab6 100644 --- a/src/panelWebView/components/SeoKeywords.tsx +++ b/src/panelWebView/components/SeoKeywords.tsx @@ -14,6 +14,7 @@ export interface ISeoKeywordsProps { content: string; headings?: string[]; wordCount?: number; + firstParagraph?: string; } const SeoKeywords: React.FunctionComponent = ({ diff --git a/src/panelWebView/components/SeoStatus.tsx b/src/panelWebView/components/SeoStatus.tsx index af28b788..aeb3cf27 100644 --- a/src/panelWebView/components/SeoStatus.tsx +++ b/src/panelWebView/components/SeoStatus.tsx @@ -96,6 +96,7 @@ const SeoStatus: React.FunctionComponent = ({ headings={metadata?.articleDetails?.headingsText} wordCount={metadata?.articleDetails?.wordCount} content={metadata?.articleDetails?.content} + firstParagraph={metadata?.articleDetails?.firstParagraph} />