import type { PageServerLoad } from './$types'; import { getPostBySlug, getPageConfigBySlug, getPageConfigs } from '$lib/cms'; import { resolveContentImages, ensureTransformedImage, processMarkdownHtmlImages, } from '$lib/rusty-image'; import { resolvePostTagsInPost, resolvePostOverviewBlocks, resolveSearchableTextBlocks, resolveDeadlineBannerBlocks, getPostImageField, formatPostDate, } from '$lib/blog-utils'; import { POST_RESOLVE, POST_SOCIAL_IMAGE_TRANSFORM, SITE_NAME } from '$lib/constants'; import { env } from '$env/dynamic/public'; import { error } from '@sveltejs/kit'; import { marked } from 'marked'; marked.setOptions({ gfm: true }); export const load: PageServerLoad = async ({ params, locals, url }) => { const { slug } = params; const translations = locals.translations ?? {}; const post = await getPostBySlug(slug, { locale: 'de', resolve: POST_RESOLVE, }); if (!post) { error(404, 'Beitrag nicht gefunden'); } await resolveContentImages(post); const tagsMap = await resolvePostOverviewBlocks(post); await resolveSearchableTextBlocks(post, tagsMap); await resolveDeadlineBannerBlocks(post); resolvePostTagsInPost(post, tagsMap); const postImageField = getPostImageField(post.postImage); const postImageUrl = postImageField ? await ensureTransformedImage(postImageField.url, { width: 150, height: 200, fit: 'cover', focalX: postImageField.focal?.x, focalY: postImageField.focal?.y, }) : null; const siteBaseUrl = (env.PUBLIC_SITE_URL || '').replace(/\/$/, ''); const canonical = `${siteBaseUrl}/post/${slug}/`; const postSocialImageRel = postImageField ? await ensureTransformedImage(postImageField.url, { ...POST_SOCIAL_IMAGE_TRANSFORM, focalX: postImageField.focal?.x, focalY: postImageField.focal?.y, }) : null; const postSocialImageAbs = postSocialImageRel ? postSocialImageRel.startsWith('/') ? `${siteBaseUrl}${postSocialImageRel}` : postSocialImageRel : null; const postDate = formatPostDate(post.created); const postContent = (post as { content?: string }).content; let contentHtml = typeof postContent === 'string' && postContent.trim() ? (marked.parse(postContent) as string) : ''; if (contentHtml) { contentHtml = await processMarkdownHtmlImages(contentHtml); } let contentHeadlineHtml = ''; let contentBodyHtml = contentHtml; const h1Match = contentHtml.match(/^\s*(]*>[\s\S]*?<\/h1>)([\s\S]*)$/); if (h1Match) { contentHeadlineHtml = h1Match[1]; contentBodyHtml = h1Match[2]; } const hasRowContent = ((post as { row1Content?: unknown[] }).row1Content?.length ?? 0) > 0 || ((post as { row2Content?: unknown[] }).row2Content?.length ?? 0) > 0 || ((post as { row3Content?: unknown[] }).row3Content?.length ?? 0) > 0; const siteName = (import.meta.env.PUBLIC_SITE_NAME as string | undefined) || SITE_NAME; const postCreated = (post as { created?: string }).created ?? null; const postUpdated = (post as { updated?: string }).updated ?? postCreated; const postAuthor = (post as { author?: string }).author ?? null; const postHeadline = post.headline ?? post.linkName ?? slug; const postDescription = post.seoDescription ?? post.subheadline ?? post.excerpt ?? ''; const isEvent = !!(post as { isEvent?: boolean }).isEvent; const eventDateRaw = (post as { eventDate?: string }).eventDate ?? null; const eventDateLabel = isEvent && eventDateRaw ? new Date(eventDateRaw).toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', }) : null; const eventLocation = isEvent ? ((post as { eventLocation?: { text?: string; lat?: number; lng?: number } }).eventLocation ?? null) : null; const hasCoords = !!(eventLocation?.lat && eventLocation?.lng); const _lat = eventLocation?.lat ?? 0; const _lng = eventLocation?.lng ?? 0; const mapEmbedUrl = hasCoords ? `https://www.openstreetmap.org/export/embed.html?bbox=${_lng - 0.01},${_lat - 0.007},${_lng + 0.01},${_lat + 0.007}&layer=mapnik&marker=${_lat},${_lng}` : null; const mapLinkUrl = hasCoords ? `https://www.openstreetmap.org/?mlat=${eventLocation!.lat}&mlon=${eventLocation!.lng}#map=15/${eventLocation!.lat}/${eventLocation!.lng}` : eventLocation?.text ? `https://www.openstreetmap.org/search?query=${encodeURIComponent(eventLocation.text)}` : null; const pageConfig = (await getPageConfigBySlug('page-config-default', { locale: 'de' })) ?? (await getPageConfigs({ locale: 'de', per_page: 1 }))[0] ?? null; const commentSlotRaw = (pageConfig as { postCommentSlot?: string } | null)?.postCommentSlot ?? ''; const commentSlot = commentSlotRaw .replace(/\{\{PAGE_ID\}\}/g, slug) .replace(/\{\{PAGE_URL\}\}/g, url.href) .replace(/\{\{PAGE_TITLE\}\}/g, post.headline ?? post.linkName ?? slug); const showCommentSection = (post as { showCommentSection?: boolean }).showCommentSection !== false; const jsonLd: Record[] = []; const articleLd: Record = { '@context': 'https://schema.org', '@type': isEvent ? 'Article' : 'BlogPosting', headline: postHeadline, mainEntityOfPage: { '@type': 'WebPage', '@id': canonical }, url: canonical, inLanguage: 'de-DE', ...(postDescription ? { description: postDescription } : {}), ...(postCreated ? { datePublished: postCreated } : {}), ...(postUpdated ? { dateModified: postUpdated } : {}), ...(postSocialImageAbs ? { image: postSocialImageAbs } : {}), author: { '@type': postAuthor ? 'Person' : 'Organization', name: postAuthor ?? siteName, }, publisher: { '@type': 'Organization', name: siteName, ...(siteBaseUrl ? { url: siteBaseUrl } : {}), }, }; jsonLd.push(articleLd); if (isEvent && eventDateRaw) { const eventLd: Record = { '@context': 'https://schema.org', '@type': 'Event', name: postHeadline, startDate: eventDateRaw, eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode', eventStatus: 'https://schema.org/EventScheduled', url: canonical, inLanguage: 'de-DE', ...(postDescription ? { description: postDescription } : {}), ...(postSocialImageAbs ? { image: postSocialImageAbs } : {}), organizer: { '@type': 'Organization', name: siteName, ...(siteBaseUrl ? { url: siteBaseUrl } : {}), }, ...(eventLocation?.text ? { location: { '@type': 'Place', name: eventLocation.text, ...(hasCoords ? { geo: { '@type': 'GeoCoordinates', latitude: eventLocation.lat, longitude: eventLocation.lng, }, } : {}), }, } : {}), }; jsonLd.push(eventLd); } return { post, postImageUrl, rawPostImageUrl: postImageField?.url ?? null, postImageFocal: postImageField?.focal ?? null, postDate, contentHtml, contentHeadlineHtml, contentBodyHtml, hasRowContent, isEvent, eventDateRaw, eventDateLabel, eventLocationText: eventLocation?.text ?? null, mapEmbedUrl, mapLinkUrl, commentSlot: showCommentSection ? commentSlot : '', translations, seoTitle: post.seoTitle ?? postHeadline, seoDescription: postDescription, socialImage: postSocialImageAbs ?? postImageUrl ?? undefined, robots: (post as { seoMetaRobots?: string }).seoMetaRobots ?? undefined, keywords: (post as { seoKeywords?: string }).seoKeywords ?? undefined, jsonLd, breadcrumbItems: [ { href: '/', label: 'Start' }, { href: '/posts/', label: 'Beiträge' }, { label: post.headline ?? post.linkName ?? post.slug ?? post._slug ?? slug }, ], }; };