feat(poc): add basic cms pages

This commit is contained in:
Björn Meyer 2023-07-07 15:13:16 +02:00
parent a598b3f1e5
commit f61c41f24a
9 changed files with 111 additions and 48 deletions

View File

@ -11,9 +11,9 @@ export const revalidate = 43200; // 12 hours in seconds
export async function generateMetadata({ export async function generateMetadata({
params params
}: { }: {
params: { page: string }; params: { cms: string };
}): Promise<Metadata> { }): Promise<Metadata> {
const page = await getPage(params.page); const page = await getPage(params.cms);
if (!page) return notFound(); if (!page) return notFound();
@ -35,8 +35,8 @@ export async function generateMetadata({
}; };
} }
export default async function Page({ params }: { params: { page: string } }) { export default async function Page({ params }: { params: { cms: string } }) {
const page = await getPage(params.page); const page = await getPage(params.cms);
if (!page) return notFound(); if (!page) return notFound();
let date = page.createdAt; let date = page.createdAt;

View File

@ -56,7 +56,7 @@ export default async function Footer() {
</nav> </nav>
))} ))}
<div className="col-span-1 text-black dark:text-white lg:col-span-2 inline-grid justify-items-end"> <div className="col-span-1 text-black dark:text-white lg:col-span-2 inline-grid justify-items-end">
<a aria-label="Github Repository" href="https://github.com/vercel/commerce"> <a aria-label="Github Repository" href="https://github.com/shopware/frontends" target="_blank" rel="noopener noreferrer">
<GitHubIcon className="h-6" /> <GitHubIcon className="h-6" />
</a> </a>
</div> </div>

View File

