feat: Domain-based routing — marketing site at bare domain, panel at subdomain

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 <a> tags for auth links
- LandingView: CTAs point to panel domain via VITE_PANEL_URL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-15 10:21:11 -05:00
parent 1c3aece4de
commit 77155d30be
5 changed files with 137 additions and 54 deletions

View File

@@ -39,3 +39,6 @@ SMTP_FROM=noreply@corrosionmgmt.com
# Server # Server
API_PORT=3000 API_PORT=3000
FRONTEND_URL=http://localhost:5174 FRONTEND_URL=http://localhost:5174
# Frontend (Vite — must be prefixed with VITE_)
VITE_PANEL_URL=https://panel.corrosionmgmt.com

View File

@@ -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) # Wildcard server — *.corrosionmgmt.com (public server sites)
server { server {
listen 80; listen 80;

View File

@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterView, RouterLink } from 'vue-router' import { RouterView, RouterLink } from 'vue-router'
const panelUrl = import.meta.env.VITE_PANEL_URL || ''
</script> </script>
<template> <template>
@@ -7,19 +9,19 @@ import { RouterView, RouterLink } from 'vue-router'
<!-- Navigation --> <!-- Navigation -->
<nav class="border-b border-neutral-800 bg-neutral-950/80 backdrop-blur-sm sticky top-0 z-50"> <nav class="border-b border-neutral-800 bg-neutral-950/80 backdrop-blur-sm sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between"> <div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<RouterLink to="/site" class="flex items-center gap-3"> <RouterLink :to="{ name: 'landing' }" class="flex items-center gap-3">
<img src="/logo.png" alt="Corrosion" class="h-8 w-8" /> <img src="/logo.png" alt="Corrosion" class="h-8 w-8" />
<span class="text-lg font-bold text-neutral-100">Corrosion</span> <span class="text-lg font-bold text-neutral-100">Corrosion</span>
</RouterLink> </RouterLink>
<div class="hidden md:flex items-center gap-6"> <div class="hidden md:flex items-center gap-6">
<RouterLink to="/site/how-it-works" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">How It Works</RouterLink> <RouterLink :to="{ name: 'how-it-works' }" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">How It Works</RouterLink>
<RouterLink to="/site/pricing" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Pricing</RouterLink> <RouterLink :to="{ name: 'pricing' }" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Pricing</RouterLink>
<RouterLink to="/site/roadmap" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Roadmap</RouterLink> <RouterLink :to="{ name: 'roadmap' }" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Roadmap</RouterLink>
<RouterLink to="/site/faq" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">FAQ</RouterLink> <RouterLink :to="{ name: 'faq' }" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">FAQ</RouterLink>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<RouterLink to="/login" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Sign In</RouterLink> <a :href="panelUrl + '/login'" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Sign In</a>
<RouterLink to="/register" class="px-4 py-2 bg-oxide-600 hover:bg-oxide-700 text-white text-sm font-medium rounded-lg transition-colors">Get Started</RouterLink> <a :href="panelUrl + '/register'" class="px-4 py-2 bg-oxide-600 hover:bg-oxide-700 text-white text-sm font-medium rounded-lg transition-colors">Get Started</a>
</div> </div>
</div> </div>
</nav> </nav>
@@ -36,22 +38,22 @@ import { RouterView, RouterLink } from 'vue-router'
<div> <div>
<h4 class="text-sm font-semibold text-neutral-300 mb-3">Product</h4> <h4 class="text-sm font-semibold text-neutral-300 mb-3">Product</h4>
<div class="space-y-2"> <div class="space-y-2">
<RouterLink to="/site/how-it-works" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">How It Works</RouterLink> <RouterLink :to="{ name: 'how-it-works' }" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">How It Works</RouterLink>
<RouterLink to="/site/pricing" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Pricing</RouterLink> <RouterLink :to="{ name: 'pricing' }" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Pricing</RouterLink>
<RouterLink to="/site/roadmap" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Roadmap</RouterLink> <RouterLink :to="{ name: 'roadmap' }" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Roadmap</RouterLink>
</div> </div>
</div> </div>
<div> <div>
<h4 class="text-sm font-semibold text-neutral-300 mb-3">Support</h4> <h4 class="text-sm font-semibold text-neutral-300 mb-3">Support</h4>
<div class="space-y-2"> <div class="space-y-2">
<RouterLink to="/site/faq" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">FAQ</RouterLink> <RouterLink :to="{ name: 'faq' }" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">FAQ</RouterLink>
<a href="https://discord.gg/corrosion" target="_blank" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Discord</a> <a href="https://discord.gg/corrosion" target="_blank" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Discord</a>
</div> </div>
</div> </div>
<div> <div>
<h4 class="text-sm font-semibold text-neutral-300 mb-3">Company</h4> <h4 class="text-sm font-semibold text-neutral-300 mb-3">Company</h4>
<div class="space-y-2"> <div class="space-y-2">
<RouterLink to="/site" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">About</RouterLink> <RouterLink :to="{ name: 'landing' }" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">About</RouterLink>
<RouterLink to="/status" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Status</RouterLink> <RouterLink to="/status" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Status</RouterLink>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,53 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth' 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) // Auth routes (no layout)
{ {
path: '/login', path: '/login',
@@ -113,7 +159,7 @@ const routes: RouteRecordRaw[] = [
name: 'settings', name: 'settings',
component: () => import('@/views/admin/SettingsView.vue'), component: () => import('@/views/admin/SettingsView.vue'),
}, },
// Platform Admin views (super-admin only, guarded in components) // Platform Admin views (super-admin only)
{ {
path: 'admin', path: 'admin',
name: 'platform-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', path: '/site',
component: () => import('@/components/layout/MarketingLayout.vue'), component: () => import('@/components/layout/MarketingLayout.vue'),
children: [ children: marketingChildren,
{
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'),
},
],
}, },
// Status page // 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({ const router = createRouter({
history: createWebHistory(), 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) => { router.beforeEach((to, _from, next) => {
const auth = useAuthStore() const auth = useAuthStore()

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink } from 'vue-router'
import { Shield, Zap, RefreshCw, Terminal, Users, Wifi, Server, ChevronRight } from 'lucide-vue-next' import { Shield, Zap, RefreshCw, Terminal, Users, Wifi, Server, ChevronRight } from 'lucide-vue-next'
const panelUrl = import.meta.env.VITE_PANEL_URL || ''
</script> </script>
<template> <template>
@@ -16,9 +17,9 @@ import { Shield, Zap, RefreshCw, Terminal, Users, Wifi, Server, ChevronRight } f
Deploy once. Automate everything. Never SSH again. Deploy once. Automate everything. Never SSH again.
</p> </p>
<div class="flex items-center justify-center gap-4"> <div class="flex items-center justify-center gap-4">
<RouterLink to="/register" class="px-8 py-3.5 bg-oxide-600 hover:bg-oxide-700 text-white font-semibold rounded-lg transition-colors text-lg"> <a :href="panelUrl + '/register'" class="px-8 py-3.5 bg-oxide-600 hover:bg-oxide-700 text-white font-semibold rounded-lg transition-colors text-lg">
Buy License Buy License
</RouterLink> </a>
<a href="#demo" class="px-8 py-3.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-200 font-semibold rounded-lg border border-neutral-700 transition-colors text-lg"> <a href="#demo" class="px-8 py-3.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-200 font-semibold rounded-lg border border-neutral-700 transition-colors text-lg">
View Live Demo View Live Demo
</a> </a>
@@ -278,9 +279,9 @@ import { Shield, Zap, RefreshCw, Terminal, Users, Wifi, Server, ChevronRight } f
<div class="max-w-3xl mx-auto px-6 text-center"> <div class="max-w-3xl mx-auto px-6 text-center">
<p class="text-2xl text-neutral-400 mb-2">Stop babysitting your server.</p> <p class="text-2xl text-neutral-400 mb-2">Stop babysitting your server.</p>
<p class="text-3xl font-bold text-neutral-100 mb-10">Start orchestrating it.</p> <p class="text-3xl font-bold text-neutral-100 mb-10">Start orchestrating it.</p>
<RouterLink to="/register" class="inline-block px-10 py-4 bg-oxide-600 hover:bg-oxide-700 text-white font-semibold rounded-lg transition-colors text-lg"> <a :href="panelUrl + '/register'" class="inline-block px-10 py-4 bg-oxide-600 hover:bg-oxide-700 text-white font-semibold rounded-lg transition-colors text-lg">
Get Corrosion Get Corrosion
</RouterLink> </a>
</div> </div>
</section> </section>
</div> </div>