feat: image optimisation
This commit is contained in:
@@ -12,16 +12,36 @@
|
|||||||
const placeholder = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxyZWN0IGZpbGw9IiMzMzMiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48L3N2Zz4=';
|
const placeholder = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxyZWN0IGZpbGw9IiMzMzMiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48L3N2Zz4=';
|
||||||
|
|
||||||
let imageSrc = $derived(src || placeholder);
|
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";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 grid-rows-1">
|
<div class="grid grid-cols-1 grid-rows-1">
|
||||||
<img
|
<img
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
|
{srcset}
|
||||||
|
{sizes}
|
||||||
{alt}
|
{alt}
|
||||||
|
loading="lazy"
|
||||||
class="row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover"
|
class="row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover"
|
||||||
onerror={(e) => {
|
onerror={(e) => {
|
||||||
const img = e.currentTarget as HTMLImageElement;
|
const img = e.currentTarget as HTMLImageElement;
|
||||||
img.src = placeholder;
|
img.src = placeholder;
|
||||||
|
// Clear srcset on error to avoid loading broken thumbnails
|
||||||
|
img.srcset = "";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="row-start-1 col-start-1">
|
<div class="row-start-1 col-start-1">
|
||||||
|
|||||||
@@ -21,8 +21,17 @@
|
|||||||
src = `${normalizedBasePath}/${href}`.replace(/\/+/g, '/');
|
src = `${normalizedBasePath}/${href}`.replace(/\/+/g, '/');
|
||||||
}
|
}
|
||||||
src = `/assets${src}`;
|
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 `<img src="${src}" srcset="${srcset}" sizes="${sizes}" alt="${text}" title="${title || ''}" loading="lazy" />`;
|
||||||
}
|
}
|
||||||
return `<img src="${src}" alt="${text}" title="${title || ''}" />`;
|
return `<img src="${src}" alt="${text}" title="${title || ''}" loading="lazy" />`;
|
||||||
};
|
};
|
||||||
|
|
||||||
let html = $derived(
|
let html = $derived(
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export async function getCoverImage(slug: string): Promise<string | null> {
|
|||||||
return withCache(`cover:${slug}`, async () => {
|
return withCache(`cover:${slug}`, async () => {
|
||||||
try {
|
try {
|
||||||
const contents = await client.getDirectoryContents("/" + slug);
|
const contents = await client.getDirectoryContents("/" + slug);
|
||||||
console.log(contents);
|
|
||||||
if (!Array.isArray(contents)) return null;
|
if (!Array.isArray(contents)) return null;
|
||||||
|
|
||||||
const cover = contents.find(
|
const cover = contents.find(
|
||||||
|
|||||||
@@ -2,24 +2,80 @@ import { client } from "$lib/server/nextcloud";
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import type { RequestHandler } from "./$types";
|
import type { RequestHandler } from "./$types";
|
||||||
import mime from "mime-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 { path } = params;
|
||||||
|
const width = url.searchParams.get("w");
|
||||||
|
const height = url.searchParams.get("h");
|
||||||
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
throw error(400, "Path is required");
|
throw error(400, "Path is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch from Nextcloud. We assume the path is relative to the root or a specific folder.
|
let buffer: ArrayBuffer;
|
||||||
// If your files are in a specific folder, e.g., "Schreifuchs", prepend it here.
|
let contentType: string;
|
||||||
const buffer = await client.getFileContents("/" + path);
|
|
||||||
const contentType = mime.lookup(path) || "application/octet-stream";
|
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: {
|
headers: {
|
||||||
"Content-Type": contentType,
|
"Content-Type": contentType,
|
||||||
"Cache-Control": "public, max-age=3600" // Cache for 1 hour
|
"Cache-Control": "public, max-age=86400",
|
||||||
|
"Vary": "Accept-Encoding"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user