diff --git a/app/[page]/page.tsx b/app/(cms)/[...cms]/page.tsx similarity index 86% rename from app/[page]/page.tsx rename to app/(cms)/[...cms]/page.tsx index 52eefdb06..b7f6d8e72 100644 --- a/app/[page]/page.tsx +++ b/app/(cms)/[...cms]/page.tsx @@ -11,9 +11,9 @@ export const revalidate = 43200; // 12 hours in seconds export async function generateMetadata({ params }: { - params: { page: string }; + params: { cms: string }; }): Promise { - const page = await getPage(params.page); + const page = await getPage(params.cms); if (!page) return notFound(); @@ -35,8 +35,8 @@ export async function generateMetadata({ }; } -export default async function Page({ params }: { params: { page: string } }) { - const page = await getPage(params.page); +export default async function Page({ params }: { params: { cms: string } }) { + const page = await getPage(params.cms); if (!page) return notFound(); let date = page.createdAt; diff --git a/app/[page]/layout.tsx b/app/(cms)/layout.tsx similarity index 100% rename from app/[page]/layout.tsx rename to app/(cms)/layout.tsx diff --git a/app/[page]/opengraph-image.tsx b/app/(cms)/opengraph-image.tsx similarity index 100% rename from app/[page]/opengraph-image.tsx rename to app/(cms)/opengraph-image.tsx diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx index 3d884cac4..dc17c763d 100644 --- a/components/layout/footer.tsx +++ b/components/layout/footer.tsx @@ -56,7 +56,7 @@ export default async function Footer() { ))}
- +
diff --git a/lib/shopware/api-types/apiTypes-6.5.2.0.d.ts b/lib/shopware/api-types/apiTypes-6.5.2.0.d.ts index b223d1509..2bc67f1e3 100644 --- a/lib/shopware/api-types/apiTypes-6.5.2.0.d.ts +++ b/lib/shopware/api-types/apiTypes-6.5.2.0.d.ts @@ -950,7 +950,7 @@ export type components = { position: number; sectionId: string; sectionPosition?: string; - slots?: components['schemas']['CmsSlot']; + slots?: components['schemas']['CmsSlot'][]; type: string; /** Format: date-time */ updatedAt?: string; @@ -993,7 +993,7 @@ export type components = { name?: string; previewMedia?: components['schemas']['Media']; previewMediaId?: string; - sections?: components['schemas']['CmsSection']; + sections?: components['schemas']['CmsSection'][]; translated?: { cssClass?: string; entity?: string; @@ -1029,7 +1029,7 @@ export type components = { backgroundMedia?: components['schemas']['Media']; backgroundMediaId?: string; backgroundMediaMode?: string; - blocks?: components['schemas']['CmsBlock']; + blocks?: components['schemas']['CmsBlock'][]; cmsPageVersionId?: string; /** Format: date-time */ createdAt: string; diff --git a/lib/shopware/criteria.ts b/lib/shopware/criteria.ts index 52b41e929..8b323186f 100644 --- a/lib/shopware/criteria.ts +++ b/lib/shopware/criteria.ts @@ -85,6 +85,28 @@ export function getDefaultCategoryCriteria(page: number = 1, limit: number = 1) }; } +export function getDefaultCategoryWithCmsCriteria(page: number = 1, limit: number = 1) { + return { + page: page, + limit: limit, + associations: { + cmsPage: { + associations: { + sections: { + associations: { + blocks: { + associations: { + slots: {} + } + } + } + } + } + } + } + }; +} + export function getStaticCollectionCriteria(page: number = 1, limit: number = 20) { return { page: page, diff --git a/lib/shopware/index.ts b/lib/shopware/index.ts index 4b54e6806..54e90652f 100644 --- a/lib/shopware/index.ts +++ b/lib/shopware/index.ts @@ -8,6 +8,7 @@ import { } from './types'; import { getDefaultCategoryCriteria, + getDefaultCategoryWithCmsCriteria, getDefaultCrossSellingCriteria, getDefaultProductCriteria, getDefaultProductsCriteria, @@ -26,29 +27,31 @@ import { requestSeoUrl } from './api'; import { + transformCollection, + transformHandle, transformMenu, transformPage, transformProduct, transformProducts, - transformCollection, transformStaticCollection } from './transform'; -export async function getMenu(params?: { type?: StoreNavigationTypeSW, depth?: number }): Promise { +export async function getMenu(params?: { + type?: StoreNavigationTypeSW; + depth?: number; +}): Promise { const type = params?.type || 'main-navigation'; const depth = params?.depth || 1; const res = await requestNavigation(type, depth); - return res ? transformMenu(res) : []; + return res ? transformMenu(res, type) : []; } -export async function getPage(handle: string): Promise { - const seoUrlElement = await getFirstSeoUrlElement(handle); +export async function getPage(handle: string | []): Promise { + const pageHandle = transformHandle(handle).replace('cms/', ''); + const seoUrlElement = await getFirstSeoUrlElement(pageHandle); if (seoUrlElement) { - const resCategory = await requestCategory( - seoUrlElement.foreignKey, - getDefaultCategoryCriteria() - ); + const resCategory = await getCategory(seoUrlElement); return resCategory ? transformPage(seoUrlElement, resCategory) : undefined; } @@ -107,15 +110,11 @@ export async function getCollectionProducts(params?: { defaultSearchCriteria?: Partial; }): Promise { let res; - let collectionName: string | [] | undefined = params?.collection; let category = params?.categoryId; + const collectionName = transformHandle(params?.collection ?? ''); const sorting = getSortingCriteria(params?.sortKey, params?.reverse); - if (!category && collectionName) { - // this part is important for the catch all collection handle/slug - if (Array.isArray(collectionName)) { - collectionName = collectionName.join('/'); - } + if (!category && collectionName !== '') { const seoUrlElement = await getFirstSeoUrlElement(collectionName); if (seoUrlElement) { category = seoUrlElement.foreignKey; @@ -139,18 +138,22 @@ export async function getCollectionProducts(params?: { return res ? transformProducts(res) : []; } +export async function getCategory( + seoUrl: ApiSchemas['SeoUrl'], + cms: boolean = false +): Promise { + const criteria = cms ? getDefaultCategoryWithCmsCriteria() : getDefaultCategoryCriteria(); + const resCategory = await requestCategory(seoUrl.foreignKey, criteria); + + return resCategory; +} + // This function is only used for generateMetadata at app/search/(collection)/[...collection]/page.tsx export async function getCollection(handle: string | []) { - let collectionName: string | [] | undefined = handle; - if (Array.isArray(collectionName)) { - collectionName = collectionName.join('/'); - } + const collectionName = transformHandle(handle); const seoUrlElement = await getFirstSeoUrlElement(collectionName); if (seoUrlElement) { - const resCategory = await requestCategory( - seoUrlElement.foreignKey, - getDefaultCategoryCriteria() - ); + const resCategory = await getCategory(seoUrlElement); const path = seoUrlElement.seoPathInfo ?? ''; const collection = transformCollection(seoUrlElement, resCategory); @@ -164,10 +167,7 @@ export async function getCollection(handle: string | []) { export async function getProduct(handle: string | []): Promise { let productSW: ApiSchemas['Product'] | undefined; let productId: string | undefined; - let productHandle: string | [] | undefined = handle; - if (Array.isArray(productHandle)) { - productHandle = productHandle.join('/'); - } + const productHandle = transformHandle(handle); const seoUrlElement = await getFirstSeoUrlElement(productHandle); if (seoUrlElement) { diff --git a/lib/shopware/transform.ts b/lib/shopware/transform.ts index 97f9202a5..9bff11aad 100644 --- a/lib/shopware/transform.ts +++ b/lib/shopware/transform.ts @@ -3,28 +3,32 @@ import { CategoryListingResultSW, Collection, Menu, + Page, Product, ProductOption, ProductVariant } from './types'; import { ListItem } from 'components/layout/search/filter'; -export function transformMenu(res: ApiSchemas['NavigationRouteResponse']) { +export function transformMenu(res: ApiSchemas['NavigationRouteResponse'], type: string) { let menu: Menu[] = []; - res.map((item) => menu.push(transformMenuItem(item))); + res.map((item) => menu.push(transformMenuItem(item, type))); return menu; } -function transformMenuItem(item: ApiSchemas['Category']): Menu { +function transformMenuItem(item: ApiSchemas['Category'], type: string): Menu { + // @ToDo: currently only footer-navigation is used for cms pages, this need to be more dynamic (shoud depending on the item) return { id: item.id ?? '', title: item.name, - children: item.children?.map((item) => transformMenuItem(item)) ?? [], + children: item.children?.map((item) => transformMenuItem(item, type)) ?? [], path: item.seoUrls && item.seoUrls.length > 0 && item.seoUrls[0] && item.seoUrls[0].seoPathInfo - ? '/search/' + item.seoUrls[0].seoPathInfo + ? type === 'footer-navigation' + ? '/cms/' + item.seoUrls[0].seoPathInfo + : '/search/' + item.seoUrls[0].seoPathInfo : '', type: item.children && item.children.length > 0 ? 'headline' : 'link' }; @@ -32,25 +36,52 @@ function transformMenuItem(item: ApiSchemas['Category']): Menu { export function transformPage( seoUrlElement: ApiSchemas['SeoUrl'], - resCategory: ApiSchemas['Category'] -) { + category: ApiSchemas['Category'] +): Page { + let plainHtmlContent; + if (category.cmsPage) { + const cmsPage: ApiSchemas['CmsPage'] = category.cmsPage; + plainHtmlContent = transformToPlainHtmlContent(cmsPage); + } + return { id: seoUrlElement.id ?? '', - title: resCategory.translated?.metaTitle ?? resCategory.name ?? '', + title: category.translated?.metaTitle ?? category.name ?? '', handle: seoUrlElement.seoPathInfo, - body: resCategory.description ?? '', - bodySummary: resCategory.translated?.metaDescription ?? resCategory.description ?? '', + body: plainHtmlContent ?? category.description ?? '', + bodySummary: category.translated?.metaDescription ?? category.description ?? '', seo: { - title: resCategory.translated?.metaTitle ?? resCategory.name ?? '', - description: resCategory.translated?.metaDescription ?? resCategory.description ?? '' + title: category.translated?.metaTitle ?? category.name ?? '', + description: category.translated?.metaDescription ?? category.description ?? '' }, createdAt: seoUrlElement.createdAt ?? '', updatedAt: seoUrlElement.updatedAt ?? '', routeName: seoUrlElement.routeName, + originalCmsPage: category.cmsPage, foreignKey: seoUrlElement.foreignKey }; } +export function transformToPlainHtmlContent(cmsPage: ApiSchemas['CmsPage']): string { + let plainHtmlContent = ''; + + cmsPage.sections?.map((section) => { + section.blocks?.map((block) => { + block.slots?.map((slot) => { + if (slot.slot === 'content' && slot.config?.content) { + const currentContent: string = slot.config.content.value + ''; + // we do not add content with h1, because will be added via template already + if (!currentContent.match(/(<\/?h)([1])/)) { + plainHtmlContent += currentContent; + } + } + }); + }); + }); + + return plainHtmlContent; +} + export function transformCollection( seoUrlElement: ApiSchemas['SeoUrl'], resCategory: ApiSchemas['Category'] @@ -234,3 +265,12 @@ function transformVariants(parent: ApiSchemas['Product']): ProductVariant[] { return productVariants; } + +export function transformHandle(handle: string | []): string { + let collectionName: string | [] | undefined = handle; + if (Array.isArray(collectionName)) { + collectionName = collectionName.join('/'); + } + + return collectionName ?? ''; +} diff --git a/lib/shopware/types.ts b/lib/shopware/types.ts index 12be345b1..a561ba948 100644 --- a/lib/shopware/types.ts +++ b/lib/shopware/types.ts @@ -18,7 +18,7 @@ export type SeoURLResultSW = { } & ApiSchemas['EntitySearchResult']; /** Vercel Commerce Types */ -export type Menu = { id: string; title: string; path: string, type: string, children: Menu[] }; +export type Menu = { id: string; title: string; path: string; type: string; children: Menu[] }; export type Page = { id: string; @@ -31,6 +31,7 @@ export type Page = { updatedAt: string; routeName?: string; foreignKey?: string; + originalCmsPage?: ApiSchemas['CmsPage']; }; export type ProductOption = {