feat(branding): multi-tenant brand naming + per-port email shell + auth UI continuity
Removes the last hardcoded "Port Nimara" references so a tenant cloning
the deploy with a fresh slug sees their own brand throughout.
Browser + native chrome:
- `generateMetadata` reads `branding_app_name` from the first port row
so the browser tab title, apple-web-app title, and template literal
reflect the tenant (fallback "CRM" until DB is seeded).
- Mobile topbar derives the brand-mark initials from the port slug
("port-nimara" → "PN", "marina-alpha" → "MA") — no code edit on clone.
- `documenso-payload` default redirect URL is `""` so Documenso falls
back to its own post-sign page instead of routing every tenant's
signers to portnimara.com; per-port `redirectUrl` setting still wins.
- Server-startup log uses generic "CRM server listening".
Email + auth shell:
- New `auth-shell-branding.ts` resolves logo / background / appName once
per request from `system_settings`; used by both the email shell and
the auth-pages SSR layout.
- `auth-branding-provider` wraps `/login`, `/reset-password`, `/set-password`,
portal `/portal/*` so the branded shell hydrates with the same assets
the inbox sees.
- `me/email` change email uses the branded shell instead of inline HTML
with "Port Nimara CRM" baked into copy.
- Admin branding page adds an email-preview card (POSTs to
`/api/v1/admin/branding/email-preview`) so an admin can spot-check
their templates before going live.
- `/api/public/files/[id]` exposes branding-category files anonymously
so inbox images (no session cookie) can render; any other category
still flows through authenticated `/api/v1/files/[id]/preview`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { getLocale, getMessages } from 'next-intl/server';
|
||||
import { Toaster } from 'sonner';
|
||||
import { classifyFormFactor } from '@/lib/form-factor';
|
||||
import { ReactGrabViewportSync } from '@/components/dev/react-grab-viewport-sync';
|
||||
import { resolveAuthShellBranding } from '@/lib/email/auth-shell-branding';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({
|
||||
@@ -28,26 +29,37 @@ export const viewport: Viewport = {
|
||||
themeColor: '#1e2844',
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'Port Nimara CRM',
|
||||
template: '%s | Port Nimara CRM',
|
||||
},
|
||||
description: 'Marina management system for Port Nimara',
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: 'black-translucent',
|
||||
title: 'Port Nimara',
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||
{ url: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||||
],
|
||||
apple: '/apple-touch-icon.png',
|
||||
},
|
||||
manifest: '/manifest.json',
|
||||
};
|
||||
/**
|
||||
* Resolve the browser tab title from the first-port `branding_app_name`
|
||||
* setting so a tenant's deploy sees their own brand in the title bar
|
||||
* (and in `Cmd+T` browser history). Falls back to a generic label when
|
||||
* the DB hasn't been seeded yet (e.g. fresh `pnpm dev` against an empty
|
||||
* database during onboarding).
|
||||
*/
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const branding = await resolveAuthShellBranding();
|
||||
const appName = branding?.appName?.trim() || 'CRM';
|
||||
return {
|
||||
title: {
|
||||
default: appName,
|
||||
template: `%s | ${appName}`,
|
||||
},
|
||||
description: `${appName} — marina management system`,
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: 'black-translucent',
|
||||
title: appName,
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||
{ url: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||||
],
|
||||
apple: '/apple-touch-icon.png',
|
||||
},
|
||||
manifest: '/manifest.json',
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const headerList = await headers();
|
||||
|
||||
Reference in New Issue
Block a user