config.brand centralises every brand-sized asset in one declarative object. The platform reads this once per render and wires up favicons, OG image, Apple touch icon, PWA manifest, and the logos you reference from colors.logo — no per-feature configuration scattered across the config file.
The four icons most sites need — favicon, dark favicon, Apple touch icon and PWA icon — are editable under Configurations → Branding, beside the logo fields. The rest of the block (colours, typography, asset families) is JSON-only and is preserved untouched when you save from the panel.
Only root, name, and favicon.dir are required. Every other field is
written when you have something to put in it — an imported site typically has
a .ico, an apple-touch tile and two or three PNGs, and no SVG masters or
maskable variants at all.
Minimal example
Three fields carry the paths; everything else is optional and written only when you have the file:
{
"brand": {
"root": "/brand",
"name": "acme",
"favicon": {
"dir": "favicon",
"ico": "favicon.ico",
"png": { "32": "favicon-32x32.png" }
}
}
}Files resolve as ${root}/${favicon.dir}/${file} — the block above points at
/brand/favicon/favicon.ico. Declare only the icons you actually ship: a site
with a .ico and one PNG writes those two and leaves the SVG entries out.
A complete block
{
"brand": {
"root": "/brand-assets",
"name": "acme",
"shortName": "Acme",
"description": "Acme documentation",
"colors": {
"brand": { "light": "#3b82f6", "dark": "#60a5fa" },
"text": { "light": "#0f172a", "dark": "#f8fafc" },
"background": { "light": "#ffffff", "dark": "#0f172a" }
},
"themeColor": { "light": "#ffffff", "dark": "#0f172a" },
"backgroundColor": { "light": "#ffffff", "dark": "#0f172a" },
"typography": {
"family": "Inter",
"weight": 500,
"fallback": "system-ui, -apple-system, sans-serif"
},
"assets": {
"wordmark": {
"dir": "wordmark",
"file": "acme-wordmark",
"hasSvg": true,
"hasTheme": true,
"pngSizes": ["h40", "h80", "h120", "h240"]
},
"icon": {
"dir": "icon",
"file": "acme-icon",
"hasSvg": true,
"hasTheme": false,
"pngSizes": [32, 48, 64, 128, 256, 512]
},
"maskable": {
"dir": "maskable",
"file": "acme-maskable",
"hasSvg": false,
"hasTheme": false,
"pngSizes": [192, 512, 1024]
},
"og": {
"dir": "og",
"file": "acme-og",
"hasSvg": false,
"hasTheme": true,
"pngSizes": ["1200x630"],
"dimensions": { "width": 1200, "height": 630 }
}
},
"favicon": {
"dir": "favicon",
"ico": "favicon.ico",
"svg": "favicon.svg",
"svgLight": "favicon-light.svg",
"svgDark": "favicon-dark.svg",
"apple": "apple-touch-icon.png",
"maskable512": "maskable-512.png",
"png": { "16": "favicon-16.png", "32": "favicon-32.png", "48": "favicon-48.png" },
"androidChrome": { "192": "android-chrome-192.png", "512": "android-chrome-512.png" }
},
"manifest": "/manifest.webmanifest"
}
}Fields
brand.rootstringrequiredAbsolute URL prefix for every asset. All assets.*.dir paths resolve under this prefix. Example: "/brand-assets" means the wordmark SVG lives at /brand-assets/wordmark/acme-wordmark.svg.
brand.namestringrequiredSlug-form brand name. Used in generated filenames + metadata.
brand.shortNamestringPWA short name (shown on Android / iOS home screens when the docs site is installed).
brand.descriptionstringPWA manifest description + generic fallback description when seo.description is absent.
brand.colorsBrandColorPairThemed colours used by the platform for generated assets:
brand.light/brand.dark— the primary accent pair (same values you'd set incolors.light/colors.dark, duplicated here so brand pipelines have a standalone source).text.light/text.dark— default body text colour for generated OG images.background.light/background.dark— page background colour for OG images.
brand.themeColor{ light: string; dark: string }Browser chrome colour (<meta name="theme-color">). Controls the mobile Safari/Chrome top bar colour per light/dark mode.
brand.backgroundColor{ light: string; dark: string }PWA manifest background_color. Shown while the docs site is loading in a PWA shell.
brand.typographyBrandTypographyDefault site typography. family + weight + fallback are emitted as the base font-family CSS variable.
brand.assetsBrandAssetPattern mapFour required asset families, each described by a BrandAssetPattern:
wordmark— full logo with text. Sizes typed as height (h40→ 40px tall).icon— square app icon. Sizes typed as pixel dimensions (32→ 32×32).maskable— PWA maskable icon (safe-area padding included). Sizes192,512,1024.og— Open Graph preview image. Must carrydimensions: { width, height }(usually 1200×630).
Each pattern:
dir— subfolder underbrand.root.file— filename stem (no extension, no size suffix).hasSvg— settrueif an SVG version is present under{root}/{dir}/{file}.svg.hasTheme— settrueif you ship separate light/dark SVG variants ({file}-light.svg,{file}-dark.svg).pngSizes— array of PNG sizes you've generated; the renderer links to each as{file}-{size}.png.
brand.faviconBrandFaviconSetThe block is optional; when present, dir is required — it is the directory
every filename below resolves against. Each entry names a file you ship, and
the ones you do not ship are simply left out.
Favicons follow a standard layout:
ico— classicfavicon.ico(16x16, 32x32, 48x48 multi-size).svg— adaptive SVG favicon (uses@media (prefers-color-scheme)internally).svgLight/svgDark— explicit per-scheme SVG for browsers that want a strict media query.png—{16, 32, 48}PNGs for legacy browsers.apple—apple-touch-icon.png(180x180).androidChrome—{192, 512}PNGs for the PWA.maskable512— maskable 512x512 PNG (separate from the regular 512 because of safe-area padding).
brand.manifeststringAbsolute path to the .webmanifest file. Usually /manifest.webmanifest at your repo root.
Building the asset set — exact files, sizes, folder
Everything lives under {brand.root}/ in your repo's public/ folder, in five
subfolders. This is exactly how NookDocs ships its own brand (the live
reference). With "root": "/brand-assets" and the name-stemmed example
acme:
public/brand-assets/
├── manifest/
│ └── manifest.json ← brand.manifest → "/brand-assets/manifest/manifest.json"
├── favicon/ ← brand.favicon — RENDERED on every page (tab + PWA icons)
│ ├── favicon.svg ← adaptive (light+dark via inline @media)
│ ├── favicon-light.svg ← explicit light
│ ├── favicon-dark.svg ← explicit dark
│ ├── favicon.ico ← 16+32+48 multi-res
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-48x48.png
│ ├── apple-touch-icon.png ← 180×180 (iOS home screen)
│ ├── android-chrome-192x192.png ← 192×192 (Android / PWA)
│ ├── android-chrome-512x512.png ← 512×512 (PWA splash)
│ └── maskable-icon-512.png ← 512×512 (safe-area padding)
├── icon/ ← brand.assets.icon — square app icon, themed
│ ├── acme-icon-light.svg
│ ├── acme-icon-dark.svg
│ ├── acme-icon-light-16.png … acme-icon-light-1024.png
│ └── acme-icon-dark-16.png … acme-icon-dark-1024.png
├── maskable/ ← brand.assets.maskable — padded PWA icon, themed
│ ├── acme-maskable-light.svg / acme-maskable-dark.svg
│ └── acme-maskable-{light,dark}-{192,512,1024}.png
├── og/ ← brand.assets.og — social preview, themed
│ ├── acme-og-light.svg / acme-og-dark.svg
│ └── acme-og-{light,dark}-1200x630.png
└── wordmark/ ← brand.assets.wordmark — full logo, themed (by height)
├── acme-wordmark-light.svg / acme-wordmark-dark.svg
└── acme-wordmark-{light,dark}-{h40,h80,h120,h240,h480}.pngNaming convention
Favicon dir uses fixed, standard names (the values you put in
brand.favicon). These are exactly what realfavicongenerator.net produces.Themed families (
icon/maskable/og/wordmark) follow{file}-{theme}.svgand{file}-{theme}-{size}.png, wherethemeislight/dark(becausehasTheme: true) andsizeis each entry in that family'spngSizes. Icon/maskable sizes are pixels; wordmark sizes are heights (h40= 40px tall); OG is1200x630.
What renders where
| Family | Rendered on tenant <head>? | Sizes | Light/Dark |
favicon + manifest | Yes — tab icon, iOS icon, PWA install | ico, 16/32/48, 180, 192, 512, maskable 512 | SVG light/dark; PNGs single |
icon | App/PWA icon sources | 16, 32, 48, 64, 96, 128, 180, 192, 256, 384, 512, 1024 | both |
maskable | Maskable PWA icon | 192, 512, 1024 | both |
og | Social preview | 1200×630 | both |
wordmark | Logo in richer layouts | h40, h80, h120, h240, h480 | both |
Only the favicon set + manifest are emitted into the page <head> for a
tenant site. The themed icon / maskable / og / wordmark families round
out a complete brand package (and feed richer layouts), but your visible
navbar logo comes from colors.logo and your
social preview can come from seo.ogImage. Ship the favicon
set for the essentials; add the rest for a full brand.
Generate from masters
Provide a light and a dark master SVG per family (square for
favicon/icon/maskable; wide for wordmark; 1200×630 for og), then scale with
rsvg-convert + imagemagick:
ROOT=public/brand-assets
# Favicon (single-variant PNGs; SVGs are light/dark) — from a square master
for s in 16x16 32x32 48x48; do rsvg-convert -w ${s%x*} -h ${s#*x} favicon.svg > $ROOT/favicon/favicon-$s.png; done
rsvg-convert -w 180 -h 180 favicon.svg > $ROOT/favicon/apple-touch-icon.png
rsvg-convert -w 192 -h 192 favicon.svg > $ROOT/favicon/android-chrome-192x192.png
rsvg-convert -w 512 -h 512 favicon.svg > $ROOT/favicon/android-chrome-512x512.png
rsvg-convert -w 512 -h 512 favicon-maskable.svg > $ROOT/favicon/maskable-icon-512.png
convert $ROOT/favicon/favicon-16x16.png $ROOT/favicon/favicon-32x32.png $ROOT/favicon/favicon-48x48.png $ROOT/favicon/favicon.ico
# Themed families — repeat per theme + size (icon shown; same loop for maskable/og/wordmark)
for t in light dark; do
for s in 16 32 48 64 96 128 180 192 256 384 512 1024; do
rsvg-convert -w $s -h $s acme-icon-$t.svg > $ROOT/icon/acme-icon-$t-$s.png
done
doneManifest
brand.manifest points at the .webmanifest you ship:
{
"name": "Acme Docs",
"short_name": "Acme",
"icons": [
{ "src": "/brand-assets/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/brand-assets/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/brand-assets/favicon/maskable-icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
],
"theme_color": "#0f172a",
"background_color": "#0f172a",
"display": "standalone"
}No brand block? Leave it out and your site uses
seo.favicon (a single SVG) for the tab icon — nothing
breaks. The brand block is the upgrade path to the full favicon + PWA +
maskable + OG set. The starter template ships without a brand block on
purpose — so add this folder + block only when you're ready.
Why this is a block, not individual keys
Older config formats scattered brand metadata across colors.logo.light, seo.ogImage, seo.favicon, theme.colors, PWA manifest fields, and Apple touch icon. Every feature had to reach into a different key to find the right asset, and cross-feature consistency was manual. The brand block is one source of truth — change the primary colour in one place and favicons, OG images, and themed wordmarks all track it.
The root-level colors.logo.light / colors.logo.dark are still accepted (backwards compat for projects that haven't adopted brand), but brand.assets.wordmark is the authoritative path when both are set.