#705 - UX improvements for SEO panel

This commit is contained in:
Elio Struyf
2024-11-21 16:14:25 +01:00
parent cb649a9a97
commit 22ce41c3eb
16 changed files with 405 additions and 333 deletions
+33 -50
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeTable, VSCodeTableBody, VSCodeTableCell, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
import { VSCodeTableCell, VSCodeTableRow } from './VSCode/VSCodeTable';
export interface IArticleDetailsProps {
details: {
@@ -22,59 +22,42 @@ const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({
}
return (
<div className={`seo__status__details valid`}>
<h4>{l10n.t(LocalizationKey.panelArticleDetailsTitle)}</h4>
<>
{details?.headings !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsHeadings)}</VSCodeTableCell>
<VSCodeTableCell>{details.headings}</VSCodeTableCell>
</VSCodeTableRow>
)}
<VSCodeTable>
<VSCodeTableHeader>
<VSCodeTableRow>
<VSCodeTableHead>
{l10n.t(LocalizationKey.panelArticleDetailsType)}
</VSCodeTableHead>
<VSCodeTableHead>
{l10n.t(LocalizationKey.panelArticleDetailsTotal)}
</VSCodeTableHead>
</VSCodeTableRow>
</VSCodeTableHeader>
{details?.paragraphs !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsParagraphs)}</VSCodeTableCell>
<VSCodeTableCell>{details.paragraphs}</VSCodeTableCell>
</VSCodeTableRow>
)}
<VSCodeTableBody>
{details?.headings !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsHeadings)}</VSCodeTableCell>
<VSCodeTableCell>{details.headings}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.internalLinks !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsInternalLinks)}</VSCodeTableCell>
<VSCodeTableCell>{details.internalLinks}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.paragraphs !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsParagraphs)}</VSCodeTableCell>
<VSCodeTableCell>{details.paragraphs}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.externalLinks !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsExternalLinks)}</VSCodeTableCell>
<VSCodeTableCell>{details.externalLinks}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.internalLinks !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsInternalLinks)}</VSCodeTableCell>
<VSCodeTableCell>{details.internalLinks}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.externalLinks !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsExternalLinks)}</VSCodeTableCell>
<VSCodeTableCell>{details.externalLinks}</VSCodeTableCell>
</VSCodeTableRow>
)}
{details?.images !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsImages)}</VSCodeTableCell>
<VSCodeTableCell>{details.images}</VSCodeTableCell>
</VSCodeTableRow>
)}
</VSCodeTableBody>
</VSCodeTable>
</div>
{details?.images !== undefined && (
<VSCodeTableRow>
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsImages)}</VSCodeTableCell>
<VSCodeTableCell>{details.images}</VSCodeTableCell>
</VSCodeTableRow>
)}
</>
);
};
+5 -4
View File
@@ -7,19 +7,20 @@ export interface ISeoFieldInfoProps {
value: string;
recommendation: string;
isValid?: boolean;
className?: string;
}
const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({
title,
value,
recommendation,
isValid
isValid,
className
}: React.PropsWithChildren<ISeoFieldInfoProps>) => {
return (
<VSCodeTableRow>
<VSCodeTableRow className={className || ""}>
<VSCodeTableCell className={`capitalize`}>{title}</VSCodeTableCell>
<VSCodeTableCell>{value}/{recommendation}</VSCodeTableCell>
<VSCodeTableCell>{isValid !== undefined ? <ValidInfo label={undefined} isValid={isValid} /> : <span>-</span>}</VSCodeTableCell>
<VSCodeTableCell className='flex items-center text-nowrap'>{isValid !== undefined ? <ValidInfo label={undefined} isValid={isValid} /> : <span className='inline-block w-4 mr-2'>&mdash;</span>} {value}/{recommendation}</VSCodeTableCell>
</VSCodeTableRow>
);
};
+34 -40
View File
@@ -1,7 +1,5 @@
import * as React from 'react';
import { ValidInfo } from './ValidInfo';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { VSCodeTableCell, VSCodeTableRow } from './VSCode/VSCodeTable';
export interface ISeoKeywordInfoProps {
@@ -31,14 +29,14 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
const pattern = new RegExp(`(^${keyword.toLowerCase()}(?=\\s|$))|(\\s${keyword.toLowerCase()}(?=\\s|$))`, 'ig');
const count = (content.match(pattern) || []).length;
const density = (count / wordCount) * 100;
const densityTitle = l10n.t(LocalizationKey.panelSeoKeywordInfoDensity, `${density.toFixed(2)}%`);
const densityTitle = `${density.toFixed(2)}% *`;
if (density < 0.75) {
return <ValidInfo label={densityTitle} isValid={false} />;
return <ValidInfo label={densityTitle} isValid={false} className='text-xs' />;
} else if (density >= 0.75 && density < 1.5) {
return <ValidInfo label={densityTitle} isValid={true} />;
return <ValidInfo label={densityTitle} isValid={true} className='text-xs' />;
} else {
return <ValidInfo label={densityTitle} isValid={false} />;
return <ValidInfo label={densityTitle} isValid={false} className='text-xs' />;
}
};
@@ -63,7 +61,7 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
}
const exists = headings.filter((heading) => validateKeywords(heading, keyword));
return <ValidInfo label={l10n.t(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)} isValid={exists.length > 0} />;
return <ValidInfo isValid={exists.length > 0} />;
};
if (!keyword || typeof keyword !== 'string') {
@@ -73,39 +71,35 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
return (
<VSCodeTableRow>
<VSCodeTableCell>{keyword}</VSCodeTableCell>
<VSCodeTableCell className={` table__cell__validation`}>
<div className='flex items-center'>
<ValidInfo
label={l10n.t(LocalizationKey.commonTitle)}
isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())}
/>
</div>
<div className='flex items-center'>
<ValidInfo
label={l10n.t(LocalizationKey.commonDescription)}
isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())}
/>
</div>
<div className='flex items-center'>
<ValidInfo
label={l10n.t(LocalizationKey.commonSlug)}
isValid={
!!slug &&
(slug.toLowerCase().includes(keyword.toLowerCase()) ||
slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase()))
}
/>
</div>
<div className='flex items-center'>
<ValidInfo
label={l10n.t(LocalizationKey.panelSeoKeywordInfoValidInfoContent)}
isValid={!!content && content.toLowerCase().includes(keyword.toLowerCase())}
/>
</div>
{headings && headings.length > 0 &&
<div className='flex items-center'>{checkHeadings()}</div>}
{wordCount &&
<div className='flex items-center'>{density()}</div>}
<VSCodeTableCell className={`text-center`}>
<ValidInfo
isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())}
/>
</VSCodeTableCell>
<VSCodeTableCell className={`text-center`}>
<ValidInfo
isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())}
/>
</VSCodeTableCell>
<VSCodeTableCell className={`text-center`}>
<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 className={`text-center`}>
{density()}
</VSCodeTableCell>
</VSCodeTableRow>
);
+102 -10
View File
@@ -1,9 +1,10 @@
import * as React from 'react';
import { SeoKeywordInfo } from './SeoKeywordInfo';
import { ErrorBoundary } from '@sentry/react';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../localization';
import { Tooltip } from 'react-tooltip'
import { LocalizationKey, localize } from '../../localization';
import { VSCodeTable, VSCodeTableBody, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
import { Icon } from 'vscrui';
export interface ISeoKeywordsProps {
keywords: string[] | null;
@@ -22,6 +23,8 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
}: React.PropsWithChildren<ISeoKeywordsProps>) => {
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 validateKeywords = () => {
if (!keywords) {
return [];
@@ -51,17 +54,106 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
}
return (
<div className={`seo__status__keywords`}>
<h4>{l10n.t(LocalizationKey.panelSeoKeywordsTitle)}</h4>
<section className={`seo__keywords mb-8`}>
<h4 className='!text-left'>{localize(LocalizationKey.panelSeoKeywordsTitle)}</h4>
<VSCodeTable>
<VSCodeTableHeader>
<VSCodeTableRow>
<VSCodeTableRow className={`border-t border-t-[var(--vscode-editorGroup-border)]`}>
<VSCodeTableHead>
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderKeyword)}
{localize(LocalizationKey.panelSeoKeywordsHeaderKeyword)}
</VSCodeTableHead>
<VSCodeTableHead>
{l10n.t(LocalizationKey.panelSeoKeywordsHeaderDetails)}
<VSCodeTableHead className='text-center'>
<div
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 className='text-center'>
<div
className='flex items-center justify-center h-full'
>
<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-content={localize(LocalizationKey.panelSeoKeywordsDensity)} />
<Tooltip id="tooltip-density" className={tooltipClasses} style={{
fontSize: '12px',
lineHeight: '19px'
}} />
</div>
</VSCodeTableHead>
</VSCodeTableRow>
</VSCodeTableHeader>
@@ -79,10 +171,10 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
{data.wordCount && (
<div className={`text-xs mt-2`}>
{l10n.t(LocalizationKey.panelSeoKeywordsDensity)}
{localize(LocalizationKey.panelSeoKeywordsDensityDescription)}
</div>
)}
</div>
</section>
);
};
+10 -17
View File
@@ -4,13 +4,13 @@ import { TagType } from '../TagType';
import { ArticleDetails } from './ArticleDetails';
import { Collapsible } from './Collapsible';
import FieldBoundary from './ErrorBoundary/FieldBoundary';
import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon';
import { SeoFieldInfo } from './SeoFieldInfo';
import { SeoKeywords } from './SeoKeywords';
import { TagPicker } from './Fields/TagPicker';
import { LocalizationKey, localize } from '../../localization';
import { VSCodeTable, VSCodeTableBody, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
import { VSCodeTable, VSCodeTableBody } from './VSCode/VSCodeTable';
import useContentType from '../../hooks/useContentType';
import { Icon } from 'vscrui';
export interface ISeoStatusProps {
seo: SEO;
@@ -38,19 +38,11 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
const descriptionFieldName = contentType?.fields.find(f => f.name === descriptionField)?.title || descriptionField;
return (
<div>
<div className={`seo__status__details`}>
<h4>{localize(LocalizationKey.panelSeoStatusTitle)}</h4>
<div className='space-y-8'>
<section className={`seo__insights`}>
<h4 className='!text-left'>{localize(LocalizationKey.panelSeoStatusTitle)}</h4>
<VSCodeTable>
<VSCodeTableHeader>
<VSCodeTableRow>
<VSCodeTableHead>{localize(LocalizationKey.panelSeoStatusHeaderProperty)}</VSCodeTableHead>
<VSCodeTableHead>{localize(LocalizationKey.panelSeoStatusHeaderLength)}</VSCodeTableHead>
<VSCodeTableHead>{localize(LocalizationKey.panelSeoStatusHeaderValid)}</VSCodeTableHead>
</VSCodeTableRow>
</VSCodeTableHeader>
<VSCodeTableBody>
{metadata[titleField] && seo.title > 0 ? (
<SeoFieldInfo
@@ -58,6 +50,7 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
value={metadata[titleField].length}
recommendation={localize(LocalizationKey.panelSeoStatusSeoFieldInfoCharacters, seo.title)}
isValid={metadata[titleField].length <= seo.title}
className={`border-t border-t-[var(--vscode-editorGroup-border)]`}
/>
) : null}
@@ -86,9 +79,11 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
recommendation={localize(LocalizationKey.panelSeoStatusSeoFieldInfoWords, seo.content)}
/>
) : null}
<ArticleDetails details={metadata.articleDetails} />
</VSCodeTableBody>
</VSCodeTable>
</div>
</section>
<SeoKeywords
keywords={metadata?.keywords}
@@ -103,7 +98,7 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
<FieldBoundary fieldName={`Keywords`}>
<TagPicker
type={TagType.keywords}
icon={<SymbolKeywordIcon />}
icon={<Icon name="symbol-keyword" className='mr-2' />}
crntSelected={(metadata.keywords as string[]) || []}
options={[]}
freeform={true}
@@ -112,8 +107,6 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
disableConfigurable
/>
</FieldBoundary>
<ArticleDetails details={metadata.articleDetails} />
</div>
);
}, [contentType, metadata, seo, focusElm, unsetFocus]);
+6 -4
View File
@@ -4,21 +4,23 @@ import { CheckIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'
export interface IValidInfoProps {
label?: string;
isValid: boolean;
className?: string;
}
const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({
label,
isValid
isValid,
className,
}: React.PropsWithChildren<IValidInfoProps>) => {
return (
<>
<div className='inline-flex items-center h-full'>
{isValid ? (
<CheckIcon className={`h-4 w-4 text-[#46ec86] mr-2`} />
) : (
<ExclamationTriangleIcon className={`h-4 w-4 text-[var(--vscode-statusBarItem-warningBackground)] mr-2`} />
)}
{label && <span>{label}</span>}
</>
{label && <span className={className || ""}>{label}</span>}
</div>
);
};