feat(branding): add @brand vite alias + migrate in-app img consumers

vite-branding.ts is the shared resolver. Exports BRAND_DIR (absolute,
defaults to ./branding/default) and brandAlias for spreading into each
vite config's resolve.alias map.

All 9 vite configs now spread brandAlias so `@brand/<file>` resolves
to the active brand dir at build time.

Migrates the four <img src="@/assets/logo.png"> consumers
(Login.vue, LoginDemo.vue, AppSidebar.vue, MobileDrawer.vue) to
@brand/logo.png. Unused Navbar.old.vue left as-is.

Build verified: dist/assets/logo-<hash>.png emits from the aliased
import. Future deployers point BRAND_DIR at their brand kit and the
in-app logo follows automatically.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-09 22:55:42 +02:00
commit eebb566323
14 changed files with 41 additions and 4 deletions

View file

@ -48,7 +48,7 @@ const isActive = (href: string) => {
<div class="flex h-16 shrink-0 items-center"> <div class="flex h-16 shrink-0 items-center">
<router-link to="/" class="flex items-center gap-2"> <router-link to="/" class="flex items-center gap-2">
<img <img
src="@/assets/logo.png" src="@brand/logo.png"
alt="Logo" alt="Logo"
class="h-8 w-8" class="h-8 w-8"
/> />

View file

@ -77,7 +77,7 @@ const navigateTo = (href: string) => {
<SheetHeader class="px-6 py-4 border-b border-border"> <SheetHeader class="px-6 py-4 border-b border-border">
<SheetTitle class="flex items-center gap-2"> <SheetTitle class="flex items-center gap-2">
<img <img
src="@/assets/logo.png" src="@brand/logo.png"
alt="Logo" alt="Logo"
class="h-8 w-8" class="h-8 w-8"
/> />

View file

@ -4,7 +4,7 @@
<!-- Logo and Title --> <!-- Logo and Title -->
<div class="text-center space-y-6"> <div class="text-center space-y-6">
<div class="flex justify-center"> <div class="flex justify-center">
<img src="@/assets/logo.png" alt="Logo" class="h-24 w-24 sm:h-32 sm:w-32" /> <img src="@brand/logo.png" alt="Logo" class="h-24 w-24 sm:h-32 sm:w-32" />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h1 class="text-3xl font-bold tracking-tight">Virtual Realm</h1> <h1 class="text-3xl font-bold tracking-tight">Virtual Realm</h1>

View file

@ -5,7 +5,7 @@
<!-- Welcome Section --> <!-- Welcome Section -->
<div class="text-center space-y-2 sm:space-y-4"> <div class="text-center space-y-2 sm:space-y-4">
<div class="flex justify-center"> <div class="flex justify-center">
<img src="@/assets/logo.png" alt="Logo" class="h-34 w-34 sm:h-42 sm:w-42 lg:h-50 lg:w-50" /> <img src="@brand/logo.png" alt="Logo" class="h-34 w-34 sm:h-42 sm:w-42 lg:h-50 lg:w-50" />
</div> </div>
<div class="space-y-1 sm:space-y-3"> <div class="space-y-1 sm:space-y-3">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">Welcome to the Virtual Realm</h1> <h1 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">Welcome to the Virtual Realm</h1>

19
vite-branding.ts Normal file
View file

@ -0,0 +1,19 @@
import { resolve } from 'node:path'
/**
* Absolute path to the active brand kit. Deployers point this at their
* own `branding/<name>/` directory (see branding/README.md).
*
* Defaults to the committed aiolabs default brand. Used by vite configs
* for the `@brand` import alias and by pwa-assets.config.ts.
*/
export const BRAND_DIR = resolve(process.env.BRAND_DIR ?? './branding/default')
/**
* Spread into a vite config's `resolve.alias` map. Lets components
* import deployer-provided assets via `@brand/<file>` instead of
* hardcoding `@/assets/logo.png`.
*/
export const brandAlias = {
'@brand': BRAND_DIR,
} as const

View file

@ -5,6 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
function chatHtmlPlugin(): Plugin { function chatHtmlPlugin(): Plugin {
return { return {
@ -103,6 +104,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/chat-app/app.config.ts', import.meta.url)), '@/app.config': fileURLToPath(new URL('./src/chat-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),

View file

@ -5,6 +5,7 @@ import { defineConfig } from 'vite'
import Inspect from 'vite-plugin-inspect' import Inspect from 'vite-plugin-inspect'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
// https://vite.dev/config/ // https://vite.dev/config/
// //
@ -43,6 +44,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
} }
}, },

View file

@ -5,6 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
/** /**
* Plugin to rewrite dev server requests to events.html * Plugin to rewrite dev server requests to events.html
@ -118,6 +119,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },

View file

@ -5,6 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
function forumHtmlPlugin(): Plugin { function forumHtmlPlugin(): Plugin {
return { return {
@ -103,6 +104,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/forum-app/app.config.ts', import.meta.url)), '@/app.config': fileURLToPath(new URL('./src/forum-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),

View file

@ -5,6 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
/** /**
* Plugin to rewrite dev server requests to libra.html * Plugin to rewrite dev server requests to libra.html
@ -110,6 +111,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
// ORDER MATTERS — @rollup/plugin-alias is first-match-wins. // ORDER MATTERS — @rollup/plugin-alias is first-match-wins.
// The more specific @/app.config remap must precede the @ prefix // The more specific @/app.config remap must precede the @ prefix
// alias, otherwise '@/app.config' matches '@' first and resolves // alias, otherwise '@/app.config' matches '@' first and resolves

View file

@ -5,6 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
function marketHtmlPlugin(): Plugin { function marketHtmlPlugin(): Plugin {
return { return {
@ -103,6 +104,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)), '@/app.config': fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),

View file

@ -5,6 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
function restaurantHtmlPlugin(): Plugin { function restaurantHtmlPlugin(): Plugin {
return { return {
@ -110,6 +111,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/restaurant-app/app.config.ts', import.meta.url)), '@/app.config': fileURLToPath(new URL('./src/restaurant-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),

View file

@ -5,6 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
function tasksHtmlPlugin(): Plugin { function tasksHtmlPlugin(): Plugin {
return { return {
@ -103,6 +104,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/tasks-app/app.config.ts', import.meta.url)), '@/app.config': fileURLToPath(new URL('./src/tasks-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),

View file

@ -5,6 +5,7 @@ import { defineConfig, type Plugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
import { brandAlias } from './vite-branding'
/** /**
* Plugin to rewrite dev server requests to wallet.html * Plugin to rewrite dev server requests to wallet.html
@ -109,6 +110,7 @@ export default defineConfig(({ mode }) => ({
], ],
resolve: { resolve: {
alias: { alias: {
...brandAlias,
// ORDER MATTERS — @/app.config must precede @ (first-match-wins). // ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/wallet-app/app.config.ts', import.meta.url)), '@/app.config': fileURLToPath(new URL('./src/wallet-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),