feat: initialize template
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json",
|
||||
"mcpServers": {
|
||||
"svelte": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@sveltejs/mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
# Project Context: Schreifuchs.ch
|
||||
|
||||
## 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.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** Svelte 5 (using Runes) & SvelteKit
|
||||
- **Language:** TypeScript
|
||||
- **Styling:** Tailwind CSS (with custom text-shadow plugin)
|
||||
- **Bundler:** Vite
|
||||
- **Adapter:** `@sveltejs/adapter-static` (SSG)
|
||||
- **Markdown:** `@ts-stack/markdown` for content rendering
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `src/routes/`: File-based routing.
|
||||
- `src/routes/fotos`, `informatik`, `musig`, `video`: Feature-specific pages.
|
||||
- `src/lib/`: Shared utilities and components.
|
||||
- `src/lib/components/`: Reusable UI components (`ImageTile`, `ImageLinkTile`, `Markdown`).
|
||||
- `src/lib/images/`: Local image assets imported in components.
|
||||
- `static/`: Static assets served as-is.
|
||||
- Contains Markdown content files (`Fotos.md`, etc.) and their associated attachments.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- **Svelte 5:** Use Runes (`$state`, `$derived`, `$props`, etc.) for reactivity.
|
||||
- **Styling:** Use Tailwind utility classes. The font family is configured to 'Outfit'.
|
||||
- **Content:** Content is largely driven by Markdown files located on Nextcloud which are fetched and parsed.
|
||||
|
||||
## Development Scripts
|
||||
|
||||
- `npm run dev`: Start development server.
|
||||
- `npm run build`: Build for production.
|
||||
- `npm run check`: Run Svelte and TypeScript checks.
|
||||
|
||||
---
|
||||
|
||||
# Agent Instructions (Svelte MCP)
|
||||
|
||||
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
## Available MCP Tools:
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||
|
||||
### 3. svelte-autofixer
|
||||
|
||||
Analyzes Svelte code and returns issues and suggestions.
|
||||
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||
|
||||
### 4. playground-link
|
||||
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := server.NewServer()
|
||||
|
||||
slog.Info("Server started", "addr", server.Addr)
|
||||
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
slog.Error("Server failed to start", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
module git.schreifuchs.ch/schreifuchs/schreifuchs.ch
|
||||
|
||||
go 1.25.7
|
||||
|
||||
tool github.com/a-h/templ/cmd/templ
|
||||
|
||||
require github.com/a-h/templ v0.3.977
|
||||
|
||||
require (
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/natefinch/atomic v1.0.1 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/pages"
|
||||
)
|
||||
|
||||
func Home() http.Handler {
|
||||
return templ.Handler(pages.Index())
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/pages"
|
||||
)
|
||||
|
||||
func Home() http.Handler {
|
||||
return templ.Handler(pages.Index())
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handler"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web"
|
||||
)
|
||||
|
||||
func (s *Server) RegisterRoutes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
fileServer := http.FileServer(http.FS(web.GetStaticFS()))
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
|
||||
|
||||
mux.Handle("/", handler.Home())
|
||||
|
||||
return s.loggingMiddleware(mux)
|
||||
}
|
||||
|
||||
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
slog.Info("request",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"duration", time.Since(start),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
port int
|
||||
}
|
||||
|
||||
func NewServer() *http.Server {
|
||||
port, _ := strconv.Atoi(os.Getenv("PORT"))
|
||||
if port == 0 {
|
||||
port = 8080
|
||||
}
|
||||
s := &Server{
|
||||
port: port,
|
||||
}
|
||||
|
||||
// Declare Server config
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", s.port),
|
||||
Handler: s.RegisterRoutes(),
|
||||
IdleTimeout: time.Minute,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/message-format-plugin",
|
||||
"hello_world": "Hallo Welt",
|
||||
"pfadi": "Pfadi",
|
||||
"site_title": "Schreifuchs.ch",
|
||||
"site_description": "Portfolio von Schreifuchs - Fotografie, Informatik und Musik."
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/message-format-plugin",
|
||||
"hello_world": "Hello World",
|
||||
"pfadi": "Scouts",
|
||||
"site_title": "Schreifuchs.ch",
|
||||
"site_description": "Portfolio of Schreifuchs - Photography, Informatics and Music ."
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
cache
|
||||
@@ -1 +0,0 @@
|
||||
aa922b989658d017b7a6296093cef3dac23a39ddc1afa6cf7ea24c049a284b56
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"telemetry": "off",
|
||||
"sourceLanguageTag": "de",
|
||||
"languageTags": ["de", "en"],
|
||||
"modules": [
|
||||
"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"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{languageTag}.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
@import "@fontsource/outfit";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,13 +0,0 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,22 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="%paraglide.lang%" dir="%paraglide.textDirection%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script
|
||||
defer
|
||||
src="https://umami.schreifuchs.ch/script.js"
|
||||
data-website-id="f09f9cc5-6357-48df-8fb2-5c62089c04a0"
|
||||
></script>
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>schreifuchs.ch</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Portfolio von Niklas - Fotografie, Informatik, Musik und Video."
|
||||
/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +0,0 @@
|
||||
import { i18n } from "$lib/i18n"
|
||||
|
||||
export const handle = i18n.handle()
|
||||
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ImageTile from "./ImageTile.svelte";
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
href: string;
|
||||
target?: "_self" | "_blank" | "_parent" | "_top";
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
src,
|
||||
href,
|
||||
target = "_self",
|
||||
children
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ImageTile {src}>
|
||||
<a {href} {target} class="flex items-center justify-center h-full">
|
||||
{@render children?.()}
|
||||
</a>
|
||||
</ImageTile>
|
||||
</div>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
alt?: string;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { src, alt = "", children }: Props = $props();
|
||||
|
||||
const placeholder = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxyZWN0IGZpbGw9IiMzMzMiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48L3N2Zz4=';
|
||||
|
||||
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>
|
||||
|
||||
<div class="grid grid-cols-1 grid-rows-1">
|
||||
<img
|
||||
src={imageSrc}
|
||||
{srcset}
|
||||
{sizes}
|
||||
{alt}
|
||||
loading="lazy"
|
||||
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;
|
||||
// Clear srcset on error to avoid loading broken thumbnails
|
||||
img.srcset = "";
|
||||
}}
|
||||
/>
|
||||
<div class="row-start-1 col-start-1">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,99 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Marked, Renderer } from "@ts-stack/markdown";
|
||||
|
||||
interface Props {
|
||||
content?: string;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
let { content = "", basePath = "" }: Props = $props();
|
||||
|
||||
const renderer = new Renderer();
|
||||
|
||||
// Custom image renderer to proxy through /assets/
|
||||
renderer.image = (href, title, text) => {
|
||||
let src = href;
|
||||
if (href && !href.startsWith('http')) {
|
||||
// Ensure basePath starts with / if provided
|
||||
const normalizedBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;
|
||||
// If href starts with /, it's relative to root. Otherwise, relative to basePath.
|
||||
if (!href.startsWith('/')) {
|
||||
src = `${normalizedBasePath}/${href}`.replace(/\/+/g, '/');
|
||||
}
|
||||
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 || ''}" loading="lazy" />`;
|
||||
};
|
||||
|
||||
let html = $derived(
|
||||
Marked.parse(content || "__nothing to see here__", {
|
||||
renderer,
|
||||
gfm: true,
|
||||
tables: true,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
sanitize: false,
|
||||
smartLists: true,
|
||||
smartypants: false,
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="markdown">
|
||||
{@html html}
|
||||
</div>
|
||||
|
||||
<style global>
|
||||
.markdown {
|
||||
color: white;
|
||||
text-align: left;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.markdown h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.markdown h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
.markdown h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.markdown h4 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.markdown * {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
max-height: 90vh;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.markdown table,
|
||||
.markdown tr,
|
||||
.markdown th,
|
||||
.markdown td {
|
||||
border-collapse: collapse;
|
||||
border: 1px solid;
|
||||
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
||||
|
||||
.markdown th {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createI18n } from "@inlang/paraglide-sveltekit"
|
||||
import * as runtime from "$lib/paraglide/runtime"
|
||||
|
||||
export const i18n = createI18n(runtime, {
|
||||
pathnames: {
|
||||
"/about": {
|
||||
en: "/about",
|
||||
de: "/ueber-uns"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
Before Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 423 KiB |
|
Before Width: | Height: | Size: 439 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 766 KiB |
@@ -1 +0,0 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -1,87 +0,0 @@
|
||||
import { LRUCache } from "lru-cache";
|
||||
|
||||
// const cache = new Map<string, { timestamp: number; data: any }>();
|
||||
const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL
|
||||
const MAX_CACHE_SIZE = 1000 * 1000 * 750; // 750 MB
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 100,
|
||||
// Maximum allowed size (in bytes)
|
||||
maxSize: MAX_CACHE_SIZE,
|
||||
|
||||
// IMPORTANT: We must teach the cache how to calculate the size of an item.
|
||||
// We check for your specific image structure, generic buffers, or fallback to JSON length.
|
||||
sizeCalculation: (value) => {
|
||||
// 1. Check if it's our specific image object { buffer: ArrayBuffer, ... }
|
||||
if (value && typeof value === "object") {
|
||||
const obj = value as Record<string, any>;
|
||||
if (obj.buffer instanceof ArrayBuffer) {
|
||||
return obj.buffer.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Handle raw ArrayBuffers or Node.js Buffers
|
||||
if (value instanceof ArrayBuffer) {
|
||||
return value.byteLength;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(value)) {
|
||||
return value.length;
|
||||
}
|
||||
|
||||
// 3. Fallback for strings or JSON metadata
|
||||
try {
|
||||
const str = JSON.stringify(value);
|
||||
return str ? str.length : 1;
|
||||
} catch {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
ttl: 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 entry = cache.get(key);
|
||||
|
||||
if (entry) {
|
||||
return entry as T;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetcher();
|
||||
if (data) {
|
||||
cache.set(key, data, { ttl: ttl });
|
||||
}
|
||||
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,124 +0,0 @@
|
||||
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.",
|
||||
);
|
||||
}
|
||||
|
||||
// Construct the standard Nextcloud WebDAV URL: https://example.com/remote.php/dav/files/USERNAME/[BASE_DIR/]
|
||||
const getWebdavUrl = () => {
|
||||
if (!env.NEXTCLOUD_URL) return "";
|
||||
|
||||
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(), {
|
||||
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);
|
||||
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(lang: string = 'de') {
|
||||
return withCache(`pages:${lang}`, async () => {
|
||||
try {
|
||||
const items = await client.getDirectoryContents("/");
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const directories = items.filter(
|
||||
(item: any) =>
|
||||
item.type === "directory" &&
|
||||
!item.basename.startsWith(".") &&
|
||||
!item.basename.startsWith("_"),
|
||||
);
|
||||
|
||||
const pages = await Promise.all(
|
||||
directories.map(async (item: any) => {
|
||||
const slug = item.basename;
|
||||
const baseName = slug.charAt(0).toUpperCase() + slug.slice(1);
|
||||
const filename = lang === 'de' ? `${baseName}.md` : `${baseName}.${lang}.md`;
|
||||
const path = `/${slug}/${filename}`;
|
||||
|
||||
let title = baseName;
|
||||
try {
|
||||
const content = await getPageContent(path);
|
||||
const headerMatch = content.match(/^#{1,6}\s+(.+)$/m);
|
||||
if (headerMatch) {
|
||||
title = headerMatch[1];
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to German if English is missing
|
||||
if (lang !== 'de') {
|
||||
try {
|
||||
const fallbackContent = await getPageContent(`/${slug}/${baseName}.md`);
|
||||
const headerMatch = fallbackContent.match(/^#{1,6}\s+(.+)$/m);
|
||||
if (headerMatch) {
|
||||
title = headerMatch[1];
|
||||
}
|
||||
} catch (innerE) {
|
||||
console.warn(`Could not fetch fallback title for ${slug}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { ParamMatcher } from '@sveltejs/kit';
|
||||
|
||||
export const match: ParamMatcher = (param) => {
|
||||
return ['de', 'en'].includes(param);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { getPages } from '$lib/server/nextcloud';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ params }) => {
|
||||
const pages = await getPages(params.lang);
|
||||
return {
|
||||
pages
|
||||
};
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
<script lang="ts">
|
||||
import "../app.css";
|
||||
import type { Snippet } from "svelte";
|
||||
import { ParaglideJS } from "@inlang/paraglide-sveltekit";
|
||||
import { i18n } from "$lib/i18n";
|
||||
import { languageTag } from "$lib/paraglide/runtime";
|
||||
import { page } from "$app/state";
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
let { children }: { children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.site_title()}</title>
|
||||
<meta name="description" content={m.site_description()} />
|
||||
</svelte:head>
|
||||
|
||||
<ParaglideJS {i18n}>
|
||||
<header
|
||||
class="fixed z-50 mix-blend-difference p-1 w-full flex justify-between items-start"
|
||||
>
|
||||
<a href={i18n.resolveRoute("/")} class="text-white text-2xl"
|
||||
>schreifuchs.ch</a
|
||||
>
|
||||
<nav class="flex gap-2 p-2">
|
||||
<a
|
||||
href={i18n.resolveRoute(i18n.route(page.url.pathname), "de")}
|
||||
hreflang="de"
|
||||
data-no-translate
|
||||
class="text-white hover:underline {languageTag() === 'de'
|
||||
? 'font-bold'
|
||||
: ''}"
|
||||
>
|
||||
DE
|
||||
</a>
|
||||
<a
|
||||
href={i18n.resolveRoute(i18n.route(page.url.pathname), "en")}
|
||||
hreflang="en"
|
||||
data-no-translate
|
||||
class="text-white hover:underline {languageTag() === 'en'
|
||||
? 'font-bold'
|
||||
: ''}"
|
||||
>
|
||||
EN
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
{@render children?.()}
|
||||
</ParaglideJS>
|
||||
@@ -1,3 +0,0 @@
|
||||
// This can be false if you're using a fallback (i.e. SPA mode)
|
||||
// export const prerender = true;
|
||||
// export const trailingSlash = 'always';
|
||||
@@ -1,50 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ImageLinkTile from "$lib/components/ImageLinkTile.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import { i18n } from "$lib/i18n";
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
// 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";
|
||||
|
||||
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 page.coverImage || staticImages[page.slug.toLowerCase()] || "";
|
||||
};
|
||||
</script>
|
||||
|
||||
<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">
|
||||
{m.pfadi()}
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
</main>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
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, lang } = params;
|
||||
const language = lang || 'de';
|
||||
|
||||
// Try both the regular slug and the hidden (_slug) folder
|
||||
const slugsToTry = [slug, `_${slug}`];
|
||||
let rawContent = "";
|
||||
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 {
|
||||
rawContent = await getPageContent(path);
|
||||
finalSlug = s;
|
||||
break; // Found it!
|
||||
} catch (e) {
|
||||
if (language !== 'de') {
|
||||
console.warn(`Localized content ${filename} not found in ${s}, falling back to German.`);
|
||||
rawContent = await getPageContent(`/${s}/${baseName}.md`);
|
||||
finalSlug = s;
|
||||
break; // Found fallback
|
||||
} else {
|
||||
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)
|
||||
const headerMatch = rawContent.match(/^#{1,6}\s+(.+)$/m);
|
||||
const title = headerMatch ? headerMatch[1] : (finalSlug.startsWith('_') ? finalSlug.slice(1) : finalSlug);
|
||||
|
||||
// Remove the first header from content so it's not rendered twice
|
||||
const content = headerMatch
|
||||
? rawContent.replace(headerMatch[0], "").trim()
|
||||
: rawContent;
|
||||
|
||||
const coverImage = await getCoverImage(finalSlug);
|
||||
|
||||
return {
|
||||
content,
|
||||
title,
|
||||
slug: finalSlug,
|
||||
coverImage,
|
||||
lang: language
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`Error processing content for ${finalSlug}`, e);
|
||||
throw error(404, "Page not found");
|
||||
}
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
<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>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.title} - schreifuchs.ch</title>
|
||||
</svelte:head>
|
||||
|
||||
<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,136 +0,0 @@
|
||||
// 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";
|
||||
|
||||
const DEFAULT_TTL = 1000 * 60 * 15; // 15min
|
||||
const EXTENDED_TTL = 1000 * 60 * 60 * 24 * 7; // 1 week
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
// 1. Create a unique cache key for this specific asset variation
|
||||
const cacheKey = `asset:${path}?w=${width ?? ""}&h=${height ?? ""}`;
|
||||
|
||||
const ttl = path.toString().includes("cover") ? DEFAULT_TTL : EXTENDED_TTL;
|
||||
|
||||
try {
|
||||
// 2. Use withCache to wrap the fetching logic
|
||||
const cachedData = await withCache<CachedAsset>(
|
||||
cacheKey,
|
||||
async () => {
|
||||
// --- START OF FETCH LOGIC ---
|
||||
// This function only runs if the data is NOT in the cache
|
||||
|
||||
const pathsToTry = [path];
|
||||
if (!path.startsWith("_")) {
|
||||
pathsToTry.push(`_${path}`);
|
||||
}
|
||||
let lastError: any = null;
|
||||
|
||||
for (const currentPath of pathsToTry) {
|
||||
try {
|
||||
// A. Try Nextcloud Preview (Resizing)
|
||||
if (width || height) {
|
||||
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;
|
||||
}
|
||||
|
||||
const isJpg = fullPath.match(/\.(jpg|jpeg)$/i);
|
||||
const previewExtension = isJpg ? "jpg" : "png";
|
||||
const previewUrl = new URL(
|
||||
`${baseUrl}index.php/core/preview.${previewExtension}`,
|
||||
);
|
||||
|
||||
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}`,
|
||||
);
|
||||
|
||||
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");
|
||||
// --- END OF FETCH LOGIC ---
|
||||
},
|
||||
ttl,
|
||||
);
|
||||
|
||||
// 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");
|
||||
}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { sveltePreprocess } from 'svelte-preprocess';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
},
|
||||
preprocess: [vitePreprocess(), sveltePreprocess({})]
|
||||
};
|
||||
export default config;
|
||||
@@ -1,29 +0,0 @@
|
||||
import plugin from "tailwindcss/plugin"
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Outfit', 'outfit', 'sans-serif']
|
||||
},
|
||||
textShadow: {
|
||||
sm: '0 1px 2px #000',
|
||||
DEFAULT: '0 2px 4px #000',
|
||||
lg: '0px 0px 20px #000',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
plugin(function({ matchUtilities, theme }) {
|
||||
matchUtilities(
|
||||
{
|
||||
'text-shadow': (value) => ({
|
||||
textShadow: value,
|
||||
}),
|
||||
},
|
||||
{ values: theme('textShadow') }
|
||||
)
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||
@@ -1,13 +0,0 @@
|
||||
import { paraglide } from '@inlang/paraglide-sveltekit/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
paraglide({
|
||||
project: './project.inlang',
|
||||
outdir: './src/lib/paraglide'
|
||||
}),
|
||||
sveltekit()
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
package components
|
||||
|
||||
templ Hello() {
|
||||
<h1>Hello World</h1>
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.977
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Hello() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1>Hello World</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,18 @@
|
||||
package layouts
|
||||
|
||||
templ Base(title string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>{ title }</title>
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png"/>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
{ children... }
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.977
|
||||
package layouts
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Base(title string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `layouts/base.templ`, Line: 9, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"icon\" type=\"image/png\" href=\"/static/favicon.png\"></head><body><main>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,12 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/layouts"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/components"
|
||||
)
|
||||
|
||||
templ Index() {
|
||||
@layouts.Base("Home") {
|
||||
@components.Hello()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.977
|
||||
package pages
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/components"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/layouts"
|
||||
)
|
||||
|
||||
func Index() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = components.Hello().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layouts.Base("Home").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:generate go tool templ generate
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var Static embed.FS
|
||||
|
||||
func GetStaticFS() fs.FS {
|
||||
// fs.Sub returns an fs.FS corresponding to the subtree rooted at "static"
|
||||
f, err := fs.Sub(Static, "static")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |