Initial SvelteKit frontend port of windwiderstand.de
Full parity with Astro site: content rows, post/tag routes, pagination, event badges + OSM map, comments, Live-Search via /api/search-index, CMS image proxy, RSS, sitemap. Deploy: Dockerfile + docker-compose.prod.yml + Gitea Actions workflow to build + push to git.pm86.de registry and ssh-deploy to Contabo.
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user