-
+
{companyInfo.name}
diff --git a/src/i18n/dictionaries/de.ts b/src/i18n/dictionaries/de.ts
index 18deaaa..752f514 100644
--- a/src/i18n/dictionaries/de.ts
+++ b/src/i18n/dictionaries/de.ts
@@ -1281,6 +1281,7 @@ export const de: Dictionary = {
sectionImagesIntro: "Alle Bilder und das Hero-Video, die du hochladen kannst, sortiert nach Seite.",
sectionText: "Texte",
sectionTextIntro: "Alle Textinhalte, sortiert nach Bereich.",
+ groupBrand: "Logo und Favicon",
groupCompany: "Firmendaten",
groupShipping: "Versand",
groupHome: "Homepage",
diff --git a/src/i18n/dictionaries/en.ts b/src/i18n/dictionaries/en.ts
index d02ca48..250c2e5 100644
--- a/src/i18n/dictionaries/en.ts
+++ b/src/i18n/dictionaries/en.ts
@@ -1267,6 +1267,7 @@ export const en: Dictionary = {
sectionImagesIntro: "Every image and the hero video you can upload, grouped by page.",
sectionText: "Text",
sectionTextIntro: "Every text field, grouped by section.",
+ groupBrand: "Logo and favicon",
groupCompany: "Company details",
groupShipping: "Shipping",
groupHome: "Homepage",
diff --git a/src/i18n/dictionary-type.ts b/src/i18n/dictionary-type.ts
index bf0daad..367d540 100644
--- a/src/i18n/dictionary-type.ts
+++ b/src/i18n/dictionary-type.ts
@@ -940,6 +940,7 @@ export interface Dictionary {
sectionImagesIntro: string;
sectionText: string;
sectionTextIntro: string;
+ groupBrand: string;
groupCompany: string;
groupShipping: string;
groupHome: string;
diff --git a/src/lib/brand-icon.ts b/src/lib/brand-icon.ts
new file mode 100644
index 0000000..f002d5b
--- /dev/null
+++ b/src/lib/brand-icon.ts
@@ -0,0 +1,58 @@
+import { readFile } from "node:fs/promises";
+import sharp from "sharp";
+import { isValidUploadPath, resolveUploadPath } from "@/lib/uploads";
+
+/**
+ * Standard-Icon (Waben-Rahmen mit drei Tannen auf Waldgruen) — frueher als
+ * statische app/icon.svg ausgeliefert. Jetzt Quelle fuer app/icon.tsx und
+ * app/apple-icon.tsx, solange im Admin kein eigenes Favicon hochgeladen ist.
+ */
+const DEFAULT_ICON_SVG = `
`;
+
+async function readUploadedIcon(faviconPath: string | null): Promise
{
+ if (!faviconPath || !isValidUploadPath(faviconPath)) return null;
+ // resolveUploadPath haelt den Zugriff innerhalb von UPLOAD_DIR.
+ const file = resolveUploadPath(faviconPath.replace(/^\/uploads\//, "").split("/"));
+ if (!file) return null;
+ try {
+ return await readFile(file);
+ } catch {
+ // Datei fehlt (z.B. Upload-Volume nicht persistent): bewusst kein Fehler,
+ // der Browser bekaeme sonst gar kein Icon — das Standard-Icon springt ein.
+ // Das Dashboard meldet fehlende Upload-Dateien ohnehin gesondert.
+ return null;
+ }
+}
+
+/**
+ * Quadratisches PNG in `size` Pixeln. Nicht quadratische Uploads werden
+ * eingepasst (nicht beschnitten), der Rand bleibt transparent — ausser
+ * `background` ist gesetzt (Apple-Touch-Icon: iOS zeigt Transparenz schwarz).
+ */
+export async function renderBrandIcon(
+ faviconPath: string | null,
+ size: number,
+ background?: string
+): Promise {
+ const uploaded = await readUploadedIcon(faviconPath);
+ const input = uploaded ?? Buffer.from(DEFAULT_ICON_SVG);
+ const image = sharp(input, { density: 384 }).resize(size, size, {
+ fit: "contain",
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
+ });
+ return (background ? image.flatten({ background }) : image).png().toBuffer();
+}
diff --git a/src/lib/brand.ts b/src/lib/brand.ts
new file mode 100644
index 0000000..4a53945
--- /dev/null
+++ b/src/lib/brand.ts
@@ -0,0 +1,35 @@
+import { getContentValues } from "@/lib/content";
+import { isValidUploadPath } from "@/lib/uploads";
+
+export interface BrandAssets {
+ /** Logo fuer helle Flaechen (Kopfzeile); null = gezeichnetes Waben-Zeichen. */
+ logo: string | null;
+ /** Logo fuer dunkle Flaechen (Mobilmenue, Fusszeile, Admin-Leiste); faellt auf `logo` zurueck. */
+ logoInverted: string | null;
+ /** Quadratisches Favicon-Bild; null = Standard-Icon (siehe lib/brand-icon.ts). */
+ favicon: string | null;
+}
+
+export const BRAND_CONTENT_KEYS = ["brand.logo", "brand.logoInverted", "brand.favicon"] as const;
+
+/**
+ * Hochgeladene Marken-Bilder aus den Inhalten (Admin > Inhalte > Logo und
+ * Favicon). Jeder Wert wird hier noch einmal gegen das Upload-Pfadmuster
+ * geprueft, obwohl updateSiteContentAction das beim Speichern schon tut:
+ * der Pfad landet ungefiltert in einem src-Attribut auf jeder Seite, ein
+ * irgendwie in die Datenbank geratener fremder Wert (z.B. eine externe URL)
+ * soll dort nie ankommen.
+ */
+export async function getBrandAssets(): Promise {
+ // Nicht lokalisierte Felder — die Locale spielt fuer den Wert keine Rolle.
+ const values = await getContentValues([...BRAND_CONTENT_KEYS], "de");
+ const pick = (value: string | undefined): string | null =>
+ value && isValidUploadPath(value) ? value : null;
+
+ const logo = pick(values["brand.logo"]);
+ return {
+ logo,
+ logoInverted: pick(values["brand.logoInverted"]) ?? logo,
+ favicon: pick(values["brand.favicon"]),
+ };
+}
diff --git a/src/lib/content-registry.ts b/src/lib/content-registry.ts
index 09fcd92..6f596c9 100644
--- a/src/lib/content-registry.ts
+++ b/src/lib/content-registry.ts
@@ -18,7 +18,7 @@ export type ContentFieldType =
export interface ContentFieldDef {
key: string;
- group: "company" | "shipping" | "home" | "pages" | "legal" | "social";
+ group: "brand" | "company" | "shipping" | "home" | "pages" | "legal" | "social";
labelDe: string;
labelEn: string;
type: ContentFieldType;
@@ -54,6 +54,14 @@ function joinSections(sections: LegalSectionContent[]): string {
}
export const CONTENT_REGISTRY: ContentFieldDef[] = [
+ // --- Logo und Favicon (nicht lokalisiert). Leer = das gezeichnete
+ // Waben-Zeichen (components/logomark.tsx) bzw. das Standard-Icon
+ // (lib/brand-icon.ts). Nur Rasterbilder: der Upload nimmt bewusst kein SVG
+ // an (ein SVG kann Skripte enthalten). Gelesen ueber lib/brand.ts.
+ { key: "brand.logo", group: "brand", labelDe: "Logo (PNG mit transparentem Hintergrund, erscheint neben dem Firmennamen)", labelEn: "Logo (PNG with transparent background, shown next to the company name)", type: "image", localized: false, getDefault: () => "" },
+ { key: "brand.logoInverted", group: "brand", labelDe: "Logo für dunkle Flächen (optional: Mobilmenü, Fußzeile, Admin-Leiste; leer = normales Logo)", labelEn: "Logo for dark backgrounds (optional: mobile menu, footer, admin sidebar; empty = regular logo)", type: "image", localized: false, getDefault: () => "" },
+ { key: "brand.favicon", group: "brand", labelDe: "Favicon (quadratisch, mindestens 512 × 512 px; leer = Standard-Wabe)", labelEn: "Favicon (square, at least 512 × 512 px; empty = default honeycomb)", type: "image", localized: false, getDefault: () => "" },
+
// --- Firmendaten (nicht lokalisiert) ---
{ key: "company.name", group: "company", labelDe: "Firmenname", labelEn: "Company name", type: "text", localized: false, getDefault: () => siteConfig.name },
{ key: "company.claim", group: "company", labelDe: "Slogan", labelEn: "Tagline", type: "text", localized: false, getDefault: () => siteConfig.claim },