Files
windwiderstand/src/routes/post/[slug]/+page.server.ts
T
Peter Meier a18c0a8cf7
Deploy / verify (push) Successful in 45s
Deploy / deploy (push) Successful in 59s
feat(post-page): separate headline and body content for improved rendering
- Extracted headline and body content from the post's HTML, allowing for distinct rendering of the headline and body in the post page.
- Updated the Svelte component to display the extracted headline and body content separately, enhancing the layout and readability of posts.
2026-04-19 21:56:30 +02:00

234 lines
8.0 KiB
TypeScript

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*(<h1\b[^>]*>[\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<string, unknown>[] = [];
const articleLd: Record<string, unknown> = {
'@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<string, unknown> = {
'@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 },
],
};
};