feat(admin): Logo und Favicon per Bild-Upload austauschbar
Neue Gruppe "Logo und Favicon" unter Admin > Inhalte mit drei Bildfeldern: Logo (Kopfzeile), optionales Logo fuer dunkle Flaechen (Mobilmenue, Fusszeile, Admin-Leiste; faellt sonst auf das normale Logo zurueck) und Favicon. Leer bleibt alles wie bisher (gezeichnetes Waben-Zeichen bzw. Standard-Icon). - lib/brand.ts liest die Pfade und prueft sie erneut gegen das Upload-Muster, bevor sie in ein src-Attribut gelangen. - Statische app/icon.svg und app/favicon.ico ersetzt durch dynamische app/icon.tsx (512 px PNG) und app/apple-icon.tsx (180 px auf Creme); lib/brand-icon.ts rendert Upload oder Standard-Icon per sharp und liest Dateien nur ueber resolveUploadPath. Cache-Control no-cache, damit ein neues Favicon ohne Tage Verzoegerung erscheint. - /favicon.ico per Rewrite auf /icon, Manifest nutzt /icon. - Hochgeladenes Logo mit fester Hoehe und begrenzter Breite (Kopfzeile mobil 56 px, ab sm 112 px), damit breite Wortmarken nichts sprengen. Uploads bleiben auf Rasterbilder beschraenkt (kein SVG, XSS-Schutz). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
ae2726b0ee
commit
4f442ec06d
19 files changed
+217
-77
No files matched your search
@@ -204,6 +204,7 @@ export default async function AdminInhaltePage() {
|
||||
const current = await loadCurrentValues();
|
||||
|
||||
const groups: { key: ContentFieldDef["group"]; heading: string }[] = [
|
||||
{ key: "brand", heading: t.adminContentPage.groupBrand },
|
||||
{ key: "company", heading: t.adminContentPage.groupCompany },
|
||||
{ key: "shipping", heading: t.adminContentPage.groupShipping },
|
||||
{ key: "home", heading: t.adminContentPage.groupHome },
|
||||
|
||||
@@ -7,6 +7,7 @@ import { logoutAction } from "@/app/konto/actions";
|
||||
import { AdminShell } from "@/components/admin-shell";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
import { getBrandAssets } from "@/lib/brand";
|
||||
|
||||
// Admin-Daten (Bestellungen, Bestand) muessen immer aktuell sein; zudem
|
||||
// entkoppelt das den Docker-Build-Schritt von einer DB-Verbindung.
|
||||
@@ -47,6 +48,7 @@ export default async function AdminLayout({ children }: LayoutProps<"/admin">) {
|
||||
// Seitenleiste. Nur echte Arbeit: bezahlte, noch nicht versendete oder
|
||||
// abgeholte Bestellungen (keine offenen Zahlungen), neue Retouren,
|
||||
// unfreigegebene Bewertungen, neue Firmenanfragen. Reine COUNT-Abfragen.
|
||||
const brand = await getBrandAssets();
|
||||
const [openOrders, openReturns, pendingReviews, newInquiries] = await Promise.all([
|
||||
prisma.order.count({
|
||||
where: { status: { in: ["BEZAHLT", "IN_VORBEREITUNG", "VERSANDBEREIT", "ABHOLBEREIT"] }, archivedAt: null },
|
||||
@@ -59,6 +61,7 @@ export default async function AdminLayout({ children }: LayoutProps<"/admin">) {
|
||||
return (
|
||||
<AdminShell
|
||||
adminName={`${admin.firstName} ${admin.lastName}`}
|
||||
logoSrc={brand.logoInverted}
|
||||
badges={{
|
||||
"/admin/bestellungen": openOrders,
|
||||
"/admin/retouren": openReturns,
|
||||
|
||||
+15
-33
@@ -1,41 +1,23 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { getBrandAssets } from "@/lib/brand";
|
||||
import { renderBrandIcon } from "@/lib/brand-icon";
|
||||
|
||||
export const size = { width: 180, height: 180 };
|
||||
export const contentType = "image/png";
|
||||
|
||||
// Wie app/icon.tsx: im Admin austauschbar, deshalb nicht zur Build-Zeit festschreiben.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Apple-Touch-Icon fuer "Zum Home-Bildschirm hinzufuegen" auf iOS/iPadOS —
|
||||
* Apple akzeptiert dafuer ausschliesslich PNG/JPG, kein SVG (anders als
|
||||
* app/icon.svg, das moderne Chrome/Android direkt lesen, siehe
|
||||
* app/manifest.ts). Ohne diese Datei nimmt iOS ersatzweise einen
|
||||
* Screenshot der Seite als Icon — unbrandet und meist unbrauchbar. Gleiche
|
||||
* Grafik wie app/icon.svg, per ImageResponse als Raster gerendert.
|
||||
* Apple akzeptiert dafuer ausschliesslich PNG/JPG, kein SVG. Ohne diese
|
||||
* Datei nimmt iOS ersatzweise einen Screenshot der Seite als Icon. Gleiche
|
||||
* Quelle wie das Favicon, aber auf Creme-Grund: iOS stellt transparente
|
||||
* Flaechen schwarz dar.
|
||||
*/
|
||||
export default function AppleIcon() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<svg width={180} height={180} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width={100} height={100} rx={20} fill="#24352B" />
|
||||
<polygon points="50,12 82,30 82,68 50,86 18,68 18,30" fill="none" stroke="#B88A44" strokeWidth={4} />
|
||||
<g fill="#F4EFE5">
|
||||
<g transform="translate(26,42) scale(0.62)">
|
||||
<polygon points="10,2 5,13 15,13" />
|
||||
<polygon points="10,10 2,22 18,22" />
|
||||
<rect x={8} y={22} width={4} height={4} />
|
||||
</g>
|
||||
<g transform="translate(40,30) scale(0.82)">
|
||||
<polygon points="10,2 5,13 15,13" />
|
||||
<polygon points="10,10 2,22 18,22" />
|
||||
<rect x={8} y={22} width={4} height={4} />
|
||||
</g>
|
||||
<g transform="translate(56,42) scale(0.62)">
|
||||
<polygon points="10,2 5,13 15,13" />
|
||||
<polygon points="10,10 2,22 18,22" />
|
||||
<rect x={8} y={22} width={4} height={4} />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
{ ...size }
|
||||
);
|
||||
export default async function AppleIcon(): Promise<Response> {
|
||||
const { favicon } = await getBrandAssets();
|
||||
const png = await renderBrandIcon(favicon, size.width, "#FAF8F3");
|
||||
return new Response(new Uint8Array(png), {
|
||||
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" },
|
||||
});
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,21 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" rx="20" fill="#24352B" />
|
||||
<polygon points="50,12 82,30 82,68 50,86 18,68 18,30" fill="none" stroke="#B88A44" stroke-width="4" />
|
||||
<g fill="#F4EFE5">
|
||||
<g transform="translate(26,42) scale(0.62)">
|
||||
<polygon points="10,2 5,13 15,13" />
|
||||
<polygon points="10,10 2,22 18,22" />
|
||||
<rect x="8" y="22" width="4" height="4" />
|
||||
</g>
|
||||
<g transform="translate(40,30) scale(0.82)">
|
||||
<polygon points="10,2 5,13 15,13" />
|
||||
<polygon points="10,10 2,22 18,22" />
|
||||
<rect x="8" y="22" width="4" height="4" />
|
||||
</g>
|
||||
<g transform="translate(56,42) scale(0.62)">
|
||||
<polygon points="10,2 5,13 15,13" />
|
||||
<polygon points="10,10 2,22 18,22" />
|
||||
<rect x="8" y="22" width="4" height="4" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 844 B |
@@ -0,0 +1,24 @@
|
||||
import { getBrandAssets } from "@/lib/brand";
|
||||
import { renderBrandIcon } from "@/lib/brand-icon";
|
||||
|
||||
export const size = { width: 512, height: 512 };
|
||||
export const contentType = "image/png";
|
||||
|
||||
// Das Favicon ist im Admin austauschbar (Inhalte > Logo und Favicon) — nicht
|
||||
// zur Build-Zeit einfrieren. "no-cache" statt langem Caching: Browser halten
|
||||
// Favicons sonst tagelang fest, ein neues Bild waere nicht zu sehen.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Favicon und Manifest-Icon. 512 px, weil dieselbe Datei auch als Android-
|
||||
* Startbildschirm-Icon dient (app/manifest.ts); Browser skalieren fuer den
|
||||
* Tab selbst herunter. /favicon.ico wird in next.config.ts hierher
|
||||
* umgeleitet.
|
||||
*/
|
||||
export default async function Icon(): Promise<Response> {
|
||||
const { favicon } = await getBrandAssets();
|
||||
const png = await renderBrandIcon(favicon, size.width);
|
||||
return new Response(new Uint8Array(png), {
|
||||
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" },
|
||||
});
|
||||
}
|
||||
+9
-3
@@ -14,6 +14,7 @@ import { getSocialLinks } from "@/lib/social-links";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
import { I18nProvider } from "@/i18n/i18n-provider";
|
||||
import { getBrandAssets } from "@/lib/brand";
|
||||
|
||||
const cormorant = Cormorant_Garamond({
|
||||
variable: "--font-cormorant",
|
||||
@@ -47,7 +48,12 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
export default async function RootLayout({ children }: LayoutProps<"/">) {
|
||||
const locale = await getLocale();
|
||||
const dict = getDictionary(locale);
|
||||
const [company, socialLinks, flash] = await Promise.all([getCompanyInfo(), getSocialLinks(), readFlash()]);
|
||||
const [company, socialLinks, flash, brand] = await Promise.all([
|
||||
getCompanyInfo(),
|
||||
getSocialLinks(),
|
||||
readFlash(),
|
||||
getBrandAssets(),
|
||||
]);
|
||||
const base = getSiteUrl();
|
||||
|
||||
// LocalBusiness-Schema fuer die gesamte Seite (einmal im Root-Layout statt
|
||||
@@ -131,13 +137,13 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
|
||||
{dict.nav.skipToContent}
|
||||
</a>
|
||||
<HideOnAdmin>
|
||||
<Header companyName={company.name} />
|
||||
<Header companyName={company.name} logoSrc={brand.logo} logoInvertedSrc={brand.logoInverted} />
|
||||
</HideOnAdmin>
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<HideOnAdmin>
|
||||
<Footer companyInfo={company} socialLinks={socialLinks} />
|
||||
<Footer companyInfo={company} socialLinks={socialLinks} logoSrc={brand.logoInverted} />
|
||||
</HideOnAdmin>
|
||||
<CartDrawer />
|
||||
<CookieConsentBanner />
|
||||
|
||||
+6
-7
@@ -4,10 +4,9 @@ import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
|
||||
/**
|
||||
* Web-App-Manifest fuer "Zum Startbildschirm hinzufuegen" auf Mobilgeraeten —
|
||||
* nutzt das bestehende icon.svg direkt statt zusaetzlich generierter
|
||||
* Raster-Varianten (Chrome/Android unterstuetzen SVG-Manifest-Icons mit
|
||||
* sizes:"any" nativ, siehe MDN). Sprachabhaengig wie der Rest der Seite.
|
||||
* Web-App-Manifest fuer "Zum Startbildschirm hinzufuegen" auf Mobilgeraeten.
|
||||
* Icon ist das im Admin austauschbare Favicon (app/icon.tsx, 512 px PNG).
|
||||
* Sprachabhaengig wie der Rest der Seite.
|
||||
*/
|
||||
export default async function manifest(): Promise<MetadataRoute.Manifest> {
|
||||
const locale = await getLocale();
|
||||
@@ -24,9 +23,9 @@ export default async function manifest(): Promise<MetadataRoute.Manifest> {
|
||||
theme_color: "#24352B",
|
||||
icons: [
|
||||
{
|
||||
src: "/icon.svg",
|
||||
sizes: "any",
|
||||
type: "image/svg+xml",
|
||||
src: "/icon",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user