GenerateSaaS

Banners

Show announcement banners on marketing and dashboard surfaces, gated per surface by a config flag.

Announcement banners surface promos and notices across the app. They are gated by config.banner (per-surface booleans), with content defined per-app in apps/web-nuxt/app/config/banner.ts.

Feature flag

config.banner holds two independent booleans. A generated project ships them false (banners off); the demo build sets both true.

// packages/config/src/index.ts
banner: { marketing: false, dashboard: false },
  • When a surface flag is off, that banner never renders.
  • A surface also self-hides when its slot has no text, so emptying the copy removes it without touching the flag.

Configuring content

Define one slot per surface in bannerConfig (a BannerSlots). Icons are Phosphor components imported directly from @phosphor-icons/vue, text is a banner.* i18n key, and link accepts an internal path or external URL:

// apps/web-nuxt/app/config/banner.ts
import { PhGift, PhSparkle } from "@phosphor-icons/vue";

export const bannerConfig: BannerSlots<Component> = {
  marketing: {
    text: "banner.marketing", // i18n key
    icon: PhGift,
    variant: "primary",       // BannerVariant, default "primary"
    link: "/pricing",
  },
  dashboard: {
    text: "banner.dashboard",
    icon: PhSparkle,
    variant: "primary",
    link: "/settings/developers",
    // dashboard-only, all optional:
    // dismissible: true, roles: ["admin"], plans: ["pro"],
  },
};

Marketing vs dashboard

The two surfaces differ in lifecycle and capabilities:

AspectMarketingDashboard
AudiencePublic visitorsAuthenticated users
RenderingStatic, prerenderedResolved server-side per user
DismissableNo (always shown)Yes via dismissible (default true)
Dismiss persistence-dashboard_banner_dismissed cookie, keyed by content
TargetingNoneBy roles and/or plans
Slot typeBannerSlotDashboardBannerSlot

Slot fields

text, icon, variant, and link apply to both slots; the rest extend DashboardBannerSlot only:

KeyTypeDefaultDescription
textstring-A banner.* i18n key; absence hides the surface
iconComponent-Optional Phosphor icon component
variantBannerVariant"primary"primary / secondary / destructive / accent / muted, mapped to shadcn bg + fg classes
linkstring-Wraps the banner text. Internal paths route through localePath(); external URLs open in a new tab (rel="noopener noreferrer")
dismissiblebooleantrueDashboard only - show the ✕ and persist dismissal
rolesUserRole[]-Dashboard only - restrict to matching roles
plansstring[]-Dashboard only - restrict to matching plan ids

The dashboard layout resolves visibility server-side with useCookie plus the matchesBannerTargeting and bannerContentKey helpers from @repo/utils/helpers.

There is no separate maintenance-mode system. To announce downtime or critical notices, use a dashboard banner with a destructive variant and roles/plans targeting.

Frequently asked questions

How do I hide a banner? Set its surface to false in config.banner, or remove the slot's text - a slot with no text self-hides.

Why can't visitors dismiss the marketing banner? Marketing pages are prerendered with no per-user state, so the marketing banner is intentionally static and non-dismissable. Use a dashboard banner when you need dismissal.

How is a dismissed dashboard banner remembered? Dismissal is stored in the dashboard_banner_dismissed cookie, keyed by a hash of the banner's text, variant, and link (bannerContentKey). Editing any of those re-shows the banner to users who dismissed the old one.

Can I target a banner to specific users? Yes, on the dashboard surface only - set roles and/or plans, evaluated by matchesBannerTargeting (omitted or empty means "match everyone").

On this page