#705 - further improve keywords section

This commit is contained in:
Elio Struyf
2024-11-25 11:59:22 +01:00
parent e10ee11f0e
commit a7f183b6cc
8 changed files with 109 additions and 145 deletions

View File

@@ -10,12 +10,6 @@
"values": ["#build", "#deploy", "#skip"] "values": ["#build", "#deploy", "#skip"]
} }
], ],
"workbench.colorCustomizations": {
"titleBar.activeBackground": "#15c2cb",
"titleBar.inactiveBackground": "#44ffd299",
"titleBar.activeForeground": "#0E131F",
"titleBar.inactiveForeground": "#0E131F99"
},
"files.exclude": { "files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files "out": false // set this to true to hide the "out" folder with the compiled JS files
}, },

View File

@@ -496,6 +496,8 @@
"panel.seoDetails.recommended": "Recommended", "panel.seoDetails.recommended": "Recommended",
"panel.seoKeywords.checks": "Checks",
"panel.seoKeywords.density.tableTitle": "Freq.",
"panel.seoKeywords.density": "Keyword density", "panel.seoKeywords.density": "Keyword density",
"panel.seoKeywordInfo.validInfo.label": "Used in heading(s)", "panel.seoKeywordInfo.validInfo.label": "Used in heading(s)",
"panel.seoKeywordInfo.validInfo.content": "Content", "panel.seoKeywordInfo.validInfo.content": "Content",

View File

