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:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user