feat(cms): add storage based website content management

This commit is contained in:
Schubert Ferenc 2026-07-04 00:58:33 +02:00
parent 7d5689cfa4
commit 12d31f5468
31 changed files with 1347 additions and 158 deletions

View file

@ -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() {
<div><dt>SMTP</dt><dd>{smtpConfigured ? "configured" : "missing"}</dd></div>
<div><dt>Letzte SMTP-Testmail</dt><dd>{smtpSettings.lastTestAt ? `${new Date(smtpSettings.lastTestAt).toLocaleString("de-DE")} (${smtpSettings.lastTestStatus})` : "keine"}</dd></div>
<div><dt>Letzter SMTP-Formularversand</dt><dd>{smtpSettings.lastDeliveryAt ? `${new Date(smtpSettings.lastDeliveryAt).toLocaleString("de-DE")} (${smtpSettings.lastDeliveryStatus})` : "keiner"}</dd></div>
<div><dt>Content Version</dt><dd>{contentStatus.version}</dd></div>
<div><dt>Letzte Content-Änderung</dt><dd>{contentStatus.lastModified ? new Date(contentStatus.lastModified).toLocaleString("de-DE") : "keine"}</dd></div>
<div><dt>Anzahl Seiten</dt><dd>{contentStatus.pageCount}</dd></div>
<div><dt>Anzahl Medien</dt><dd>{contentStatus.mediaCount}</dd></div>
</dl>
</section>
<section className="admin-card">

View file

@ -1,18 +1,13 @@
import { redirect } from "next/navigation";
import AdminShell from "@/components/admin/AdminShell";
import ContentManager from "@/components/admin/ContentManager";
import { requireAdminSession } from "@/lib/admin/auth";
const editablePages = [
["Startseite", "Hero, Vorteile, Textblöcke"],
["Leistungen", "Funktechnik, Messtechnik, Servicebereiche"],
["Funkgeräte-Service", "Diagnose, Abgleich, Fehlerbilder"],
["Reparatur", "Reparaturannahme und Hinweise"],
["Über uns", "Profil und Werkstattbeschreibung"],
["Kontakt", "Kontakttext und Hinweise"],
] as const;
import { listMediaFiles } from "@/lib/admin/media";
import { getAllContent } from "@/lib/content/service";
export default async function AdminWebsitePage() {
if (!await requireAdminSession()) redirect("/admin/login");
const [content, mediaFiles] = await Promise.all([getAllContent(), listMediaFiles()]);
return (
<AdminShell>
@ -20,19 +15,7 @@ export default async function AdminWebsitePage() {
<p className="eyebrow">Website</p>
<h1>Website-Inhalte</h1>
</div>
<section className="admin-card">
<h2>Editierbare Inhalte</h2>
<p className="admin-muted">Diese Foundation bereitet die Inhaltsverwaltung vor. Die öffentliche Website bleibt bis zur finalen Publishing-Funktion weiterhin quellcodebasiert.</p>
<div className="admin-detail-grid">
{editablePages.map(([title, description]) => (
<article key={title} className="admin-detail">
<h3>{title}</h3>
<p>{description}</p>
<button className="button light" type="button" disabled>Bearbeiten vorbereitet</button>
</article>
))}
</div>
</section>
<ContentManager initialContent={content} mediaFiles={mediaFiles} />
</AdminShell>
);
}

View file

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

View file

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

View file

@ -10,15 +10,15 @@ export default function DatenschutzPage() {
<PageHero eyebrow="Rechtliches" title="Datenschutz">Informationen zur Verarbeitung personenbezogener Daten.</PageHero>
<section className="section">
<div className="container legal">
<p className="notice">TODO: Rechtliche Angaben ergänzen</p>
<p className="notice">Datenschutzhinweise müssen vor produktiver Veröffentlichung durch den Betreiber ergänzt und rechtlich geprüft werden.</p>
<h2>Verantwortlicher</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<p>Die Angaben zum Verantwortlichen werden durch den Betreiber gepflegt.</p>
<h2>Kontakt- und Reparaturanfragen</h2>
<p>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.</p>
<h2>Hosting und Server-Logs</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<p>Informationen zum Hosting und zur Verarbeitung technischer Serverdaten werden durch den Betreiber gepflegt.</p>
<h2>Betroffenenrechte</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<p>Hinweise zu Betroffenenrechten werden durch den Betreiber rechtlich geprüft und gepflegt.</p>
</div>
</section>
</>

View file

@ -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 (
<>
<PageHero eyebrow="Funkgeräte-Service" title="Prüfung, Diagnose und Abgleich">
Funkgeräte zeigen Fehler oft erst im Zusammenspiel von Empfang, Sendeteil, Versorgung, Antennenanpassung, Bedienung und Abgleich. Der Service betrachtet diese Zusammenhänge strukturiert.
<PageHero eyebrow={content.eyebrow} title={content.title}>
{content.subtitle}
</PageHero>
<section className="section">
<div className="container split">
<div>
<h2>Typische Fehlerbilder</h2>
<h2>{content.listTitle}</h2>
<ul className="list">
<li>Kein oder schwacher Empfang</li>
<li>Keine, leise oder verzerrte Modulation</li>
<li>Frequenzversatz, PLL-Rasten oder instabile Kanäle</li>
<li>Sendeprobleme, schwankende Leistung oder auffällige Erwärmung</li>
<li>Audio-/NF-Probleme, Displayfehler oder Aussetzer durch Bedienelemente</li>
<li>Unklare Vorarbeiten, fehlende Serviceunterlagen oder bereits geöffnete Geräte</li>
{content.symptoms.map((symptom) => <li key={symptom}>{symptom}</li>)}
</ul>
</div>
<div className="panel">
<h2>Messung und Einordnung</h2>
<p className="lead">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.</p>
<Link className="button" href="/reparatur">Reparatur anfragen</Link>
<h2>{content.cta.text}</h2>
<p className="lead">{content.intro}</p>
<ul className="list">
{content.measurements.map((measurement) => <li key={measurement}>{measurement}</li>)}
</ul>
<p>{content.alignment}</p>
<p>{content.repair}</p>
<Link className="button" href={content.cta.primary.href}>{content.cta.primary.label}</Link>
</div>
</div>
</section>

View file

@ -985,6 +985,171 @@ h3 {
overflow-wrap: anywhere;
}
.content-manager {
display: grid;
gap: 18px;
}
.content-editor-layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.content-tabs {
position: sticky;
top: 18px;
display: grid;
gap: 6px;
}
.content-tabs button,
.content-preview-toolbar button {
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
color: var(--navy);
font-weight: 800;
padding: 10px 12px;
text-align: left;
}
.content-tabs button[aria-current="page"],
.content-preview-toolbar button[aria-current="true"] {
border-color: var(--navy);
background: var(--navy);
color: #fff;
}
.content-editor {
display: grid;
gap: 16px;
min-width: 0;
}
.content-editor-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.content-editor-top h2 {
margin-bottom: 0;
}
.content-editor-heading {
margin: 14px 0 0;
border-top: 1px solid var(--line);
padding-top: 16px;
color: var(--navy);
}
.content-field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.content-field-grid label,
.content-editor label {
display: grid;
gap: 7px;
color: var(--ink);
font-size: 14px;
font-weight: 800;
}
.content-field-grid input,
.content-field-grid select,
.content-field-grid textarea,
.content-editor input,
.content-editor select,
.content-editor textarea {
width: 100%;
border: 1px solid #cfd8e5;
border-radius: 8px;
background: #fff;
padding: 11px 12px;
color: var(--ink);
}
.content-editor textarea {
min-height: 110px;
resize: vertical;
}
.content-repeaters {
display: grid;
gap: 12px;
}
.content-repeater {
display: grid;
gap: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: #f8fafc;
padding: 14px;
}
.content-preview-card {
overflow: hidden;
}
.content-preview-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.content-preview-toolbar h2 {
margin-bottom: 0;
}
.content-preview-toolbar div {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.content-preview {
width: 100%;
max-width: 100%;
min-height: 320px;
border: 1px solid var(--line);
border-radius: 8px;
background:
linear-gradient(90deg, rgba(6, 24, 52, 0.95), rgba(8, 42, 96, 0.72)),
url("/workbench-signal.svg"),
#07172d;
background-size: cover;
color: #fff;
padding: 34px;
}
.content-preview.tablet {
max-width: 760px;
margin: 0 auto;
}
.content-preview.mobile {
max-width: 390px;
margin: 0 auto;
}
.content-preview h1 {
font-size: clamp(30px, 5vw, 52px);
}
.content-preview .lead,
.content-preview p {
color: rgba(255, 255, 255, 0.82);
}
@media (max-width: 1080px) and (min-width: 861px) {
.brand-logo {
width: 236px;
@ -1095,4 +1260,17 @@ h3 {
grid-template-columns: 1fr;
gap: 4px;
}
.content-editor-layout {
grid-template-columns: 1fr;
}
.content-tabs {
position: static;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.content-field-grid {
grid-template-columns: 1fr;
}
}

View file

@ -10,13 +10,13 @@ export default function ImpressumPage() {
<PageHero eyebrow="Rechtliches" title="Impressum">Rechtliche Angaben für die öffentliche Firmenwebsite.</PageHero>
<section className="section">
<div className="container legal">
<p className="notice">TODO: Rechtliche Angaben ergänzen</p>
<p className="notice">Rechtliche Angaben müssen vor produktiver Veröffentlichung durch den Betreiber ergänzt und geprüft werden.</p>
<h2>Anbieterkennzeichnung</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<p>Die vollständige Anbieterkennzeichnung wird durch den Betreiber gepflegt.</p>
<h2>Kontakt</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<p>Die rechtlich relevanten Kontaktdaten werden durch den Betreiber gepflegt.</p>
<h2>Umsatzsteuer / Registerangaben</h2>
<p>TODO: Rechtliche Angaben ergänzen</p>
<p>Umsatzsteuer- und Registerangaben werden durch den Betreiber gepflegt, sofern sie erforderlich sind.</p>
</div>
</section>
</>

View file

@ -1,21 +1,40 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import ContactForm from "@/components/ContactForm";
import { createMetadata } from "../seo";
import { getContent } from "@/lib/content/service";
import { createContentMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Kontakt", "Kontakt zu Funktechnik Schubert aufnehmen.", "/kontakt");
export const dynamic = "force-dynamic";
export async function generateMetadata() {
const content = await getContent("contact");
return createContentMetadata(content.seo);
}
export default async function ContactPage() {
const content = await getContent("contact");
const contactDetails = [
content.phone && `Telefon: ${content.phone}`,
content.email && `E-Mail: ${content.email}`,
content.address && `Adresse: ${content.address}`,
content.openingHours && `Öffnungszeiten: ${content.openingHours}`,
].filter(Boolean);
export default function ContactPage() {
return (
<>
<PageHero eyebrow="Kontakt" title="Kontakt aufnehmen">
Beschreiben Sie kurz Ihr Anliegen. Für Reparaturen nutzen Sie idealerweise die strukturierte Reparaturannahme.
<PageHero eyebrow={content.eyebrow} title={content.title}>
{content.subtitle}
</PageHero>
<section className="section">
<div className="container split">
<div>
<h2>Direkt und technisch</h2>
<p className="lead">Je genauer Gerät, Anliegen und gewünschte Unterstützung beschrieben werden, desto gezielter kann die Rückmeldung erfolgen.</p>
<h2>{content.cta.text}</h2>
<p className="lead">{content.heroText}</p>
{contactDetails.length > 0 && (
<ul className="list">
{contactDetails.map((detail) => <li key={detail}>{detail}</li>)}
</ul>
)}
{content.googleMapsLink && <p><a className="button light" href={content.googleMapsLink}>Karte öffnen</a></p>}
</div>
<div className="panel">
<ContactForm />

View file

@ -1,12 +1,13 @@
import type { Metadata } from "next";
import SiteFrame from "@/components/SiteFrame";
import "./globals.css";
import { createMetadata } from "./seo";
import { getContent } from "@/lib/content/service";
import { createContentMetadata } from "./seo";
export const metadata: Metadata = createMetadata(
"Funktechnik Schubert",
"Service, Reparatur, Diagnose und Abgleich von Funktechnik, CB-Funk, Amateurfunk und Kommunikationstechnik.",
);
export async function generateMetadata(): Promise<Metadata> {
const settings = await getContent("settings");
return createContentMetadata(settings.seo);
}
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (

View file

@ -1,27 +1,29 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import ServiceCard from "@/components/ServiceCard";
import { createMetadata } from "../seo";
import { getContent } from "@/lib/content/service";
import { createContentMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Leistungen", "Funkgeräte-Service, Diagnose, Abgleich und Dokumentation für Kommunikationstechnik.", "/leistungen");
export const dynamic = "force-dynamic";
const services = [
["Funkgeräte-Service", "Service für CB-Funk, Amateurfunk, Betriebsfunk sowie ältere und moderne Funkgeräte. Dazu gehören Sichtprüfung, Funktionsprüfung und eine technische Einschätzung des Gerätezustands."],
["Fehlersuche & Reparatur", "Strukturierte Analyse bei fehlender Modulation, Frequenzabweichung, PLL-Problemen, Empfangs- oder Sendeproblemen, Audio-/NF-Fehlern und Anzeige- oder Displayfehlern."],
["Abgleich & Prüfung", "Frequenzabgleich, Hub- und Modulationsprüfung, Sendeleistung, Empfindlichkeit und Funktionsprüfung nach technischen Unterlagen und sinnvoller Werkstattpraxis."],
["Messtechnik & Diagnose", "Systematische Fehlersuche mit geeigneter Messtechnik wie Oszilloskop, Frequenzzähler, Signalgenerator, RF-Messung und Modulationsmessung."],
["Serviceunterlagen", "Sorgfältige Einordnung von Schaltplänen, Service Manuals, Abgleichanleitungen, Dokumentation und Reparaturhinweisen für nachvollziehbare Arbeitsschritte."],
] as const;
export async function generateMetadata() {
const content = await getContent("services");
return createContentMetadata(content.seo);
}
export default async function ServicesPage() {
const content = await getContent("services");
const services = content.services
.filter((service) => service.visible)
.sort((left, right) => left.sortOrder - right.sortOrder);
export default function ServicesPage() {
return (
<>
<PageHero eyebrow="Leistungen" title="Service für Funkgeräte und Kommunikationstechnik">
Technische Unterstützung für Geräte, bei denen Diagnose, Messtechnik, Erfahrung und saubere Dokumentation zählen.
<PageHero eyebrow={content.eyebrow} title={content.title}>
{content.subtitle}
</PageHero>
<section className="section">
<div className="container grid service-grid">
{services.map(([title, text]) => <ServiceCard key={title} title={title}>{text}</ServiceCard>)}
{services.map((service) => <ServiceCard key={service.title} title={service.title}>{service.description}</ServiceCard>)}
</div>
</section>
</>

View file

@ -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 (
<>
<section className="hero">
<div className="container hero-content">
<p className="eyebrow">Funktechnik, Messtechnik, Werkstattservice</p>
<h1>Funktechnik Schubert</h1>
<p className="lead">Service, Reparatur und Diagnose für Funkgeräte, Messtechnik und Kommunikationstechnik.</p>
<p className="hero-copy">Von CB-Funk und Amateurfunk bis zur professionellen Funktechnik: Wir unterstützen bei Fehlersuche, Abgleich, Modulationsproblemen, Frequenzproblemen und technischer Dokumentation.</p>
<p className="eyebrow">{content.eyebrow}</p>
<h1>{content.title}</h1>
<p className="lead">{content.subtitle}</p>
<p className="hero-copy">{content.heroText}</p>
<div className="hero-badges" aria-label="Technische Schwerpunkte">
<span>CB-Funk</span>
<span>Amateurfunk</span>
<span>Messtechnik</span>
<span>Abgleich</span>
<span>Diagnose</span>
<span>Service Manuals</span>
{content.badges.map((badge) => <span key={badge}>{badge}</span>)}
</div>
<div className="hero-actions">
<Link className="button" href="/reparatur">Reparatur anfragen</Link>
<Link className="button secondary" href="/leistungen">Leistungen ansehen</Link>
<Link className="button" href={content.cta.primary.href}>{content.cta.primary.label}</Link>
<Link className="button secondary" href={content.cta.secondary.href}>{content.cta.secondary.label}</Link>
</div>
</div>
</section>
@ -29,13 +35,11 @@ export default function HomePage() {
<div className="container">
<div className="section-head">
<p className="eyebrow">Leistungsbereiche</p>
<h2>Werkstattservice für Funktechnik und Messtechnik</h2>
<p className="lead">Von der ersten Fehlerbeschreibung bis zur dokumentierten Prüfung: Funktechnik Schubert verbindet technische Erfahrung, geeignete Messmittel und strukturierte Kommunikation.</p>
<h2>{content.cta.text}</h2>
<p className="lead">{content.intro}</p>
</div>
<div className="grid three">
<ServiceCard title="Funkgeräte-Service">Sichtprüfung, Funktionsprüfung und technische Beurteilung für CB-Funk, Amateurfunk, Betriebsfunk sowie ältere und moderne Geräte.</ServiceCard>
<ServiceCard title="Fehlersuche & Reparatur">Systematische Diagnose bei Empfangsproblemen, Sendeproblemen, PLL-Fehlern, Frequenzabweichungen und Audio-/NF-Störungen.</ServiceCard>
<ServiceCard title="Abgleich & Messtechnik">Frequenzabgleich, Hub- und Modulationsprüfung, Sendeleistung, Empfindlichkeit und RF-Messung mit geeigneter Messtechnik.</ServiceCard>
{content.features.map((feature) => <ServiceCard key={feature.title} title={feature.title}>{feature.text}</ServiceCard>)}
</div>
</div>
</section>
@ -43,20 +47,17 @@ export default function HomePage() {
<section className="section dark">
<div className="container split">
<div>
<p className="eyebrow">Persönlicher Support</p>
<h2>Direkte technische Einschätzung statt anonymer Abwicklung</h2>
<p className="lead">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.</p>
<p className="eyebrow">{content.promise.eyebrow}</p>
<h2>{content.promise.title}</h2>
<p className="lead">{content.promise.text}</p>
<div className="actions">
<Link className="button" href="/reparatur">Reparaturformular öffnen</Link>
<Link className="button" href={content.promise.button.href}>{content.promise.button.label}</Link>
</div>
</div>
<div className="card">
<h3>Warum Funktechnik Schubert?</h3>
<h3>{content.promise.title}</h3>
<ul className="list">
<li>Technischer Fokus auf Funkgeräte und Kommunikationstechnik</li>
<li>Nachvollziehbare Diagnose mit Blick auf Messwerte und Fehlerbild</li>
<li>Sauberer Umgang mit Service Manuals, Schaltplänen und Abgleichanleitungen</li>
<li>Klare Rückmeldung zu Zustand, Aufwand und sinnvollen nächsten Schritten</li>
{content.promise.bullets.map((item) => <li key={item}>{item}</li>)}
</ul>
</div>
</div>

View file

@ -1,4 +1,5 @@
import type { Metadata } from "next";
import type { SeoContent } from "@/lib/content/types";
const siteName = "Funktechnik Schubert";
const defaultTitle = "Funktechnik Schubert | Funkgeräte-Service & Messtechnik";
@ -59,4 +60,32 @@ export function createMetadata(title: string, description: string, path = "/"):
};
}
export function createContentMetadata(seo: SeoContent): Metadata {
const path = seo.canonicalUrl || "/";
const url = new URL(path, siteUrl).toString();
const title = seo.metaTitle || siteName;
const description = seo.metaDescription || defaultTitle;
return {
...createMetadata(title, description, path),
keywords: seo.keywords ? seo.keywords.split(",").map((keyword) => keyword.trim()).filter(Boolean) : keywords,
openGraph: {
type: "website",
locale: "de_DE",
url,
siteName,
title: seo.openGraphTitle || title,
description: seo.openGraphDescription || description,
images: [
{
url: seo.socialImage || "/funktechnik_schubert_logo.jpg",
width: 1200,
height: 630,
alt: seo.openGraphTitle || title,
},
],
},
};
}
export { defaultTitle, siteName, siteUrl };

View file

@ -1,25 +1,34 @@
import type { Metadata } from "next";
import PageHero from "@/components/PageHero";
import { createMetadata } from "../seo";
import { getContent } from "@/lib/content/service";
import { createContentMetadata } from "../seo";
export const metadata: Metadata = createMetadata("Über uns", "Funktechnik Schubert steht für technischen Service und klare Kommunikation.", "/ueber-uns");
export const dynamic = "force-dynamic";
export async function generateMetadata() {
const content = await getContent("about");
return createContentMetadata(content.seo);
}
export default async function AboutPage() {
const content = await getContent("about");
const blocks = content.paragraphs.length > 0 ? content.paragraphs : [
{ title: content.companyDescription, text: content.workshopDescription },
{ title: content.philosophy, text: content.intro },
];
export default function AboutPage() {
return (
<>
<PageHero eyebrow="Über uns" title="Technik verstehen, Fehler nachvollziehbar lösen">
Funktechnik Schubert richtet sich an Kunden, die bei Funkgeräten und Kommunikationstechnik eine persönliche, technische Einschätzung suchen.
<PageHero eyebrow={content.eyebrow} title={content.title}>
{content.subtitle}
</PageHero>
<section className="section">
<div className="container grid two">
<article className="card">
<h2>Arbeitsweise</h2>
<p>Im Mittelpunkt stehen saubere Diagnose, transparente Kommunikation und der respektvolle Umgang mit bestehenden Geräten und Serviceunterlagen.</p>
</article>
<article className="card">
<h2>Fokus</h2>
<p>Der Schwerpunkt liegt auf Funktechnik, Elektronikservice, Abgleichfragen und technischer Unterstützung rund um Kommunikationstechnik.</p>
</article>
{blocks.map((block) => (
<article key={block.title} className="card">
<h2>{block.title}</h2>
<p>{block.text}</p>
</article>
))}
</div>
</section>
</>