@@ -1600,6 +1600,14 @@ export enum LocalizationKey {
* Recommended * Recommended
*/ */
panelSeoDetailsRecommended = 'panel.seoDetails.recommended', panelSeoDetailsRecommended = 'panel.seoDetails.recommended',
/**
* Checks
*/
panelSeoKeywordsChecks = 'panel.seoKeywords.checks',
/**
* Freq.
*/
panelSeoKeywordsDensityTableTitle = 'panel.seoKeywords.density.tableTitle',
/** /**
* Keyword density * Keyword density
*/ */

View File

@@ -5,6 +5,7 @@ import { Tag } from './Tag';
import { LocalizationKey, localize } from '../../localization'; import { LocalizationKey, localize } from '../../localization';
import { Messenger } from '@estruyf/vscode/dist/client'; import { Messenger } from '@estruyf/vscode/dist/client';
import { CommandToCode } from '../CommandToCode'; import { CommandToCode } from '../CommandToCode';
import { Tooltip } from 'react-tooltip'
export interface ISeoKeywordInfoProps { export interface ISeoKeywordInfoProps {
keywords: string[]; keywords: string[];
@@ -27,6 +28,9 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
wordCount, wordCount,
headings headings
}: React.PropsWithChildren<ISeoKeywordInfoProps>) => { }: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
const tooltipClasses = `!py-[2px] !px-[8px] !rounded-[3px] !border-[var(--vscode-editorHoverWidget-border)] !border !border-solid !bg-[var(--vscode-editorHoverWidget-background)] !text-[var(--vscode-editorHoverWidget-foreground)] !font-normal !opacity-100 shadow-[0_2px_8px_var(--vscode-widget-shadow)] !z-[9999]`;
const density = () => { const density = () => {
if (!wordCount) { if (!wordCount) {
return null; return null;
@@ -35,7 +39,7 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
const pattern = new RegExp(`(^${keyword.toLowerCase()}(?=\\s|$))|(\\s${keyword.toLowerCase()}(?=\\s|$))`, 'ig'); const pattern = new RegExp(`(^${keyword.toLowerCase()}(?=\\s|$))|(\\s${keyword.toLowerCase()}(?=\\s|$))`, 'ig');
const count = (content.match(pattern) || []).length; const count = (content.match(pattern) || []).length;
const density = (count / wordCount) * 100; const density = (count / wordCount) * 100;
const densityTitle = `${density.toFixed(2)}% *`; const densityTitle = `${density.toFixed(2)}* %`;
if (density < 0.75) { if (density < 0.75) {
return <ValidInfo label={densityTitle} isValid={false} className='text-xs' />; return <ValidInfo label={densityTitle} isValid={false} className='text-xs' />;
@@ -67,7 +71,7 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
} }
const exists = headings.filter((heading) => validateKeywords(heading, keyword)); const exists = headings.filter((heading) => validateKeywords(heading, keyword));
return <ValidInfo isValid={exists.length > 0} />; return exists.length > 0;
}; };
const onRemove = React.useCallback((tag: string) => { const onRemove = React.useCallback((tag: string) => {
@@ -78,6 +82,50 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
}); });
}, [keywords]); }, [keywords]);
const checks = React.useMemo(() => {
return {
title: !!title && title.toLowerCase().includes(keyword.toLowerCase()),
description: !!description && description.toLowerCase().includes(keyword.toLowerCase()),
slug:
!!slug &&
(slug.toLowerCase().includes(keyword.toLowerCase()) ||
slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase())),
content: !!content && content.toLowerCase().includes(keyword.toLowerCase()),
heading: checkHeadings()
};
}, [title, description, slug, content, headings, wordCount]);
const tooltipContent = React.useMemo(() => {
return (
<>
<span className='inline-flex items-center gap-1'>{localize(LocalizationKey.commonTitle)}: <ValidInfo isValid={checks.title} /></span><br />
<span className='inline-flex items-center gap-1'>{localize(LocalizationKey.commonDescription)}: <ValidInfo isValid={checks.description} /></span><br />
<span className='inline-flex items-center gap-1'>{localize(LocalizationKey.commonSlug)}: <ValidInfo isValid={checks.slug} /></span><br />
<span className='inline-flex items-center gap-1'>{localize(LocalizationKey.panelSeoKeywordInfoValidInfoContent)}: <ValidInfo isValid={checks.content} /></span><br />
<span className='inline-flex items-center gap-1'>{localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)}: <ValidInfo isValid={!!checks.heading} /></span>
</>
)
}, [checks]);
const checksMarkup = React.useMemo(() => {
const validData = Object.values(checks).filter((check) => check).length;
const totalChecks = Object.values(checks).length;
const isValid = validData === totalChecks;
return (
<div
className={`inline-flex py-1 px-[4px] rounded-[3px] justify-center items-center text-[12px] leading-[16px] border border-solid ${isValid ? "text-[#1f883d] border-[#1f883d]" : "text-[var(--vscode-statusBarItem-warningBackground)] border-[var(--vscode-statusBarItem-warningBackground)]"}`}
data-tooltip-id={`tooltip-checks-${keyword}`}
>
<ValidInfo isValid={isValid} />
<span className='mr-[1px]'>{validData}</span>
<span>/</span>
<span className='ml-[1px]'>{totalChecks}</span>
</div>
);
}, [checks]);
if (!keyword || typeof keyword !== 'string') { if (!keyword || typeof keyword !== 'string') {
return null; return null;
} }
@@ -88,7 +136,7 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
<div className='flex h-full items-center'> <div className='flex h-full items-center'>
<Tag <Tag
value={keyword} value={keyword}
className={`!mx-0 !my-1 !px-2 !py-1`} className={`!w-full !justify-between !mx-0 !my-1 !px-2 !py-1`}
onRemove={onRemove} onRemove={onRemove}
onCreate={() => void 0} onCreate={() => void 0}
title={localize(LocalizationKey.panelTagsTagWarning, keyword)} title={localize(LocalizationKey.panelTagsTagWarning, keyword)}
@@ -96,32 +144,17 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
/> />
</div> </div>
</VSCodeTableCell> </VSCodeTableCell>
<VSCodeTableCell className={`text-center`}> <VSCodeTableCell>
<ValidInfo {checksMarkup}
isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())}
/> <Tooltip
</VSCodeTableCell> id={`tooltip-checks-${keyword}`}
<VSCodeTableCell className={`text-center`}> className={tooltipClasses}
<ValidInfo style={{
isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())} fontSize: '12px',
/> lineHeight: '19px'
</VSCodeTableCell> }}
<VSCodeTableCell className={`text-center`}> render={() => tooltipContent} />
<ValidInfo
isValid={
!!slug &&
(slug.toLowerCase().includes(keyword.toLowerCase()) ||
slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase()))
}
/>
</VSCodeTableCell>
<VSCodeTableCell className={`text-center`}>
<ValidInfo
isValid={!!content && content.toLowerCase().includes(keyword.toLowerCase())}
/>
</VSCodeTableCell>
<VSCodeTableCell className={`text-center`}>
{checkHeadings()}
</VSCodeTableCell> </VSCodeTableCell>
<VSCodeTableCell className={`text-center`}> <VSCodeTableCell className={`text-center`}>
{density()} {density()}

View File

