Branding, UI and dashboard customization
Change the product identity and navigation while preserving the shared design and access boundaries.
Start with product identity
Set APP_NAME for the configured name, then review config/site.ts and config/marketing.ts. The former supplies a default description and server-consumed name. It does not rewrite all sample headings, legal text, illustrations or email subjects. Search the source for sample product names before launch, excluding environments, dependencies and generated files.
rg -n 'Forma|starter|sample' app components config content emailsReview each occurrence rather than doing an unrestricted replacement. Some references explain the starter to developers; others are customer-facing copy that should describe your product. Keep credentials and private setup state out of search output and screenshots.
Logo, favicon and social identity
components/brand.tsx renders the shared brand component. Replace or adapt its mark in that component; store approved public assets in public/. Change app/favicon.ico for browser tabs and the social image referenced by config/seo.ts. The SEO metadata guide explains canonical origins, page titles and sharing previews.
Workspace logos and personal avatars are user data, not public brand assets. Do not copy one customer's logo into the public directory as a global product icon. Their authenticated access paths are documented under Storage.
Preserve the neutral visual foundation
The installed shadcn preset is Base UI/Vega with neutral colors, Inter and Lucide. All 61 UI component files were intentionally installed. Use the existing primitives in components/ui and the semantic CSS variables in app/globals.css.
For a subtle radius adjustment, edit the shared --radius token and inspect cards, inputs, dialogs and buttons together. For a color adjustment, review both :root and .dark definitions of the relevant semantic pair. For example, a primary background needs a readable primary foreground. Avoid hardcoding a light background in one component that becomes unreadable in dark mode.
:root {
--radius: 0.75rem;
}This small example changes radius only. Keep the rest of the existing token declarations in place; do not replace the entire stylesheet with it. The documentation uses its own scoped app/docs/docs.css layered over shared tokens.
Add a protected page
Create app/dashboard/reports/page.tsx for a product-specific page. This minimal example displays the verified selected workspace; it does not claim a shipped reporting backend:
import { getWorkspace } from '@/lib/auth';
export default async function ReportsPage() {
const { organization } = await getWorkspace();
return (
<section className="grid gap-3 p-6">
<h1 className="text-2xl font-semibold">Reports</h1>
<p>Reports for {organization.name}.</p>
<p>No reports have been created yet.</p>
</section>
);
}The dashboard layout already establishes the application shell and onboarding flow. getWorkspace() confirms current account/workspace access for the page's data. Add actual data access through user-scoped clients and database policies when the feature becomes functional; a protected route alone does not isolate records.
Add the navigation item
The sidebar entries live in components/dashboard/app-shell.tsx. Import the chosen Lucide icon, then add an item beside the existing entries with href, label and icon:
{ href: '/dashboard/reports', label: 'Reports', icon: FileText }Import FileText from lucide-react in the same file. Verify its route exists, the active state is correct and the collapsed sidebar still exposes an accessible name. Use workspace notes for a complete create/list feature rather than leaving a sample page as a finished product claim.
Loading, error and empty states
A loading state should say what is loading. An empty state should explain the user's next permitted action. An error state should preserve entered data when possible and give a retry path. Avoid passing raw database/provider error payloads into customer UI.
Use route loading.tsx and client error.tsx boundaries for data-loading transitions. Handle validation failures as inline form state instead of throwing every rejected input into a route-wide error boundary. Disable duplicate submission while pending, but keep server/database retry protection too.
Responsive and keyboard checks
Inspect desktop and narrow mobile layouts, including the sidebar drawer, long workspace names, dialogs and tables. Check 320px and 390px widths for page-level horizontal overflow. Code blocks can scroll inside their container; the entire page should not require sideways navigation.
Use keyboard Tab and Shift+Tab, open dialogs and return focus to the invoking control. Inputs need labels and validation messages. Do not rely on an icon's shape or color alone to communicate state. Honor reduced-motion preferences if adding animation.
Verify branding across surfaces
Check the public homepage, login, dashboard, account menu, email templates, product docs, metadata and social image. Some branding is rendered server-side from APP_NAME; a Client Component should receive necessary public text through explicit props or an intentional public config, not import a secret-reading server module.
Run npm run typecheck, scoped lint and npm run build. Inspect changed UI in light/dark and mobile/desktop. These checks validate local presentation; they do not show a received email with the new brand. Complete that delivery check through email templates when an authorized sender and recipient are available.
Template 0.0.1 · September 2026 source snapshot