diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 30c807f..2a05155 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -4,6 +4,7 @@ import "./globals.css";
import { PluginProvider } from "@/lib/plugins/client";
import { getEnabledPluginKeys, syncPluginsFromRegistry } from "@/lib/plugins/server";
import { SeasonBanner } from "@/components/SeasonBanner";
+import { SiteHeaderGuard } from "@/components/SiteHeaderGuard";
import { LocaleProvider } from "@/lib/i18n/client";
import { dict, getLocale } from "@/lib/i18n/server";
@@ -102,6 +103,7 @@ export default async function RootLayout({
+
{children}
diff --git a/src/components/SignOutButton.tsx b/src/components/SignOutButton.tsx
new file mode 100644
index 0000000..9837157
--- /dev/null
+++ b/src/components/SignOutButton.tsx
@@ -0,0 +1,19 @@
+import { signOut } from "@/auth";
+
+export function SignOutButton() {
+ return (
+
+ );
+}
diff --git a/src/components/SiteHeader.tsx b/src/components/SiteHeader.tsx
new file mode 100644
index 0000000..5e88e57
--- /dev/null
+++ b/src/components/SiteHeader.tsx
@@ -0,0 +1,79 @@
+/**
+ * Header global affiché sur toutes les pages PUBLIQUES (hors /admin qui a son
+ * propre shell). Charge la session côté serveur pour adapter les liens.
+ */
+
+import Link from "next/link";
+
+import { auth } from "@/auth";
+import { UserRole } from "@/generated/prisma/enums";
+
+import { SignOutButton } from "./SignOutButton";
+
+export async function SiteHeader() {
+ const session = await auth();
+ const u = session?.user;
+ const isAdmin = u?.role === UserRole.ADMIN;
+ const isOwner = u?.role === UserRole.OWNER || isAdmin;
+
+ return (
+
+ );
+}
diff --git a/src/components/SiteHeaderGuard.tsx b/src/components/SiteHeaderGuard.tsx
new file mode 100644
index 0000000..6a1eddb
--- /dev/null
+++ b/src/components/SiteHeaderGuard.tsx
@@ -0,0 +1,26 @@
+/**
+ * N'affiche le SiteHeader QUE sur les pages publiques.
+ * Sur /admin, le shell admin a déjà sa propre TopBar + Sidebar.
+ * Sur /connexion et /inscription, on garde la page nue.
+ */
+
+import { headers } from "next/headers";
+
+import { SiteHeader } from "./SiteHeader";
+
+export async function SiteHeaderGuard() {
+ const h = await headers();
+ // Next.js 16 expose le pathname via le header x-pathname si on l'a posé,
+ // sinon on retombe sur next-url ou referer. On utilise une heuristique simple :
+ // pathname depuis x-invoke-path (Next internal) ou x-next-url-path-prefix.
+ const pathname =
+ h.get("x-pathname") ??
+ h.get("x-invoke-path") ??
+ h.get("next-url") ??
+ "";
+
+ if (pathname.startsWith("/admin")) return null;
+ if (pathname === "/connexion" || pathname === "/inscription") return null;
+
+ return ;
+}
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..db39867
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,23 @@
+/**
+ * Middleware Karbé.
+ *
+ * Pose `x-pathname` sur tous les requests pour que les server components puissent
+ * lire le path courant via `headers()` (utile pour SiteHeaderGuard qui décide
+ * de rendre ou non le header global selon /admin vs reste).
+ */
+
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+
+export function middleware(request: NextRequest) {
+ const response = NextResponse.next();
+ response.headers.set("x-pathname", request.nextUrl.pathname);
+ return response;
+}
+
+export const config = {
+ // Exclut les assets statiques + API auth (qu'on ne veut pas modifier).
+ matcher: [
+ "/((?!_next/static|_next/image|favicon.ico|api/auth|api/health|api/metrics).*)",
+ ],
+};