da340530ee
- +layout.server: expose pageConfig.analyticsPm86SiteId on layout data - +layout.svelte: inject analytics.pm86.de script when CMS value present - +error.svelte: light 404 page with wald-700 accent, dropped dark gradient - .env.example + deploy.yml: remove PUBLIC_ANALYTICS_PM86_SITE_ID
245 lines
7.5 KiB
TypeScript
245 lines
7.5 KiB
TypeScript
import type { LayoutServerLoad } from './$types';
|
|
|
|
export const trailingSlash = 'always';
|
|
|
|
import {
|
|
getNavigationByKey,
|
|
NavigationKeys,
|
|
getPages,
|
|
getPosts,
|
|
getFooterBySlug,
|
|
getPageConfigBySlug,
|
|
} from '$lib/cms';
|
|
import type { NavLinkEntry } from '$lib/cms';
|
|
import { ensureTransformedImage } from '$lib/rusty-image';
|
|
import {
|
|
DEFAULT_SOCIAL_IMAGE_URL,
|
|
ROW_RESOLVE,
|
|
SITE_NAME,
|
|
SOCIAL_IMAGE_TRANSFORM,
|
|
} from '$lib/constants';
|
|
import { env } from '$env/dynamic/public';
|
|
|
|
interface NavLink {
|
|
href: string;
|
|
label: string;
|
|
}
|
|
|
|
interface SocialLink {
|
|
href: string;
|
|
icon: string;
|
|
label?: string;
|
|
}
|
|
|
|
/** Führenden Slash ignorieren (CMS liefert z. B. "slug": "/about"). */
|
|
function normalizeSlug(s: string | undefined): string {
|
|
return (s ?? '').replace(/^\//, '').trim();
|
|
}
|
|
|
|
function getSlugFromEntry(entry: NavLinkEntry): string {
|
|
if (typeof entry === 'string') return entry;
|
|
const e = entry as { _slug?: string; slug?: string };
|
|
return e.slug ?? e._slug ?? '';
|
|
}
|
|
|
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
|
const translations = locals.translations ?? {};
|
|
|
|
// Header: Navigation laden
|
|
let headerLinks: NavLink[] = [];
|
|
try {
|
|
const nav = await getNavigationByKey(NavigationKeys.header, {
|
|
locale: 'de',
|
|
resolve: 'links',
|
|
});
|
|
const rawLinks = nav?.links ?? [];
|
|
const [pages, posts] = await Promise.all([getPages(), getPosts()]);
|
|
const slugToLabel = new Map<string, string>();
|
|
const slugToPageHref = new Map<string, string>();
|
|
for (const p of pages) {
|
|
const label = p.linkName ?? p.name ?? p.headline ?? p.slug ?? p._slug ?? '';
|
|
const canonicalSlug = normalizeSlug(p.slug ?? p._slug) || 'home';
|
|
const keySlug = normalizeSlug(p.slug);
|
|
const keyUnderscore = normalizeSlug(p._slug);
|
|
if (keySlug) {
|
|
slugToLabel.set(keySlug, label);
|
|
slugToPageHref.set(keySlug, canonicalSlug);
|
|
}
|
|
if (keyUnderscore && keyUnderscore !== keySlug) {
|
|
slugToLabel.set(keyUnderscore, label);
|
|
slugToPageHref.set(keyUnderscore, canonicalSlug);
|
|
}
|
|
}
|
|
const slugToPostHref = new Map<string, string>();
|
|
for (const p of posts) {
|
|
const label = p.linkName ?? p.headline ?? p.slug ?? p._slug ?? '';
|
|
const urlSlug = normalizeSlug(p.slug ?? p._slug);
|
|
const canonical = urlSlug ? `/post/${encodeURIComponent(urlSlug)}/` : '';
|
|
const keySlug = normalizeSlug(p.slug);
|
|
const keyUnderscore = normalizeSlug(p._slug);
|
|
if (keySlug) {
|
|
slugToLabel.set(keySlug, label);
|
|
slugToPostHref.set(keySlug, canonical);
|
|
}
|
|
if (keyUnderscore && keyUnderscore !== keySlug) {
|
|
slugToLabel.set(keyUnderscore, label);
|
|
slugToPostHref.set(keyUnderscore, canonical);
|
|
}
|
|
}
|
|
for (const entry of rawLinks) {
|
|
const asObj =
|
|
typeof entry === 'object' && entry !== null
|
|
? (entry as { url?: string; linkName?: string; name?: string })
|
|
: null;
|
|
if (asObj?.url) {
|
|
const href =
|
|
asObj.url.startsWith('/') || asObj.url.startsWith('http')
|
|
? asObj.url
|
|
: `/${asObj.url.replace(/^\//, '')}`;
|
|
const label = asObj.linkName ?? asObj.name ?? asObj.url ?? '';
|
|
headerLinks.push({ href, label });
|
|
continue;
|
|
}
|
|
const raw = getSlugFromEntry(entry);
|
|
const slug = normalizeSlug(raw) || 'home';
|
|
const label = slugToLabel.get(slug) ?? slug;
|
|
const postHref = slugToPostHref.get(slug);
|
|
const pageSlug = slugToPageHref.get(slug) ?? slug;
|
|
const hrefSlug = normalizeSlug(pageSlug) || 'home';
|
|
const pageHref =
|
|
hrefSlug === 'home'
|
|
? '/'
|
|
: '/' +
|
|
hrefSlug
|
|
.split('/')
|
|
.filter(Boolean)
|
|
.map((seg) => encodeURIComponent(seg))
|
|
.join('/') +
|
|
'/';
|
|
const href = postHref ?? pageHref;
|
|
headerLinks.push({ href, label });
|
|
}
|
|
} catch {
|
|
// CMS nicht erreichbar
|
|
}
|
|
|
|
// Social-Media-Links
|
|
let socialLinks: SocialLink[] = [];
|
|
try {
|
|
const socialNav = await getNavigationByKey(NavigationKeys.socialMedia, {
|
|
locale: 'de',
|
|
resolve: 'all',
|
|
});
|
|
const rawSocialLinks = socialNav?.links ?? [];
|
|
for (const entry of rawSocialLinks) {
|
|
const asObj =
|
|
typeof entry === 'object' && entry !== null
|
|
? (entry as { url?: string; icon?: string; linkName?: string; name?: string })
|
|
: null;
|
|
if (!asObj?.url) continue;
|
|
const href =
|
|
asObj.url.startsWith('/') || asObj.url.startsWith('http')
|
|
? asObj.url
|
|
: `/${asObj.url.replace(/^\//, '')}`;
|
|
const icon = asObj.icon ?? 'mdi:link';
|
|
const label = asObj.linkName ?? asObj.name ?? '';
|
|
socialLinks.push({ href, icon, label });
|
|
}
|
|
} catch {
|
|
/* Social-Links optional */
|
|
}
|
|
|
|
// Footer laden (mit Row-Resolve)
|
|
let footerData = null;
|
|
try {
|
|
footerData = await getFooterBySlug('footer-main', {
|
|
locale: 'de',
|
|
resolve: ROW_RESOLVE,
|
|
});
|
|
} catch {
|
|
// CMS nicht erreichbar
|
|
}
|
|
|
|
// Logo und SEO-Templates aus page_config
|
|
let logoUrl: string | null = null;
|
|
let logoSvgHtml: string | null = null;
|
|
let pageConfig = null;
|
|
try {
|
|
pageConfig =
|
|
(await getPageConfigBySlug('page-config-default', {
|
|
locale: 'de',
|
|
resolve: 'logo',
|
|
})) ??
|
|
(await getPageConfigBySlug('default', { locale: 'de', resolve: 'logo' }));
|
|
const rawLogo = pageConfig?.logo;
|
|
const rawSrc =
|
|
typeof rawLogo === 'string'
|
|
? rawLogo
|
|
: (rawLogo as { src?: string } | undefined)?.src ?? null;
|
|
if (rawSrc) {
|
|
const cmsBase = (env.PUBLIC_CMS_URL || 'http://localhost:3000').replace(/\/$/, '');
|
|
// Absolute URL aufbauen
|
|
const u = rawSrc.startsWith('//')
|
|
? `https:${rawSrc}`
|
|
: rawSrc.startsWith('/')
|
|
? `${cmsBase}${rawSrc}`
|
|
: rawSrc.startsWith('http')
|
|
? rawSrc
|
|
: `${cmsBase}/api/assets/${rawSrc}`;
|
|
const isSvg = u.split('?')[0].toLowerCase().endsWith('.svg');
|
|
if (isSvg) {
|
|
// SVGs können nicht transformiert werden — direkt verwenden und inline einbetten
|
|
logoUrl = u;
|
|
try {
|
|
const res = await fetch(u);
|
|
if (res.ok) {
|
|
const text = (await res.text()).trim();
|
|
if (text.includes('<svg')) logoSvgHtml = text;
|
|
}
|
|
} catch { /* Fallback: img mit logoUrl */ }
|
|
} else {
|
|
logoUrl = await ensureTransformedImage(u, {
|
|
height: 56,
|
|
fit: 'contain',
|
|
format: 'webp',
|
|
});
|
|
}
|
|
}
|
|
} catch {
|
|
/* Logo optional */
|
|
}
|
|
|
|
const siteName =
|
|
(import.meta.env.PUBLIC_SITE_NAME as string | undefined) || SITE_NAME;
|
|
const siteBaseUrl = (env.PUBLIC_SITE_URL || '').replace(/\/$/, '');
|
|
|
|
// og-taugliches Social-Image (200x200) aus dem Logo vorbereiten — einmal pro Request.
|
|
let logoSocialImage: string | null = null;
|
|
try {
|
|
const source = logoUrl ?? DEFAULT_SOCIAL_IMAGE_URL;
|
|
const transformed = await ensureTransformedImage(source, SOCIAL_IMAGE_TRANSFORM);
|
|
logoSocialImage = transformed.startsWith('/')
|
|
? `${siteBaseUrl}${transformed}`
|
|
: transformed;
|
|
} catch {
|
|
logoSocialImage = null;
|
|
}
|
|
|
|
return {
|
|
headerLinks,
|
|
socialLinks,
|
|
footerData,
|
|
logoUrl,
|
|
logoSvgHtml,
|
|
logoSocialImage,
|
|
siteName,
|
|
siteBaseUrl,
|
|
translations,
|
|
pageConfig,
|
|
seoTitleTemplate: pageConfig?.seoTitle ?? null,
|
|
seoDescriptionTemplate: pageConfig?.seoDescription ?? null,
|
|
analyticsPm86SiteId:
|
|
(pageConfig as { analyticsPm86SiteId?: string } | null)?.analyticsPm86SiteId ?? null,
|
|
};
|
|
};
|