diff --git a/.gitignore b/.gitignore
index 204a542..b537029 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,8 +11,12 @@ data/site-settings.json
data/smtp-settings.json
storage/uploads/images/*
storage/config/*
+storage/content/*
!storage/.gitkeep
!storage/config/.gitkeep
+!storage/content/.gitkeep
+!storage/content/backups/
+!storage/content/backups/.gitkeep
!storage/uploads/.gitkeep
!storage/uploads/images/.gitkeep
.DS_Store
diff --git a/Dockerfile b/Dockerfile
index c0bb863..94b7f82 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,7 +14,7 @@ FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
-ENV NEXT_PUBLIC_APP_VERSION=0.2.2
+ENV NEXT_PUBLIC_APP_VERSION=0.4.0
ENV DOCKER_ENV=true
ENV PORT=3010
@@ -26,7 +26,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/scripts/runtime-start.js ./runtime-start.js
-RUN mkdir -p /app/data /app/storage/config /app/storage/uploads/images /app/.next/cache \
+RUN mkdir -p /app/data /app/storage/config /app/storage/content/backups /app/storage/uploads/images /app/.next/cache \
&& chown -R nextjs:nodejs /app/data /app/storage /app/.next/cache
USER nextjs
diff --git a/README.md b/README.md
index 4c9a96d..591218f 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,12 @@ Eigenständige öffentliche Firmenwebsite für Funktechnik Schubert. Dieses Proj
- TypeScript strict
- Serverseitige API-Routen für Kontakt und Reparaturannahme
- Geschützter Admin-Bereich unter `/admin`
-- Lokale Foundation-Datenablage für Anfragen und Einstellungen
+- Lokale Storage-Datenablage für Anfragen, Einstellungen, Medien und Website-Inhalte
- Docker
- Nginx/Reverse-Proxy-fähig
- SEO Metadata, Sitemap und robots.txt
-Version: `0.3.0`
+Version: `0.4.0`
## Seiten
@@ -125,6 +125,9 @@ Für lokale Entwicklung kann `AUTH_COOKIE_SECURE=false` bleiben. Produktiv muss
- `POST /api/admin/settings`
- `POST /api/admin/smtp`
- `GET/POST /api/admin/media`
+- `GET/PUT /api/admin/content`
+- `GET /api/content/settings`
+- `GET /api/media/[filename]`
- `GET /api/health`
Aktuell validieren die Routen serverseitig, geben klare JSON-Antworten zurück und schreiben nur technische Metadaten in Server-Logs. Es werden keine Nachrichteninhalte oder Tokens geloggt. Kontakt- und Reparaturanfragen werden lokal unter `data/*.json` gespeichert und nicht versioniert.
@@ -138,14 +141,14 @@ Funktionen:
- Dashboard mit Kennzahlen
- Kontaktanfragen verwalten
- Reparaturanfragen verwalten
-- Website-Inhaltsverwaltung vorbereitet
+- Website-Inhalte pflegen und sofort veröffentlichen
- Firmendaten pflegen
- SMTP-Konfiguration speichern und Testmail senden
- Medien hochladen
- SEO-Übersicht
- Systemübersicht
-Publishing von Website-Inhalten und Olympus-Übernahme sind bewusst noch nicht aktiv gekoppelt. SMTP-Versand fuer Kontakt- und Reparaturanfragen ist serverseitig angebunden, sofern `/admin/smtp` vollstaendig konfiguriert ist.
+Website-Inhalte werden unter `/admin/website` gepflegt und beim Speichern sofort auf der öffentlichen Website wirksam. Die Content-Architektur ist bewusst über einen zentralen `ContentService` gekapselt, damit später Olympus CMS oder PostgreSQL als Backend angebunden werden können. SMTP-Versand fuer Kontakt- und Reparaturanfragen ist serverseitig angebunden, sofern `/admin/smtp` vollstaendig konfiguriert ist.
## Runtime Data
@@ -171,6 +174,32 @@ Uploads liegen unter `storage/uploads/images/`, werden über `/api/media/[filena
Alte Uploads aus `public/uploads/images` werden beim Start einmalig nach `storage/uploads/images` migriert, falls sie dort noch nicht vorhanden sind.
+Website-Inhalte liegen unter `storage/content/` und werden nicht committed:
+
+```text
+storage/content/home.json
+storage/content/services.json
+storage/content/radio-service.json
+storage/content/about.json
+storage/content/contact.json
+storage/content/settings.json
+storage/content/backups/
+```
+
+Jede Seite besitzt eine eigene JSON-Datei. React-Komponenten lesen diese Dateien nicht direkt, sondern ausschließlich über `lib/content/service.ts`. Vor jedem Speichern erzeugt der Service eine Backup-Datei unter `storage/content/backups/` und behält pro Dokument die letzten 20 Versionen. Änderungen benötigen keinen Neustart und keinen Docker-Build.
+
+Bearbeitbar sind aktuell:
+
+- Startseite mit Hero, Call-to-Actions, Featureboxen, Leistungsboxen und Kundenversprechen
+- Leistungen mit beliebig vielen sortierbaren und ein-/ausblendbaren Einträgen
+- Funkgeräte-Service mit Einleitung, Marken, Fehlerbildern, Ablauf, Messmöglichkeiten, Abgleich und Reparaturtexten
+- Über Uns mit Firmenbeschreibung, Werkstattbeschreibung und Philosophie
+- Kontakt mit Kontaktinformationen, Öffnungszeiten, Maps-Link und Seitentexten
+- Footer, Logo, Copyright, Header-CTA und globale SEO-Einstellungen
+- SEO pro Seite inklusive Meta Title, Meta Description, Keywords, OpenGraph und Social Image
+
+Bilder für Logo und Social Image werden aus dem Medienbestand ausgewählt. Neue Uploads bleiben im privaten Storage und werden über `/api/media/[filename]` ausgeliefert.
+
## Healthcheck
```bash
@@ -188,7 +217,7 @@ Antwort:
```json
{
"status": "ok",
- "version": "0.3.0",
+ "version": "0.4.0",
"storage": "ok",
"admin": "configured",
"smtp": "configured",
diff --git a/app/admin/system/page.tsx b/app/admin/system/page.tsx
index c6f59ff..35d1cea 100644
--- a/app/admin/system/page.tsx
+++ b/app/admin/system/page.tsx
@@ -3,6 +3,7 @@ import AdminShell from "@/components/admin/AdminShell";
import { requireAdminSession } from "@/lib/admin/auth";
import { adminConfigurationStatus, appVersion, checkStorage, configDirectory, dataDirectory, isDockerEnvironment, olympusStatus, uploadDirectory } from "@/lib/runtime/config";
import { getSmtpSettings, isSmtpConfigured } from "@/lib/mail/config";
+import { getContentSystemStatus } from "@/lib/content/service";
export default async function AdminSystemPage() {
if (!await requireAdminSession()) redirect("/admin/login");
@@ -13,7 +14,7 @@ export default async function AdminSystemPage() {
} catch {
storage = "error";
}
- const smtpSettings = await getSmtpSettings();
+ const [smtpSettings, contentStatus] = await Promise.all([getSmtpSettings(), getContentSystemStatus()]);
const smtpConfigured = isSmtpConfigured(smtpSettings);
return (
@@ -37,6 +38,10 @@ export default async function AdminSystemPage() {
-
+
);
}
diff --git a/app/api/admin/content/route.ts b/app/api/admin/content/route.ts
new file mode 100644
index 0000000..dc2c9b8
--- /dev/null
+++ b/app/api/admin/content/route.ts
@@ -0,0 +1,27 @@
+import { NextResponse } from "next/server";
+import { requireAdminSession } from "@/lib/admin/auth";
+import { getAllContent, saveContent } from "@/lib/content/service";
+import type { ContentDocuments, ContentKey } from "@/lib/content/types";
+
+const keys: ContentKey[] = ["home", "services", "radio-service", "about", "contact", "settings"];
+
+function isContentKey(value: unknown): value is ContentKey {
+ return typeof value === "string" && keys.includes(value as ContentKey);
+}
+
+export async function GET() {
+ if (!await requireAdminSession()) return NextResponse.json({ message: "Nicht autorisiert." }, { status: 401 });
+ return NextResponse.json({ content: await getAllContent() });
+}
+
+export async function PUT(request: Request) {
+ if (!await requireAdminSession()) return NextResponse.json({ message: "Nicht autorisiert." }, { status: 401 });
+
+ const body = await request.json() as { key?: unknown; content?: unknown };
+ if (!isContentKey(body.key) || !body.content) {
+ return NextResponse.json({ message: "Ungültige Content-Anfrage." }, { status: 400 });
+ }
+
+ const saved = await saveContent(body.key, body.content as ContentDocuments[typeof body.key]);
+ return NextResponse.json({ message: "Inhalt gespeichert.", key: body.key, content: saved });
+}
diff --git a/app/api/content/settings/route.ts b/app/api/content/settings/route.ts
new file mode 100644
index 0000000..9da8ad9
--- /dev/null
+++ b/app/api/content/settings/route.ts
@@ -0,0 +1,7 @@
+import { NextResponse } from "next/server";
+import { getContent } from "@/lib/content/service";
+
+export async function GET() {
+ const settings = await getContent("settings");
+ return NextResponse.json({ settings });
+}
diff --git a/app/datenschutz/page.tsx b/app/datenschutz/page.tsx
index 265ff71..31e2d04 100644
--- a/app/datenschutz/page.tsx
+++ b/app/datenschutz/page.tsx
@@ -10,15 +10,15 @@ export default function DatenschutzPage() {
Informationen zur Verarbeitung personenbezogener Daten.
-
TODO: Rechtliche Angaben ergänzen
+
Datenschutzhinweise müssen vor produktiver Veröffentlichung durch den Betreiber ergänzt und rechtlich geprüft werden.
Verantwortlicher
-
TODO: Rechtliche Angaben ergänzen
+
Die Angaben zum Verantwortlichen werden durch den Betreiber gepflegt.
Kontakt- und Reparaturanfragen
Die Formulare erfassen Angaben, die zur Bearbeitung der jeweiligen Anfrage erforderlich sind. Eine produktive Datenschutzerklärung muss vor Veröffentlichung rechtlich geprüft und ergänzt werden.
Hosting und Server-Logs
-
TODO: Rechtliche Angaben ergänzen
+
Informationen zum Hosting und zur Verarbeitung technischer Serverdaten werden durch den Betreiber gepflegt.
Betroffenenrechte
-
TODO: Rechtliche Angaben ergänzen
+
Hinweise zu Betroffenenrechten werden durch den Betreiber rechtlich geprüft und gepflegt.
>
diff --git a/app/funkgeraete-service/page.tsx b/app/funkgeraete-service/page.tsx
index ef23bd7..2f58d11 100644
--- a/app/funkgeraete-service/page.tsx
+++ b/app/funkgeraete-service/page.tsx
@@ -1,33 +1,40 @@
-import type { Metadata } from "next";
import Link from "next/link";
import PageHero from "@/components/PageHero";
-import { createMetadata } from "../seo";
+import { getContent } from "@/lib/content/service";
+import { createContentMetadata } from "../seo";
-export const metadata: Metadata = createMetadata("Funkgeräte-Service", "Werkstattservice für CB-Funk, Amateurfunk und Funkgeräte-Abgleich.", "/funkgeraete-service");
+export const dynamic = "force-dynamic";
+
+export async function generateMetadata() {
+ const content = await getContent("radio-service");
+ return createContentMetadata(content.seo);
+}
+
+export default async function RadioServicePage() {
+ const content = await getContent("radio-service");
-export default function RadioServicePage() {
return (
<>
-
- Funkgeräte zeigen Fehler oft erst im Zusammenspiel von Empfang, Sendeteil, Versorgung, Antennenanpassung, Bedienung und Abgleich. Der Service betrachtet diese Zusammenhänge strukturiert.
+
+ {content.subtitle}
-
Typische Fehlerbilder
+
{content.listTitle}
-
Kein oder schwacher Empfang
-
Keine, leise oder verzerrte Modulation
-
Frequenzversatz, PLL-Rasten oder instabile Kanäle
-
Sendeprobleme, schwankende Leistung oder auffällige Erwärmung
-
Audio-/NF-Probleme, Displayfehler oder Aussetzer durch Bedienelemente
-
Unklare Vorarbeiten, fehlende Serviceunterlagen oder bereits geöffnete Geräte
+ {content.symptoms.map((symptom) =>
{symptom}
)}
-
Messung und Einordnung
-
Eine gute Reparatur beginnt mit vollständigen Angaben zu Gerät, Fehlerbild und Vorgeschichte. Danach folgen technische Einschätzung, Prüfung mit geeigneter Messtechnik und die weitere Abstimmung.
>
diff --git a/app/page.tsx b/app/page.tsx
index 859a1a6..4d43644 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,26 +1,32 @@
import Link from "next/link";
import ServiceCard from "@/components/ServiceCard";
+import { getContent } from "@/lib/content/service";
+import { createContentMetadata } from "./seo";
+
+export const dynamic = "force-dynamic";
+
+export async function generateMetadata() {
+ const content = await getContent("home");
+ return createContentMetadata(content.seo);
+}
+
+export default async function HomePage() {
+ const content = await getContent("home");
-export default function HomePage() {
return (
<>
-
Funktechnik, Messtechnik, Werkstattservice
-
Funktechnik Schubert
-
Service, Reparatur und Diagnose für Funkgeräte, Messtechnik und Kommunikationstechnik.
-
Von CB-Funk und Amateurfunk bis zur professionellen Funktechnik: Wir unterstützen bei Fehlersuche, Abgleich, Modulationsproblemen, Frequenzproblemen und technischer Dokumentation.
@@ -29,13 +35,11 @@ export default function HomePage() {
Leistungsbereiche
-
Werkstattservice für Funktechnik und Messtechnik
-
Von der ersten Fehlerbeschreibung bis zur dokumentierten Prüfung: Funktechnik Schubert verbindet technische Erfahrung, geeignete Messmittel und strukturierte Kommunikation.
+
{content.cta.text}
+
{content.intro}
- Sichtprüfung, Funktionsprüfung und technische Beurteilung für CB-Funk, Amateurfunk, Betriebsfunk sowie ältere und moderne Geräte.
- Systematische Diagnose bei Empfangsproblemen, Sendeproblemen, PLL-Fehlern, Frequenzabweichungen und Audio-/NF-Störungen.
- Frequenzabgleich, Hub- und Modulationsprüfung, Sendeleistung, Empfindlichkeit und RF-Messung mit geeigneter Messtechnik.
+ {content.features.map((feature) => {feature.text})}
@@ -43,20 +47,17 @@ export default function HomePage() {
-
Persönlicher Support
-
Direkte technische Einschätzung statt anonymer Abwicklung
-
Viele Fehlerbilder lassen sich bereits mit einer klaren Beschreibung und den richtigen Gerätedaten eingrenzen. Die Reparaturannahme fragt deshalb die wichtigsten technischen Details strukturiert ab.