Initial SvelteKit frontend port of windwiderstand.de
Deploy / verify (push) Failing after 32s
Deploy / deploy (push) Has been skipped

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:
Peter Meier
2026-04-17 22:01:30 +02:00
parent 852cef0980
commit 2fef91a548
137 changed files with 28585 additions and 0 deletions
+238
View File
@@ -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,
};
};