feat: caching
This commit is contained in:
@@ -1,32 +1,73 @@
|
|||||||
# Schreifuchs
|
# schreifuchs.ch
|
||||||
|
|
||||||
Schreifuchs.ch ist meine persönliche Webseite.
|
Personal portfolio website for Schreifuchs, featuring photography, informatics, music, and video.
|
||||||
|
|
||||||
## Developing
|
The site is built with **Svelte 5** and **SvelteKit**, and it is powered by a dynamic **Nextcloud** backend via WebDAV.
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
## Features
|
||||||
|
|
||||||
|
- **Dynamic Content:** Pages are automatically discovered based on the folder structure in Nextcloud.
|
||||||
|
- **Markdown Driven:** Content for each page is fetched from a corresponding Markdown file.
|
||||||
|
- **Asset Proxying:** Images and attachments are securely streamed from Nextcloud through a server-side proxy.
|
||||||
|
- **Performance:** In-memory caching for Nextcloud requests (15-minute TTL).
|
||||||
|
- **Responsive Design:** Styled with Tailwind CSS, optimized for all screen sizes.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Framework:** Svelte 5 (Runes) & SvelteKit
|
||||||
|
- **Styling:** Tailwind CSS
|
||||||
|
- **Backend:** Node.js (via `@sveltejs/adapter-node`)
|
||||||
|
- **Integration:** `webdav` library for Nextcloud communication
|
||||||
|
- **Content:** `@ts-stack/markdown` for rendering
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application requires the following environment variables in a `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXTCLOUD_URL=https://your-nextcloud-instance.com
|
||||||
|
NEXTCLOUD_USER=your_username
|
||||||
|
NEXTCLOUD_PASSWORD=your_app_password
|
||||||
|
NEXTCLOUD_BASE_DIR=WebsiteContent # Optional: Root folder for site content
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content Management
|
||||||
|
|
||||||
|
To add or update content, manage files in your configured Nextcloud directory:
|
||||||
|
|
||||||
|
1. **Create a Page:** Create a folder (e.g., `Travel`).
|
||||||
|
2. **Add Content:** Inside that folder, create a Markdown file named after the folder (e.g., `Travel.md`).
|
||||||
|
3. **Add Header Image:** Upload a file named `cover.webp`, `cover.jpg`, or `cover.png` to the folder. This will be used as the header on the page and the tile on the homepage.
|
||||||
|
4. **Add Assets:** Any other images or files uploaded to the folder can be linked in the Markdown.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
Start the development server:
|
||||||
npm run dev -- --open
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
To create a production version of your app:
|
Build for production (Node.js):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
pnpm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
The output will be in the `build/` directory, ready to be served by Node.
|
||||||
|
|
||||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
## Image Processing
|
||||||
|
|
||||||
## Resize images
|
To optimize images before uploading to Nextcloud:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
convert input.png -resize 3000 -quality 80 output.webp
|
convert input.png -resize 3000 -quality 80 output.webp
|
||||||
```
|
```
|
||||||
+17
-16
@@ -1,36 +1,37 @@
|
|||||||
# Plan: Nextcloud WebDAV Integration
|
# Plan: Nextcloud WebDAV Integration (COMPLETED)
|
||||||
|
|
||||||
Transition from static file hosting to a dynamic SvelteKit (Node.js) application that fetches content directly from Nextcloud.
|
Transition from static file hosting to a dynamic SvelteKit (Node.js) application that fetches content directly from Nextcloud.
|
||||||
|
|
||||||
## 1. Infrastructure Shift
|
## 1. Infrastructure Shift
|
||||||
- [ ] Replace `@sveltejs/adapter-static` with `@sveltejs/adapter-node`.
|
- [x] Replace `@sveltejs/adapter-static` with `@sveltejs/adapter-node`.
|
||||||
- [ ] Update `svelte.config.js` to use the Node adapter.
|
- [x] Update `svelte.config.js` to use the Node adapter.
|
||||||
- [ ] Remove `fallback: undefined` and other static-specific configurations.
|
- [x] Remove `fallback: undefined` and other static-specific configurations.
|
||||||
|
|
||||||
## 2. Dependencies
|
## 2. Dependencies
|
||||||
- [ ] Install `webdav` client library.
|
- [x] Install `webdav` client library.
|
||||||
- [ ] Install `dotenv` (if not already handled by SvelteKit's built-in env support).
|
- [x] Install `dotenv` (handled by SvelteKit's built-in env support).
|
||||||
|
|
||||||
## 3. Secure Configuration
|
## 3. Secure Configuration
|
||||||
- [ ] Set up `.env` with:
|
- [x] Set up `.env` with:
|
||||||
- `NEXTCLOUD_URL`
|
- `NEXTCLOUD_URL`
|
||||||
- `NEXTCLOUD_USER`
|
- `NEXTCLOUD_USER`
|
||||||
- `NEXTCLOUD_PASSWORD`
|
- `NEXTCLOUD_PASSWORD`
|
||||||
- [ ] Use `$env/dynamic/private` to access these in server-side code only.
|
- [x] Use `$env/dynamic/private` to access these in server-side code only.
|
||||||
|
|
||||||
## 4. Server-Side Data Fetching
|
## 4. Server-Side Data Fetching
|
||||||
- [ ] Create `src/lib/server/nextcloud.ts` to initialize and export the WebDAV client.
|
- [x] Create `src/lib/server/nextcloud.ts` to initialize and export the WebDAV client.
|
||||||
- [ ] Implement `+page.server.ts` for each content route:
|
- [x] Implement `+page.server.ts` for each content route:
|
||||||
- Fetch Markdown content from Nextcloud.
|
- Fetch Markdown content from Nextcloud.
|
||||||
- Return the raw string to the frontend.
|
- Return the raw string to the frontend.
|
||||||
- [ ] Update `Markdown.svelte` to receive and render raw strings instead of fetching from a URL on the client.
|
- [x] Update `Markdown.svelte` to receive and render raw strings instead of fetching from a URL on the client.
|
||||||
|
|
||||||
## 5. Asset Proxying
|
## 5. Asset Proxying
|
||||||
- [ ] Create a catch-all server route (e.g., `src/routes/assets/[...path]/+server.ts`).
|
- [x] Create a catch-all server route (e.g., `src/routes/assets/[...path]/+server.ts`).
|
||||||
- [ ] This route will:
|
- [x] This route will:
|
||||||
- Authenticate with WebDAV.
|
- Authenticate with WebDAV.
|
||||||
- Stream images/files from Nextcloud directly to the browser.
|
- Stream images/files from Nextcloud directly to the browser.
|
||||||
- [ ] Update Markdown rendering logic to rewrite image URLs to point to this proxy route.
|
- [x] Update Markdown rendering logic to rewrite image URLs to point to this proxy route.
|
||||||
|
|
||||||
## 6. Optimization (Optional but Recommended)
|
## 6. Optimization
|
||||||
- [ ] Implement server-side caching (e.g., using a simple TTL cache or Redis) to avoid hitting Nextcloud on every single request.
|
- [x] Implement server-side caching to avoid hitting Nextcloud on every single request.
|
||||||
|
- [x] Implement dynamic route discovery to automatically create pages from Nextcloud folders.
|
||||||
@@ -3,16 +3,26 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
src: string;
|
src: string;
|
||||||
|
alt?: string;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { src, children }: Props = $props();
|
let { src, alt = "", children }: Props = $props();
|
||||||
|
|
||||||
|
const placeholder = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxyZWN0IGZpbGw9IiMzMzMiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48L3N2Zz4=';
|
||||||
|
|
||||||
|
let imageSrc = $derived(src || placeholder);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 grid-rows-1">
|
<div class="grid grid-cols-1 grid-rows-1">
|
||||||
<img
|
<img
|
||||||
{src}
|
src={imageSrc}
|
||||||
|
{alt}
|
||||||
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) => {
|
||||||
|
const img = e.currentTarget as HTMLImageElement;
|
||||||
|
img.src = placeholder;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="row-start-1 col-start-1">
|
<div class="row-start-1 col-start-1">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const cache = new Map<string, { timestamp: number; data: any }>();
|
||||||
|
const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches the result of a promise-returning function.
|
||||||
|
* @param key Unique key for the cache entry.
|
||||||
|
* @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<T>(key: string, fetcher: () => Promise<T>, ttl = DEFAULT_TTL): Promise<T> {
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = cache.get(key);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually clears a specific cache key.
|
||||||
|
*/
|
||||||
|
export function clearCache(key: string) {
|
||||||
|
cache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the entire cache.
|
||||||
|
*/
|
||||||
|
export function clearAllCache() {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
+87
-10
@@ -1,21 +1,98 @@
|
|||||||
import { createClient } from "webdav";
|
import { createClient } from "webdav";
|
||||||
import { env } from "$env/dynamic/private";
|
import { env } from "$env/dynamic/private";
|
||||||
|
import { withCache } from "./cache";
|
||||||
|
|
||||||
if (!env.NEXTCLOUD_URL || !env.NEXTCLOUD_USER || !env.NEXTCLOUD_PASSWORD) {
|
if (!env.NEXTCLOUD_URL || !env.NEXTCLOUD_USER || !env.NEXTCLOUD_PASSWORD) {
|
||||||
console.warn("Nextcloud environment variables are not fully set. WebDAV client might not work correctly.");
|
console.warn(
|
||||||
|
"Nextcloud environment variables are not fully set. WebDAV client might not work correctly.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the standard Nextcloud WebDAV URL: https://example.com/remote.php/dav/files/USERNAME/
|
// Construct the standard Nextcloud WebDAV URL: https://example.com/remote.php/dav/files/USERNAME/[BASE_DIR/]
|
||||||
const getWebdavUrl = () => {
|
const getWebdavUrl = () => {
|
||||||
if (!env.NEXTCLOUD_URL) return "";
|
if (!env.NEXTCLOUD_URL) return "";
|
||||||
const base = env.NEXTCLOUD_URL.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`;
|
|
||||||
return `${base}remote.php/dav/files/${env.NEXTCLOUD_USER}/`;
|
let url = env.NEXTCLOUD_URL.endsWith("/")
|
||||||
|
? env.NEXTCLOUD_URL
|
||||||
|
: `${env.NEXTCLOUD_URL}/`;
|
||||||
|
url += `remote.php/dav/files/${env.NEXTCLOUD_USER}/`;
|
||||||
|
|
||||||
|
if (env.NEXTCLOUD_BASE_DIR) {
|
||||||
|
// Remove leading/trailing slashes from base dir to ensure clean concatenation
|
||||||
|
const baseDir = env.NEXTCLOUD_BASE_DIR.replace(/^\/+|\/+$/g, "");
|
||||||
|
if (baseDir) {
|
||||||
|
url += `${baseDir}/`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const client = createClient(
|
export const client = createClient(getWebdavUrl(), {
|
||||||
getWebdavUrl(),
|
username: env.NEXTCLOUD_USER || "",
|
||||||
{
|
password: env.NEXTCLOUD_PASSWORD || "",
|
||||||
username: env.NEXTCLOUD_USER || "",
|
});
|
||||||
password: env.NEXTCLOUD_PASSWORD || ""
|
|
||||||
|
export async function getCoverImage(slug: string): Promise<string | null> {
|
||||||
|
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(
|
||||||
|
(item: any) =>
|
||||||
|
item.type === "file" &&
|
||||||
|
item.basename.match(/^cover\.(webp|jpg|jpeg|png)$/i),
|
||||||
|
);
|
||||||
|
|
||||||
|
return cover ? `/assets/${slug}/${cover.basename}` : null;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to check cover image for ${slug}:`, e);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPages() {
|
||||||
|
return withCache("pages", async () => {
|
||||||
|
try {
|
||||||
|
const items = await client.getDirectoryContents("/");
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const directories = items.filter(
|
||||||
|
(item: any) =>
|
||||||
|
item.type === "directory" && !item.basename.startsWith("."),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pages = await Promise.all(
|
||||||
|
directories.map(async (item: any) => {
|
||||||
|
const slug = item.basename;
|
||||||
|
const title = slug.charAt(0).toUpperCase() + slug.slice(1);
|
||||||
|
const coverImage = await getCoverImage(slug);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
title,
|
||||||
|
coverImage,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch pages from Nextcloud:", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPageContent(path: string): Promise<string> {
|
||||||
|
return withCache(`content:${path}`, async () => {
|
||||||
|
const content = await client.getFileContents(path, { format: "text" });
|
||||||
|
return content as string;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
import { getPages } from '$lib/server/nextcloud';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async () => {
|
||||||
|
const pages = await getPages();
|
||||||
|
return {
|
||||||
|
pages
|
||||||
|
};
|
||||||
|
};
|
||||||
+42
-35
@@ -1,41 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ImageLinkTile from "$lib/components/ImageLinkTile.svelte";
|
import ImageLinkTile from "$lib/components/ImageLinkTile.svelte";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
import mouse from "$lib/images/home/mouse.webp";
|
// Import local images to preserve existing design
|
||||||
import drummachine from "$lib/images/home/drummachine.webp";
|
import mouse from "$lib/images/home/mouse.webp";
|
||||||
import scouts from "$lib/images/home/scouts.webp";
|
import drummachine from "$lib/images/home/drummachine.webp";
|
||||||
import console_image from "$lib/images/home/console.webp";
|
import scouts from "$lib/images/home/scouts.webp";
|
||||||
import lauch from "$lib/images/home/lauch.webp";
|
import console_image from "$lib/images/home/console.webp";
|
||||||
|
import lauch from "$lib/images/home/lauch.webp";
|
||||||
|
import monitor from "$lib/images/informatik/monitor.webp"; // Using monitor for Informatik if console is not preferred
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
// Map slugs to local images for backward compatibility/styling
|
||||||
|
const staticImages: Record<string, string> = {
|
||||||
|
informatik: console_image,
|
||||||
|
fotos: mouse,
|
||||||
|
video: lauch,
|
||||||
|
musig: drummachine,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 || "";
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main class="">
|
||||||
<ImageLinkTile src={console_image} href="/informatik">
|
{#if data.pages}
|
||||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
{#each data.pages as page}
|
||||||
Informatik
|
<ImageLinkTile src={getTileImage(page)} href={`/${page.slug}`}>
|
||||||
</h2>
|
<h2
|
||||||
</ImageLinkTile>
|
class="text-3xl font-bold text-white hover:underline text-shadow-lg"
|
||||||
|
>
|
||||||
|
{page.title}
|
||||||
|
</h2>
|
||||||
|
</ImageLinkTile>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<ImageLinkTile src={scouts} href="https://pfadifrisco.ch/" target="_blank">
|
<!-- Static/External Links -->
|
||||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
<ImageLinkTile src={scouts} href="https://pfadifrisco.ch/" target="_blank">
|
||||||
Pfadi
|
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||||
</h2>
|
Pfadi
|
||||||
</ImageLinkTile>
|
</h2>
|
||||||
|
</ImageLinkTile>
|
||||||
<ImageLinkTile src={mouse} href="/fotos">
|
|
||||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
|
||||||
Fotos
|
|
||||||
</h2>
|
|
||||||
</ImageLinkTile>
|
|
||||||
|
|
||||||
<ImageLinkTile src={lauch} href="/video">
|
|
||||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
|
||||||
Videos
|
|
||||||
</h2>
|
|
||||||
</ImageLinkTile>
|
|
||||||
|
|
||||||
<ImageLinkTile src={drummachine} href="/musig">
|
|
||||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
|
||||||
Musig
|
|
||||||
</h2>
|
|
||||||
</ImageLinkTile>
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { getCoverImage, getPageContent } from "$lib/server/nextcloud";
|
||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const { slug } = params;
|
||||||
|
// Capitalize the first letter to match the convention: fotos -> Fotos.md
|
||||||
|
const filename = slug.charAt(0).toUpperCase() + slug.slice(1) + ".md";
|
||||||
|
const path = `/${slug}/${filename}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [content, coverImage] = await Promise.all([
|
||||||
|
getPageContent(path),
|
||||||
|
getCoverImage(slug)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
title: slug.charAt(0).toUpperCase() + slug.slice(1),
|
||||||
|
slug,
|
||||||
|
coverImage
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error fetching ${filename} from Nextcloud`, e);
|
||||||
|
throw error(404, "Page not found");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ImageTile from "$lib/components/ImageTile.svelte";
|
||||||
|
import Markdown from "$lib/components/Markdown.svelte";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
|
// Legacy images
|
||||||
|
import sunne_untergang from "$lib/images/fotos/sunne_untergang.webp";
|
||||||
|
import monitor from "$lib/images/informatik/monitor.webp";
|
||||||
|
import plattespiler from "$lib/images/musig/plattespiler.webp";
|
||||||
|
import jochen from "$lib/images/video/jochen.webp";
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
const legacyHeaders: Record<string, string> = {
|
||||||
|
fotos: sunne_untergang,
|
||||||
|
informatik: monitor,
|
||||||
|
musig: plattespiler,
|
||||||
|
video: jochen
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback image data URI
|
||||||
|
const placeholder = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxyZWN0IGZpbGw9IiMzMzMiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48L3N2Zz4=';
|
||||||
|
|
||||||
|
// Use the cover image found by the server, or legacy local image, or placeholder
|
||||||
|
let headerImage = $derived(
|
||||||
|
data.coverImage ||
|
||||||
|
legacyHeaders[data.slug.toLowerCase()] ||
|
||||||
|
"" // ImageTile handles empty/null src with placeholder or its own logic
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ImageTile src={headerImage} alt={data.title}>
|
||||||
|
<h2 class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg">
|
||||||
|
{data.title}
|
||||||
|
</h2>
|
||||||
|
</ImageTile>
|
||||||
|
|
||||||
|
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
||||||
|
<section class="max-w-screen-md w-full">
|
||||||
|
<Markdown content={data.content} basePath={`/${data.slug}`} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { client } from "$lib/server/nextcloud";
|
|
||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
try {
|
|
||||||
const content = await client.getFileContents("/fotos/Fotos.md", { format: "text" });
|
|
||||||
return {
|
|
||||||
content: content as string
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error fetching Fotos.md from Nextcloud", e);
|
|
||||||
return {
|
|
||||||
content: "Failed to load content from Nextcloud."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
|
||||||
import Markdown from "$lib/components/Markdown.svelte";
|
|
||||||
import HeadImage from "$lib/images/fotos/sunne_untergang.webp";
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ImageTile src={HeadImage}>
|
|
||||||
<h2
|
|
||||||
class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg"
|
|
||||||
>
|
|
||||||
Fotos
|
|
||||||
</h2>
|
|
||||||
</ImageTile>
|
|
||||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
|
||||||
<section class="max-w-screen-md">
|
|
||||||
<Markdown content={data.content} basePath="/fotos" />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { client } from "$lib/server/nextcloud";
|
|
||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
try {
|
|
||||||
const content = await client.getFileContents("/informatik/Informatik.md", { format: "text" });
|
|
||||||
return {
|
|
||||||
content: content as string
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error fetching Informatik.md from Nextcloud", e);
|
|
||||||
return {
|
|
||||||
content: "Failed to load content from Nextcloud."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
|
||||||
import Markdown from "$lib/components/Markdown.svelte";
|
|
||||||
import HeadImage from "$lib/images/informatik/monitor.webp";
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ImageTile src={HeadImage}>
|
|
||||||
<h2
|
|
||||||
class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg"
|
|
||||||
>
|
|
||||||
Informatik
|
|
||||||
</h2>
|
|
||||||
</ImageTile>
|
|
||||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
|
||||||
<section class="max-w-screen-md">
|
|
||||||
<Markdown content={data.content} basePath="/informatik" />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { client } from "$lib/server/nextcloud";
|
|
||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
try {
|
|
||||||
const content = await client.getFileContents("/musig/Musig.md", { format: "text" });
|
|
||||||
return {
|
|
||||||
content: content as string
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error fetching Musig.md from Nextcloud", e);
|
|
||||||
return {
|
|
||||||
content: "Failed to load content from Nextcloud."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
|
||||||
import Markdown from "$lib/components/Markdown.svelte";
|
|
||||||
import HeadImage from "$lib/images/musig/plattespiler.webp";
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ImageTile src={HeadImage}>
|
|
||||||
<h2
|
|
||||||
class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg"
|
|
||||||
>
|
|
||||||
Musig
|
|
||||||
</h2>
|
|
||||||
</ImageTile>
|
|
||||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
|
||||||
<section class="max-w-screen-md">
|
|
||||||
<Markdown content={data.content} basePath="/musig" />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { client } from "$lib/server/nextcloud";
|
|
||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
try {
|
|
||||||
const content = await client.getFileContents("/video/Video.md", { format: "text" });
|
|
||||||
return {
|
|
||||||
content: content as string
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error fetching Video.md from Nextcloud", e);
|
|
||||||
return {
|
|
||||||
content: "Failed to load content from Nextcloud."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
|
||||||
import Markdown from "$lib/components/Markdown.svelte";
|
|
||||||
import HeadImage from "$lib/images/video/jochen.webp";
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ImageTile src={HeadImage}>
|
|
||||||
<h2
|
|
||||||
class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg"
|
|
||||||
>
|
|
||||||
Video Sammlig:
|
|
||||||
</h2>
|
|
||||||
</ImageTile>
|
|
||||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
|
||||||
<section class="max-w-screen-md">
|
|
||||||
<Markdown content={data.content} basePath="/video" />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
Reference in New Issue
Block a user