Compare commits

..

9 Commits

Author SHA1 Message Date
Hördle Bot
fd8f4adcc0 Bump version to 0.1.4 2025-12-01 18:51:31 +01:00
Hördle Bot
23997ccc3a Refactor: Plausible-Konfiguration - automatisches Domain-Tracking und NEXT_PUBLIC_PLAUSIBLE_DOMAIN entfernt
- Domain wird automatisch aus Request-Header erkannt (hoerdle.de / hördle.de)
- NEXT_PUBLIC_PLAUSIBLE_DOMAIN komplett entfernt (nicht mehr benötigt)
- CSP in proxy.ts konfigurierbar gemacht
- twitterHandle entfernt (wurde nicht verwendet)
- Dokumentation aktualisiert
2025-12-01 18:51:15 +01:00
Hördle Bot
85bdbf795c Refactor: Plausible-Konfiguration aktualisiert und twitterHandle entfernt
- Defaults auf neue Domains aktualisiert (hoerdle.de statt hoerdle.elpatron.me)
- CSP in proxy.ts konfigurierbar gemacht (liest Plausible-URL aus Umgebungsvariablen)
- twitterHandle entfernt (wurde nirgendwo verwendet)
- Dokumentation aktualisiert
2025-12-01 18:29:09 +01:00
Hördle Bot
ac0bb02ba0 Bump version to 0.1.3 2025-12-01 18:18:17 +01:00
Hördle Bot
63269c2600 Change: Standard-Sprache von Deutsch auf Englisch geändert
- defaultLocale in proxy.ts auf 'en' geändert
- Fallback in i18n/request.ts auf 'en' geändert
- Fallback-Reihenfolge in lib/i18n.ts angepasst (en vor de)
- Share-URL-Logik in Game.tsx angepasst
- Dokumentation aktualisiert
2025-12-01 18:18:11 +01:00
Hördle Bot
17a39d677d Update: package-lock.json aktualisiert für baseline-browser-mapping 2025-12-01 18:15:22 +01:00
Hördle Bot
1ff0787e4e Bump version to 0.1.2 2025-12-01 18:11:39 +01:00
Hördle Bot
ed5f02bdec Fix: 'Kurieren' zu 'Kuratieren' korrigiert im Admin-Dashboard 2025-12-01 18:11:24 +01:00
Hördle Bot
e3a09864a6 Refactor: Dokumentation nach docs/ verschoben
- Alle Markdown-Dateien (außer README.md) nach docs/ verschoben
- Referenzen in README.md aktualisiert
- /docs zu .dockerignore hinzugefügt
2025-12-01 17:58:32 +01:00
24 changed files with 251 additions and 42 deletions

View File

@@ -50,6 +50,7 @@ Dockerfile*
.dockerignore .dockerignore
# Documentation # Documentation
/docs
*.md *.md
!README.md !README.md

View File

@@ -38,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
@@ -46,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
@@ -60,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

View File

@@ -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:

View File

@@ -5,6 +5,7 @@ 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 InstallPrompt from "@/components/InstallPrompt"; import InstallPrompt from "@/components/InstallPrompt";
@@ -52,12 +53,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"
/> />

View File

@@ -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) {

View File

@@ -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}

View File

@@ -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
View 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)

View File

@@ -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

View File

@@ -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);
} }

View File

@@ -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',

View File

@@ -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]];

View File

@@ -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",

4
package-lock.json generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoerdle", "name": "hoerdle",
"version": "0.1.1", "version": "0.1.4",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@@ -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;