diff --git a/src/lib/components/ImageTile.svelte b/src/lib/components/ImageTile.svelte index 3f8ac13..dfe8c74 100644 --- a/src/lib/components/ImageTile.svelte +++ b/src/lib/components/ImageTile.svelte @@ -12,16 +12,36 @@ const placeholder = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxyZWN0IGZpbGw9IiMzMzMiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48L3N2Zz4='; let imageSrc = $derived(src || placeholder); + + // Generate srcset for optimized images from our proxy + let srcset = $derived.by(() => { + if (!src || !src.startsWith('/assets/')) return undefined; + + const widths = [400, 800, 1200, 1600]; + const sets = widths.map(w => `${src}${src.includes('?') ? '&' : '?'}w=${w} ${w}w`); + + // Add original as fallback + sets.push(`${src} 2000w`); + + return sets.join(', '); + }); + + const sizes = "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw";
{ const img = e.currentTarget as HTMLImageElement; img.src = placeholder; + // Clear srcset on error to avoid loading broken thumbnails + img.srcset = ""; }} />
diff --git a/src/lib/components/Markdown.svelte b/src/lib/components/Markdown.svelte index d030734..f0e1fb5 100644 --- a/src/lib/components/Markdown.svelte +++ b/src/lib/components/Markdown.svelte @@ -21,8 +21,17 @@ src = `${normalizedBasePath}/${href}`.replace(/\/+/g, '/'); } src = `/assets${src}`; + + // Generate srcset for optimized images + const widths = [400, 800, 1200, 1600, 2000]; + const srcset = widths + .map(w => `${src}?w=${w} ${w}w`) + .join(', '); + const sizes = "(max-width: 1024px) 100vw, 1024px"; + + return `${text}`; } - return `${text}`; + return `${text}`; }; let html = $derived( diff --git a/src/lib/server/nextcloud.ts b/src/lib/server/nextcloud.ts index 42490eb..6d80ce6 100644 --- a/src/lib/server/nextcloud.ts +++ b/src/lib/server/nextcloud.ts @@ -37,7 +37,6 @@ export async function getCoverImage(slug: string): Promise { return withCache(`cover:${slug}`, async () => { try { const contents = await client.getDirectoryContents("/" + slug); - console.log(contents); if (!Array.isArray(contents)) return null; const cover = contents.find( diff --git a/src/routes/assets/[...path]/+server.ts b/src/routes/assets/[...path]/+server.ts index c08a4a2..5080a11 100644 --- a/src/routes/assets/[...path]/+server.ts +++ b/src/routes/assets/[...path]/+server.ts @@ -2,24 +2,80 @@ import { client } from "$lib/server/nextcloud"; import { error } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import mime from "mime-types"; +import { env } from "$env/dynamic/private"; -export const GET: RequestHandler = async ({ params }) => { +export const GET: RequestHandler = async ({ params, url }) => { const { path } = params; + const width = url.searchParams.get("w"); + const height = url.searchParams.get("h"); if (!path) { throw error(400, "Path is required"); } try { - // Fetch from Nextcloud. We assume the path is relative to the root or a specific folder. - // If your files are in a specific folder, e.g., "Schreifuchs", prepend it here. - const buffer = await client.getFileContents("/" + path); - const contentType = mime.lookup(path) || "application/octet-stream"; + let buffer: ArrayBuffer; + let contentType: string; + + if (width || height) { + // Use Nextcloud Preview API + const baseUrl = env.NEXTCLOUD_URL?.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`; + + let fullPath = path; + if (env.NEXTCLOUD_BASE_DIR) { + const baseDir = env.NEXTCLOUD_BASE_DIR.replace(/^\/+|\/+$/g, ""); + fullPath = baseDir ? `${baseDir}/${path}` : path; + } + + const previewUrl = new URL(`${baseUrl}index.php/core/preview`); + // Ensure leading slash for the file parameter + const fileParam = fullPath.startsWith('/') ? fullPath : `/${fullPath}`; + previewUrl.searchParams.set("file", fileParam); + if (width) previewUrl.searchParams.set("x", width); + if (height) previewUrl.searchParams.set("y", height); + previewUrl.searchParams.set("forceIcon", "0"); + + const auth = Buffer.from(`${env.NEXTCLOUD_USER}:${env.NEXTCLOUD_PASSWORD}`).toString("base64"); + + console.log(`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`); + + try { + const response = await fetch(previewUrl.toString(), { + headers: { + "Authorization": `Basic ${auth}` + } + }); + + if (response.ok) { + buffer = await response.arrayBuffer(); + contentType = response.headers.get("Content-Type") || "image/png"; + + return new Response(buffer, { + headers: { + "Content-Type": contentType, + "Cache-Control": "public, max-age=86400", + "Vary": "Accept-Encoding" + } + }); + } else { + console.warn(`Nextcloud preview failed (${response.status} ${response.statusText}) for ${fullPath}. Falling back to original.`); + } + } catch (fetchError) { + console.error(`Fetch error during preview request for ${fullPath}:`, fetchError); + } + } + + // Fetch original via WebDAV (Fallback or no resize requested) + console.log(`Fetching original file via WebDAV: /${path}`); + const data = await client.getFileContents("/" + path); + buffer = data as ArrayBuffer; + contentType = mime.lookup(path) || "application/octet-stream"; - return new Response(buffer as any, { + return new Response(buffer, { headers: { "Content-Type": contentType, - "Cache-Control": "public, max-age=3600" // Cache for 1 hour + "Cache-Control": "public, max-age=86400", + "Vary": "Accept-Encoding" } }); } catch (e) {