From c4141802cc689d7b238852d39fdaa5fd692e8c54 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Feb 2026 09:45:31 +0000 Subject: [PATCH] Host Open Notebook at /opennotebook with wildcard subdomain support Add /opennotebook route embedding Open Notebook via iframe with space-aware nav breadcrumb. Middleware detects subdomain (e.g. cca.rnotes.online) and sets rnotes-space cookie. /ai redirects to /opennotebook. Traefik wildcard router at priority 100 catches *.rnotes.online subdomains. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 5 +++ src/app/ai/page.tsx | 47 ++--------------------------- src/app/opennotebook/page.tsx | 57 +++++++++++++++++++++++++++++++++++ src/lib/space-context.ts | 36 ++++++++++++++++++++++ src/middleware.ts | 27 +++++++++++++++++ 5 files changed, 127 insertions(+), 45 deletions(-) create mode 100644 src/app/opennotebook/page.tsx create mode 100644 src/lib/space-context.ts create mode 100644 src/middleware.ts diff --git a/docker-compose.yml b/docker-compose.yml index 4e8de75..4d40d41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,11 @@ services: - "traefik.http.routers.rnotes.entrypoints=web" - "traefik.http.routers.rnotes.priority=130" - "traefik.http.services.rnotes.loadbalancer.server.port=3000" + # Wildcard subdomain routing (e.g. cca.rnotes.online) + - "traefik.http.routers.rnotes-wildcard.rule=HostRegexp(`{sub:[a-z0-9-]+}.rnotes.online`)" + - "traefik.http.routers.rnotes-wildcard.entrypoints=web" + - "traefik.http.routers.rnotes-wildcard.priority=100" + - "traefik.http.routers.rnotes-wildcard.service=rnotes" networks: - traefik-public - rnotes-internal diff --git a/src/app/ai/page.tsx b/src/app/ai/page.tsx index 8907483..a4fb24c 100644 --- a/src/app/ai/page.tsx +++ b/src/app/ai/page.tsx @@ -1,48 +1,5 @@ -'use client'; - -import Link from 'next/link'; -import { OpenNotebookEmbed } from '@/components/OpenNotebookEmbed'; -import { UserMenu } from '@/components/UserMenu'; -import { SearchBar } from '@/components/SearchBar'; +import { redirect } from 'next/navigation'; export default function AIPage() { - return ( -
- - -
- -
-
- ); + redirect('/opennotebook'); } diff --git a/src/app/opennotebook/page.tsx b/src/app/opennotebook/page.tsx new file mode 100644 index 0000000..79062fc --- /dev/null +++ b/src/app/opennotebook/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +import Link from 'next/link'; +import { OpenNotebookEmbed } from '@/components/OpenNotebookEmbed'; +import { UserMenu } from '@/components/UserMenu'; +import { SearchBar } from '@/components/SearchBar'; +import { useSpaceContext } from '@/lib/space-context'; + +export default function OpenNotebookPage() { + const space = useSpaceContext(); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/src/lib/space-context.ts b/src/lib/space-context.ts new file mode 100644 index 0000000..316f0b3 --- /dev/null +++ b/src/lib/space-context.ts @@ -0,0 +1,36 @@ +'use client'; + +import { useSyncExternalStore } from 'react'; + +function getCookie(name: string): string | null { + if (typeof document === 'undefined') return null; + const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); + return match ? decodeURIComponent(match[1]) : null; +} + +function subscribe(callback: () => void) { + // Re-check on visibility change (covers navigation) + document.addEventListener('visibilitychange', callback); + return () => document.removeEventListener('visibilitychange', callback); +} + +function getSnapshot(): string | null { + return getCookie('rnotes-space'); +} + +function getServerSnapshot(): string | null { + return null; +} + +export function useSpaceContext(): string | null { + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); +} + +export function getSpaceFromHeaders(headers: Headers): string | null { + const host = headers.get('host') || ''; + const match = host.match(/^([a-z0-9-]+)\.rnotes\.online$/); + if (match && !['www', 'opennotebook', 'api'].includes(match[1])) { + return match[1]; + } + return null; +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..fe6ea76 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,27 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +const RESERVED_SUBDOMAINS = new Set(['www', 'opennotebook', 'api']); + +export function middleware(request: NextRequest) { + const host = request.headers.get('host') || ''; + const match = host.match(/^([a-z0-9-]+)\.rnotes\.online$/); + + if (match && !RESERVED_SUBDOMAINS.has(match[1])) { + const space = match[1]; + const response = NextResponse.next(); + response.cookies.set('rnotes-space', space, { + path: '/', + httpOnly: false, + sameSite: 'lax', + secure: true, + }); + return response; + } + + return NextResponse.next(); +} + +export const config = { + matcher: ['/((?!_next/static|_next/image|favicon.ico|icon.svg).*)'], +};