feat: add FilesBlock interface and component integration
Deploy / verify (push) Successful in 46s
Deploy / deploy (push) Successful in 1m8s

- Introduced FilesBlockItem and FilesBlockData interfaces to support file handling in the CMS.
- Integrated FilesBlock component into ContentRows.svelte for rendering file blocks.
- Updated block type handling to include support for files, enhancing content versatility.
This commit is contained in:
Peter Meier
2026-04-22 10:23:20 +02:00
parent bedfe597ab
commit 5b5ec7c659
6 changed files with 481 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import type { RequestHandler } from './$types';
import { env } from '$env/dynamic/public';
import {
buildFileCacheKey,
cacheFile,
contentTypeFromExt,
extFromSrc,
getCachedFile,
} from '$lib/file-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(/\/$/, '');
}
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)}`;
}
function buildResponseHeaders(
contentType: string,
download: string | null,
filename?: string,
): HeadersInit {
const headers: Record<string, string> = {
'content-type': contentType,
...IMMUTABLE_HEADERS,
};
if (download === '1' || download === 'true') {
const safe = (filename ?? 'download').replace(/"/g, '');
headers['content-disposition'] = `attachment; filename="${safe}"`;
} else if (filename) {
const safe = filename.replace(/"/g, '');
headers['content-disposition'] = `inline; filename="${safe}"`;
}
return headers;
}
export const GET: RequestHandler = async ({ url }) => {
const src = url.searchParams.get('src');
if (!src) {
return new Response('Missing "src" parameter', { status: 400 });
}
const download = url.searchParams.get('dl');
const filenameParam = url.searchParams.get('name') ?? undefined;
const inferredName = (() => {
try {
const path = src.split('?')[0].split('#')[0];
const last = path.split('/').pop();
return last ? decodeURIComponent(last) : undefined;
} catch {
return undefined;
}
})();
const filename = filenameParam ?? inferredName;
const key = buildFileCacheKey(src);
const cached = await getCachedFile(key);
if (cached) {
return new Response(new Uint8Array(cached.buffer), {
headers: buildResponseHeaders(cached.contentType, download, filename),
});
}
const sourceUrl = resolveSourceUrl(src);
let res: Response;
try {
res = await fetch(sourceUrl);
} catch (err) {
console.error('[cms-files] CMS fetch failed:', err);
return new Response('CMS not reachable', { status: 502 });
}
if (!res.ok) {
return new Response('File fetch failed', { status: res.status });
}
const buffer = await res.arrayBuffer();
const upstreamCt = res.headers.get('content-type') ?? '';
const ext = extFromSrc(src);
const contentType = upstreamCt && !upstreamCt.startsWith('application/octet-stream')
? upstreamCt
: ext
? contentTypeFromExt(ext)
: upstreamCt || 'application/octet-stream';
const result = await cacheFile(key, buffer, contentType, src);
return new Response(new Uint8Array(result.buffer), {
headers: buildResponseHeaders(result.contentType, download, filename),
});
};