diff --git a/packages/api-client/src/api/cmsPage/cmsPage.ts b/packages/api-client/src/api/cmsPage/cmsPage.ts index db59d6814..f31a61f7f 100644 --- a/packages/api-client/src/api/cmsPage/cmsPage.ts +++ b/packages/api-client/src/api/cmsPage/cmsPage.ts @@ -5,9 +5,10 @@ export default gql` cmsPage(identifier:$identifier) { identifier content - title, + title meta_title meta_description + meta_keywords content_heading } } diff --git a/packages/theme/helpers/getMetaInfo.ts b/packages/theme/helpers/getMetaInfo.ts new file mode 100644 index 000000000..3816aa5cc --- /dev/null +++ b/packages/theme/helpers/getMetaInfo.ts @@ -0,0 +1,37 @@ +import type { MetaInfo } from 'vue-meta'; + +export const getMetaInfo = (page: any, isNoIndex: boolean = false): MetaInfo => { + if (!page) { + return null; + } + + const seoTags: MetaInfo = { + meta: [], + }; + + if (page?.meta_title || page?.title || page?.name) { + seoTags.title = page?.meta_title || page?.title || page?.name; + } + if (page?.meta_description) { + seoTags.meta.push({ + hid: 'description', + name: 'description', + content: page.meta_description, + }); + } + if (page?.meta_keyword || page?.meta_keywords) { + seoTags.meta.push({ + hid: 'keywords', + name: 'keywords', + content: page?.meta_keyword || page?.meta_keywords, + }); + } + if (isNoIndex) { + seoTags.meta.push({ + name: 'robots', + content: 'noindex, nofollow', + }); + } + + return seoTags; +}; diff --git a/packages/theme/modules/catalog/category/composables/useCategory/categoryMeta.gql.ts b/packages/theme/modules/catalog/category/composables/useCategory/categoryMeta.gql.ts new file mode 100644 index 000000000..f51ec2764 --- /dev/null +++ b/packages/theme/modules/catalog/category/composables/useCategory/categoryMeta.gql.ts @@ -0,0 +1,12 @@ +export default ` + query categoryMeta($filters: CategoryFilterInput) { + categories(filters: $filters) { + items { + meta_title + meta_description + meta_keywords + name + } + } + } +`; diff --git a/packages/theme/modules/catalog/category/composables/useCategory/index.ts b/packages/theme/modules/catalog/category/composables/useCategory/index.ts index 6500e50cb..269a02254 100644 --- a/packages/theme/modules/catalog/category/composables/useCategory/index.ts +++ b/packages/theme/modules/catalog/category/composables/useCategory/index.ts @@ -1,10 +1,12 @@ import { Ref, ref, useContext } from '@nuxtjs/composition-api'; import { Logger } from '~/helpers/logger'; import type{ CategoryTree } from '~/modules/GraphQL/types'; +import categoryMetaGql from '~/modules/catalog/category/composables/useCategory/categoryMeta.gql'; import type { UseCategoryErrors, UseCategoryInterface, UseCategoryParamsInput, + UseCategoryMetaParamsInput, } from './useCategory'; /** @@ -72,6 +74,7 @@ export function useCategory(): UseCategoryInterface { const loading: Ref = ref(false); const error: Ref = ref({ load: null, + loadCategoryMeta: null, }); const categories: Ref> = ref(null); @@ -92,8 +95,39 @@ export function useCategory(): UseCategoryInterface { } }; + const loadCategoryMeta = async (params: UseCategoryMetaParamsInput): Promise => { + Logger.debug('useCategory/loadCategoryMeta', params); + let categoryMeta = null; + + try { + loading.value = true; + + const { data } = await app.context.$vsf.$magento.api.customQuery({ + query: categoryMetaGql, + queryVariables: { + filters: { + category_uid: { + eq: params.category_uid, + }, + }, + }, + }); + Logger.debug('[Result]:', { data }); + categoryMeta = data.categoryList?.[0] || null; + error.value.loadCategoryMeta = null; + } catch (err) { + error.value.loadCategoryMeta = err; + Logger.error('useCategory/loadCategoryMeta', err); + } finally { + loading.value = false; + } + + return categoryMeta; + }; + return { load, + loadCategoryMeta, loading, error, categories, diff --git a/packages/theme/modules/catalog/category/composables/useCategory/useCategory.ts b/packages/theme/modules/catalog/category/composables/useCategory/useCategory.ts index ba546186b..afd558c23 100644 --- a/packages/theme/modules/catalog/category/composables/useCategory/useCategory.ts +++ b/packages/theme/modules/catalog/category/composables/useCategory/useCategory.ts @@ -34,6 +34,7 @@ import type{ CategoryTree } from '~/modules/GraphQL/types'; export interface UseCategoryErrors { /** Error when loading categories fails, otherwise is `null`. */ load: Error; + loadCategoryMeta: Error; } /** The {@link useCategory} params object received by `load` function. */ @@ -41,6 +42,11 @@ export type UseCategoryParamsInput = ComposableFunctionArgs< { pageSize: number; }>; +/** The {@link useCategory} params object received by `loadCategoryMeta` function. */ +export type UseCategoryMetaParamsInput = ComposableFunctionArgs< { + category_uid: string; +}>; + /** * Data and methods returned from the {@link useCategory} composable * */ @@ -87,4 +93,5 @@ export interface UseCategoryInterface { * ``` */ load(params: ComposableFunctionArgs): Promise; + loadCategoryMeta(params: ComposableFunctionArgs): Promise; } diff --git a/packages/theme/modules/catalog/pages/category.vue b/packages/theme/modules/catalog/pages/category.vue index cd0da0b94..9cda3c279 100644 --- a/packages/theme/modules/catalog/pages/category.vue +++ b/packages/theme/modules/catalog/pages/category.vue @@ -113,13 +113,18 @@ import { } from '@storefront-ui/vue'; import { computed, - defineComponent, onMounted, ref, ssrRef, useFetch, + defineComponent, + onMounted, + ref, + ssrRef, + useFetch, } from '@nuxtjs/composition-api'; import { CacheTagPrefix, useCache } from '@vue-storefront/cache'; import { usePageStore } from '~/stores/page'; import SkeletonLoader from '~/components/SkeletonLoader/index.vue'; import CategoryPagination from '~/modules/catalog/category/components/pagination/CategoryPagination.vue'; import { + useCategory, useFacet, useUiHelpers, useUiState, @@ -131,6 +136,7 @@ import { usePrice } from '~/modules/catalog/pricing/usePrice'; import { useCategoryContent } from '~/modules/catalog/category/components/cms/useCategoryContent'; import { useTraverseCategory } from '~/modules/catalog/category/helpers/useTraverseCategory'; import facetGetters from '~/modules/catalog/category/getters/facetGetters'; +import { getMetaInfo } from '~/helpers/getMetaInfo'; import CategoryNavbar from '~/modules/catalog/category/components/navbar/CategoryNavbar.vue'; import CategoryBreadcrumbs from '~/modules/catalog/category/components/breadcrumbs/CategoryBreadcrumbs.vue'; @@ -160,6 +166,7 @@ export default defineComponent({ setup() { const { routeData } = usePageStore(); const { getContentData } = useCategoryContent(); + const { loadCategoryMeta } = useCategory(); const { addTags } = useCache(); const uiHelpers = useUiHelpers(); const cmsContent = ref(''); @@ -186,6 +193,8 @@ export default defineComponent({ const { result, search } = useFacet(); const { addItemToCart } = useAddToCart(); + const categoryMeta = ref(null); + const addItemToWishlist = async (product: Product) => { await (isInWishlist({ product }) ? removeItemFromWishlist({ product }) @@ -202,11 +211,13 @@ export default defineComponent({ const categoryUid = routeData.uid; - const [content] = await Promise.all([ + const [content, categoryMetaData] = await Promise.all([ getContentData(categoryUid as string), + loadCategoryMeta({ category_uid: routeData.value?.uid }), search({ ...uiHelpers.getFacetsFromURL(), category_uid: categoryUid }), ]); + categoryMeta.value = categoryMetaData; cmsContent.value = content?.cmsBlock?.content ?? ''; isShowCms.value = content.isShowCms; isShowProducts.value = content.isShowProducts; @@ -273,10 +284,14 @@ export default defineComponent({ routeData, doChangeItemsPerPage, productContainerElement, + categoryMeta, onReloadProducts, goToPage, }; }, + head() { + return getMetaInfo(this.categoryMeta); + }, }); diff --git a/packages/theme/modules/catalog/pages/product.vue b/packages/theme/modules/catalog/pages/product.vue index 2f608278c..75fcc61bf 100644 --- a/packages/theme/modules/catalog/pages/product.vue +++ b/packages/theme/modules/catalog/pages/product.vue @@ -43,6 +43,7 @@ import { useCache, CacheTagPrefix } from '@vue-storefront/cache'; import { SfBreadcrumbs, SfLoader } from '@storefront-ui/vue'; import { getBreadcrumbs } from '~/modules/catalog/product/getters/productGetters'; import { useProduct } from '~/modules/catalog/product/composables/useProduct'; +import { getMetaInfo } from '~/helpers/getMetaInfo'; import { usePageStore } from '~/stores/page'; import { ProductTypeEnum } from '~/modules/catalog/product/enums/ProductTypeEnum'; import LoadWhenVisible from '~/components/utils/LoadWhenVisible.vue'; @@ -148,6 +149,9 @@ export default defineComponent({ fetchProduct: fetchProductExtendedData, }; }, + head() { + return getMetaInfo(this.product); + }, });