feat: caching
This commit is contained in:
@@ -1,31 +1,72 @@
|
||||
# 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
|
||||
npm run dev
|
||||
pnpm install
|
||||
```
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
Start the development server:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
Build for production (Node.js):
|
||||
|
||||
```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
|
||||
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.
|
||||
|
||||
## 1. Infrastructure Shift
|
||||
- [ ] Replace `@sveltejs/adapter-static` with `@sveltejs/adapter-node`.
|
||||
- [ ] Update `svelte.config.js` to use the Node adapter.
|
||||
- [ ] Remove `fallback: undefined` and other static-specific configurations.
|
||||
- [x] Replace `@sveltejs/adapter-static` with `@sveltejs/adapter-node`.
|
||||
- [x] Update `svelte.config.js` to use the Node adapter.
|
||||
- [x] Remove `fallback: undefined` and other static-specific configurations.
|
||||
|
||||
## 2. Dependencies
|
||||
- [ ] Install `webdav` client library.
|
||||
- [ ] Install `dotenv` (if not already handled by SvelteKit's built-in env support).
|
||||
- [x] Install `webdav` client library.
|
||||
- [x] Install `dotenv` (handled by SvelteKit's built-in env support).
|
||||
|
||||
## 3. Secure Configuration
|
||||
- [ ] Set up `.env` with:
|
||||
- [x] Set up `.env` with:
|
||||
- `NEXTCLOUD_URL`
|
||||
- `NEXTCLOUD_USER`
|
||||
- `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
|
||||
- [ ] Create `src/lib/server/nextcloud.ts` to initialize and export the WebDAV client.
|
||||
- [ ] Implement `+page.server.ts` for each content route:
|
||||
- [x] Create `src/lib/server/nextcloud.ts` to initialize and export the WebDAV client.
|
||||
- [x] Implement `+page.server.ts` for each content route:
|
||||
- Fetch Markdown content from Nextcloud.
|
||||
- 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
|
||||
- [ ] Create a catch-all server route (e.g., `src/routes/assets/[...path]/+server.ts`).
|
||||
- [ ] This route will:
|
||||
- [x] Create a catch-all server route (e.g., `src/routes/assets/[...path]/+server.ts`).
|
||||
- [x] This route will:
|
||||
- Authenticate with WebDAV.
|
||||
- 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)
|
||||
- [ ] Implement server-side caching (e.g., using a simple TTL cache or Redis) to avoid hitting Nextcloud on every single request.
|
||||
## 6. Optimization
|
||||
- [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 {
|
||||
src: string;
|
||||
alt?: string;
|
||||
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>
|
||||
|
||||
<div class="grid grid-cols-1 grid-rows-1">
|
||||
<img
|
||||
{src}
|
||||
src={imageSrc}
|
||||
{alt}
|
||||
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">
|
||||
{@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();
|
||||
}
|
||||
@@ -1,21 +1,98 @@
|
||||
import { createClient } from "webdav";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import { withCache } from "./cache";
|
||||
|
||||
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 = () => {
|
||||
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(
|
||||
getWebdavUrl(),
|
||||
{
|
||||
export const client = createClient(getWebdavUrl(), {
|
||||
username: env.NEXTCLOUD_USER || "",
|
||||
password: env.NEXTCLOUD_PASSWORD || ""
|
||||
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
|
||||
};
|
||||
};
|
||||
+29
-22
@@ -1,41 +1,48 @@
|
||||
<script lang="ts">
|
||||
import ImageLinkTile from "$lib/components/ImageLinkTile.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
// Import local images to preserve existing design
|
||||
import mouse from "$lib/images/home/mouse.webp";
|
||||
import drummachine from "$lib/images/home/drummachine.webp";
|
||||
import scouts from "$lib/images/home/scouts.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>
|
||||
|
||||
<main>
|
||||
<ImageLinkTile src={console_image} href="/informatik">
|
||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||
Informatik
|
||||
<main class="">
|
||||
{#if data.pages}
|
||||
{#each data.pages as page}
|
||||
<ImageLinkTile src={getTileImage(page)} href={`/${page.slug}`}>
|
||||
<h2
|
||||
class="text-3xl font-bold text-white hover:underline text-shadow-lg"
|
||||
>
|
||||
{page.title}
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- Static/External Links -->
|
||||
<ImageLinkTile src={scouts} href="https://pfadifrisco.ch/" target="_blank">
|
||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||
Pfadi
|
||||
</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>
|
||||
|
||||
@@ -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