From 2cc63c9b23a5903b77f4b83c7996b4a1ec50f358 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 21 Mar 2026 21:19:35 +0000 Subject: [PATCH] feat: migrate routing to rspace.online/rcal basePath Move from standalone rcal.online domain to path-based routing under rspace.online/rcal. Updates Traefik labels for primary path-based routing while keeping subdomain routing for spaces on rcal.online. Adds basePath /rcal to next.config.js and updates middleware to handle both rcal.online and rspace.online subdomain patterns. Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.yml | 13 +++++++++++-- next.config.js | 1 + src/middleware.ts | 29 ++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b900d03..a1b6e3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,9 @@ services: - INFISICAL_CLIENT_ID=${INFISICAL_CLIENT_ID} - INFISICAL_CLIENT_SECRET=${INFISICAL_CLIENT_SECRET} - INFISICAL_PROJECT_SLUG=rcal-online + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} + - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + - GOOGLE_OAUTH_REDIRECT_URI=https://rspace.online/rcal/api/auth/google/callback depends_on: rcal-postgres: condition: service_healthy @@ -23,10 +26,16 @@ services: - /tmp labels: - "traefik.enable=true" - - "traefik.http.routers.rcal.rule=Host(`rcal.jeffemmett.com`) || Host(`rcal.online`) || Host(`www.rcal.online`) || Host(`booking.xhiva.art`) || HostRegexp(`{subdomain:[a-z0-9-]+}.rcal.online`)" - - "traefik.http.routers.rcal.priority=130" + # Primary: path-based routing under rspace.online + - "traefik.http.routers.rcal.rule=(Host(`rspace.online`) || HostRegexp(`{subdomain:[a-z0-9-]+}.rspace.online`)) && PathPrefix(`/rcal`)" + - "traefik.http.routers.rcal.priority=140" - "traefik.http.routers.rcal.entrypoints=web" - "traefik.http.services.rcal.loadbalancer.server.port=3000" + # Subdomain routing for spaces: {space}.rcal.online + - "traefik.http.routers.rcal-spaces.rule=HostRegexp(`{subdomain:[a-z0-9-]+}.rcal.online`)" + - "traefik.http.routers.rcal-spaces.priority=130" + - "traefik.http.routers.rcal-spaces.entrypoints=web" + - "traefik.http.routers.rcal-spaces.service=rcal" networks: - traefik-public - rcal-internal diff --git a/next.config.js b/next.config.js index 248c251..0ab3684 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,7 @@ const nextConfig = { reactStrictMode: true, output: 'standalone', + basePath: '/rcal', } module.exports = nextConfig diff --git a/src/middleware.ts b/src/middleware.ts index d5247da..3b9ec31 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -5,10 +5,11 @@ import type { NextRequest } from 'next/server'; * Middleware to handle subdomain-based routing. * * Routes: - * - rcal.online -> landing page (/) - * - www.rcal.online -> landing page (/) + * - rspace.online/rcal/* -> primary (basePath handles this) + * - rcal.online -> redirected by rspace-redirects to rspace.online/rcal * - demo.rcal.online -> calendar demo (/demo) * - .rcal.online -> rewrite to /s/ + * - .rspace.online/rcal/* -> rewrite to /s/ * * Also handles localhost for development. */ @@ -18,12 +19,22 @@ export function middleware(request: NextRequest) { let subdomain: string | null = null; - // Match production: .rcal.online - const match = hostname.match(/^([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9])\.\w+\.online/); - if (match && match[1] !== 'www') { - subdomain = match[1]; - } else if (hostname.includes('localhost')) { - // Development: .localhost:port + // Match subdomain from rcal.online: .rcal.online + const rcalMatch = hostname.match(/^([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9])\.rcal\.online/); + if (rcalMatch && rcalMatch[1] !== 'www') { + subdomain = rcalMatch[1]; + } + + // Match subdomain from rspace.online: .rspace.online + if (!subdomain) { + const rspaceMatch = hostname.match(/^([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9])\.rspace\.online/); + if (rspaceMatch && rspaceMatch[1] !== 'www' && rspaceMatch[1] !== 'registry') { + subdomain = rspaceMatch[1]; + } + } + + // Development: .localhost:port + if (!subdomain && hostname.includes('localhost')) { const parts = hostname.split('.localhost')[0].split('.'); if (parts.length > 0 && parts[0] !== 'localhost') { subdomain = parts[parts.length - 1]; @@ -31,7 +42,7 @@ export function middleware(request: NextRequest) { } if (subdomain && subdomain.length > 0) { - // demo.rcal.online → serve the calendar demo + // demo subdomain → serve the calendar demo if (subdomain === 'demo') { if (url.pathname === '/') { url.pathname = '/demo';