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
|
||||
API_PORT=3000
|
||||
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)
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user