From a1dc0f4626c60d567a53cb8885d5305668100c13 Mon Sep 17 00:00:00 2001 From: Tomas Vykoukal Date: Tue, 19 Sep 2023 12:45:18 +0200 Subject: [PATCH] refactor Canonical URL generator --- storefront/components/Basic/Head/SeoMeta.tsx | 19 ++++---- storefront/components/Layout/CommonLayout.tsx | 13 +++++- storefront/helpers/queryParamNames.ts | 7 --- .../helpers/seo/generateCanonicalUrl.ts | 46 +++++++++++++------ storefront/hooks/seo/useSeo.ts | 43 +++++++++-------- storefront/pages/articles/[articleSlug].tsx | 1 + .../pages/blogArticles/[blogArticleSlug].tsx | 1 + storefront/pages/products/[productSlug].tsx | 1 + storefront/pages/stores/[storeSlug].tsx | 6 ++- 9 files changed, 81 insertions(+), 56 deletions(-) diff --git a/storefront/components/Basic/Head/SeoMeta.tsx b/storefront/components/Basic/Head/SeoMeta.tsx index cba47dcf79..b7e89b6c22 100644 --- a/storefront/components/Basic/Head/SeoMeta.tsx +++ b/storefront/components/Basic/Head/SeoMeta.tsx @@ -1,4 +1,5 @@ import logMessage from 'helpers/errors/logMessage'; +import { CanonicalQueryParameters } from 'helpers/seo/generateCanonicalUrl'; import useSeo from 'hooks/seo/useSeo'; import { useDomainConfig } from 'hooks/useDomainConfig'; import Head from 'next/head'; @@ -8,14 +9,16 @@ import { useEffect, useState } from 'react'; type SeoMetaProps = { defaultTitle?: string | null; defaultDescription?: string | null; + canonicalQueryParams?: CanonicalQueryParameters; }; -export const SeoMeta: FC = ({ defaultTitle, defaultDescription }) => { +export const SeoMeta: FC = ({ defaultTitle, defaultDescription, canonicalQueryParams }) => { const [areMissingRequiredTagsReported, setAreMissingRequiredTagsReported] = useState(false); const { title, titleSuffix, description, ogTitle, ogDescription, ogImageUrl, canonicalUrl } = useSeo({ defaultTitle, defaultDescription, + canonicalQueryParams, }); const currentUri = useRouter().asPath; @@ -23,7 +26,7 @@ export const SeoMeta: FC = ({ defaultTitle, defaultDescription }) const currentUrlWithDomain = url.substring(0, url.length - 1) + currentUri; useEffect(() => { - if (title === null && !areMissingRequiredTagsReported) { + if (!title && !areMissingRequiredTagsReported) { logMessage('Missing required tags', [ { key: 'tags', @@ -37,13 +40,11 @@ export const SeoMeta: FC = ({ defaultTitle, defaultDescription }) return ( {`${title} ${titleSuffix}`} - {description !== null && } - {ogTitle !== null && } - {ogDescription !== null && } - {ogImageUrl !== null && } - {canonicalUrl !== null && canonicalUrl !== currentUrlWithDomain && ( - - )} + {description && } + {ogTitle && } + {ogDescription && } + {ogImageUrl && } + {canonicalUrl && canonicalUrl !== currentUrlWithDomain && } ); }; diff --git a/storefront/components/Layout/CommonLayout.tsx b/storefront/components/Layout/CommonLayout.tsx index 511082c1cc..4d7deb3252 100644 --- a/storefront/components/Layout/CommonLayout.tsx +++ b/storefront/components/Layout/CommonLayout.tsx @@ -9,17 +9,26 @@ import { SeoMeta } from 'components/Basic/Head/SeoMeta'; import { Adverts } from 'components/Blocks/Adverts/Adverts'; import { Breadcrumbs } from './Breadcrumbs/Breadcrumbs'; import { FriendlyPagesTypesKeys } from 'types/friendlyUrl'; +import { CanonicalQueryParameters } from 'helpers/seo/generateCanonicalUrl'; type CommonLayoutProps = { title?: string | null; description?: string | null; breadcrumbs?: BreadcrumbFragmentApi[]; breadcrumbsType?: FriendlyPagesTypesKeys; + canonicalQueryParams?: CanonicalQueryParameters; }; -export const CommonLayout: FC = ({ children, description, title, breadcrumbs, breadcrumbsType }) => ( +export const CommonLayout: FC = ({ + children, + description, + title, + breadcrumbs, + breadcrumbsType, + canonicalQueryParams, +}) => ( <> - + diff --git a/storefront/helpers/queryParamNames.ts b/storefront/helpers/queryParamNames.ts index 5401994096..0cf69ecb4f 100644 --- a/storefront/helpers/queryParamNames.ts +++ b/storefront/helpers/queryParamNames.ts @@ -4,10 +4,3 @@ export const LOAD_MORE_QUERY_PARAMETER_NAME = 'lm' as const; export const FILTER_QUERY_PARAMETER_NAME = 'filter' as const; export const SORT_QUERY_PARAMETER_NAME = 'sort' as const; export const SLUG_TYPE_QUERY_PARAMETER_NAME = 'slugType' as const; -export const INTERNAL_QUERY_PARAMETERS = [ - SEARCH_QUERY_PARAMETER_NAME, - PAGE_QUERY_PARAMETER_NAME, - FILTER_QUERY_PARAMETER_NAME, - SORT_QUERY_PARAMETER_NAME, - SLUG_TYPE_QUERY_PARAMETER_NAME, -]; diff --git a/storefront/helpers/seo/generateCanonicalUrl.ts b/storefront/helpers/seo/generateCanonicalUrl.ts index 5d3840b3ad..95cfeedbbf 100644 --- a/storefront/helpers/seo/generateCanonicalUrl.ts +++ b/storefront/helpers/seo/generateCanonicalUrl.ts @@ -1,34 +1,50 @@ -import { - getQueryWithoutSlugTypeParameterFromParsedUrlQuery, - getUrlWithoutGetParameters, -} from 'helpers/parsing/urlParsing'; +import { getUrlWithoutGetParameters } from 'helpers/parsing/urlParsing'; import { getStringWithoutTrailingSlash } from 'helpers/parsing/stringWIthoutSlash'; -import { INTERNAL_QUERY_PARAMETERS } from 'helpers/queryParamNames'; import { NextRouter } from 'next/router'; +import { + PAGE_QUERY_PARAMETER_NAME, + SEARCH_QUERY_PARAMETER_NAME, + FILTER_QUERY_PARAMETER_NAME, + SORT_QUERY_PARAMETER_NAME, +} from 'helpers/queryParamNames'; + +const DEFAULT_CANONICAL_QUERY_PARAMS = [ + PAGE_QUERY_PARAMETER_NAME, + SEARCH_QUERY_PARAMETER_NAME, + FILTER_QUERY_PARAMETER_NAME, + SORT_QUERY_PARAMETER_NAME, +] as const; + +export type CanonicalQueryParameters = (typeof DEFAULT_CANONICAL_QUERY_PARAMS)[number][]; -export const generateCanonicalUrl = (router: NextRouter, url: string): string | null => { +export const generateCanonicalUrl = ( + router: NextRouter, + url: string, + canonicalQueryParams?: CanonicalQueryParameters, +): string | null => { const newQueryOverwrite: Record = {}; - const queryWithoutAllParameter = getQueryWithoutSlugTypeParameterFromParsedUrlQuery(router.query); + const queries = router.query; - for (const queryParam in queryWithoutAllParameter) { - if ((INTERNAL_QUERY_PARAMETERS as string[]).includes(queryParam)) { - const queryParamValue = queryWithoutAllParameter[queryParam]?.toString(); + for (const queryParam in queries) { + if ((canonicalQueryParams || DEFAULT_CANONICAL_QUERY_PARAMS).includes(queryParam as any)) { + const queryParamValue = queries[queryParam]?.toString(); - if (queryParamValue !== undefined) { + if (queryParamValue) { newQueryOverwrite[queryParam] = queryParamValue; } } } - if (JSON.stringify(newQueryOverwrite) === JSON.stringify(queryWithoutAllParameter)) { + if (JSON.stringify(newQueryOverwrite) === JSON.stringify(queries)) { return null; } const queryParams = new URLSearchParams(newQueryOverwrite).toString(); + const canonicalUrl = `${getStringWithoutTrailingSlash(url)}${getUrlWithoutGetParameters(router.asPath)}`; - if (queryParams.length === 0) { - return `${getStringWithoutTrailingSlash(url)}${getUrlWithoutGetParameters(router.asPath)}`; + if (!queryParams.length) { + return canonicalUrl; } - return `${getStringWithoutTrailingSlash(url)}${getUrlWithoutGetParameters(router.asPath)}?${queryParams}`; + return `${canonicalUrl}?${queryParams}`; }; diff --git a/storefront/hooks/seo/useSeo.ts b/storefront/hooks/seo/useSeo.ts index fc7ebb16be..ee2b1dcb4f 100644 --- a/storefront/hooks/seo/useSeo.ts +++ b/storefront/hooks/seo/useSeo.ts @@ -1,26 +1,27 @@ import { useSeoPageQueryApi, useSettingsQueryApi } from 'graphql/generated'; import { extractSeoPageSlugFromUrl } from 'helpers/seo/extractSeoPageSlugFromUrl'; -import { generateCanonicalUrl } from 'helpers/seo/generateCanonicalUrl'; +import { CanonicalQueryParameters, generateCanonicalUrl } from 'helpers/seo/generateCanonicalUrl'; import { useDomainConfig } from 'hooks/useDomainConfig'; import { useRouter } from 'next/router'; import { useMemo } from 'react'; type UseSeoHookReturn = { - title: string | null; - titleSuffix: string | null; - description: string | null; - ogTitle: string | null; - ogDescription: string | null; - ogImageUrl: string | null; - canonicalUrl: string | null; + title: string | null | undefined; + titleSuffix: string | null | undefined; + description: string | null | undefined; + ogTitle: string | null | undefined; + ogDescription: string | null | undefined; + ogImageUrl: string | null | undefined; + canonicalUrl: string | null | undefined; }; type UseSeoHookProps = { defaultTitle?: string | null; defaultDescription?: string | null; + canonicalQueryParams?: CanonicalQueryParameters; }; -const useSeo = ({ defaultTitle, defaultDescription }: UseSeoHookProps): UseSeoHookReturn => { +const useSeo = ({ defaultTitle, defaultDescription, canonicalQueryParams }: UseSeoHookProps): UseSeoHookReturn => { const { url } = useDomainConfig(); const router = useRouter(); @@ -33,23 +34,21 @@ const useSeo = ({ defaultTitle, defaultDescription }: UseSeoHookProps): UseSeoHo variables: { pageSlug: pageSlug!, }, - pause: pageSlug === null, + pause: !pageSlug, }); - const preferredTitle = seoPageData?.seoPage?.title ?? null; - const preferredDescription = seoPageData?.seoPage?.metaDescription ?? null; - const preferredCanonicalUrl = seoPageData?.seoPage?.canonicalUrl ?? null; - const preferredOgTitle = seoPageData?.seoPage?.ogTitle ?? null; - const preferredOgDescription = seoPageData?.seoPage?.ogDescription ?? null; - const preferredOgImageUrl = seoPageData?.seoPage?.ogImage?.sizes[0]?.url ?? null; + const preferredTitle = seoPageData?.seoPage?.title; + const preferredDescription = seoPageData?.seoPage?.metaDescription; + const preferredCanonicalUrl = seoPageData?.seoPage?.canonicalUrl; + const preferredOgTitle = seoPageData?.seoPage?.ogTitle; + const preferredOgDescription = seoPageData?.seoPage?.ogDescription; + const preferredOgImageUrl = seoPageData?.seoPage?.ogImage?.sizes[0]?.url; - const fallbackTitle = settingsData?.settings?.seo.title ?? null; - const fallbackDescription = settingsData?.settings?.seo.metaDescription ?? null; - const fallbackTitleSuffix = settingsData?.settings?.seo.titleAddOn ?? null; + const fallbackTitle = settingsData?.settings?.seo.title; + const fallbackDescription = settingsData?.settings?.seo.metaDescription; + const fallbackTitleSuffix = settingsData?.settings?.seo.titleAddOn; - const canonicalUrl = useMemo(() => { - return preferredCanonicalUrl ?? generateCanonicalUrl(router, url); - }, [router, url, preferredCanonicalUrl]); + const canonicalUrl = preferredCanonicalUrl || generateCanonicalUrl(router, url, canonicalQueryParams); return { title: preferredTitle ?? defaultTitle ?? fallbackTitle, diff --git a/storefront/pages/articles/[articleSlug].tsx b/storefront/pages/articles/[articleSlug].tsx index 849fde92b3..fad0090b9d 100644 --- a/storefront/pages/articles/[articleSlug].tsx +++ b/storefront/pages/articles/[articleSlug].tsx @@ -36,6 +36,7 @@ const ArticleDetailPage: NextPage = () => { title={article?.seoTitle} description={article?.seoMetaDescription} breadcrumbs={article?.breadcrumb} + canonicalQueryParams={[]} > {!!article && !fetching ? : } diff --git a/storefront/pages/blogArticles/[blogArticleSlug].tsx b/storefront/pages/blogArticles/[blogArticleSlug].tsx index 542149c40c..40261f1fbb 100644 --- a/storefront/pages/blogArticles/[blogArticleSlug].tsx +++ b/storefront/pages/blogArticles/[blogArticleSlug].tsx @@ -35,6 +35,7 @@ const BlogArticleDetailPage: NextPage = () => { description={blogArticleData?.blogArticle?.seoMetaDescription} breadcrumbs={blogArticleData?.blogArticle?.breadcrumb} breadcrumbsType="blogCategory" + canonicalQueryParams={[]} > {!!blogArticleData?.blogArticle && !fetching ? ( diff --git a/storefront/pages/products/[productSlug].tsx b/storefront/pages/products/[productSlug].tsx index 2cb2e21932..a4c55b5cac 100644 --- a/storefront/pages/products/[productSlug].tsx +++ b/storefront/pages/products/[productSlug].tsx @@ -39,6 +39,7 @@ const ProductDetailPage: NextPage = () => { description={product?.seoMetaDescription} breadcrumbs={product?.breadcrumb} breadcrumbsType="category" + canonicalQueryParams={[]} > {fetching && } diff --git a/storefront/pages/stores/[storeSlug].tsx b/storefront/pages/stores/[storeSlug].tsx index ef9d6b0b25..f3a6205e7b 100644 --- a/storefront/pages/stores/[storeSlug].tsx +++ b/storefront/pages/stores/[storeSlug].tsx @@ -28,7 +28,11 @@ const StoreDetailPage: NextPage = () => { useGtmPageViewEvent(pageViewEvent, fetching); return ( - + {!!storeDetailData?.store && !fetching ? ( ) : (