Skip to content

Brand & Logos

cms module

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

ts
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:

ts
import {
  resolveLogo, generateFaviconHtml, generateWebManifest
} from '@system-core/core/cms-client'

BrandAssets Shape

ts
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

ContextWhat rendersFallback
sitePublic site header
loginAuth / login cardsiteLogo
dashboardAdmin sidebar (expanded)siteLogo
dashboard-markAdmin sidebar (collapsed icon)dashboard.fullsiteLogo
ts
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:

ts
import { generateFaviconHtml } from '@system-core/core/cms'

const headHtml = generateFaviconHtml(assets)

Output example:

html
<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

ts
import { generatePwaManifestIcons } from '@system-core/core/cms'

const icons = generatePwaManifestIcons(assets)
// → [{ src: '...', sizes: '192x192', type: 'image/png' }, ...]

Full manifest object

ts
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:

ts
// 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:

ts
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

ts
import { validateBrandAssets } from '@system-core/core/cms'

const { errors, warnings } = validateBrandAssets(assets)

Errors (block save): invalid / javascript: URLs.

Warnings (advisory):

WarningMeaning
No faviconBrowser tab shows default icon
No apple180iOS add-to-home uses a screenshot
No pwa192 / pwa512Chrome install prompt has no icon
pwa512 without maskableAndroid adaptive icons may clip the logo
AssetSizeFormatNotes
favicon.svgAnySVGBest quality, modern browsers
favicon.ico32×32ICO/PNGLegacy fallback
appIcons.apple180180×180PNGiOS home screen
appIcons.pwa192192×192PNGAndroid Chrome
appIcons.pwa512512×512PNGChrome install prompt
appIcons.maskable512×512PNGAndroid adaptive — safe zone = centre 80%
siteLogo.images.light~200×60pxSVG/PNGSite header
siteLogo.images.dark~200×60pxSVG/PNGDark-mode header
dashboardLogo.mark~40×40pxSVG/PNGCollapsed sidebar

system-core documentation for maintainers, integrators, and AI build agents.