GenerateSaaS

Banners

Ship announcement bars on the marketing and dashboard surfaces, each toggled by config.banner, with role and plan targeting plus dismissal on the dashboard.

Announcement bars are gated per surface by config.banner, with content defined per-app in apps/web/config/banner.ts. The two surfaces are independent - ship a marketing announcement without touching the dashboard.

The flag

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

packages/config/src/index.ts
banner: { marketing: false, dashboard: false },

A surface renders nothing when its flag is false, and self-hides when its slot has no text - so emptying the copy removes it without touching the flag.

Marketing vs dashboard

AspectMarketingDashboard
Flagconfig.banner.marketingconfig.banner.dashboard
Slot typeBannerSlotDashboardBannerSlot
RenderingStatic, prerendered with the pageResolved server-side per user in the dashboard layout
DismissableNoYes (dismissible, default true)
TargetingEveryoneBy roles and/or plans
Use forLaunch notices, promos, global newsPlan-specific notices, upgrade nudges, role alerts

Both surfaces render the same "use client" <AnnouncementBanner>. The marketing layout passes serializable props only, so the page stays prerenderable.

Slot fields

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

FieldTypeDefaultNotes
textstring-A banner.* i18n key, rendered as plain text; copy lives in @repo/i18n
iconPhosphor Icon-Imported directly from @phosphor-icons/react/ssr, rendered weight="fill"
variantBannerVariant"primary"primary / secondary / destructive / accent / muted, mapped to shadcn bg and fg
linkstring-Internal path (next-intl Link) or external URL (opens in a new tab)
dismissiblebooleantrueDashboard only - renders the ✕
rolesUserRole[]all rolesDashboard only - resolved from the session
plansstring[]all plansDashboard only - resolves the viewer's plan id lazily, only when set
apps/web/config/banner.ts
export const bannerConfig: BannerSlots<Icon> = {
  marketing: {
    text: "banner.marketing", // i18n key
    icon: GiftIcon,
    variant: "primary",
    link: "/#pricing"
  },
  dashboard: {
    text: "banner.dashboard",
    icon: SparkleIcon,
    variant: "primary",
    link: "/settings/developers"
    // dismissible?: boolean  (default true)
    // roles?: UserRole[]     (omit = everyone)
    // plans?: string[]       (omit = every plan)
  }
};

Targeting and dismissal

Both resolve server-side in the dashboard layout via matchesBannerTargeting and bannerContentKey from @repo/utils/helpers, so the banner never flashes.

  • Targeting: an omitted or empty roles / plans list matches everyone; plans resolves the viewer's plan id only after the role check passes.
  • Dismissal: persists in the dashboard_banner_dismissed cookie for a year, keyed to the content - editing the text, variant, or link mints a new key and re-shows the banner to everyone who dismissed the old one.

There is no maintenance mode. To warn users about downtime or an incident, enable the dashboard banner with a destructive variant and target the relevant roles or plans.

On this page