diff --git a/apps/labfox/app/globals.css b/apps/labfox/app/globals.css
new file mode 100644
index 0000000..6350c7b
--- /dev/null
+++ b/apps/labfox/app/globals.css
@@ -0,0 +1,106 @@
+:root {
+ color-scheme: dark;
+ --ground: #100b06;
+ --raised: #191009;
+ --raised-2: #22160c;
+ --porcelain: #f7f0e6;
+ --mist: #b8a892;
+ --line: rgb(247 240 230 / 14%);
+ --accent: #f97316;
+ --accent-2: #fbbf24;
+ --accent-soft: rgb(249 115 22 / 16%);
+ --ok: #62d59d;
+ --run: #fbbf24;
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; background: var(--ground); }
+body { margin: 0; overflow-x: hidden; background: var(--ground); color: var(--porcelain); font-family: var(--font-sans); }
+a { color: inherit; text-underline-offset: 4px; }
+a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
+@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
+
+.shell { width: min(1140px, calc(100% - 3rem)); margin: auto; }
+.skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 20; padding: .7rem 1rem; background: var(--accent); color: #1a1006; font-weight: 700; text-decoration: none; }
+.skip-link:focus { top: 1rem; }
+
+.eyebrow { margin: 0; color: var(--accent); font: 800 .68rem var(--font-mono); letter-spacing: .14em; text-transform: uppercase; }
+.wordmark { font: 800 1rem var(--font-mono); letter-spacing: .12em; text-decoration: none; }
+.wordmark span { color: var(--accent); }
+
+.button { display: inline-flex; align-items: center; justify-content: center; min-height: 46px; border: 1px solid var(--line); padding: .7rem 1.15rem; color: var(--porcelain); font: 800 .72rem var(--font-mono); letter-spacing: .04em; text-decoration: none; border-radius: 10px; }
+.button:hover { border-color: rgb(247 240 230 / 42%); }
+.button.primary { border-color: var(--accent); background: linear-gradient(135deg, var(--accent-2), var(--accent)); color: #1a1006; }
+.button.ghost { min-height: 40px; padding: .5rem .8rem; }
+
+.site-header { position: relative; z-index: 4; min-height: 74px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); }
+.site-header nav { display: flex; align-items: center; gap: .5rem; }
+.site-header nav a:not(.button) { min-height: 44px; display: grid; place-items: center; padding: 0 .6rem; font: 700 .72rem var(--font-mono); letter-spacing: .04em; text-decoration: none; color: var(--mist); }
+.site-header nav a:not(.button):hover { color: var(--porcelain); }
+
+.hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); background: radial-gradient(circle at 82% 12%, rgb(249 115 22 / 22%), transparent 42%), radial-gradient(circle at 96% 84%, rgb(251 191 36 / 12%), transparent 40%), var(--ground); }
+.hero__grid { display: grid; grid-template-columns: minmax(0, 1.02fr) minmax(20rem, .82fr); gap: clamp(2rem, 6vw, 5rem); align-items: center; min-height: min(680px, calc(100vh - 74px)); }
+.hero__copy { padding: clamp(4.5rem, 9vw, 8rem) 0; }
+h1 { max-width: 12ch; margin: 1.2rem 0 1.5rem; font-size: clamp(3.4rem, 7vw, 6.4rem); line-height: .9; letter-spacing: -.06em; }
+h1 em, h2 em { font-family: Georgia, serif; font-weight: 400; font-style: italic; color: var(--accent-2); }
+.lede { max-width: 40rem; margin: 0; color: var(--mist); font-size: clamp(1.05rem, 1.5vw, 1.22rem); line-height: 1.65; }
+.actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2.2rem; }
+.hero__meta { margin: 1.75rem 0 0; color: var(--mist); font: 700 .68rem var(--font-mono); letter-spacing: .1em; }
+
+.hero__panel { position: relative; justify-self: end; width: min(100%, 30rem); padding: 1.25rem; border: 1px solid var(--line); border-radius: 18px; background: linear-gradient(180deg, var(--raised-2), var(--raised)); box-shadow: 0 30px 90px rgb(0 0 0 / 45%); }
+.hero__panel-bar { display: flex; gap: .5rem; margin-bottom: 1.25rem; }
+.hero__panel-bar span { width: 11px; height: 11px; border-radius: 50%; background: rgb(247 240 230 / 20%); }
+.hero__panel-bar span:first-child { background: var(--accent); }
+.hero__mr-label { margin: 0 0 .4rem; color: var(--accent); font: 700 .66rem var(--font-mono); letter-spacing: .08em; }
+.hero__mr h2 { margin: 0; font-size: 1.5rem; line-height: 1.1; letter-spacing: -.02em; }
+.hero__mr-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: 1rem; color: var(--mist); font: 700 .66rem var(--font-mono); }
+.pill { padding: .28rem .55rem; border-radius: 999px; font: 800 .62rem var(--font-mono); letter-spacing: .04em; }
+.pill--open { background: var(--accent-soft); color: var(--accent-2); }
+.pill--pipe { background: rgb(98 213 157 / 16%); color: var(--ok); }
+.hero__pipeline { display: flex; align-items: center; gap: .5rem; margin-top: 1.25rem; padding-top: 1.1rem; border-top: 1px solid var(--line); color: var(--mist); font: 700 .68rem var(--font-mono); letter-spacing: .06em; }
+.dot { width: 9px; height: 9px; border-radius: 50%; }
+.dot--ok { background: var(--ok); }
+.dot--run { background: var(--run); box-shadow: 0 0 0 4px rgb(251 191 36 / 18%); }
+
+.section-intro { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 1.5rem 3rem; align-items: end; }
+.section-intro .eyebrow { grid-column: 1 / -1; }
+h2 { margin: 0; font-size: clamp(2.4rem, 5vw, 4.6rem); line-height: .94; letter-spacing: -.06em; }
+.section-intro > p:last-child { margin: 0; color: var(--mist); line-height: 1.65; align-self: end; }
+
+.features { padding: clamp(5rem, 10vw, 9rem) 0; }
+.feature-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin-top: 3.5rem; border: 1px solid var(--line); background: var(--line); }
+.feature { position: relative; padding: 2rem 1.75rem 2.25rem; background: var(--raised); transition: background .25s ease; }
+.feature:hover { background: var(--raised-2); }
+.feature__num { color: var(--accent); font: 800 .8rem var(--font-mono); letter-spacing: .08em; }
+.feature h3 { margin: 1.5rem 0 .8rem; font-size: 1.5rem; line-height: 1.05; letter-spacing: -.02em; }
+.feature p { margin: 0; color: var(--mist); font-size: .98rem; line-height: 1.6; }
+
+.platforms { padding: 0 0 clamp(5rem, 10vw, 9rem); }
+.platform-rail { display: flex; flex-wrap: wrap; gap: 1px; margin: 3rem 0 0; padding: 0; list-style: none; border: 1px solid var(--line); background: var(--line); }
+.platform-rail li { flex: 1 1 8rem; padding: 1.6rem 1.25rem; background: var(--raised); font: 800 1.1rem var(--font-sans); letter-spacing: -.01em; }
+.platform-rail li::before { content: "→ "; color: var(--accent); }
+
+.close { padding: clamp(5rem, 10vw, 8rem) 0; border-top: 1px solid var(--line); background: radial-gradient(circle at 12% 20%, rgb(249 115 22 / 14%), transparent 42%), var(--ground); }
+.close__grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2rem; align-items: center; }
+.close h2 { max-width: 12ch; }
+.close__grid > div { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; }
+.close__grid p { margin: 0; color: var(--mist); font: 700 .7rem var(--font-mono); letter-spacing: .06em; }
+
+.site-footer { border-top: 1px solid var(--line); padding: clamp(3rem, 6vw, 5rem) 0 2rem; }
+.site-footer__grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 2.5rem; }
+.site-footer__grid > div:first-child p { max-width: 26rem; color: var(--mist); line-height: 1.6; }
+.site-footer nav { display: flex; gap: 3rem; }
+.site-footer nav div { display: flex; flex-direction: column; gap: .55rem; }
+.site-footer nav strong { margin-bottom: .3rem; font: 800 .7rem var(--font-mono); letter-spacing: .1em; text-transform: uppercase; color: var(--porcelain); }
+.site-footer nav a { color: var(--mist); font-size: .92rem; text-decoration: none; }
+.site-footer nav a:hover { color: var(--porcelain); }
+.site-footer__legal { display: flex; justify-content: space-between; margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--line); color: var(--mist); font: 700 .66rem var(--font-mono); letter-spacing: .06em; }
+
+@media (max-width: 900px) {
+ .hero__grid { grid-template-columns: 1fr; }
+ .hero__panel { justify-self: stretch; margin-bottom: 4rem; }
+ .section-intro { grid-template-columns: 1fr; }
+ .feature-grid, .close__grid, .site-footer__grid { grid-template-columns: 1fr; }
+ .site-footer nav { gap: 2rem; }
+ .site-footer__legal { flex-direction: column; gap: .5rem; }
+}
diff --git a/apps/labfox/app/icon.svg b/apps/labfox/app/icon.svg
new file mode 100644
index 0000000..cbbfccc
--- /dev/null
+++ b/apps/labfox/app/icon.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/labfox/app/layout.tsx b/apps/labfox/app/layout.tsx
new file mode 100644
index 0000000..a8fdcf0
--- /dev/null
+++ b/apps/labfox/app/layout.tsx
@@ -0,0 +1,32 @@
+import type { Metadata } from "next";
+import "@theorvane/design-tokens/tokens.css";
+import "./globals.css";
+
+const siteUrl = new URL("https://labfox.theorvane.tech");
+const title = "LabFox — the GitLab workflow client for every platform";
+const description = "LabFox is a cross-platform GitLab client for merge requests, code review, and CI/CD — on Android, iOS, Windows, and macOS, with first-class self-hosted support.";
+
+export const metadata: Metadata = {
+ metadataBase: siteUrl,
+ title,
+ description,
+ applicationName: "LabFox",
+ keywords: ["LabFox", "GitLab", "GitLab client", "merge requests", "code review", "CI/CD", "self-hosted", "Flutter", "cross-platform"],
+ authors: [{ name: "Theorvane", url: "https://theorvane.tech" }],
+ creator: "Theorvane",
+ publisher: "Theorvane",
+ category: "technology",
+ alternates: { canonical: "/" },
+ robots: { index: true, follow: true, googleBot: { index: true, follow: true, "max-image-preview": "large", "max-snippet": -1, "max-video-preview": -1 } },
+ openGraph: { type: "website", url: siteUrl, title, description, siteName: "LabFox", locale: "en_US" },
+ twitter: { card: "summary_large_image", title, description },
+};
+
+export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
+ return (
+
+ {/* Analytics: add the Rybbit script here once a LabFox site id is provisioned. */}
+
{children}
+
+ );
+}
diff --git a/apps/labfox/app/opengraph-image.tsx b/apps/labfox/app/opengraph-image.tsx
new file mode 100644
index 0000000..ebdef30
--- /dev/null
+++ b/apps/labfox/app/opengraph-image.tsx
@@ -0,0 +1,55 @@
+import { ImageResponse } from "next/og";
+
+export const alt = "LabFox — the GitLab workflow client for every platform";
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+
+// The site's own warm dark palette, echoing the amber accent on the landing page.
+const ink = "#f7f0e6";
+const ground = "#100b06";
+const glow = "#2a1608";
+const accent = "#f97316";
+const muted = "#b8a892";
+
+export default function OpenGraphImage() {
+ return new ImageResponse(
+ (
+
+
+
+
+
+ GitLab workflow client
+
+
+
Your GitLab,
+
in hand.
+
+
+ Merge requests, code review, and pipelines — native on every platform, self-hosted included.
+
+
+
+
+
labfox.theorvane.tech
+
Android · iOS · Windows · macOS
+
+
+ ),
+ size,
+ );
+}
diff --git a/apps/labfox/app/page.tsx b/apps/labfox/app/page.tsx
new file mode 100644
index 0000000..82fcecb
--- /dev/null
+++ b/apps/labfox/app/page.tsx
@@ -0,0 +1,92 @@
+import { ExternalLink, SkipLink } from "@theorvane/ui";
+
+const github = "https://github.com/Theorvane/labfox";
+const studio = "https://theorvane.tech/";
+const siteUrl = "https://labfox.theorvane.tech/";
+
+const features = [
+ {
+ number: "01",
+ title: "Merge requests, reviewed in full",
+ body: "Open, filter, and act on merge requests with threaded discussion, diffs, and approvals — the whole review loop, not a read-only mirror.",
+ },
+ {
+ number: "02",
+ title: "Pipelines you can actually watch",
+ body: "Follow CI/CD jobs as they run, read logs inline, and retry or cancel a stage without leaving the request you were reviewing.",
+ },
+ {
+ number: "03",
+ title: "Self-hosted is first-class",
+ body: "Point LabFox at gitlab.com or your own instance. Personal and project tokens stay on the device; nothing is proxied through us.",
+ },
+ {
+ number: "04",
+ title: "One client, every platform",
+ body: "A single Flutter codebase ships a native-feeling app for Android, iOS, Windows, and macOS — the same workflow wherever you work.",
+ },
+] as const;
+
+const platforms = ["Android", "iOS", "Windows", "macOS"] as const;
+
+const schema = {
+ "@context": "https://schema.org",
+ "@type": "SoftwareApplication",
+ name: "LabFox",
+ url: siteUrl,
+ applicationCategory: "DeveloperApplication",
+ operatingSystem: "Android, iOS, Windows, macOS",
+ description: "A cross-platform GitLab workflow client for merge requests, code review, and CI/CD, with first-class self-hosted support.",
+ offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
+ publisher: { "@type": "Organization", name: "Theorvane", url: studio },
+};
+
+export default function HomePage() {
+ return <>
+
+
+
+
+
+
+
+
GitLab workflow client · cross-platform
+
Your GitLab, in hand.
+
LabFox brings merge requests, code review, and pipelines to a fast, native app on every platform you carry — with self-hosted instances treated as first-class, not an afterthought.
+
+
Android · iOS · Windows · macOS · self-hosted
+
+
+
+
Merge request · !482
Gate risky applies behind review Open Pipeline passed 3 approvals
+
build test deploy
+
+
+
+
+
+ What it does / 01
The whole review loop,on your device. LabFox is a client, not a dashboard. Every action you take on the web — approve, comment, retry a job — you take here.
+
+ {features.map((feature) => (
+
+ {feature.number}
+ {feature.title}
+ {feature.body}
+
+ ))}
+
+
+
+
+
+ Open source · MIT
Bring GitLab with you. Get LabFox ↗ A Theorvane product · your tokens stay yours
+
+
+ >;
+}
diff --git a/apps/labfox/app/robots.ts b/apps/labfox/app/robots.ts
new file mode 100644
index 0000000..36b2e8d
--- /dev/null
+++ b/apps/labfox/app/robots.ts
@@ -0,0 +1,11 @@
+import type { MetadataRoute } from "next";
+
+const siteUrl = "https://labfox.theorvane.tech";
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: { userAgent: "*", allow: "/" },
+ sitemap: `${siteUrl}/sitemap.xml`,
+ host: siteUrl,
+ };
+}
diff --git a/apps/labfox/app/sitemap.ts b/apps/labfox/app/sitemap.ts
new file mode 100644
index 0000000..7adfe21
--- /dev/null
+++ b/apps/labfox/app/sitemap.ts
@@ -0,0 +1,9 @@
+import type { MetadataRoute } from "next";
+
+const siteUrl = "https://labfox.theorvane.tech";
+// Bump when published page content changes. A build timestamp would report freshness that did not happen.
+const lastModified = "2026-08-28";
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ return [{ url: `${siteUrl}/`, lastModified, changeFrequency: "monthly", priority: 1 }];
+}
diff --git a/apps/labfox/next-env.d.ts b/apps/labfox/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/apps/labfox/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/labfox/next.config.ts b/apps/labfox/next.config.ts
new file mode 100644
index 0000000..27a1e6b
--- /dev/null
+++ b/apps/labfox/next.config.ts
@@ -0,0 +1,5 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = { turbopack: { root: "../.." } };
+
+export default nextConfig;
diff --git a/apps/labfox/package.json b/apps/labfox/package.json
new file mode 100644
index 0000000..1c54bf1
--- /dev/null
+++ b/apps/labfox/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@theorvane/labfox-site",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": { "dev": "next dev", "build": "next build", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run" },
+ "dependencies": { "@theorvane/design-tokens": "*", "@theorvane/ui": "*", "next": "^16.2.1", "react": "^19.2.0", "react-dom": "^19.2.0" },
+ "devDependencies": { "@testing-library/react": "^16.3.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "jsdom": "^28.1.0", "vitest": "^4.1.0" }
+}
diff --git a/apps/labfox/public/logo.svg b/apps/labfox/public/logo.svg
new file mode 100644
index 0000000..61120da
--- /dev/null
+++ b/apps/labfox/public/logo.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/labfox/test/home.test.tsx b/apps/labfox/test/home.test.tsx
new file mode 100644
index 0000000..b38bd15
--- /dev/null
+++ b/apps/labfox/test/home.test.tsx
@@ -0,0 +1,44 @@
+import { createElement } from "react";
+import { render, screen, within } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import HomePage from "../app/page";
+
+const github = "https://github.com/Theorvane/labfox";
+
+describe("LabFox homepage", () => {
+ it("renders the brand promise and accessible landmarks", () => {
+ render(createElement(HomePage));
+
+ expect(screen.getByRole("heading", { name: /your gitlab/i })).toBeTruthy();
+ const hero = screen.getByRole("heading", { name: /your gitlab/i }).closest("section");
+ expect(hero?.id).toBe("top");
+ expect(hero?.getAttribute("data-testid")).toBe("labfox-hero");
+ expect(screen.getByRole("main")).toBeTruthy();
+ expect(screen.getByRole("navigation", { name: /primary/i })).toBeTruthy();
+ });
+
+ it("routes both primary calls to action to the GitHub repository", () => {
+ render(createElement(HomePage));
+
+ const getLinks = screen.getAllByRole("link", { name: /get labfox/i });
+ expect(getLinks.length).toBeGreaterThanOrEqual(1);
+ for (const link of getLinks) {
+ expect(link.getAttribute("href")).toBe(github);
+ expect(link.getAttribute("target")).toBe("_blank");
+ expect(link.getAttribute("rel")).toContain("noreferrer");
+ }
+ });
+
+ it("documents the review loop and every supported platform", () => {
+ render(createElement(HomePage));
+
+ expect(screen.getByRole("heading", { name: /merge requests, reviewed in full/i })).toBeTruthy();
+ expect(screen.getByRole("heading", { name: /self-hosted is first-class/i })).toBeTruthy();
+
+ const platforms = screen.getByRole("list", { name: /supported platforms/i });
+ for (const name of ["Android", "iOS", "Windows", "macOS"]) {
+ expect(within(platforms).getByText(name)).toBeTruthy();
+ }
+ });
+});
diff --git a/apps/labfox/test/seo.test.tsx b/apps/labfox/test/seo.test.tsx
new file mode 100644
index 0000000..cfcc689
--- /dev/null
+++ b/apps/labfox/test/seo.test.tsx
@@ -0,0 +1,42 @@
+import { createElement } from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { metadata } from "../app/layout";
+import HomePage from "../app/page";
+import { alt as ogAlt, contentType as ogContentType, size as ogSize } from "../app/opengraph-image";
+import robots from "../app/robots";
+import sitemap from "../app/sitemap";
+
+describe("LabFox technical SEO", () => {
+ it("publishes canonical and social metadata for the subdomain", () => {
+ expect(metadata.metadataBase?.toString()).toBe("https://labfox.theorvane.tech/");
+ expect(metadata.alternates?.canonical).toBe("/");
+ expect(metadata.applicationName).toBe("LabFox");
+ expect(metadata.robots).toMatchObject({ index: true, follow: true, googleBot: { index: true, follow: true, "max-image-preview": "large", "max-snippet": -1 } });
+ expect(metadata.openGraph?.url?.toString()).toBe("https://labfox.theorvane.tech/");
+ expect((metadata.twitter as { card?: string } | undefined)?.card).toBe("summary_large_image");
+ });
+
+ it("publishes crawl directives and a canonical sitemap", () => {
+ expect(robots().rules).toMatchObject({ userAgent: "*", allow: "/" });
+ expect(robots().sitemap).toBe("https://labfox.theorvane.tech/sitemap.xml");
+ expect(robots().host).toBe("https://labfox.theorvane.tech");
+ expect(sitemap()).toEqual([{ url: "https://labfox.theorvane.tech/", lastModified: "2026-08-28", changeFrequency: "monthly", priority: 1 }]);
+ });
+
+ it("renders SoftwareApplication JSON-LD naming Theorvane as publisher", () => {
+ render(createElement(HomePage));
+ const schema = screen.getByTestId("product-schema");
+ const parsed = JSON.parse(schema.textContent ?? "{}") as { "@type"?: string; name?: string; publisher?: { name?: string } };
+ expect(parsed["@type"]).toBe("SoftwareApplication");
+ expect(parsed.name).toBe("LabFox");
+ expect(parsed.publisher?.name).toBe("Theorvane");
+ });
+
+ it("publishes a rendered social preview image at the documented size", () => {
+ expect(ogSize).toEqual({ width: 1200, height: 630 });
+ expect(ogContentType).toBe("image/png");
+ expect(ogAlt).toContain("LabFox");
+ });
+});
diff --git a/apps/labfox/tsconfig.json b/apps/labfox/tsconfig.json
new file mode 100644
index 0000000..6439864
--- /dev/null
+++ b/apps/labfox/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "allowJs": false, "incremental": true, "jsx": "react-jsx", "noEmit": true, "plugins": [{ "name": "next" }] },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/apps/labfox/vitest.config.ts b/apps/labfox/vitest.config.ts
new file mode 100644
index 0000000..41268e9
--- /dev/null
+++ b/apps/labfox/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ environment: "jsdom",
+ globals: true,
+ },
+});
diff --git a/apps/planguard/app/globals.css b/apps/planguard/app/globals.css
new file mode 100644
index 0000000..a45ee21
--- /dev/null
+++ b/apps/planguard/app/globals.css
@@ -0,0 +1,109 @@
+:root {
+ color-scheme: dark;
+ --ground: #0c0709;
+ --raised: #150a0e;
+ --raised-2: #1d0d13;
+ --porcelain: #f7ecef;
+ --mist: #b39aa2;
+ --line: rgb(247 236 239 / 14%);
+ --accent: #e11d48;
+ --accent-2: #fb7185;
+ --accent-soft: rgb(225 29 72 / 16%);
+ --ok: #62d59d;
+ --run: #fb7185;
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; background: var(--ground); }
+body { margin: 0; overflow-x: hidden; background: var(--ground); color: var(--porcelain); font-family: var(--font-sans); }
+a { color: inherit; text-underline-offset: 4px; }
+a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
+@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
+
+.shell { width: min(1140px, calc(100% - 3rem)); margin: auto; }
+.skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 20; padding: .7rem 1rem; background: var(--accent); color: #1a0308; font-weight: 700; text-decoration: none; }
+.skip-link:focus { top: 1rem; }
+
+.eyebrow { margin: 0; color: var(--accent); font: 800 .68rem var(--font-mono); letter-spacing: .14em; text-transform: uppercase; }
+.wordmark { font: 800 1rem var(--font-mono); letter-spacing: .12em; text-decoration: none; }
+.wordmark span { color: var(--accent); }
+
+.button { display: inline-flex; align-items: center; justify-content: center; min-height: 46px; border: 1px solid var(--line); padding: .7rem 1.15rem; color: var(--porcelain); font: 800 .72rem var(--font-mono); letter-spacing: .04em; text-decoration: none; border-radius: 10px; }
+.button:hover { border-color: rgb(247 236 239 / 42%); }
+.button.primary { border-color: var(--accent); background: linear-gradient(135deg, var(--accent-2), var(--accent)); color: #1a0308; }
+.button.ghost { min-height: 40px; padding: .5rem .8rem; }
+
+.site-header { position: relative; z-index: 4; min-height: 74px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); }
+.site-header nav { display: flex; align-items: center; gap: .5rem; }
+.site-header nav a:not(.button) { min-height: 44px; display: grid; place-items: center; padding: 0 .6rem; font: 700 .72rem var(--font-mono); letter-spacing: .04em; text-decoration: none; color: var(--mist); }
+.site-header nav a:not(.button):hover { color: var(--porcelain); }
+
+.hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); background: radial-gradient(circle at 82% 12%, rgb(225 29 72 / 22%), transparent 42%), radial-gradient(circle at 96% 84%, rgb(251 113 133 / 12%), transparent 40%), var(--ground); }
+.hero__grid { display: grid; grid-template-columns: minmax(0, 1.02fr) minmax(20rem, .82fr); gap: clamp(2rem, 6vw, 5rem); align-items: center; min-height: min(680px, calc(100vh - 74px)); }
+.hero__copy { padding: clamp(4.5rem, 9vw, 8rem) 0; }
+h1 { max-width: 14ch; margin: 1.2rem 0 1.5rem; font-size: clamp(3.4rem, 7vw, 6.4rem); line-height: .9; letter-spacing: -.06em; }
+h1 em, h2 em { font-family: Georgia, serif; font-weight: 400; font-style: italic; color: var(--accent-2); }
+.lede { max-width: 40rem; margin: 0; color: var(--mist); font-size: clamp(1.05rem, 1.5vw, 1.22rem); line-height: 1.65; }
+.actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2.2rem; }
+.hero__meta { margin: 1.75rem 0 0; color: var(--mist); font: 700 .68rem var(--font-mono); letter-spacing: .1em; }
+
+.hero__panel { position: relative; justify-self: end; width: min(100%, 30rem); padding: 1.25rem; border: 1px solid var(--line); border-radius: 18px; background: linear-gradient(180deg, var(--raised-2), var(--raised)); box-shadow: 0 30px 90px rgb(0 0 0 / 45%); }
+.hero__panel-bar { display: flex; gap: .5rem; margin-bottom: 1.25rem; }
+.hero__panel-bar span { width: 11px; height: 11px; border-radius: 50%; background: rgb(247 236 239 / 20%); }
+.hero__panel-bar span:first-child { background: var(--accent); }
+.hero__mr-label { margin: 0 0 .4rem; color: var(--accent); font: 700 .66rem var(--font-mono); letter-spacing: .08em; }
+.hero__mr h2 { margin: 0; font-size: 1.5rem; line-height: 1.1; letter-spacing: -.02em; }
+.hero__mr-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: 1rem; color: var(--mist); font: 700 .66rem var(--font-mono); }
+.pill { padding: .28rem .55rem; border-radius: 999px; font: 800 .62rem var(--font-mono); letter-spacing: .04em; }
+.pill--risk { background: var(--accent-soft); color: var(--accent-2); }
+.pill--review { background: rgb(251 113 133 / 16%); color: var(--accent-2); }
+.hero__plan { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; margin-top: 1.25rem; padding-top: 1.1rem; border-top: 1px solid var(--line); color: var(--mist); font: 700 .68rem var(--font-mono); letter-spacing: .06em; }
+.hero__plan .count { color: var(--porcelain); }
+.hero__plan .count--change { color: var(--accent-2); }
+.hero__plan .count--destroy { color: var(--accent); }
+.dot { width: 9px; height: 9px; border-radius: 50%; }
+.dot--ok { background: var(--ok); }
+.dot--run { background: var(--run); box-shadow: 0 0 0 4px rgb(251 113 133 / 18%); }
+
+.section-intro { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 1.5rem 3rem; align-items: end; }
+.section-intro .eyebrow { grid-column: 1 / -1; }
+h2 { margin: 0; font-size: clamp(2.4rem, 5vw, 4.6rem); line-height: .94; letter-spacing: -.06em; }
+.section-intro > p:last-child { margin: 0; color: var(--mist); line-height: 1.65; align-self: end; }
+
+.features { padding: clamp(5rem, 10vw, 9rem) 0; }
+.feature-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin-top: 3.5rem; border: 1px solid var(--line); background: var(--line); }
+.feature { position: relative; padding: 2rem 1.75rem 2.25rem; background: var(--raised); transition: background .25s ease; }
+.feature:hover { background: var(--raised-2); }
+.feature__num { color: var(--accent); font: 800 .8rem var(--font-mono); letter-spacing: .08em; }
+.feature h3 { margin: 1.5rem 0 .8rem; font-size: 1.5rem; line-height: 1.05; letter-spacing: -.02em; }
+.feature p { margin: 0; color: var(--mist); font-size: .98rem; line-height: 1.6; }
+
+.checks { padding: 0 0 clamp(5rem, 10vw, 9rem); }
+.check-rail { display: flex; flex-wrap: wrap; gap: 1px; margin: 3rem 0 0; padding: 0; list-style: none; border: 1px solid var(--line); background: var(--line); }
+.check-rail li { flex: 1 1 12rem; padding: 1.6rem 1.25rem; background: var(--raised); font: 800 1.1rem var(--font-sans); letter-spacing: -.01em; }
+.check-rail li::before { content: "→ "; color: var(--accent); }
+
+.close { padding: clamp(5rem, 10vw, 8rem) 0; border-top: 1px solid var(--line); background: radial-gradient(circle at 12% 20%, rgb(225 29 72 / 14%), transparent 42%), var(--ground); }
+.close__grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2rem; align-items: center; }
+.close h2 { max-width: 14ch; }
+.close__grid > div { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; }
+.close__grid p { margin: 0; color: var(--mist); font: 700 .7rem var(--font-mono); letter-spacing: .06em; }
+
+.site-footer { border-top: 1px solid var(--line); padding: clamp(3rem, 6vw, 5rem) 0 2rem; }
+.site-footer__grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 2.5rem; }
+.site-footer__grid > div:first-child p { max-width: 26rem; color: var(--mist); line-height: 1.6; }
+.site-footer nav { display: flex; gap: 3rem; }
+.site-footer nav div { display: flex; flex-direction: column; gap: .55rem; }
+.site-footer nav strong { margin-bottom: .3rem; font: 800 .7rem var(--font-mono); letter-spacing: .1em; text-transform: uppercase; color: var(--porcelain); }
+.site-footer nav a { color: var(--mist); font-size: .92rem; text-decoration: none; }
+.site-footer nav a:hover { color: var(--porcelain); }
+.site-footer__legal { display: flex; justify-content: space-between; margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--line); color: var(--mist); font: 700 .66rem var(--font-mono); letter-spacing: .06em; }
+
+@media (max-width: 900px) {
+ .hero__grid { grid-template-columns: 1fr; }
+ .hero__panel { justify-self: stretch; margin-bottom: 4rem; }
+ .section-intro { grid-template-columns: 1fr; }
+ .feature-grid, .close__grid, .site-footer__grid { grid-template-columns: 1fr; }
+ .site-footer nav { gap: 2rem; }
+ .site-footer__legal { flex-direction: column; gap: .5rem; }
+}
diff --git a/apps/planguard/app/icon.svg b/apps/planguard/app/icon.svg
new file mode 100644
index 0000000..f165484
--- /dev/null
+++ b/apps/planguard/app/icon.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/planguard/app/layout.tsx b/apps/planguard/app/layout.tsx
new file mode 100644
index 0000000..f494118
--- /dev/null
+++ b/apps/planguard/app/layout.tsx
@@ -0,0 +1,32 @@
+import type { Metadata } from "next";
+import "@theorvane/design-tokens/tokens.css";
+import "./globals.css";
+
+const siteUrl = new URL("https://planguard.theorvane.tech");
+const title = "PlanGuard — see the blast radius of a Terraform change before it merges";
+const description = "PlanGuard is an AI-assisted infrastructure change review platform for Terraform pull requests. It reads the plan, surfaces the real risk — destroys, replacements, permission and network changes — before merge, and leaves the apply in your team's hands.";
+
+export const metadata: Metadata = {
+ metadataBase: siteUrl,
+ title,
+ description,
+ applicationName: "PlanGuard",
+ keywords: ["PlanGuard", "Terraform", "Terraform plan", "infrastructure as code", "pull request review", "change review", "blast radius", "IaC", "DevOps", "Theorvane"],
+ authors: [{ name: "Theorvane", url: "https://theorvane.tech" }],
+ creator: "Theorvane",
+ publisher: "Theorvane",
+ category: "technology",
+ alternates: { canonical: "/" },
+ robots: { index: true, follow: true, googleBot: { index: true, follow: true, "max-image-preview": "large", "max-snippet": -1, "max-video-preview": -1 } },
+ openGraph: { type: "website", url: siteUrl, title, description, siteName: "PlanGuard", locale: "en_US" },
+ twitter: { card: "summary_large_image", title, description },
+};
+
+export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
+ return (
+
+ {/* Analytics: add the Rybbit script here once a PlanGuard site id is provisioned. */}
+ {children}
+
+ );
+}
diff --git a/apps/planguard/app/opengraph-image.tsx b/apps/planguard/app/opengraph-image.tsx
new file mode 100644
index 0000000..9c91e33
--- /dev/null
+++ b/apps/planguard/app/opengraph-image.tsx
@@ -0,0 +1,55 @@
+import { ImageResponse } from "next/og";
+
+export const alt = "PlanGuard — see the blast radius of a Terraform change before it merges";
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+
+// The site's own cool-dark palette, echoing the rose accent on the landing page.
+const ink = "#f7ecef";
+const ground = "#0c0709";
+const glow = "#2a0912";
+const accent = "#e11d48";
+const muted = "#b39aa2";
+
+export default function OpenGraphImage() {
+ return new ImageResponse(
+ (
+
+
+
+
+
+ Terraform change review
+
+
+
Know the blast radius
+
before you apply.
+
+
+ Read the plan for every pull request — destroys, replacements, IAM and network changes — before it merges.
+
+
+
+
+
planguard.theorvane.tech
+
We review the change · you keep the apply
+
+
+ ),
+ size,
+ );
+}
diff --git a/apps/planguard/app/page.tsx b/apps/planguard/app/page.tsx
new file mode 100644
index 0000000..7ca18fe
--- /dev/null
+++ b/apps/planguard/app/page.tsx
@@ -0,0 +1,92 @@
+import { ExternalLink, SkipLink } from "@theorvane/ui";
+
+const github = "https://github.com/Theorvane/planguard";
+const studio = "https://theorvane.tech/";
+const siteUrl = "https://planguard.theorvane.tech/";
+
+const features = [
+ {
+ number: "01",
+ title: "The real risk, surfaced per PR",
+ body: "PlanGuard reads the Terraform plan for every pull request and calls out what actually bites — destroys, forced replacements, IAM and network changes — before anyone hits merge.",
+ },
+ {
+ number: "02",
+ title: "The plan, explained in plain language",
+ body: "No scrolling a thousand-line diff. Each change is summarised in words a reviewer can act on: what resource, what happens to it, and why it counts as risk.",
+ },
+ {
+ number: "03",
+ title: "Guardrails you configure",
+ body: "Set the policies that matter to your team — block on destroys, flag public ingress, require a second reviewer past a blast-radius threshold — and PlanGuard enforces them on the PR.",
+ },
+ {
+ number: "04",
+ title: "You keep the apply",
+ body: "PlanGuard reviews the change; it never holds your credentials and never runs terraform apply. The plan stays in your CI, the decision stays with your team.",
+ },
+] as const;
+
+const checks = ["Destroys", "Forced replacements", "IAM & permission changes", "Blast radius"] as const;
+
+const schema = {
+ "@context": "https://schema.org",
+ "@type": "SoftwareApplication",
+ name: "PlanGuard",
+ url: siteUrl,
+ applicationCategory: "DeveloperApplication",
+ operatingSystem: "Web, CI",
+ description: "An AI-assisted infrastructure change review platform that reads the Terraform plan for a pull request and surfaces the real risk — destroys, replacements, permission and network changes — before it merges.",
+ offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
+ publisher: { "@type": "Organization", name: "Theorvane", url: studio },
+};
+
+export default function HomePage() {
+ return <>
+
+
+
+
+
+
+
+
Terraform change review · pull requests
+
Know the blast radius before you apply.
+
PlanGuard reads the Terraform plan on every pull request and surfaces the real risk in the change — destroys, replacements, permission and network shifts — before it merges. It reviews the change; you keep the apply.
+
+
Terraform · pull requests · your CI · your credentials
+
+
+
+
Terraform plan · #128
Review required before merge High risk Review required prod / networking
+
~ 4 to change - 2 to destroy + 1 to add
+
+
+
+
+
+ What it does / 01
The risk in the change,before the merge. PlanGuard is a reviewer, not an applier. It tells you what a plan will really do — then hands the decision back to your team.
+
+ {features.map((feature) => (
+
+ {feature.number}
+ {feature.title}
+ {feature.body}
+
+ ))}
+
+
+
+
+ What it checks / 02
Every plan,read for risk. PlanGuard scans the plan for the changes that actually cause outages and blast radius — the ones a diff makes easy to miss.
+ {checks.map((name) => {name} )}
+
+
+ Open source · MIT
Review the plan. Keep the apply. Get PlanGuard ↗ A Theorvane product · your credentials never leave your CI
+
+
+ >;
+}
diff --git a/apps/planguard/app/robots.ts b/apps/planguard/app/robots.ts
new file mode 100644
index 0000000..64666be
--- /dev/null
+++ b/apps/planguard/app/robots.ts
@@ -0,0 +1,11 @@
+import type { MetadataRoute } from "next";
+
+const siteUrl = "https://planguard.theorvane.tech";
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: { userAgent: "*", allow: "/" },
+ sitemap: `${siteUrl}/sitemap.xml`,
+ host: siteUrl,
+ };
+}
diff --git a/apps/planguard/app/sitemap.ts b/apps/planguard/app/sitemap.ts
new file mode 100644
index 0000000..c35e67e
--- /dev/null
+++ b/apps/planguard/app/sitemap.ts
@@ -0,0 +1,9 @@
+import type { MetadataRoute } from "next";
+
+const siteUrl = "https://planguard.theorvane.tech";
+// Bump when published page content changes. A build timestamp would report freshness that did not happen.
+const lastModified = "2026-08-28";
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ return [{ url: `${siteUrl}/`, lastModified, changeFrequency: "monthly", priority: 1 }];
+}
diff --git a/apps/planguard/next-env.d.ts b/apps/planguard/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/apps/planguard/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/planguard/next.config.ts b/apps/planguard/next.config.ts
new file mode 100644
index 0000000..27a1e6b
--- /dev/null
+++ b/apps/planguard/next.config.ts
@@ -0,0 +1,5 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = { turbopack: { root: "../.." } };
+
+export default nextConfig;
diff --git a/apps/planguard/package.json b/apps/planguard/package.json
new file mode 100644
index 0000000..1a33f55
--- /dev/null
+++ b/apps/planguard/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@theorvane/planguard-site",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": { "dev": "next dev", "build": "next build", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run" },
+ "dependencies": { "@theorvane/design-tokens": "*", "@theorvane/ui": "*", "next": "^16.2.1", "react": "^19.2.0", "react-dom": "^19.2.0" },
+ "devDependencies": { "@testing-library/react": "^16.3.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "jsdom": "^28.1.0", "vitest": "^4.1.0" }
+}
diff --git a/apps/planguard/public/logo.svg b/apps/planguard/public/logo.svg
new file mode 100644
index 0000000..082202f
--- /dev/null
+++ b/apps/planguard/public/logo.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/planguard/test/home.test.tsx b/apps/planguard/test/home.test.tsx
new file mode 100644
index 0000000..773822d
--- /dev/null
+++ b/apps/planguard/test/home.test.tsx
@@ -0,0 +1,44 @@
+import { createElement } from "react";
+import { render, screen, within } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import HomePage from "../app/page";
+
+const github = "https://github.com/Theorvane/planguard";
+
+describe("PlanGuard homepage", () => {
+ it("renders the brand promise and accessible landmarks", () => {
+ render(createElement(HomePage));
+
+ expect(screen.getByRole("heading", { name: /know the blast radius/i })).toBeTruthy();
+ const hero = screen.getByRole("heading", { name: /know the blast radius/i }).closest("section");
+ expect(hero?.id).toBe("top");
+ expect(hero?.getAttribute("data-testid")).toBe("planguard-hero");
+ expect(screen.getByRole("main")).toBeTruthy();
+ expect(screen.getByRole("navigation", { name: /primary/i })).toBeTruthy();
+ });
+
+ it("routes both primary calls to action to the GitHub repository", () => {
+ render(createElement(HomePage));
+
+ const getLinks = screen.getAllByRole("link", { name: /get planguard/i });
+ expect(getLinks.length).toBeGreaterThanOrEqual(1);
+ for (const link of getLinks) {
+ expect(link.getAttribute("href")).toBe(github);
+ expect(link.getAttribute("target")).toBe("_blank");
+ expect(link.getAttribute("rel")).toContain("noreferrer");
+ }
+ });
+
+ it("documents the review loop and every risk signal it checks", () => {
+ render(createElement(HomePage));
+
+ expect(screen.getByRole("heading", { name: /the real risk, surfaced per PR/i })).toBeTruthy();
+ expect(screen.getByRole("heading", { name: /you keep the apply/i })).toBeTruthy();
+
+ const checks = screen.getByRole("list", { name: /risk signals/i });
+ for (const name of ["Destroys", "Forced replacements", "IAM & permission changes", "Blast radius"]) {
+ expect(within(checks).getByText(name)).toBeTruthy();
+ }
+ });
+});
diff --git a/apps/planguard/test/seo.test.tsx b/apps/planguard/test/seo.test.tsx
new file mode 100644
index 0000000..fce7b5b
--- /dev/null
+++ b/apps/planguard/test/seo.test.tsx
@@ -0,0 +1,42 @@
+import { createElement } from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { metadata } from "../app/layout";
+import HomePage from "../app/page";
+import { alt as ogAlt, contentType as ogContentType, size as ogSize } from "../app/opengraph-image";
+import robots from "../app/robots";
+import sitemap from "../app/sitemap";
+
+describe("PlanGuard technical SEO", () => {
+ it("publishes canonical and social metadata for the subdomain", () => {
+ expect(metadata.metadataBase?.toString()).toBe("https://planguard.theorvane.tech/");
+ expect(metadata.alternates?.canonical).toBe("/");
+ expect(metadata.applicationName).toBe("PlanGuard");
+ expect(metadata.robots).toMatchObject({ index: true, follow: true, googleBot: { index: true, follow: true, "max-image-preview": "large", "max-snippet": -1 } });
+ expect(metadata.openGraph?.url?.toString()).toBe("https://planguard.theorvane.tech/");
+ expect((metadata.twitter as { card?: string } | undefined)?.card).toBe("summary_large_image");
+ });
+
+ it("publishes crawl directives and a canonical sitemap", () => {
+ expect(robots().rules).toMatchObject({ userAgent: "*", allow: "/" });
+ expect(robots().sitemap).toBe("https://planguard.theorvane.tech/sitemap.xml");
+ expect(robots().host).toBe("https://planguard.theorvane.tech");
+ expect(sitemap()).toEqual([{ url: "https://planguard.theorvane.tech/", lastModified: "2026-08-28", changeFrequency: "monthly", priority: 1 }]);
+ });
+
+ it("renders SoftwareApplication JSON-LD naming Theorvane as publisher", () => {
+ render(createElement(HomePage));
+ const schema = screen.getByTestId("product-schema");
+ const parsed = JSON.parse(schema.textContent ?? "{}") as { "@type"?: string; name?: string; publisher?: { name?: string } };
+ expect(parsed["@type"]).toBe("SoftwareApplication");
+ expect(parsed.name).toBe("PlanGuard");
+ expect(parsed.publisher?.name).toBe("Theorvane");
+ });
+
+ it("publishes a rendered social preview image at the documented size", () => {
+ expect(ogSize).toEqual({ width: 1200, height: 630 });
+ expect(ogContentType).toBe("image/png");
+ expect(ogAlt).toContain("PlanGuard");
+ });
+});
diff --git a/apps/planguard/tsconfig.json b/apps/planguard/tsconfig.json
new file mode 100644
index 0000000..6439864
--- /dev/null
+++ b/apps/planguard/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "allowJs": false, "incremental": true, "jsx": "react-jsx", "noEmit": true, "plugins": [{ "name": "next" }] },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/apps/planguard/vitest.config.ts b/apps/planguard/vitest.config.ts
new file mode 100644
index 0000000..41268e9
--- /dev/null
+++ b/apps/planguard/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ environment: "jsdom",
+ globals: true,
+ },
+});
diff --git a/apps/proxmox/app/globals.css b/apps/proxmox/app/globals.css
new file mode 100644
index 0000000..e0eaae2
--- /dev/null
+++ b/apps/proxmox/app/globals.css
@@ -0,0 +1,111 @@
+:root {
+ color-scheme: dark;
+ --ground: #06110f;
+ --raised: #08171a;
+ --raised-2: #0a1e22;
+ --porcelain: #eaf6f6;
+ --mist: #8fb0b0;
+ --line: rgb(234 246 246 / 14%);
+ --accent: #0891b2;
+ --accent-2: #22d3ee;
+ --accent-soft: rgb(8 145 178 / 16%);
+ --ok: #62d59d;
+ --warn: #fbbf24;
+ --run: #fbbf24;
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; background: var(--ground); }
+body { margin: 0; overflow-x: hidden; background: var(--ground); color: var(--porcelain); font-family: var(--font-sans); }
+a { color: inherit; text-underline-offset: 4px; }
+a:focus-visible, button:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 4px; }
+@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
+
+.shell { width: min(1140px, calc(100% - 3rem)); margin: auto; }
+.skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 20; padding: .7rem 1rem; background: var(--accent-2); color: #04211f; font-weight: 700; text-decoration: none; }
+.skip-link:focus { top: 1rem; }
+
+.eyebrow { margin: 0; color: var(--accent-2); font: 800 .68rem var(--font-mono); letter-spacing: .14em; text-transform: uppercase; }
+.wordmark { font: 800 1rem var(--font-mono); letter-spacing: .12em; text-decoration: none; }
+.wordmark span { color: var(--accent-2); }
+
+.button { display: inline-flex; align-items: center; justify-content: center; min-height: 46px; border: 1px solid var(--line); padding: .7rem 1.15rem; color: var(--porcelain); font: 800 .72rem var(--font-mono); letter-spacing: .04em; text-decoration: none; border-radius: 10px; }
+.button:hover { border-color: rgb(234 246 246 / 42%); }
+.button.primary { border-color: var(--accent); background: linear-gradient(135deg, var(--accent-2), var(--accent)); color: #04211f; }
+.button.ghost { min-height: 40px; padding: .5rem .8rem; }
+
+.site-header { position: relative; z-index: 4; min-height: 74px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); }
+.site-header nav { display: flex; align-items: center; gap: .5rem; }
+.site-header nav a:not(.button) { min-height: 44px; display: grid; place-items: center; padding: 0 .6rem; font: 700 .72rem var(--font-mono); letter-spacing: .04em; text-decoration: none; color: var(--mist); }
+.site-header nav a:not(.button):hover { color: var(--porcelain); }
+
+.hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); background: radial-gradient(circle at 82% 12%, rgb(8 145 178 / 26%), transparent 42%), radial-gradient(circle at 96% 84%, rgb(34 211 238 / 12%), transparent 40%), var(--ground); }
+.hero__grid { display: grid; grid-template-columns: minmax(0, 1.02fr) minmax(20rem, .82fr); gap: clamp(2rem, 6vw, 5rem); align-items: center; min-height: min(680px, calc(100vh - 74px)); }
+.hero__copy { padding: clamp(4.5rem, 9vw, 8rem) 0; }
+h1 { max-width: 14ch; margin: 1.2rem 0 1.5rem; font-size: clamp(3.4rem, 7vw, 6.4rem); line-height: .9; letter-spacing: -.06em; }
+h1 em, h2 em { font-family: Georgia, serif; font-weight: 400; font-style: italic; color: var(--accent-2); }
+.lede { max-width: 40rem; margin: 0; color: var(--mist); font-size: clamp(1.05rem, 1.5vw, 1.22rem); line-height: 1.65; }
+.actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2.2rem; }
+.hero__meta { margin: 1.75rem 0 0; color: var(--mist); font: 700 .68rem var(--font-mono); letter-spacing: .1em; }
+
+.hero__panel { position: relative; justify-self: end; width: min(100%, 30rem); padding: 1.25rem; border: 1px solid var(--line); border-radius: 18px; background: linear-gradient(180deg, var(--raised-2), var(--raised)); box-shadow: 0 30px 90px rgb(0 0 0 / 45%); }
+.hero__panel-bar { display: flex; gap: .5rem; margin-bottom: 1.25rem; }
+.hero__panel-bar span { width: 11px; height: 11px; border-radius: 50%; background: rgb(234 246 246 / 20%); }
+.hero__panel-bar span:first-child { background: var(--accent-2); }
+.hero__call-label { margin: 0 0 .4rem; color: var(--accent-2); font: 700 .66rem var(--font-mono); letter-spacing: .08em; }
+.hero__call h2 { margin: 0; font-size: 1.5rem; line-height: 1.1; letter-spacing: -.02em; }
+.hero__call-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: 1rem; color: var(--mist); font: 700 .66rem var(--font-mono); }
+.pill { padding: .28rem .55rem; border-radius: 999px; font: 800 .62rem var(--font-mono); letter-spacing: .04em; }
+.pill--gate { background: rgb(251 191 36 / 16%); color: var(--warn); }
+.pill--read { background: var(--accent-soft); color: var(--accent-2); }
+.hero__log { display: flex; flex-direction: column; gap: .55rem; margin-top: 1.25rem; padding-top: 1.1rem; border-top: 1px solid var(--line); color: var(--mist); font: 700 .68rem var(--font-mono); letter-spacing: .04em; }
+.hero__log span { display: flex; align-items: center; gap: .5rem; }
+.dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
+.dot--ok { background: var(--ok); }
+.dot--gate { background: var(--warn); box-shadow: 0 0 0 4px rgb(251 191 36 / 18%); }
+.dot--run { background: var(--accent-2); box-shadow: 0 0 0 4px rgb(34 211 238 / 18%); }
+
+.section-intro { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 1.5rem 3rem; align-items: end; }
+.section-intro .eyebrow { grid-column: 1 / -1; }
+h2 { margin: 0; font-size: clamp(2.4rem, 5vw, 4.6rem); line-height: .94; letter-spacing: -.06em; }
+.section-intro > p:last-child { margin: 0; color: var(--mist); line-height: 1.65; align-self: end; }
+
+.features { padding: clamp(5rem, 10vw, 9rem) 0; }
+.feature-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin-top: 3.5rem; border: 1px solid var(--line); background: var(--line); }
+.feature { position: relative; padding: 2rem 1.75rem 2.25rem; background: var(--raised); transition: background .25s ease; }
+.feature:hover { background: var(--raised-2); }
+.feature__num { color: var(--accent-2); font: 800 .8rem var(--font-mono); letter-spacing: .08em; }
+.feature h3 { margin: 1.5rem 0 .8rem; font-size: 1.5rem; line-height: 1.05; letter-spacing: -.02em; }
+.feature p { margin: 0; color: var(--mist); font-size: .98rem; line-height: 1.6; }
+
+.boundaries { padding: 0 0 clamp(5rem, 10vw, 9rem); }
+.boundary-rail { display: flex; flex-wrap: wrap; gap: 1px; margin: 3rem 0 0; padding: 0; list-style: none; border: 1px solid var(--line); background: var(--line); }
+.boundary-rail li { flex: 1 1 12rem; padding: 1.6rem 1.25rem; background: var(--raised); }
+.boundary-rail strong { display: block; font: 800 1.05rem var(--font-sans); letter-spacing: -.01em; }
+.boundary-rail strong::before { content: "→ "; color: var(--accent-2); }
+.boundary-rail span { display: block; margin-top: .5rem; color: var(--mist); font-size: .9rem; line-height: 1.55; }
+
+.close { padding: clamp(5rem, 10vw, 8rem) 0; border-top: 1px solid var(--line); background: radial-gradient(circle at 12% 20%, rgb(8 145 178 / 16%), transparent 42%), var(--ground); }
+.close__grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2rem; align-items: center; }
+.close h2 { max-width: 12ch; }
+.close__grid > div { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; }
+.close__grid p { margin: 0; color: var(--mist); font: 700 .7rem var(--font-mono); letter-spacing: .06em; }
+
+.site-footer { border-top: 1px solid var(--line); padding: clamp(3rem, 6vw, 5rem) 0 2rem; }
+.site-footer__grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 2.5rem; }
+.site-footer__grid > div:first-child p { max-width: 26rem; color: var(--mist); line-height: 1.6; }
+.site-footer nav { display: flex; gap: 3rem; }
+.site-footer nav div { display: flex; flex-direction: column; gap: .55rem; }
+.site-footer nav strong { margin-bottom: .3rem; font: 800 .7rem var(--font-mono); letter-spacing: .1em; text-transform: uppercase; color: var(--porcelain); }
+.site-footer nav a { color: var(--mist); font-size: .92rem; text-decoration: none; }
+.site-footer nav a:hover { color: var(--porcelain); }
+.site-footer__legal { display: flex; justify-content: space-between; margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--line); color: var(--mist); font: 700 .66rem var(--font-mono); letter-spacing: .06em; }
+
+@media (max-width: 900px) {
+ .hero__grid { grid-template-columns: 1fr; }
+ .hero__panel { justify-self: stretch; margin-bottom: 4rem; }
+ .section-intro { grid-template-columns: 1fr; }
+ .feature-grid, .close__grid, .site-footer__grid { grid-template-columns: 1fr; }
+ .site-footer nav { gap: 2rem; }
+ .site-footer__legal { flex-direction: column; gap: .5rem; }
+}
diff --git a/apps/proxmox/app/icon.svg b/apps/proxmox/app/icon.svg
new file mode 100644
index 0000000..33e483a
--- /dev/null
+++ b/apps/proxmox/app/icon.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/proxmox/app/layout.tsx b/apps/proxmox/app/layout.tsx
new file mode 100644
index 0000000..8ea01ce
--- /dev/null
+++ b/apps/proxmox/app/layout.tsx
@@ -0,0 +1,32 @@
+import type { Metadata } from "next";
+import "@theorvane/design-tokens/tokens.css";
+import "./globals.css";
+
+const siteUrl = new URL("https://proxmox.theorvane.tech");
+const title = "Proxmox MCP — a safety-gated MCP server for Proxmox VE";
+const description = "Proxmox MCP is a safety-gated Model Context Protocol server for the Proxmox VE API. Let an AI agent list nodes, VMs, and containers and read status, while every mutating operation stays behind explicit, auditable boundaries.";
+
+export const metadata: Metadata = {
+ metadataBase: siteUrl,
+ title,
+ description,
+ applicationName: "Proxmox MCP",
+ keywords: ["Proxmox MCP", "Proxmox VE", "MCP", "Model Context Protocol", "MCP server", "TypeMCP", "AI agent", "virtualization", "safety-gated", "developer tool"],
+ authors: [{ name: "Theorvane", url: "https://theorvane.tech" }],
+ creator: "Theorvane",
+ publisher: "Theorvane",
+ category: "technology",
+ alternates: { canonical: "/" },
+ robots: { index: true, follow: true, googleBot: { index: true, follow: true, "max-image-preview": "large", "max-snippet": -1, "max-video-preview": -1 } },
+ openGraph: { type: "website", url: siteUrl, title, description, siteName: "Proxmox MCP", locale: "en_US" },
+ twitter: { card: "summary_large_image", title, description },
+};
+
+export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
+ return (
+
+ {/* Analytics: add the Rybbit script here once a Proxmox MCP site id is provisioned. */}
+ {children}
+
+ );
+}
diff --git a/apps/proxmox/app/opengraph-image.tsx b/apps/proxmox/app/opengraph-image.tsx
new file mode 100644
index 0000000..d7443af
--- /dev/null
+++ b/apps/proxmox/app/opengraph-image.tsx
@@ -0,0 +1,55 @@
+import { ImageResponse } from "next/og";
+
+export const alt = "Proxmox MCP — a safety-gated MCP server for Proxmox VE";
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+
+// The site's own cool teal-ink palette, echoing the cyan accent on the landing page.
+const ink = "#eaf6f6";
+const ground = "#06110f";
+const glow = "#08312f";
+const accent = "#22d3ee";
+const muted = "#8fb0b0";
+
+export default function OpenGraphImage() {
+ return new ImageResponse(
+ (
+
+
+
+
+
+ Safety-gated MCP server
+
+
+
Proxmox,
+
behind a safety gate.
+
+
+ An MCP server for the Proxmox VE API — reads stay open, every mutation stays behind explicit, auditable boundaries.
+
+
+
+
+
proxmox.theorvane.tech
+
Built on TypeMCP
+
+
+ ),
+ size,
+ );
+}
diff --git a/apps/proxmox/app/page.tsx b/apps/proxmox/app/page.tsx
new file mode 100644
index 0000000..c2fdc43
--- /dev/null
+++ b/apps/proxmox/app/page.tsx
@@ -0,0 +1,98 @@
+import { ExternalLink, SkipLink } from "@theorvane/ui";
+
+const github = "https://github.com/Theorvane/proxmox-mcp";
+const studio = "https://theorvane.tech/";
+const typemcp = "https://typemcp.theorvane.tech/";
+const siteUrl = "https://proxmox.theorvane.tech/";
+
+const features = [
+ {
+ number: "01",
+ title: "Mutations stay behind a gate",
+ body: "Start, stop, snapshot, delete — every write is gated behind an explicit confirmation and a dry-run that shows what would change, so an agent can never mutate a host on a hunch.",
+ },
+ {
+ number: "02",
+ title: "A typed MCP contract",
+ body: "Built on TypeMCP's decorator-first typed contracts, every tool has a declared shape and a declared boundary. No hand-rolled JSON, no raw API surface leaking through.",
+ },
+ {
+ number: "03",
+ title: "Read and write, cleanly separated",
+ body: "Reads — nodes, VMs, containers, status — stay open and fast. Writes are a separate, named class of operation, and every one of them lands in an auditable log.",
+ },
+ {
+ number: "04",
+ title: "Works with any MCP client",
+ body: "Point any MCP-compatible client at it — an autonomous agent, an assistant, your own tooling. The same gated contract governs all of them the same way.",
+ },
+] as const;
+
+const boundaries = [
+ { title: "Read operations", body: "List nodes, VMs, and containers and read status — open, no confirmation needed." },
+ { title: "Gated mutations", body: "Start, stop, and snapshot require an explicit confirmation before they run." },
+ { title: "Dry-run first", body: "Preview the exact changes a write would make before committing to it." },
+ { title: "Audit log", body: "Every mutating call is recorded — who asked, what ran, what changed." },
+] as const;
+
+const schema = {
+ "@context": "https://schema.org",
+ "@type": "SoftwareApplication",
+ name: "Proxmox MCP",
+ url: siteUrl,
+ applicationCategory: "DeveloperApplication",
+ operatingSystem: "Linux, macOS, Windows",
+ description: "A safety-gated Model Context Protocol server for the Proxmox VE API, exposing reads openly while keeping every mutating operation behind explicit, auditable boundaries.",
+ offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
+ publisher: { "@type": "Organization", name: "Theorvane", url: studio },
+};
+
+export default function HomePage() {
+ return <>
+
+
+
+
+
+
+
+
Safety-gated MCP server · Proxmox VE
+
Proxmox, behind a safety gate.
+
Proxmox MCP lets an AI agent operate a Proxmox VE host — list nodes, read status, snapshot, start and stop — but every mutating call stays behind an explicit, auditable boundary instead of raw API access.
+
+
Reads open · writes gated · built on TypeMCP
+
+
+
+
MCP tool call · vm.stop
Requires confirmation Gated dry-run available node1 / 108
+
vm.stop node1/108 — confirm to proceed vm.snapshot — dry-run: 1 change planned node.status node1 — online · 42% mem
+
+
+
+
+
+ What it does / 01
Full control,never raw access. Proxmox MCP is a boundary, not a passthrough. Reads flow freely; every mutation is named, previewable, confirmed, and logged.
+
+ {features.map((feature) => (
+
+ {feature.number}
+ {feature.title}
+ {feature.body}
+
+ ))}
+
+
+
+
+ Where the line is / 02
Reads open.Writes gated. The whole design is one boundary drawn on purpose: what an agent can see, and what it must ask before it does.
+ {boundaries.map((item) => {item.title} {item.body} )}
+
+
+ Open source · MIT
Give an agent Proxmox, not the keys. Get Proxmox MCP ↗ A Theorvane product · built on TypeMCP
+
+
+ >;
+}
diff --git a/apps/proxmox/app/robots.ts b/apps/proxmox/app/robots.ts
new file mode 100644
index 0000000..e8a3c8b
--- /dev/null
+++ b/apps/proxmox/app/robots.ts
@@ -0,0 +1,11 @@
+import type { MetadataRoute } from "next";
+
+const siteUrl = "https://proxmox.theorvane.tech";
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: { userAgent: "*", allow: "/" },
+ sitemap: `${siteUrl}/sitemap.xml`,
+ host: siteUrl,
+ };
+}
diff --git a/apps/proxmox/app/sitemap.ts b/apps/proxmox/app/sitemap.ts
new file mode 100644
index 0000000..e66d080
--- /dev/null
+++ b/apps/proxmox/app/sitemap.ts
@@ -0,0 +1,9 @@
+import type { MetadataRoute } from "next";
+
+const siteUrl = "https://proxmox.theorvane.tech";
+// Bump when published page content changes. A build timestamp would report freshness that did not happen.
+const lastModified = "2026-08-28";
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ return [{ url: `${siteUrl}/`, lastModified, changeFrequency: "monthly", priority: 1 }];
+}
diff --git a/apps/proxmox/next-env.d.ts b/apps/proxmox/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/apps/proxmox/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/proxmox/next.config.ts b/apps/proxmox/next.config.ts
new file mode 100644
index 0000000..27a1e6b
--- /dev/null
+++ b/apps/proxmox/next.config.ts
@@ -0,0 +1,5 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = { turbopack: { root: "../.." } };
+
+export default nextConfig;
diff --git a/apps/proxmox/package.json b/apps/proxmox/package.json
new file mode 100644
index 0000000..9a7a1be
--- /dev/null
+++ b/apps/proxmox/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@theorvane/proxmox-mcp-site",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": { "dev": "next dev", "build": "next build", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run" },
+ "dependencies": { "@theorvane/design-tokens": "*", "@theorvane/ui": "*", "next": "^16.2.1", "react": "^19.2.0", "react-dom": "^19.2.0" },
+ "devDependencies": { "@testing-library/react": "^16.3.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "jsdom": "^28.1.0", "vitest": "^4.1.0" }
+}
diff --git a/apps/proxmox/public/logo.svg b/apps/proxmox/public/logo.svg
new file mode 100644
index 0000000..50d8a54
--- /dev/null
+++ b/apps/proxmox/public/logo.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/proxmox/test/home.test.tsx b/apps/proxmox/test/home.test.tsx
new file mode 100644
index 0000000..a323fec
--- /dev/null
+++ b/apps/proxmox/test/home.test.tsx
@@ -0,0 +1,44 @@
+import { createElement } from "react";
+import { render, screen, within } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import HomePage from "../app/page";
+
+const github = "https://github.com/Theorvane/proxmox-mcp";
+
+describe("Proxmox MCP homepage", () => {
+ it("renders the brand promise and accessible landmarks", () => {
+ render(createElement(HomePage));
+
+ expect(screen.getByRole("heading", { name: /proxmox, behind a safety gate/i })).toBeTruthy();
+ const hero = screen.getByRole("heading", { name: /proxmox, behind a safety gate/i }).closest("section");
+ expect(hero?.id).toBe("top");
+ expect(hero?.getAttribute("data-testid")).toBe("proxmox-hero");
+ expect(screen.getByRole("main")).toBeTruthy();
+ expect(screen.getByRole("navigation", { name: /primary/i })).toBeTruthy();
+ });
+
+ it("routes both primary calls to action to the GitHub repository", () => {
+ render(createElement(HomePage));
+
+ const getLinks = screen.getAllByRole("link", { name: /get proxmox mcp/i });
+ expect(getLinks.length).toBeGreaterThanOrEqual(1);
+ for (const link of getLinks) {
+ expect(link.getAttribute("href")).toBe(github);
+ expect(link.getAttribute("target")).toBe("_blank");
+ expect(link.getAttribute("rel")).toContain("noreferrer");
+ }
+ });
+
+ it("documents the gated design and every safety boundary", () => {
+ render(createElement(HomePage));
+
+ expect(screen.getByRole("heading", { name: /mutations stay behind a gate/i })).toBeTruthy();
+ expect(screen.getByRole("heading", { name: /read and write, cleanly separated/i })).toBeTruthy();
+
+ const boundaries = screen.getByRole("list", { name: /safety boundaries/i });
+ for (const name of ["Read operations", "Gated mutations", "Dry-run first", "Audit log"]) {
+ expect(within(boundaries).getByText(name)).toBeTruthy();
+ }
+ });
+});
diff --git a/apps/proxmox/test/seo.test.tsx b/apps/proxmox/test/seo.test.tsx
new file mode 100644
index 0000000..4503860
--- /dev/null
+++ b/apps/proxmox/test/seo.test.tsx
@@ -0,0 +1,42 @@
+import { createElement } from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { metadata } from "../app/layout";
+import HomePage from "../app/page";
+import { alt as ogAlt, contentType as ogContentType, size as ogSize } from "../app/opengraph-image";
+import robots from "../app/robots";
+import sitemap from "../app/sitemap";
+
+describe("Proxmox MCP technical SEO", () => {
+ it("publishes canonical and social metadata for the subdomain", () => {
+ expect(metadata.metadataBase?.toString()).toBe("https://proxmox.theorvane.tech/");
+ expect(metadata.alternates?.canonical).toBe("/");
+ expect(metadata.applicationName).toBe("Proxmox MCP");
+ expect(metadata.robots).toMatchObject({ index: true, follow: true, googleBot: { index: true, follow: true, "max-image-preview": "large", "max-snippet": -1 } });
+ expect(metadata.openGraph?.url?.toString()).toBe("https://proxmox.theorvane.tech/");
+ expect((metadata.twitter as { card?: string } | undefined)?.card).toBe("summary_large_image");
+ });
+
+ it("publishes crawl directives and a canonical sitemap", () => {
+ expect(robots().rules).toMatchObject({ userAgent: "*", allow: "/" });
+ expect(robots().sitemap).toBe("https://proxmox.theorvane.tech/sitemap.xml");
+ expect(robots().host).toBe("https://proxmox.theorvane.tech");
+ expect(sitemap()).toEqual([{ url: "https://proxmox.theorvane.tech/", lastModified: "2026-08-28", changeFrequency: "monthly", priority: 1 }]);
+ });
+
+ it("renders SoftwareApplication JSON-LD naming Theorvane as publisher", () => {
+ render(createElement(HomePage));
+ const schema = screen.getByTestId("product-schema");
+ const parsed = JSON.parse(schema.textContent ?? "{}") as { "@type"?: string; name?: string; publisher?: { name?: string } };
+ expect(parsed["@type"]).toBe("SoftwareApplication");
+ expect(parsed.name).toBe("Proxmox MCP");
+ expect(parsed.publisher?.name).toBe("Theorvane");
+ });
+
+ it("publishes a rendered social preview image at the documented size", () => {
+ expect(ogSize).toEqual({ width: 1200, height: 630 });
+ expect(ogContentType).toBe("image/png");
+ expect(ogAlt).toContain("Proxmox");
+ });
+});
diff --git a/apps/proxmox/tsconfig.json b/apps/proxmox/tsconfig.json
new file mode 100644
index 0000000..6439864
--- /dev/null
+++ b/apps/proxmox/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "allowJs": false, "incremental": true, "jsx": "react-jsx", "noEmit": true, "plugins": [{ "name": "next" }] },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/apps/proxmox/vitest.config.ts b/apps/proxmox/vitest.config.ts
new file mode 100644
index 0000000..41268e9
--- /dev/null
+++ b/apps/proxmox/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ environment: "jsdom",
+ globals: true,
+ },
+});
diff --git a/apps/theorvane/app/globals.css b/apps/theorvane/app/globals.css
index fa99233..281d46f 100644
--- a/apps/theorvane/app/globals.css
+++ b/apps/theorvane/app/globals.css
@@ -49,9 +49,12 @@ h1 em, h2 em { font-family: Georgia, serif; font-weight: 400; }
.studio-orbit__core span { color: var(--signal); font: 700 2rem Georgia, serif; }
.studio-orbit__node { position: absolute; z-index: 3; display: grid; place-items: center; width: 74px; aspect-ratio: 1; border: 1px solid var(--line); border-radius: 16px; background: #101426; box-shadow: 11px 11px 0 rgb(0 0 0 / 20%); }
.studio-orbit__node img { width: 55%; height: 55%; object-fit: contain; }
-.studio-orbit__node--mcp { top: 9%; left: 15%; }
-.studio-orbit__node--chain { right: 7%; bottom: 20%; }
-.studio-orbit__node--video { bottom: 5%; left: 24%; }
+.studio-orbit__node--mcp { top: 24.8%; left: 12%; }
+.studio-orbit__node--proxmox { top: 24.8%; right: 12%; }
+.studio-orbit__node--chain { top: 59.8%; right: 12%; }
+.studio-orbit__node--guard { bottom: 7.3%; left: 42.3%; }
+.studio-orbit__node--scene { top: 59.8%; left: 12%; }
+.studio-orbit__node--fox { top: 7.3%; left: 42.3%; }
.studio-orbit p { position: absolute; right: -4%; bottom: -1.5rem; margin: 0; color: var(--mist); font: 800 .62rem var(--font-mono); letter-spacing: .13em; }
.studio-system { padding: clamp(5rem, 10vw, 10rem) 0; background: var(--porcelain); color: var(--ink); }
@@ -84,6 +87,9 @@ h2 { margin: 0; font-size: clamp(2.7rem, 5.5vw, 5.6rem); line-height: .92; lette
.product-editorial__mark img { display: block; width: 62%; height: 62%; object-fit: contain; }
.product-editorial__feature:nth-child(2) .product-editorial__art { background: radial-gradient(circle at 36% 30%, rgb(98 213 157 / 22%), transparent 44%), #0a1215; }
.product-editorial__feature:nth-child(3) .product-editorial__art { background: radial-gradient(circle at 36% 30%, rgb(169 141 255 / 22%), transparent 42%), radial-gradient(circle at 72% 78%, rgb(98 213 157 / 17%), transparent 34%), #10101a; }
+.product-editorial__feature:nth-child(4) .product-editorial__art { background: radial-gradient(circle at 36% 30%, rgb(251 146 60 / 22%), transparent 44%), #171009; }
+.product-editorial__feature:nth-child(5) .product-editorial__art { background: radial-gradient(circle at 36% 30%, rgb(244 63 94 / 22%), transparent 44%), #170a10; }
+.product-editorial__feature:nth-child(6) .product-editorial__art { background: radial-gradient(circle at 36% 30%, rgb(34 211 238 / 22%), transparent 44%), #081416; }
.principles { padding: clamp(5.5rem, 10vw, 10rem) 0; border-top: 1px solid var(--line); }
.principles__heading { max-width: 54rem; }
diff --git a/apps/theorvane/app/page.tsx b/apps/theorvane/app/page.tsx
index ec69240..d4d61bf 100644
--- a/apps/theorvane/app/page.tsx
+++ b/apps/theorvane/app/page.tsx
@@ -7,6 +7,9 @@ const github = "https://github.com/Theorvane";
const typeMcp = "https://typemcp.theorvane.tech/";
const typeChain = "https://typechain.theorvane.tech/";
const openScene = "https://openscene.app/";
+const labFox = "https://labfox.theorvane.tech/";
+const planGuard = "https://planguard.theorvane.tech/";
+const proxmoxMcp = "https://proxmox.theorvane.tech/";
const products = [
{
@@ -36,6 +39,33 @@ const products = [
number: "03",
signal: "Local workflow",
},
+ {
+ description: "A cross-platform GitLab workflow client for merge requests, code review, and CI/CD—on Android, iOS, Windows, and macOS, with self-hosted support.",
+ href: labFox,
+ icon: "/products/labfox.svg",
+ iconAlt: "LabFox official product mark",
+ name: "LabFox",
+ number: "04",
+ signal: "GitLab client",
+ },
+ {
+ description: "An AI-assisted review platform for Terraform pull requests. It surfaces the real risk in an infrastructure change before it merges, and leaves the apply in your hands.",
+ href: planGuard,
+ icon: "/products/planguard.svg",
+ iconAlt: "PlanGuard official product mark",
+ name: "PlanGuard",
+ number: "05",
+ signal: "Infra review",
+ },
+ {
+ description: "A safety-gated TypeMCP server for the Proxmox VE API. It exposes virtualization operations behind explicit, auditable boundaries instead of raw access.",
+ href: proxmoxMcp,
+ icon: "/products/proxmox-mcp.svg",
+ iconAlt: "Proxmox MCP official product mark",
+ name: "Proxmox MCP",
+ number: "06",
+ signal: "Safety-gated MCP",
+ },
] as const;
const schema = {
@@ -69,6 +99,9 @@ export default function HomePage() {
+
+
+
THE SYSTEM STAYS YOURS
@@ -82,7 +115,7 @@ export default function HomePage() {
- Product index / 02
Choose a focused tool. Three products. Three concrete boundaries. No platform-shaped abstraction around them.
+ Product index / 02
Choose a focused tool. Six products. Six concrete boundaries. No platform-shaped abstraction around them.
@@ -93,6 +126,6 @@ export default function HomePage() {
Theorvane / Open by default
Explore the products.Keep the system yours.
-
+
>;
}
diff --git a/apps/theorvane/public/products/labfox.svg b/apps/theorvane/public/products/labfox.svg
new file mode 100644
index 0000000..61120da
--- /dev/null
+++ b/apps/theorvane/public/products/labfox.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/theorvane/public/products/planguard.svg b/apps/theorvane/public/products/planguard.svg
new file mode 100644
index 0000000..082202f
--- /dev/null
+++ b/apps/theorvane/public/products/planguard.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/theorvane/public/products/proxmox-mcp.svg b/apps/theorvane/public/products/proxmox-mcp.svg
new file mode 100644
index 0000000..50d8a54
--- /dev/null
+++ b/apps/theorvane/public/products/proxmox-mcp.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/theorvane/test/home.test.tsx b/apps/theorvane/test/home.test.tsx
index 9e9160f..ca5a39f 100644
--- a/apps/theorvane/test/home.test.tsx
+++ b/apps/theorvane/test/home.test.tsx
@@ -33,6 +33,9 @@ describe("Theorvane homepage", () => {
expect(footer.querySelector('a[href="https://typemcp.theorvane.tech/"]')).toBeTruthy();
expect(footer.querySelector('a[href="https://typechain.theorvane.tech/"]')).toBeTruthy();
expect(footer.querySelector('a[href="https://openscene.app/"]')).toBeTruthy();
+ expect(footer.querySelector('a[href="https://labfox.theorvane.tech/"]')).toBeTruthy();
+ expect(footer.querySelector('a[href="https://planguard.theorvane.tech/"]')).toBeTruthy();
+ expect(footer.querySelector('a[href="https://proxmox.theorvane.tech/"]')).toBeTruthy();
});
it("offers a product index with all canonical destinations", () => {
@@ -78,4 +81,17 @@ describe("Theorvane homepage", () => {
expect(screen.getByText(/local-first, open-source video editor/i)).toBeTruthy();
expect(screen.getByText(/no cloud uploads, accounts, or analytics/i)).toBeTruthy();
});
+
+ it("features the expanded product lineup with dedicated site destinations", () => {
+ render(createElement(HomePage));
+
+ for (const [name, href] of [
+ ["LabFox", "https://labfox.theorvane.tech/"],
+ ["PlanGuard", "https://planguard.theorvane.tech/"],
+ ["Proxmox MCP", "https://proxmox.theorvane.tech/"],
+ ] as const) {
+ expect(screen.getByRole("heading", { name })).toBeTruthy();
+ expect(screen.getByRole("link", { name: `Explore ${name} ↗` }).getAttribute("href")).toBe(href);
+ }
+ });
});
diff --git a/package-lock.json b/package-lock.json
index 0946dd9..242b7bf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,24 @@
"typescript-eslint": "^8.50.1"
}
},
+ "apps/labfox": {
+ "name": "@theorvane/labfox-site",
+ "version": "0.1.0",
+ "dependencies": {
+ "@theorvane/design-tokens": "*",
+ "@theorvane/ui": "*",
+ "next": "^16.2.1",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@testing-library/react": "^16.3.0",
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "jsdom": "^28.1.0",
+ "vitest": "^4.1.0"
+ }
+ },
"apps/openscene": {
"name": "@theorvane/openscene-site",
"version": "0.1.0",
@@ -40,6 +58,42 @@
"vitest": "^4.1.0"
}
},
+ "apps/planguard": {
+ "name": "@theorvane/planguard-site",
+ "version": "0.1.0",
+ "dependencies": {
+ "@theorvane/design-tokens": "*",
+ "@theorvane/ui": "*",
+ "next": "^16.2.1",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@testing-library/react": "^16.3.0",
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "jsdom": "^28.1.0",
+ "vitest": "^4.1.0"
+ }
+ },
+ "apps/proxmox": {
+ "name": "@theorvane/proxmox-mcp-site",
+ "version": "0.1.0",
+ "dependencies": {
+ "@theorvane/design-tokens": "*",
+ "@theorvane/ui": "*",
+ "next": "^16.2.1",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@testing-library/react": "^16.3.0",
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "jsdom": "^28.1.0",
+ "vitest": "^4.1.0"
+ }
+ },
"apps/theorvane": {
"name": "@theorvane/theorvane-site",
"version": "0.1.0",
@@ -625,7 +679,6 @@
"os": [
"aix"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -643,7 +696,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -661,7 +713,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -679,7 +730,6 @@
"os": [
"android"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -697,7 +747,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -715,7 +764,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -733,7 +781,6 @@
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -751,7 +798,6 @@
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -769,7 +815,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -787,7 +832,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -805,7 +849,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -823,7 +866,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -841,7 +883,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -859,7 +900,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -877,7 +917,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -895,7 +934,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -913,7 +951,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -931,7 +968,6 @@
"os": [
"netbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -949,7 +985,6 @@
"os": [
"netbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -967,7 +1002,6 @@
"os": [
"openbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -985,7 +1019,6 @@
"os": [
"openbsd"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1003,7 +1036,6 @@
"os": [
"openharmony"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1021,7 +1053,6 @@
"os": [
"sunos"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1039,7 +1070,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1057,7 +1087,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1075,7 +1104,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -2488,10 +2516,22 @@
"resolved": "packages/design-tokens",
"link": true
},
+ "node_modules/@theorvane/labfox-site": {
+ "resolved": "apps/labfox",
+ "link": true
+ },
"node_modules/@theorvane/openscene-site": {
"resolved": "apps/openscene",
"link": true
},
+ "node_modules/@theorvane/planguard-site": {
+ "resolved": "apps/planguard",
+ "link": true
+ },
+ "node_modules/@theorvane/proxmox-mcp-site": {
+ "resolved": "apps/proxmox",
+ "link": true
+ },
"node_modules/@theorvane/theorvane-site": {
"resolved": "apps/theorvane",
"link": true