Compare commits
40 Commits
ff6aff25e8
...
v0.1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9006b208af | ||
|
|
20c8ad7eaf | ||
|
|
03129a5611 | ||
|
|
fd8f4adcc0 | ||
|
|
23997ccc3a | ||
|
|
85bdbf795c | ||
|
|
ac0bb02ba0 | ||
|
|
63269c2600 | ||
|
|
17a39d677d | ||
|
|
1ff0787e4e | ||
|
|
ed5f02bdec | ||
|
|
e3a09864a6 | ||
|
|
107739ade9 | ||
|
|
e4eae67612 | ||
|
|
891f52b0b8 | ||
|
|
725d3bcff4 | ||
|
|
69f69cf172 | ||
|
|
68c8f9a05a | ||
|
|
2b8733dea0 | ||
|
|
317eed5ea6 | ||
|
|
a503edb220 | ||
|
|
a80c14223b | ||
|
|
8c9c4eb159 | ||
|
|
68dfba38df | ||
|
|
b51ad2ff1a | ||
|
|
5613e5d48e | ||
|
|
09b998ea75 | ||
|
|
74a8a59083 | ||
|
|
f2c64281dd | ||
|
|
ca40b1efb9 | ||
|
|
3c051ec49d | ||
|
|
b268abb7d3 | ||
|
|
c7793dcb9d | ||
|
|
95fd6405be | ||
|
|
e881979da3 | ||
|
|
8ec713297a | ||
|
|
4aef034aa6 | ||
|
|
b120e5df45 | ||
|
|
68c074e9da | ||
|
|
20910e5cbf |
67
.dockerignore
Normal file
67
.dockerignore
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Next.js build outputs
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
build
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# Git (NICHT ausschließen - wird für Version-Extraktion benötigt!)
|
||||||
|
# .git wird benötigt für: git describe --tags --always
|
||||||
|
# .gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Hördle specific - WICHTIG: Upload-Dateien NICHT ins Image kopieren!
|
||||||
|
# Diese werden als Volume gemountet und sollten nicht im Image sein
|
||||||
|
/public/uploads/*
|
||||||
|
!/public/uploads/.gitkeep
|
||||||
|
|
||||||
|
# Database files - werden als Volume gemountet
|
||||||
|
/data/*
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Backups
|
||||||
|
/backups
|
||||||
|
|
||||||
|
# Docker files (nicht notwendig im Image)
|
||||||
|
docker-compose*.yml
|
||||||
|
Dockerfile*
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
/docs
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# Scripts die nicht im Container gebraucht werden
|
||||||
|
scripts/fix-*.sh
|
||||||
|
scripts/check-*.sh
|
||||||
|
scripts/debug-*.sh
|
||||||
|
scripts/quick-*.sh
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.log
|
||||||
|
|
||||||
106
.env.example
Normal file
106
.env.example
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# ============================================
|
||||||
|
# Hördle Environment Variables
|
||||||
|
# ============================================
|
||||||
|
# Kopiere diese Datei zu .env und passe die Werte an deine Umgebung an:
|
||||||
|
# cp .env.example .env
|
||||||
|
#
|
||||||
|
# WICHTIG: Die .env-Datei sollte niemals in Git committed werden!
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Build-Time Variables (NEXT_PUBLIC_*)
|
||||||
|
# ============================================
|
||||||
|
# Diese Variablen werden beim Build-Zeitpunkt in die Next.js-App eingebettet.
|
||||||
|
# Nach dem Build können sie nicht mehr geändert werden (ohne Rebuild).
|
||||||
|
|
||||||
|
# App-Name (wird in Browser-Tab, PWA, etc. verwendet)
|
||||||
|
NEXT_PUBLIC_APP_NAME=Hördle
|
||||||
|
|
||||||
|
# App-Beschreibung (für SEO, PWA, etc.)
|
||||||
|
NEXT_PUBLIC_APP_DESCRIPTION=Daily music guessing game - Guess the song from short audio clips
|
||||||
|
|
||||||
|
# Hauptdomain (ohne https://)
|
||||||
|
NEXT_PUBLIC_DOMAIN=hoerdle.de
|
||||||
|
|
||||||
|
# Twitter/X Handle (für Meta-Tags)
|
||||||
|
NEXT_PUBLIC_TWITTER_HANDLE=@hoerdle
|
||||||
|
|
||||||
|
# Plausible Analytics - Domain
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=hoerdle.de
|
||||||
|
|
||||||
|
# Plausible Analytics - Script-URL (selbst gehostet oder extern)
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC=https://plausible.example.com/js/script.js
|
||||||
|
|
||||||
|
# Theme-Farbe (für Browser-UI, PWA, etc.)
|
||||||
|
NEXT_PUBLIC_THEME_COLOR=#000000
|
||||||
|
|
||||||
|
# Hintergrundfarbe (für PWA, etc.)
|
||||||
|
NEXT_PUBLIC_BACKGROUND_COLOR=#ffffff
|
||||||
|
|
||||||
|
# Credits im Footer aktivieren (true/false)
|
||||||
|
NEXT_PUBLIC_CREDITS_ENABLED=true
|
||||||
|
|
||||||
|
# Credits-Text (vor dem Link)
|
||||||
|
NEXT_PUBLIC_CREDITS_TEXT=Vibe coded with ☕ and 🍺 by
|
||||||
|
|
||||||
|
# Credits-Link-Text
|
||||||
|
NEXT_PUBLIC_CREDITS_LINK_TEXT=@yourhandle@server.social
|
||||||
|
|
||||||
|
# Credits-Link-URL
|
||||||
|
NEXT_PUBLIC_CREDITS_LINK_URL=https://server.social/@yourhandle
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Runtime Variables
|
||||||
|
# ============================================
|
||||||
|
# Diese Variablen können zur Laufzeit geändert werden (benötigen keinen Rebuild).
|
||||||
|
|
||||||
|
# Datenbank-URL (SQLite für lokale/kleine Deployments)
|
||||||
|
# Format: file:/path/to/database.db
|
||||||
|
DATABASE_URL=file:/app/data/prod.db
|
||||||
|
|
||||||
|
# Admin-Passwort (bcrypt Hash)
|
||||||
|
# Generiere einen Hash mit: node scripts/hash-password.js dein_passwort
|
||||||
|
# In docker-compose.yml müssen $ als $$ escaped werden!
|
||||||
|
ADMIN_PASSWORD=$2b$10$SHOt9G1qUNIvHoWre7499.eEtp5PtOII0daOQGNV.dhDEuPmOUdsq
|
||||||
|
|
||||||
|
# Zeitzone (für tägliche Puzzle-Rotation)
|
||||||
|
TZ=Europe/Berlin
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Optional: Gotify Integration
|
||||||
|
# ============================================
|
||||||
|
# Für Benachrichtigungen (z.B. Fehler-Alerts)
|
||||||
|
|
||||||
|
# Gotify Server URL
|
||||||
|
GOTIFY_URL=https://gotify.example.com
|
||||||
|
|
||||||
|
# Gotify App Token
|
||||||
|
GOTIFY_APP_TOKEN=your_gotify_app_token_here
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Optional: OpenRouter Integration
|
||||||
|
# ============================================
|
||||||
|
# Für AI-Features (falls vorhanden)
|
||||||
|
|
||||||
|
# OpenRouter API Key
|
||||||
|
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Caddy Reverse Proxy (Optional - Production)
|
||||||
|
# ============================================
|
||||||
|
# Nur benötigt, wenn Caddy für SSL/TLS verwendet wird.
|
||||||
|
|
||||||
|
# GoDaddy API Key (für DNS-01 Challenge bei Wildcard-Zertifikaten)
|
||||||
|
# Siehe CADDY_SETUP.md für Anleitung zur Erstellung
|
||||||
|
GODADDY_API_KEY=your_godaddy_api_key_here
|
||||||
|
|
||||||
|
# GoDaddy API Secret
|
||||||
|
GODADDY_API_SECRET=your_godaddy_api_secret_here
|
||||||
|
|
||||||
|
# Email für Let's Encrypt Benachrichtigungen (optional)
|
||||||
|
CADDY_EMAIL=admin@hoerdle.de
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Build-Time Overrides
|
||||||
|
# ============================================
|
||||||
|
# Optional: Spezifische Version beim Build setzen
|
||||||
|
# APP_VERSION=v1.0.0
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
@@ -50,3 +51,4 @@ next-env.d.ts
|
|||||||
/data
|
/data
|
||||||
.release-years-migrated
|
.release-years-migrated
|
||||||
.covers-migrated
|
.covers-migrated
|
||||||
|
docker-compose.yml
|
||||||
|
|||||||
54
Caddyfile
Normal file
54
Caddyfile
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Caddy-Konfiguration für Hördle
|
||||||
|
# Root-Domains: hoerdle.de und hördle.de (xn--hrdle-jua.de)
|
||||||
|
# Hinweis: Diese Konfiguration funktioniert nur für Root-Domains, nicht für Subdomains
|
||||||
|
# Für Subdomains wären Wildcard-Zertifikate mit DNS-01 Challenge nötig
|
||||||
|
|
||||||
|
# Domain 1: hoerdle.de (ASCII)
|
||||||
|
hoerdle.de {
|
||||||
|
# TLS mit automatischer HTTP-01 Challenge (funktioniert nur für Root-Domain)
|
||||||
|
# Caddy verwendet automatisch Let's Encrypt
|
||||||
|
|
||||||
|
# Upload-Limit: 50MB (wie in nginx.conf.example)
|
||||||
|
request_body {
|
||||||
|
max_size 50MB
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse Proxy zu hoerdle Container
|
||||||
|
reverse_proxy hoerdle:3000 {
|
||||||
|
# HTTP/1.1 für WebSocket Support
|
||||||
|
transport http {
|
||||||
|
versions 1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP zu HTTPS Redirect
|
||||||
|
@http {
|
||||||
|
protocol http
|
||||||
|
}
|
||||||
|
redir @http https://{host}{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Domain 2: hördle.de (Punycode: xn--hrdle-jua.de)
|
||||||
|
xn--hrdle-jua.de {
|
||||||
|
# TLS mit automatischer HTTP-01 Challenge (funktioniert nur für Root-Domain)
|
||||||
|
# Caddy verwendet automatisch Let's Encrypt
|
||||||
|
|
||||||
|
# Upload-Limit: 50MB
|
||||||
|
request_body {
|
||||||
|
max_size 50MB
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse Proxy zu hoerdle Container
|
||||||
|
reverse_proxy hoerdle:3000 {
|
||||||
|
# HTTP/1.1 für WebSocket Support
|
||||||
|
transport http {
|
||||||
|
versions 1.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP zu HTTPS Redirect
|
||||||
|
@http {
|
||||||
|
protocol http
|
||||||
|
}
|
||||||
|
redir @http https://{host}{uri} permanent
|
||||||
|
}
|
||||||
13
Dockerfile
13
Dockerfile
@@ -23,11 +23,13 @@ RUN apk add --no-cache git
|
|||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Extract version: use build arg if provided, otherwise get from git
|
# Extract version: use build arg if provided, otherwise get from git, fallback to package.json
|
||||||
RUN if [ -n "$APP_VERSION" ]; then \
|
RUN if [ -n "$APP_VERSION" ]; then \
|
||||||
echo "$APP_VERSION" > /tmp/version.txt; \
|
echo "$APP_VERSION" > /tmp/version.txt; \
|
||||||
else \
|
else \
|
||||||
git describe --tags --always 2>/dev/null > /tmp/version.txt || echo "unknown" > /tmp/version.txt; \
|
(git describe --tags --always 2>/dev/null || \
|
||||||
|
(grep -o '"version": "[^"]*"' package.json 2>/dev/null | cut -d'"' -f4 | sed 's/^/v/') || \
|
||||||
|
echo "dev") > /tmp/version.txt; \
|
||||||
fi && \
|
fi && \
|
||||||
echo "Building version: $(cat /tmp/version.txt)"
|
echo "Building version: $(cat /tmp/version.txt)"
|
||||||
|
|
||||||
@@ -36,6 +38,9 @@ RUN if [ -n "$APP_VERSION" ]; then \
|
|||||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
# Suppress baseline-browser-mapping warning about old data (informational only)
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# Generate Prisma Client
|
# Generate Prisma Client
|
||||||
ENV DATABASE_URL="file:./dev.db"
|
ENV DATABASE_URL="file:./dev.db"
|
||||||
RUN node_modules/.bin/prisma generate
|
RUN node_modules/.bin/prisma generate
|
||||||
@@ -44,8 +49,6 @@ RUN node_modules/.bin/prisma generate
|
|||||||
ARG NEXT_PUBLIC_APP_NAME
|
ARG NEXT_PUBLIC_APP_NAME
|
||||||
ARG NEXT_PUBLIC_APP_DESCRIPTION
|
ARG NEXT_PUBLIC_APP_DESCRIPTION
|
||||||
ARG NEXT_PUBLIC_DOMAIN
|
ARG NEXT_PUBLIC_DOMAIN
|
||||||
ARG NEXT_PUBLIC_TWITTER_HANDLE
|
|
||||||
ARG NEXT_PUBLIC_PLAUSIBLE_DOMAIN
|
|
||||||
ARG NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC
|
ARG NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC
|
||||||
ARG NEXT_PUBLIC_THEME_COLOR
|
ARG NEXT_PUBLIC_THEME_COLOR
|
||||||
ARG NEXT_PUBLIC_BACKGROUND_COLOR
|
ARG NEXT_PUBLIC_BACKGROUND_COLOR
|
||||||
@@ -58,8 +61,6 @@ ARG NEXT_PUBLIC_CREDITS_LINK_URL
|
|||||||
ENV NEXT_PUBLIC_APP_NAME=$NEXT_PUBLIC_APP_NAME
|
ENV NEXT_PUBLIC_APP_NAME=$NEXT_PUBLIC_APP_NAME
|
||||||
ENV NEXT_PUBLIC_APP_DESCRIPTION=$NEXT_PUBLIC_APP_DESCRIPTION
|
ENV NEXT_PUBLIC_APP_DESCRIPTION=$NEXT_PUBLIC_APP_DESCRIPTION
|
||||||
ENV NEXT_PUBLIC_DOMAIN=$NEXT_PUBLIC_DOMAIN
|
ENV NEXT_PUBLIC_DOMAIN=$NEXT_PUBLIC_DOMAIN
|
||||||
ENV NEXT_PUBLIC_TWITTER_HANDLE=$NEXT_PUBLIC_TWITTER_HANDLE
|
|
||||||
ENV NEXT_PUBLIC_PLAUSIBLE_DOMAIN=$NEXT_PUBLIC_PLAUSIBLE_DOMAIN
|
|
||||||
ENV NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC=$NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC
|
ENV NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC=$NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC
|
||||||
ENV NEXT_PUBLIC_THEME_COLOR=$NEXT_PUBLIC_THEME_COLOR
|
ENV NEXT_PUBLIC_THEME_COLOR=$NEXT_PUBLIC_THEME_COLOR
|
||||||
ENV NEXT_PUBLIC_BACKGROUND_COLOR=$NEXT_PUBLIC_BACKGROUND_COLOR
|
ENV NEXT_PUBLIC_BACKGROUND_COLOR=$NEXT_PUBLIC_BACKGROUND_COLOR
|
||||||
|
|||||||
10
Dockerfile.caddy
Normal file
10
Dockerfile.caddy
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Dockerfile für Caddy mit GoDaddy DNS-Provider Plugin
|
||||||
|
FROM caddy:2-builder AS builder
|
||||||
|
|
||||||
|
RUN xcaddy build \
|
||||||
|
--with github.com/caddy-dns/godaddy
|
||||||
|
|
||||||
|
FROM caddy:2
|
||||||
|
|
||||||
|
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||||
|
|
||||||
10
README.md
10
README.md
@@ -56,18 +56,18 @@ Eine Web-App inspiriert von Heardle, bei der Nutzer täglich einen Song anhand k
|
|||||||
|
|
||||||
Hördle unterstützt vollständige Mehrsprachigkeit für Deutsch und Englisch.
|
Hördle unterstützt vollständige Mehrsprachigkeit für Deutsch und Englisch.
|
||||||
|
|
||||||
👉 **[Vollständige i18n-Dokumentation](I18N.md)**
|
👉 **[Vollständige i18n-Dokumentation](docs/I18N.md)**
|
||||||
|
|
||||||
**Schnellstart:**
|
**Schnellstart:**
|
||||||
- Deutsche Version: `http://localhost:3000/de`
|
- Deutsche Version: `http://localhost:3000/de`
|
||||||
- Englische Version: `http://localhost:3000/en`
|
- Englische Version: `http://localhost:3000/en`
|
||||||
- Root (`/`) leitet automatisch zur Standardsprache (Deutsch) um
|
- Root (`/`) leitet automatisch zur Standardsprache (Englisch) um
|
||||||
|
|
||||||
## White Labeling
|
## White Labeling
|
||||||
|
|
||||||
Hördle ist "White Label Ready". Das bedeutet, du kannst das Branding (Name, Farben, Logos) komplett anpassen, ohne den Code zu ändern.
|
Hördle ist "White Label Ready". Das bedeutet, du kannst das Branding (Name, Farben, Logos) komplett anpassen, ohne den Code zu ändern.
|
||||||
|
|
||||||
👉 **[Anleitung zur Anpassung (White Label Guide)](WHITE_LABEL.md)**
|
👉 **[Anleitung zur Anpassung (White Label Guide)](docs/WHITE_LABEL.md)**
|
||||||
|
|
||||||
Die Konfiguration erfolgt einfach über Umgebungsvariablen und CSS-Variablen.
|
Die Konfiguration erfolgt einfach über Umgebungsvariablen und CSS-Variablen.
|
||||||
|
|
||||||
@@ -115,13 +115,13 @@ Das Ziel ist es, den Song mit so wenigen Hinweisen wie möglich zu erraten und d
|
|||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
Die App läuft unter `http://localhost:3000` (leitet automatisch zu `/de` um).
|
Die App läuft unter `http://localhost:3000` (leitet automatisch zu `/en` um).
|
||||||
|
|
||||||
## Deployment mit Docker
|
## Deployment mit Docker
|
||||||
|
|
||||||
Das Projekt ist für den Betrieb mit Docker optimiert.
|
Das Projekt ist für den Betrieb mit Docker optimiert.
|
||||||
|
|
||||||
👉 **[White Labeling mit Docker? Hier klicken!](WHITE_LABEL.md#docker-deployment)**
|
👉 **[White Labeling mit Docker? Hier klicken!](docs/WHITE_LABEL.md#docker-deployment)**
|
||||||
|
|
||||||
1. **Vorbereitung:**
|
1. **Vorbereitung:**
|
||||||
Kopiere die Beispiel-Konfiguration:
|
Kopiere die Beispiel-Konfiguration:
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { getLocalizedValue } from '@/lib/i18n';
|
import { getLocalizedValue } from '@/lib/i18n';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
import { generateBaseMetadata } from '@/lib/metadata';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -15,6 +17,32 @@ interface PageProps {
|
|||||||
params: Promise<{ locale: string; genre: string }>;
|
params: Promise<{ locale: string; genre: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||||
|
const { locale, genre } = await params;
|
||||||
|
const decodedGenre = decodeURIComponent(genre);
|
||||||
|
|
||||||
|
// Fetch genre to get localized name
|
||||||
|
const allGenres = await prisma.genre.findMany();
|
||||||
|
const currentGenre = allGenres.find(g => getLocalizedValue(g.name, locale) === decodedGenre);
|
||||||
|
|
||||||
|
if (!currentGenre || !currentGenre.active) {
|
||||||
|
return await generateBaseMetadata(locale, genre);
|
||||||
|
}
|
||||||
|
|
||||||
|
const genreName = getLocalizedValue(currentGenre.name, locale);
|
||||||
|
const genreSubtitle = getLocalizedValue(currentGenre.subtitle, locale);
|
||||||
|
|
||||||
|
const title = locale === 'de'
|
||||||
|
? `${genreName} - Hördle`
|
||||||
|
: `${genreName} - Hördle`;
|
||||||
|
|
||||||
|
const description = genreSubtitle || (locale === 'de'
|
||||||
|
? `Spiele Hördle im Genre ${genreName} und errate Songs aus kurzen Audio-Clips!`
|
||||||
|
: `Play Hördle in the ${genreName} genre and guess songs from short audio clips!`);
|
||||||
|
|
||||||
|
return await generateBaseMetadata(locale, genre, title, description);
|
||||||
|
}
|
||||||
|
|
||||||
export default async function GenrePage({ params }: PageProps) {
|
export default async function GenrePage({ params }: PageProps) {
|
||||||
const { locale, genre } = await params;
|
const { locale, genre } = await params;
|
||||||
const decodedGenre = decodeURIComponent(genre);
|
const decodedGenre = decodeURIComponent(genre);
|
||||||
|
|||||||
263
app/[locale]/about/page.tsx
Normal file
263
app/[locale]/about/page.tsx
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { Link } from "@/lib/navigation";
|
||||||
|
import { generateBaseMetadata } from "@/lib/metadata";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
interface AboutPageProps {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: AboutPageProps): Promise<Metadata> {
|
||||||
|
const { locale } = await params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "About" });
|
||||||
|
|
||||||
|
const title = t("title");
|
||||||
|
const description = t("intro");
|
||||||
|
|
||||||
|
return await generateBaseMetadata(locale, "about", title, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AboutPage({ params }: AboutPageProps) {
|
||||||
|
const { locale } = await params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "About" });
|
||||||
|
|
||||||
|
const sheetUrl =
|
||||||
|
"https://docs.google.com/spreadsheets/d/1LuMkDsnidlvMtzzSqwrz-GACnqMaqzs-VBa-ZK0nZeI/edit?usp=sharing";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
style={{
|
||||||
|
maxWidth: "960px",
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: "2rem 1rem",
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ fontSize: "2rem", marginBottom: "1rem" }}>{t("title")}</h1>
|
||||||
|
<p style={{ marginBottom: "2rem", color: "#4b5563" }}>{t("intro")}</p>
|
||||||
|
|
||||||
|
<section style={{ marginBottom: "2rem" }}>
|
||||||
|
<h2 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>
|
||||||
|
{t("projectTitle")}
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>{t("projectPrivateNote")}</p>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>{t("projectIdea")}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style={{ marginBottom: "2rem" }}>
|
||||||
|
<h2 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>
|
||||||
|
{t("imprintTitle")}
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>
|
||||||
|
<strong>{t("imprintOperator")}</strong>
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: 0, lineHeight: "1.5" }}>
|
||||||
|
Markus Busche
|
||||||
|
<br />
|
||||||
|
Knorrstr. 16
|
||||||
|
<br />
|
||||||
|
24106 Kiel
|
||||||
|
<br />
|
||||||
|
{t("imprintCountry")}
|
||||||
|
<br />
|
||||||
|
{t("imprintEmailLabel")}{" "}
|
||||||
|
<a href="mailto:markus@hoerdle.de">markus@hoerdle.de</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style={{ marginBottom: "2rem" }}>
|
||||||
|
<h2 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>
|
||||||
|
{t("costsTitle")}
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>{t("costsIntro")}</p>
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
marginLeft: "1.25rem",
|
||||||
|
marginBottom: "0.75rem",
|
||||||
|
listStyleType: "disc",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<li>{t("costsDomain")}</li>
|
||||||
|
<li>{t("costsServer")}</li>
|
||||||
|
<li>{t("costsEmail")}</li>
|
||||||
|
<li>{t("costsLicenses")}</li>
|
||||||
|
</ul>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>
|
||||||
|
{t.rich("costsSheetLinkText", {
|
||||||
|
link: (chunks) => (
|
||||||
|
<a
|
||||||
|
href={sheetUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ textDecoration: "underline" }}
|
||||||
|
>
|
||||||
|
{chunks}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginBottom: "0.75rem",
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
color: "#6b7280",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("costsSheetPrivacyNote")}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style={{ marginBottom: "2rem" }}>
|
||||||
|
<h2 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>
|
||||||
|
{t("supportTitle")}
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginBottom: "1rem" }}>{t("supportIntro")}</p>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "1rem",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
backgroundColor: "#f9fafb",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
style={{
|
||||||
|
fontSize: "1.125rem",
|
||||||
|
fontWeight: "600",
|
||||||
|
marginBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("supportSepaTitle")}
|
||||||
|
</h3>
|
||||||
|
<p style={{ marginBottom: "0.25rem" }}>
|
||||||
|
<strong>{t("supportSepaName")}</strong>
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: 0, fontFamily: "monospace" }}>
|
||||||
|
{t("supportSepaIban")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "1rem",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
backgroundColor: "#f9fafb",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
style={{
|
||||||
|
fontSize: "1.125rem",
|
||||||
|
fontWeight: "600",
|
||||||
|
marginBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("supportPaypalTitle")}
|
||||||
|
</h3>
|
||||||
|
<p style={{ marginBottom: 0 }}>
|
||||||
|
<a
|
||||||
|
href="https://paypal.me/MBusche"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ textDecoration: "underline" }}
|
||||||
|
>
|
||||||
|
{t("supportPaypalLink")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "1rem",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
backgroundColor: "#f9fafb",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
style={{
|
||||||
|
fontSize: "1.125rem",
|
||||||
|
fontWeight: "600",
|
||||||
|
marginBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("supportSteadyTitle")}
|
||||||
|
</h3>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>
|
||||||
|
{t("supportSteadyDescription")}
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: 0 }}>
|
||||||
|
<a
|
||||||
|
href="https://steady.page/de/hoerdle"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ textDecoration: "underline" }}
|
||||||
|
>
|
||||||
|
https://steady.page/de/hoerdle
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style={{ marginBottom: "2rem" }}>
|
||||||
|
<h2 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>
|
||||||
|
{t("privacyTitle")}
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>{t("privacyIntro")}</p>
|
||||||
|
<h3
|
||||||
|
style={{
|
||||||
|
fontSize: "1.25rem",
|
||||||
|
marginTop: "1rem",
|
||||||
|
marginBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("privacyPlausibleTitle")}
|
||||||
|
</h3>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>
|
||||||
|
{t("privacyPlausibleSelfHosted")}
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>
|
||||||
|
{t("privacyPlausibleGemaTariff")}
|
||||||
|
</p>
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
marginLeft: "1.25rem",
|
||||||
|
marginBottom: "0.75rem",
|
||||||
|
listStyleType: "disc",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<li>{t("privacyPlausibleNoCookies")}</li>
|
||||||
|
<li>{t("privacyPlausibleNoTrackingAcrossSites")}</li>
|
||||||
|
<li>{t("privacyPlausibleAggregated")}</li>
|
||||||
|
</ul>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>{t("privacyServerLogs")}</p>
|
||||||
|
<p style={{ marginBottom: "0.5rem" }}>{t("privacyContact")}</p>
|
||||||
|
<p
|
||||||
|
style={{ marginTop: "0.5rem", fontSize: "0.9rem", color: "#6b7280" }}
|
||||||
|
>
|
||||||
|
{t("privacyNoLegalAdvice")}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style={{ marginBottom: "2rem" }}>
|
||||||
|
<h2 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>
|
||||||
|
{t("backTitle")}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<Link href="/" style={{ textDecoration: "underline" }}>
|
||||||
|
{t("backToGame")}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@ import "../globals.css"; // Adjusted path
|
|||||||
import { NextIntlClientProvider } from 'next-intl';
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
import { getMessages } from 'next-intl/server';
|
import { getMessages } from 'next-intl/server';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
import { headers } from 'next/headers';
|
||||||
|
|
||||||
import { config } from "@/lib/config";
|
import { config } from "@/lib/config";
|
||||||
|
import { generateBaseMetadata } from "@/lib/metadata";
|
||||||
import InstallPrompt from "@/components/InstallPrompt";
|
import InstallPrompt from "@/components/InstallPrompt";
|
||||||
import AppFooter from "@/components/AppFooter";
|
import AppFooter from "@/components/AppFooter";
|
||||||
|
|
||||||
@@ -20,10 +22,10 @@ const geistMono = Geist_Mono({
|
|||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
title: config.appName,
|
const { locale } = await params;
|
||||||
description: config.appDescription,
|
return await generateBaseMetadata(locale);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
themeColor: config.colors.themeColor,
|
themeColor: config.colors.themeColor,
|
||||||
@@ -52,12 +54,32 @@ export default async function LocaleLayout({
|
|||||||
// Providing all messages to the client
|
// Providing all messages to the client
|
||||||
const messages = await getMessages();
|
const messages = await getMessages();
|
||||||
|
|
||||||
|
// Get current domain from request headers for dynamic Plausible tracking
|
||||||
|
// This automatically tracks the correct domain (hoerdle.de or hördle.de)
|
||||||
|
const headersList = await headers();
|
||||||
|
const host = headersList.get('host') || headersList.get('x-forwarded-host') || '';
|
||||||
|
|
||||||
|
// Automatically detect which domain to track in Plausible based on the request
|
||||||
|
let plausibleDomain = 'hoerdle.de'; // Default fallback
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
// Extract domain from host (remove port if present)
|
||||||
|
const domain = host.split(':')[0].toLowerCase();
|
||||||
|
|
||||||
|
// Map domains: automatically track the current domain
|
||||||
|
if (domain === 'hoerdle.de') {
|
||||||
|
plausibleDomain = 'hoerdle.de';
|
||||||
|
} else if (domain === 'hördle.de' || domain === 'xn--hrdle-jua.de') {
|
||||||
|
plausibleDomain = 'hördle.de';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
<head>
|
<head>
|
||||||
<Script
|
<Script
|
||||||
defer
|
defer
|
||||||
data-domain={config.plausibleDomain}
|
data-domain={plausibleDomain}
|
||||||
src={config.plausibleScriptSrc}
|
src={config.plausibleScriptSrc}
|
||||||
strategy="beforeInteractive"
|
strategy="beforeInteractive"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,15 +7,33 @@ import { Link } from '@/lib/navigation';
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { getLocalizedValue } from '@/lib/i18n';
|
import { getLocalizedValue } from '@/lib/i18n';
|
||||||
|
import { generateBaseMetadata } from '@/lib/metadata';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const { locale } = await params;
|
||||||
|
const t = await getTranslations('Home');
|
||||||
|
|
||||||
|
// Get localized title and description
|
||||||
|
const title = locale === 'de'
|
||||||
|
? 'Hördle - Tägliches Musik-Erraten'
|
||||||
|
: 'Hördle - Daily Music Guessing Game';
|
||||||
|
|
||||||
|
const description = locale === 'de'
|
||||||
|
? 'Spiele Hördle und errate Songs aus kurzen Audio-Clips! Täglich neue Rätsel aus verschiedenen Genres. Inspiriert von Wordle, aber für Musikfans.'
|
||||||
|
: 'Play Hördle and guess songs from short audio clips! Daily new puzzles from various genres. Inspired by Wordle, but for music lovers.';
|
||||||
|
|
||||||
|
return await generateBaseMetadata(locale, '', title, description);
|
||||||
|
}
|
||||||
|
|
||||||
export default async function Home({
|
export default async function Home({
|
||||||
params
|
params
|
||||||
}: {
|
}: {
|
||||||
params: { locale: string };
|
params: Promise<{ locale: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations('Home');
|
const t = await getTranslations('Home');
|
||||||
@@ -45,10 +63,14 @@ export default async function Home({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="tour-genres" style={{ textAlign: 'center', padding: '1rem', background: '#f3f4f6' }}>
|
<div id="tour-genres" style={{ textAlign: 'center', padding: '1rem', background: '#f3f4f6', position: 'relative' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem', maxWidth: '1200px', margin: '0 auto', padding: '0 1rem' }}>
|
{/* Language Switcher - rechts oben */}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ position: 'absolute', top: '1rem', right: '1rem', zIndex: 10 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '1rem', flexWrap: 'wrap', alignItems: 'center', flex: 2 }}>
|
<LanguageSwitcher />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zentrierte Navigation */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', gap: '1rem', flexWrap: 'wrap', alignItems: 'center', marginBottom: '0.5rem' }}>
|
||||||
<div className="tooltip">
|
<div className="tooltip">
|
||||||
<Link href="/" style={{ fontWeight: 'bold', textDecoration: 'underline' }}>{tNav('global')}</Link>
|
<Link href="/" style={{ fontWeight: 'bold', textDecoration: 'underline' }}>{tNav('global')}</Link>
|
||||||
<span className="tooltip-text">{t('globalTooltip')}</span>
|
<span className="tooltip-text">{t('globalTooltip')}</span>
|
||||||
@@ -104,7 +126,7 @@ export default async function Home({
|
|||||||
|
|
||||||
{/* Upcoming Specials */}
|
{/* Upcoming Specials */}
|
||||||
{upcomingSpecials.length > 0 && (
|
{upcomingSpecials.length > 0 && (
|
||||||
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#666' }}>
|
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#666', textAlign: 'center' }}>
|
||||||
{t('comingSoon')}: {upcomingSpecials.map(s => {
|
{t('comingSoon')}: {upcomingSpecials.map(s => {
|
||||||
const name = getLocalizedValue(s.name, locale);
|
const name = getLocalizedValue(s.name, locale);
|
||||||
return (
|
return (
|
||||||
@@ -122,10 +144,6 @@ export default async function Home({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end' }}>
|
|
||||||
<LanguageSwitcher />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tour-news">
|
<div id="tour-news">
|
||||||
<NewsSection locale={locale} />
|
<NewsSection locale={locale} />
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Link } from '@/lib/navigation';
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { getLocalizedValue } from '@/lib/i18n';
|
import { getLocalizedValue } from '@/lib/i18n';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
import { generateBaseMetadata } from '@/lib/metadata';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -14,6 +16,30 @@ interface PageProps {
|
|||||||
params: Promise<{ locale: string; name: string }>;
|
params: Promise<{ locale: string; name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||||
|
const { locale, name } = await params;
|
||||||
|
const decodedName = decodeURIComponent(name);
|
||||||
|
|
||||||
|
// Fetch special to get localized name
|
||||||
|
const allSpecials = await prisma.special.findMany();
|
||||||
|
const currentSpecial = allSpecials.find(s => getLocalizedValue(s.name, locale) === decodedName);
|
||||||
|
|
||||||
|
if (!currentSpecial) {
|
||||||
|
return await generateBaseMetadata(locale, `special/${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialName = getLocalizedValue(currentSpecial.name, locale);
|
||||||
|
const specialSubtitle = getLocalizedValue(currentSpecial.subtitle, locale);
|
||||||
|
|
||||||
|
const title = `★ ${specialName} - Hördle`;
|
||||||
|
|
||||||
|
const description = specialSubtitle || (locale === 'de'
|
||||||
|
? `Spiele das Hördle-Special "${specialName}" und errate Songs aus kurzen Audio-Clips!`
|
||||||
|
: `Play the Hördle special "${specialName}" and guess songs from short audio clips!`);
|
||||||
|
|
||||||
|
return await generateBaseMetadata(locale, `special/${name}`, title, description);
|
||||||
|
}
|
||||||
|
|
||||||
export default async function SpecialPage({ params }: PageProps) {
|
export default async function SpecialPage({ params }: PageProps) {
|
||||||
const { locale, name } = await params;
|
const { locale, name } = await params;
|
||||||
const decodedName = decodeURIComponent(name);
|
const decodedName = decodeURIComponent(name);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function GET() {
|
|||||||
for (const versionFilePath of versionPaths) {
|
for (const versionFilePath of versionPaths) {
|
||||||
if (existsSync(versionFilePath)) {
|
if (existsSync(versionFilePath)) {
|
||||||
const version = readFileSync(versionFilePath, 'utf-8').trim();
|
const version = readFileSync(versionFilePath, 'utf-8').trim();
|
||||||
if (version && version !== 'unknown') {
|
if (version && version !== 'unknown' && version !== '') {
|
||||||
return NextResponse.json({ version });
|
return NextResponse.json({ version });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,19 @@ export async function GET() {
|
|||||||
return NextResponse.json({ version: process.env.APP_VERSION });
|
return NextResponse.json({ version: process.env.APP_VERSION });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: check package.json
|
||||||
|
try {
|
||||||
|
const packageJsonPath = join(process.cwd(), 'package.json');
|
||||||
|
if (existsSync(packageJsonPath)) {
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||||
|
if (packageJson.version) {
|
||||||
|
return NextResponse.json({ version: `v${packageJson.version}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore package.json read errors
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: try to get from git (local development)
|
// Fallback: try to get from git (local development)
|
||||||
let version = 'dev';
|
let version = 'dev';
|
||||||
|
|
||||||
|
|||||||
20
app/robots.ts
Normal file
20
app/robots.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_DOMAIN || config.domain;
|
||||||
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
const siteUrl = `${protocol}://${baseUrl}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
userAgent: '*',
|
||||||
|
allow: '/',
|
||||||
|
disallow: ['/admin/', '/api/'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sitemap: `${siteUrl}/sitemap.xml`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
128
app/sitemap.ts
Normal file
128
app/sitemap.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { getLocalizedValue } from '@/lib/i18n';
|
||||||
|
import { config } from '@/lib/config';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_DOMAIN || config.domain;
|
||||||
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
const siteUrl = `${protocol}://${baseUrl}`;
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Static pages
|
||||||
|
const staticPages: MetadataRoute.Sitemap = [
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/en`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily',
|
||||||
|
priority: 1.0,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
'de': `${siteUrl}/de`,
|
||||||
|
'en': `${siteUrl}/en`,
|
||||||
|
'x-default': `${siteUrl}/en`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/de`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.8,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
'de': `${siteUrl}/de`,
|
||||||
|
'en': `${siteUrl}/en`,
|
||||||
|
'x-default': `${siteUrl}/en`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/en/about`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.7,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
'de': `${siteUrl}/de/about`,
|
||||||
|
'en': `${siteUrl}/en/about`,
|
||||||
|
'x-default': `${siteUrl}/en/about`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/de/about`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.7,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
'de': `${siteUrl}/de/about`,
|
||||||
|
'en': `${siteUrl}/en/about`,
|
||||||
|
'x-default': `${siteUrl}/en/about`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Dynamic genre pages
|
||||||
|
try {
|
||||||
|
const genres = await prisma.genre.findMany({
|
||||||
|
where: { active: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const genrePages: MetadataRoute.Sitemap = [];
|
||||||
|
|
||||||
|
for (const genre of genres) {
|
||||||
|
const genreNameEn = getLocalizedValue(genre.name, 'en');
|
||||||
|
const genreNameDe = getLocalizedValue(genre.name, 'de');
|
||||||
|
|
||||||
|
// Only add if genre name is valid
|
||||||
|
if (genreNameEn && genreNameDe) {
|
||||||
|
const encodedEn = encodeURIComponent(genreNameEn);
|
||||||
|
const encodedDe = encodeURIComponent(genreNameDe);
|
||||||
|
|
||||||
|
genrePages.push(
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/en/${encodedEn}`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily',
|
||||||
|
priority: 0.9,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
'de': `${siteUrl}/de/${encodedDe}`,
|
||||||
|
'en': `${siteUrl}/en/${encodedEn}`,
|
||||||
|
'x-default': `${siteUrl}/en/${encodedEn}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${siteUrl}/de/${encodedDe}`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily',
|
||||||
|
priority: 0.9,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
'de': `${siteUrl}/de/${encodedDe}`,
|
||||||
|
'en': `${siteUrl}/en/${encodedEn}`,
|
||||||
|
'x-default': `${siteUrl}/en/${encodedEn}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...staticPages, ...genrePages];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating sitemap:', error);
|
||||||
|
// Return static pages only if database query fails
|
||||||
|
return staticPages;
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { config } from '@/lib/config';
|
import { config } from "@/lib/config";
|
||||||
import { useEffect, useState } from 'react';
|
import { Link } from "@/lib/navigation";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function AppFooter() {
|
export default function AppFooter() {
|
||||||
const [version, setVersion] = useState<string>('');
|
const [version, setVersion] = useState<string>("");
|
||||||
|
const t = useTranslations("About");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/version')
|
fetch("/api/version")
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(data => setVersion(data.version))
|
.then((data) => setVersion(data.version))
|
||||||
.catch(() => setVersion(''));
|
.catch(() => setVersion(""));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!config.credits.enabled) return null;
|
if (!config.credits.enabled) return null;
|
||||||
@@ -18,19 +21,27 @@ export default function AppFooter() {
|
|||||||
return (
|
return (
|
||||||
<footer className="app-footer">
|
<footer className="app-footer">
|
||||||
<p>
|
<p>
|
||||||
{config.credits.text}{' '}
|
{config.credits.text}{" "}
|
||||||
<a href={config.credits.linkUrl} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href={config.credits.linkUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{config.credits.linkText}
|
{config.credits.linkText}
|
||||||
</a>
|
</a>
|
||||||
{version && (
|
{version && (
|
||||||
<>
|
<>
|
||||||
{' '}·{' '}
|
{" "}
|
||||||
<span style={{ fontSize: '0.85em', opacity: 0.7 }}>
|
·{" "}
|
||||||
{version}
|
<span style={{ fontSize: "0.85em", opacity: 0.7 }}>{version}</span>
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
<p style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>
|
||||||
|
<Link href="/about" style={{ textDecoration: "underline" }}>
|
||||||
|
{t("footerLinkLabel")}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,8 +275,8 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
|
|||||||
const genreText = genre ? `${isSpecial ? t('special') : t('genre')}: ${genre}\n` : '';
|
const genreText = genre ? `${isSpecial ? t('special') : t('genre')}: ${genre}\n` : '';
|
||||||
|
|
||||||
let shareUrl = `https://${config.domain}`;
|
let shareUrl = `https://${config.domain}`;
|
||||||
// Add locale prefix if not default (de)
|
// Add locale prefix if not default (en)
|
||||||
if (locale !== 'de') {
|
if (locale !== 'en') {
|
||||||
shareUrl += `/${locale}`;
|
shareUrl += `/${locale}`;
|
||||||
}
|
}
|
||||||
if (genre) {
|
if (genre) {
|
||||||
|
|||||||
59
docker-compose.caddy.yml
Normal file
59
docker-compose.caddy.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Docker Compose Konfiguration für Caddy Reverse Proxy
|
||||||
|
# Optional: Nur in Produktionsumgebung verwenden
|
||||||
|
#
|
||||||
|
# Starten: docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
||||||
|
# Stoppen: docker compose -f docker-compose.yml -f docker-compose.caddy.yml down
|
||||||
|
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
# Verwende Custom-Image mit GoDaddy DNS-Plugin
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.caddy
|
||||||
|
# Alternativ: Verwende Standard-Caddy und manuelle DNS-Konfiguration
|
||||||
|
# image: caddy:2-alpine
|
||||||
|
container_name: hoerdle-caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Standard HTTP/HTTPS Ports
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp" # Für HTTP/3 (QUIC)
|
||||||
|
environment:
|
||||||
|
# GoDaddy API-Credentials für DNS-01 Challenge
|
||||||
|
# Diese müssen in einer .env-Datei gesetzt werden:
|
||||||
|
# GODADDY_API_KEY=your_api_key
|
||||||
|
# GODADDY_API_SECRET=your_api_secret
|
||||||
|
- GODADDY_API_KEY=${GODADDY_API_KEY:-}
|
||||||
|
- GODADDY_API_SECRET=${GODADDY_API_SECRET:-}
|
||||||
|
# Optional: Email für Let's Encrypt Benachrichtigungen
|
||||||
|
- CADDY_EMAIL=${CADDY_EMAIL:-}
|
||||||
|
volumes:
|
||||||
|
# Caddyfile-Konfiguration
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
# Persistente Zertifikat-Speicherung
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
# Health Check
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "caddy", "version"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
# Nur starten, wenn ENABLE_CADDY=true gesetzt ist
|
||||||
|
profiles:
|
||||||
|
- production
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
driver: local
|
||||||
|
caddy_config:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: hoerdle_default
|
||||||
|
external: true
|
||||||
|
|
||||||
@@ -8,8 +8,6 @@ services:
|
|||||||
NEXT_PUBLIC_APP_NAME: ${NEXT_PUBLIC_APP_NAME}
|
NEXT_PUBLIC_APP_NAME: ${NEXT_PUBLIC_APP_NAME}
|
||||||
NEXT_PUBLIC_APP_DESCRIPTION: ${NEXT_PUBLIC_APP_DESCRIPTION}
|
NEXT_PUBLIC_APP_DESCRIPTION: ${NEXT_PUBLIC_APP_DESCRIPTION}
|
||||||
NEXT_PUBLIC_DOMAIN: ${NEXT_PUBLIC_DOMAIN}
|
NEXT_PUBLIC_DOMAIN: ${NEXT_PUBLIC_DOMAIN}
|
||||||
NEXT_PUBLIC_TWITTER_HANDLE: ${NEXT_PUBLIC_TWITTER_HANDLE}
|
|
||||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN: ${NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
|
|
||||||
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC: ${NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC}
|
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC: ${NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC}
|
||||||
NEXT_PUBLIC_THEME_COLOR: ${NEXT_PUBLIC_THEME_COLOR}
|
NEXT_PUBLIC_THEME_COLOR: ${NEXT_PUBLIC_THEME_COLOR}
|
||||||
NEXT_PUBLIC_BACKGROUND_COLOR: ${NEXT_PUBLIC_BACKGROUND_COLOR}
|
NEXT_PUBLIC_BACKGROUND_COLOR: ${NEXT_PUBLIC_BACKGROUND_COLOR}
|
||||||
@@ -37,4 +35,11 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
# docker-entrypoint.sh handles migrations and server startup (with baseline fallback)
|
# docker-entrypoint.sh handles migrations and server startup (with baseline fallback)
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: hoerdle_default
|
||||||
|
external: true
|
||||||
|
|||||||
289
docs/CADDY_SETUP.md
Normal file
289
docs/CADDY_SETUP.md
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
# Caddy-Setup für Hördle
|
||||||
|
|
||||||
|
Diese Anleitung erklärt, wie du Caddy als Reverse-Proxy mit automatischen Let's Encrypt Wildcard-Zertifikaten für die Domains `hoerdle.de` und `hördle.de` (xn--hrdle-jua.de) einrichtest.
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Caddy übernimmt folgende Aufgaben:
|
||||||
|
- Automatische SSL/TLS-Zertifikate via Let's Encrypt
|
||||||
|
- Wildcard-Zertifikate für beide Domains (inkl. Subdomains)
|
||||||
|
- Reverse Proxy zu deinem Hördle-Container
|
||||||
|
- HTTP zu HTTPS Redirect
|
||||||
|
- Optimierte Einstellungen für Audio-Streaming und Uploads
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
1. Docker und Docker Compose installiert
|
||||||
|
2. Zugriff auf deine GoDaddy Domain-Verwaltung
|
||||||
|
3. Ports 80 und 443 müssen frei sein (Caddy übernimmt diese)
|
||||||
|
|
||||||
|
## Schritt 1: GoDaddy DNS-API-Zugangsdaten erstellen
|
||||||
|
|
||||||
|
Für Wildcard-Zertifikate benötigt Caddy DNS-01 Challenge, was API-Zugriff auf dein GoDaddy-Konto erfordert.
|
||||||
|
|
||||||
|
### GoDaddy API-Keys erstellen
|
||||||
|
|
||||||
|
1. Gehe zu [GoDaddy Developer Portal](https://developer.godaddy.com/)
|
||||||
|
2. Melde dich mit deinem GoDaddy-Konto an
|
||||||
|
3. Klicke auf **"Keys"** in der Navigation
|
||||||
|
4. Klicke auf **"Create New API Key"**
|
||||||
|
5. Fülle das Formular aus:
|
||||||
|
- **Key Name**: z.B. "Hördle Caddy DNS"
|
||||||
|
- **Environment**: Production (für echte Domains)
|
||||||
|
6. Klicke auf **"Create"**
|
||||||
|
7. **Wichtig**: Kopiere dir den **API Key** und das **API Secret** - das Secret wird nur einmal angezeigt!
|
||||||
|
|
||||||
|
### Alternative: Manuelle DNS-TXT-Records (ohne API)
|
||||||
|
|
||||||
|
Wenn du keine API-Keys verwenden möchtest, kannst du die DNS-TXT-Records manuell setzen. **Hinweis**: Dies ist nur für die initiale Zertifikatsanfrage möglich, nicht für automatische Erneuerungen.
|
||||||
|
|
||||||
|
Siehe Abschnitt "Manuelle DNS-Konfiguration (ohne API)" weiter unten.
|
||||||
|
|
||||||
|
## Schritt 2: Environment-Variablen konfigurieren
|
||||||
|
|
||||||
|
Erstelle eine `.env`-Datei im Projektverzeichnis (oder erweitere die bestehende):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GoDaddy API-Credentials für DNS-01 Challenge
|
||||||
|
GODADDY_API_KEY=your_api_key_here
|
||||||
|
GODADDY_API_SECRET=your_api_secret_here
|
||||||
|
|
||||||
|
# Optional: Email für Let's Encrypt Benachrichtigungen
|
||||||
|
CADDY_EMAIL=markus@hoerdle.de
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig**: Die `.env`-Datei sollte nicht in Git committed werden (sollte bereits in `.gitignore` sein).
|
||||||
|
|
||||||
|
## Schritt 3: Docker-Netzwerk erstellen
|
||||||
|
|
||||||
|
Caddy und Hördle müssen im gleichen Docker-Netzwerk kommunizieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe, ob das Netzwerk bereits existiert
|
||||||
|
docker network ls | grep hoerdle
|
||||||
|
|
||||||
|
# Falls das Netzwerk bereits existiert, aber falsche Labels hat:
|
||||||
|
# 1. Stoppe alle Container, die das Netzwerk nutzen
|
||||||
|
docker compose -f docker-compose.yml down
|
||||||
|
|
||||||
|
# 2. Lösche das alte Netzwerk (falls keine Container mehr dranhängen)
|
||||||
|
docker network rm hoerdle_default
|
||||||
|
|
||||||
|
# 3. Erstelle das Netzwerk neu
|
||||||
|
docker network create hoerdle_default
|
||||||
|
|
||||||
|
# Falls das Netzwerk nicht existiert, erstelle es:
|
||||||
|
docker network create hoerdle_default
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis**: Die docker-compose.caddy.yml ist so konfiguriert, dass sie das Netzwerk als externes Netzwerk nutzt. Das bedeutet, dass das Netzwerk bereits existieren muss, bevor Caddy gestartet wird.
|
||||||
|
|
||||||
|
## Schritt 4: Caddy starten
|
||||||
|
|
||||||
|
### Option A: Mit docker-compose (Empfohlen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Starte Hördle + Caddy zusammen
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
|
||||||
|
# Nur Caddy starten (wenn Hördle bereits läuft)
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Nur Caddy starten (Hördle läuft bereits)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schritt 5: DNS-Konfiguration in GoDaddy
|
||||||
|
|
||||||
|
### Automatisch (mit API-Keys)
|
||||||
|
|
||||||
|
Wenn du API-Keys konfiguriert hast, wird Caddy automatisch die benötigten DNS-TXT-Records erstellen. Keine manuellen DNS-Änderungen nötig!
|
||||||
|
|
||||||
|
### Manuell (ohne API-Keys)
|
||||||
|
|
||||||
|
Wenn du die API-Keys nicht verwenden möchtest, musst du die DNS-TXT-Records manuell setzen:
|
||||||
|
|
||||||
|
#### Für hoerdle.de:
|
||||||
|
|
||||||
|
1. Gehe zu deinem [GoDaddy DNS-Verwaltung](https://dcc.godaddy.com/manage/YOUR_DOMAIN/dns)
|
||||||
|
2. Für jedes Wildcard-Zertifikat benötigst du einen TXT-Record:
|
||||||
|
- **Typ**: TXT
|
||||||
|
- **Name**: `_acme-challenge`
|
||||||
|
- **Wert**: (wird von Let's Encrypt generiert - siehe Caddy-Logs)
|
||||||
|
- **TTL**: 600 (10 Minuten)
|
||||||
|
|
||||||
|
**Wichtig**: Für Wildcard-Zertifikate brauchst du:
|
||||||
|
- Einen TXT-Record für `_acme-challenge.hoerdle.de` (Domain selbst)
|
||||||
|
- Einen TXT-Record für `_acme-challenge.*.hoerdle.de` (Wildcard)
|
||||||
|
|
||||||
|
#### Für hördle.de (xn--hrdle-jua.de):
|
||||||
|
|
||||||
|
Das gleiche Vorgehen für die Punycode-Domain:
|
||||||
|
- `_acme-challenge.xn--hrdle-jua.de`
|
||||||
|
- `_acme-challenge.*.xn--hrdle-jua.de`
|
||||||
|
|
||||||
|
**Hinweis**: Die manuelle Methode funktioniert nur für die initiale Zertifikatsanfrage. Für automatische Erneuerungen benötigst du die API-Keys.
|
||||||
|
|
||||||
|
## Schritt 6: Prüfen, ob alles funktioniert
|
||||||
|
|
||||||
|
### Caddy-Logs ansehen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f hoerdle-caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
Du solltest sehen:
|
||||||
|
- Caddy startet erfolgreich
|
||||||
|
- Let's Encrypt-Zertifikate werden angefordert
|
||||||
|
- Zertifikate sind gültig
|
||||||
|
|
||||||
|
### Zertifikate prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Zertifikate im Browser
|
||||||
|
# Öffne: https://hoerdle.de
|
||||||
|
# Öffne: https://hördle.de
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder via Command-Line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Zertifikat für hoerdle.de
|
||||||
|
openssl s_client -connect hoerdle.de:443 -servername hoerdle.de < /dev/null 2>/dev/null | openssl x509 -noout -text | grep "Subject:"
|
||||||
|
|
||||||
|
# Prüfe Zertifikat für hördle.de
|
||||||
|
openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de < /dev/null 2>/dev/null | openssl x509 -noout -text | grep "Subject:"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Caddy startet nicht
|
||||||
|
|
||||||
|
**Problem**: Container stoppt sofort nach Start.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe Caddy-Logs: `docker logs hoerdle-caddy`
|
||||||
|
2. Prüfe Caddyfile-Syntax: `docker run --rm -v $(pwd)/Caddyfile:/etc/caddy/Caddyfile:ro caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile`
|
||||||
|
3. Prüfe, ob Ports 80/443 frei sind: `sudo netstat -tlnp | grep -E ':80|:443'`
|
||||||
|
|
||||||
|
### Zertifikate werden nicht erstellt
|
||||||
|
|
||||||
|
**Problem**: Let's Encrypt-Zertifikate werden nicht angefordert.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe GoDaddy API-Credentials in `.env`
|
||||||
|
2. Prüfe Caddy-Logs für DNS-Challenge-Fehler
|
||||||
|
3. Stelle sicher, dass die Domains korrekt auf deinen Server zeigen (A-Records)
|
||||||
|
4. Bei manueller DNS-Konfiguration: Prüfe, ob TXT-Records korrekt gesetzt sind
|
||||||
|
|
||||||
|
### DNS-Challenge schlägt fehl
|
||||||
|
|
||||||
|
**Problem**: DNS-01 Challenge kann DNS-Records nicht erstellen.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe GoDaddy API-Permissions
|
||||||
|
2. Stelle sicher, dass API-Keys Production-Keys sind (nicht Development)
|
||||||
|
3. Prüfe Domain-Ownership in GoDaddy
|
||||||
|
4. Warte einige Minuten - DNS-Propagierung kann dauern
|
||||||
|
|
||||||
|
### Audio-Dateien funktionieren nicht
|
||||||
|
|
||||||
|
**Problem**: MP3-Dateien werden nicht korrekt gestreamt.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe Caddy-Logs: `docker logs hoerdle-caddy | grep -i range`
|
||||||
|
2. Prüfe, ob Range-Header weitergegeben werden (Browser DevTools → Network)
|
||||||
|
3. Stelle sicher, dass der `/uploads/` Handle korrekt konfiguriert ist
|
||||||
|
|
||||||
|
### Container können nicht kommunizieren
|
||||||
|
|
||||||
|
**Problem**: Caddy kann den hoerdle-Container nicht erreichen.
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe, ob beide Container im gleichen Netzwerk sind:
|
||||||
|
```bash
|
||||||
|
docker network inspect hoerdle_default
|
||||||
|
```
|
||||||
|
2. Prüfe, ob hoerdle-Container läuft: `docker ps | grep hoerdle`
|
||||||
|
3. Teste Verbindung von Caddy zu Hördle:
|
||||||
|
```bash
|
||||||
|
docker exec hoerdle-caddy wget -O- http://hoerdle:3000/api/health
|
||||||
|
```
|
||||||
|
**Hinweis**: Der Container-Port ist 3000 (nicht 3010, das ist nur der Host-Port).
|
||||||
|
|
||||||
|
### Netzwerk-Warnung beim Deployment
|
||||||
|
|
||||||
|
**Problem**: Warnung `network hoerdle_default was found but has incorrect label`
|
||||||
|
|
||||||
|
**Erklärung**: Diese Warnung ist **harmlos** und kann ignoriert werden. Docker Compose funktioniert trotzdem einwandfrei. Sie entsteht, wenn das Netzwerk bereits existiert, aber nicht von Docker Compose erstellt wurde.
|
||||||
|
|
||||||
|
**Optional: Warnung beheben** (nur wenn sie stört):
|
||||||
|
```bash
|
||||||
|
# Reparatur-Skript ausführen (stoppt Container kurz)
|
||||||
|
./scripts/fix-network.sh
|
||||||
|
|
||||||
|
# Danach Container neu starten
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis**: Das Reparatur-Skript stoppt alle Container kurz, die das Netzwerk nutzen. In Produktion sollte dies außerhalb der Hauptnutzungszeit erfolgen.
|
||||||
|
|
||||||
|
## Deployment-Workflow
|
||||||
|
|
||||||
|
### Caddy nur in Produktion aktivieren
|
||||||
|
|
||||||
|
Die `docker-compose.caddy.yml` verwendet das `production`-Profile. Um Caddy zu aktivieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mit Production-Profile
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
|
||||||
|
# Ohne Caddy (nur Hördle)
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddy aktualisieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull neues Caddy-Image
|
||||||
|
docker compose -f docker-compose.caddy.yml pull
|
||||||
|
|
||||||
|
# Restart Caddy-Container
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddy-Konfiguration ändern
|
||||||
|
|
||||||
|
Nach Änderungen am Caddyfile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Caddyfile validieren
|
||||||
|
docker run --rm -v $(pwd)/Caddyfile:/etc/caddy/Caddyfile:ro caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# Caddy neu laden (ohne Downtime)
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production exec caddy caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
|
||||||
|
### API-Keys schützen
|
||||||
|
|
||||||
|
- **Niemals** API-Keys in Git committen
|
||||||
|
- Verwende `.env`-Dateien (sollten in `.gitignore` sein)
|
||||||
|
- Setze minimale Berechtigungen für API-Keys in GoDaddy
|
||||||
|
- Rotiere API-Keys regelmäßig
|
||||||
|
|
||||||
|
### Firewall
|
||||||
|
|
||||||
|
Stelle sicher, dass nur Ports 80 und 443 öffentlich erreichbar sind. Port 3010 (Hördle) sollte nicht öffentlich erreichbar sein.
|
||||||
|
|
||||||
|
## Weitere Ressourcen
|
||||||
|
|
||||||
|
- [Caddy Dokumentation](https://caddyserver.com/docs/)
|
||||||
|
- [Caddy DNS-Provider](https://caddyserver.com/docs/modules/tls.dns)
|
||||||
|
- [GoDaddy API Dokumentation](https://developer.godaddy.com/doc/endpoint/domains)
|
||||||
|
- [Let's Encrypt Wildcard-Zertifikate](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge)
|
||||||
|
|
||||||
183
docs/CADDY_TROUBLESHOOTING.md
Normal file
183
docs/CADDY_TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Caddy Zertifikat-Troubleshooting
|
||||||
|
|
||||||
|
## Problem: Zertifikat für Punycode-Domain (hördle.de / xn--hrdle-jua.de) fehlt
|
||||||
|
|
||||||
|
Wenn die Domain `hördle.de` (xn--hrdle-jua.de) einen `ERR_SSL_PROTOCOL_ERROR` zeigt, bedeutet das, dass kein gültiges SSL-Zertifikat vorhanden ist.
|
||||||
|
|
||||||
|
### Schritt 1: Zertifikat-Status prüfen
|
||||||
|
|
||||||
|
Führe das Check-Script aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/check-caddy-certificates.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Script prüft:
|
||||||
|
- Ob Caddy läuft
|
||||||
|
- Welche Zertifikate vorhanden sind
|
||||||
|
- Ob die DNS-Einträge korrekt sind
|
||||||
|
- Ob die HTTPS-Verbindungen funktionieren
|
||||||
|
|
||||||
|
### Schritt 2: DNS-Einträge prüfen
|
||||||
|
|
||||||
|
**Wichtig**: Beide Domains müssen auf die gleiche Server-IP zeigen!
|
||||||
|
|
||||||
|
#### In GoDaddy prüfen:
|
||||||
|
|
||||||
|
1. Gehe zu [GoDaddy DNS-Verwaltung](https://dcc.godaddy.com/manage/hoerdle.de/dns)
|
||||||
|
2. Prüfe die A-Records:
|
||||||
|
|
||||||
|
**Für hoerdle.de:**
|
||||||
|
- Name: `@` oder `hoerdle.de`
|
||||||
|
- Typ: `A`
|
||||||
|
- Wert: `DEINE_SERVER_IP`
|
||||||
|
|
||||||
|
**Für hördle.de (Punycode):**
|
||||||
|
- Name: `@` oder `xn--hrdle-jua.de` (oder der Unicode-Name, falls unterstützt)
|
||||||
|
- Typ: `A`
|
||||||
|
- Wert: **GLEICHE_SERVER_IP wie hoerdle.de**
|
||||||
|
|
||||||
|
#### DNS manuell testen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe hoerdle.de
|
||||||
|
dig +short hoerdle.de @8.8.8.8
|
||||||
|
|
||||||
|
# Prüfe xn--hrdle-jua.de (Punycode)
|
||||||
|
dig +short xn--hrdle-jua.de @8.8.8.8
|
||||||
|
|
||||||
|
# Beide sollten die gleiche IP zurückgeben!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 3: Zertifikat neu erstellen
|
||||||
|
|
||||||
|
Wenn die DNS-Einträge korrekt sind, lösche das alte (fehlgeschlagene) Zertifikat und lass Caddy es neu erstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/renew-caddy-certificates.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Wähle Option 2: "Nur Zertifikat für xn--hrdle-jua.de löschen"
|
||||||
|
|
||||||
|
### Schritt 4: Caddy-Logs überwachen
|
||||||
|
|
||||||
|
Während Caddy das Zertifikat erstellt, überwache die Logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs hoerdle-caddy -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Du solltest sehen:
|
||||||
|
- `[INFO] attempting ACME challenge` - Caddy versucht die Challenge
|
||||||
|
- `[INFO] successfully completed ACME challenge` - Challenge erfolgreich
|
||||||
|
- `[INFO] certificate obtained successfully` - Zertifikat erstellt
|
||||||
|
|
||||||
|
Bei Fehlern siehst du:
|
||||||
|
- `[ERROR] acme: error` - Challenge fehlgeschlagen
|
||||||
|
- `[ERROR] unable to validate` - Validierung fehlgeschlagen
|
||||||
|
|
||||||
|
### Schritt 5: Häufige Probleme und Lösungen
|
||||||
|
|
||||||
|
#### Problem 1: DNS zeigt auf falsche IP
|
||||||
|
|
||||||
|
**Symptom**: `dig` zeigt eine andere IP als erwartet
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe DNS-Einträge in GoDaddy
|
||||||
|
2. Warte auf DNS-Propagierung (kann 5-60 Minuten dauern)
|
||||||
|
3. Verwende einen DNS-Checker: https://www.whatsmydns.net/
|
||||||
|
|
||||||
|
#### Problem 2: Port 80 nicht erreichbar
|
||||||
|
|
||||||
|
**Symptom**: Caddy-Logs zeigen "connection refused" oder Timeout
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe Firewall: `sudo ufw status`
|
||||||
|
2. Prüfe ob Port 80 offen ist: `sudo netstat -tulpn | grep :80`
|
||||||
|
3. Prüfe ob Caddy auf Port 80 lauscht: `docker exec hoerdle-caddy netstat -tulpn | grep :80`
|
||||||
|
|
||||||
|
#### Problem 3: Let's Encrypt Rate Limit
|
||||||
|
|
||||||
|
**Symptom**: Logs zeigen "too many certificates already issued"
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
- Warte 1 Woche (Rate Limit von Let's Encrypt)
|
||||||
|
- Oder verwende Staging-Environment zum Testen:
|
||||||
|
```caddyfile
|
||||||
|
tls {
|
||||||
|
staging
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Problem 4: Punycode-Domain wird nicht erkannt
|
||||||
|
|
||||||
|
**Symptom**: Caddy erstellt Zertifikat nur für hoerdle.de, nicht für xn--hrdle-jua.de
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfe ob beide Domains in der Caddyfile stehen
|
||||||
|
2. Prüfe DNS-Einträge (siehe Schritt 2)
|
||||||
|
3. Erzwinge Zertifikat-Erstellung (siehe Schritt 3)
|
||||||
|
|
||||||
|
### Manuelle Zertifikat-Löschung
|
||||||
|
|
||||||
|
Falls das Script nicht funktioniert, kannst du Zertifikate manuell löschen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Zertifikate löschen
|
||||||
|
docker exec hoerdle-caddy rm -rf /data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/*
|
||||||
|
|
||||||
|
# Nur Punycode-Zertifikat löschen (manuell)
|
||||||
|
docker exec hoerdle-caddy find /data/caddy/certificates -name "*xn--*" -delete
|
||||||
|
|
||||||
|
# Container neu starten
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS-Propagierung prüfen
|
||||||
|
|
||||||
|
Nach DNS-Änderungen kann es bis zu 60 Minuten dauern, bis alle DNS-Server aktualisiert sind:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe DNS-Propagierung weltweit
|
||||||
|
curl "https://dnschecker.org/#A/hoerdle.de"
|
||||||
|
curl "https://dnschecker.org/#A/xn--hrdle-jua.de"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test-Zertifikat erstellen (Staging)
|
||||||
|
|
||||||
|
Zum Testen ohne Rate-Limits kannst du ein Staging-Zertifikat erstellen:
|
||||||
|
|
||||||
|
1. Temporär Caddyfile ändern (in beiden Domain-Blocks):
|
||||||
|
```caddyfile
|
||||||
|
tls {
|
||||||
|
staging
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Container neu starten
|
||||||
|
3. Zertifikat erstellen lassen
|
||||||
|
4. Zurück zu Produktion ändern (Staging-Block entfernen)
|
||||||
|
5. Erneut Container neu starten
|
||||||
|
|
||||||
|
### Verifizieren, dass es funktioniert
|
||||||
|
|
||||||
|
Nach erfolgreicher Zertifikats-Erstellung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Teste HTTPS-Verbindung
|
||||||
|
curl -I https://hoerdle.de
|
||||||
|
curl -I https://xn--hrdle-jua.de
|
||||||
|
|
||||||
|
# Prüfe Zertifikat-Details
|
||||||
|
echo | openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | openssl x509 -noout -subject -dates
|
||||||
|
echo | openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | openssl x509 -noout -subject -dates
|
||||||
|
```
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
Falls das Problem weiterhin besteht:
|
||||||
|
1. Prüfe Caddy-Logs: `docker logs hoerdle-caddy`
|
||||||
|
2. Prüfe DNS: `dig +short xn--hrdle-jua.de @8.8.8.8`
|
||||||
|
3. Prüfe Firewall: `sudo ufw status`
|
||||||
|
4. Prüfe Port-Zugriff: `curl -I http://hoerdle.de`
|
||||||
|
|
||||||
@@ -82,3 +82,35 @@ docker ps
|
|||||||
```
|
```
|
||||||
|
|
||||||
Look for the "healthy" status in the STATUS column.
|
Look for the "healthy" status in the STATUS column.
|
||||||
|
|
||||||
|
## Caddy Reverse Proxy (Optional - Production)
|
||||||
|
|
||||||
|
For production deployments with automatic SSL/TLS certificates, Caddy can be used as a reverse proxy. Caddy provides:
|
||||||
|
|
||||||
|
- Automatic Let's Encrypt certificates (including wildcard certificates)
|
||||||
|
- HTTP to HTTPS redirect
|
||||||
|
- Optimized settings for audio streaming and file uploads
|
||||||
|
- Support for both `hoerdle.de` and `hördle.de` (Punycode: `xn--hrdle-jua.de`)
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. **Follow the setup guide**: See `CADDY_SETUP.md` for detailed instructions
|
||||||
|
2. **Configure environment variables**: Add GoDaddy API credentials to your `.env` file
|
||||||
|
3. **Start with Caddy**:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.caddy.yml --profile production up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without Caddy
|
||||||
|
|
||||||
|
If you don't want to use Caddy, you can deploy normally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will still be accessible on port 3010, but you'll need to configure SSL/TLS separately (e.g., with nginx).
|
||||||
|
|
||||||
|
### Caddy Troubleshooting
|
||||||
|
|
||||||
|
See `CADDY_SETUP.md` for detailed troubleshooting information.
|
||||||
106
docs/DOCKER_BUILD_FIX.md
Normal file
106
docs/DOCKER_BUILD_FIX.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Docker Build Fix: Upload-Dateien ausschließen
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Der Docker Build schlug fehl mit:
|
||||||
|
```
|
||||||
|
no space left on device
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ursache**: Die großen MP3-Dateien in `public/uploads/` wurden in den Build-Context kopiert und verbrauchten zu viel Speicherplatz.
|
||||||
|
|
||||||
|
## Lösung
|
||||||
|
|
||||||
|
Eine `.dockerignore` Datei wurde erstellt, die folgende Dateien/Ordner vom Build ausschließt:
|
||||||
|
|
||||||
|
- `public/uploads/*` - Upload-Dateien (werden als Volume gemountet)
|
||||||
|
- `data/*` - Datenbank-Dateien (werden als Volume gemountet)
|
||||||
|
- `node_modules` - werden während des Builds installiert
|
||||||
|
- `.next`, `out`, `build` - Build-Artefakte
|
||||||
|
- Backup-Dateien, Logs, temporäre Dateien
|
||||||
|
|
||||||
|
## Zusätzliche Maßnahmen
|
||||||
|
|
||||||
|
Falls der Build weiterhin Probleme macht:
|
||||||
|
|
||||||
|
### 1. Docker aufräumen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Entferne nicht verwendete Images
|
||||||
|
docker image prune -a
|
||||||
|
|
||||||
|
# Entferne nicht verwendete Container
|
||||||
|
docker container prune
|
||||||
|
|
||||||
|
# Entferne nicht verwendete Volumes (VORSICHT: kann Daten löschen!)
|
||||||
|
docker volume prune
|
||||||
|
|
||||||
|
# Kompletter Cleanup (alles außer laufenden Containern)
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Speicherplatz prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Zeige Speicherplatz
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Zeige Docker-Speicherverbrauch
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
# Zeige größte Images
|
||||||
|
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | sort -k3 -h
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build-Kontext prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe was in den Build-Context kopiert wird
|
||||||
|
docker build --no-cache --progress=plain -t test-build . 2>&1 | grep "transferring context"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Upload-Dateien manuell ausschließen
|
||||||
|
|
||||||
|
Falls die `.dockerignore` nicht greift, können Upload-Dateien vorübergehend verschoben werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vor dem Build
|
||||||
|
mv public/uploads public/uploads.backup
|
||||||
|
mkdir -p public/uploads
|
||||||
|
touch public/uploads/.gitkeep
|
||||||
|
|
||||||
|
# Build durchführen
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
# Uploads wiederherstellen
|
||||||
|
rm -rf public/uploads
|
||||||
|
mv public/uploads.backup public/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtig
|
||||||
|
|
||||||
|
Die Upload-Dateien werden **nicht** ins Docker-Image kopiert, sondern als Volume gemountet:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./public/uploads:/app/public/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
Das bedeutet:
|
||||||
|
- Upload-Dateien bleiben auf dem Host-System
|
||||||
|
- Sie werden zur Laufzeit gemountet
|
||||||
|
- Sie sollten **nicht** ins Image kopiert werden (spart viel Speicher)
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Nach dem Build sollte das Image deutlich kleiner sein:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Zeige Image-Größe
|
||||||
|
docker images hoerdle-hoerdle
|
||||||
|
|
||||||
|
# Prüfe ob Uploads im Image sind
|
||||||
|
docker run --rm hoerdle-hoerdle ls -lh /app/public/uploads
|
||||||
|
# Sollte nur .gitkeep oder Covers zeigen, keine MP3-Dateien
|
||||||
|
```
|
||||||
|
|
||||||
83
docs/FIX_I18N.md
Normal file
83
docs/FIX_I18N.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Fix für i18n-Daten (String → JSON Konvertierung)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Die Datenbank hat Genre-/Special-/News-Namen als einfache Strings (`"Rock"`) statt JSON (`{"de": "Rock", "en": "Rock"}`) gespeichert, was zu `SyntaxError: "Rock" is not valid JSON` führt.
|
||||||
|
|
||||||
|
## Lösung: Manuell ausführen
|
||||||
|
|
||||||
|
Führe diese Befehle **direkt auf dem Server** aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/hoerdle
|
||||||
|
|
||||||
|
# 1. Backup erstellen
|
||||||
|
docker cp hoerdle:/app/data/prod.db ./data/prod.db.backup.$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# 2. Kopiere DB lokal
|
||||||
|
docker cp hoerdle:/app/data/prod.db ./data/prod.db.tmp
|
||||||
|
|
||||||
|
# 3. Setze Berechtigungen
|
||||||
|
sudo chmod 666 ./data/prod.db.tmp
|
||||||
|
sudo chmod 775 ./data
|
||||||
|
|
||||||
|
# 4. Prüfe ob sqlite3 installiert ist
|
||||||
|
which sqlite3 || sudo apt-get install -y sqlite3
|
||||||
|
|
||||||
|
# 5. Fixe die Datenbank (kopiere diesen Block komplett)
|
||||||
|
sqlite3 ./data/prod.db.tmp << 'EOF'
|
||||||
|
UPDATE Genre SET name = json_object('de', name, 'en', name) WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
UPDATE Genre SET subtitle = json_object('de', subtitle, 'en', subtitle) WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
UPDATE Special SET name = json_object('de', name, 'en', name) WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
UPDATE Special SET subtitle = json_object('de', subtitle, 'en', subtitle) WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
UPDATE News SET title = json_object('de', title, 'en', title) WHERE typeof(title) = 'text' AND title NOT LIKE '{%';
|
||||||
|
UPDATE News SET content = json_object('de', content, 'en', content) WHERE typeof(content) = 'text' AND content NOT LIKE '{%';
|
||||||
|
SELECT '✅ Fertig!' as status;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 6. Kopiere zurück
|
||||||
|
docker cp ./data/prod.db.tmp hoerdle:/app/data/prod.db
|
||||||
|
|
||||||
|
# 7. Aufräumen
|
||||||
|
rm ./data/prod.db.tmp
|
||||||
|
|
||||||
|
# 8. Container neu starten
|
||||||
|
docker compose restart hoerdle
|
||||||
|
|
||||||
|
# 9. Logs prüfen
|
||||||
|
docker logs hoerdle --tail=50
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls Schritt 5 mit "permission denied" fehlschlägt, verwende `sudo`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo sqlite3 ./data/prod.db.tmp << 'EOF'
|
||||||
|
[... SQL-Befehle wie oben ...]
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatisiertes Skript
|
||||||
|
|
||||||
|
Alternativ kannst du das automatische Skript verwenden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/fix-i18n-easy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder das lokale Skript:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/fix-i18n-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prüfen ob es funktioniert hat
|
||||||
|
|
||||||
|
Nach dem Neustart sollte die Seite wieder funktionieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Logs (sollte keine JSON-Fehler mehr zeigen)
|
||||||
|
docker logs hoerdle --tail=100 | grep -i "json\|error" || echo "✅ Keine JSON-Fehler gefunden"
|
||||||
|
|
||||||
|
# Teste die Seite
|
||||||
|
curl -s https://hoerdle.de/de | head -20
|
||||||
|
```
|
||||||
|
|
||||||
@@ -8,14 +8,14 @@ Die i18n-Implementierung basiert auf [next-intl](https://next-intl-docs.vercel.a
|
|||||||
|
|
||||||
## Unterstützte Sprachen
|
## Unterstützte Sprachen
|
||||||
|
|
||||||
- **Deutsch (de)** - Standardsprache
|
- **Englisch (en)** - Standardsprache
|
||||||
- **Englisch (en)**
|
- **Deutsch (de)**
|
||||||
|
|
||||||
## URL-Struktur
|
## URL-Struktur
|
||||||
|
|
||||||
Alle Routen sind lokalisiert:
|
Alle Routen sind lokalisiert:
|
||||||
|
|
||||||
- `http://localhost:3000/` → Redirect zu `/de` (Standard)
|
- `http://localhost:3000/` → Redirect zu `/en` (Standard)
|
||||||
- `http://localhost:3000/de` → Deutsche Version
|
- `http://localhost:3000/de` → Deutsche Version
|
||||||
- `http://localhost:3000/en` → Englische Version
|
- `http://localhost:3000/en` → Englische Version
|
||||||
- `http://localhost:3000/de/admin` → Admin-Dashboard (Deutsch)
|
- `http://localhost:3000/de/admin` → Admin-Dashboard (Deutsch)
|
||||||
@@ -103,8 +103,8 @@ const genreNameEn = getLocalizedValue(genre.name, 'en'); // "Rock"
|
|||||||
|
|
||||||
**Fallback-Verhalten:**
|
**Fallback-Verhalten:**
|
||||||
1. Versucht die angeforderte Locale (`de` oder `en`)
|
1. Versucht die angeforderte Locale (`de` oder `en`)
|
||||||
2. Fallback zu `de` falls nicht vorhanden
|
2. Fallback zu `en` falls nicht vorhanden
|
||||||
3. Fallback zu `en` falls nicht vorhanden
|
3. Fallback zu `de` falls nicht vorhanden
|
||||||
4. Fallback zum ersten verfügbaren Schlüssel
|
4. Fallback zum ersten verfügbaren Schlüssel
|
||||||
5. Fallback zum übergebenen `fallback`-Parameter
|
5. Fallback zum übergebenen `fallback`-Parameter
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ Bestehende Daten werden automatisch migriert:
|
|||||||
|
|
||||||
Der Proxy (`proxy.ts`) leitet Anfragen automatisch um:
|
Der Proxy (`proxy.ts`) leitet Anfragen automatisch um:
|
||||||
|
|
||||||
- `/` → `/de` (Standard)
|
- `/` → `/en` (Standard)
|
||||||
- Ungültige Locales → 404
|
- Ungültige Locales → 404
|
||||||
- Validiert Locale-Parameter
|
- Validiert Locale-Parameter
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ GET /api/specials?locale=en
|
|||||||
GET /api/news?locale=de
|
GET /api/news?locale=de
|
||||||
```
|
```
|
||||||
|
|
||||||
Falls kein `locale` angegeben wird, wird `de` als Standard verwendet.
|
Falls kein `locale` angegeben wird, wird `en` als Standard verwendet.
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
167
docs/PLAUSIBLE_SETUP.md
Normal file
167
docs/PLAUSIBLE_SETUP.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Plausible Analytics Konfiguration
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Die App verwendet Plausible Analytics für anonyme Nutzungsstatistiken. Die Konfiguration erfolgt über Umgebungsvariablen.
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### Erforderliche Variablen
|
||||||
|
|
||||||
|
**Nur eine Variable ist erforderlich:**
|
||||||
|
|
||||||
|
1. **`NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC`** (erforderlich)
|
||||||
|
- Die vollständige URL zum Plausible-Script
|
||||||
|
- Beispiel (selbst gehostet): `https://plausible.elpatron.me/js/script.js`
|
||||||
|
- Beispiel (extern): `https://plausible.io/js/script.js`
|
||||||
|
|
||||||
|
**Hinweis:** Die Domain wird automatisch aus der Request-Domain erkannt. Beide Domains (`hoerdle.de` und `hördle.de`) werden automatisch getrackt.
|
||||||
|
|
||||||
|
### Konfiguration für Docker
|
||||||
|
|
||||||
|
Da es sich um **Build-Time Variablen** handelt (NEXT_PUBLIC_*), muss die App neu gebaut werden, wenn diese geändert werden.
|
||||||
|
|
||||||
|
#### Schritt 1: Umgebungsvariablen setzen
|
||||||
|
|
||||||
|
Erstelle oder bearbeite eine `.env`-Datei im Projektverzeichnis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Plausible Analytics (Script-URL ist erforderlich)
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC=https://plausible.elpatron.me/js/script.js
|
||||||
|
|
||||||
|
# Die Domain wird automatisch erkannt - keine weitere Konfiguration nötig!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Schritt 2: docker-compose.yml konfigurieren
|
||||||
|
|
||||||
|
Stelle sicher, dass die Variablen als Build-Args übergeben werden:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
hoerdle:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC: ${NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC}
|
||||||
|
```
|
||||||
|
|
||||||
|
Die `docker-compose.example.yml` enthält bereits diese Konfiguration.
|
||||||
|
|
||||||
|
#### Schritt 3: App neu bauen
|
||||||
|
|
||||||
|
**WICHTIG:** Nach Änderung der Plausible-Variablen muss die App neu gebaut werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder mit dem Deploy-Script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Konfiguration für beide Domains
|
||||||
|
|
||||||
|
Die App unterstützt **automatisches Tracking** für beide Domains (`hoerdle.de` und `hördle.de`). Die Domain wird automatisch aus dem Request-Header ausgelesen und entsprechend in Plausible getrackt.
|
||||||
|
|
||||||
|
#### Automatisches Domain-Tracking
|
||||||
|
|
||||||
|
**Standard-Verhalten:** Die App erkennt automatisch, welche Domain aufgerufen wurde, und setzt die entsprechende `data-domain` im Plausible-Script:
|
||||||
|
- `https://hoerdle.de/*` → `data-domain="hoerdle.de"`
|
||||||
|
- `https://hördle.de/*` → `data-domain="hördle.de"`
|
||||||
|
|
||||||
|
#### In Plausible konfigurieren
|
||||||
|
|
||||||
|
Du hast zwei Optionen:
|
||||||
|
|
||||||
|
##### Option 1: Beide Domains als separate Sites (separate Statistiken) - Empfohlen für getrenntes Tracking
|
||||||
|
|
||||||
|
1. Erstelle in Plausible zwei separate Sites:
|
||||||
|
- `hoerdle.de`
|
||||||
|
- `hördle.de`
|
||||||
|
|
||||||
|
2. Fertig! Die App trackt automatisch die richtige Domain.
|
||||||
|
|
||||||
|
**Vorteil:** Separate Statistiken für jede Domain.
|
||||||
|
|
||||||
|
##### Option 2: Beide Domains als Aliase für eine Site (gemeinsame Statistiken)
|
||||||
|
|
||||||
|
1. Erstelle in Plausible eine Site: `hoerdle.de`
|
||||||
|
2. Füge `hördle.de` als Alias hinzu (in den Site-Einstellungen)
|
||||||
|
|
||||||
|
3. Fertig! Die App trackt automatisch die richtige Domain, und Plausible behandelt beide als Aliase für die gleiche Site.
|
||||||
|
|
||||||
|
**Hinweis:** Du musst nichts zusätzlich konfigurieren. Die App trackt automatisch `hoerdle.de` oder `hördle.de` basierend auf der Request-Domain, und Plausible erkennt beide als Aliase.
|
||||||
|
|
||||||
|
**Vorteil:** Gemeinsame Statistiken für beide Domains in einer Site.
|
||||||
|
|
||||||
|
#### Empfehlung
|
||||||
|
|
||||||
|
Für separate Statistiken: **Option 1** (automatisches Tracking)
|
||||||
|
Für gemeinsame Statistiken: **Option 2** (Aliase in Plausible)
|
||||||
|
|
||||||
|
### Automatische CSP-Anpassung
|
||||||
|
|
||||||
|
Die Content Security Policy (CSP) in `proxy.ts` wird automatisch an die konfigurierte Plausible-URL angepasst. Die Domain wird automatisch aus der Script-URL extrahiert.
|
||||||
|
|
||||||
|
### Prüfen der Konfiguration
|
||||||
|
|
||||||
|
Nach dem Neubau kannst du prüfen, ob Plausible korrekt geladen wird:
|
||||||
|
|
||||||
|
1. **Browser-Entwicklertools öffnen**
|
||||||
|
- Network-Tab: Suche nach dem Plausible-Script
|
||||||
|
- Console: Prüfe auf Fehler
|
||||||
|
|
||||||
|
2. **Prüfe die Meta-Tags**
|
||||||
|
```html
|
||||||
|
<script defer data-domain="hoerdle.de" src="https://plausible.elpatron.me/js/script.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Prüfe Plausible-Dashboard**
|
||||||
|
- Öffne dein Plausible-Dashboard
|
||||||
|
- Prüfe, ob Daten ankommen
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Plausible wird nicht geladen
|
||||||
|
|
||||||
|
- Prüfe, ob die Umgebungsvariablen korrekt gesetzt sind
|
||||||
|
- Prüfe, ob die App neu gebaut wurde (Build-Time Variablen!)
|
||||||
|
- Prüfe Browser-Console auf CSP-Fehler
|
||||||
|
|
||||||
|
#### CSP blockiert Plausible
|
||||||
|
|
||||||
|
Die CSP sollte automatisch angepasst werden. Falls Probleme auftreten:
|
||||||
|
- Prüfe, ob `NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC` korrekt gesetzt ist
|
||||||
|
- Prüfe die Logs des Containers
|
||||||
|
|
||||||
|
#### Daten werden nicht in Plausible angezeigt
|
||||||
|
|
||||||
|
- Prüfe, ob die Domain in Plausible als Site konfiguriert ist
|
||||||
|
- Prüfe, ob `data-domain` Attribut mit der konfigurierten Domain übereinstimmt
|
||||||
|
- Prüfe Browser-Console auf Fehler beim Laden des Scripts
|
||||||
|
|
||||||
|
### Beispiel-Konfiguration
|
||||||
|
|
||||||
|
#### Für selbst gehostetes Plausible:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC=https://plausible.elpatron.me/js/script.js
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Für Plausible.io (extern):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC=https://plausible.io/js/script.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** Die Domain wird automatisch aus der Request-Domain erkannt - keine weitere Konfiguration nötig!
|
||||||
|
|
||||||
|
### Weitere Informationen
|
||||||
|
|
||||||
|
- [Plausible Dokumentation](https://plausible.io/docs)
|
||||||
|
- [Plausible Self-Hosting](https://plausible.io/docs/self-hosting)
|
||||||
|
|
||||||
206
docs/TROUBLESHOOTING.md
Normal file
206
docs/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Troubleshooting Guide
|
||||||
|
|
||||||
|
## Application Error: "a server-side exception has occurred"
|
||||||
|
|
||||||
|
Dieser Fehler tritt auf, wenn die Next.js-Anwendung auf dem Server einen Fehler hat.
|
||||||
|
|
||||||
|
### ⚠️ Datenbank-Berechtigungen (wenn DB von anderem Server kopiert wurde)
|
||||||
|
|
||||||
|
**Symptom**: Application Error nach dem Kopieren einer Datenbank von einem anderen Server
|
||||||
|
|
||||||
|
**Ursache**: SQLite benötigt Schreibrechte auf:
|
||||||
|
- Die Datenbankdatei selbst (`prod.db`)
|
||||||
|
- Das Datenbankverzeichnis (für temporäre Dateien wie `-wal`, `-shm`)
|
||||||
|
|
||||||
|
**Sofort-Lösung (auf dem Server ausführen)**:
|
||||||
|
```bash
|
||||||
|
# 1. Setze Berechtigungen für Datenbankverzeichnis und Datei
|
||||||
|
chmod 775 ./data
|
||||||
|
chmod 664 ./data/prod.db
|
||||||
|
|
||||||
|
# 2. Falls temporäre SQLite-Dateien existieren, auch diese:
|
||||||
|
chmod 664 ./data/*.db-wal 2>/dev/null || true
|
||||||
|
chmod 664 ./data/*.db-shm 2>/dev/null || true
|
||||||
|
|
||||||
|
# 3. Oder verwende das Fix-Skript:
|
||||||
|
./scripts/fix-database-permissions.sh
|
||||||
|
|
||||||
|
# 4. Container neu starten
|
||||||
|
docker compose restart hoerdle
|
||||||
|
|
||||||
|
# 5. Logs prüfen
|
||||||
|
docker logs hoerdle --tail=50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warum passiert das?**
|
||||||
|
- Wenn du eine Datenbankdatei von einem anderen Server kopierst, behält sie die ursprünglichen Berechtigungen
|
||||||
|
- SQLite muss Schreibrechte haben, um zu funktionieren
|
||||||
|
- Auch das Verzeichnis braucht Schreibrechte (für SQLite-WAL-Modus)
|
||||||
|
|
||||||
|
### Sofort-Diagnose (auf dem Server ausführen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Container-Logs prüfen (die wichtigste Information!)
|
||||||
|
docker logs hoerdle --tail=100
|
||||||
|
|
||||||
|
# 2. Container-Status prüfen
|
||||||
|
docker ps | grep hoerdle
|
||||||
|
|
||||||
|
# 3. Prüfe ob Datenbank existiert
|
||||||
|
docker exec hoerdle ls -lh /app/data/prod.db
|
||||||
|
|
||||||
|
# 4. Prüfe ob Server auf Port 3000 antwortet (intern)
|
||||||
|
docker exec hoerdle curl -f http://localhost:3000/api/daily
|
||||||
|
|
||||||
|
# 5. Prüfe Health Check
|
||||||
|
docker inspect hoerdle --format='{{json .State.Health}}' | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Häufige Ursachen und Lösungen
|
||||||
|
|
||||||
|
#### 1. Datenbankfehler / Migrationen fehlgeschlagen
|
||||||
|
|
||||||
|
**Symptom**: Logs zeigen Prisma-Fehler oder "database locked"
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Container-Logs prüfen
|
||||||
|
docker logs hoerdle | grep -i "migration\|database\|prisma"
|
||||||
|
|
||||||
|
# Falls Migrationen fehlgeschlagen sind:
|
||||||
|
docker compose restart hoerdle
|
||||||
|
|
||||||
|
# Bei persistierenden Problemen: Datenbank-Backup prüfen
|
||||||
|
ls -lh ./backups/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Container läuft nicht oder ist crashed
|
||||||
|
|
||||||
|
**Symptom**: Container existiert nicht oder Status zeigt "Exited"
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Container-Status prüfen
|
||||||
|
docker ps -a | grep hoerdle
|
||||||
|
|
||||||
|
# Container neu starten
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Falls Container nicht startet, Logs prüfen
|
||||||
|
docker logs hoerdle --tail=200
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Caddy kann Container nicht erreichen
|
||||||
|
|
||||||
|
**Symptom**: 502 Bad Gateway oder "connection refused" in Caddy-Logs
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Prüfe ob hoerdle Container läuft
|
||||||
|
docker ps | grep hoerdle
|
||||||
|
|
||||||
|
# Prüfe Netzwerk
|
||||||
|
docker network inspect hoerdle_default
|
||||||
|
|
||||||
|
# Prüfe Caddy-Logs
|
||||||
|
docker logs hoerdle-caddy --tail=50
|
||||||
|
|
||||||
|
# Stelle sicher, dass Caddyfile Port 3000 verwendet (nicht 3010!)
|
||||||
|
grep "reverse_proxy" Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Fehlende Umgebungsvariablen
|
||||||
|
|
||||||
|
**Symptom**: Logs zeigen undefined variables
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Prüfe wichtige Umgebungsvariablen
|
||||||
|
docker exec hoerdle env | grep -E "DATABASE_URL|NODE_ENV"
|
||||||
|
|
||||||
|
# Prüfe .env Datei (falls vorhanden)
|
||||||
|
cat .env | grep DATABASE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Build-Fehler oder fehlerhafte Dateien
|
||||||
|
|
||||||
|
**Symptom**: Container startet, aber App crasht sofort
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
```bash
|
||||||
|
# Container komplett neu bauen
|
||||||
|
docker compose down
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Prüfe Build-Logs
|
||||||
|
docker compose build 2>&1 | tee build.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detaillierte Log-Analyse
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Fehler in Logs finden
|
||||||
|
docker logs hoerdle 2>&1 | grep -i -E "error|exception|fatal|panic" | tail -50
|
||||||
|
|
||||||
|
# Prisma-spezifische Fehler
|
||||||
|
docker logs hoerdle 2>&1 | grep -i prisma | tail -20
|
||||||
|
|
||||||
|
# Next.js-spezifische Fehler
|
||||||
|
docker logs hoerdle 2>&1 | grep -i "next\|react" | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Netzwerk-Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Teste Verbindung von Caddy zu Hördle
|
||||||
|
docker exec hoerdle-caddy wget -O- http://hoerdle:3000/api/daily
|
||||||
|
|
||||||
|
# Prüfe alle Container im Netzwerk
|
||||||
|
docker network inspect hoerdle_default --format='{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenbank-Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe Datenbank-Integrität
|
||||||
|
docker exec hoerdle npx prisma db pull
|
||||||
|
|
||||||
|
# Prüfe Datenbank-Struktur
|
||||||
|
docker exec hoerdle npx prisma studio &
|
||||||
|
# (dann Browser öffnen - erfordert X11 forwarding oder lokalen Zugriff)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick-Fix: Vollständiger Neustart
|
||||||
|
|
||||||
|
Wenn nichts anderes hilft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Backup erstellen
|
||||||
|
cp ./data/prod.db ./backups/prod_$(date +%Y%m%d_%H%M%S).db
|
||||||
|
|
||||||
|
# 2. Container stoppen
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# 3. Container neu starten
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 4. Logs beobachten
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bei weiterem Bedarf
|
||||||
|
|
||||||
|
Sammle folgende Informationen für weitere Hilfe:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "=== Container Status ===" && \
|
||||||
|
docker ps -a | grep hoerdle && \
|
||||||
|
echo -e "\n=== Letzte 50 Log-Zeilen ===" && \
|
||||||
|
docker logs hoerdle --tail=50 && \
|
||||||
|
echo -e "\n=== Fehler in Logs ===" && \
|
||||||
|
docker logs hoerdle 2>&1 | grep -i error | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
Kopiere die vollständige Ausgabe und sende sie weiter.
|
||||||
|
|
||||||
@@ -12,15 +12,15 @@ The application is configured via environment variables. You can set these in a
|
|||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `NEXT_PUBLIC_APP_NAME` | The name of the application. | `Hördle` |
|
| `NEXT_PUBLIC_APP_NAME` | The name of the application. | `Hördle` |
|
||||||
| `NEXT_PUBLIC_APP_DESCRIPTION` | The description used in metadata. | `Daily music guessing game...` |
|
| `NEXT_PUBLIC_APP_DESCRIPTION` | The description used in metadata. | `Daily music guessing game...` |
|
||||||
| `NEXT_PUBLIC_DOMAIN` | The domain name (used for sharing). | `hoerdle.elpatron.me` |
|
| `NEXT_PUBLIC_DOMAIN` | The domain name (used for sharing). | `hoerdle.de` |
|
||||||
| `NEXT_PUBLIC_TWITTER_HANDLE` | Twitter handle for metadata. | `@elpatron` |
|
|
||||||
|
|
||||||
### Analytics (Plausible)
|
### Analytics (Plausible)
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `NEXT_PUBLIC_PLAUSIBLE_DOMAIN` | The domain to track in Plausible. | `hoerdle.elpatron.me` |
|
| `NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC` | The URL of the Plausible script. | `https://plausible.example.com/js/script.js` |
|
||||||
| `NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC` | The URL of the Plausible script. | `https://plausible.elpatron.me/js/script.js` |
|
|
||||||
|
**Hinweis:** Die Domain wird automatisch aus der Request-Domain erkannt. Beide Domains (`hoerdle.de` und `hördle.de`) werden automatisch getrackt.
|
||||||
|
|
||||||
### Credits
|
### Credits
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
|||||||
console.log('[i18n/request] incoming requestLocale:', locale);
|
console.log('[i18n/request] incoming requestLocale:', locale);
|
||||||
|
|
||||||
if (!locale || !locales.includes(locale as (typeof locales)[number])) {
|
if (!locale || !locales.includes(locale as (typeof locales)[number])) {
|
||||||
locale = 'de';
|
locale = 'en';
|
||||||
console.log('[i18n/request] falling back to default locale:', locale);
|
console.log('[i18n/request] falling back to default locale:', locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
export const config = {
|
export const config = {
|
||||||
appName: process.env.NEXT_PUBLIC_APP_NAME || 'Hördle',
|
appName: process.env.NEXT_PUBLIC_APP_NAME || 'Hördle',
|
||||||
appDescription: process.env.NEXT_PUBLIC_APP_DESCRIPTION || 'Daily music guessing game - Guess the song from short audio clips',
|
appDescription: process.env.NEXT_PUBLIC_APP_DESCRIPTION || 'Daily music guessing game - Guess the song from short audio clips',
|
||||||
domain: process.env.NEXT_PUBLIC_DOMAIN || 'hoerdle.elpatron.me',
|
domain: process.env.NEXT_PUBLIC_DOMAIN || 'hoerdle.de',
|
||||||
twitterHandle: process.env.NEXT_PUBLIC_TWITTER_HANDLE || '@elpatron',
|
plausibleScriptSrc: process.env.NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC || 'https://plausible.example.com/js/script.js',
|
||||||
plausibleDomain: process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN || 'hoerdle.elpatron.me',
|
|
||||||
plausibleScriptSrc: process.env.NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC || 'https://plausible.elpatron.me/js/script.js',
|
|
||||||
colors: {
|
colors: {
|
||||||
themeColor: process.env.NEXT_PUBLIC_THEME_COLOR || '#000000',
|
themeColor: process.env.NEXT_PUBLIC_THEME_COLOR || '#000000',
|
||||||
backgroundColor: process.env.NEXT_PUBLIC_BACKGROUND_COLOR || '#ffffff',
|
backgroundColor: process.env.NEXT_PUBLIC_BACKGROUND_COLOR || '#ffffff',
|
||||||
@@ -14,5 +12,9 @@ export const config = {
|
|||||||
text: process.env.NEXT_PUBLIC_CREDITS_TEXT || 'Vibe coded with ☕ and 🍺 by',
|
text: process.env.NEXT_PUBLIC_CREDITS_TEXT || 'Vibe coded with ☕ and 🍺 by',
|
||||||
linkText: process.env.NEXT_PUBLIC_CREDITS_LINK_TEXT || '@elpatron@digitalcourage.social',
|
linkText: process.env.NEXT_PUBLIC_CREDITS_LINK_TEXT || '@elpatron@digitalcourage.social',
|
||||||
linkUrl: process.env.NEXT_PUBLIC_CREDITS_LINK_URL || 'https://digitalcourage.social/@elpatron',
|
linkUrl: process.env.NEXT_PUBLIC_CREDITS_LINK_URL || 'https://digitalcourage.social/@elpatron',
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
ogImage: process.env.NEXT_PUBLIC_OG_IMAGE || '/favicon.ico',
|
||||||
|
twitterHandle: process.env.NEXT_PUBLIC_TWITTER_HANDLE || undefined,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ export function getLocalizedValue(
|
|||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
if (value[locale]) return value[locale];
|
if (value[locale]) return value[locale];
|
||||||
|
|
||||||
// Fallback to 'de'
|
|
||||||
if (value['de']) return value['de'];
|
|
||||||
|
|
||||||
// Fallback to 'en'
|
// Fallback to 'en'
|
||||||
if (value['en']) return value['en'];
|
if (value['en']) return value['en'];
|
||||||
|
|
||||||
|
// Fallback to 'de'
|
||||||
|
if (value['de']) return value['de'];
|
||||||
|
|
||||||
// Fallback to first key
|
// Fallback to first key
|
||||||
const keys = Object.keys(value);
|
const keys = Object.keys(value);
|
||||||
if (keys.length > 0) return value[keys[0]];
|
if (keys.length > 0) return value[keys[0]];
|
||||||
|
|||||||
64
lib/metadata.ts
Normal file
64
lib/metadata.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { config } from './config';
|
||||||
|
import { getBaseUrl } from './seo';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate base metadata with Open Graph, Twitter Cards, and canonical URLs
|
||||||
|
*/
|
||||||
|
export async function generateBaseMetadata(
|
||||||
|
locale: string,
|
||||||
|
path: string = '',
|
||||||
|
title?: string,
|
||||||
|
description?: string,
|
||||||
|
image?: string
|
||||||
|
): Promise<Metadata> {
|
||||||
|
const baseUrl = await getBaseUrl();
|
||||||
|
const pathSegment = path ? `/${path}` : '';
|
||||||
|
const fullUrl = `${baseUrl}/${locale}${pathSegment}`;
|
||||||
|
|
||||||
|
// Determine alternate URLs for both locales (same path for both)
|
||||||
|
const alternateLocale = locale === 'de' ? 'en' : 'de';
|
||||||
|
const alternateUrl = `${baseUrl}/${alternateLocale}${pathSegment}`;
|
||||||
|
|
||||||
|
// Default values
|
||||||
|
const metaTitle = title || config.appName;
|
||||||
|
const metaDescription = description || config.appDescription;
|
||||||
|
const ogImage = image || `${baseUrl}${config.seo.ogImage}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: metaTitle,
|
||||||
|
description: metaDescription,
|
||||||
|
alternates: {
|
||||||
|
canonical: fullUrl,
|
||||||
|
languages: {
|
||||||
|
[locale]: fullUrl,
|
||||||
|
[alternateLocale]: alternateUrl,
|
||||||
|
'x-default': `${baseUrl}/en${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: metaTitle,
|
||||||
|
description: metaDescription,
|
||||||
|
url: fullUrl,
|
||||||
|
siteName: config.appName,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: ogImage,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: metaTitle,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: locale,
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: metaTitle,
|
||||||
|
description: metaDescription,
|
||||||
|
images: [ogImage],
|
||||||
|
...(config.seo.twitterHandle && { creator: config.seo.twitterHandle }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
43
lib/seo.ts
Normal file
43
lib/seo.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { headers } from 'next/headers';
|
||||||
|
import { config } from './config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current base URL from request headers
|
||||||
|
* Automatically detects hoerdle.de or hördle.de (xn--hrdle-jua.de)
|
||||||
|
*/
|
||||||
|
export async function getBaseUrl(): Promise<string> {
|
||||||
|
const headersList = await headers();
|
||||||
|
const host = headersList.get('host') || headersList.get('x-forwarded-host') || '';
|
||||||
|
|
||||||
|
let domain = config.domain; // Default fallback
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
// Extract domain from host (remove port if present)
|
||||||
|
const detectedDomain = host.split(':')[0].toLowerCase();
|
||||||
|
|
||||||
|
// Map domains
|
||||||
|
if (detectedDomain === 'hoerdle.de') {
|
||||||
|
domain = 'hoerdle.de';
|
||||||
|
} else if (detectedDomain === 'hördle.de' || detectedDomain === 'xn--hrdle-jua.de') {
|
||||||
|
domain = 'hördle.de';
|
||||||
|
} else {
|
||||||
|
// Use detected domain if it's different from default
|
||||||
|
domain = detectedDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always use HTTPS in production
|
||||||
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
return `${protocol}://${domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get base URL synchronously (for use in non-async contexts)
|
||||||
|
* Uses environment variable or config as fallback
|
||||||
|
*/
|
||||||
|
export function getBaseUrlSync(): string {
|
||||||
|
const domain = process.env.NEXT_PUBLIC_DOMAIN || config.domain;
|
||||||
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
return `${protocol}://${domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"curate": "Kurieren",
|
"curate": "Kuratieren",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"subtitle": "Untertitel",
|
"subtitle": "Untertitel",
|
||||||
"maxAttempts": "Max. Versuche",
|
"maxAttempts": "Max. Versuche",
|
||||||
@@ -151,5 +151,47 @@
|
|||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"deletePuzzle": "Löschen",
|
"deletePuzzle": "Löschen",
|
||||||
"wrongPassword": "Falsches Passwort"
|
"wrongPassword": "Falsches Passwort"
|
||||||
|
},
|
||||||
|
"About": {
|
||||||
|
"title": "Über Hördle & Impressum",
|
||||||
|
"intro": "Hördle ist ein nicht-kommerzielles, privat betriebenes Hobbyprojekt. Es gibt keine Werbeanzeigen, keine gesponserten Inhalte und keine versteckten Abo-Modelle.",
|
||||||
|
"projectTitle": "Über dieses Projekt",
|
||||||
|
"projectPrivateNote": "Hördle wird privat in der Freizeit entwickelt, betrieben, kuratiert und finanziert. Es besteht kein Anspruch auf permanente Verfügbarkeit oder Vollständigkeit.",
|
||||||
|
"projectIdea": "Die Idee hinter Hördle ist, Musik spielerisch neu zu entdecken und Lieblingssongs wiederzuentdecken – inspiriert von Wordle, aber für Musikfans.",
|
||||||
|
"imprintTitle": "Impressum",
|
||||||
|
"imprintOperator": "Verantwortlich für den Inhalt dieser Seite (Anbieter nach § 5 TMG):",
|
||||||
|
"imprintCountry": "Deutschland",
|
||||||
|
"imprintEmailLabel": "E-Mail:",
|
||||||
|
"costsTitle": "Laufende Kosten des Projekts",
|
||||||
|
"costsIntro": "Auch wenn Hördle ein privates Projekt ist, entstehen für den Betrieb laufende Kosten, zum Beispiel:",
|
||||||
|
"costsDomain": "Domains (z. B. hördle.de / hoerdle.de)",
|
||||||
|
"costsServer": "Server / vServer für App und Tracking",
|
||||||
|
"costsEmail": "E-Mail-Hosting",
|
||||||
|
"costsLicenses": "ggf. Gebühren für Urheberrechte oder andere Lizenzen",
|
||||||
|
"costsSheetLinkText": "Eine detaillierte, laufend gepflegte Übersicht über die aktuellen Kosten findest du in dieser <link>Google-Tabelle</link>.",
|
||||||
|
"costsSheetPrivacyNote": "Beim Aufruf oder Einbetten der Google-Tabelle können Daten (z. B. deine IP-Adresse) an Google übermittelt werden. Wenn du das nicht möchtest, öffne die Tabelle nicht.",
|
||||||
|
"supportTitle": "Hördle unterstützen",
|
||||||
|
"supportIntro": "Hördle ist ein nicht-kommerzielles Projekt, das von laufenden Kosten finanziert werden muss. Wenn du das Projekt finanziell unterstützen möchtest, gibt es folgende Möglichkeiten:",
|
||||||
|
"supportSepaTitle": "SEPA Banküberweisung (bevorzugt)",
|
||||||
|
"supportSepaName": "Markus Busche",
|
||||||
|
"supportSepaIban": "IBAN: DE28500310001071584000",
|
||||||
|
"supportPaypalTitle": "PayPal Spende",
|
||||||
|
"supportPaypalLink": "paypal.me/MBusche",
|
||||||
|
"supportSteadyTitle": "Steady",
|
||||||
|
"supportSteadyDescription": "Regelmäßige Unterstützung über Steady",
|
||||||
|
"privacyTitle": "Datenschutz",
|
||||||
|
"privacyIntro": "Der Schutz deiner Privatsphäre ist wichtig. Dieses Projekt versucht, so datensparsam wie möglich zu arbeiten.",
|
||||||
|
"privacyPlausibleTitle": "Selbst gehostetes Plausible Analytics",
|
||||||
|
"privacyPlausibleSelfHosted": "Für anonyme Nutzungsstatistiken wird Plausible Analytics auf einem selbst gehosteten Server verwendet. Es werden keine personalisierten Profile erstellt.",
|
||||||
|
"privacyPlausibleGemaTariff": "Das Tracking ist erforderlich, um den passenden GEMA Tarif zu bestimmen.",
|
||||||
|
"privacyPlausibleNoCookies": "Es werden keine Cookies für das Tracking gesetzt.",
|
||||||
|
"privacyPlausibleNoTrackingAcrossSites": "Es findet kein Tracking über mehrere Webseiten oder Geräte hinweg statt.",
|
||||||
|
"privacyPlausibleAggregated": "Auswertungen erfolgen ausschließlich in aggregierter Form (z. B. Seitenaufrufe, genutzte Browser).",
|
||||||
|
"privacyServerLogs": "Der Server kann technisch bedingt Logdateien mit IP-Adresse, Zeitpunkt des Zugriffs und abgerufenen Ressourcen führen. Diese Daten werden nur zur Sicherstellung des Betriebs und zur Fehleranalyse verwendet und regelmäßig gelöscht.",
|
||||||
|
"privacyContact": "Wenn du Fragen zu den verarbeiteten Daten hast oder Auskunft wünschst, kannst du dich über die im Impressum genannte E-Mail-Adresse melden.",
|
||||||
|
"privacyNoLegalAdvice": "Hinweis: Diese Datenschutzhinweise dienen nur als Beispiel und ersetzen keine rechtliche Beratung. Für eine rechtskonforme Datenschutzerklärung solltest du eine Fachperson konsultieren.",
|
||||||
|
"backTitle": "Zurück zum Spiel",
|
||||||
|
"backToGame": "Zurück zu Hördle",
|
||||||
|
"footerLinkLabel": "Über & Impressum"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,5 +151,47 @@
|
|||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"deletePuzzle": "Delete",
|
"deletePuzzle": "Delete",
|
||||||
"wrongPassword": "Wrong password"
|
"wrongPassword": "Wrong password"
|
||||||
|
},
|
||||||
|
"About": {
|
||||||
|
"title": "About Hördle & Imprint",
|
||||||
|
"intro": "Hördle is a non-commercial, privately run hobby project. There are no ads, no sponsored content and no hidden subscription models.",
|
||||||
|
"projectTitle": "About this project",
|
||||||
|
"projectPrivateNote": "Hördle is developed, operated, curated and financed privately in the creator's spare time. There is no guarantee for permanent availability or completeness.",
|
||||||
|
"projectIdea": "The idea behind Hördle is to (re)discover music in a playful way – inspired by Wordle, but for music lovers.",
|
||||||
|
"imprintTitle": "Imprint",
|
||||||
|
"imprintOperator": "Responsible for the content of this site (provider under German law):",
|
||||||
|
"imprintCountry": "Germany",
|
||||||
|
"imprintEmailLabel": "Email:",
|
||||||
|
"costsTitle": "Ongoing costs of the project",
|
||||||
|
"costsIntro": "Even though Hördle is a private project, there are ongoing costs for running it, for example:",
|
||||||
|
"costsDomain": "Domains (e.g. hördle.de / hoerdle.de)",
|
||||||
|
"costsServer": "Servers / vServers for the app and tracking",
|
||||||
|
"costsEmail": "Email hosting",
|
||||||
|
"costsLicenses": "Possible fees for copyrights or other licenses",
|
||||||
|
"costsSheetLinkText": "You can find a detailed, continuously updated overview of the current costs in this <link>Google Sheet</link>.",
|
||||||
|
"costsSheetPrivacyNote": "When accessing or embedding the Google Sheet, data (e.g. your IP address) may be transmitted to Google. If you don't want that, please do not open the sheet.",
|
||||||
|
"supportTitle": "Support Hördle",
|
||||||
|
"supportIntro": "Hördle is a non-commercial project that needs to be financed by ongoing costs. If you would like to support the project financially, here are the options:",
|
||||||
|
"supportSepaTitle": "SEPA Bank Transfer (preferred)",
|
||||||
|
"supportSepaName": "Markus Busche",
|
||||||
|
"supportSepaIban": "IBAN: DE28500310001071584000",
|
||||||
|
"supportPaypalTitle": "PayPal Donation",
|
||||||
|
"supportPaypalLink": "paypal.me/MBusche",
|
||||||
|
"supportSteadyTitle": "Steady",
|
||||||
|
"supportSteadyDescription": "Regular support via Steady",
|
||||||
|
"privacyTitle": "Privacy",
|
||||||
|
"privacyIntro": "Protecting your privacy matters. This project aims to collect as little data as possible.",
|
||||||
|
"privacyPlausibleTitle": "Self-hosted Plausible Analytics",
|
||||||
|
"privacyPlausibleSelfHosted": "For anonymous usage statistics, Plausible Analytics is used on a self-hosted server. No personal profiles are created.",
|
||||||
|
"privacyPlausibleGemaTariff": "Tracking is required to determine the appropriate GEMA tariff.",
|
||||||
|
"privacyPlausibleNoCookies": "No cookies are set for analytics purposes.",
|
||||||
|
"privacyPlausibleNoTrackingAcrossSites": "There is no tracking across multiple websites or devices.",
|
||||||
|
"privacyPlausibleAggregated": "Analytics are only performed in aggregated form (e.g. page views, browsers used).",
|
||||||
|
"privacyServerLogs": "For technical reasons, the server may log IP address, time of access and accessed resources. This data is only used to keep the service running and to debug issues and is deleted on a regular basis.",
|
||||||
|
"privacyContact": "If you have questions about the data processed or want to request information, please contact the email address given in the imprint.",
|
||||||
|
"privacyNoLegalAdvice": "Note: These privacy notes are only an example and do not replace legal advice. For a legally compliant privacy policy you should consult a professional.",
|
||||||
|
"backTitle": "Back to the game",
|
||||||
|
"backToGame": "Back to Hördle",
|
||||||
|
"footerLinkLabel": "About & Imprint"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hoerdle",
|
"name": "hoerdle",
|
||||||
"version": "0.1.0.15",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hoerdle",
|
"name": "hoerdle",
|
||||||
"version": "0.1.0.15",
|
"version": "0.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.19.0",
|
"@prisma/client": "^6.19.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoerdle",
|
"name": "hoerdle",
|
||||||
"version": "0.1.0.15",
|
"version": "0.1.4.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
35
proxy.ts
35
proxy.ts
@@ -2,9 +2,9 @@ import createMiddleware from 'next-intl/middleware';
|
|||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
const i18nMiddleware = createMiddleware({
|
const i18nMiddleware = createMiddleware({
|
||||||
locales: ['de', 'en'],
|
locales: ['en', 'de'],
|
||||||
defaultLocale: 'de',
|
defaultLocale: 'en',
|
||||||
// Wir nutzen überall Locale-Präfixe (`/de`, `/en`)
|
// Wir nutzen überall Locale-Präfixe (`/en`, `/de`)
|
||||||
localePrefix: 'always'
|
localePrefix: 'always'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -21,16 +21,41 @@ export default function proxy(request: NextRequest) {
|
|||||||
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||||
|
|
||||||
|
// Extract Plausible domain from script URL for CSP
|
||||||
|
const plausibleScriptSrc = process.env.NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC || 'https://plausible.example.com/js/script.js';
|
||||||
|
let plausibleOrigin = 'https://plausible.example.com';
|
||||||
|
try {
|
||||||
|
const url = new URL(plausibleScriptSrc);
|
||||||
|
plausibleOrigin = url.origin;
|
||||||
|
} catch {
|
||||||
|
// If URL parsing fails, try to extract domain manually
|
||||||
|
const match = plausibleScriptSrc.match(/https?:\/\/([^/]+)/);
|
||||||
|
if (match) {
|
||||||
|
plausibleOrigin = `https://${match[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get other service URLs from environment (only add to CSP if configured)
|
||||||
|
const gotifyUrl = process.env.GOTIFY_URL;
|
||||||
|
const openrouterUrl = process.env.NEXT_PUBLIC_OPENROUTER_URL || 'https://openrouter.ai';
|
||||||
|
|
||||||
|
// Build CSP dynamically based on environment variables
|
||||||
|
const connectSrcParts = ["'self'", openrouterUrl, plausibleOrigin];
|
||||||
|
if (gotifyUrl && !gotifyUrl.includes('example.com')) {
|
||||||
|
connectSrcParts.push(gotifyUrl);
|
||||||
|
}
|
||||||
|
|
||||||
const csp = [
|
const csp = [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://plausible.elpatron.me",
|
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${plausibleOrigin}`,
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' data: blob:",
|
"img-src 'self' data: blob:",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
"connect-src 'self' https://openrouter.ai https://gotify.example.com https://plausible.elpatron.me",
|
`connect-src ${connectSrcParts.join(' ')}`,
|
||||||
"media-src 'self' blob:",
|
"media-src 'self' blob:",
|
||||||
"frame-ancestors 'self'",
|
"frame-ancestors 'self'",
|
||||||
].join('; ');
|
].join('; ');
|
||||||
|
|
||||||
headers.set('Content-Security-Policy', csp);
|
headers.set('Content-Security-Policy', csp);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
94
scripts/check-caddy-certificates.sh
Executable file
94
scripts/check-caddy-certificates.sh
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script zum Prüfen der Caddy-Zertifikate
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔍 Prüfe Caddy-Zertifikat-Status..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Caddy-Container läuft
|
||||||
|
if ! docker ps | grep -q hoerdle-caddy; then
|
||||||
|
echo "❌ Caddy-Container läuft nicht!"
|
||||||
|
echo " Starte ihn mit: docker compose -f docker-compose.caddy.yml --profile production up -d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Caddy-Container läuft"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Zertifikate im Volume
|
||||||
|
echo "📜 Gespeicherte Zertifikate im Volume:"
|
||||||
|
docker exec hoerdle-caddy find /data/caddy/certificates -name "*.crt" -o -name "*.key" 2>/dev/null | head -20 || echo " (Keine Zertifikate gefunden oder Fehler beim Zugriff)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Caddy-Logs für Zertifikats-Fehler
|
||||||
|
echo "📋 Letzte Caddy-Logs (Zertifikat-bezogen):"
|
||||||
|
docker logs hoerdle-caddy 2>&1 | grep -i -E "(certificate|tls|acme|challenge|error)" | tail -20 || echo " (Keine relevanten Log-Einträge gefunden)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe DNS-Einträge
|
||||||
|
echo "🌐 DNS-Einträge prüfen:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe hoerdle.de
|
||||||
|
echo "1️⃣ hoerdle.de:"
|
||||||
|
HOERDLE_IP=$(dig +short hoerdle.de @8.8.8.8 | head -1 || echo "DNS-Fehler")
|
||||||
|
if [ -n "$HOERDLE_IP" ] && [ "$HOERDLE_IP" != "DNS-Fehler" ]; then
|
||||||
|
echo " ✅ DNS-A-Record: $HOERDLE_IP"
|
||||||
|
else
|
||||||
|
echo " ❌ DNS-A-Record: nicht aufgelöst"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe xn--hrdle-jua.de (Punycode)
|
||||||
|
echo "2️⃣ xn--hrdle-jua.de (hördle.de):"
|
||||||
|
PUNYCODE_IP=$(dig +short xn--hrdle-jua.de @8.8.8.8 | head -1 || echo "DNS-Fehler")
|
||||||
|
if [ -n "$PUNYCODE_IP" ] && [ "$PUNYCODE_IP" != "DNS-Fehler" ]; then
|
||||||
|
echo " ✅ DNS-A-Record: $PUNYCODE_IP"
|
||||||
|
else
|
||||||
|
echo " ❌ DNS-A-Record: nicht aufgelöst"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe auch die Unicode-Domain direkt (falls unterstützt)
|
||||||
|
echo "3️⃣ hördle.de (Unicode):"
|
||||||
|
UNICODE_IP=$(dig +short hördle.de @8.8.8.8 2>/dev/null | head -1 || echo "Nicht unterstützt")
|
||||||
|
if [ -n "$UNICODE_IP" ] && [ "$UNICODE_IP" != "Nicht unterstützt" ]; then
|
||||||
|
echo " ✅ DNS-A-Record: $UNICODE_IP"
|
||||||
|
else
|
||||||
|
echo " ⚠️ Unicode-Domain-Abfrage nicht unterstützt (normal)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔒 Zertifikat-Test:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Teste HTTPS-Verbindung zu hoerdle.de
|
||||||
|
echo "1️⃣ hoerdle.de HTTPS:"
|
||||||
|
if echo | timeout 5 openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | grep -q "Verify return code: 0"; then
|
||||||
|
echo " ✅ Zertifikat gültig"
|
||||||
|
CERT_INFO=$(echo | timeout 5 openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null || echo "")
|
||||||
|
echo " $CERT_INFO" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo " ❌ Zertifikat fehlt oder ungültig"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2️⃣ hördle.de (xn--hrdle-jua.de) HTTPS:"
|
||||||
|
if echo | timeout 5 openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | grep -q "Verify return code: 0"; then
|
||||||
|
echo " ✅ Zertifikat gültig"
|
||||||
|
CERT_INFO=$(echo | timeout 5 openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null || echo "")
|
||||||
|
echo " $CERT_INFO" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo " ❌ Zertifikat fehlt oder ungültig"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📝 Zertifikat-Details aus Caddy Volume:"
|
||||||
|
echo ""
|
||||||
|
docker exec hoerdle-caddy ls -la /data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/ 2>/dev/null | head -10 || echo " (Verzeichnis nicht gefunden oder leer)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "💡 Tipps:"
|
||||||
|
echo " - Wenn kein Zertifikat vorhanden ist, führe aus: ./scripts/renew-caddy-certificates.sh"
|
||||||
|
echo " - Prüfe die Caddy-Logs: docker logs hoerdle-caddy"
|
||||||
|
echo " - Prüfe DNS-Einträge in GoDaddy: https://dcc.godaddy.com/manage/hoerdle.de/dns"
|
||||||
|
|
||||||
65
scripts/check-db-permissions.sh
Executable file
65
scripts/check-db-permissions.sh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script zum Prüfen der Datenbank-Berechtigungen und User-Konfiguration
|
||||||
|
|
||||||
|
echo "🔍 Datenbank-Berechtigungen und User-Check"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe User im Container
|
||||||
|
echo "👤 User im Container:"
|
||||||
|
docker exec hoerdle whoami
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe UID/GID
|
||||||
|
echo "🆔 UID/GID des laufenden Prozesses:"
|
||||||
|
docker exec hoerdle id
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Datenbankdatei
|
||||||
|
echo "💾 Datenbank-Datei-Informationen (im Container):"
|
||||||
|
docker exec hoerdle ls -lh /app/data/prod.db
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Datenbankverzeichnis
|
||||||
|
echo "📁 Datenbankverzeichnis-Berechtigungen (im Container):"
|
||||||
|
docker exec hoerdle ls -ld /app/data
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Datei schreibbar ist
|
||||||
|
echo "✍️ Schreibbarkeitstest:"
|
||||||
|
if docker exec hoerdle sh -c "test -w /app/data/prod.db && echo '✅ Datei ist schreibbar' || echo '❌ Datei ist NICHT schreibbar'"; then
|
||||||
|
echo " Datei ist schreibbar"
|
||||||
|
else
|
||||||
|
echo " ❌ Datei ist NICHT schreibbar!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe ob Verzeichnis schreibbar ist
|
||||||
|
if docker exec hoerdle sh -c "test -w /app/data && echo '✅ Verzeichnis ist schreibbar' || echo '❌ Verzeichnis ist NICHT schreibbar'"; then
|
||||||
|
echo " Verzeichnis ist schreibbar"
|
||||||
|
else
|
||||||
|
echo " ❌ Verzeichnis ist NICHT schreibbar!"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Host-Seite
|
||||||
|
echo "🖥️ Host-Seite Berechtigungen:"
|
||||||
|
ls -ld ./data
|
||||||
|
ls -lh ./data/prod.db
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Container-User aus docker-compose
|
||||||
|
echo "🐳 Docker Compose Konfiguration:"
|
||||||
|
if [ -f "docker-compose.yml" ]; then
|
||||||
|
grep -E "^[[:space:]]*user:" docker-compose.yml || echo " Keine 'user:' Direktive gefunden"
|
||||||
|
else
|
||||||
|
echo " ⚠️ docker-compose.yml nicht gefunden (verwende example?)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Empfehlung
|
||||||
|
echo "💡 Empfehlung:"
|
||||||
|
echo " Wenn die Datei 'node:node' gehört und Container als 'root' läuft,"
|
||||||
|
echo " sollte es funktionieren. Falls nicht, setze Besitzer auf root:"
|
||||||
|
echo " sudo chown root:root ./data/prod.db"
|
||||||
|
echo " Oder entferne 'user: root' aus docker-compose.yml"
|
||||||
|
|
||||||
99
scripts/debug-server-error.sh
Executable file
99
scripts/debug-server-error.sh
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script zum Debuggen von Server-Errors in Hördle
|
||||||
|
# Zeigt relevante Logs und Status-Informationen
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔍 Hördle Server Error Debugging"
|
||||||
|
echo "=================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Container-Status prüfen
|
||||||
|
echo "📦 Container-Status:"
|
||||||
|
docker ps --filter "name=hoerdle" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Container läuft
|
||||||
|
if ! docker ps | grep -q "hoerdle"; then
|
||||||
|
echo "❌ FEHLER: hoerdle Container läuft nicht!"
|
||||||
|
echo ""
|
||||||
|
echo "Versuche Container zu starten..."
|
||||||
|
docker compose up -d
|
||||||
|
sleep 5
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Letzte Logs anzeigen
|
||||||
|
echo "📋 Letzte 50 Zeilen der Container-Logs:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker logs --tail=50 hoerdle 2>&1 | tail -50
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Suche nach Fehlern in den Logs
|
||||||
|
echo "🚨 Fehler in den Logs (letzte 100 Zeilen):"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker logs --tail=100 hoerdle 2>&1 | grep -i -E "error|exception|failed|fatal|panic" || echo "Keine offensichtlichen Fehler gefunden"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Container Health Status
|
||||||
|
echo "💚 Health Check Status:"
|
||||||
|
docker inspect hoerdle --format='{{json .State.Health}}' | python3 -m json.tool 2>/dev/null || docker inspect hoerdle --format='{{.State.Status}}'
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob der Server auf Port 3000 antwortet (intern)
|
||||||
|
echo "🔌 Port-Verbindungstest (intern, Port 3000):"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker exec hoerdle curl -f -s -o /dev/null -w "HTTP Status: %{http_code}\n" http://localhost:3000/api/daily 2>&1 || echo "❌ Verbindung fehlgeschlagen"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Datenbank
|
||||||
|
echo "💾 Datenbank-Status:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
if docker exec hoerdle test -f /app/data/prod.db; then
|
||||||
|
echo "✅ Datenbankdatei existiert"
|
||||||
|
DB_SIZE=$(docker exec hoerdle stat -c%s /app/data/prod.db 2>/dev/null || echo "unbekannt")
|
||||||
|
echo " Größe: $DB_SIZE Bytes"
|
||||||
|
else
|
||||||
|
echo "❌ Datenbankdatei nicht gefunden!"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe Umgebungsvariablen (wichtige)
|
||||||
|
echo "🔐 Wichtige Umgebungsvariablen:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker exec hoerdle env | grep -E "DATABASE_URL|NODE_ENV|PORT|HOSTNAME" || echo "Keine gefunden"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Next.js Server läuft
|
||||||
|
echo "🌐 Next.js Prozess-Status:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker exec hoerdle ps aux | grep -E "node|next" | grep -v grep || echo "Keine Next.js Prozesse gefunden"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Netzwerk-Verbindung prüfen
|
||||||
|
echo "🌐 Netzwerk-Verbindungen:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker network inspect hoerdle_default --format='{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}' 2>/dev/null || echo "Netzwerk nicht gefunden"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Caddy Status (falls vorhanden)
|
||||||
|
if docker ps | grep -q "hoerdle-caddy"; then
|
||||||
|
echo "🚪 Caddy-Container Status:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
docker logs --tail=20 hoerdle-caddy 2>&1 | tail -20
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=================================="
|
||||||
|
echo "✅ Debug-Informationen gesammelt"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Nächste Schritte:"
|
||||||
|
echo "1. Prüfe die Fehler-Logs oben"
|
||||||
|
echo "2. Prüfe ob die Datenbank erreichbar ist"
|
||||||
|
echo "3. Prüfe ob alle Umgebungsvariablen gesetzt sind"
|
||||||
|
echo "4. Bei weiteren Problemen: docker logs hoerdle --tail=200"
|
||||||
|
|
||||||
@@ -54,6 +54,19 @@ git pull
|
|||||||
echo "🏷️ Fetching git tags..."
|
echo "🏷️ Fetching git tags..."
|
||||||
git fetch --tags
|
git fetch --tags
|
||||||
|
|
||||||
|
# Prüfe und erstelle/repariere Netzwerk falls nötig
|
||||||
|
echo "🌐 Prüfe Docker-Netzwerk..."
|
||||||
|
if ! docker network ls | grep -q "hoerdle_default"; then
|
||||||
|
echo " Netzwerk existiert nicht, erstelle es..."
|
||||||
|
docker network create hoerdle_default
|
||||||
|
echo "✅ Netzwerk erstellt"
|
||||||
|
else
|
||||||
|
# Prüfe ob Netzwerk falsche Labels hat (wird durch external: true umgangen)
|
||||||
|
echo "✅ Netzwerk existiert bereits"
|
||||||
|
echo " (Hinweis: Falls Warnungen über falsche Labels erscheinen, verwende: ./scripts/fix-network.sh)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Build new image in background (doesn't stop running container)
|
# Build new image in background (doesn't stop running container)
|
||||||
echo "🔨 Building new Docker image (this runs while app is still online)..."
|
echo "🔨 Building new Docker image (this runs while app is still online)..."
|
||||||
docker compose build
|
docker compose build
|
||||||
|
|||||||
71
scripts/docker-cleanup.sh
Executable file
71
scripts/docker-cleanup.sh
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Docker Cleanup-Skript
|
||||||
|
# Räumt nicht verwendete Docker-Images, Container, Volumes und Build-Cache auf
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🧹 Docker Cleanup..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Zeige aktuellen Speicherverbrauch
|
||||||
|
echo "📊 Aktueller Docker-Speicherverbrauch:"
|
||||||
|
docker system df
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Frage nach Bestätigung (falls interaktiv)
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
echo "⚠️ Dies wird folgende Ressourcen entfernen:"
|
||||||
|
echo " - Alle nicht verwendeten Images"
|
||||||
|
echo " - Alle gestoppten Container"
|
||||||
|
echo " - Alle nicht verwendeten Netzwerke"
|
||||||
|
echo " - Build-Cache"
|
||||||
|
echo ""
|
||||||
|
echo "Möchtest du fortfahren? (j/n)"
|
||||||
|
read -r response
|
||||||
|
if [ "$response" != "j" ] && [ "$response" != "J" ]; then
|
||||||
|
echo "❌ Abgebrochen."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Entferne gestoppte Container
|
||||||
|
echo "🗑️ Entferne gestoppte Container..."
|
||||||
|
STOPPED_CONTAINERS=$(docker ps -a -q -f status=exited | wc -l)
|
||||||
|
if [ "$STOPPED_CONTAINERS" -gt 0 ]; then
|
||||||
|
docker container prune -f
|
||||||
|
echo "✅ Gestoppte Container entfernt"
|
||||||
|
else
|
||||||
|
echo " Keine gestoppten Container gefunden"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Entferne nicht verwendete Images
|
||||||
|
echo "🗑️ Entferne nicht verwendete Images..."
|
||||||
|
docker image prune -a -f
|
||||||
|
echo "✅ Nicht verwendete Images entfernt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Entferne nicht verwendete Netzwerke
|
||||||
|
echo "🗑️ Entferne nicht verwendete Netzwerke..."
|
||||||
|
docker network prune -f
|
||||||
|
echo "✅ Nicht verwendete Netzwerke entfernt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. Entferne Build-Cache (optional, kann lange dauern)
|
||||||
|
echo "🗑️ Entferne Build-Cache..."
|
||||||
|
docker builder prune -f
|
||||||
|
echo "✅ Build-Cache entfernt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Zeige neuen Speicherverbrauch
|
||||||
|
echo "📊 Neuer Docker-Speicherverbrauch:"
|
||||||
|
docker system df
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Zeige verfügbaren Speicherplatz
|
||||||
|
echo "💾 Verfügbarer Speicherplatz:"
|
||||||
|
df -h / | tail -1 | awk '{print " Gesamt: " $2 ", Verfügbar: " $4 ", Belegt: " $5}'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "✅ Cleanup abgeschlossen!"
|
||||||
|
|
||||||
150
scripts/fix-i18n-local.sh
Executable file
150
scripts/fix-i18n-local.sh
Executable file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Fix für i18n-Daten: Kopiert DB lokal, fixt sie, kopiert zurück
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Fixe i18n-Daten (lokal kopieren, fixen, zurück kopieren)..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Container läuft
|
||||||
|
if ! docker ps | grep -q hoerdle; then
|
||||||
|
echo "❌ Container 'hoerdle' läuft nicht!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup erstellen
|
||||||
|
BACKUP_FILE="./data/prod.db.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
echo "💾 Erstelle Backup..."
|
||||||
|
docker cp hoerdle:/app/data/prod.db "$BACKUP_FILE"
|
||||||
|
# Setze Berechtigungen (kann root gehören)
|
||||||
|
sudo chmod 666 "$BACKUP_FILE" 2>/dev/null || chmod 666 "$BACKUP_FILE" 2>/dev/null || true
|
||||||
|
echo "✅ Backup erstellt: $BACKUP_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Kopiere DB lokal
|
||||||
|
echo "📥 Kopiere Datenbank lokal..."
|
||||||
|
docker cp hoerdle:/app/data/prod.db ./data/prod.db.tmp
|
||||||
|
# Setze Berechtigungen (Datei kann root gehören)
|
||||||
|
sudo chmod 666 ./data/prod.db.tmp 2>/dev/null || chmod 666 ./data/prod.db.tmp 2>/dev/null || {
|
||||||
|
echo "⚠️ Konnte Berechtigungen nicht setzen. Versuche mit sudo..."
|
||||||
|
sudo chmod 666 ./data/prod.db.tmp
|
||||||
|
}
|
||||||
|
chmod 775 ./data 2>/dev/null || sudo chmod 775 ./data 2>/dev/null || true
|
||||||
|
echo "✅ Datenbank kopiert"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob sqlite3 verfügbar ist
|
||||||
|
if ! command -v sqlite3 &> /dev/null; then
|
||||||
|
echo "❌ sqlite3 ist nicht installiert!"
|
||||||
|
echo " Installiere es mit: sudo apt-get install sqlite3"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fixe die Datenbank
|
||||||
|
echo "🔧 Fixe i18n-Daten..."
|
||||||
|
# Stelle sicher, dass wir Schreibrechte haben (auch für WAL-Dateien)
|
||||||
|
chmod 666 ./data/prod.db.tmp 2>/dev/null || sudo chmod 666 ./data/prod.db.tmp
|
||||||
|
chmod 775 ./data 2>/dev/null || sudo chmod 775 ./data
|
||||||
|
# Prüfe ob wir die Datei lesen können
|
||||||
|
if [ ! -r "./data/prod.db.tmp" ]; then
|
||||||
|
echo "❌ Kann Datenbankdatei nicht lesen. Setze Besitzer..."
|
||||||
|
sudo chown $(whoami):$(whoami) ./data/prod.db.tmp || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Führe SQL-Befehle aus (mit sudo falls nötig)
|
||||||
|
if [ -r "./data/prod.db.tmp" ] && [ -w "./data" ]; then
|
||||||
|
sqlite3 ./data/prod.db.tmp << 'SQL'
|
||||||
|
-- Fix Genre.name
|
||||||
|
UPDATE Genre
|
||||||
|
SET name = json_object('de', name, 'en', name)
|
||||||
|
WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Genre.subtitle
|
||||||
|
UPDATE Genre
|
||||||
|
SET subtitle = json_object('de', subtitle, 'en', subtitle)
|
||||||
|
WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Special.name
|
||||||
|
UPDATE Special
|
||||||
|
SET name = json_object('de', name, 'en', name)
|
||||||
|
WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Special.subtitle
|
||||||
|
UPDATE Special
|
||||||
|
SET subtitle = json_object('de', subtitle, 'en', subtitle)
|
||||||
|
WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix News.title
|
||||||
|
UPDATE News
|
||||||
|
SET title = json_object('de', title, 'en', title)
|
||||||
|
WHERE typeof(title) = 'text' AND title NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix News.content
|
||||||
|
UPDATE News
|
||||||
|
SET content = json_object('de', content, 'en', content)
|
||||||
|
WHERE typeof(content) = 'text' AND content NOT LIKE '{%';
|
||||||
|
|
||||||
|
SELECT '✅ Alle i18n-Daten wurden gefixt!' as status;
|
||||||
|
SQL
|
||||||
|
else
|
||||||
|
echo "❌ Kann Datenbankdatei nicht lesen oder schreiben!"
|
||||||
|
echo " Versuche mit sudo..."
|
||||||
|
sudo sqlite3 ./data/prod.db.tmp << 'SQL'
|
||||||
|
-- Fix Genre.name
|
||||||
|
UPDATE Genre
|
||||||
|
SET name = json_object('de', name, 'en', name)
|
||||||
|
WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Genre.subtitle
|
||||||
|
UPDATE Genre
|
||||||
|
SET subtitle = json_object('de', subtitle, 'en', subtitle)
|
||||||
|
WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Special.name
|
||||||
|
UPDATE Special
|
||||||
|
SET name = json_object('de', name, 'en', name)
|
||||||
|
WHERE typeof(name) = 'text' AND name NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix Special.subtitle
|
||||||
|
UPDATE Special
|
||||||
|
SET subtitle = json_object('de', subtitle, 'en', subtitle)
|
||||||
|
WHERE subtitle IS NOT NULL AND typeof(subtitle) = 'text' AND subtitle NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix News.title
|
||||||
|
UPDATE News
|
||||||
|
SET title = json_object('de', title, 'en', title)
|
||||||
|
WHERE typeof(title) = 'text' AND title NOT LIKE '{%';
|
||||||
|
|
||||||
|
-- Fix News.content
|
||||||
|
UPDATE News
|
||||||
|
SET content = json_object('de', content, 'en', content)
|
||||||
|
WHERE typeof(content) = 'text' AND content NOT LIKE '{%';
|
||||||
|
|
||||||
|
SELECT '✅ Alle i18n-Daten wurden gefixt!' as status;
|
||||||
|
SQL
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "❌ Fehler beim Fixen der Datenbank!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Datenbank gefixt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Kopiere zurück
|
||||||
|
echo "📤 Kopiere gefixte Datenbank zurück..."
|
||||||
|
docker cp ./data/prod.db.tmp hoerdle:/app/data/prod.db
|
||||||
|
echo "✅ Datenbank zurück kopiert"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Aufräumen
|
||||||
|
rm -f ./data/prod.db.tmp ./data/prod.db.tmp.backup
|
||||||
|
|
||||||
|
echo "🔄 Starte Container neu..."
|
||||||
|
docker compose restart hoerdle
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "✅ Fertig! Prüfe die Logs:"
|
||||||
|
echo " docker logs hoerdle --tail=50"
|
||||||
|
|
||||||
51
scripts/fix-network.sh
Executable file
51
scripts/fix-network.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script zum Reparieren des Docker-Netzwerks hoerdle_default
|
||||||
|
# Dieses Script behebt die Warnung über falsche Netzwerk-Labels
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Repariere Docker-Netzwerk hoerdle_default..."
|
||||||
|
|
||||||
|
# Prüfe, ob Container laufen
|
||||||
|
RUNNING_CONTAINERS=$(docker ps --filter "network=hoerdle_default" --format "{{.Names}}" | wc -l)
|
||||||
|
|
||||||
|
if [ "$RUNNING_CONTAINERS" -gt 0 ]; then
|
||||||
|
echo "⚠️ Warnung: Es laufen noch Container, die das Netzwerk nutzen."
|
||||||
|
echo "📋 Container, die betroffen sind:"
|
||||||
|
docker ps --filter "network=hoerdle_default" --format " - {{.Names}}"
|
||||||
|
echo ""
|
||||||
|
echo "Möchtest du fortfahren? Die Container müssen neu gestartet werden. (j/n)"
|
||||||
|
read -r response
|
||||||
|
if [ "$response" != "j" ] && [ "$response" != "J" ]; then
|
||||||
|
echo "❌ Abgebrochen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🛑 Stoppe Container..."
|
||||||
|
docker compose down || true
|
||||||
|
if [ -f "docker-compose.caddy.yml" ]; then
|
||||||
|
docker compose -f docker-compose.caddy.yml down || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prüfe, ob Netzwerk existiert
|
||||||
|
if docker network ls | grep -q "hoerdle_default"; then
|
||||||
|
echo "🗑️ Lösche altes Netzwerk..."
|
||||||
|
docker network rm hoerdle_default || {
|
||||||
|
echo "❌ Netzwerk konnte nicht gelöscht werden. Möglicherweise sind noch Container verbunden."
|
||||||
|
echo " Versuche, alle Container zu trennen..."
|
||||||
|
docker network disconnect hoerdle_default $(docker ps -q --filter "network=hoerdle_default") 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
docker network rm hoerdle_default || {
|
||||||
|
echo "❌ Netzwerk konnte immer noch nicht gelöscht werden."
|
||||||
|
echo " Bitte manuell prüfen: docker network inspect hoerdle_default"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✨ Netzwerk erfolgreich gelöscht."
|
||||||
|
echo "📝 Das Netzwerk wird beim nächsten 'docker compose up' automatisch neu erstellt."
|
||||||
|
echo ""
|
||||||
|
echo "✅ Fertig! Du kannst jetzt 'docker compose up -d' ausführen."
|
||||||
|
|
||||||
110
scripts/quick-check-punycode-dns.sh
Executable file
110
scripts/quick-check-punycode-dns.sh
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick-Check für Punycode-Domain DNS und Zertifikat
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔍 Quick-Check für hördle.de (xn--hrdle-jua.de)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe DNS
|
||||||
|
echo "1️⃣ DNS-Auflösung:"
|
||||||
|
echo ""
|
||||||
|
echo " hoerdle.de:"
|
||||||
|
HOERDLE_IP=$(dig +short hoerdle.de @8.8.8.8 | head -1 || echo "")
|
||||||
|
if [ -z "$HOERDLE_IP" ]; then
|
||||||
|
echo " ❌ Konnte nicht aufgelöst werden"
|
||||||
|
else
|
||||||
|
echo " ✅ IP: $HOERDLE_IP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " xn--hrdle-jua.de:"
|
||||||
|
PUNYCODE_IP=$(dig +short xn--hrdle-jua.de @8.8.8.8 | head -1 || echo "")
|
||||||
|
if [ -z "$PUNYCODE_IP" ]; then
|
||||||
|
echo " ❌ Konnte nicht aufgelöst werden"
|
||||||
|
else
|
||||||
|
echo " ✅ IP: $PUNYCODE_IP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$HOERDLE_IP" ] && [ -n "$PUNYCODE_IP" ]; then
|
||||||
|
if [ "$HOERDLE_IP" = "$PUNYCODE_IP" ]; then
|
||||||
|
echo ""
|
||||||
|
echo " ✅ Beide Domains zeigen auf die gleiche IP ($HOERDLE_IP)"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo " ⚠️ WARNUNG: Domains zeigen auf unterschiedliche IPs!"
|
||||||
|
echo " hoerdle.de: $HOERDLE_IP"
|
||||||
|
echo " xn--hrdle-jua.de: $PUNYCODE_IP"
|
||||||
|
echo " → Beide sollten auf die gleiche IP zeigen!"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2️⃣ HTTPS-Verbindungstest:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test hoerdle.de
|
||||||
|
echo " hoerdle.de:"
|
||||||
|
if timeout 5 bash -c "echo > /dev/tcp/hoerdle.de/443" 2>/dev/null; then
|
||||||
|
echo " ✅ Port 443 ist erreichbar"
|
||||||
|
if echo | timeout 5 openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | grep -q "Verify return code: 0"; then
|
||||||
|
CERT_VALID_UNTIL=$(echo | timeout 5 openssl s_client -connect hoerdle.de:443 -servername hoerdle.de 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "")
|
||||||
|
echo " ✅ Zertifikat gültig bis: $CERT_VALID_UNTIL"
|
||||||
|
else
|
||||||
|
echo " ❌ Zertifikat ungültig oder fehlt"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ❌ Port 443 nicht erreichbar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " xn--hrdle-jua.de:"
|
||||||
|
if timeout 5 bash -c "echo > /dev/tcp/xn--hrdle-jua.de/443" 2>/dev/null; then
|
||||||
|
echo " ✅ Port 443 ist erreichbar"
|
||||||
|
if echo | timeout 5 openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | grep -q "Verify return code: 0"; then
|
||||||
|
CERT_VALID_UNTIL=$(echo | timeout 5 openssl s_client -connect xn--hrdle-jua.de:443 -servername xn--hrdle-jua.de 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "")
|
||||||
|
echo " ✅ Zertifikat gültig bis: $CERT_VALID_UNTIL"
|
||||||
|
else
|
||||||
|
echo " ❌ Zertifikat ungültig oder fehlt (ERR_SSL_PROTOCOL_ERROR)"
|
||||||
|
echo ""
|
||||||
|
echo " 💡 Lösung: Führe aus: ./scripts/renew-caddy-certificates.sh"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ❌ Port 443 nicht erreichbar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3️⃣ Caddy-Container Status:"
|
||||||
|
echo ""
|
||||||
|
if docker ps | grep -q hoerdle-caddy; then
|
||||||
|
echo " ✅ Caddy-Container läuft"
|
||||||
|
echo ""
|
||||||
|
echo " 📋 Letzte Caddy-Logs (Zertifikat-bezogen):"
|
||||||
|
docker logs hoerdle-caddy 2>&1 | grep -i -E "(xn--hrdle|punycode|certificate|tls|acme|challenge)" | tail -5 || echo " (Keine relevanten Einträge gefunden)"
|
||||||
|
else
|
||||||
|
echo " ❌ Caddy-Container läuft nicht"
|
||||||
|
echo " Starte mit: docker compose -f docker-compose.caddy.yml --profile production up -d"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Nächste Schritte:"
|
||||||
|
echo ""
|
||||||
|
if [ -n "$HOERDLE_IP" ] && [ -n "$PUNYCODE_IP" ] && [ "$HOERDLE_IP" != "$PUNYCODE_IP" ]; then
|
||||||
|
echo " 1. ❗ DNS-Konfiguration prüfen!"
|
||||||
|
echo " Beide Domains müssen auf die gleiche IP zeigen."
|
||||||
|
echo " Prüfe in GoDaddy: https://dcc.godaddy.com/manage/hoerdle.de/dns"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " 2. Wenn DNS korrekt ist, Zertifikat neu erstellen:"
|
||||||
|
echo " ./scripts/renew-caddy-certificates.sh"
|
||||||
|
echo ""
|
||||||
|
echo " 3. Caddy-Logs überwachen:"
|
||||||
|
echo " docker logs hoerdle-caddy -f"
|
||||||
|
echo ""
|
||||||
|
echo " 4. Detaillierte Diagnose:"
|
||||||
|
echo " ./scripts/check-caddy-certificates.sh"
|
||||||
|
echo ""
|
||||||
|
|
||||||
47
scripts/quick-fix-db.sh
Executable file
47
scripts/quick-fix-db.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick-Fix für Datenbank-Berechtigungen
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Quick-Fix für Datenbank-Berechtigungen..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
|
# Setze Berechtigungen
|
||||||
|
echo "1️⃣ Setze Berechtigungen..."
|
||||||
|
chmod 775 ./data
|
||||||
|
chmod 664 ./data/prod.db 2>/dev/null || echo " ⚠️ Datenbankdatei nicht gefunden"
|
||||||
|
|
||||||
|
# Setze Besitzer auf root (Container läuft als root)
|
||||||
|
echo "2️⃣ Setze Besitzer auf root..."
|
||||||
|
sudo chown -R root:root ./data
|
||||||
|
|
||||||
|
# Zeige aktuelle Berechtigungen
|
||||||
|
echo ""
|
||||||
|
echo "✅ Berechtigungen gesetzt!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Aktuelle Berechtigungen:"
|
||||||
|
ls -ld ./data
|
||||||
|
ls -lh ./data/prod.db 2>/dev/null || echo " (Datei nicht gefunden)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Teste im Container
|
||||||
|
echo "3️⃣ Teste Zugriff im Container..."
|
||||||
|
if docker ps | grep -q hoerdle; then
|
||||||
|
echo " Container läuft, teste Zugriff..."
|
||||||
|
docker exec hoerdle sh -c "test -r /app/data/prod.db && echo '✅ Lesbar' || echo '❌ Nicht lesbar'"
|
||||||
|
docker exec hoerdle sh -c "test -w /app/data/prod.db && echo '✅ Schreibbar' || echo '❌ Nicht schreibbar'"
|
||||||
|
docker exec hoerdle sh -c "test -w /app/data && echo '✅ Verzeichnis schreibbar' || echo '❌ Verzeichnis nicht schreibbar'"
|
||||||
|
else
|
||||||
|
echo " ⚠️ Container läuft nicht"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Starte Container neu..."
|
||||||
|
docker compose restart hoerdle 2>/dev/null || echo " ⚠️ Konnte Container nicht neustarten (vielleicht läuft docker compose nicht?)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Fertig! Prüfe jetzt die Logs:"
|
||||||
|
echo " docker logs hoerdle --tail=50"
|
||||||
|
|
||||||
83
scripts/renew-caddy-certificates.sh
Executable file
83
scripts/renew-caddy-certificates.sh
Executable file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script zum Erneuern/Löschen und Neu-Erstellen von Caddy-Zertifikaten
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔄 Caddy-Zertifikat-Erneuerung"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ WICHTIG: Dieses Script wird die Zertifikate löschen und Caddy zwingen, sie neu zu erstellen."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Caddy-Container läuft
|
||||||
|
if ! docker ps | grep -q hoerdle-caddy; then
|
||||||
|
echo "❌ Caddy-Container läuft nicht!"
|
||||||
|
echo " Starte ihn mit: docker compose -f docker-compose.caddy.yml --profile production up -d"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📋 Verfügbare Optionen:"
|
||||||
|
echo " 1) Alle Zertifikate löschen und neu erstellen (empfohlen bei Problemen)"
|
||||||
|
echo " 2) Nur Zertifikat für xn--hrdle-jua.de löschen (Punycode-Domain)"
|
||||||
|
echo " 3) Nur Zertifikat für hoerdle.de löschen"
|
||||||
|
echo " 4) Abbrechen"
|
||||||
|
echo ""
|
||||||
|
read -p "Wähle eine Option (1-4): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ Lösche ALLE Zertifikate..."
|
||||||
|
docker exec hoerdle-caddy rm -rf /data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/* 2>/dev/null || true
|
||||||
|
echo "✅ Alle Zertifikate gelöscht"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ Lösche Zertifikat für xn--hrdle-jua.de..."
|
||||||
|
docker exec hoerdle-caddy find /data/caddy/certificates -name "*xn--hrdle-jua.de*" -delete 2>/dev/null || true
|
||||||
|
docker exec hoerdle-caddy find /data/caddy/certificates -name "*xn--*" -delete 2>/dev/null || true
|
||||||
|
echo "✅ Zertifikat für Punycode-Domain gelöscht"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ Lösche Zertifikat für hoerdle.de..."
|
||||||
|
docker exec hoerdle-caddy find /data/caddy/certificates -name "*hoerdle.de*" ! -name "*xn--*" -delete 2>/dev/null || true
|
||||||
|
echo "✅ Zertifikat für hoerdle.de gelöscht"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo "❌ Abgebrochen."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Ungültige Option. Abgebrochen."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Starte Caddy-Container neu, um Zertifikate neu zu erstellen..."
|
||||||
|
docker compose -f docker-compose.caddy.yml --profile production restart caddy
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⏳ Warte 5 Sekunden, damit Caddy startet..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📋 Prüfe Caddy-Logs auf Zertifikats-Erstellung:"
|
||||||
|
echo ""
|
||||||
|
docker logs hoerdle-caddy --tail=30 2>&1 | grep -i -E "(certificate|tls|acme|challenge|error|success)" || echo " (Keine relevanten Log-Einträge in den letzten 30 Zeilen)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Container wurde neu gestartet."
|
||||||
|
echo ""
|
||||||
|
echo "💡 Nächste Schritte:"
|
||||||
|
echo " 1. Warte 1-2 Minuten, damit Caddy die Zertifikate erstellt"
|
||||||
|
echo " 2. Prüfe die Logs: docker logs hoerdle-caddy -f"
|
||||||
|
echo " 3. Prüfe den Status: ./scripts/check-caddy-certificates.sh"
|
||||||
|
echo " 4. Teste die Domain: curl -I https://xn--hrdle-jua.de"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Falls das Zertifikat nicht erstellt wird:"
|
||||||
|
echo " - Prüfe DNS-Einträge: Beide Domains müssen auf die Server-IP zeigen"
|
||||||
|
echo " - Prüfe Port 80: Muss von außen erreichbar sein (für HTTP-01 Challenge)"
|
||||||
|
echo " - Prüfe Firewall: Ports 80 und 443 müssen offen sein"
|
||||||
|
echo " - Prüfe Caddy-Logs: docker logs hoerdle-caddy"
|
||||||
|
|
||||||
Reference in New Issue
Block a user