diff --git a/src/app.html b/src/app.html
index 6c747a3..29518d9 100644
--- a/src/app.html
+++ b/src/app.html
@@ -2,11 +2,11 @@
-
+
+
+
+
+
schreifuchs.ch
diff --git a/src/lib/server/cache.ts b/src/lib/server/cache.ts
index b839855..ec4c41d 100644
--- a/src/lib/server/cache.ts
+++ b/src/lib/server/cache.ts
@@ -7,37 +7,41 @@ const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL
* @param fetcher Function that returns a promise with the data to cache.
* @param ttl Time to live in milliseconds. Defaults to 15 minutes.
*/
-export async function withCache(key: string, fetcher: () => Promise, ttl = DEFAULT_TTL): Promise {
- const now = Date.now();
- const entry = cache.get(key);
+export async function withCache(
+ key: string,
+ fetcher: () => Promise,
+ ttl = DEFAULT_TTL,
+): Promise {
+ const now = Date.now();
+ const entry = cache.get(key);
- if (entry && (now - entry.timestamp < ttl)) {
- return entry.data as T;
- }
+ if (entry && now - entry.timestamp < ttl) {
+ return entry.data as T;
+ }
- try {
- const data = await fetcher();
- cache.set(key, { timestamp: now, data });
- return data;
- } catch (error) {
- console.error(`Error fetching data for key ${key}:`, error);
- // If fetch fails but we have stale data, return it?
- // For now, let's bubble the error up so the page handles it (e.g. 404),
- // or maybe just re-throw.
- throw error;
- }
+ try {
+ const data = await fetcher();
+ cache.set(key, { timestamp: now, data });
+ return data;
+ } catch (error) {
+ console.error(`Error fetching data for key ${key}:`, error);
+ // If fetch fails but we have stale data, return it?
+ // For now, let's bubble the error up so the page handles it (e.g. 404),
+ // or maybe just re-throw.
+ throw error;
+ }
}
/**
* Manually clears a specific cache key.
*/
export function clearCache(key: string) {
- cache.delete(key);
+ cache.delete(key);
}
/**
* Clears the entire cache.
*/
export function clearAllCache() {
- cache.clear();
+ cache.clear();
}
diff --git a/src/routes/[[lang=lang]]/+page.svelte b/src/routes/[[lang=lang]]/+page.svelte
index 2e52eac..209b927 100644
--- a/src/routes/[[lang=lang]]/+page.svelte
+++ b/src/routes/[[lang=lang]]/+page.svelte
@@ -23,7 +23,7 @@
// Helper to resolve image source: Local override -> Nextcloud cover -> Placeholder (handled by ImageTile)
const getTileImage = (page: { slug: string; coverImage: string | null }) => {
- return staticImages[page.slug.toLowerCase()] || page.coverImage || "";
+ return page.coverImage || staticImages[page.slug.toLowerCase()] || "";
};
@@ -46,4 +46,5 @@
{m.pfadi()}
-
\ No newline at end of file
+
+
diff --git a/src/routes/assets/[...path]/+server.ts b/src/routes/assets/[...path]/+server.ts
index d196a69..446c199 100644
--- a/src/routes/assets/[...path]/+server.ts
+++ b/src/routes/assets/[...path]/+server.ts
@@ -1,101 +1,132 @@
+// src/routes/assets/[...path]/+server.ts
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";
+import { withCache } from "$lib/server/cache";
+
+// Define what we are storing in the cache
+type CachedAsset = {
+ buffer: ArrayBuffer;
+ contentType: string;
+};
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");
- }
+ const { path } = params;
+ const width = url.searchParams.get("w");
+ const height = url.searchParams.get("h");
- try {
- let buffer: ArrayBuffer;
- let contentType: string;
+ if (!path) {
+ throw error(400, "Path is required");
+ }
+
+ // 1. Create a unique cache key for this specific asset variation
+ const cacheKey = `asset:${path}?w=${width ?? ""}&h=${height ?? ""}`;
+
+ try {
+ // 2. Use withCache to wrap the fetching logic
+ const cachedData = await withCache(
+ cacheKey,
+ async () => {
+ // --- START OF FETCH LOGIC ---
+ // This function only runs if the data is NOT in the cache
- // Try both original path and hidden variant if applicable
const pathsToTry = [path];
- if (!path.startsWith('_')) {
- pathsToTry.push(`_${path}`);
+ if (!path.startsWith("_")) {
+ pathsToTry.push(`_${path}`);
}
-
- let response: Response | null = null;
let lastError: any = null;
for (const currentPath of pathsToTry) {
- try {
- if (width || height) {
- // Use Nextcloud Preview API
- const baseUrl = env.NEXTCLOUD_URL?.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`;
-
- let fullPath = currentPath;
- if (env.NEXTCLOUD_BASE_DIR) {
- const baseDir = env.NEXTCLOUD_BASE_DIR.replace(/^\/+|\/+$/g, "");
- fullPath = baseDir ? `${baseDir}/${currentPath}` : currentPath;
- }
+ try {
+ // A. Try Nextcloud Preview (Resizing)
+ if (width || height) {
+ const baseUrl = env.NEXTCLOUD_URL?.endsWith("/")
+ ? env.NEXTCLOUD_URL
+ : `${env.NEXTCLOUD_URL}/`;
- const isJpg = fullPath.match(/\.(jpg|jpeg)$/i);
- const previewExtension = isJpg ? 'jpg' : 'png';
- const previewUrl = new URL(`${baseUrl}index.php/core/preview.${previewExtension}`);
- // 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");
+ let fullPath = currentPath;
+ if (env.NEXTCLOUD_BASE_DIR) {
+ const baseDir = env.NEXTCLOUD_BASE_DIR.replace(
+ /^\/+|\/+$/g,
+ "",
+ );
+ fullPath = baseDir ? `${baseDir}/${currentPath}` : currentPath;
+ }
- const auth = Buffer.from(`${env.NEXTCLOUD_USER}:${env.NEXTCLOUD_PASSWORD}`).toString("base64");
-
- console.log(`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`);
+ const isJpg = fullPath.match(/\.(jpg|jpeg)$/i);
+ const previewExtension = isJpg ? "jpg" : "png";
+ const previewUrl = new URL(
+ `${baseUrl}index.php/core/preview.${previewExtension}`,
+ );
- const ncResponse = await fetch(previewUrl.toString(), {
- headers: {
- "Authorization": `Basic ${auth}`
- }
- });
+ 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");
- if (ncResponse.ok) {
- buffer = await ncResponse.arrayBuffer();
- contentType = ncResponse.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 (${ncResponse.status}) for ${fullPath}.`);
- }
- }
+ const auth = Buffer.from(
+ `${env.NEXTCLOUD_USER}:${env.NEXTCLOUD_PASSWORD}`,
+ ).toString("base64");
- // Fetch original via WebDAV (Fallback or no resize requested)
- console.log(`Fetching original file via WebDAV: /${currentPath}`);
- const data = await client.getFileContents("/" + currentPath);
- buffer = data as ArrayBuffer;
- contentType = mime.lookup(currentPath) || "application/octet-stream";
-
- return new Response(buffer, {
- headers: {
- "Content-Type": contentType,
- "Cache-Control": "public, max-age=86400",
- "Vary": "Accept-Encoding"
- }
- });
- } catch (e) {
- lastError = e;
- continue; // Try next path variant
+ console.log(
+ `Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`,
+ );
+
+ const ncResponse = await fetch(previewUrl.toString(), {
+ headers: { Authorization: `Basic ${auth}` },
+ });
+
+ if (ncResponse.ok) {
+ const buffer = await ncResponse.arrayBuffer();
+ const contentType =
+ ncResponse.headers.get("Content-Type") || "image/png";
+
+ // Return the raw data to be cached
+ return { buffer, contentType };
+ } else {
+ console.warn(
+ `Nextcloud preview failed (${ncResponse.status}) for ${fullPath}.`,
+ );
+ }
}
+
+ // B. Try WebDAV (Original)
+ console.log(`Fetching original file via WebDAV: /${currentPath}`);
+ const data = await client.getFileContents("/" + currentPath);
+ const buffer = data as ArrayBuffer;
+ const contentType =
+ mime.lookup(currentPath) || "application/octet-stream";
+
+ // Return the raw data to be cached
+ return { buffer, contentType };
+ } catch (e) {
+ lastError = e;
+ continue; // Try next path variant
+ }
}
-
+
throw lastError || new Error("Asset not found");
- } catch (e) {
- console.error(`Error fetching asset from Nextcloud: ${path}`, e);
- throw error(404, "Asset not found");
- }
+ // --- END OF FETCH LOGIC ---
+ },
+ // Optional: Set a specific TTL for images (e.g., 1 hour instead of default 15m)
+ 1000 * 60 * 60,
+ );
+
+ // 3. Construct a fresh Response using the cached data
+ return new Response(cachedData.buffer, {
+ headers: {
+ "Content-Type": cachedData.contentType,
+ "Cache-Control": "public, max-age=86400", // Browser cache
+ Vary: "Accept-Encoding",
+ },
+ });
+ } catch (e) {
+ console.error(`Error fetching asset: ${path}`, e);
+ throw error(404, "Asset not found");
+ }
};