@@ -1,10 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { SeoKeywordInfo } from './SeoKeywordInfo'; import { SeoKeywordInfo } from './SeoKeywordInfo';
import { ErrorBoundary } from '@sentry/react'; import { ErrorBoundary } from '@sentry/react';
import { Tooltip } from 'react-tooltip'
import { LocalizationKey, localize } from '../../localization'; import { LocalizationKey, localize } from '../../localization';
import { VSCodeTable, VSCodeTableBody, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable'; import { VSCodeTable, VSCodeTableBody, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
import { Icon } from 'vscrui'; import { Tooltip } from 'react-tooltip'
export interface ISeoKeywordsProps { export interface ISeoKeywordsProps {
keywords: string[] | null; keywords: string[] | null;
@@ -23,7 +22,7 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
}: React.PropsWithChildren<ISeoKeywordsProps>) => { }: React.PropsWithChildren<ISeoKeywordsProps>) => {
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
const tooltipClasses = `!py-[2px] !px-[8px] !rounded-[3px] !border-[var(--vscode-editorHoverWidget-border)] !border !border-solid !bg-[var(--vscode-editorHoverWidget-background)] !text-[var(--vscode-editorHoverWidget-foreground)] !font-normal !opacity-100`; const tooltipClasses = `!py-[2px] !px-[8px] !rounded-[3px] !border-[var(--vscode-editorHoverWidget-border)] !border !border-solid !bg-[var(--vscode-editorHoverWidget-background)] !text-[var(--vscode-editorHoverWidget-foreground)] !font-normal !opacity-100 shadow-[0_2px_8px_var(--vscode-widget-shadow)]`;
const validateKeywords = () => { const validateKeywords = () => {
if (!keywords) { if (!keywords) {
@@ -59,98 +58,25 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
return ( return (
<section className={`seo__keywords__table`}> <section className={`seo__keywords__table`}>
<VSCodeTable> <VSCodeTable disableOverflow>
<VSCodeTableHeader> <VSCodeTableHeader>
<VSCodeTableRow className={`border-t border-t-[var(--vscode-editorGroup-border)]`}> <VSCodeTableRow className={`border-t border-t-[var(--vscode-editorGroup-border)]`}>
<VSCodeTableHead> <VSCodeTableHead>
{localize(LocalizationKey.panelSeoKeywordsHeaderKeyword)} {localize(LocalizationKey.panelSeoKeywordsHeaderKeyword)}
</VSCodeTableHead> </VSCodeTableHead>
<VSCodeTableHead className='text-center'> <VSCodeTableHead>
<div {localize(LocalizationKey.panelSeoKeywordsChecks)}
className='flex items-center justify-center h-full'
>
<Icon
className='!text-[var(--vscode-foreground)]'
name='quote'
data-tooltip-id="tooltip-title"
data-tooltip-content={localize(LocalizationKey.commonTitle)} />
<Tooltip id="tooltip-title" className={tooltipClasses} style={{
fontSize: '12px',
lineHeight: '19px'
}} />
</div>
</VSCodeTableHead>
<VSCodeTableHead className='text-center'>
<div
className='flex items-center justify-center h-full'
>
<Icon
className='!text-[var(--vscode-foreground)]'
name='note'
data-tooltip-id="tooltip-description"
data-tooltip-content={localize(LocalizationKey.commonDescription)} />
<Tooltip id="tooltip-description" className={tooltipClasses} style={{
fontSize: '12px',
lineHeight: '19px'
}} />
</div>
</VSCodeTableHead>
<VSCodeTableHead className='text-center'>
<div
className='flex items-center justify-center h-full'
>
<Icon
className='!text-[var(--vscode-foreground)]'
name='link'
data-tooltip-id="tooltip-slug"
data-tooltip-content={localize(LocalizationKey.commonSlug)} />
<Tooltip id="tooltip-slug" className={tooltipClasses} style={{
fontSize: '12px',
lineHeight: '19px'
}} />
</div>
</VSCodeTableHead>
<VSCodeTableHead className='text-center'>
<div
className='flex items-center justify-center h-full'
>
<Icon
className='!text-[var(--vscode-foreground)]'
name='book'
data-tooltip-id="tooltip-content"
data-tooltip-content={localize(LocalizationKey.panelSeoKeywordInfoValidInfoContent)} />
<Tooltip id="tooltip-content" className={tooltipClasses} style={{
fontSize: '12px',
lineHeight: '19px'
}} />
</div>
</VSCodeTableHead> </VSCodeTableHead>
<VSCodeTableHead className='text-center'> <VSCodeTableHead className='text-center'>
<div <div
className='flex items-center justify-center h-full' className='flex items-center justify-center h-full'
> >
<span <span
className='text-[var(--vscode-foreground)] cursor-default select-none'
data-tooltip-id="tooltip-heading"
data-tooltip-content={localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)}
>
H1
</span>
<Tooltip id="tooltip-heading" className={tooltipClasses} style={{
fontSize: '12px',
lineHeight: '19px'
}} />
</div>
</VSCodeTableHead>
<VSCodeTableHead className='text-center'>
<div
className='flex items-center justify-center h-full'
>
<Icon
className='!text-[var(--vscode-foreground)]'
name='percentage'
data-tooltip-id="tooltip-density" data-tooltip-id="tooltip-density"
data-tooltip-content={localize(LocalizationKey.panelSeoKeywordsDensity)} /> data-tooltip-content={localize(LocalizationKey.panelSeoKeywordsDensity)}>
{localize(LocalizationKey.panelSeoKeywordsDensityTableTitle)}
</span>
<Tooltip id="tooltip-density" className={tooltipClasses} style={{ <Tooltip id="tooltip-density" className={tooltipClasses} style={{
fontSize: '12px', fontSize: '12px',
lineHeight: '19px' lineHeight: '19px'
@@ -163,8 +89,8 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
<VSCodeTableBody> <VSCodeTableBody>
{validKeywords.map((keyword, index) => { {validKeywords.map((keyword, index) => {
return ( return (
<ErrorBoundary key={keyword} fallback={<div />}> <ErrorBoundary key={`${keyword}-${index}`} fallback={<div />}>
<SeoKeywordInfo key={index} keywords={validKeywords} keyword={keyword} {...data} /> <SeoKeywordInfo keywords={validKeywords} keyword={keyword} {...data} />
</ErrorBoundary> </ErrorBoundary>
); );
})} })}
@@ -172,7 +98,7 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
</VSCodeTable> </VSCodeTable>
{data.wordCount && ( {data.wordCount && (
<div className={`text-xs mt-2`}> <div className={`text-xs my-2`}>
{localize(LocalizationKey.panelSeoKeywordsDensityDescription)} {localize(LocalizationKey.panelSeoKeywordsDensityDescription)}
</div> </div>
)} )}

View File

@@ -3,9 +3,9 @@ import { cn } from "../../../utils/cn"
const VSCodeTable = React.forwardRef< const VSCodeTable = React.forwardRef<
HTMLTableElement, HTMLTableElement,
React.HTMLAttributes<HTMLTableElement> React.HTMLAttributes<HTMLTableElement> & { disableOverflow?: boolean }
>(({ className, ...props }, ref) => ( >(({ className, disableOverflow, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className={`relative w-full ${disableOverflow ? "" : "overflow-auto"}`}>
<table <table
ref={ref} ref={ref}
className={cn("w-full text-base border-collapse indent-0 [&_tr:nth-child(2n)]:bg-[var(--vscode-keybindingTable-rowsBackground)]", className)} className={cn("w-full text-base border-collapse indent-0 [&_tr:nth-child(2n)]:bg-[var(--vscode-keybindingTable-rowsBackground)]", className)}

View File

@@ -15,7 +15,7 @@ const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({
return ( return (
<div className='inline-flex items-center h-full'> <div className='inline-flex items-center h-full'>
{isValid ? ( {isValid ? (
<CheckIcon className={`h-4 w-4 text-[#46ec86] mr-2`} /> <CheckIcon className={`h-4 w-4 text-[#1f883d] mr-2`} />
) : ( ) : (
<ExclamationTriangleIcon className={`h-4 w-4 text-[var(--vscode-statusBarItem-warningBackground)] mr-2`} /> <ExclamationTriangleIcon className={`h-4 w-4 text-[var(--vscode-statusBarItem-warningBackground)] mr-2`} />
)} )}

View File

@@ -866,9 +866,8 @@ vscode-divider {
padding: 0.25rem 0.25rem; padding: 0.25rem 0.25rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
margin-right: 0.5rem; margin-right: 0.5rem;
}
.tag button { button {
background: none; background: none;
border: none; border: none;
color: inherit; color: inherit;
@@ -878,17 +877,19 @@ vscode-divider {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 0.25rem; padding: 0.25rem;
width: auto;
} }
.tag .tag__create { .tag__create {
margin-right: 0.25rem; margin-right: 0.25rem;
}
.tag .tag__create:hover { &:hover {
color: var(--vscode-inputValidation-infoForeground, #000); color: var(--vscode-inputValidation-infoForeground, #000);
background-color: var(--vscode-inputValidation-infoBackground); background-color: var(--vscode-inputValidation-infoBackground);
border-radius: 2px; border-radius: 2px;
} }
}
}
.vscode-dark .tag .tag__create:hover { .vscode-dark .tag .tag__create:hover {
color: var(--vscode-inputValidation-infoForeground, #fff); color: var(--vscode-inputValidation-infoForeground, #fff);