From 334e9ea47c4823ef8f8587bdfe22c6442672c92d Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Wed, 8 Feb 2023 18:19:00 +0100 Subject: [PATCH 1/3] Udpate: 1.8 --- admin/src/api/settings/index.js | 17 + .../SeoChecks/AlternativeTextCheck/index.js | 39 +- .../SeoChecks/CanonicalUrlCheck/index.js | 24 +- .../SeoChecks/KeywordDensityCheck/index.js | 8 +- .../SeoChecks/LastUpdatedAtCheck/index.js | 20 +- .../SeoChecks/MetaDescriptionCheck/index.js | 41 +- .../SeoChecks/MetaRobotCheck/index.js | 33 +- .../SeoChecks/MetaSocialCheck/index.js | 6 +- .../SeoChecks/MetaTitleCheck/index.js | 36 +- .../SeoChecks/SEOAccordion/index.js | 6 +- .../SeoChecks/StructuredDataCheck/index.js | 12 +- .../SeoChecks/WordCountCheck/index.js | 8 +- .../{Summary => }/SeoChecks/index.js | 108 +++-- .../BrowserPreview/KeywordCheck/index.js | 3 +- .../BrowserPreview/MetaChecks/index.js | 2 +- .../Summary/BrowserPreview/index.js | 22 +- .../Summary/PreviewChecks/index.js | 6 +- .../Summary/SocialPreview/TabContent/index.js | 6 +- .../Summary/SocialPreview/index.js | 16 +- .../RightLinksCompo/Summary/index.js | 19 +- .../RightLinksCompo/Summary/reducer.js | 4 +- .../CMEditView/RightLinksCompo/index.js | 2 - .../src/components/CMEditView/utils/checks.js | 47 ++- .../utils/getRegularImageAltTexts.js | 58 +++ .../CMEditView/utils/getRichTextFields.js | 84 ++++ .../src/components/CMEditView/utils/index.js | 164 ++------ .../{SeoPage => HomePage}/Header/index.js | 4 +- .../Main}/EmptyComponentLayout/illo.js | 0 .../Main}/EmptyComponentLayout/index.js | 0 .../HomePage/Main/SettingsModal/index.js | 378 ++++++++++++++++++ .../{SeoPage/Info => HomePage/Main}/index.js | 31 +- admin/src/containers/App/index.js | 25 -- admin/src/containers/Initializer/index.js | 26 -- admin/src/pages/HomePage/index.js | 8 +- admin/src/translations/en.json | 28 +- admin/src/translations/fr.json | 28 +- package.json | 1 + server/controllers/index.js | 2 + server/controllers/settings.js | 26 ++ server/routes/index.js | 33 +- server/routes/seo.js | 33 ++ server/routes/settings.js | 18 + server/services/index.js | 2 + server/services/seo.js | 1 - server/services/settings.js | 65 +++ yarn.lock | 5 + 46 files changed, 1087 insertions(+), 418 deletions(-) create mode 100644 admin/src/api/settings/index.js rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/AlternativeTextCheck/index.js (75%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/CanonicalUrlCheck/index.js (73%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/KeywordDensityCheck/index.js (94%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/LastUpdatedAtCheck/index.js (80%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/MetaDescriptionCheck/index.js (56%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/MetaRobotCheck/index.js (75%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/MetaSocialCheck/index.js (94%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/MetaTitleCheck/index.js (64%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/SEOAccordion/index.js (88%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/StructuredDataCheck/index.js (80%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/WordCountCheck/index.js (92%) rename admin/src/components/CMEditView/RightLinksCompo/{Summary => }/SeoChecks/index.js (55%) create mode 100644 admin/src/components/CMEditView/utils/getRegularImageAltTexts.js create mode 100644 admin/src/components/CMEditView/utils/getRichTextFields.js rename admin/src/components/{SeoPage => HomePage}/Header/index.js (91%) rename admin/src/components/{SeoPage/Info => HomePage/Main}/EmptyComponentLayout/illo.js (100%) rename admin/src/components/{SeoPage/Info => HomePage/Main}/EmptyComponentLayout/index.js (100%) create mode 100644 admin/src/components/HomePage/Main/SettingsModal/index.js rename admin/src/components/{SeoPage/Info => HomePage/Main}/index.js (89%) delete mode 100644 admin/src/containers/App/index.js delete mode 100644 admin/src/containers/Initializer/index.js create mode 100644 server/controllers/settings.js create mode 100644 server/routes/seo.js create mode 100644 server/routes/settings.js create mode 100644 server/services/settings.js diff --git a/admin/src/api/settings/index.js b/admin/src/api/settings/index.js new file mode 100644 index 0000000..3d2b9b1 --- /dev/null +++ b/admin/src/api/settings/index.js @@ -0,0 +1,17 @@ +import { request } from '@strapi/helper-plugin'; + +const settings = { + get: async () => { + const data = await request(`/seo/settings`, { + method: 'GET', + }); + return data; + }, + set: async (data) => { + return await request(`/seo/settings`, { + method: 'POST', + body: data, + }); + } +}; +export default settings; diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/AlternativeTextCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/AlternativeTextCheck/index.js similarity index 75% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/AlternativeTextCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/AlternativeTextCheck/index.js index 7684762..a3b68b0 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/AlternativeTextCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/AlternativeTextCheck/index.js @@ -6,15 +6,16 @@ import { Box } from '@strapi/design-system/Box'; import { Icon } from '@strapi/design-system/Icon'; import { Stack } from '@strapi/design-system/Stack'; import { Typography } from '@strapi/design-system/Typography'; +import { Status } from '@strapi/design-system'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import Dot from '@strapi/icons/Dot'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const AlternativeTextCheck = ({ intersections, @@ -76,7 +77,10 @@ const AlternativeTextCheck = ({ return ( ( + - {alt.occurences}{' '} + {alt.occurences} {formatMessage({ id: getTrad('SEOChecks.alternativeTextCheck.missing-text'), defaultMessage: 'missing alt in the following richtext field:', - })}{' '} - {alt.field} + })} + {alt.field} ))} + + + + Tip: + Implement a rule in your front-end code that uses the name of + your images if the + alternativeText + field is empty to further reduce the risk of an empty alt + attribute on your website. + + + } /> diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/CanonicalUrlCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/CanonicalUrlCheck/index.js similarity index 73% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/CanonicalUrlCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/CanonicalUrlCheck/index.js index 302d6c0..c7020c1 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/CanonicalUrlCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/CanonicalUrlCheck/index.js @@ -3,14 +3,15 @@ import React, { useContext, useEffect } from 'react'; import _ from 'lodash'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import { Box } from '@strapi/design-system/Box'; import { Typography } from '@strapi/design-system/Typography'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; + const CanonicalUrlCheck = ({ canonicalUrl, checks }) => { const { formatMessage } = useIntl(); @@ -43,24 +44,23 @@ const CanonicalUrlCheck = ({ canonicalUrl, checks }) => { return ( - - {formatMessage({ - id: getTrad('SEOChecks.canonicalUrlCheck.url'), - defaultMessage: 'Canonical URL:', - })}{' '} + + {canonicalUrl && ( + {canonicalUrl} - - ) + )} + } /> ); diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/KeywordDensityCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/KeywordDensityCheck/index.js similarity index 94% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/KeywordDensityCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/KeywordDensityCheck/index.js index 5d26c68..b491314 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/KeywordDensityCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/KeywordDensityCheck/index.js @@ -7,11 +7,11 @@ import { Flex } from '@strapi/design-system/Flex'; import { Badge } from '@strapi/design-system/Badge'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const KeywordDensityCheck = ({ keywordsDensity, checks }) => { const { formatMessage } = useIntl(); @@ -63,8 +63,6 @@ const KeywordDensityCheck = ({ keywordsDensity, checks }) => { }); }, []); - console.log(checks); - return ( { {Object.keys(keywordsDensity).map((keyword) => ( - + {`${keyword}: ${_.get( diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/LastUpdatedAtCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/LastUpdatedAtCheck/index.js similarity index 80% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/LastUpdatedAtCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/LastUpdatedAtCheck/index.js index db2157d..5422680 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/LastUpdatedAtCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/LastUpdatedAtCheck/index.js @@ -1,16 +1,18 @@ import React, { useContext, useEffect } from 'react'; +import { formatDistance, subDays } from 'date-fns'; + import _ from 'lodash'; import { Box } from '@strapi/design-system/Box'; import { Typography } from '@strapi/design-system/Typography'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const LastUpdatedAtCheck = ({ updatedAt, checks }) => { const { formatMessage } = useIntl(); @@ -58,7 +60,10 @@ const LastUpdatedAtCheck = ({ updatedAt, checks }) => { return ( { status={checks.lastUpdatedAt} component={ updatedAt && ( - + {formatMessage({ id: getTrad('SEOChecks.lastUpdatedAtCheck.last"'), defaultMessage: 'Last updated at:', })}{' '} - {updatedAt} + + {' '} + {formatDistance(new Date(updatedAt), new Date(), { + addSuffix: true, + })} + ) diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaDescriptionCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaDescriptionCheck/index.js similarity index 56% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaDescriptionCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaDescriptionCheck/index.js index 252ac12..12ab60f 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaDescriptionCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaDescriptionCheck/index.js @@ -3,19 +3,24 @@ import React, { useContext, useEffect } from 'react'; import _ from 'lodash'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import { Box } from '@strapi/design-system/Box'; +import { Stack } from '@strapi/design-system/Stack'; +import { ProgressBar } from '@strapi/design-system'; import { Typography } from '@strapi/design-system/Typography'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const MetaDescriptionCheck = ({ metaDescription, checks }) => { const { formatMessage } = useIntl(); const dispatch = useContext(SeoCheckerContext); + const maxLength = 160; + const minimumLength = 50; + let status = { message: formatMessage({ id: getTrad('SEOChecks.metaDescriptionCheck.default'), @@ -33,7 +38,7 @@ const MetaDescriptionCheck = ({ metaDescription, checks }) => { }), color: 'danger', }; - } else if (metaDescription.length > 160) { + } else if (metaDescription.length > maxLength) { status = { message: formatMessage({ id: getTrad('Title-settings.metaDescription-too-long'), @@ -41,7 +46,7 @@ const MetaDescriptionCheck = ({ metaDescription, checks }) => { }), color: 'warning', }; - } else if (metaDescription.length < 50) { + } else if (metaDescription.length < minimumLength) { status = { message: formatMessage({ id: getTrad('Title-settings.metaDescription-too-short'), @@ -59,19 +64,35 @@ const MetaDescriptionCheck = ({ metaDescription, checks }) => { return ( - - {metaDescription} ({metaDescription.length}/160) + + + {metaDescription} + + + 100 + ? 100 + : (metaDescription.length * 100) / maxLength + } + > + + ({metaDescription.length}/{maxLength}) + + + ) } diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaRobotCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaRobotCheck/index.js similarity index 75% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaRobotCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaRobotCheck/index.js index a9327bd..00edd5c 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaRobotCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaRobotCheck/index.js @@ -3,18 +3,20 @@ import React, { useContext, useState, useEffect } from 'react'; import _ from 'lodash'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import { Box } from '@strapi/design-system/Box'; -import { Stack } from '@strapi/design-system/Stack'; +import { Status } from '@strapi/design-system'; import { Icon } from '@strapi/design-system/Icon'; +import { Stack } from '@strapi/design-system/Stack'; import { Typography } from '@strapi/design-system/Typography'; + import Dot from '@strapi/icons/Dot'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const robotTags = [ { name: 'noindex', message: 'Search engines will index this page.' }, @@ -72,7 +74,10 @@ const MetaRobotCheck = ({ metaRobots, checks }) => { return ( { {robotTags.map((tag, index) => ( { ))} + + + + Notice: + In order to not index your entry and no follow, your MetaRobots field should contain the following: + noindex, nofollow . + The rest should be handled by your front-end code logic. "If the field contains noindex, then you need to create the corresponding meta tag etc..." + + + } /> diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaSocialCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaSocialCheck/index.js similarity index 94% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaSocialCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaSocialCheck/index.js index 9384862..31dff02 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaSocialCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaSocialCheck/index.js @@ -3,7 +3,7 @@ import React, { useContext, useEffect } from 'react'; import _ from 'lodash'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import { Box } from '@strapi/design-system/Box'; import { Badge } from '@strapi/design-system/Badge'; @@ -11,7 +11,7 @@ import { Flex } from '@strapi/design-system/Flex'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const MetaSocialCheck = ({ metaSocial, checks }) => { const { formatMessage } = useIntl(); @@ -82,7 +82,7 @@ const MetaSocialCheck = ({ metaSocial, checks }) => { {metaSocial.map((tag, index) => ( - + {tag.socialNetwork} ))} diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaTitleCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaTitleCheck/index.js similarity index 64% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaTitleCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaTitleCheck/index.js index b4cd992..0d7ff29 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/MetaTitleCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/MetaTitleCheck/index.js @@ -3,19 +3,23 @@ import React, { useEffect, useContext } from 'react'; import _ from 'lodash'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import { Box } from '@strapi/design-system/Box'; +import { Stack } from '@strapi/design-system/Stack'; +import { ProgressBar } from '@strapi/design-system'; import { Typography } from '@strapi/design-system/Typography'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const MetaTitleCheck = ({ metaTitle, checks }) => { const { formatMessage } = useIntl(); const dispatch = useContext(SeoCheckerContext); + const maxLength = 60; + let status = { message: formatMessage({ id: getTrad('SEOChecks.metaTitleCheck.default'), @@ -33,7 +37,7 @@ const MetaTitleCheck = ({ metaTitle, checks }) => { }), color: 'danger', }; - } else if (metaTitle.length > 60) { + } else if (metaTitle.length > maxLength) { status = { message: formatMessage({ id: getTrad('Title-settings.metaTitle-too-long'), @@ -52,7 +56,10 @@ const MetaTitleCheck = ({ metaTitle, checks }) => { return ( <> { })} component={ metaTitle && ( - - - {metaTitle} ({metaTitle.length}/60) + + + {metaTitle} + + + + 100 + ? 100 + : (metaTitle.length * 100) / maxLength + } + > + + ({metaTitle.length}/{maxLength}) + + + ) } diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/SEOAccordion/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/SEOAccordion/index.js similarity index 88% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/SEOAccordion/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/SEOAccordion/index.js index e279134..a84b497 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/SEOAccordion/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/SEOAccordion/index.js @@ -34,7 +34,7 @@ const SEOAccordion = ({ title, status, component, label }) => { aria-hidden={true} colors={(theme) => ({ rect: { - fill: _.get(theme, `colors.${status.color}600`), + fill: _.get(theme, `colors.${status?.color}600`), }, })} as={Dot} @@ -43,8 +43,8 @@ const SEOAccordion = ({ title, status, component, label }) => { action={} />} /> - - {status.message} + + {status?.message} diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/StructuredDataCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/StructuredDataCheck/index.js similarity index 80% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/StructuredDataCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/StructuredDataCheck/index.js index 3ee2885..bab5cf4 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/StructuredDataCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/StructuredDataCheck/index.js @@ -3,11 +3,13 @@ import React, { useEffect, useContext } from 'react'; import _ from 'lodash'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; + +import { Box } from '@strapi/design-system/Box'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const StructuredDataCheck = ({ structuredData, checks }) => { const { formatMessage } = useIntl(); @@ -41,13 +43,17 @@ const StructuredDataCheck = ({ structuredData, checks }) => { return ( } /> ); }; diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/WordCountCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/WordCountCheck/index.js similarity index 92% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/WordCountCheck/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/WordCountCheck/index.js index bcbc691..fce480f 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/WordCountCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/WordCountCheck/index.js @@ -3,14 +3,14 @@ import React, { useContext, useEffect } from 'react'; import _ from 'lodash'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../../utils'; +import { getTrad } from '../../../../../utils'; import { Box } from '@strapi/design-system/Box'; import { Typography } from '@strapi/design-system/Typography'; import SEOAccordion from '../SEOAccordion'; -import { SeoCheckerContext } from '../../../Summary'; +import { SeoCheckerContext } from '../../Summary'; const WordCountCheck = ({ wordCount, checks }) => { const { formatMessage } = useIntl(); @@ -63,8 +63,8 @@ const WordCountCheck = ({ wordCount, checks }) => { })} component={ _.isNumber(wordCount) && ( - - + + {formatMessage({ id: getTrad('SEOChecks.wordCountCheck.words'), defaultMessage: 'Words:', diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/index.js similarity index 55% rename from admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/index.js rename to admin/src/components/CMEditView/RightLinksCompo/SeoChecks/index.js index b06cfea..7dc39a6 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SeoChecks/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/index.js @@ -13,7 +13,7 @@ import { Divider } from '@strapi/design-system/Divider'; import { Typography } from '@strapi/design-system/Typography'; import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout'; -import { Illo } from '../../../../SeoPage/Info/EmptyComponentLayout/illo'; +import { Illo } from '../../../HomePage/Main/EmptyComponentLayout/illo'; import MetaRobotCheck from './MetaRobotCheck'; import WordCountCheck from './WordCountCheck'; @@ -26,10 +26,10 @@ import StructuredDataCheck from './StructuredDataCheck'; import MetaDescriptionCheck from './MetaDescriptionCheck'; import AlternativeTextCheck from './AlternativeTextCheck'; -import { getRichTextCheck } from '../../../utils'; +import { getRichTextCheck } from '../../utils'; import { useIntl } from 'react-intl'; -import { getTrad } from '../../../../../utils'; +import { getTrad } from '../../../../utils'; import _ from 'lodash'; @@ -78,45 +78,69 @@ const SeoChecks = ({ {seo ? ( - - - - - - - - - - + {checks?.metaTitle && ( + + )} + {checks?.metaDescription && ( + + )} + {checks?.wordCount && ( + + )} + {checks?.keywordsDensity && ( + + )} + {checks?.metaSocial && ( + + )} + {checks?.canonicalUrl && ( + + )} + {checks?.structuredData && ( + + )} + {checks?.metaRobots && ( + + )} + {checks?.alternativeText && ( + + )} + {checks?.lastUpdatedAt && ( + + )} ) : ( diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/KeywordCheck/index.js b/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/KeywordCheck/index.js index 2895f2b..f12cc64 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/KeywordCheck/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/KeywordCheck/index.js @@ -3,6 +3,7 @@ import React from 'react'; import _ from 'lodash'; import { Icon } from '@strapi/design-system/Icon'; +import { Divider } from '@strapi/design-system'; import { Stack } from '@strapi/design-system/Stack'; import { Typography } from '@strapi/design-system/Typography'; @@ -13,7 +14,7 @@ const KeywordCheck = ({ item, keywords, label }) => { const matches = _keywords.filter((x) => item.toLowerCase().includes(x.toLowerCase().trim())); return ( - + ({ diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/MetaChecks/index.js b/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/MetaChecks/index.js index 2e1d6b7..c924642 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/MetaChecks/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/MetaChecks/index.js @@ -10,7 +10,7 @@ import Dot from '@strapi/icons/Dot'; const MetaChecks = ({ item, max, label, minLimit = false }) => { return ( - + ({ diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/index.js b/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/index.js index d593565..7fe4c3a 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/Summary/BrowserPreview/index.js @@ -20,7 +20,7 @@ import SerpMobile from './SerpMobile'; import MetaChecks from './MetaChecks'; import KeywordCheck from './KeywordCheck'; -import { Illo } from '../../../../SeoPage/Info/EmptyComponentLayout/illo'; +import { Illo } from '../../../../HomePage/Main/EmptyComponentLayout/illo'; import { useIntl } from 'react-intl'; import { getTrad } from '../../../../../utils'; @@ -63,7 +63,7 @@ const BrowserPreview = ({ modifiedData, setIsVisible }) => { {seo ? ( - + { )} + + + + {metaTitle && ( )} @@ -129,19 +133,7 @@ const BrowserPreview = ({ modifiedData, setIsVisible }) => { )} - setIsVisible((prev) => !prev)} - variant="tertiary" - > - {formatMessage({ - id: getTrad('Modal.cancel'), - defaultMessage: 'Cancel', - })} - - } - /> + ); }; diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/PreviewChecks/index.js b/admin/src/components/CMEditView/RightLinksCompo/Summary/PreviewChecks/index.js index 8aae844..d6b1d51 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/PreviewChecks/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/Summary/PreviewChecks/index.js @@ -33,7 +33,7 @@ const SeoChecker = ({ checks }) => { - + ({ @@ -51,7 +51,7 @@ const SeoChecker = ({ checks }) => { : {good} - + ({ @@ -69,7 +69,7 @@ const SeoChecker = ({ checks }) => { : {improvements} - + ({ diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SocialPreview/TabContent/index.js b/admin/src/components/CMEditView/RightLinksCompo/Summary/SocialPreview/TabContent/index.js index 5d23dca..075cab8 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SocialPreview/TabContent/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/Summary/SocialPreview/TabContent/index.js @@ -4,6 +4,7 @@ import { Box } from '@strapi/design-system/Box'; import { Flex } from '@strapi/design-system/Flex'; import { Stack } from '@strapi/design-system/Stack'; import { TabPanel } from '@strapi/design-system/Tabs'; +import { Divider } from '@strapi/design-system/Divider'; import FacebookPreview from '../FacebookPreview'; import TwitterPreview from '../TwitterPreview'; @@ -14,7 +15,7 @@ import KeywordCheck from '../../BrowserPreview/KeywordCheck'; const TabContent = ({ item, keywords }) => { return ( - + {item.image && ( @@ -33,6 +34,9 @@ const TabContent = ({ item, keywords }) => { )} )} + + + diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/SocialPreview/index.js b/admin/src/components/CMEditView/RightLinksCompo/Summary/SocialPreview/index.js index 89c9453..dd31dca 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/SocialPreview/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/Summary/SocialPreview/index.js @@ -23,7 +23,7 @@ import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout'; import TabContent from './TabContent'; -import { Illo } from '../../../../SeoPage/Info/EmptyComponentLayout/illo'; +import { Illo } from '../../../../HomePage/Main/EmptyComponentLayout/illo'; const SocialPreview = ({ modifiedData, setIsVisible }) => { const { formatMessage } = useIntl(); @@ -117,19 +117,7 @@ const SocialPreview = ({ modifiedData, setIsVisible }) => { )} - setIsVisible((prev) => !prev)} - variant="tertiary" - > - {formatMessage({ - id: getTrad('Modal.cancel'), - defaultMessage: 'Cancel', - })} - - } - /> + ); }; diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/index.js b/admin/src/components/CMEditView/RightLinksCompo/Summary/index.js index 151b778..c02b46e 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/Summary/index.js @@ -8,7 +8,7 @@ import { Divider } from '@strapi/design-system/Divider'; import { Typography } from '@strapi/design-system/Typography'; import { TextButton } from '@strapi/design-system/TextButton'; -import SeoChecks from './SeoChecks'; +import SeoChecks from '../SeoChecks'; import SocialPreview from './SocialPreview'; import PreviewChecks from './PreviewChecks'; import BrowserPreview from './BrowserPreview'; @@ -23,8 +23,6 @@ import { getTrad } from '../../../../utils'; import reducer from './reducer'; -import _ from 'lodash'; - const initialState = { preview: true, }; @@ -40,14 +38,19 @@ const Summary = () => { const [isSeoChecksVisible, setIsSeoChecksVisible] = useState(false); const [localChecks, setLocalChecks] = useState({}); const [checks, dispatch] = useReducer(reducer, initialState); - const { allLayoutData, modifiedData } = useCMEditViewDataManager(); + const { allLayoutData, layout, modifiedData } = useCMEditViewDataManager(); const { contentType, components } = allLayoutData; - useEffect(() => { - if (!_.isEqual(localChecks, checks)) { - if (_.has(checks, 'preview')) { - const status = getAllChecks(modifiedData, components, contentType); + useEffect(async () => { + if (!(JSON.stringify(localChecks) === JSON.stringify(checks))) { + if (checks?.preview) { + const status = await getAllChecks( + layout, + modifiedData, + components, + contentType + ); dispatch({ type: 'UPDATE_FOR_PREVIEW', value: status, diff --git a/admin/src/components/CMEditView/RightLinksCompo/Summary/reducer.js b/admin/src/components/CMEditView/RightLinksCompo/Summary/reducer.js index 10cdf23..98ca3b6 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/Summary/reducer.js +++ b/admin/src/components/CMEditView/RightLinksCompo/Summary/reducer.js @@ -6,8 +6,8 @@ function reducer(state, action) { return { ...state, [action.value.entity]: { - color: action.value.color, - message: action.value.message, + color: action.value?.color, + message: action.value?.message, }, }; case 'UPDATE_FOR_PREVIEW': diff --git a/admin/src/components/CMEditView/RightLinksCompo/index.js b/admin/src/components/CMEditView/RightLinksCompo/index.js index 33e01d6..6351b7c 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/index.js @@ -14,8 +14,6 @@ const SeoChecker = () => { if (modifiedData.hasOwnProperty('seo')) { return ( { @@ -240,7 +242,9 @@ const structuredDataPreview = (modifiedData) => { return status; }; -const getAllChecks = (modifiedData, components, contentType) => { +const getAllChecks = async (layout, modifiedData, components, contentType) => { + const defaultSettings = await settingsAPI.get(); + const { wordCount, keywordsDensity, emptyAltCount } = getRichTextCheck( modifiedData, components, @@ -248,17 +252,38 @@ const getAllChecks = (modifiedData, components, contentType) => { ); let result = { - wordCount: getWordCountPreview(wordCount), - metaRobots: metaRobotPreview(modifiedData), - metaSocial: metaSocialPreview(modifiedData), - canonicalUrl: canonicalUrlPreview(modifiedData), - metaTitle: getMetaTitleCheckPreview(modifiedData), - lastUpdatedAt: lastUpdatedAtPreview(modifiedData), - structuredData: structuredDataPreview(modifiedData), - metaDescription: getMetaDescriptionPreview(modifiedData), - alternativeText: getAlternativeTextPreview(emptyAltCount), - keywordsDensity: getKeywordDensityPreview(keywordsDensity), + ...(defaultSettings[layout?.uid].seoChecks.metaTitle && { + metaTitle: getMetaTitleCheckPreview(modifiedData), + }), + ...(defaultSettings[layout?.uid].seoChecks.wordCount && { + wordCount: getWordCountPreview(wordCount), + }), + ...(defaultSettings[layout?.uid].seoChecks.metaRobots && { + metaRobots: metaRobotPreview(modifiedData), + }), + ...(defaultSettings[layout?.uid].seoChecks.metaSocial && { + metaSocial: metaSocialPreview(modifiedData), + }), + ...(defaultSettings[layout?.uid].seoChecks.canonicalUrl && { + canonicalUrl: canonicalUrlPreview(modifiedData), + }), + ...(defaultSettings[layout?.uid].seoChecks.lastUpdatedAt && { + lastUpdatedAt: lastUpdatedAtPreview(modifiedData), + }), + ...(defaultSettings[layout?.uid].seoChecks.structuredData && { + structuredData: structuredDataPreview(modifiedData), + }), + ...(defaultSettings[layout?.uid].seoChecks.metaDescription && { + metaDescription: getMetaDescriptionPreview(modifiedData), + }), + ...(defaultSettings[layout?.uid].seoChecks.alternativeText && { + alternativeText: getAlternativeTextPreview(emptyAltCount), + }), + ...(defaultSettings[layout?.uid].seoChecks.keywordDensity && { + keywordsDensity: getKeywordDensityPreview(keywordsDensity), + }), }; + return result; }; diff --git a/admin/src/components/CMEditView/utils/getRegularImageAltTexts.js b/admin/src/components/CMEditView/utils/getRegularImageAltTexts.js new file mode 100644 index 0000000..a90cfeb --- /dev/null +++ b/admin/src/components/CMEditView/utils/getRegularImageAltTexts.js @@ -0,0 +1,58 @@ +import _ from 'lodash'; + +const recursiveSearch = ( + obj, + keyword, + relations, + results = [], + allImageNames = [] +) => { + + const alternativeTexts = results; + const imageNames = allImageNames; + + // Iterating on every entry fields starting with modifiedData + Object.keys(obj).forEach((key) => { + // Get the value of the field + const value = obj[key]; + // If the field is an alternativeText + if (key === keyword && typeof value !== 'object') { + // It pushes the value in an array and the name in another + alternativeTexts.push(value); + imageNames.push(obj['name']); + } else if ( + typeof value === 'object' && + !relations.includes(key) && + !_.isNull(value) + ) { + recursiveSearch(value, keyword, relations, alternativeTexts, imageNames); + } + }); + return { alternativeTexts, imageNames }; +}; + +const getRegularImageAltTexts = (contentType, modifiedData) => { + // Prevent to recursively search in relations starting with localizations + let relations = ['localizations']; + + // Get every relations to black list them + Object.keys(contentType.attributes).map((field) => { + if (contentType.attributes[field].type === 'relation') { + relations.push(field); + } + }); + + const { alternativeTexts, imageNames } = recursiveSearch( + modifiedData, + 'alternativeText', + relations + ); + + const alternativeTextCount = alternativeTexts.length; + const intersections = + alternativeTexts.filter((x) => imageNames.includes(x)).length - alternativeTextCount; + + return { intersections, altTexts: alternativeTexts }; +}; + +export default getRegularImageAltTexts; diff --git a/admin/src/components/CMEditView/utils/getRichTextFields.js b/admin/src/components/CMEditView/utils/getRichTextFields.js new file mode 100644 index 0000000..38e9f3e --- /dev/null +++ b/admin/src/components/CMEditView/utils/getRichTextFields.js @@ -0,0 +1,84 @@ +import _ from 'lodash'; + +// Get every 1st level richtext fields +const getRichTextFields = (contentType, components, modifiedData) => { + let dynamicZones = []; + let richTextFields = []; + + // Get every Dynamic Zones from the actual content-type + Object.values(modifiedData).map((field, index) => { + if (_.isArray(field)) { + const isComponent = field.find((subFields) => + Object.keys(subFields).includes('__component') + ); + + if (isComponent) dynamicZones.push(Object.keys(modifiedData)[index]); + } + }); + + // Get every 1st level richtext fields + Object.keys(contentType.attributes).map((field) => { + if (contentType.attributes[field].type === 'richtext') { + richTextFields.push({ name: field, field: null }); + } + }); + + // Get every 1st level richtext fields in the CT component + Object.keys(components).map((name) => { + Object.keys(components[name].attributes).map((field) => { + if (components[name].attributes[field].type === 'richtext') { + richTextFields.push({ name, field }); + } + }); + }); + + // Replace every component names by the parent DZ name if exists + // Necessary when having DZ => Component => richtext + richTextFields.map((item, index) => { + const exploded = item.name.split('.'); + const last = _.last(exploded); + const tmp = _.get(modifiedData, last, null); + if (!tmp) { + Object.keys(components).map((name) => { + if (components[name].isComponent) { + Object.keys(components[name].attributes).map((field) => { + if ( + components[name].attributes[field].component && + components[name].attributes[field].component === item.name + ) { + const associatedDZ = dynamicZones.find( + (dz) => dz === name.split('.')[0] + ); + const newObject = { name, field: item.field, inDz: associatedDZ }; + richTextFields[index] = newObject; + } + }); + } + }); + } + }); + + // Remove components that are not in the CT + dynamicZones.map((dz) => { + const item = _.get(modifiedData, dz, []); + richTextFields.map((field, index) => { + const compoIsInModifiedData = item.find( + (x) => x.__component === field.name + ); + + // If component is in the DZ but doesn't have an associated DZ, we add it to the object + if (!_.isEmpty(compoIsInModifiedData) && !field.inDz) { + richTextFields[index] = { ...field, inDz: dz }; + } + + // If the component is not included in the DZ, we remove the object + if (_.isEmpty(compoIsInModifiedData) && field.inDz) { + _.pull(richTextFields, field); + } + }); + }); + + return richTextFields; +}; + +export default getRichTextFields; diff --git a/admin/src/components/CMEditView/utils/index.js b/admin/src/components/CMEditView/utils/index.js index a85ea06..2b0311f 100644 --- a/admin/src/components/CMEditView/utils/index.js +++ b/admin/src/components/CMEditView/utils/index.js @@ -1,102 +1,35 @@ import _ from 'lodash'; +import getRichTextFields from './getRichTextFields'; +import getRegularImageAltTexts from './getRegularImageAltTexts'; + const showdown = require('showdown'); const converter = new showdown.Converter(); -let keywordsDensity = {}; - -// Function that get every 1st level richtext fields -const getRichTextFields = (contentType, components, modifiedData) => { - let richTextFields = []; - - // Get every Dynamic Zones from the actual content-type - let dynamicZones = []; - Object.values(modifiedData).map((field, index) => { - if (_.isArray(field)) { - const isComponent = field.find((subFields) => - Object.keys(subFields).includes('__component') - ); - - if (isComponent) dynamicZones.push(Object.keys(modifiedData)[index]); - } - }); - - // Get every 1st level richtext fields - Object.keys(contentType.attributes).map((field) => { - if (contentType.attributes[field].type === 'richtext') { - richTextFields.push({ name: field, field: null }); - } - }); - - // Get every 1st level richtext fields in the CT component - Object.keys(components).map((name) => { - Object.keys(components[name].attributes).map((field) => { - if (components[name].attributes[field].type === 'richtext') { - richTextFields.push({ name, field }); - } - }); - }); - - // Replace every component names by the parent DZ name if exists - // Necessary when having DZ => Component => richtext - richTextFields.map((item, index) => { - const exploded = item.name.split('.'); - const last = _.last(exploded); - const tmp = _.get(modifiedData, last, null); - if (!tmp) { - Object.keys(components).map((name) => { - if (components[name].isComponent) { - Object.keys(components[name].attributes).map((field) => { - if ( - components[name].attributes[field].component && - components[name].attributes[field].component === item.name - ) { - const associatedDZ = dynamicZones.find( - (dz) => dz === name.split('.')[0] - ); - const newObject = { name, field: item.field, inDz: associatedDZ }; - richTextFields[index] = newObject; - } - }); - } - }); - } - }); - - // Remove components that are not in the CT - dynamicZones.map((dz) => { - const item = _.get(modifiedData, dz, []); - richTextFields.map((field, index) => { - const compoIsInModifiedData = item.find( - (x) => x.__component === field.name - ); - - // If component is in the DZ but doesn't have an associated DZ, we add it to the object - if (!_.isEmpty(compoIsInModifiedData) && !field.inDz) { - richTextFields[index] = { ...field, inDz: dz }; - } - // If the component is not included in the DZ, we remove the object - if (_.isEmpty(compoIsInModifiedData) && field.inDz) { - _.pull(richTextFields, field); - } - }); - }); - return richTextFields; -}; +let keywordsDensity = {}; const getEmptyAltCount = (richtext, field) => { if (richtext) { - const occurences = richtext - .split('\n') - .filter((x) => x.includes('![](')).length; - - return { field, occurences }; + let htmlOccurences = 0; + const splittedRichtext = richtext.split('\n'); + const occurences = splittedRichtext.filter((x) => + x.includes('![](') + ).length; + + const images = richtext.match(/]*\/?>/g); + if (images) { + htmlOccurences += images.filter( + (image) => !image.includes('alt=') + ).length; + } + return { field, occurences: occurences + htmlOccurences }; } return { field, occurences: 0 }; }; const increaseCounter = (base, field) => { const richtext = _.get(base, field, ''); + // Get empty HTML and Markdown empty alternativeText const emptyAlts = getEmptyAltCount(richtext, field); if (richtext) { const html = converter.makeHtml(richtext); @@ -129,52 +62,6 @@ const buildKeywordDensityObject = (keywords, words) => { }); }; -const recursiveSearch = ( - obj, - searchKey, - blackList, - results = [], - imageNames = [] -) => { - const altTexts = results; - const names = imageNames; - Object.keys(obj).forEach((key) => { - const value = obj[key]; - if (key === searchKey && typeof value !== 'object') { - altTexts.push(value); - names.push(obj['name']); - } else if ( - typeof value === 'object' && - !blackList.includes(key) && - !_.isNull(value) - ) { - recursiveSearch(value, searchKey, blackList, altTexts, names); - } - }); - return { altTexts, names }; -}; - -const getAltCheck = (contentType, modifiedData) => { - let relations = ['localizations']; - - // Get every 1st level richtext fields - Object.keys(contentType.attributes).map((field) => { - if (contentType.attributes[field].type === 'relation') { - relations.push(field); - } - }); - - const { altTexts, names } = recursiveSearch( - modifiedData, - 'alternativeText', - relations - ); - const altCount = altTexts.length; - const intersection = - altTexts.filter((x) => names.includes(x)).length - altCount; - return { intersection, altTexts }; -}; - const getRichTextCheck = (modifiedData, components, contentType) => { const richTextFields = getRichTextFields( contentType, @@ -182,10 +69,17 @@ const getRichTextCheck = (modifiedData, components, contentType) => { modifiedData ); - const { intersections, altTexts } = getAltCheck(contentType, modifiedData); + const { intersections, altTexts } = getRegularImageAltTexts( + contentType, + modifiedData + ); + let emptyAltCount = { intersections, richTextAlts: [], altTexts }; + let wordCount = 0; let keywords = []; + + // Keywords const tmp = _.get(modifiedData, 'seo.keywords', null); if (tmp) keywords = tmp.toLowerCase().split(','); keywordsDensity = {}; @@ -268,7 +162,11 @@ const getRichTextCheck = (modifiedData, components, contentType) => { } }); - return { wordCount, keywordsDensity, emptyAltCount }; + return { + wordCount, + keywordsDensity, + emptyAltCount: emptyAltCount, + }; }; export { getRichTextCheck }; diff --git a/admin/src/components/SeoPage/Header/index.js b/admin/src/components/HomePage/Header/index.js similarity index 91% rename from admin/src/components/SeoPage/Header/index.js rename to admin/src/components/HomePage/Header/index.js index 52e8db0..b314474 100644 --- a/admin/src/components/SeoPage/Header/index.js +++ b/admin/src/components/HomePage/Header/index.js @@ -1,13 +1,13 @@ import React from 'react'; import { Box } from '@strapi/design-system/Box'; -import { LinkButton } from '@strapi/design-system/LinkButton'; +import { Button } from '@strapi/design-system/Button'; import { BaseHeaderLayout } from '@strapi/design-system/Layout'; import { useIntl } from 'react-intl'; import { getTrad } from '../../../utils'; -import Pencil from '@strapi/icons/Pencil'; +import Equalizer from '@strapi/icons/Equalizer'; const Header = (seoComponent) => { const { formatMessage } = useIntl(); diff --git a/admin/src/components/SeoPage/Info/EmptyComponentLayout/illo.js b/admin/src/components/HomePage/Main/EmptyComponentLayout/illo.js similarity index 100% rename from admin/src/components/SeoPage/Info/EmptyComponentLayout/illo.js rename to admin/src/components/HomePage/Main/EmptyComponentLayout/illo.js diff --git a/admin/src/components/SeoPage/Info/EmptyComponentLayout/index.js b/admin/src/components/HomePage/Main/EmptyComponentLayout/index.js similarity index 100% rename from admin/src/components/SeoPage/Info/EmptyComponentLayout/index.js rename to admin/src/components/HomePage/Main/EmptyComponentLayout/index.js diff --git a/admin/src/components/HomePage/Main/SettingsModal/index.js b/admin/src/components/HomePage/Main/SettingsModal/index.js new file mode 100644 index 0000000..e37771b --- /dev/null +++ b/admin/src/components/HomePage/Main/SettingsModal/index.js @@ -0,0 +1,378 @@ +import React, { useState, useEffect } from 'react'; + +import { Box } from '@strapi/design-system'; +import { Stack } from '@strapi/design-system'; +import { Switch } from '@strapi/design-system/Switch'; +import { Button } from '@strapi/design-system/Button'; +import { Typography } from '@strapi/design-system/Typography'; +import { GridLayout } from '@strapi/design-system'; + +import { + ModalLayout, + ModalBody, + ModalHeader, + ModalFooter, +} from '@strapi/design-system'; + +import { useIntl } from 'react-intl'; +import { getTrad } from '../../../../utils'; + +import Equalizer from '@strapi/icons/Equalizer'; +import InformationSquare from '@strapi/icons/InformationSquare'; + +import { ContentBox } from '@strapi/helper-plugin'; + +const settingsAPI = require('../../../../api/settings').default; + +const SettingsModal = ({ item }) => { + const [metaTitle, setMetaTitle] = useState(true); + const [metaDescription, setMetaDescription] = useState(true); + const [metaRobots, setMetaRobots] = useState(true); + const [metaSocial, setMetaSocial] = useState(true); + const [wordCount, setWordCount] = useState(true); + const [canonicalUrl, setCanonicalUrl] = useState(true); + const [keywordDensity, setKeywordDensity] = useState(true); + const [structuredData, setStructuredData] = useState(true); + const [alternativeText, setAlternativeText] = useState(true); + const [lastUpdatedAt, setLastUpdatedAt] = useState(true); + + const [defaultSettings, setDefaultSettings] = useState(null); + + const [isVisible, setIsVisible] = useState(false); + const { formatMessage } = useIntl(); + + useEffect(() => { + const fetchDefaultSettings = async () => { + const tmpSettings = await settingsAPI.get(); + setDefaultSettings(tmpSettings); + }; + fetchDefaultSettings(); + }, []); + + const handleOpeningModal = (prev) => { + const matchingContentType = defaultSettings[item?.uid]; + + setMetaTitle(matchingContentType?.seoChecks?.metaTitle); + setMetaDescription(matchingContentType?.seoChecks?.metaDescription); + setMetaRobots(matchingContentType?.seoChecks?.metaRobots); + setMetaSocial(matchingContentType?.seoChecks?.metaSocial); + setWordCount(matchingContentType?.seoChecks?.wordCount); + setCanonicalUrl(matchingContentType?.seoChecks?.canonicalUrl); + setKeywordDensity(matchingContentType?.seoChecks?.keywordDensity); + setStructuredData(matchingContentType?.seoChecks?.structuredData); + setAlternativeText(matchingContentType?.seoChecks?.alternativeText); + setLastUpdatedAt(matchingContentType?.seoChecks?.lastUpdatedAt); + + setIsVisible((prev) => !prev); + }; + + const handleSavingSettingsModal = (prev) => { + const newSettings = { + ...defaultSettings, + [item?.uid]: { + collectionName: defaultSettings[item?.uid]?.collectionName, + seoChecks: { + metaTitle, + metaDescription, + metaRobots, + metaSocial, + wordCount, + canonicalUrl, + keywordDensity, + structuredData, + alternativeText, + lastUpdatedAt, + }, + }, + }; + + settingsAPI.set(newSettings).then(async () => { + setDefaultSettings(newSettings); + setIsVisible((prev) => !prev); + }); + }; + + return ( + <> + + {isVisible && ( + setIsVisible((prev) => !prev)} + labelledBy="title" + > + + + {formatMessage({ + id: getTrad('SEOPage.info.settings'), + defaultMessage: 'Settings', + })} + + + + + } + iconBackground="primary100" + /> + + + + + setMetaTitle((s) => !s)} + /> + + {formatMessage({ + id: getTrad('SEOPage.info.settings.meta-title-check'), + defaultMessage: 'Meta Title', + })} + + + + + + setMetaDescription((s) => !s)} + /> + + {formatMessage({ + id: getTrad( + 'SEOPage.info.settings.meta-description-check' + ), + defaultMessage: 'Meta Description', + })} + + + + + + setMetaRobots((s) => !s)} + /> + + {formatMessage({ + id: getTrad('SEOPage.info.settings.meta-robots-check'), + defaultMessage: 'Meta Robots', + })} + + + + + + setMetaSocial((s) => !s)} + /> + + {formatMessage({ + id: getTrad('SEOPage.info.settings.meta-social-check'), + defaultMessage: 'Meta Social', + })} + + + + + + setWordCount((s) => !s)} + /> + + {formatMessage({ + id: getTrad('SEOPage.info.settings.word-count-check'), + defaultMessage: 'Word Count', + })} + + + + + + setCanonicalUrl((s) => !s)} + /> + + {formatMessage({ + id: getTrad('SEOPage.info.settings.canonical-url-check'), + defaultMessage: 'Canonical URL', + })} + + + + + + setKeywordDensity((s) => !s)} + /> + + {formatMessage({ + id: getTrad( + 'SEOPage.info.settings.keyword-density-check' + ), + defaultMessage: 'Keyword Density', + })} + + + + + + setStructuredData((s) => !s)} + /> + + {formatMessage({ + id: getTrad( + 'SEOPage.info.settings.structured-data-check' + ), + defaultMessage: 'Structured Data', + })} + + + + + + setAlternativeText((s) => !s)} + /> + + {formatMessage({ + id: getTrad( + 'SEOPage.info.settings.alternative-text-check' + ), + defaultMessage: 'Alt Text', + })} + + + + + + setLastUpdatedAt((s) => !s)} + /> + + {formatMessage({ + id: getTrad( + 'SEOPage.info.settings.last-updated-at-check' + ), + defaultMessage: 'Last Updated At', + })} + + + + + + setIsVisible((prev) => !prev)} + variant="tertiary" + > + {formatMessage({ + id: getTrad('SEOPage.info.settings.cancel.button'), + defaultMessage: 'Cancel', + })} + + } + endActions={ + <> + + + } + /> + + )} + + ); +}; + +export default SettingsModal; diff --git a/admin/src/components/SeoPage/Info/index.js b/admin/src/components/HomePage/Main/index.js similarity index 89% rename from admin/src/components/SeoPage/Info/index.js rename to admin/src/components/HomePage/Main/index.js index 07f943a..62875f0 100644 --- a/admin/src/components/SeoPage/Info/index.js +++ b/admin/src/components/HomePage/Main/index.js @@ -1,19 +1,23 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Box } from '@strapi/design-system/Box'; import { Flex } from '@strapi/design-system/Flex'; +import { Button } from '@strapi/design-system/Button'; import { LinkButton } from '@strapi/design-system/LinkButton'; import { Typography } from '@strapi/design-system/Typography'; import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout'; import { Table, Thead, Tbody, Tr, Td, Th } from '@strapi/design-system/Table'; + import { useIntl } from 'react-intl'; import { getTrad } from '../../../utils'; import Plus from '@strapi/icons/Plus'; import Check from '@strapi/icons/Check'; -import { Illo } from '../Info/EmptyComponentLayout/illo'; +import { Illo } from './EmptyComponentLayout/illo'; + +import SettingsModal from './SettingsModal' import { Tabs, @@ -25,15 +29,16 @@ import { import _ from 'lodash'; -const Info = ({ contentTypes }) => { +const Main = ({ contentTypes }) => { + const { formatMessage } = useIntl(); return ( <> - + - + {formatMessage({ id: getTrad('SEOPage.tab.collection-type-title'), defaultMessage: 'Collection Types', @@ -82,12 +87,7 @@ const Info = ({ contentTypes }) => { {item.seo ? ( - }> - {formatMessage({ - id: getTrad('SEOPage.info.added'), - defaultMessage: 'Added', - })} - + ) : ( } @@ -165,12 +165,7 @@ const Info = ({ contentTypes }) => { {item.seo ? ( - }> - {formatMessage({ - id: getTrad('SEOPage.info.added'), - defaultMessage: 'Added', - })} - + ) : ( } @@ -223,4 +218,4 @@ const Info = ({ contentTypes }) => { ); }; -export default Info; +export default Main; diff --git a/admin/src/containers/App/index.js b/admin/src/containers/App/index.js deleted file mode 100644 index b2d80ef..0000000 --- a/admin/src/containers/App/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * - * This component is the skeleton around the actual pages, and should only - * contain code that should be seen on all pages. (e.g. navigation bar) - * - */ - -import React from 'react'; -import { Switch, Route } from 'react-router-dom'; -import { NotFound } from '@strapi/helper-plugin'; -import pluginId from '../../pluginId'; -import HomePage from '../HomePage'; - -const App = () => { - return ( -
- - - - -
- ); -}; - -export default App; diff --git a/admin/src/containers/Initializer/index.js b/admin/src/containers/Initializer/index.js deleted file mode 100644 index 71dc50e..0000000 --- a/admin/src/containers/Initializer/index.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * - * Initializer - * - */ - -import { useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import pluginId from '../../pluginId'; - -const Initializer = ({ setPlugin }) => { - const ref = useRef(); - ref.current = setPlugin; - - useEffect(() => { - ref.current(pluginId); - }, []); - - return null; -}; - -Initializer.propTypes = { - setPlugin: PropTypes.func.isRequired, -}; - -export default Initializer; diff --git a/admin/src/pages/HomePage/index.js b/admin/src/pages/HomePage/index.js index 19fe388..7fe3cb0 100644 --- a/admin/src/pages/HomePage/index.js +++ b/admin/src/pages/HomePage/index.js @@ -19,8 +19,8 @@ import { getTrad } from '../../utils'; import { fetchSeoComponent, fetchContentTypes } from '../../utils/api'; -import Info from '../../components/SeoPage/Info'; -import Header from '../../components/SeoPage/Header'; +import Info from '../../components/HomePage/Main'; +import Header from '../../components/HomePage/Header'; import { createSeoComponent } from '../../utils/api'; @@ -66,14 +66,14 @@ const HomePage = () => { <>
- + { + const settingsService = strapi.plugins['seo'].services.settings; + + const getSettings = async (ctx) => { + try { + return settingsService.getSettings(); + } catch (err) { + ctx.throw(500, err); + } + }; + const setSettings = async (ctx) => { + const { body } = ctx.request; + try { + await settingsService.setSettings(body); + return settingsService.getSettings(); + } catch (err) { + ctx.throw(500, err); + } + }; + return { + getSettings, + setSettings, + }; +}; diff --git a/server/routes/index.js b/server/routes/index.js index 7e9d924..2c31a78 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,29 +1,4 @@ -module.exports = [ - { - method: 'GET', - path: '/component', - handler: 'seo.findSeoComponent', - config: { - auth: false, - policies: [], - }, - }, - { - method: 'POST', - path: '/component', - handler: 'seo.createSeoComponent', - config: { - auth: false, - policies: [], - }, - }, - { - method: 'GET', - path: '/content-types', - handler: 'seo.findContentTypes', - config: { - auth: false, - policies: [], - }, - }, -]; +module.exports = { + seo: require('./seo'), + settings: require('./settings'), +}; diff --git a/server/routes/seo.js b/server/routes/seo.js new file mode 100644 index 0000000..99c8e07 --- /dev/null +++ b/server/routes/seo.js @@ -0,0 +1,33 @@ +module.exports = { + // accessible only from admin UI + type: 'admin', + routes: [ + { + method: 'GET', + path: '/component', + handler: 'seo.findSeoComponent', + config: { + auth: false, // To Remove + policies: [], + }, + }, + { + method: 'POST', + path: '/component', + handler: 'seo.createSeoComponent', + config: { + auth: false, // To Remove + policies: [], + }, + }, + { + method: 'GET', + path: '/content-types', + handler: 'seo.findContentTypes', + config: { + auth: false, // To Remove + policies: [], + }, + }, + ], +}; diff --git a/server/routes/settings.js b/server/routes/settings.js new file mode 100644 index 0000000..be5d5ea --- /dev/null +++ b/server/routes/settings.js @@ -0,0 +1,18 @@ +module.exports = { + // accessible only from admin UI + type: 'admin', + routes: [ + { + method: 'GET', + path: '/settings', + handler: 'settings.getSettings', + config: { auth: false, policies: [] }, + }, + { + method: 'POST', + path: '/settings', + handler: 'settings.setSettings', + config: { policies: [] }, + }, + ], +}; diff --git a/server/services/index.js b/server/services/index.js index 4f67394..09de106 100644 --- a/server/services/index.js +++ b/server/services/index.js @@ -1,7 +1,9 @@ 'use strict'; const seo = require('./seo'); +const settings = require('./settings'); module.exports = { seo, + settings }; diff --git a/server/services/seo.js b/server/services/seo.js index 7fef5ff..183f660 100644 --- a/server/services/seo.js +++ b/server/services/seo.js @@ -1,7 +1,6 @@ 'use strict'; const _ = require('lodash'); -const fetch = require('node-fetch'); const seoContent = require('../components/seo.json'); const metaSocialContent = require('../components/meta-social.json'); diff --git a/server/services/settings.js b/server/services/settings.js new file mode 100644 index 0000000..99979ce --- /dev/null +++ b/server/services/settings.js @@ -0,0 +1,65 @@ +'use strict'; + +module.exports = ({ strapi }) => { + const getPluginStore = () => { + return strapi.store({ + environment: '', + type: 'plugin', + name: 'seo', + }); + }; + + // Create default settings + const createDefaultConfig = async () => { + const pluginStore = getPluginStore(); + + const newContentTypes = {}; + + const keys = Object.keys(strapi.contentTypes); + keys.map((key) => { + if (key.includes('api::')) { + newContentTypes[key] = {}; + newContentTypes[key]['collectionName'] = key.split('.').pop(); + newContentTypes[key]['seoChecks'] = { + metaTitle: true, + metaDescription: true, + metaRobots: true, + metaSocial: true, + wordCount: true, + canonicalUrl: true, + keywordDensity: true, + structuredData: true, + alternativeText: true, + lastUpdatedAt: true, + }; + } + }); + + const value = newContentTypes; + + await pluginStore.set({ key: 'settings', value }); + return pluginStore.get({ key: 'settings' }); + }; + const createConfigFromData = async (settings) => { + const value = settings; + const pluginStore = getPluginStore(); + await pluginStore.set({ key: 'settings', value }); + return pluginStore.get({ key: 'settings' }); + }; + const getSettings = async () => { + const pluginStore = getPluginStore(); + + let config = await pluginStore.get({ key: 'settings' }); + if (!config) { + config = await createDefaultConfig(); + } + return config; + }; + const setSettings = async (data) => { + return createConfigFromData(data); + }; + return { + getSettings, + setSettings, + }; +}; diff --git a/yarn.lock b/yarn.lock index b0b2ff0..ba159ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +date-fns@^2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" From aaf32f03da38ab396328d03a374732e9810e3df1 Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Thu, 16 Feb 2023 16:22:50 +0100 Subject: [PATCH 2/3] Remove: Divider in SEOChecks modal --- .../components/CMEditView/RightLinksCompo/SeoChecks/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/index.js b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/index.js index 7dc39a6..fa6bea5 100644 --- a/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/index.js +++ b/admin/src/components/CMEditView/RightLinksCompo/SeoChecks/index.js @@ -9,7 +9,6 @@ import { import { Box } from '@strapi/design-system/Box'; import { Button } from '@strapi/design-system/Button'; -import { Divider } from '@strapi/design-system/Divider'; import { Typography } from '@strapi/design-system/Typography'; import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout'; @@ -71,9 +70,7 @@ const SeoChecks = ({ defaultMessage: 'SEO Analyze', })} - - - + {seo ? ( From 3f0c20c04e90a52a2021e42bffc8b9589dfdf3c8 Mon Sep 17 00:00:00 2001 From: Maxime Castres Date: Thu, 16 Feb 2023 16:26:12 +0100 Subject: [PATCH 3/3] Remove: auth: false to routes --- server/routes/seo.js | 3 --- server/routes/settings.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/server/routes/seo.js b/server/routes/seo.js index 99c8e07..8f15209 100644 --- a/server/routes/seo.js +++ b/server/routes/seo.js @@ -7,7 +7,6 @@ module.exports = { path: '/component', handler: 'seo.findSeoComponent', config: { - auth: false, // To Remove policies: [], }, }, @@ -16,7 +15,6 @@ module.exports = { path: '/component', handler: 'seo.createSeoComponent', config: { - auth: false, // To Remove policies: [], }, }, @@ -25,7 +23,6 @@ module.exports = { path: '/content-types', handler: 'seo.findContentTypes', config: { - auth: false, // To Remove policies: [], }, }, diff --git a/server/routes/settings.js b/server/routes/settings.js index be5d5ea..78255e9 100644 --- a/server/routes/settings.js +++ b/server/routes/settings.js @@ -6,7 +6,7 @@ module.exports = { method: 'GET', path: '/settings', handler: 'settings.getSettings', - config: { auth: false, policies: [] }, + config: { policies: [] }, }, { method: 'POST',