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
+97
View File
@@ -0,0 +1,97 @@
/**
* Datei-URLs für SvelteKit: /cms-files mit persistentem Server-Disk-Cache.
* Analog zu rusty-image.ts, aber für generische Dateien (PDF, DOCX, ...).
*/
export interface CmsFileField {
url: string;
title?: string;
description?: string;
size?: number;
mime?: string;
filename?: string;
}
/**
* Extrahiert URL/Title/Description aus einem CMS-File-Field
* (string oder { src, title?, description?, mime?, size?, ... }).
*/
export function extractCmsFileField(field: unknown): CmsFileField | null {
if (typeof field === 'string') {
const s = field.trim();
if (!s) return null;
return { url: s.startsWith('//') ? `https:${s}` : s };
}
if (field && typeof field === 'object') {
const o = field as {
src?: string;
file?: { url?: string };
title?: string;
description?: string;
size?: number;
mime?: string;
filename?: string;
};
const rawUrl =
typeof o.src === 'string' && o.src.trim()
? o.src
: typeof o.file?.url === 'string' && o.file.url.trim()
? o.file.url
: undefined;
if (!rawUrl) return null;
const url = rawUrl.startsWith('//') ? `https:${rawUrl}` : rawUrl;
return {
url,
...(o.title ? { title: o.title } : {}),
...(o.description ? { description: o.description } : {}),
...(typeof o.size === 'number' ? { size: o.size } : {}),
...(o.mime ? { mime: o.mime } : {}),
...(o.filename ? { filename: o.filename } : {}),
};
}
return null;
}
export interface CmsFileUrlOptions {
/** Filename hint for Content-Disposition header. */
filename?: string;
/** Force download (Content-Disposition: attachment). */
download?: boolean;
}
/**
* Lokale Datei-URL: /cms-files?src=…&name=…&dl=…
* `src` = volle Datei-URL oder CMS-Asset-Pfad (`/api/assets/...`).
*/
export function getCmsFileUrl(src: string, opts: CmsFileUrlOptions = {}): string {
const params = new URLSearchParams();
params.set('src', src);
if (opts.filename) params.set('name', opts.filename);
if (opts.download) params.set('dl', '1');
return `/cms-files?${params.toString()}`;
}
const SIZE_UNITS = ['B', 'KB', 'MB', 'GB'];
export function formatFileSize(bytes?: number): string | null {
if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes <= 0) return null;
let b = bytes;
let i = 0;
while (b >= 1024 && i < SIZE_UNITS.length - 1) {
b /= 1024;
i++;
}
const rounded = b >= 10 || i === 0 ? Math.round(b) : Math.round(b * 10) / 10;
return `${rounded} ${SIZE_UNITS[i]}`;
}
export function fileExtFromUrl(url: string): string | null {
try {
const path = url.split('?')[0].split('#')[0];
const last = path.split('/').pop() ?? '';
const dot = last.lastIndexOf('.');
return dot >= 0 ? last.slice(dot + 1).toLowerCase() : null;
} catch {
return null;
}
}