diff --git a/.changeset/new-rocks-explode.md b/.changeset/new-rocks-explode.md
new file mode 100644
index 000000000..4038d69b1
--- /dev/null
+++ b/.changeset/new-rocks-explode.md
@@ -0,0 +1,5 @@
+---
+"gitbook": minor
+---
+
+Support site announcement banner
diff --git a/packages/gitbook/src/components/Announcement/Announcement.tsx b/packages/gitbook/src/components/Announcement/Announcement.tsx
new file mode 100644
index 000000000..0497ce3ab
--- /dev/null
+++ b/packages/gitbook/src/components/Announcement/Announcement.tsx
@@ -0,0 +1,32 @@
+import { resolveContentRef } from '@/lib/references';
+import type { GitBookSiteContext } from '@v2/lib/context';
+import { AnnouncementBanner } from './AnnouncementBanner';
+
+/**
+ * Server-side component to resolve content refs and pass down to client-side component
+ */
+export async function Announcement(props: {
+ context: GitBookSiteContext;
+}) {
+ const { context } = props;
+ const { customization } = context;
+
+ if (
+ !customization.announcement ||
+ !customization.announcement.enabled ||
+ !customization.announcement.message
+ ) {
+ return null;
+ }
+
+ const resolvedContentRef = customization.announcement?.link
+ ? await resolveContentRef(customization.announcement?.link?.to, context)
+ : null;
+
+ return (
+
+ );
+}
diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
new file mode 100644
index 000000000..cbfd49131
--- /dev/null
+++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
@@ -0,0 +1,126 @@
+'use client';
+
+import * as storage from '@/lib/local-storage';
+import type { ResolvedContentRef } from '@/lib/references';
+import { tcls } from '@/lib/tailwind';
+import type { CustomizationAnnouncement } from '@gitbook/api';
+import { Icon, type IconName } from '@gitbook/icons';
+import Link from 'next/link';
+import { CONTAINER_STYLE } from '../layout';
+import { linkStyles } from '../primitives';
+import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants';
+
+/**
+ * Client-side component to enable closing the banner
+ */
+export function AnnouncementBanner(props: {
+ announcement: CustomizationAnnouncement;
+ contentRef: ResolvedContentRef | null;
+}) {
+ const { announcement, contentRef } = props;
+
+ const hasLink = announcement.link && contentRef?.href;
+ const closeable = announcement.style !== 'danger';
+
+ const Tag = hasLink ? Link : 'div';
+ const style = BANNER_STYLES[announcement.style];
+
+ return (
+
+
+
+
+
+ {announcement.message}
+ {hasLink ? (
+
+ {contentRef?.icon ? (
+ {contentRef?.icon}
+ ) : null}
+ {announcement.link?.title && (
+ {announcement.link?.title}
+ )}
+
+
+ ) : null}
+
+
+ {closeable ? (
+
+ ) : null}
+
+
+ );
+}
+
+/**
+ * Dismiss the announcement banner and store the dismissal state in local storage.
+ * @see AnnouncementScript
+ */
+function dismissAnnouncement() {
+ storage.setItem(ANNOUNCEMENT_STORAGE_KEY, {
+ visible: false,
+ at: Date.now(),
+ });
+
+ document.documentElement.classList.add(ANNOUNCEMENT_CSS_CLASS);
+}
+
+const BANNER_STYLES = {
+ info: {
+ container: 'bg-info ring-info-subtle',
+ hover: 'hover:bg-info-hover active:bg-info-active',
+ icon: 'circle-info',
+ iconColor: 'text-info-subtle',
+ close: 'hover:bg-tint-base hover:ring-info-subtle',
+ link: '',
+ },
+ warning: {
+ container: 'bg-warning decoration-warning/6 ring-warning-subtle',
+ hover: 'hover:bg-warning-hover',
+ icon: 'circle-exclamation',
+ iconColor: 'text-warning-subtle',
+ close: 'hover:bg-tint-base hover:ring-warning-subtle',
+ link: 'links-default:text-warning links-default:hover:text-warning-strong links-default:decoration-warning/6 links-accent:decoration-warning',
+ },
+ danger: {
+ container: 'bg-danger decoration-danger/6 ring-danger-subtle',
+ hover: 'hover:bg-danger-hover',
+ icon: 'triangle-exclamation',
+ iconColor: 'text-danger-subtle',
+ close: 'hover:bg-tint-base hover:ring-danger-subtle',
+ link: 'links-default:text-danger links-default:hover:text-danger-strong links-default:decoration-danger/6 links-accent:decoration-danger',
+ },
+ success: {
+ container: 'bg-success decoration-success/6 ring-success-subtle',
+ hover: 'hover:bg-success-hover',
+ icon: 'circle-check',
+ iconColor: 'text-success-subtle',
+ close: 'hover:bg-tint-base hover:ring-success-subtle',
+ link: 'links-default:text-success links-default:hover:text-success-strong links-default:decoration-success/6 links-accent:decoration-success',
+ },
+};
diff --git a/packages/gitbook/src/components/Announcement/AnnouncementDismissedScript.tsx b/packages/gitbook/src/components/Announcement/AnnouncementDismissedScript.tsx
new file mode 100644
index 000000000..2956e55ba
--- /dev/null
+++ b/packages/gitbook/src/components/Announcement/AnnouncementDismissedScript.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import {
+ ANNOUNCEMENT_CSS_CLASS,
+ ANNOUNCEMENT_DAYS_TILL_RESET,
+ ANNOUNCEMENT_STORAGE_KEY,
+} from './constants';
+import { checkStorageForDismissedScript } from './script';
+
+/**
+ * Inject a script to read the local storage state for the announcement banner and apply the appropriate CSS class to the element as early as possible.
+ * Bypasses react state to prevent flickering.
+ */
+export function AnnouncementDismissedScript() {
+ const scriptArgs = JSON.stringify([
+ ANNOUNCEMENT_STORAGE_KEY,
+ ANNOUNCEMENT_DAYS_TILL_RESET,
+ ANNOUNCEMENT_CSS_CLASS,
+ ]).slice(1, -1);
+
+ return (
+
+ );
+}
diff --git a/packages/gitbook/src/components/Announcement/constants.ts b/packages/gitbook/src/components/Announcement/constants.ts
new file mode 100644
index 000000000..ceca2c207
--- /dev/null
+++ b/packages/gitbook/src/components/Announcement/constants.ts
@@ -0,0 +1,12 @@
+/**
+ * The local storage key for the announcement banner.
+ */
+export const ANNOUNCEMENT_STORAGE_KEY = '@gitbook/announcement';
+/**
+ * The CSS class to hide the announcement banner. Applies to the element.
+ */
+export const ANNOUNCEMENT_CSS_CLASS = 'announcement-hidden';
+/**
+ * The number of days until the announcement banner resets.
+ */
+export const ANNOUNCEMENT_DAYS_TILL_RESET = 7;
diff --git a/packages/gitbook/src/components/Announcement/index.ts b/packages/gitbook/src/components/Announcement/index.ts
new file mode 100644
index 000000000..46a6eead1
--- /dev/null
+++ b/packages/gitbook/src/components/Announcement/index.ts
@@ -0,0 +1,2 @@
+export * from './Announcement';
+export * from './AnnouncementDismissedScript';
diff --git a/packages/gitbook/src/components/Announcement/script.ts b/packages/gitbook/src/components/Announcement/script.ts
new file mode 100644
index 000000000..287e9fc8b
--- /dev/null
+++ b/packages/gitbook/src/components/Announcement/script.ts
@@ -0,0 +1,34 @@
+/**
+ * Read the local storage state for the announcement banner and apply the appropriate CSS class to the element.
+ *
+ * NOTE: this script is stringified and run in the browser, so it must be self-contained and have syntax supported in all browsers.
+ */
+export function checkStorageForDismissedScript(
+ storageKey: string,
+ daysTillReset: number,
+ cssClass: string
+) {
+ let showBanner = true;
+
+ try {
+ const announcementStateStr = window.localStorage.getItem(storageKey);
+ const announcementState = announcementStateStr
+ ? JSON.parse(announcementStateStr)
+ : undefined;
+
+ if (announcementState && !announcementState.visible) {
+ const dismissedAt = announcementState.at;
+ const nowTime = new Date().getTime();
+
+ // Check if enough days have passed since dismissal
+ const daysSinceDismissal = Math.floor((nowTime - dismissedAt) / (1000 * 60 * 60 * 24));
+ if (daysSinceDismissal < daysTillReset) {
+ showBanner = false;
+ }
+ }
+ } catch {}
+
+ if (!showBanner) {
+ document.documentElement.classList.add(cssClass);
+ }
+}
diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
index 24ba744ee..218dabac0 100644
--- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
+++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
@@ -34,6 +34,7 @@ import { ClientContexts } from './ClientContexts';
import '@gitbook/icons/style.css';
import './globals.css';
import { GITBOOK_FONTS_URL, GITBOOK_ICONS_TOKEN, GITBOOK_ICONS_URL } from '@v2/lib/env';
+import { AnnouncementDismissedScript } from '../Announcement';
/**
* Layout shared between the content and the PDF renderer.
@@ -96,6 +97,11 @@ export async function CustomizationRootLayout(props: {
{/* Inject custom font @font-face rules */}
{fontData.type === 'custom' ? : null}
+ {/* Inject a script to detect if the announcmeent banner has been dismissed */}
+ {'announcement' in customization && customization.announcement?.enabled ? (
+
+ ) : null}
+