a18c0a8cf7
- 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.
234 lines
8.0 KiB
TypeScript
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 },
|
|
],
|
|
};
|
|
};
|