import type { ArticlesBlock, BannerBlock, BannerStyle, Breadcrumb, ContentBlock, FaqBlock, LanguageAlternative, Media, MediaBlock, MediaContentBlock, NavigationLink, NavigationLinkVariant, PageBlock, PageDocument, PageHero, PageRequest, PageResolution, PageTeaser, PageTeaserList, RichText, SitemapEntry, SiteMenu, SiteSettings, UnknownBlock } from '../types'; const LINK_VARIANTS = new Set([ 'primary', 'secondary', 'outline', 'ghost', 'inline' ]); const HERO_IMPACTS = new Set(['none', 'low', 'middle', 'high']); const BANNER_STYLES = new Set(['info', 'success', 'warning', 'error']); export function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function asString(value: unknown, fallback = ''): string { return typeof value === 'string' ? value : fallback; } function asOptionalString(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 ? value : undefined; } function asOptionalNumber(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) ? value : undefined; } function normalizePath(value: string): string { return value.split('/').filter(Boolean).join('/'); } function normalizeRichText(value: unknown): RichText { if (isRecord(value)) { return { content: asString(value['content']) }; } return { content: asString(value) }; } function normalizeMedia(value: unknown): Media | undefined { if (!isRecord(value)) { return undefined; } const url = asString(value['url']); if (!url) { return undefined; } const media: Media = { url, alt: asString(value['alt']) }; const width = asOptionalNumber(value['width']); const height = asOptionalNumber(value['height']); const mimeType = asOptionalString(value['mimeType']); if (width !== undefined) media.width = width; if (height !== undefined) media.height = height; if (mimeType !== undefined) media.mimeType = mimeType; return media; } function normalizeNavigationLink(value: unknown): NavigationLink | null { if (!isRecord(value)) { return null; } const label = asString(value['label']); if (!label) { return null; } const rawVariant = asString(value['variant']); const variant = LINK_VARIANTS.has(rawVariant as NavigationLinkVariant) ? (rawVariant as NavigationLinkVariant) : 'ghost'; const rawLinkType = asString(value['linkType']); const linkType = rawLinkType === 'internal' ? 'internal' : 'external'; const rawPageId = value['pageId']; const link: NavigationLink = { linkType, pageId: typeof rawPageId === 'number' || typeof rawPageId === 'string' ? String(rawPageId) : '', externalUrl: asString(value['externalUrl']), openInNewTab: value['openInNewTab'] === true, label, variant }; const href = asOptionalString(value['href'] ?? value['url'] ?? value['path']); if (href !== undefined) link.href = href; return link; } function normalizeNavigationLinks(value: unknown): NavigationLink[] { if (!Array.isArray(value)) { return []; } return value .map((item) => normalizeNavigationLink(item)) .filter((item): item is NavigationLink => item !== null); } function normalizeBreadcrumbs(value: unknown): Breadcrumb[] { if (!Array.isArray(value)) { return []; } return value.filter(isRecord).flatMap((item) => { const label = asString(item['label']); const href = asString(item['href']); return label ? [{ label, href }] : []; }); } function normalizeHero(value: unknown): PageHero | undefined { if (!isRecord(value)) { return undefined; } const rawImpact = asString(value['type']); const type = HERO_IMPACTS.has(rawImpact) ? (rawImpact as PageHero['type']) : 'middle'; const hero: PageHero = { type, ctaItems: normalizeNavigationLinks(value['ctaItems']) }; const title = asOptionalString(value['title']); const subtitle = asOptionalString(value['subtitle']); const content = normalizeRichText(value['content']); const media = normalizeMedia(value['media']); if (title !== undefined) hero.title = title; if (subtitle !== undefined) hero.subtitle = subtitle; if (content.content) hero.content = content; if (media !== undefined) hero.media = media; return hero; } function unwrapBlock(value: Record): Record { const data = value['data']; if (!isRecord(data)) { return value; } return { ...data, id: value['id'] ?? data['id'], type: value['type'] ?? data['type'] }; } function normalizeBlock(value: unknown, index: number): PageBlock | null { if (!isRecord(value)) { return null; } const block = unwrapBlock(value); const type = asString(block['type']); if (!type) { return null; } const id = String(block['id'] ?? `${type}-${index + 1}`); if (type === 'content') { const columns = Array.isArray(block['columns']) ? block['columns'].filter(isRecord).map((column) => ({ content: normalizeRichText(column['content']), ctaItems: normalizeNavigationLinks(column['ctaItems']) })) : []; return { id, type, title: asString(block['title']), subtitle: asString(block['subtitle']), columns } satisfies ContentBlock; } if (type === 'faq') { const items = Array.isArray(block['items']) ? block['items'].filter(isRecord).map((item) => ({ question: asString(item['question']), answer: asString(item['answer']) })) : []; return { id, type, title: asString(block['title']), subtitle: asString(block['subtitle']), items } satisfies FaqBlock; } if (type === 'media-content') { return { id, type, title: asString(block['title']), mediaPosition: block['mediaPosition'] === 'right' ? 'right' : 'left', media: normalizeMedia(block['media']), content: normalizeRichText(block['content']), ctaItems: normalizeNavigationLinks(block['ctaItems']) } satisfies MediaContentBlock; } if (type === 'articles') { const items = Array.isArray(block['items']) ? block['items'].filter(isRecord).map((item) => ({ title: asString(item['title']), content: normalizeRichText(item['content']) })) : []; const cta = normalizeNavigationLink(block['cta']); const articlesBlock: ArticlesBlock = { id, type, title: asString(block['title']), subtitle: normalizeRichText(block['subtitle']), items }; if (cta !== null) articlesBlock.cta = cta; return articlesBlock; } if (type === 'banner') { const rawStyle = asString(block['style']); return { id, type, style: BANNER_STYLES.has(rawStyle as BannerStyle) ? (rawStyle as BannerStyle) : 'info', content: normalizeRichText(block['content']), ctaItems: normalizeNavigationLinks(block['ctaItems']) } satisfies BannerBlock; } if (type === 'media') { return { id, type, media: normalizeMedia(block['media']) } satisfies MediaBlock; } return { id, type, data: block } satisfies UnknownBlock; } function normalizeBlocks(value: unknown): PageBlock[] { if (!Array.isArray(value)) { return []; } return value .map((block, index) => normalizeBlock(block, index)) .filter((block): block is PageBlock => block !== null); } function normalizeLayout(value: unknown, fallbackValue: unknown): string { if (typeof value === 'string') { return value; } if (isRecord(value)) { return asString(value['type'], 'landing'); } return asString(fallbackValue, 'landing'); } export function normalizePage(value: unknown, request: PageRequest): PageDocument | null { if (!isRecord(value)) { return null; } const hasPageIdentity = value['id'] !== undefined || typeof value['path'] === 'string' || typeof value['title'] === 'string'; if (!hasPageIdentity) { return null; } const path = normalizePath(asString(value['path'], request.path)); const finalPathSegment = path.split('/').at(-1) ?? ''; const locale = asString(value['locale'], request.locale); const hero = normalizeHero(value['hero']); const seoValue = isRecord(value['seo']) ? value['seo'] : {}; const title = asString(value['title'], hero?.title ?? finalPathSegment); const page: PageDocument = { id: String(value['id'] ?? `${locale}:${path || 'index'}`), status: asString(value['status'], 'published'), locale, path, groupKey: asString(value['groupKey'], finalPathSegment), layout: normalizeLayout(value['layout'], value['pageType']), title, seo: { title: asString(seoValue['title'] ?? value['seoTitle'], title), description: asString(seoValue['description'] ?? value['seoDescription']) }, breadcrumbs: normalizeBreadcrumbs(value['breadcrumbs']), blocks: normalizeBlocks(value['blocks'] ?? value['components']), publishedAt: asOptionalString(value['publishedAt']) ?? null, updatedAt: asOptionalString(value['updatedAt']) ?? null }; if (hero !== undefined) page.hero = hero; return page; } function normalizeMenus(value: unknown): SiteMenu[] { if (!Array.isArray(value)) { return []; } return value.filter(isRecord).map((menu) => ({ title: asString(menu['title']).trim(), items: normalizeNavigationLinks(menu['items']) })); } export function createEmptySiteSettings(locale: string): SiteSettings { return { locale, header: { menus: [], ctaItems: [] }, footer: { copyright: '', menus: [] }, legal: { privacyPath: '', termsPath: '' } }; } export function normalizeSiteSettings(value: unknown, locale: string): SiteSettings | null { if (!isRecord(value)) { return null; } const header = isRecord(value['header']) ? value['header'] : {}; const footer = isRecord(value['footer']) ? value['footer'] : {}; const legal = isRecord(value['legal']) ? value['legal'] : {}; return { locale: asString(value['locale'], locale), header: { menus: normalizeMenus(header['menus']), ctaItems: normalizeNavigationLinks(header['ctaItems']) }, footer: { copyright: asString(footer['copyright']), menus: normalizeMenus(footer['menus']) }, legal: { privacyPath: asString(legal['privacyPath']), termsPath: asString(legal['termsPath']) } }; } function normalizeLanguageAlternatives(value: unknown): LanguageAlternative[] { if (!Array.isArray(value)) { return []; } return value.filter(isRecord).flatMap((item) => { const locale = asString(item['locale']); const path = normalizePath(asString(item['path'])); return locale ? [{ locale, path }] : []; }); } export function normalizePageResolution( value: unknown, request: PageRequest ): PageResolution | null { if (!isRecord(value)) { return null; } const root = isRecord(value['data']) ? value['data'] : value; const wrappedPage = isRecord(root['page']) ? root['page'] : null; const rawPage = wrappedPage ? { ...wrappedPage, blocks: root['components'] ?? wrappedPage['blocks'], hero: wrappedPage['hero'] ?? root['hero'], breadcrumbs: root['breadcrumbs'] ?? wrappedPage['breadcrumbs'] } : root; const page = normalizePage(rawPage, request); if (!page) { return null; } const resolution: PageResolution = { page, languageAlternatives: normalizeLanguageAlternatives(root['languageAlternatives']) }; const settings = normalizeSiteSettings(root['settings'], request.locale); if (settings) resolution.settings = settings; return resolution; } export function normalizeSitemapEntries(payload: unknown): SitemapEntry[] { if (!isRecord(payload) || !Array.isArray(payload['items'])) { return []; } return payload['items'].filter(isRecord).flatMap((item) => { const locale = asString(item['locale']); if (!locale) { return []; } return [ { locale, path: normalizePath(asString(item['path'])), groupKey: asString(item['groupKey']), pageType: asString(item['pageType']), publishedAt: asOptionalString(item['publishedAt']) ?? null, updatedAt: asOptionalString(item['updatedAt']) ?? null } ]; }); } export function sitemapLastPage(payload: unknown): number { const pagination = isRecord(payload) && isRecord(payload['pagination']) ? payload['pagination'] : {}; const lastPage = pagination['lastPage']; return typeof lastPage === 'number' && Number.isInteger(lastPage) && lastPage > 0 ? lastPage : 1; } export function normalizeRoutePath(path: string): string { return normalizePath(path); } export function normalizePageTeaserList(payload: unknown): PageTeaserList { const items = normalizePageTeasers(payload); const rawPagination = isRecord(payload) && isRecord(payload['pagination']) ? payload['pagination'] : {}; const asPositiveInt = (value: unknown, fallback: number): number => { return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback; }; return { items, pagination: { page: asPositiveInt(rawPagination['page'], 1), perPage: asPositiveInt(rawPagination['perPage'], Math.max(items.length, 1)), total: asPositiveInt(rawPagination['total'], items.length), lastPage: asPositiveInt(rawPagination['lastPage'], 1) } }; } export function normalizePageTeasers(payload: unknown): PageTeaser[] { if (!isRecord(payload) || !Array.isArray(payload['items'])) { return []; } return payload['items'].filter(isRecord).flatMap((item) => { const title = asString(item['title']); const path = normalizePath(asString(item['path'])); if (!title || !path) { return []; } const teaser: PageTeaser = { id: String(item['id'] ?? path), path, title, description: asString(item['description']), publishedAt: asOptionalString(item['publishedAt']) ?? null }; const media = normalizeMedia(item['media']); if (media !== undefined) teaser.media = media; return [teaser]; }); }