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
@@ -75,10 +75,11 @@ function useEscapeKey(active: boolean, onEscape: () => void) {
|
||||
}, [active, onEscape]);
|
||||
}
|
||||
|
||||
function Logo({ t, companyName }: { t: Dictionary; companyName: string }) {
|
||||
function Logo({ t, companyName, logoSrc }: { t: Dictionary; companyName: string; logoSrc: string | null }) {
|
||||
return (
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<Logomark className="h-9 w-9 flex-shrink-0" />
|
||||
{/* Kopfzeile ist bei 1280-1440 px knapp: breite Logos hier enger begrenzt. */}
|
||||
<Logomark className="h-9 w-9 max-w-14 flex-shrink-0 sm:max-w-28" src={logoSrc} />
|
||||
<span className="flex flex-col leading-none">
|
||||
<span className="text-2xl tracking-[0.2em] text-forest font-serif">
|
||||
{companyName}
|
||||
@@ -375,7 +376,15 @@ function isActivePath(pathname: string, href: string): boolean {
|
||||
return href === "/" ? pathname === "/" : pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
export function Header({ companyName }: { companyName: string }) {
|
||||
export function Header({
|
||||
companyName,
|
||||
logoSrc = null,
|
||||
logoInvertedSrc = null,
|
||||
}: {
|
||||
companyName: string;
|
||||
logoSrc?: string | null;
|
||||
logoInvertedSrc?: string | null;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const pathname = usePathname();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
@@ -410,7 +419,7 @@ export function Header({ companyName }: { companyName: string }) {
|
||||
Navigation und Button ab 1280px Breite ohne jeden Abstand aneinander.
|
||||
*/}
|
||||
<div className="mx-auto flex h-20 w-full max-w-[100rem] items-center justify-between px-6 2xl:px-10">
|
||||
<Logo t={t} companyName={companyName} />
|
||||
<Logo t={t} companyName={companyName} logoSrc={logoSrc} />
|
||||
|
||||
<nav aria-label="Hauptnavigation" className="hidden xl:flex items-center gap-3 2xl:gap-6">
|
||||
{mainNav.map((item) => {
|
||||
@@ -478,7 +487,7 @@ export function Header({ companyName }: { companyName: string }) {
|
||||
>
|
||||
<Container className="flex items-center justify-between h-20">
|
||||
<span className="flex items-center gap-3">
|
||||
<Logomark className="h-8 w-8" variant="inverted" />
|
||||
<Logomark className="h-8 w-8" variant="inverted" src={logoInvertedSrc} />
|
||||
<span className="text-xl tracking-[0.2em] font-serif">
|
||||
{companyName}
|
||||
</span>
|
||||
|
||||
Reference in new issue
Block a user