diff --git a/packages/web/src/lib/seo.ts b/packages/web/src/lib/seo.ts
new file mode 100644
index 0000000..d242478
--- /dev/null
+++ b/packages/web/src/lib/seo.ts
@@ -0,0 +1,28 @@
+export const SITE_URL = "https://reins.tech";
+
+/**
+ * Per-page head tags: title, description, canonical, and Open Graph
+ * overrides. Root-level tags (og:image, twitter:card, site defaults) live in
+ * __root.tsx; TanStack dedupes by name/property so these win per page.
+ */
+export function seo({
+ title,
+ description,
+ path,
+}: {
+ title: string;
+ description: string;
+ path: string;
+}) {
+ const url = path === "/" ? SITE_URL : `${SITE_URL}${path}`;
+ return {
+ meta: [
+ { title },
+ { name: "description", content: description },
+ { property: "og:title", content: title },
+ { property: "og:description", content: description },
+ { property: "og:url", content: url },
+ ],
+ links: [{ rel: "canonical", href: url }],
+ };
+}
diff --git a/packages/web/src/routes/__root.tsx b/packages/web/src/routes/__root.tsx
index 3baf351..c6206fd 100644
--- a/packages/web/src/routes/__root.tsx
+++ b/packages/web/src/routes/__root.tsx
@@ -17,9 +17,18 @@ export const Route = createRootRoute({
{ property: "og:title", content: TITLE },
{ property: "og:description", content: DESCRIPTION },
{ property: "og:type", content: "website" },
+ { property: "og:site_name", content: "reins" },
{ property: "og:url", content: SITE_URL },
{ property: "og:image", content: `${SITE_URL}/og.png` },
+ { property: "og:image:width", content: "1200" },
+ { property: "og:image:height", content: "630" },
+ {
+ property: "og:image:alt",
+ content: "reins — drive your real browser from your coding agent",
+ },
{ name: "twitter:card", content: "summary_large_image" },
+ { name: "theme-color", media: "(prefers-color-scheme: light)", content: "#ffffff" },
+ { name: "theme-color", media: "(prefers-color-scheme: dark)", content: "#0a0a0a" },
],
links: [
{ rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
diff --git a/packages/web/src/routes/changelog/extension.tsx b/packages/web/src/routes/changelog/extension.tsx
index 3d4ae43..c6e6984 100644
--- a/packages/web/src/routes/changelog/extension.tsx
+++ b/packages/web/src/routes/changelog/extension.tsx
@@ -1,8 +1,16 @@
import { createFileRoute } from "@tanstack/react-router";
import { ReleaseList } from "@/components/release-list";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/changelog/extension")({
- head: () => ({ meta: [{ title: "Extension changelog · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Extension changelog · reins",
+ description:
+ "Release notes for the reins Chrome extension, straight from the package changelog.",
+ path: "/changelog/extension",
+ }),
+ }),
component: ExtensionChangelogPage,
});
diff --git a/packages/web/src/routes/changelog/index.tsx b/packages/web/src/routes/changelog/index.tsx
index c0a89f7..38998a1 100644
--- a/packages/web/src/routes/changelog/index.tsx
+++ b/packages/web/src/routes/changelog/index.tsx
@@ -1,8 +1,16 @@
import { createFileRoute } from "@tanstack/react-router";
import { ReleaseList } from "@/components/release-list";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/changelog/")({
- head: () => ({ meta: [{ title: "Changelog · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Changelog · reins",
+ description:
+ "Release notes for the reins CLI (@karnstack/reins), straight from the package changelog.",
+ path: "/changelog",
+ }),
+ }),
component: CliChangelogPage,
});
diff --git a/packages/web/src/routes/docs/architecture.tsx b/packages/web/src/routes/docs/architecture.tsx
index a484434..e28b3b0 100644
--- a/packages/web/src/routes/docs/architecture.tsx
+++ b/packages/web/src/routes/docs/architecture.tsx
@@ -1,8 +1,16 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { Fragment } from "react";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/docs/architecture")({
- head: () => ({ meta: [{ title: "Architecture · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Architecture · reins",
+ description:
+ "How the reins CLI, local daemon, and Chrome extension fit together — one WebSocket on 127.0.0.1, no cloud, nothing to keep running.",
+ path: "/docs/architecture",
+ }),
+ }),
component: ArchitecturePage,
});
diff --git a/packages/web/src/routes/docs/commands.tsx b/packages/web/src/routes/docs/commands.tsx
index a3446a1..17df028 100644
--- a/packages/web/src/routes/docs/commands.tsx
+++ b/packages/web/src/routes/docs/commands.tsx
@@ -1,9 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { Fragment } from "react";
import { CodeBlock } from "@/components/code-block";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/docs/commands")({
- head: () => ({ meta: [{ title: "Commands · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Commands · reins",
+ description:
+ "The full reins command reference: tabs, snapshot, click, type, fill, screenshot, console, network, eval, and raw Chrome DevTools Protocol access.",
+ path: "/docs/commands",
+ }),
+ }),
component: CommandsPage,
});
diff --git a/packages/web/src/routes/docs/comparison.tsx b/packages/web/src/routes/docs/comparison.tsx
index 547cd47..e369c7d 100644
--- a/packages/web/src/routes/docs/comparison.tsx
+++ b/packages/web/src/routes/docs/comparison.tsx
@@ -1,7 +1,15 @@
import { createFileRoute } from "@tanstack/react-router";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/docs/comparison")({
- head: () => ({ meta: [{ title: "How it compares · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "How it compares · reins",
+ description:
+ "How reins compares to agent-browser, dev3000, and playwright-mcp — and when to pick each for agent-driven browser automation.",
+ path: "/docs/comparison",
+ }),
+ }),
component: ComparisonPage,
});
diff --git a/packages/web/src/routes/docs/faq.tsx b/packages/web/src/routes/docs/faq.tsx
index 70d98a4..e77c344 100644
--- a/packages/web/src/routes/docs/faq.tsx
+++ b/packages/web/src/routes/docs/faq.tsx
@@ -5,9 +5,31 @@ import {
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/docs/faq")({
- head: () => ({ meta: [{ title: "FAQ · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "FAQ · reins",
+ description:
+ "Answers to common questions about reins: supported browsers and agents, the local-only daemon, Chrome's debugging banner, and installing without the store.",
+ path: "/docs/faq",
+ }),
+ scripts: [
+ {
+ type: "application/ld+json",
+ children: JSON.stringify({
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ mainEntity: FAQS.map((faq) => ({
+ "@type": "Question",
+ name: faq.question,
+ acceptedAnswer: { "@type": "Answer", text: faq.answer },
+ })),
+ }),
+ },
+ ],
+ }),
component: FaqPage,
});
@@ -63,11 +85,20 @@ function FaqPage() {
return (
FAQ
+
+ Quick answers about how reins works. Anything missing? Ask on{" "}
+
+ GitHub
+
+ .
+
{FAQS.map((faq) => (
- {faq.question}
-
+
+ {faq.question}
+
+
{faq.answer}
diff --git a/packages/web/src/routes/docs/index.tsx b/packages/web/src/routes/docs/index.tsx
index 37d76ad..bbef6bc 100644
--- a/packages/web/src/routes/docs/index.tsx
+++ b/packages/web/src/routes/docs/index.tsx
@@ -1,11 +1,19 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { CodeBlock } from "@/components/code-block";
+import { seo } from "@/lib/seo";
const CHROME_WEB_STORE_URL =
"https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo";
export const Route = createFileRoute("/docs/")({
- head: () => ({ meta: [{ title: "Getting started · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Getting started · reins",
+ description:
+ "Install the reins CLI and Chrome extension, teach your coding agent the command loop, and have it driving your real logged-in browser in minutes.",
+ path: "/docs",
+ }),
+ }),
component: GettingStarted,
});
diff --git a/packages/web/src/routes/docs/permissions.tsx b/packages/web/src/routes/docs/permissions.tsx
index 51f8535..3438b99 100644
--- a/packages/web/src/routes/docs/permissions.tsx
+++ b/packages/web/src/routes/docs/permissions.tsx
@@ -1,8 +1,16 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { CodeBlock } from "@/components/code-block";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/docs/permissions")({
- head: () => ({ meta: [{ title: "Site permissions · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Site permissions · reins",
+ description:
+ "How reins site permissions work: per-site deny, read-only, or full tiers enforced inside the extension, granted only by a click in the popup.",
+ path: "/docs/permissions",
+ }),
+ }),
component: PermissionsPage,
});
diff --git a/packages/web/src/routes/docs/security.tsx b/packages/web/src/routes/docs/security.tsx
index 9783274..4b6d1ff 100644
--- a/packages/web/src/routes/docs/security.tsx
+++ b/packages/web/src/routes/docs/security.tsx
@@ -1,7 +1,15 @@
import { createFileRoute, Link } from "@tanstack/react-router";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/docs/security")({
- head: () => ({ meta: [{ title: "Security · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Security · reins",
+ description:
+ "The reins security model: localhost-only daemon, DNS-rebinding protection, extension-origin allowlisting, Chrome's native debug banner, and instant disconnect.",
+ path: "/docs/security",
+ }),
+ }),
component: SecurityPage,
});
diff --git a/packages/web/src/routes/docs/sideload.tsx b/packages/web/src/routes/docs/sideload.tsx
index d86efff..77d65c9 100644
--- a/packages/web/src/routes/docs/sideload.tsx
+++ b/packages/web/src/routes/docs/sideload.tsx
@@ -1,8 +1,16 @@
import { createFileRoute } from "@tanstack/react-router";
import { CodeBlock } from "@/components/code-block";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/docs/sideload")({
- head: () => ({ meta: [{ title: "Install without the store · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Install without the store · reins",
+ description:
+ "Install the bundled reins extension without the Chrome Web Store: reins extension stages it for Chrome's Load unpacked, no store access required.",
+ path: "/docs/sideload",
+ }),
+ }),
component: SideloadPage,
});
diff --git a/packages/web/src/routes/index.tsx b/packages/web/src/routes/index.tsx
index ecaf10d..fda145d 100644
--- a/packages/web/src/routes/index.tsx
+++ b/packages/web/src/routes/index.tsx
@@ -8,33 +8,52 @@ import {
ScanSearch,
ShieldCheck,
} from "lucide-react";
-import { AnimatedTerminal, output, prompt } from "@/components/animated-terminal";
import { CopyCommand } from "@/components/copy-command";
+import { Hero } from "@/components/hero";
import { IdeasSection } from "@/components/ideas-section";
import { PopupMock } from "@/components/popup-mock";
import { Reveal } from "@/components/reveal";
import { SiteFooter } from "@/components/site-footer";
import { SiteHeader } from "@/components/site-header";
import { Button } from "@/components/ui/button";
+import { seo } from "@/lib/seo";
const CHROME_WEB_STORE_URL =
"https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo";
export const Route = createFileRoute("/")({
+ head: () => ({
+ ...seo({
+ title: "reins: drive your real browser from your coding agent",
+ description:
+ "reins lets coding agents drive the logged-in Chromium browser you already use, through a local CLI, daemon, and extension. Everything stays on 127.0.0.1.",
+ path: "/",
+ }),
+ scripts: [
+ {
+ type: "application/ld+json",
+ children: JSON.stringify({
+ "@context": "https://schema.org",
+ "@type": "SoftwareApplication",
+ name: "reins",
+ applicationCategory: "DeveloperApplication",
+ operatingSystem: "macOS, Linux, Windows",
+ description:
+ "reins lets coding agents drive the logged-in Chromium browser you already use, through a local CLI, daemon, and extension.",
+ url: "https://reins.tech",
+ offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
+ license: "https://github.com/karnstack/reins/blob/main/LICENSE",
+ sameAs: [
+ "https://github.com/karnstack/reins",
+ "https://www.npmjs.com/package/@karnstack/reins",
+ ],
+ }),
+ },
+ ],
+ }),
component: LandingPage,
});
-const HERO_LINES = [
- prompt("reins tabs"),
- output(" b1 chrome tab 12 * Dashboard — localhost:3000"),
- prompt("reins snapshot"),
- output(' e3: input "Email"\n e7: button "Sign in"'),
- prompt('reins type --ref e3 --text "you@work.dev"'),
- prompt("reins click --ref e7"),
- prompt("reins screenshot"),
- output(" ~/.reins/shots/tab-12.png"),
-];
-
const FEATURES = [
{
icon: AppWindow,
@@ -96,32 +115,7 @@ function LandingPage() {
<>
-
-
-
-
- Take the reins of your real browser
-
-
- reins lets coding agents (Claude Code, Cursor, Codex, anything with a shell) drive
- the logged-in browser you already use. No debug profile, no launch flags, no MCP
- server to register.
-
-
-
-
-
-
-
+
@@ -296,7 +290,11 @@ function LandingPage() {
-
+
+
diff --git a/packages/web/src/routes/privacy.tsx b/packages/web/src/routes/privacy.tsx
index c8e2754..7d7b507 100644
--- a/packages/web/src/routes/privacy.tsx
+++ b/packages/web/src/routes/privacy.tsx
@@ -1,9 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { SiteFooter } from "@/components/site-footer";
import { SiteHeader } from "@/components/site-header";
+import { seo } from "@/lib/seo";
export const Route = createFileRoute("/privacy")({
- head: () => ({ meta: [{ title: "Privacy policy · reins" }] }),
+ head: () => ({
+ ...seo({
+ title: "Privacy policy · reins",
+ description:
+ "The reins privacy policy: everything stays on your machine — no analytics, no telemetry, no remote servers.",
+ path: "/privacy",
+ }),
+ }),
component: PrivacyPage,
});
diff --git a/packages/web/src/styles.css b/packages/web/src/styles.css
index 01966df..c51ab1c 100644
--- a/packages/web/src/styles.css
+++ b/packages/web/src/styles.css
@@ -62,6 +62,19 @@
--ring: oklch(0.606 0.25 292.717);
}
+/* Tailwind v4 has no default border color; without this, bare `border-*`
+ utilities (e.g. the accordion's `border-b`) fall back to currentColor and
+ render as harsh near-black/near-white lines. */
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-border);
+ }
+}
+
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
@@ -249,3 +262,33 @@
opacity: 0;
}
}
+
+/* Decorative hero backdrop: a faint dot grid that fades out toward the edges,
+ plus a soft violet glow behind the headline. Sits in an aria-hidden layer
+ below the content; tuned low enough to stay subtle in light and dark. */
+@utility hero-grid {
+ background-image: radial-gradient(var(--color-foreground) 0.75px, transparent 0.75px);
+ background-size: 22px 22px;
+ mask-image: radial-gradient(120% 90% at 50% 0%, black, transparent 70%);
+ opacity: 0.04;
+
+ @variant dark {
+ opacity: 0.05;
+ }
+}
+
+@utility hero-glow {
+ background: radial-gradient(
+ 42rem 30rem at 70% -10%,
+ --alpha(var(--color-primary) / 14%),
+ transparent 60%
+ );
+
+ @variant dark {
+ background: radial-gradient(
+ 42rem 30rem at 70% -10%,
+ --alpha(var(--color-primary) / 22%),
+ transparent 62%
+ );
+ }
+}