feat: hidden pages
/ publish (push) Failing after 2m18s

This commit is contained in:
2026-01-21 18:54:29 +01:00
parent 86803dd181
commit c937be1274
11 changed files with 154 additions and 144 deletions
+7 -4
View File
@@ -1,11 +1,11 @@
# Project Context: Schreifuchs.ch # Project Context: Schreifuchs.ch
## Overview ## Overview
This project is a personal portfolio website for "Schreifuchs", featuring sections for Photography (Fotos), Informatics (Informatik), Music (Musig), and Video. It is currently being transitioned from a static site to a dynamic Node.js site powered by Nextcloud via WebDAV. This project is a personal portfolio website for "Schreifuchs", featuring sections for Photography (Fotos), Informatics (Informatik), Music (Musig), and Video. It is currently being transitioned from a static site to a dynamic Node.js site powered by Nextcloud via WebDAV.
- [Nextcloud Integration Plan](./TODO_NEXTCLOUD.md)
## Tech Stack ## Tech Stack
- **Framework:** Svelte 5 (using Runes) & SvelteKit - **Framework:** Svelte 5 (using Runes) & SvelteKit
- **Language:** TypeScript - **Language:** TypeScript
- **Styling:** Tailwind CSS (with custom text-shadow plugin) - **Styling:** Tailwind CSS (with custom text-shadow plugin)
@@ -14,6 +14,7 @@ This project is a personal portfolio website for "Schreifuchs", featuring sectio
- **Markdown:** `@ts-stack/markdown` for content rendering - **Markdown:** `@ts-stack/markdown` for content rendering
## Directory Structure ## Directory Structure
- `src/routes/`: File-based routing. - `src/routes/`: File-based routing.
- `src/routes/fotos`, `informatik`, `musig`, `video`: Feature-specific pages. - `src/routes/fotos`, `informatik`, `musig`, `video`: Feature-specific pages.
- `src/lib/`: Shared utilities and components. - `src/lib/`: Shared utilities and components.
@@ -23,12 +24,13 @@ This project is a personal portfolio website for "Schreifuchs", featuring sectio
- Contains Markdown content files (`Fotos.md`, etc.) and their associated attachments. - Contains Markdown content files (`Fotos.md`, etc.) and their associated attachments.
## Key Conventions ## Key Conventions
- **Svelte 5:** Use Runes (`$state`, `$derived`, `$props`, etc.) for reactivity. - **Svelte 5:** Use Runes (`$state`, `$derived`, `$props`, etc.) for reactivity.
- **Styling:** Use Tailwind utility classes. The font family is configured to 'Outfit'. - **Styling:** Use Tailwind utility classes. The font family is configured to 'Outfit'.
- **Content:** Content is largely driven by Markdown files located in `static/` which are fetched and parsed. - **Content:** Content is largely driven by Markdown files located on Nextcloud which are fetched and parsed.
- **Static Generation:** The site is built as a static site outputting to the `build/` directory.
## Development Scripts ## Development Scripts
- `npm run dev`: Start development server. - `npm run dev`: Start development server.
- `npm run build`: Build for production. - `npm run build`: Build for production.
- `npm run check`: Run Svelte and TypeScript checks. - `npm run check`: Run Svelte and TypeScript checks.
@@ -60,3 +62,4 @@ You MUST use this tool whenever writing Svelte code before sending it to the use
Generates a Svelte Playground link with the provided code. Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
+4 -1
View File
@@ -7,7 +7,9 @@ The site is built with **Svelte 5** and **SvelteKit**, and it is powered by a dy
## Features ## Features
- **Dynamic Content:** Pages are automatically discovered based on the folder structure in Nextcloud. - **Dynamic Content:** Pages are automatically discovered based on the folder structure in Nextcloud.
- **Hidden Sites:** Folders starting with `_` are excluded from the navigation menu but remain accessible via direct links.
- **Markdown Driven:** Content for each page is fetched from a corresponding Markdown file. - **Markdown Driven:** Content for each page is fetched from a corresponding Markdown file.
- **Image Optimization:** Automatic image resizing via Nextcloud's Preview API for optimized delivery and `srcset` support.
- **Asset Proxying:** Images and attachments are securely streamed from Nextcloud through a server-side proxy. - **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). - **Performance:** In-memory caching for Nextcloud requests (15-minute TTL).
- **Responsive Design:** Styled with Tailwind CSS, optimized for all screen sizes. - **Responsive Design:** Styled with Tailwind CSS, optimized for all screen sizes.
@@ -36,7 +38,8 @@ NEXTCLOUD_BASE_DIR=WebsiteContent # Optional: Root folder for site content
To add or update content, manage files in your configured Nextcloud directory: To add or update content, manage files in your configured Nextcloud directory:
1. **Create a Page:** Create a folder (e.g., `Travel`). 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`). - **Hidden Sites:** To hide a page from the navigation menu, prefix the folder name with an underscore (e.g., `_Secret`). It will still be accessible at `/secret`.
2. **Add Content:** Inside that folder, create a Markdown file named after the folder (e.g., `Travel.md` or `Secret.md` for `_Secret`).
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. 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. 4. **Add Assets:** Any other images or files uploaded to the folder can be linked in the Markdown.
-37
View File
@@ -1,37 +0,0 @@
# 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
- [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
- [x] Install `webdav` client library.
- [x] Install `dotenv` (handled by SvelteKit's built-in env support).
## 3. Secure Configuration
- [x] Set up `.env` with:
- `NEXTCLOUD_URL`
- `NEXTCLOUD_USER`
- `NEXTCLOUD_PASSWORD`
- [x] Use `$env/dynamic/private` to access these in server-side code only.
## 4. Server-Side Data Fetching
- [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.
- [x] Update `Markdown.svelte` to receive and render raw strings instead of fetching from a URL on the client.
## 5. Asset Proxying
- [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.
- [x] Update Markdown rendering logic to rewrite image URLs to point to this proxy route.
## 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.
+2 -1
View File
@@ -3,5 +3,6 @@
"hello_world": "Hallo Welt", "hello_world": "Hallo Welt",
"pfadi": "Pfadi", "pfadi": "Pfadi",
"site_title": "Schreifuchs.ch", "site_title": "Schreifuchs.ch",
"site_description": "Portfolio von Niklas - Fotografie, Informatik, Musik und Video." "site_description": "Portfolio von Schreifuchs - Fotografie, Informatik und Musik."
} }
+2 -1
View File
@@ -3,5 +3,6 @@
"hello_world": "Hello World", "hello_world": "Hello World",
"pfadi": "Scouts", "pfadi": "Scouts",
"site_title": "Schreifuchs.ch", "site_title": "Schreifuchs.ch",
"site_description": "Portfolio of Niklas - Photography, Informatics, Music and Video." "site_description": "Portfolio of Schreifuchs - Photography, Informatics and Music ."
} }
+1
View File
@@ -0,0 +1 @@
cache
+1
View File
@@ -0,0 +1 @@
aa922b989658d017b7a6296093cef3dac23a39ddc1afa6cf7ea24c049a284b56
+3 -4
View File
@@ -1,10 +1,8 @@
{ {
"$schema": "https://inlang.com/schema/project-settings", "$schema": "https://inlang.com/schema/project-settings",
"telemetry": "off",
"sourceLanguageTag": "de", "sourceLanguageTag": "de",
"languageTags": [ "languageTags": ["de", "en"],
"de",
"en"
],
"modules": [ "modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js", "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js" "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
@@ -13,3 +11,4 @@
"pathPattern": "./messages/{languageTag}.json" "pathPattern": "./messages/{languageTag}.json"
} }
} }
+3 -1
View File
@@ -64,7 +64,9 @@ export async function getPages(lang: string = 'de') {
const directories = items.filter( const directories = items.filter(
(item: any) => (item: any) =>
item.type === "directory" && !item.basename.startsWith("."), item.type === "directory" &&
!item.basename.startsWith(".") &&
!item.basename.startsWith("_"),
); );
const pages = await Promise.all( const pages = await Promise.all(
+32 -12
View File
@@ -6,44 +6,64 @@ export const load: PageServerLoad = async ({ params }) => {
const { slug, lang } = params; const { slug, lang } = params;
const language = lang || 'de'; const language = lang || 'de';
// Capitalize the first letter to match the convention: fotos -> Fotos.md // Try both the regular slug and the hidden (_slug) folder
const baseName = slug.charAt(0).toUpperCase() + slug.slice(1); const slugsToTry = [slug, `_${slug}`];
const filename = language === 'de' ? `${baseName}.md` : `${baseName}.${language}.md`; let rawContent = "";
const path = `/${slug}/${filename}`; let finalSlug = slug;
let baseName = "";
let filename = "";
for (const s of slugsToTry) {
const cleanSlug = s.startsWith('_') ? s.slice(1) : s;
baseName = cleanSlug.charAt(0).toUpperCase() + cleanSlug.slice(1);
filename = language === 'de' ? `${baseName}.md` : `${baseName}.${language}.md`;
const path = `/${s}/${filename}`;
try { try {
let rawContent;
try { try {
rawContent = await getPageContent(path); rawContent = await getPageContent(path);
finalSlug = s;
break; // Found it!
} catch (e) { } catch (e) {
if (language !== 'de') { if (language !== 'de') {
console.warn(`Localized content ${filename} not found, falling back to German.`); console.warn(`Localized content ${filename} not found in ${s}, falling back to German.`);
rawContent = await getPageContent(`/${slug}/${baseName}.md`); rawContent = await getPageContent(`/${s}/${baseName}.md`);
finalSlug = s;
break; // Found fallback
} else { } else {
throw e; throw e; // Try next slug variant
}
}
} catch (e) {
if (s === slugsToTry[slugsToTry.length - 1]) {
console.error(`Error fetching page from Nextcloud for slug ${slug}`, e);
throw error(404, "Page not found");
}
} }
} }
try {
// Extract the first header (e.g., # Title or ## Title) // Extract the first header (e.g., # Title or ## Title)
const headerMatch = rawContent.match(/^#{1,6}\s+(.+)$/m); const headerMatch = rawContent.match(/^#{1,6}\s+(.+)$/m);
const title = headerMatch ? headerMatch[1] : baseName; const title = headerMatch ? headerMatch[1] : (finalSlug.startsWith('_') ? finalSlug.slice(1) : finalSlug);
// Remove the first header from content so it's not rendered twice // Remove the first header from content so it's not rendered twice
const content = headerMatch const content = headerMatch
? rawContent.replace(headerMatch[0], "").trim() ? rawContent.replace(headerMatch[0], "").trim()
: rawContent; : rawContent;
const coverImage = await getCoverImage(slug); const coverImage = await getCoverImage(finalSlug);
return { return {
content, content,
title, title,
slug, slug: finalSlug,
coverImage, coverImage,
lang: language lang: language
}; };
} catch (e) { } catch (e) {
console.error(`Error fetching ${filename} from Nextcloud`, e); console.error(`Error processing content for ${finalSlug}`, e);
throw error(404, "Page not found"); throw error(404, "Page not found");
} }
}; };
+31 -15
View File
@@ -17,17 +17,30 @@ export const GET: RequestHandler = async ({ params, url }) => {
let buffer: ArrayBuffer; let buffer: ArrayBuffer;
let contentType: string; let contentType: string;
// Try both original path and hidden variant if applicable
const pathsToTry = [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) { if (width || height) {
// Use Nextcloud Preview API // Use Nextcloud Preview API
const baseUrl = env.NEXTCLOUD_URL?.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`; const baseUrl = env.NEXTCLOUD_URL?.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`;
let fullPath = path; let fullPath = currentPath;
if (env.NEXTCLOUD_BASE_DIR) { if (env.NEXTCLOUD_BASE_DIR) {
const baseDir = env.NEXTCLOUD_BASE_DIR.replace(/^\/+|\/+$/g, ""); const baseDir = env.NEXTCLOUD_BASE_DIR.replace(/^\/+|\/+$/g, "");
fullPath = baseDir ? `${baseDir}/${path}` : path; fullPath = baseDir ? `${baseDir}/${currentPath}` : currentPath;
} }
const previewUrl = new URL(`${baseUrl}index.php/core/preview`); 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 // Ensure leading slash for the file parameter
const fileParam = fullPath.startsWith('/') ? fullPath : `/${fullPath}`; const fileParam = fullPath.startsWith('/') ? fullPath : `/${fullPath}`;
previewUrl.searchParams.set("file", fileParam); previewUrl.searchParams.set("file", fileParam);
@@ -39,16 +52,15 @@ export const GET: RequestHandler = async ({ params, url }) => {
console.log(`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`); console.log(`Fetching preview: ${previewUrl.toString()} for file: ${fullPath}`);
try { const ncResponse = await fetch(previewUrl.toString(), {
const response = await fetch(previewUrl.toString(), {
headers: { headers: {
"Authorization": `Basic ${auth}` "Authorization": `Basic ${auth}`
} }
}); });
if (response.ok) { if (ncResponse.ok) {
buffer = await response.arrayBuffer(); buffer = await ncResponse.arrayBuffer();
contentType = response.headers.get("Content-Type") || "image/png"; contentType = ncResponse.headers.get("Content-Type") || "image/png";
return new Response(buffer, { return new Response(buffer, {
headers: { headers: {
@@ -58,18 +70,15 @@ export const GET: RequestHandler = async ({ params, url }) => {
} }
}); });
} else { } else {
console.warn(`Nextcloud preview failed (${response.status} ${response.statusText}) for ${fullPath}. Falling back to original.`); console.warn(`Nextcloud preview failed (${ncResponse.status}) for ${fullPath}.`);
}
} catch (fetchError) {
console.error(`Fetch error during preview request for ${fullPath}:`, fetchError);
} }
} }
// Fetch original via WebDAV (Fallback or no resize requested) // Fetch original via WebDAV (Fallback or no resize requested)
console.log(`Fetching original file via WebDAV: /${path}`); console.log(`Fetching original file via WebDAV: /${currentPath}`);
const data = await client.getFileContents("/" + path); const data = await client.getFileContents("/" + currentPath);
buffer = data as ArrayBuffer; buffer = data as ArrayBuffer;
contentType = mime.lookup(path) || "application/octet-stream"; contentType = mime.lookup(currentPath) || "application/octet-stream";
return new Response(buffer, { return new Response(buffer, {
headers: { headers: {
@@ -78,6 +87,13 @@ export const GET: RequestHandler = async ({ params, url }) => {
"Vary": "Accept-Encoding" "Vary": "Accept-Encoding"
} }
}); });
} catch (e) {
lastError = e;
continue; // Try next path variant
}
}
throw lastError || new Error("Asset not found");
} catch (e) { } catch (e) {
console.error(`Error fetching asset from Nextcloud: ${path}`, e); console.error(`Error fetching asset from Nextcloud: ${path}`, e);
throw error(404, "Asset not found"); throw error(404, "Asset not found");