v0.7.0: marketing landing page at / + /docs (brand: WP IDE Server, env-overridable)
This commit is contained in:
@@ -4,6 +4,24 @@ All notable changes to wpide-server will be documented in this file. Format
|
||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
|
||||
is [SemVer](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.7.0] — 2026-05-26
|
||||
|
||||
### Added
|
||||
- **Marketing landing page at `/`** (`src/routes/landing.ts`). Dark-theme,
|
||||
single-file HTML/CSS (same self-contained pattern as `/app`). Sections:
|
||||
sticky nav, hero with value-prop + CTAs to `/app`, model trust-strip
|
||||
(DeepSeek/Anthropic/OpenAI/xAI), 6-card features grid, 3-step "how it
|
||||
works" with an inline chat preview, final CTA, footer.
|
||||
- **Quick docs page at `/docs`** — setup steps, brain-split explainer,
|
||||
model-routing tiers, minimal API surface reference.
|
||||
- **Brand name read from `BRAND_NAME` env** (default `WP IDE Server`) so
|
||||
the customer-facing name can be changed without touching code.
|
||||
- No `/pricing` yet — deliberately deferred until tiers are finalised.
|
||||
|
||||
### Notes
|
||||
- `/app` (the existing dashboard) is unchanged and reachable from the new
|
||||
nav. The landing nav links: Docs · Sign in · Get started.
|
||||
|
||||
## [0.6.3] — 2026-05-26
|
||||
|
||||
### Changed
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wpide-server",
|
||||
"version": "0.6.3",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"description": "Closed orchestrator server for the WordPress IDE plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
/**
|
||||
* Marketing landing pages served at /, /docs, and /pricing.
|
||||
* Self-contained inline HTML/CSS (same pattern as dashboard.ts).
|
||||
* Brand name is read from BRAND_NAME env (default below) so it's
|
||||
* trivial to rebrand without redeploying.
|
||||
*/
|
||||
export async function landingRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get('/', async (_req, reply) => {
|
||||
reply.header('content-type', 'text/html; charset=utf-8');
|
||||
return LANDING;
|
||||
});
|
||||
app.get('/docs', async (_req, reply) => {
|
||||
reply.header('content-type', 'text/html; charset=utf-8');
|
||||
return DOCS;
|
||||
});
|
||||
// /pricing intentionally deferred — owner is still deciding tiers.
|
||||
}
|
||||
|
||||
const BRAND = process.env.BRAND_NAME || 'WP IDE Server';
|
||||
|
||||
const SHELL = (title: string, body: string): string => `<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${title} — ${BRAND}</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg:#0f1419; --bg-2:#0a0e13; --card:#1a2230; --line:#2a3547;
|
||||
--fg:#e6edf3; --mut:#8b98a8; --dim:#5b6776;
|
||||
--accent:#50a8eb; --accent-2:#7c5cf5; --ok:#3fb950; --warn:#d29922;
|
||||
}
|
||||
* { box-sizing:border-box; }
|
||||
html,body { margin:0; padding:0; }
|
||||
body { font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; background:var(--bg); color:var(--fg); line-height:1.55; -webkit-font-smoothing:antialiased; }
|
||||
a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
|
||||
/* ── top nav ── */
|
||||
nav.top { position:sticky; top:0; z-index:10; background:rgba(15,20,25,.85); backdrop-filter:blur(8px); border-bottom:1px solid var(--line); }
|
||||
nav.top .row { max-width:1080px; margin:0 auto; padding:14px 22px; display:flex; align-items:center; justify-content:space-between; }
|
||||
nav.top .brand { font-weight:700; font-size:17px; letter-spacing:-.01em; }
|
||||
nav.top .brand .dot { color:var(--accent); }
|
||||
nav.top .links { display:flex; gap:22px; align-items:center; font-size:14px; }
|
||||
nav.top .links a { color:var(--mut); }
|
||||
nav.top .links a:hover { color:var(--fg); text-decoration:none; }
|
||||
nav.top .cta { background:var(--accent); color:#04121f; padding:8px 16px; border-radius:8px; font-weight:600; }
|
||||
nav.top .cta:hover { background:#7cc4ff; text-decoration:none; }
|
||||
/* ── shared ── */
|
||||
.container { max-width:1080px; margin:0 auto; padding:0 22px; }
|
||||
.pill { display:inline-block; padding:5px 12px; border-radius:999px; background:rgba(80,168,235,.12); color:var(--accent); font-size:12px; font-weight:600; letter-spacing:.02em; text-transform:uppercase; }
|
||||
h1, h2, h3 { margin:0 0 .4em; letter-spacing:-.015em; }
|
||||
/* ── hero ── */
|
||||
.hero { padding:80px 0 80px; text-align:center; background:radial-gradient(ellipse at center top, rgba(80,168,235,.08), transparent 60%); }
|
||||
.hero h1 { font-size:clamp(34px, 5.5vw, 56px); line-height:1.08; font-weight:800; max-width:780px; margin:18px auto 18px; background:linear-gradient(180deg,#fff 0%,#b6c3d6 100%); -webkit-background-clip:text; background-clip:text; color:transparent; }
|
||||
.hero .sub { font-size:clamp(16px, 1.6vw, 19px); color:var(--mut); max-width:680px; margin:0 auto 32px; }
|
||||
.hero .ctas { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; }
|
||||
.btn { display:inline-block; padding:12px 22px; border-radius:9px; font-weight:600; font-size:15px; cursor:pointer; border:0; }
|
||||
.btn-primary { background:var(--accent); color:#04121f; }
|
||||
.btn-primary:hover { background:#7cc4ff; text-decoration:none; }
|
||||
.btn-ghost { background:transparent; color:var(--fg); border:1px solid var(--line); }
|
||||
.btn-ghost:hover { background:var(--card); text-decoration:none; }
|
||||
/* ── trust strip ── */
|
||||
.trust { padding:24px 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line); background:var(--bg-2); }
|
||||
.trust .row { display:flex; gap:36px; justify-content:center; flex-wrap:wrap; color:var(--dim); font-size:13px; text-align:center; }
|
||||
.trust .row span { font-weight:500; }
|
||||
.trust .row b { color:var(--mut); font-weight:600; }
|
||||
/* ── features grid ── */
|
||||
.section { padding:80px 0; }
|
||||
.section .head { text-align:center; margin-bottom:46px; }
|
||||
.section .head h2 { font-size:clamp(28px,3.5vw,38px); font-weight:700; }
|
||||
.section .head .sub { color:var(--mut); max-width:600px; margin:10px auto 0; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:18px; }
|
||||
.card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:22px; transition:border-color .15s, transform .15s; }
|
||||
.card:hover { border-color:var(--accent); }
|
||||
.card .ico { width:38px; height:38px; border-radius:9px; background:linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%); display:flex; align-items:center; justify-content:center; font-size:18px; margin-bottom:14px; }
|
||||
.card h3 { font-size:17px; margin-bottom:6px; }
|
||||
.card p { color:var(--mut); font-size:14px; margin:0; }
|
||||
/* ── steps ── */
|
||||
.steps { display:grid; grid-template-columns:repeat(auto-fit, minmax(260px, 1fr)); gap:18px; }
|
||||
.step { background:var(--bg-2); border:1px solid var(--line); border-radius:14px; padding:24px; position:relative; }
|
||||
.step .num { position:absolute; top:-14px; left:24px; background:var(--accent); color:#04121f; font-weight:800; font-size:14px; padding:4px 10px; border-radius:999px; }
|
||||
.step h3 { font-size:16px; margin-top:6px; margin-bottom:6px; }
|
||||
.step p { color:var(--mut); font-size:14px; margin:0; }
|
||||
.step code { background:#0d1117; border:1px solid var(--line); padding:1px 6px; border-radius:4px; font-size:13px; }
|
||||
/* ── code preview ── */
|
||||
.codewrap { max-width:760px; margin:24px auto 0; border:1px solid var(--line); border-radius:12px; background:#0d1117; overflow:hidden; }
|
||||
.codewrap .top { background:var(--card); border-bottom:1px solid var(--line); padding:8px 14px; color:var(--mut); font-size:12px; font-family:ui-monospace,monospace; }
|
||||
.codewrap pre { margin:0; padding:18px 22px; color:#c5d2e0; font-family:ui-monospace,SF Mono,monospace; font-size:13px; line-height:1.7; overflow:auto; }
|
||||
.codewrap .k { color:#ff7b72; } .codewrap .s { color:#a5d6ff; } .codewrap .c { color:#7d8590; }
|
||||
/* ── final cta ── */
|
||||
.final { padding:80px 0; text-align:center; background:radial-gradient(ellipse at center, rgba(124,92,245,.08), transparent 65%); border-top:1px solid var(--line); }
|
||||
.final h2 { font-size:clamp(28px,3.5vw,38px); font-weight:700; }
|
||||
.final p { color:var(--mut); margin:10px 0 28px; }
|
||||
/* ── footer ── */
|
||||
footer { border-top:1px solid var(--line); padding:32px 0; color:var(--dim); font-size:13px; }
|
||||
footer .row { display:flex; justify-content:space-between; flex-wrap:wrap; gap:18px; }
|
||||
footer .links a { color:var(--mut); margin-right:20px; }
|
||||
/* ── docs page ── */
|
||||
.docs { padding:60px 0; }
|
||||
.docs h1 { font-size:34px; margin-bottom:8px; }
|
||||
.docs h2 { font-size:22px; margin-top:48px; padding-bottom:8px; border-bottom:1px solid var(--line); }
|
||||
.docs h3 { font-size:17px; margin-top:28px; }
|
||||
.docs p { color:var(--mut); }
|
||||
.docs pre { background:#0d1117; border:1px solid var(--line); border-radius:8px; padding:16px 18px; overflow:auto; font-size:13px; color:#c5d2e0; }
|
||||
.docs code { background:#0d1117; border:1px solid var(--line); padding:1px 6px; border-radius:4px; font-size:13px; color:#a5d6ff; }
|
||||
.docs pre code { background:transparent; border:0; padding:0; }
|
||||
</style></head>
|
||||
<body>
|
||||
<nav class="top">
|
||||
<div class="row">
|
||||
<a href="/" class="brand">${BRAND}<span class="dot">.</span></a>
|
||||
<div class="links">
|
||||
<a href="/docs">Docs</a>
|
||||
<a href="/app">Sign in</a>
|
||||
<a href="/app" class="cta">Get started</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
${body}
|
||||
<footer><div class="container row">
|
||||
<div>© ${new Date().getFullYear()} ${BRAND}</div>
|
||||
<div class="links">
|
||||
<a href="/docs">Docs</a>
|
||||
<a href="/app">Account</a>
|
||||
<a href="/v1/health">Status</a>
|
||||
</div>
|
||||
</div></footer>
|
||||
</body></html>`;
|
||||
|
||||
const LANDING = SHELL('AI brain for your WordPress IDE', `
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<span class="pill">Closed orchestrator · Open client</span>
|
||||
<h1>The AI brain behind your WordPress IDE.</h1>
|
||||
<p class="sub">Stop wrangling API keys and orchestration. One subscription, every top model, full session memory — running on a server you don't have to operate. Drop the plugin in WordPress, point it here, ship.</p>
|
||||
<div class="ctas">
|
||||
<a href="/app" class="btn btn-primary">Get started — free tier</a>
|
||||
<a href="/docs" class="btn btn-ghost">How it works</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="trust"><div class="container row">
|
||||
<span><b>DeepSeek v4</b> · flash + thinking + pro max</span>
|
||||
<span><b>Anthropic</b> · Sonnet / Opus</span>
|
||||
<span><b>OpenAI</b> · GPT-5 / 4o</span>
|
||||
<span><b>xAI</b> · Grok 4 + realtime</span>
|
||||
</div></div>
|
||||
|
||||
<section class="section"><div class="container">
|
||||
<div class="head">
|
||||
<h2>Everything you wanted from agentic WordPress.</h2>
|
||||
<p class="sub">Built for developers and agencies running real sites on shared hosting, multisite, or anything in between.</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="ico">🔑</div>
|
||||
<h3>Every model, one bill</h3>
|
||||
<p>DeepSeek, Anthropic, OpenAI, xAI behind a single subscription. No more juggling provider keys per machine.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="ico">🧠</div>
|
||||
<h3>Server-side memory</h3>
|
||||
<p>Conversations remember themselves across runs and devices. The plugin sends little; the brain fills in the rest.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="ico">⚡</div>
|
||||
<h3>Cap-immune transport</h3>
|
||||
<p>Browser-direct streaming bypasses your hosting's ~120s proxy cap. Long agentic runs survive on Hostinger, LiteSpeed, anything.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="ico">🛠️</div>
|
||||
<h3>95+ WordPress tools</h3>
|
||||
<p>File ops, DB queries, WP-CLI, snapshots, post mutation, theme/plugin install — every one of them governed by your local capability policy.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="ico">👥</div>
|
||||
<h3>Teams & seat-sharing</h3>
|
||||
<p>Org owner pays once, seats inherit the tier. Multi-tenant out of the box.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="ico">🔒</div>
|
||||
<h3>Your data stays yours</h3>
|
||||
<p>The brain orchestrates; tool execution runs locally against your WordPress. The server never touches your files or database.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="section" style="background:var(--bg-2);"><div class="container">
|
||||
<div class="head">
|
||||
<h2>How it works</h2>
|
||||
<p class="sub">Three steps from <i>I want AI in WordPress</i> to <i>I'm shipping changes from chat</i>.</p>
|
||||
</div>
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<span class="num">1</span>
|
||||
<h3>Install the plugin</h3>
|
||||
<p>Drop the open-source <code>wordpress-ide</code> plugin into any WP site. Standard ZIP upload — works on shared hosting, multisite, anywhere.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="num">2</span>
|
||||
<h3>Sign up here, copy your license key</h3>
|
||||
<p>Free tier gives you flash-tier models out of the box. Upgrade when you want thinking models or the big context windows.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="num">3</span>
|
||||
<h3>Paste, save, chat</h3>
|
||||
<p>WordPress IDE → <code>AI Brain (Server)</code> → paste the key, hit Save, start chatting. Tools run locally; the brain is here.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="codewrap">
|
||||
<div class="top">~ chat in WordPress admin</div>
|
||||
<pre><span class="c">// You</span>
|
||||
list every inactive plugin and tell me which haven't been updated in a year
|
||||
|
||||
<span class="c">// AI brain (deepseek-v4-flash, 744ms)</span>
|
||||
You have <span class="s">14 inactive plugins</span>. Of those:
|
||||
• <span class="s">classic-editor</span> — last updated 11 months ago
|
||||
• <span class="s">akismet</span> — last updated 3 weeks ago
|
||||
• <span class="s">hello-dolly</span> — last updated 4 years ago ⚠ stale
|
||||
…</pre>
|
||||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="final"><div class="container">
|
||||
<h2>Start free. Upgrade when you outgrow it.</h2>
|
||||
<p>Free tier is open-ended for personal projects. Pro/Max tiers when you're shipping at scale.</p>
|
||||
<a href="/app" class="btn btn-primary">Create your account →</a>
|
||||
</div></section>
|
||||
`);
|
||||
|
||||
const DOCS = SHELL('Docs', `
|
||||
<section class="docs"><div class="container" style="max-width:780px;">
|
||||
<h1>${BRAND} — Quick docs</h1>
|
||||
<p class="sub">The minimum you need to wire the WordPress IDE plugin into this server and start running agentic chats.</p>
|
||||
|
||||
<h2>1 · Set up</h2>
|
||||
<ol>
|
||||
<li>Install the <b>WordPress IDE</b> plugin (ZIP upload in WP admin → Plugins → Add New).</li>
|
||||
<li>Create an account on this server at <a href="/app">/app</a>. You get a license key on signup.</li>
|
||||
<li>In WP admin: <b>WordPress IDE → AI Brain (Server)</b>. Paste your license key, set <b>Server URL</b> to <code>${process.env.PUBLIC_BASE_URL || 'https://this-server'}</code>, toggle the mode on, hit Save.</li>
|
||||
<li>Open the WP IDE chat and say hello. The first reply confirms the connection.</li>
|
||||
</ol>
|
||||
|
||||
<h2>2 · How the brain split works</h2>
|
||||
<p>The plugin holds your local capability policy (file ops, DB queries, etc.). The server holds the orchestrator, system prompts, and LLM credentials. Every tool the agent calls runs <i>on your WordPress host</i>; every reasoning step runs <i>here</i>.</p>
|
||||
|
||||
<h3>Two transports</h3>
|
||||
<p><b>Relay mode</b> (default): WP plugin <code>POST</code>s the goal here, then polls our async run. Works everywhere; bounded by your host's max request time.</p>
|
||||
<p><b>Browser-direct mode</b> (cap-immune): the chat opens an <code>EventSource</code> straight to this server. The WP host is removed from the long-lived connection entirely, so a 120s proxy cap on shared hosting can no longer cut a long agentic run. Toggle it in <b>AI Brain (Server) → Browser-direct streaming</b>.</p>
|
||||
|
||||
<h2>3 · Model routing</h2>
|
||||
<p>Your subscription tier caps which models the router may pick:</p>
|
||||
<ul>
|
||||
<li><b>basic</b> → deepseek-v4-flash and equivalents</li>
|
||||
<li><b>pro</b> → +thinking-mode models</li>
|
||||
<li><b>max</b> → +deepseek-v4-pro max-thinking, frontier models from each provider</li>
|
||||
</ul>
|
||||
<p>You can request a specific model in the chat composer; the server downgrades to your tier ceiling if it's outside your plan.</p>
|
||||
|
||||
<h2>4 · API surface (if you want to poke it)</h2>
|
||||
<pre><code><span style="color:#7d8590">// health</span>
|
||||
GET /v1/health
|
||||
|
||||
<span style="color:#7d8590">// start an async agentic run (browser-direct mode)</span>
|
||||
POST /v1/runs/start
|
||||
{ goal, context, options, tools_manifest, browser_tools:true }
|
||||
→ { run_id, session_id }
|
||||
|
||||
<span style="color:#7d8590">// the live SSE event stream for that run</span>
|
||||
GET /v1/runs/:id/stream
|
||||
|
||||
<span style="color:#7d8590">// deliver a tool result (in browser-direct mode)</span>
|
||||
POST /v1/runs/:id/tool_result
|
||||
{ call_id, ok, result, error }
|
||||
|
||||
<span style="color:#7d8590">// sync run (non-browser callers; bounded by host timeout)</span>
|
||||
POST /v1/runs
|
||||
{ goal, context, options, tools_manifest, callback_url, callback_secret }
|
||||
</code></pre>
|
||||
|
||||
<h2>5 · Need help?</h2>
|
||||
<p>Open the chat in WP admin and ask. The brain knows about itself.</p>
|
||||
</div></section>
|
||||
`);
|
||||
@@ -10,6 +10,7 @@ import { billingRoutes } from './routes/billing.js';
|
||||
import { oauthRoutes } from './routes/oauth.js';
|
||||
import { dashboardRoutes } from './routes/dashboard.js';
|
||||
import { teamRoutes } from './routes/teams.js';
|
||||
import { landingRoutes } from './routes/landing.js';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
if (config.ALLOW_INSECURE_TLS) {
|
||||
@@ -53,6 +54,7 @@ async function main(): Promise<void> {
|
||||
await app.register(oauthRoutes);
|
||||
await app.register(teamRoutes);
|
||||
await app.register(dashboardRoutes);
|
||||
await app.register(landingRoutes); // GET /, /docs — marketing pages
|
||||
|
||||
app.setErrorHandler((err: FastifyError, _req, reply) => {
|
||||
logger.error({ err }, 'Request failed');
|
||||
|
||||
Reference in New Issue
Block a user