Brand & Logos
The brand system centralises every logo, favicon, touch icon, and theme-colour value into a single BrandAssets object stored in SiteSettings.brand. One source of truth drives all surfaces — public site header, login card, admin dashboard, browser tab, iOS home screen, Android PWA, and social share images.
Import
import {
getBrandAssets, putBrandAssets, patchBrandAssets,
resolveLogo, resolveLogoText,
generateFaviconHtml, generatePwaManifestIcons, generateWebManifest,
validateBrandAssets, createBrandDepsFromSettings
} from '@system-core/core/cms'Client-safe functions (no Node deps) are also re-exported from cms-client:
import {
resolveLogo, generateFaviconHtml, generateWebManifest
} from '@system-core/core/cms-client'BrandAssets Shape
interface BrandAssets {
siteLogo?: { images?: LogoPair; text: string; width?: number; link?: string }
loginLogo?: { images?: LogoPair; text?: string; width?: number }
dashboardLogo?: { full?: LogoPair; mark?: LogoPair; text?: string; width?: number }
favicon?: { ico?: MediaValue; svg?: MediaValue; png32?: MediaValue; png16?: MediaValue }
appIcons?: { apple180?: MediaValue; pwa192?: MediaValue; pwa512?: MediaValue; maskable?: MediaValue }
themeColor?: { light?: string; dark?: string }
ogImage?: MediaValue
shortName?: string
}
interface LogoPair {
light: MediaValue // default / light-mode logo
dark?: MediaValue // dark-mode logo — falls back to light when absent
}All image fields are MediaValue — the same type used throughout the CMS, supporting media library references, external URLs, alt text, and blur-hash placeholders.
Logo Surfaces
| Context | What renders | Fallback |
|---|---|---|
site | Public site header | — |
login | Auth / login card | → siteLogo |
dashboard | Admin sidebar (expanded) | → siteLogo |
dashboard-mark | Admin sidebar (collapsed icon) | → dashboard.full → siteLogo |
Resolve the right logo
import { resolveLogo, resolveLogoText } from '@system-core/core/cms'
const logo = resolveLogo(assets, 'login', 'dark')
// → loginLogo.images.dark ?? loginLogo.images.light ?? siteLogo.images.dark ?? siteLogo.images.light ?? null
const text = resolveLogoText(assets, 'dashboard')
// → dashboardLogo.text ?? siteLogo.text ?? ''resolveLogo returns a MediaValue | null. When null, render text-only using resolveLogoText.
Favicon & Head Tags
Generate all <link> and <meta> tags in one call:
import { generateFaviconHtml } from '@system-core/core/cms'
const headHtml = generateFaviconHtml(assets)Output example:
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png">
<meta name="theme-color" content="#2563eb" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1e40af" media="(prefers-color-scheme: dark)">Only tags with configured assets are emitted. If both light and dark theme colours are the same value, a single <meta name="theme-color"> without a media query is emitted instead.
PWA Manifest
Icons array only
import { generatePwaManifestIcons } from '@system-core/core/cms'
const icons = generatePwaManifestIcons(assets)
// → [{ src: '...', sizes: '192x192', type: 'image/png' }, ...]Full manifest object
import { generateWebManifest } from '@system-core/core/cms'
const manifest = generateWebManifest(settings.name, assets, {
start_url: '/',
display: 'standalone',
background_color: '#ffffff'
})Serve manifest.json from an API route:
// Nuxt server route: server/routes/manifest.json.ts
export default defineEventHandler(async () => {
const assets = await getBrandAssets(brandDeps)
const settings = await getPublicSettings(settingsDeps)
return generateWebManifest(settings.name, assets)
})Storing Brand Assets
BrandAssets is stored as a JSON blob under the brand key in site settings. Use createBrandDepsFromSettings to wire it to any settings backend that supports getSetting / setSetting:
import { createBrandDepsFromSettings, getBrandAssets, putBrandAssets } from '@system-core/core/cms'
const brandDeps = createBrandDepsFromSettings({
getSetting: (key) => prisma.setting.findFirst({ where: { key } }).then(r => r?.value ?? null),
setSetting: (key, value) => prisma.setting.upsert({
where: { key },
update: { value },
create: { key, value }
}).then(() => undefined)
})
// Read
const assets = await getBrandAssets(brandDeps)
// Write (full replace)
await putBrandAssets({ siteLogo: { text: 'Acme', images: { light: logoMedia } } }, brandDeps)
// Patch (merge)
await patchBrandAssets({ themeColor: { light: '#2563eb', dark: '#1e40af' } }, brandDeps)Validation
import { validateBrandAssets } from '@system-core/core/cms'
const { errors, warnings } = validateBrandAssets(assets)Errors (block save): invalid / javascript: URLs.
Warnings (advisory):
| Warning | Meaning |
|---|---|
| No favicon | Browser tab shows default icon |
No apple180 | iOS add-to-home uses a screenshot |
No pwa192 / pwa512 | Chrome install prompt has no icon |
pwa512 without maskable | Android adaptive icons may clip the logo |
Recommended Asset Set
| Asset | Size | Format | Notes |
|---|---|---|---|
favicon.svg | Any | SVG | Best quality, modern browsers |
favicon.ico | 32×32 | ICO/PNG | Legacy fallback |
appIcons.apple180 | 180×180 | PNG | iOS home screen |
appIcons.pwa192 | 192×192 | PNG | Android Chrome |
appIcons.pwa512 | 512×512 | PNG | Chrome install prompt |
appIcons.maskable | 512×512 | PNG | Android adaptive — safe zone = centre 80% |
siteLogo.images.light | ~200×60px | SVG/PNG | Site header |
siteLogo.images.dark | ~200×60px | SVG/PNG | Dark-mode header |
dashboardLogo.mark | ~40×40px | SVG/PNG | Collapsed sidebar |