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
API_PORT=3000
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)
server {
listen 80;

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
import { RouterView, RouterLink } from 'vue-router'
const panelUrl = import.meta.env.VITE_PANEL_URL || ''
</script>
<template>
@@ -7,19 +9,19 @@ import { RouterView, RouterLink } from 'vue-router'
<!-- Navigation -->
<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">
<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" />
<span class="text-lg font-bold text-neutral-100">Corrosion</span>
</RouterLink>
<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="/site/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="/site/faq" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">FAQ</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="{ name: 'pricing' }" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Pricing</RouterLink>
<RouterLink :to="{ name: 'roadmap' }" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Roadmap</RouterLink>
<RouterLink :to="{ name: 'faq' }" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">FAQ</RouterLink>
</div>
<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>
<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 + '/login'" class="text-sm text-neutral-400 hover:text-neutral-100 transition-colors">Sign In</a>
<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>
</nav>
@@ -36,22 +38,22 @@ import { RouterView, RouterLink } from 'vue-router'
<div>
<h4 class="text-sm font-semibold text-neutral-300 mb-3">Product</h4>
<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="/site/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: 'how-it-works' }" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">How It Works</RouterLink>
<RouterLink :to="{ name: 'pricing' }" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Pricing</RouterLink>
<RouterLink :to="{ name: 'roadmap' }" class="block text-sm text-neutral-500 hover:text-neutral-300 transition-colors">Roadmap</RouterLink>
</div>
</div>
<div>
<h4 class="text-sm font-semibold text-neutral-300 mb-3">Support</h4>
<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>
</div>
</div>
<div>
<h4 class="text-sm font-semibold text-neutral-300 mb-3">Company</h4>
<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>
</div>
</div>

View File

@@ -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()

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { Shield, Zap, RefreshCw, Terminal, Users, Wifi, Server, ChevronRight } from 'lucide-vue-next'
const panelUrl = import.meta.env.VITE_PANEL_URL || ''
</script>
<template>
@@ -16,9 +17,9 @@ import { Shield, Zap, RefreshCw, Terminal, Users, Wifi, Server, ChevronRight } f
Deploy once. Automate everything. Never SSH again.
</p>
<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
</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">
View Live Demo
</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">
<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>
<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
</RouterLink>
</a>
</div>
</section>
</div>