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
+106
View File
@@ -0,0 +1,106 @@
import type { RequestHandler } from './$types';
import { env } from '$env/dynamic/public';
import {
buildCacheKey,
getCachedImage,
cacheImage,
type ImageTransformParams,
} from '$lib/image-cache.server';
const IMMUTABLE_HEADERS = {
'cache-control': 'public, max-age=31536000, immutable',
} as const;
function getCmsBaseUrl(): string {
return (
env.PUBLIC_CMS_URL ||
import.meta.env.PUBLIC_CMS_URL ||
'http://localhost:3000'
).replace(/\/$/, '');
}
/**
* Wenn src kein vollständiger URL ist, wird er als CMS-Asset-Dateiname behandelt
* und zu {CMS_URL}/api/assets/{src} aufgelöst.
* Absolute Pfade (z.B. /api/assets/logo/logo3.svg) werden direkt an die CMS-URL angehängt.
*/
function resolveSourceUrl(src: string): string {
if (src.startsWith('http://') || src.startsWith('https://')) return src;
if (src.startsWith('//')) return `https:${src}`;
if (src.startsWith('/')) return `${getCmsBaseUrl()}${src}`;
return `${getCmsBaseUrl()}/api/assets/${encodeURIComponent(src)}`;
}
export const GET: RequestHandler = async ({ url }) => {
const src = url.searchParams.get('src');
if (!src) {
return new Response('Missing "src" parameter', { status: 400 });
}
const params: ImageTransformParams = {};
const w = url.searchParams.get('w');
const h = url.searchParams.get('h');
const q = url.searchParams.get('q');
const format = url.searchParams.get('format');
const fit = url.searchParams.get('fit');
const ar = url.searchParams.get('ar');
if (w) {
const n = Number(w);
if (Number.isFinite(n)) params.w = n;
}
if (h) {
const n = Number(h);
if (Number.isFinite(n)) params.h = n;
}
if (q) {
const n = Number(q);
if (Number.isFinite(n)) params.q = n;
}
if (format) params.format = format;
if (fit) params.fit = fit;
if (ar) params.ar = ar;
const key = buildCacheKey(src, params);
const cached = await getCachedImage(key, params);
if (cached) {
return new Response(new Uint8Array(cached.buffer), {
headers: { 'content-type': cached.contentType, ...IMMUTABLE_HEADERS },
});
}
const sourceUrl = resolveSourceUrl(src);
const cmsBase = getCmsBaseUrl();
const transformParams = new URLSearchParams();
transformParams.set('url', sourceUrl);
if (params.w != null) transformParams.set('w', String(params.w));
if (params.h != null) transformParams.set('h', String(params.h));
if (params.q != null) transformParams.set('quality', String(params.q));
if (params.format) transformParams.set('format', params.format);
if (params.fit) transformParams.set('fit', params.fit);
if (params.ar) transformParams.set('ar', params.ar);
const transformUrl = `${cmsBase}/api/transform?${transformParams.toString()}`;
let res: Response;
try {
res = await fetch(transformUrl);
} catch (err) {
console.error('[cms-images] CMS fetch failed:', err);
return new Response('CMS not reachable', { status: 502 });
}
if (!res.ok) {
return new Response('Image transform failed', { status: res.status });
}
const buffer = await res.arrayBuffer();
const contentType = res.headers.get('content-type') ?? 'image/jpeg';
const result = await cacheImage(key, buffer, contentType);
return new Response(new Uint8Array(result.buffer), {
headers: { 'content-type': result.contentType, ...IMMUTABLE_HEADERS },
});
};