@ -950,7 +950,7 @@ export type components = {
position: number; position: number;
sectionId: string; sectionId: string;
sectionPosition?: string; sectionPosition?: string;
slots?: components['schemas']['CmsSlot']; slots?: components['schemas']['CmsSlot'][];
type: string; type: string;
/** Format: date-time */ /** Format: date-time */
updatedAt?: string; updatedAt?: string;
@ -993,7 +993,7 @@ export type components = {
name?: string; name?: string;
previewMedia?: components['schemas']['Media']; previewMedia?: components['schemas']['Media'];
previewMediaId?: string; previewMediaId?: string;
sections?: components['schemas']['CmsSection']; sections?: components['schemas']['CmsSection'][];
translated?: { translated?: {
cssClass?: string; cssClass?: string;
entity?: string; entity?: string;
@ -1029,7 +1029,7 @@ export type components = {
backgroundMedia?: components['schemas']['Media']; backgroundMedia?: components['schemas']['Media'];
backgroundMediaId?: string; backgroundMediaId?: string;
backgroundMediaMode?: string; backgroundMediaMode?: string;
blocks?: components['schemas']['CmsBlock']; blocks?: components['schemas']['CmsBlock'][];
cmsPageVersionId?: string; cmsPageVersionId?: string;
/** Format: date-time */ /** Format: date-time */
createdAt: string; createdAt: string;

View File

@ -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) { export function getStaticCollectionCriteria(page: number = 1, limit: number = 20) {
return { return {
page: page, page: page,

View File

@ -8,6 +8,7 @@ import {
} from './types'; } from './types';
import { import {
getDefaultCategoryCriteria, getDefaultCategoryCriteria,
getDefaultCategoryWithCmsCriteria,
getDefaultCrossSellingCriteria, getDefaultCrossSellingCriteria,
getDefaultProductCriteria, getDefaultProductCriteria,
getDefaultProductsCriteria, getDefaultProductsCriteria,
@ -26,29 +27,31 @@ import {
requestSeoUrl requestSeoUrl
} from './api'; } from './api';
import { import {
transformCollection,
transformHandle,
transformMenu, transformMenu,
transformPage, transformPage,
transformProduct, transformProduct,
transformProducts, transformProducts,
transformCollection,
transformStaticCollection transformStaticCollection
} from './transform'; } from './transform';
export async function getMenu(params?: { type?: StoreNavigationTypeSW, depth?: number }): Promise<Menu[]> { export async function getMenu(params?: {
type?: StoreNavigationTypeSW;
depth?: number;
}): Promise<Menu[]> {
const type = params?.type || 'main-navigation'; const type = params?.type || 'main-navigation';
const depth = params?.depth || 1; const depth = params?.depth || 1;
const res = await requestNavigation(type, depth); const res = await requestNavigation(type, depth);
return res ? transformMenu(res) : []; return res ? transformMenu(res, type) : [];
} }
export async function getPage(handle: string): Promise<Page | undefined> { export async function getPage(handle: string | []): Promise<Page | undefined> {
const seoUrlElement = await getFirstSeoUrlElement(handle); const pageHandle = transformHandle(handle).replace('cms/', '');
const seoUrlElement = await getFirstSeoUrlElement(pageHandle);
if (seoUrlElement) { if (seoUrlElement) {
const resCategory = await requestCategory( const resCategory = await getCategory(seoUrlElement);
seoUrlElement.foreignKey,
getDefaultCategoryCriteria()
);
return resCategory ? transformPage(seoUrlElement, resCategory) : undefined; return resCategory ? transformPage(seoUrlElement, resCategory) : undefined;
} }
@ -107,15 +110,11 @@ export async function getCollectionProducts(params?: {
defaultSearchCriteria?: Partial<ProductListingCriteria>; defaultSearchCriteria?: Partial<ProductListingCriteria>;
}): Promise<Product[]> { }): Promise<Product[]> {
let res; let res;
let collectionName: string | [] | undefined = params?.collection;
let category = params?.categoryId; let category = params?.categoryId;
const collectionName = transformHandle(params?.collection ?? '');
const sorting = getSortingCriteria(params?.sortKey, params?.reverse); const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
if (!category && collectionName) { if (!category && collectionName !== '') {
// this part is important for the catch all collection handle/slug
if (Array.isArray(collectionName)) {
collectionName = collectionName.join('/');
}
const seoUrlElement = await getFirstSeoUrlElement(collectionName); const seoUrlElement = await getFirstSeoUrlElement(collectionName);
if (seoUrlElement) { if (seoUrlElement) {
category = seoUrlElement.foreignKey; category = seoUrlElement.foreignKey;
@ -139,18 +138,22 @@ export async function getCollectionProducts(params?: {
return res ? transformProducts(res) : []; return res ? transformProducts(res) : [];
} }
export async function getCategory(
seoUrl: ApiSchemas['SeoUrl'],
cms: boolean = false
): Promise<ApiSchemas['Category']> {
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 // This function is only used for generateMetadata at app/search/(collection)/[...collection]/page.tsx
export async function getCollection(handle: string | []) { export async function getCollection(handle: string | []) {
let collectionName: string | [] | undefined = handle; const collectionName = transformHandle(handle);
if (Array.isArray(collectionName)) {
collectionName = collectionName.join('/');
}
const seoUrlElement = await getFirstSeoUrlElement(collectionName); const seoUrlElement = await getFirstSeoUrlElement(collectionName);
if (seoUrlElement) { if (seoUrlElement) {
const resCategory = await requestCategory( const resCategory = await getCategory(seoUrlElement);
seoUrlElement.foreignKey,
getDefaultCategoryCriteria()
);
const path = seoUrlElement.seoPathInfo ?? ''; const path = seoUrlElement.seoPathInfo ?? '';
const collection = transformCollection(seoUrlElement, resCategory); const collection = transformCollection(seoUrlElement, resCategory);
@ -164,10 +167,7 @@ export async function getCollection(handle: string | []) {
export async function getProduct(handle: string | []): Promise<Product | undefined> { export async function getProduct(handle: string | []): Promise<Product | undefined> {
let productSW: ApiSchemas['Product'] | undefined; let productSW: ApiSchemas['Product'] | undefined;
let productId: string | undefined; let productId: string | undefined;
let productHandle: string | [] | undefined = handle; const productHandle = transformHandle(handle);
if (Array.isArray(productHandle)) {
productHandle = productHandle.join('/');
}
const seoUrlElement = await getFirstSeoUrlElement(productHandle); const seoUrlElement = await getFirstSeoUrlElement(productHandle);
if (seoUrlElement) { if (seoUrlElement) {

View File

@ -3,28 +3,32 @@ import {
CategoryListingResultSW, CategoryListingResultSW,
Collection, Collection,
Menu, Menu,
Page,
Product, Product,
ProductOption, ProductOption,
ProductVariant ProductVariant
} from './types'; } from './types';
import { ListItem } from 'components/layout/search/filter'; import { ListItem } from 'components/layout/search/filter';
export function transformMenu(res: ApiSchemas['NavigationRouteResponse']) { export function transformMenu(res: ApiSchemas['NavigationRouteResponse'], type: string) {
let menu: Menu[] = []; let menu: Menu[] = [];
res.map((item) => menu.push(transformMenuItem(item))); res.map((item) => menu.push(transformMenuItem(item, type)));
return menu; 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 { return {
id: item.id ?? '', id: item.id ?? '',
title: item.name, title: item.name,
children: item.children?.map((item) => transformMenuItem(item)) ?? [], children: item.children?.map((item) => transformMenuItem(item, type)) ?? [],
path: path:
item.seoUrls && item.seoUrls.length > 0 && item.seoUrls[0] && item.seoUrls[0].seoPathInfo 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' type: item.children && item.children.length > 0 ? 'headline' : 'link'
}; };
@ -32,25 +36,52 @@ function transformMenuItem(item: ApiSchemas['Category']): Menu {
export function transformPage( export function transformPage(
seoUrlElement: ApiSchemas['SeoUrl'], 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 { return {
id: seoUrlElement.id ?? '', id: seoUrlElement.id ?? '',
title: resCategory.translated?.metaTitle ?? resCategory.name ?? '', title: category.translated?.metaTitle ?? category.name ?? '',
handle: seoUrlElement.seoPathInfo, handle: seoUrlElement.seoPathInfo,
body: resCategory.description ?? '', body: plainHtmlContent ?? category.description ?? '',
bodySummary: resCategory.translated?.metaDescription ?? resCategory.description ?? '', bodySummary: category.translated?.metaDescription ?? category.description ?? '',
seo: { seo: {
title: resCategory.translated?.metaTitle ?? resCategory.name ?? '', title: category.translated?.metaTitle ?? category.name ?? '',
description: resCategory.translated?.metaDescription ?? resCategory.description ?? '' description: category.translated?.metaDescription ?? category.description ?? ''
}, },
createdAt: seoUrlElement.createdAt ?? '', createdAt: seoUrlElement.createdAt ?? '',
updatedAt: seoUrlElement.updatedAt ?? '', updatedAt: seoUrlElement.updatedAt ?? '',
routeName: seoUrlElement.routeName, routeName: seoUrlElement.routeName,
originalCmsPage: category.cmsPage,
foreignKey: seoUrlElement.foreignKey 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( export function transformCollection(
seoUrlElement: ApiSchemas['SeoUrl'], seoUrlElement: ApiSchemas['SeoUrl'],
resCategory: ApiSchemas['Category'] resCategory: ApiSchemas['Category']
@ -234,3 +265,12 @@ function transformVariants(parent: ApiSchemas['Product']): ProductVariant[] {
return productVariants; return productVariants;
} }
export function transformHandle(handle: string | []): string {
let collectionName: string | [] | undefined = handle;
if (Array.isArray(collectionName)) {
collectionName = collectionName.join('/');
}
return collectionName ?? '';
}

View File

@ -18,7 +18,7 @@ export type SeoURLResultSW = {
} & ApiSchemas['EntitySearchResult']; } & ApiSchemas['EntitySearchResult'];
/** Vercel Commerce Types */ /** 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 = { export type Page = {
id: string; id: string;
@ -31,6 +31,7 @@ export type Page = {
updatedAt: string; updatedAt: string;
routeName?: string; routeName?: string;
foreignKey?: string; foreignKey?: string;
originalCmsPage?: ApiSchemas['CmsPage'];
}; };
export type ProductOption = { export type ProductOption = {