diff --git a/.env.example b/.env.example
index b3489568..ea4ad9ea 100644
--- a/.env.example
+++ b/.env.example
@@ -8,3 +8,4 @@ NUXT_PUBLIC_PRODUCT_DIRECTUS_URL=your_url_here
 NUXT_IMAGE_DOMAINS=your_url_here
 ALGOLIA_API_KEY=
 ALGOLIA_APPLICATION_ID=
+DIRECTUS_URL=
diff --git a/app/app.vue b/app/app.vue
index a82e3d8a..7f868345 100644
--- a/app/app.vue
+++ b/app/app.vue
@@ -2,6 +2,7 @@
 import { spec } from '@directus/openapi';
 import { useRoute } from 'vue-router';
 import { nextTick, watch } from 'vue';
+
 const route = useRoute();
 
 const { data: navigation } = useAsyncData('navigation', () => fetchContentNavigation());
@@ -19,11 +20,11 @@ const { data: files } = useLazyFetch<ParsedContent[]>('/api/search.json', { defa
 const updateLinks = () => {
 	nextTick(() => {
 		const links = document.querySelectorAll('a');
-		links.forEach(link => {
+		links.forEach((link) => {
 			const href = link.getAttribute('href');
 			if (
-				href?.startsWith('http') &&
-				link.hostname !== window.location.hostname
+				href?.startsWith('http')
+				&& link.hostname !== window.location.hostname
 			) {
 				link.setAttribute('target', '_blank');
 				link.setAttribute('rel', 'noopener noreferrer');
diff --git a/app/components/DocsBanner.vue b/app/components/DocsBanner.vue
new file mode 100644
index 00000000..73f61f63
--- /dev/null
+++ b/app/components/DocsBanner.vue
@@ -0,0 +1,64 @@
+<script setup lang="ts">
+const { data: banner } = useFetch('/api/banner', {
+
+});
+
+const dismissedBanners = useCookie('directus-dismissed-banners', {
+	default: () => [] as string[],
+});
+
+const bannerVisible = computed(() => {
+	if (!unref(banner)) return false;
+	return unref(dismissedBanners).includes(unref(banner)!.id) === false;
+});
+
+const dismiss = (id: string) => {
+	dismissedBanners.value = [...unref(dismissedBanners), id];
+};
+
+const iconName = computed(() => {
+	if (!unref(banner)) return null;
+	return getIconName(unref(banner)!.icon);
+});
+</script>
+
+<template>
+	<div
+		v-if="banner && bannerVisible"
+		class="bg-foreground cursor-pointer  h-8"
+	>
+		<UContainer class="h-full flex items-center gap-x-4">
+			<NuxtLink
+				class="flex-grow h-full flex items-center text-background no-underline text-xs leading-xs font-semibold group"
+				:href="banner.link ?? undefined"
+			>
+				<Icon
+					v-if="iconName"
+					class="mr-2 size-5"
+					:name="iconName"
+				/>
+				<span
+					class="whitespace-nowrap overflow-hidden text-ellipsis"
+					v-html="banner.content"
+				/>
+				<Icon
+					class="hidden md:block transform duration-150 ease-out ml-1 group-hover:translate-x-1 size-5"
+					name="material-symbols:arrow-forward"
+				/>
+			</NuxtLink>
+
+			<button
+				aria-label="Close"
+				class="text-background"
+				:padded="false"
+				icon="material-symbols:close"
+				@click="dismiss(banner.id)"
+			>
+				<Icon
+					name="material-symbols:close"
+					class="size-5"
+				/>
+			</button>
+		</UContainer>
+	</div>
+</template>
diff --git a/app/components/DocsHeader.vue b/app/components/DocsHeader.vue
index c04b904c..f7355d3d 100644
--- a/app/components/DocsHeader.vue
+++ b/app/components/DocsHeader.vue
@@ -115,6 +115,10 @@ const algoliaNavigator = {
 		:links="links"
 		:ui="route.path.startsWith('/api') ? { container: 'max-w-screen' } : {}"
 	>
+		<template #top>
+			<DocsBanner />
+		</template>
+
 		<template #logo>
 			<LogoDocs class="w-auto h-8 shrink-0" />
 		</template>
diff --git a/app/utils/icons.ts b/app/utils/icons.ts
new file mode 100644
index 00000000..c67fd1c2
--- /dev/null
+++ b/app/utils/icons.ts
@@ -0,0 +1,74 @@
+const filledIcons = [
+	'all_inclusive',
+	'apartment',
+	'api',
+	'api',
+	'arrow_back',
+	'arrow_forward',
+	'arrow_outward',
+	'autopay',
+	'autostop',
+	'avg_pace',
+	'cached',
+	'check',
+	'checklist',
+	'checklist_rtl',
+	'close',
+	'cloudy',
+	'code',
+	'compare_arrows',
+	'cruelty_free',
+	'dynamic_feed',
+	'electrical_services',
+	'emoji_people',
+	'expand_less',
+	'expand_more',
+	'full_stacked_bar_chart',
+	'functions',
+	'globe_uk',
+	'home_app_logo',
+	'horizontal_rule',
+	'laps',
+	'link',
+	'image_search',
+	'insights',
+	'login',
+	'menu_rounded',
+	'money_off',
+	'monitoring',
+	'online_prediction',
+	'open_in_new',
+	'password',
+	'partner_exchange',
+	'post_add',
+	'public',
+	'published_with_changes',
+	'query_stats',
+	'repeat',
+	'search',
+	'security',
+	'sort_by_alpha',
+	'sports_martial_arts',
+	'support',
+	'sync_alt',
+	'timeline',
+	'translate',
+	'trending_up',
+	'update',
+	'webhook',
+	'work',
+	'support_agent',
+	'edit_sharp',
+];
+
+export function getIconName(name: string): string | undefined {
+	if (!name) return;
+	// Convert the icon coming from the API to the name of the icon component
+	// Directus uses Google Material Icons and the icon values are snake_case (e.g. "account_circle")
+	const prefix = 'material-symbols:';
+	// Change snake case to kebab case
+	const kebabCase = name.replace(/_/g, '-');
+	// If the icon is one of the filled icons, do not add the suffix '-outline'. Needed because of descrepancies between the Google Material Font we use in Directus icon interface and the Iconify library.
+	const iconName = prefix + kebabCase + (filledIcons.includes(name) ? '' : '-outline');
+	return iconName;
+}
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 9ca540b3..0e1684bd 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -97,6 +97,7 @@ export default defineNuxtConfig({
 				},
 			},
 		},
+		directusUrl: process.env.DIRECTUS_URL,
 	},
 
 	build: {
diff --git a/package.json b/package.json
index 2c355587..595c6352 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
 	},
 	"dependencies": {
 		"@directus/openapi": "0.1.15",
+		"@directus/sdk": "^19.1.0",
 		"@docsearch/css": "3.8.3",
 		"@docsearch/js": "3.8.3",
 		"@iconify-json/heroicons-outline": "1.2.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dd24bf0b..a1893d5f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
       '@directus/openapi':
         specifier: 0.1.15
         version: 0.1.15
+      '@directus/sdk':
+        specifier: ^19.1.0
+        version: 19.1.0
       '@docsearch/css':
         specifier: 3.8.3
         version: 3.8.3
@@ -410,6 +413,10 @@ packages:
   '@directus/openapi@0.1.15':
     resolution: {integrity: sha512-QGM3dWVRxpLhA2qi83MLNewxHIjY5PFftdCnssEpgD5IEAO1+s9z2F7zpEtMOKLcnRoH4H2+ttKN6bKlbV+bIw==}
 
+  '@directus/sdk@19.1.0':
+    resolution: {integrity: sha512-Nqem9BsvvGyVtAa69mGPtoMoMVkZxdIREdsWvvTzNF4/1XqaFfEiFL7PhtUNfc46/Nufus2+QUKYQbNiAWe3ZA==}
+    engines: {node: '>=22'}
+
   '@docsearch/css@3.8.3':
     resolution: {integrity: sha512-1nELpMV40JDLJ6rpVVFX48R1jsBFIQ6RnEQDsLFGmzOjPWTOMlZqUcXcvRx8VmYV/TqnS1l784Ofz+ZEb+wEOQ==}
 
@@ -7158,6 +7165,8 @@ snapshots:
 
   '@directus/openapi@0.1.15': {}
 
+  '@directus/sdk@19.1.0': {}
+
   '@docsearch/css@3.8.3': {}
 
   '@docsearch/js@3.8.3(@algolia/client-search@5.19.0)(search-insights@2.17.3)':
diff --git a/server/api/banner.get.ts b/server/api/banner.get.ts
new file mode 100644
index 00000000..279b7125
--- /dev/null
+++ b/server/api/banner.get.ts
@@ -0,0 +1,26 @@
+export default defineCachedEventHandler(async () => {
+	try {
+		const [data] = await directusServer.request(readItems('site_banners', {
+			fields: ['id', 'icon', 'content', 'link'],
+			filter: {
+				show_on: {
+					// @ts-expect-error - _contains works for csv fields in Directus
+					_contains: 'docs',
+				},
+			},
+			sort: ['-date_created'],
+			limit: 1,
+		}));
+
+		if (!data) return null;
+
+		return data;
+	}
+	catch (error) {
+		console.error(error);
+		return null;
+	}
+}, {
+	maxAge: 60 * 5, // 5 minutes
+	swr: true,
+});
diff --git a/server/utils/directus-server.ts b/server/utils/directus-server.ts
new file mode 100644
index 00000000..4af91d76
--- /dev/null
+++ b/server/utils/directus-server.ts
@@ -0,0 +1,26 @@
+import { $fetch } from 'ofetch';
+import {
+	createDirectus,
+	readItem,
+	readItems,
+	rest,
+	type QueryFilter,
+} from '@directus/sdk';
+import type { Schema } from '#shared/types/schema';
+
+const {
+	directusUrl,
+} = useRuntimeConfig();
+
+const directusServer = createDirectus<Schema>(directusUrl as string, {
+	globals: {
+		fetch: $fetch,
+	},
+}).with(rest());
+
+export {
+	directusServer,
+	readItem,
+	readItems,
+};
+export type { QueryFilter };
diff --git a/shared/types/schema.ts b/shared/types/schema.ts
new file mode 100644
index 00000000..cc2f904e
--- /dev/null
+++ b/shared/types/schema.ts
@@ -0,0 +1,16 @@
+export interface SiteBanner {
+	id: string;
+	status: string;
+	sort: number | null;
+	user_created: string | null;
+	date_created: string | null;
+	user_updated: string | null;
+	date_updated: string | null;
+	link: string | null;
+	content: string | null;
+	icon: string;
+	show_on: string[];
+}
+export interface Schema {
+	site_banners: SiteBanner[];
+}
