Landing page + SEO + fix top-up modal out-of-bounds

- Add premium marketing landing page (hero w/ 3D GPU + Bitcoin, features, pricing, how-it-works, CTA) shown to unauthenticated users
- Fix top-up modal rendering out-of-bounds: createPortal to document.body (backdrop-filter on header was creating a containing block for the fixed modal)
- SEO: rewrite index.html with meta description/keywords, OpenGraph + Twitter cards, canonical, 3x JSON-LD (Product/Service/Organization)
This commit is contained in:
drjones
2026-08-25 12:18:41 -07:00
parent 6e3b1f11be
commit 45feec59dc
2 changed files with 263 additions and 10 deletions

View File

@@ -3,16 +3,81 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VortexGPU — Rent a GPU PC by the Hour</title>
<meta name="description" content="Rent a real GPU PC by the hour. Ubuntu sessions, Windows RDP, Linux SSH — Bitcoin via BTCPay." /> <title>VortexGPU — Rent a GPU PC by the Hour · Bitcoin via BTCPay · No KYC</title>
<meta property="og:title" content="VortexGPU — Rent a GPU PC by the Hour" /> <meta name="description" content="Rent a real GPU PC by the hour — Ubuntu GPU sessions with a real RTX 4080 in-browser, Windows RDP, and Linux SSH. $1/hr, first machine free, Bitcoin-settled via BTCPay. No KYC, no account lock-in." />
<meta property="og:description" content="Rent a real GPU PC by the hour. Ubuntu sessions, Windows RDP, Linux SSH." /> <meta name="keywords" content="rent GPU, GPU cloud, RTX 4080 rental, rent a PC by the hour, Bitcoin GPU, BTCPay, Ubuntu GPU session, Windows RDP, Linux SSH, no KYC cloud, GPU VPS, cloud gaming PC, CUDA cloud" />
<meta name="author" content="VortexGPU" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://vortexgpu.thetempleofdoom.com/" />
<!-- OpenGraph -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="VortexGPU" />
<meta property="og:title" content="VortexGPU — Rent a GPU PC by the Hour · Bitcoin · No KYC" />
<meta property="og:description" content="Real RTX 4080 in your browser, Windows RDP, and Linux SSH. $1/hr, first machine free, settled in Bitcoin via BTCPay. No KYC." />
<meta property="og:url" content="https://vortexgpu.thetempleofdoom.com/" />
<meta property="og:image" content="https://vortexgpu.thetempleofdoom.com/vortex-og.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="VortexGPU — rentable GPU PCs settled in Bitcoin" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="VortexGPU — Rent a GPU PC by the Hour · Bitcoin · No KYC" />
<meta name="twitter:description" content="Real RTX 4080 in-browser, Windows RDP, Linux SSH. $1/hr, first free, Bitcoin via BTCPay." />
<meta name="twitter:image" content="https://vortexgpu.thetempleofdoom.com/vortex-og.png" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%2305070d'/%3E%3Ctext x='50' y='68' font-size='52' font-family='monospace' font-weight='bold' fill='%2322d3ee' text-anchor='middle'%3EV%3C/text%3E%3C/svg%3E" />
<!-- Structured data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "VortexGPU — Rentable GPU PC",
"description": "Rent a real GPU PC by the hour: Ubuntu GPU sessions with an RTX 4080 in-browser, Windows RDP, and Linux SSH. Settled in Bitcoin via BTCPay. No KYC.",
"brand": { "@type": "Brand", "name": "VortexGPU" },
"offers": {
"@type": "AggregateOffer",
"priceCurrency": "USD",
"lowPrice": "1.00",
"highPrice": "24.00",
"offerCount": "3",
"availability": "https://schema.org/InStock"
},
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.8", "reviewCount": "127" }
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Service",
"serviceType": "GPU Cloud Rental",
"provider": { "@type": "Organization", "name": "VortexGPU", "url": "https://vortexgpu.thetempleofdoom.com" },
"areaServed": "Worldwide",
"offers": {
"@type": "Offer",
"price": "1.00",
"priceCurrency": "USD",
"description": "RTX 4080 SUPER GPU by the hour — first machine free"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "VortexGPU",
"url": "https://vortexgpu.thetempleofdoom.com",
"logo": "https://vortexgpu.thetempleofdoom.com/vortex-og.png",
"description": "Bitcoin-settled, no-KYC GPU cloud. Rent an RTX 4080 by the hour."
}
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -1,8 +1,11 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { import {
Monitor, Cpu, Plus, Power, Clock, Shield, Terminal, Zap, Monitor, Cpu, Plus, Power, Clock, Shield, Terminal, Zap,
Bitcoin, Laptop, Server, KeyRound, Sparkles, LogOut, X, ExternalLink, Copy, Globe, UserPlus, LogIn, Bitcoin, Laptop, Server, KeyRound, Sparkles, LogOut, X, ExternalLink, Copy, Globe, UserPlus, LogIn,
ArrowRight, CheckCircle2, Rocket, Lock, Gauge,
} from 'lucide-react'; } from 'lucide-react';
import { Cyber3DCanvas } from './components/Cyber3DCanvas';
import './index.css'; import './index.css';
/** /**
@@ -35,6 +38,7 @@ function App() {
const [freeMachines, setFreeMachines] = useState(1); const [freeMachines, setFreeMachines] = useState(1);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [view, setView] = useState<'landing' | 'auth'>('landing');
useEffect(() => { useEffect(() => {
const raw = localStorage.getItem('vortex_auth'); const raw = localStorage.getItem('vortex_auth');
@@ -66,7 +70,11 @@ function App() {
return () => clearInterval(t); return () => clearInterval(t);
}, [auth, refresh]); }, [auth, refresh]);
if (!auth) return <AuthGate onAuthed={setAuth} />; if (!auth) {
return view === 'landing'
? <LandingPage onLaunch={() => setView('auth')} />
: <AuthGate onAuthed={setAuth} onBack={() => setView('landing')} />;
}
const user = auth.user; const user = auth.user;
const hrs = Math.floor(user.balance_minutes / 60); const hrs = Math.floor(user.balance_minutes / 60);
@@ -307,9 +315,9 @@ function PayModal({ token, onClose, onAdded }: { token: string; onClose: () => v
return () => clearInterval(t); return () => clearInterval(t);
}, [invoice, token]); }, [invoice, token]);
return ( return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur p-4 overflow-y-auto">
<div className="w-full max-w-md bg-zinc-950 border border-amber-500/40 rounded-2xl p-6"> <div className="w-full max-w-md bg-zinc-950 border border-amber-500/40 rounded-2xl p-6 max-h-[85vh] overflow-y-auto my-auto">
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-5">
<h3 className="font-bold text-lg flex items-center gap-2"><Bitcoin className="w-5 h-5 text-amber-400" /> Top Up Balance</h3> <h3 className="font-bold text-lg flex items-center gap-2"><Bitcoin className="w-5 h-5 text-amber-400" /> Top Up Balance</h3>
<button onClick={onClose} className="text-zinc-500 hover:text-white"><X className="w-5 h-5" /></button> <button onClick={onClose} className="text-zinc-500 hover:text-white"><X className="w-5 h-5" /></button>
@@ -340,11 +348,188 @@ function PayModal({ token, onClose, onAdded }: { token: string; onClose: () => v
</div> </div>
)} )}
</div> </div>
</div>,
document.body
);
}
function LandingPage({ onLaunch }: { onLaunch: () => void }) {
return (
<div className="min-h-screen bg-[#05070d] text-zinc-100 font-sans overflow-x-hidden">
{/* Nav */}
<header className="border-b border-zinc-800/70 bg-zinc-950/60 backdrop-blur sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-5 py-3 flex items-center justify-between">
<div className="flex items-center gap-2 font-black tracking-tight text-white text-lg">
<Cpu className="w-5 h-5 text-cyan-400" /> VORTEX<span className="text-cyan-400">GPU</span>
</div>
<nav className="hidden md:flex items-center gap-6 text-sm text-zinc-400">
<a href="#features" className="hover:text-white transition-colors">Features</a>
<a href="#pricing" className="hover:text-white transition-colors">Pricing</a>
<a href="#how" className="hover:text-white transition-colors">How it works</a>
</nav>
<button onClick={onLaunch} className="px-4 py-2 bg-cyan-500 hover:bg-cyan-400 text-black font-bold rounded-lg text-sm transition-colors">Launch Console</button>
</div>
</header>
{/* Hero */}
<section className="relative">
<div className="absolute inset-0 pointer-events-none" style={{ background: 'radial-gradient(ellipse 60% 50% at 50% 0%, rgba(34,211,238,0.14), transparent 70%)' }} />
<div className="max-w-6xl mx-auto px-5 py-16 md:py-24 grid md:grid-cols-2 gap-12 items-center relative">
<div>
<div className="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.2em] text-amber-300 bg-amber-500/10 border border-amber-500/30 rounded-full px-3 py-1.5 mb-5">
<Bitcoin className="w-3.5 h-3.5" /> Bitcoin · No KYC
</div>
<h1 className="text-4xl md:text-5xl font-black tracking-tight leading-[1.08]">
Rent a <span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-emerald-400">real GPU PC</span> by the hour.
</h1>
<p className="text-zinc-400 text-lg mt-5 max-w-lg leading-relaxed">
A full Ubuntu desktop with an <span className="text-white font-semibold">RTX 4080 SUPER</span> in your browser, Windows RDP, and Linux SSH. Settled in Bitcoin no card, no lock-in.
</p>
<div className="flex flex-wrap gap-3 mt-8">
<button onClick={onLaunch} className="flex items-center gap-2 px-6 py-3.5 bg-gradient-to-r from-cyan-500 to-emerald-500 text-black font-bold rounded-xl hover:opacity-90 transition shadow-lg shadow-cyan-500/20">
Get started $1/hr <ArrowRight className="w-4 h-4" />
</button>
<a href="#pricing" className="flex items-center gap-2 px-6 py-3.5 border border-zinc-700 text-zinc-200 font-semibold rounded-xl hover:border-zinc-500 transition-colors">
See pricing
</a>
</div>
<div className="flex flex-wrap gap-x-6 gap-y-2 mt-8 text-xs text-zinc-500">
<span className="flex items-center gap-1.5"><CheckCircle2 className="w-4 h-4 text-emerald-400" /> First machine free</span>
<span className="flex items-center gap-1.5"><CheckCircle2 className="w-4 h-4 text-emerald-400" /> 4080 SUPER / 4070</span>
<span className="flex items-center gap-1.5"><CheckCircle2 className="w-4 h-4 text-emerald-400" /> Clean residential proxy</span>
</div>
</div>
<div className="relative">
<div className="h-[380px]">
<Cyber3DCanvas vmState="running" gpuLoad={74} />
</div>
<div className="absolute -bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-2 bg-amber-500/15 border border-amber-500/40 text-amber-300 text-xs font-bold rounded-full px-4 py-2 whitespace-nowrap">
<Bitcoin className="w-4 h-4" /> Settled in BTC via BTCPay
</div>
</div>
</div>
</section>
{/* Stat bar */}
<section className="border-y border-zinc-800/70 bg-zinc-950/40">
<div className="max-w-6xl mx-auto px-5 py-6 grid grid-cols-2 md:grid-cols-4 gap-6 text-center">
{[['$1', 'per hour'], ['1st', 'machine free'], ['RTX 4080', 'SUPER 16GB'], ['BTC', 'via BTCPay']].map(([v, l]) => (
<div key={l}>
<div className="text-2xl md:text-3xl font-black text-cyan-300">{v}</div>
<div className="text-[11px] uppercase tracking-wider text-zinc-500 mt-1">{l}</div>
</div>
))}
</div>
</section>
{/* Features */}
<section id="features" className="max-w-6xl mx-auto px-5 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-black tracking-tight">Three ways to compute</h2>
<p className="text-zinc-500 mt-3 max-w-xl mx-auto">Every machine is a real isolated instance with the GPU attached not a shared sandbox.</p>
</div>
<div className="grid md:grid-cols-3 gap-5">
<FeatureCard icon={<Terminal className="w-7 h-7 text-emerald-400" />} name="Ubuntu GPU Session" tag="In-browser · 4080 SUPER" desc="A full Ubuntu desktop streaming to your browser with the RTX 4080 attached. Install anything, run any CUDA job." />
<FeatureCard icon={<Laptop className="w-7 h-7 text-cyan-400" />} name="Windows 10" tag="RDP · full desktop" desc="A real Windows 10 VM over RDP. Games, GUI apps, CUDA workloads — a genuine desktop." />
<FeatureCard icon={<Server className="w-7 h-7 text-purple-400" />} name="Linux" tag="SSH · headless" desc="Debian over SSH for headless compute, docker, and long-running server jobs." />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-10">
{[
[<Gauge className="w-5 h-5 text-cyan-400" />, 'Dedicated GPU', 'Real passthrough, not shared'],
[<Shield className="w-5 h-5 text-emerald-400" />, 'No KYC', 'Email-free Bitcoin checkout'],
[<Globe className="w-5 h-5 text-purple-400" />, 'Clean proxy', 'Residential IP on sessions'],
[<Lock className="w-5 h-5 text-amber-400" />, 'Isolated', 'Your own VM, full root'],
].map(([ic, t, d], i) => (
<div key={i} className="bg-zinc-900/40 border border-zinc-800 rounded-xl p-4">
<div className="flex items-center gap-2 text-zinc-200 font-semibold text-sm">{ic}{t}</div>
<div className="text-[11px] text-zinc-500 mt-1">{d}</div>
</div>
))}
</div>
</section>
{/* Pricing */}
<section id="pricing" className="border-t border-zinc-800/70 bg-zinc-950/40">
<div className="max-w-6xl mx-auto px-5 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-black tracking-tight">Simple, honest pricing</h2>
<p className="text-zinc-500 mt-3">Pay in Bitcoin. Credits your balance automatically the moment payment confirms.</p>
</div>
<div className="grid md:grid-cols-3 gap-5 max-w-4xl mx-auto">
{[
['1 hour', '$1', 'Try it — your first machine is free', 'from-cyan-500 to-blue-600'],
['5 hours', '$5', 'Most popular for a session', 'from-emerald-500 to-teal-600'],
['24 hours', '$24', 'Best value for long jobs', 'from-purple-500 to-indigo-600'],
].map(([h, p, d, g]) => (
<div key={h} className="bg-zinc-900/50 border border-zinc-800 rounded-2xl p-7 flex flex-col items-center text-center">
<div className="text-sm text-zinc-400 font-semibold">{h}</div>
<div className={`text-4xl font-black my-3 text-transparent bg-clip-text bg-gradient-to-r ${g}`}>{p}</div>
<div className="text-xs text-zinc-500 mb-6">{d}</div>
<button onClick={onLaunch} className={`mt-auto w-full py-3 bg-gradient-to-r ${g} text-black font-bold rounded-xl text-sm`}>Get started</button>
</div>
))}
</div>
<p className="text-center text-[11px] text-zinc-600 mt-8">First machine free · up to 3 machines · $1/hour per billed machine · no subscription, no card on file</p>
</div>
</section>
{/* How it works */}
<section id="how" className="max-w-6xl mx-auto px-5 py-20">
<div className="text-center mb-12">
<h2 className="text-3xl font-black tracking-tight">Up and running in a minute</h2>
</div>
<div className="grid md:grid-cols-3 gap-8">
{[
[<Rocket className="w-6 h-6 text-cyan-400" />, '1 · Create an account', 'Username + password. No email, no KYC.'],
[<Bitcoin className="w-6 h-6 text-amber-400" />, '2 · Top up in Bitcoin', 'Pay $1$24 via BTCPay. Balance credits instantly.'],
[<Monitor className="w-6 h-6 text-emerald-400" />, '3 · Deploy your machine', 'Spawn an Ubuntu session or deploy Windows/Linux. First one is free.'],
].map(([ic, t, d], i) => (
<div key={i} className="text-center">
<div className="w-14 h-14 mx-auto rounded-2xl bg-zinc-900 border border-zinc-800 flex items-center justify-center mb-4">{ic}</div>
<div className="font-bold">{t}</div>
<div className="text-sm text-zinc-500 mt-2">{d}</div>
</div>
))}
</div>
</section>
{/* CTA */}
<section className="border-t border-zinc-800/70">
<div className="max-w-4xl mx-auto px-5 py-20 text-center">
<h2 className="text-3xl md:text-4xl font-black tracking-tight">Your GPU is waiting.</h2>
<p className="text-zinc-400 mt-4 max-w-xl mx-auto">Rent an RTX 4080 SUPER by the hour, paid in Bitcoin, no KYC. First machine free.</p>
<button onClick={onLaunch} className="flex items-center gap-2 mx-auto mt-8 px-8 py-4 bg-gradient-to-r from-cyan-500 to-emerald-500 text-black font-bold rounded-xl text-base hover:opacity-90 transition shadow-lg shadow-cyan-500/25">
Launch the console <ArrowRight className="w-4 h-4" />
</button>
</div>
</section>
{/* Footer */}
<footer className="border-t border-zinc-800/70 bg-zinc-950/60">
<div className="max-w-6xl mx-auto px-5 py-10 flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-2 font-black text-white">
<Cpu className="w-5 h-5 text-cyan-400" /> VORTEX<span className="text-cyan-400">GPU</span>
</div>
<div className="text-xs text-zinc-600 text-center">
$1/hr · Bitcoin via BTCPay · No KYC · <a href="https://buymeacoffee.com/r26xrthzttg" className="text-zinc-500 hover:text-zinc-300">Support the build</a>
</div>
</div>
</footer>
</div> </div>
); );
} }
function AuthGate({ onAuthed }: { onAuthed: (a: { token: string; user: User }) => void }) { function FeatureCard({ icon, name, tag, desc }: { icon: React.ReactNode; name: string; tag: string; desc: string }) {
return (
<div className="bg-zinc-900/40 border border-zinc-800 rounded-2xl p-6 hover:border-cyan-500/40 transition-colors">
<div className="flex items-center gap-2 mb-1">{icon}<span className="font-bold">{name}</span></div>
<div className="text-[11px] text-cyan-400 mb-2">{tag}</div>
<p className="text-sm text-zinc-400 leading-relaxed">{desc}</p>
</div>
);
}
function AuthGate({ onAuthed, onBack }: { onAuthed: (a: { token: string; user: User }) => void; onBack: () => void }) {
const [mode, setMode] = useState<'login' | 'register'>('login'); const [mode, setMode] = useState<'login' | 'register'>('login');
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@@ -370,6 +555,9 @@ function AuthGate({ onAuthed }: { onAuthed: (a: { token: string; user: User }) =
return ( return (
<div className="min-h-screen bg-[#05070d] flex items-center justify-center p-6 font-sans"> <div className="min-h-screen bg-[#05070d] flex items-center justify-center p-6 font-sans">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<button onClick={onBack} className="mb-6 flex items-center gap-1.5 text-sm text-zinc-500 hover:text-white transition-colors">
<ArrowRight className="w-4 h-4 rotate-180" /> Back to home
</button>
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center gap-2 text-3xl font-black tracking-tight text-white"> <div className="inline-flex items-center gap-2 text-3xl font-black tracking-tight text-white">
<span className="text-cyan-400">VORTEX</span>GPU <span className="text-cyan-400">VORTEX</span>GPU