From 77155d30beacc92fb3fd746455931a90bf9c81df Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sun, 15 Feb 2026 10:21:11 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20Domain-based=20routing=20=E2=80=94=20ma?= =?UTF-8?q?rketing=20site=20at=20bare=20domain,=20panel=20at=20subdomain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit corrosionmgmt.com now serves LandingView as the default page with marketing routes at root level. panel.corrosionmgmt.com continues serving the admin panel unchanged. /site/* backward compat via redirects on marketing domain. - nginx: Add bare domain server block (only proxies /api/early-access/) - router: Detect hostname at module load, generate domain-specific routes - MarketingLayout: Named routes for nav, external tags for auth links - LandingView: CTAs point to panel domain via VITE_PANEL_URL Co-Authored-By: Claude Opus 4.6 --- .env.example | 3 + docker/nginx.conf | 22 +++ .../src/components/layout/MarketingLayout.vue | 26 ++-- frontend/src/router/index.ts | 129 +++++++++++++----- frontend/src/views/marketing/LandingView.vue | 11 +- 5 files changed, 137 insertions(+), 54 deletions(-) diff --git a/.env.example b/.env.example index efdf7b3..738916e 100644 --- a/.env.example +++ b/.env.example @@ -39,3 +39,6 @@ SMTP_FROM=noreply@corrosionmgmt.com # Server API_PORT=3000 FRONTEND_URL=http://localhost:5174 + +# Frontend (Vite — must be prefixed with VITE_) +VITE_PANEL_URL=https://panel.corrosionmgmt.com diff --git a/docker/nginx.conf b/docker/nginx.conf index 6c6f19d..18c8895 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -67,6 +67,28 @@ http { } } + # Marketing site — corrosionmgmt.com (bare domain) + server { + listen 80; + server_name corrosionmgmt.com; + + # Early access signup API + location /api/early-access/ { + limit_req zone=api burst=10 nodelay; + proxy_pass http://api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SPA + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } + } + # Wildcard server — *.corrosionmgmt.com (public server sites) server { listen 80; diff --git a/frontend/src/components/layout/MarketingLayout.vue b/frontend/src/components/layout/MarketingLayout.vue index 966c9fb..3f19ec2 100644 --- a/frontend/src/components/layout/MarketingLayout.vue +++ b/frontend/src/components/layout/MarketingLayout.vue @@ -1,5 +1,7 @@ @@ -7,19 +9,19 @@ import { RouterView, RouterLink } from 'vue-router' - + Corrosion - How It Works - Pricing - Roadmap - FAQ + How It Works + Pricing + Roadmap + FAQ - Sign In - Get Started + Sign In + Get Started @@ -36,22 +38,22 @@ import { RouterView, RouterLink } from 'vue-router' Product - How It Works - Pricing - Roadmap + How It Works + Pricing + Roadmap Support - FAQ + FAQ Discord Company - About + About Status diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 8fad6b5..b9ff5a1 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,7 +1,53 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' import { useAuthStore } from '@/stores/auth' -const routes: RouteRecordRaw[] = [ +// --------------------------------------------------------------------------- +// Domain detection — runs once at module load +// --------------------------------------------------------------------------- +const hostname = typeof window !== 'undefined' ? window.location.hostname : '' +const isMarketingDomain = hostname === 'corrosionmgmt.com' + +// --------------------------------------------------------------------------- +// Marketing page children — shared between both domain route sets +// --------------------------------------------------------------------------- +const marketingChildren: RouteRecordRaw[] = [ + { + path: '', + name: 'landing', + component: () => import('@/views/marketing/LandingView.vue'), + }, + { + path: 'pricing', + name: 'pricing', + component: () => import('@/views/marketing/PricingView.vue'), + }, + { + path: 'how-it-works', + name: 'how-it-works', + component: () => import('@/views/marketing/HowItWorksView.vue'), + }, + { + path: 'faq', + name: 'faq', + component: () => import('@/views/marketing/FaqView.vue'), + }, + { + path: 'roadmap', + name: 'roadmap', + component: () => import('@/views/marketing/RoadmapView.vue'), + }, + { + path: 'early-access', + name: 'early-access', + component: () => import('@/views/marketing/EarlyAccessView.vue'), + }, +] + +// --------------------------------------------------------------------------- +// Panel domain routes — panel.corrosionmgmt.com, localhost, etc. +// Existing behavior, unchanged. +// --------------------------------------------------------------------------- +const panelRoutes: RouteRecordRaw[] = [ // Auth routes (no layout) { path: '/login', @@ -113,7 +159,7 @@ const routes: RouteRecordRaw[] = [ name: 'settings', component: () => import('@/views/admin/SettingsView.vue'), }, - // Platform Admin views (super-admin only, guarded in components) + // Platform Admin views (super-admin only) { path: 'admin', name: 'platform-admin', @@ -165,42 +211,11 @@ const routes: RouteRecordRaw[] = [ ], }, - // Marketing site (public, no auth) + // Marketing site (accessible on panel domain at /site/*) { path: '/site', component: () => import('@/components/layout/MarketingLayout.vue'), - children: [ - { - path: '', - name: 'landing', - component: () => import('@/views/marketing/LandingView.vue'), - }, - { - path: 'pricing', - name: 'pricing', - component: () => import('@/views/marketing/PricingView.vue'), - }, - { - path: 'how-it-works', - name: 'how-it-works', - component: () => import('@/views/marketing/HowItWorksView.vue'), - }, - { - path: 'faq', - name: 'faq', - component: () => import('@/views/marketing/FaqView.vue'), - }, - { - path: 'roadmap', - name: 'roadmap', - component: () => import('@/views/marketing/RoadmapView.vue'), - }, - { - path: 'early-access', - name: 'early-access', - component: () => import('@/views/marketing/EarlyAccessView.vue'), - }, - ], + children: marketingChildren, }, // Status page @@ -211,12 +226,52 @@ const routes: RouteRecordRaw[] = [ }, ] +// --------------------------------------------------------------------------- +// Marketing domain routes — corrosionmgmt.com (bare domain) +// Marketing pages at root. No admin/auth routes. +// --------------------------------------------------------------------------- +const marketingRoutes: RouteRecordRaw[] = [ + // Marketing layout at / + { + path: '/', + component: () => import('@/components/layout/MarketingLayout.vue'), + children: marketingChildren, + }, + + // Backward compat: /site/* → root-level equivalents + { + path: '/site/:pathMatch(.*)*', + redirect: (to) => { + const sub = to.params.pathMatch + if (!sub || (Array.isArray(sub) && sub.length === 0)) return '/' + const subPath = Array.isArray(sub) ? sub.join('/') : sub + return subPath ? `/${subPath}` : '/' + }, + }, + + // Status page + { + path: '/status', + name: 'status', + component: () => import('@/views/public/StatusPageView.vue'), + }, + + // Catch-all: unknown routes → landing page + { + path: '/:pathMatch(.*)*', + redirect: '/', + }, +] + +// --------------------------------------------------------------------------- +// Router instance +// --------------------------------------------------------------------------- const router = createRouter({ history: createWebHistory(), - routes, + routes: isMarketingDomain ? marketingRoutes : panelRoutes, }) -// Auth guard +// Auth guard — only meaningful on panel domain (marketing has no requiresAuth routes) router.beforeEach((to, _from, next) => { const auth = useAuthStore() diff --git a/frontend/src/views/marketing/LandingView.vue b/frontend/src/views/marketing/LandingView.vue index 45ffb3b..6d1d2d0 100644 --- a/frontend/src/views/marketing/LandingView.vue +++ b/frontend/src/views/marketing/LandingView.vue @@ -1,6 +1,7 @@ @@ -16,9 +17,9 @@ import { Shield, Zap, RefreshCw, Terminal, Users, Wifi, Server, ChevronRight } f Deploy once. Automate everything. Never SSH again.
Stop babysitting your server.
Start orchestrating it.