Polish AI & MCP page and move out of project scope

- Move /p/$projectId/ai to /ai so the page can be linked directly
- Lead with a copyable MCP server URL and trim the surrounding copy
- Make Claude Code and Codex setup guides collapsible accordions
- Add an Available tools section grouped into Keywords and Domain
This commit is contained in:
Ben Senescu 2026-05-08 14:56:26 -04:00
parent 02fcfb9834
commit 93ee343c63
5 changed files with 404 additions and 351 deletions

View File

@ -0,0 +1,89 @@
type McpTool = {
name: string;
title: string;
description: string;
};
type ToolCategory = {
label: string;
tools: McpTool[];
};
const toolCategories: ToolCategory[] = [
{
label: "Keywords",
tools: [
{
name: "research_keywords",
title: "Research keywords",
description: "Get keyword ideas with volume, difficulty, and CPC.",
},
{
name: "get_serp_results",
title: "Get SERP results",
description: "See live Google results for a keyword.",
},
{
name: "get_rank_tracker",
title: "Get rank tracking positions",
description: "Read tracked keyword positions.",
},
{
name: "list_saved_keywords",
title: "Get saved keywords",
description: "Pull your saved keyword lists.",
},
{
name: "save_keywords",
title: "Save keywords",
description: "Save keywords back to OpenSEO.",
},
],
},
{
label: "Domain",
tools: [
{
name: "get_domain_overview",
title: "Get domain overview",
description: "Summarize a domain's organic footprint.",
},
{
name: "get_domain_keyword_suggestions",
title: "Get domain keywords",
description: "Find keywords a domain already ranks for.",
},
{
name: "get_backlinks_overview",
title: "Get backlinks overview",
description: "Check backlink and referring-domain stats.",
},
],
},
];
export function AvailableTools() {
return (
<div className="grid gap-x-8 gap-y-8 md:grid-cols-2">
{toolCategories.map((cat) => (
<div key={cat.label}>
<h3 className="text-xs font-semibold uppercase tracking-wide text-base-content/50">
{cat.label}
</h3>
<ul className="mt-3 space-y-3">
{cat.tools.map((tool) => (
<li key={tool.name} className="flex flex-col gap-0.5">
<span className="text-sm font-medium text-base-content">
{tool.title}
</span>
<p className="text-xs text-base-content/60 leading-relaxed">
{tool.description}
</p>
</li>
))}
</ul>
</div>
))}
</div>
);
}

View File

@ -60,13 +60,14 @@ const projectNavItems = [
icon: MessageSquare, icon: MessageSquare,
matchSegment: "/prompt-explorer", matchSegment: "/prompt-explorer",
}, },
{ ] as const;
to: "/p/$projectId/ai" as const,
const aiNavItem = linkOptions({
to: "/ai" as const,
label: "AI & MCP", label: "AI & MCP",
icon: Bot, icon: Bot,
matchSegment: "/ai", matchSegment: "/ai",
}, });
] as const;
function getProjectNavItems(projectId: string) { function getProjectNavItems(projectId: string) {
return linkOptions( return linkOptions(
@ -114,7 +115,7 @@ export function getProjectNavGroups(projectId: string) {
}, },
{ {
type: "standalone" as const, type: "standalone" as const,
item: bySegment("/ai"), item: aiNavItem,
}, },
]; ];
} }

View File

@ -24,6 +24,7 @@ import { Route as AuthSignInRouteImport } from './routes/_auth.sign-in'
import { Route as AppSupportRouteImport } from './routes/_app/support' import { Route as AppSupportRouteImport } from './routes/_app/support'
import { Route as AppSettingsRouteImport } from './routes/_app/settings' import { Route as AppSettingsRouteImport } from './routes/_app/settings'
import { Route as AppBillingRouteImport } from './routes/_app/billing' import { Route as AppBillingRouteImport } from './routes/_app/billing'
import { Route as AppAiRouteImport } from './routes/_app/ai'
import { Route as DotwellKnownOpenidConfigurationRouteImport } from './routes/[.]well-known/openid-configuration' import { Route as DotwellKnownOpenidConfigurationRouteImport } from './routes/[.]well-known/openid-configuration'
import { Route as DotwellKnownOauthAuthorizationServerRouteImport } from './routes/[.]well-known/oauth-authorization-server' import { Route as DotwellKnownOauthAuthorizationServerRouteImport } from './routes/[.]well-known/oauth-authorization-server'
import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$' import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$'
@ -40,7 +41,6 @@ import { Route as ProjectPProjectIdDomainRouteImport } from './routes/_project/p
import { Route as ProjectPProjectIdBrandLookupRouteImport } from './routes/_project/p/$projectId/brand-lookup' import { Route as ProjectPProjectIdBrandLookupRouteImport } from './routes/_project/p/$projectId/brand-lookup'
import { Route as ProjectPProjectIdBacklinksRouteImport } from './routes/_project/p/$projectId/backlinks' import { Route as ProjectPProjectIdBacklinksRouteImport } from './routes/_project/p/$projectId/backlinks'
import { Route as ProjectPProjectIdAuditRouteImport } from './routes/_project/p/$projectId/audit' import { Route as ProjectPProjectIdAuditRouteImport } from './routes/_project/p/$projectId/audit'
import { Route as ProjectPProjectIdAiRouteImport } from './routes/_project/p/$projectId/ai'
import { Route as DotwellKnownOauthAuthorizationServerApiAuthRouteImport } from './routes/[.]well-known/oauth-authorization-server/api/auth' import { Route as DotwellKnownOauthAuthorizationServerApiAuthRouteImport } from './routes/[.]well-known/oauth-authorization-server/api/auth'
import { Route as ProjectPProjectIdRankTrackingIndexRouteImport } from './routes/_project/p/$projectId/rank-tracking/index' import { Route as ProjectPProjectIdRankTrackingIndexRouteImport } from './routes/_project/p/$projectId/rank-tracking/index'
import { Route as ProjectPProjectIdAuditIndexRouteImport } from './routes/_project/p/$projectId/audit/index' import { Route as ProjectPProjectIdAuditIndexRouteImport } from './routes/_project/p/$projectId/audit/index'
@ -119,6 +119,11 @@ const AppBillingRoute = AppBillingRouteImport.update({
path: '/billing', path: '/billing',
getParentRoute: () => AppRouteRoute, getParentRoute: () => AppRouteRoute,
} as any) } as any)
const AppAiRoute = AppAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => AppRouteRoute,
} as any)
const DotwellKnownOpenidConfigurationRoute = const DotwellKnownOpenidConfigurationRoute =
DotwellKnownOpenidConfigurationRouteImport.update({ DotwellKnownOpenidConfigurationRouteImport.update({
id: '/.well-known/openid-configuration', id: '/.well-known/openid-configuration',
@ -207,11 +212,6 @@ const ProjectPProjectIdAuditRoute = ProjectPProjectIdAuditRouteImport.update({
path: '/audit', path: '/audit',
getParentRoute: () => ProjectPProjectIdRouteRoute, getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any) } as any)
const ProjectPProjectIdAiRoute = ProjectPProjectIdAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const DotwellKnownOauthAuthorizationServerApiAuthRoute = const DotwellKnownOauthAuthorizationServerApiAuthRoute =
DotwellKnownOauthAuthorizationServerApiAuthRouteImport.update({ DotwellKnownOauthAuthorizationServerApiAuthRouteImport.update({
id: '/api/auth', id: '/api/auth',
@ -250,6 +250,7 @@ export interface FileRoutesByFullPath {
'/verify-email': typeof VerifyEmailRoute '/verify-email': typeof VerifyEmailRoute
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren
'/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute
'/ai': typeof AppAiRoute
'/billing': typeof AppBillingRoute '/billing': typeof AppBillingRoute
'/settings': typeof AppSettingsRoute '/settings': typeof AppSettingsRoute
'/support': typeof AppSupportRoute '/support': typeof AppSupportRoute
@ -263,7 +264,6 @@ export interface FileRoutesByFullPath {
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/api/autumn/$': typeof ApiAutumnSplatRoute '/api/autumn/$': typeof ApiAutumnSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
'/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren '/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute '/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute '/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute
@ -285,6 +285,7 @@ export interface FileRoutesByTo {
'/verify-email': typeof VerifyEmailRoute '/verify-email': typeof VerifyEmailRoute
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren
'/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute
'/ai': typeof AppAiRoute
'/billing': typeof AppBillingRoute '/billing': typeof AppBillingRoute
'/settings': typeof AppSettingsRoute '/settings': typeof AppSettingsRoute
'/support': typeof AppSupportRoute '/support': typeof AppSupportRoute
@ -297,7 +298,6 @@ export interface FileRoutesByTo {
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/api/autumn/$': typeof ApiAutumnSplatRoute '/api/autumn/$': typeof ApiAutumnSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
'/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute '/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute '/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute '/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
@ -321,6 +321,7 @@ export interface FileRoutesById {
'/verify-email': typeof VerifyEmailRoute '/verify-email': typeof VerifyEmailRoute
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren
'/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute
'/_app/ai': typeof AppAiRoute
'/_app/billing': typeof AppBillingRoute '/_app/billing': typeof AppBillingRoute
'/_app/settings': typeof AppSettingsRoute '/_app/settings': typeof AppSettingsRoute
'/_app/support': typeof AppSupportRoute '/_app/support': typeof AppSupportRoute
@ -335,7 +336,6 @@ export interface FileRoutesById {
'/api/auth/$': typeof ApiAuthSplatRoute '/api/auth/$': typeof ApiAuthSplatRoute
'/api/autumn/$': typeof ApiAutumnSplatRoute '/api/autumn/$': typeof ApiAutumnSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
'/_project/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/_project/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren '/_project/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
'/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute '/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/_project/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute '/_project/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute
@ -359,6 +359,7 @@ export interface FileRouteTypes {
| '/verify-email' | '/verify-email'
| '/.well-known/oauth-authorization-server' | '/.well-known/oauth-authorization-server'
| '/.well-known/openid-configuration' | '/.well-known/openid-configuration'
| '/ai'
| '/billing' | '/billing'
| '/settings' | '/settings'
| '/support' | '/support'
@ -372,7 +373,6 @@ export interface FileRouteTypes {
| '/api/auth/$' | '/api/auth/$'
| '/api/autumn/$' | '/api/autumn/$'
| '/.well-known/oauth-authorization-server/api/auth' | '/.well-known/oauth-authorization-server/api/auth'
| '/p/$projectId/ai'
| '/p/$projectId/audit' | '/p/$projectId/audit'
| '/p/$projectId/backlinks' | '/p/$projectId/backlinks'
| '/p/$projectId/brand-lookup' | '/p/$projectId/brand-lookup'
@ -394,6 +394,7 @@ export interface FileRouteTypes {
| '/verify-email' | '/verify-email'
| '/.well-known/oauth-authorization-server' | '/.well-known/oauth-authorization-server'
| '/.well-known/openid-configuration' | '/.well-known/openid-configuration'
| '/ai'
| '/billing' | '/billing'
| '/settings' | '/settings'
| '/support' | '/support'
@ -406,7 +407,6 @@ export interface FileRouteTypes {
| '/api/auth/$' | '/api/auth/$'
| '/api/autumn/$' | '/api/autumn/$'
| '/.well-known/oauth-authorization-server/api/auth' | '/.well-known/oauth-authorization-server/api/auth'
| '/p/$projectId/ai'
| '/p/$projectId/backlinks' | '/p/$projectId/backlinks'
| '/p/$projectId/brand-lookup' | '/p/$projectId/brand-lookup'
| '/p/$projectId/domain' | '/p/$projectId/domain'
@ -429,6 +429,7 @@ export interface FileRouteTypes {
| '/verify-email' | '/verify-email'
| '/.well-known/oauth-authorization-server' | '/.well-known/oauth-authorization-server'
| '/.well-known/openid-configuration' | '/.well-known/openid-configuration'
| '/_app/ai'
| '/_app/billing' | '/_app/billing'
| '/_app/settings' | '/_app/settings'
| '/_app/support' | '/_app/support'
@ -443,7 +444,6 @@ export interface FileRouteTypes {
| '/api/auth/$' | '/api/auth/$'
| '/api/autumn/$' | '/api/autumn/$'
| '/.well-known/oauth-authorization-server/api/auth' | '/.well-known/oauth-authorization-server/api/auth'
| '/_project/p/$projectId/ai'
| '/_project/p/$projectId/audit' | '/_project/p/$projectId/audit'
| '/_project/p/$projectId/backlinks' | '/_project/p/$projectId/backlinks'
| '/_project/p/$projectId/brand-lookup' | '/_project/p/$projectId/brand-lookup'
@ -581,6 +581,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppBillingRouteImport preLoaderRoute: typeof AppBillingRouteImport
parentRoute: typeof AppRouteRoute parentRoute: typeof AppRouteRoute
} }
'/_app/ai': {
id: '/_app/ai'
path: '/ai'
fullPath: '/ai'
preLoaderRoute: typeof AppAiRouteImport
parentRoute: typeof AppRouteRoute
}
'/.well-known/openid-configuration': { '/.well-known/openid-configuration': {
id: '/.well-known/openid-configuration' id: '/.well-known/openid-configuration'
path: '/.well-known/openid-configuration' path: '/.well-known/openid-configuration'
@ -693,13 +700,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectPProjectIdAuditRouteImport preLoaderRoute: typeof ProjectPProjectIdAuditRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute parentRoute: typeof ProjectPProjectIdRouteRoute
} }
'/_project/p/$projectId/ai': {
id: '/_project/p/$projectId/ai'
path: '/ai'
fullPath: '/p/$projectId/ai'
preLoaderRoute: typeof ProjectPProjectIdAiRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/.well-known/oauth-authorization-server/api/auth': { '/.well-known/oauth-authorization-server/api/auth': {
id: '/.well-known/oauth-authorization-server/api/auth' id: '/.well-known/oauth-authorization-server/api/auth'
path: '/api/auth' path: '/api/auth'
@ -739,6 +739,7 @@ declare module '@tanstack/react-router' {
} }
interface AppRouteRouteChildren { interface AppRouteRouteChildren {
AppAiRoute: typeof AppAiRoute
AppBillingRoute: typeof AppBillingRoute AppBillingRoute: typeof AppBillingRoute
AppSettingsRoute: typeof AppSettingsRoute AppSettingsRoute: typeof AppSettingsRoute
AppSupportRoute: typeof AppSupportRoute AppSupportRoute: typeof AppSupportRoute
@ -747,6 +748,7 @@ interface AppRouteRouteChildren {
} }
const AppRouteRouteChildren: AppRouteRouteChildren = { const AppRouteRouteChildren: AppRouteRouteChildren = {
AppAiRoute: AppAiRoute,
AppBillingRoute: AppBillingRoute, AppBillingRoute: AppBillingRoute,
AppSettingsRoute: AppSettingsRoute, AppSettingsRoute: AppSettingsRoute,
AppSupportRoute: AppSupportRoute, AppSupportRoute: AppSupportRoute,
@ -794,7 +796,6 @@ const ProjectPProjectIdRankTrackingRouteWithChildren =
) )
interface ProjectPProjectIdRouteRouteChildren { interface ProjectPProjectIdRouteRouteChildren {
ProjectPProjectIdAiRoute: typeof ProjectPProjectIdAiRoute
ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren
ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute
ProjectPProjectIdBrandLookupRoute: typeof ProjectPProjectIdBrandLookupRoute ProjectPProjectIdBrandLookupRoute: typeof ProjectPProjectIdBrandLookupRoute
@ -808,7 +809,6 @@ interface ProjectPProjectIdRouteRouteChildren {
const ProjectPProjectIdRouteRouteChildren: ProjectPProjectIdRouteRouteChildren = const ProjectPProjectIdRouteRouteChildren: ProjectPProjectIdRouteRouteChildren =
{ {
ProjectPProjectIdAiRoute: ProjectPProjectIdAiRoute,
ProjectPProjectIdAuditRoute: ProjectPProjectIdAuditRouteWithChildren, ProjectPProjectIdAuditRoute: ProjectPProjectIdAuditRouteWithChildren,
ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute, ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute,
ProjectPProjectIdBrandLookupRoute: ProjectPProjectIdBrandLookupRoute, ProjectPProjectIdBrandLookupRoute: ProjectPProjectIdBrandLookupRoute,

286
src/routes/_app/ai.tsx Normal file
View File

@ -0,0 +1,286 @@
import { createFileRoute } from "@tanstack/react-router";
import { ArrowUpRight, Check, ChevronDown, Copy } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { AvailableTools } from "@/client/features/ai-mcp/AvailableTools";
const DISCORD_URL = "https://discord.gg/c9uGs3cFXr";
const SUPPORT_EMAIL = "ben@openseo.so";
const SAM_GITHUB_URL = "https://github.com/every-app/sam";
export const Route = createFileRoute("/_app/ai")({
component: AiPage,
});
function AiPage() {
const mcpUrl =
typeof window === "undefined"
? "https://app.openseo.so/mcp"
: `${window.location.origin}/mcp`;
return (
<div className="h-full overflow-auto bg-base-100 px-4 py-12 md:px-6 md:py-16 pb-24 md:pb-12">
<div className="mx-auto max-w-3xl">
<h1 className="text-2xl font-semibold">AI & MCP</h1>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
Connect your AI agent to OpenSEO. Run keyword research, SERP analysis,
and domain lookups from your editor or chat.
</p>
<section className="mt-8">
<div className="rounded-lg border border-base-300 bg-base-200 px-4 py-3.5">
<div className="flex items-center justify-between gap-3">
<p className="text-xs font-medium uppercase tracking-wide text-base-content/50">
MCP server URL
</p>
<CopyButton
value={mcpUrl}
successMessage="MCP URL copied"
label="Copy"
/>
</div>
<code className="mt-2 block break-all font-mono text-sm text-base-content">
{mcpUrl}
</code>
</div>
<p className="mt-2.5 text-xs text-base-content/55 leading-relaxed">
Paste this into any MCP client. Sign in with OpenSEO when prompted.
</p>
</section>
<section className="mt-10">
<h2 className="text-base font-semibold">Setup guides</h2>
<p className="mt-1.5 text-sm text-base-content/70">
Pick your agent.
</p>
<div className="mt-4 divide-y divide-base-300 overflow-hidden rounded-lg border border-base-300 bg-base-200">
<Collapsible
id="claude-code"
title="Claude Code"
subtitle="HTTP MCP server via the CLI"
>
<p className="text-sm text-base-content/70">
Run this in your terminal:
</p>
<CodeBlock
code={`claude mcp add --transport http --scope user openseo ${mcpUrl}`}
/>
<p className="text-sm text-base-content/70">
Approve the login when prompted. Then ask Claude Code to call{" "}
<code className="font-mono text-[0.85em]">whoami</code>.
</p>
</Collapsible>
<Collapsible
id="codex"
title="Codex"
subtitle="Hosted MCP server via the CLI"
>
<p className="text-sm text-base-content/70">
Run these commands:
</p>
<CodeBlock
code={`codex mcp add openseo --url ${mcpUrl}\ncodex mcp login openseo`}
/>
<p className="text-sm text-base-content/70">
Approve the login when prompted. Then ask Codex to call{" "}
<code className="font-mono text-[0.85em]">whoami</code>.
</p>
</Collapsible>
</div>
</section>
<section className="mt-12">
<h2 className="text-base font-semibold">Available tools</h2>
<div className="mt-5">
<AvailableTools />
</div>
</section>
<section className="mt-12">
<h2 className="text-base font-semibold">Sam: AI SEO teammate</h2>
<p className="mt-1.5 text-sm text-base-content/70 leading-relaxed">
Sam is an experimental content workflow for Claude Code and other
coding agents. It combines keyword research, source discovery,
drafting, and QA.
</p>
<a
href={SAM_GITHUB_URL}
target="_blank"
rel="noreferrer"
className="mt-3 inline-flex items-center gap-1.5 text-sm font-medium text-base-content transition-colors hover:text-base-content/60"
>
View Sam on GitHub
<ArrowUpRight className="size-3.5" />
</a>
</section>
<section className="mt-12">
<h2 className="text-base font-semibold">Roadmap</h2>
<ul className="mt-4 space-y-3">
{[
{
title: "In-app SEO Research Agent",
description:
"Ask questions and run research without leaving OpenSEO",
},
{
title: "Content Assistant",
description:
"Generate drafts using saved keywords and business context",
},
].map((item) => (
<li key={item.title} className="flex gap-2.5 text-sm">
<span className="mt-[2px] shrink-0 text-base-content/40">
&mdash;
</span>
<span className="text-base-content/70">
<span className="font-medium text-base-content">
{item.title}
</span>
<br />
{item.description}
</span>
</li>
))}
</ul>
</section>
<p className="mt-12 text-xs text-base-content/55 leading-relaxed">
Have feedback? Reach out on{" "}
<a
className="link link-primary"
href={DISCORD_URL}
target="_blank"
rel="noreferrer"
>
Discord
</a>{" "}
or email{" "}
<a className="link link-primary" href={`mailto:${SUPPORT_EMAIL}`}>
{SUPPORT_EMAIL}
</a>
.
</p>
</div>
</div>
);
}
function Collapsible({
id,
title,
subtitle,
children,
}: {
id: string;
title: string;
subtitle?: string;
children: React.ReactNode;
}) {
const [open, setOpen] = useState(false);
const contentId = `collapsible-${id}`;
return (
<div>
<button
type="button"
onClick={() => setOpen((value) => !value)}
aria-expanded={open}
aria-controls={contentId}
className="flex w-full items-center justify-between gap-3 px-4 py-3.5 text-left transition-colors hover:bg-base-300/50"
>
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium text-base-content">{title}</span>
{subtitle ? (
<span className="text-xs text-base-content/55">{subtitle}</span>
) : null}
</div>
<ChevronDown
className={`size-4 shrink-0 text-base-content/50 transition-transform ${
open ? "rotate-180" : ""
}`}
/>
</button>
{open ? (
<div id={contentId} className="space-y-3 px-4 pb-4 pt-1">
{children}
</div>
) : null}
</div>
);
}
function CodeBlock({ code }: { code: string }) {
return (
<div className="flex items-stretch overflow-hidden rounded-md border border-base-300 bg-base-100">
<pre className="min-w-0 flex-1 overflow-x-auto p-3 text-xs leading-relaxed text-base-content">
<code className="font-mono">{code}</code>
</pre>
<div className="flex shrink-0 items-start border-l border-base-300 p-1.5">
<CopyButton value={code} successMessage="Copied to clipboard" iconOnly />
</div>
</div>
);
}
function CopyButton({
value,
successMessage,
label,
iconOnly = false,
}: {
value: string;
successMessage: string;
label?: string;
iconOnly?: boolean;
}) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
toast.error("Clipboard not available");
return;
}
try {
await navigator.clipboard.writeText(value);
toast.success(successMessage);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
toast.error("Could not copy to clipboard");
}
};
if (iconOnly) {
return (
<button
type="button"
onClick={handleCopy}
aria-label="Copy"
className="flex size-7 items-center justify-center rounded-md text-base-content/60 transition-colors hover:bg-base-200 hover:text-base-content"
>
{copied ? (
<Check className="size-3.5 text-success" />
) : (
<Copy className="size-3.5" />
)}
</button>
);
}
return (
<button
type="button"
onClick={handleCopy}
className="inline-flex items-center gap-1.5 rounded-md border border-base-300 bg-base-100 px-2 py-1 text-xs font-medium text-base-content/70 transition-colors hover:bg-base-300/50 hover:text-base-content"
>
{copied ? (
<Check className="size-3 text-success" />
) : (
<Copy className="size-3" />
)}
{label ?? "Copy"}
</button>
);
}

View File

@ -1,323 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { ArrowUpRight } from "lucide-react";
const DISCORD_URL = "https://discord.gg/c9uGs3cFXr";
const SUPPORT_EMAIL = "ben@openseo.so";
const SAM_GITHUB_URL = "https://github.com/every-app/sam";
const CHATGPT_MCP_DOCS_URL = "https://platform.openai.com/docs/mcp";
const CLAUDE_CONNECTORS_DOCS_URL =
"https://support.anthropic.com/en/articles/11503834-building-custom-integrations-via-remote-mcp-servers";
const mcpTools = [
{
name: "whoami",
description:
"Check which OpenSEO user and workspace the client can access.",
},
{
name: "list_projects",
description: "Find your OpenSEO projects and copy the right projectId.",
},
{
name: "research_keywords",
description:
"Turn seed terms into keyword ideas with volume, difficulty, and CPC.",
},
{
name: "get_serp_results",
description:
"Inspect live Google results for keyword and competitor research.",
},
{
name: "get_domain_overview",
description:
"Summarize a domain's organic footprint before deeper research.",
},
{
name: "get_domain_keyword_suggestions",
description:
"Find keywords a domain already ranks for or could expand into.",
},
{
name: "get_backlinks_overview",
description: "Review backlink authority and referring-domain signals.",
},
{
name: "get_rank_tracker",
description: "Read your tracked keyword positions from OpenSEO.",
},
{
name: "list_saved_keywords",
description: "Pull saved keyword lists into an agent workflow.",
},
{
name: "save_keywords",
description: "Save promising keyword ideas back to OpenSEO.",
},
] as const;
const promptExamples = [
{
title: "Keyword research",
prompt:
"Use OpenSEO to research keywords for my project. Start with these seed topics: [seed 1], [seed 2], [seed 3]. Group the best opportunities by search intent, prioritize low-difficulty terms, and suggest which keywords I should save.",
},
{
title: "Competitor research",
prompt:
"Use OpenSEO to compare my domain [yourdomain.com] against these competitors: [competitor1.com], [competitor2.com]. Summarize their strongest keyword themes, backlink advantages, and the first content opportunities I should pursue.",
},
{
title: "SERP brief",
prompt:
"Use OpenSEO to inspect the live SERP for [keyword]. Tell me who ranks, what search intent Google is rewarding, what pages are cited repeatedly, and what a better page should include.",
},
] as const;
export const Route = createFileRoute("/_project/p/$projectId/ai")({
component: AiPage,
});
function AiPage() {
const mcpUrl =
typeof window === "undefined"
? "https://app.openseo.so/mcp"
: `${window.location.origin}/mcp`;
return (
<div className="px-4 py-12 md:px-6 md:py-20 pb-24 md:pb-8 overflow-auto">
<div className="mx-auto max-w-3xl">
<h1 className="text-2xl font-semibold">AI & MCP</h1>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
Connect OpenSEO to your coding agent or AI workspace so it can run SEO
research with your project data: keyword ideas, SERP results, domain
overviews, backlinks, rank tracking, and saved keywords.
</p>
<p className="mt-3 text-sm text-base-content/70 leading-relaxed">
This is community-driven we want to hear what matters to you. Reach
out on{" "}
<a
className="link link-primary"
href={DISCORD_URL}
target="_blank"
rel="noreferrer"
>
Discord
</a>{" "}
or email{" "}
<a className="link link-primary" href={`mailto:${SUPPORT_EMAIL}`}>
{SUPPORT_EMAIL}
</a>
.
</p>
<section className="mt-12">
<h2 className="text-xl font-semibold">MCP server</h2>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
Use this endpoint when a client asks for the OpenSEO MCP server URL.
You will be sent through OpenSEO login the first time a client
connects.
</p>
<div className="mt-4 rounded-lg border border-base-300 bg-base-200/50 px-4 py-3">
<p className="text-xs font-medium uppercase tracking-wide text-base-content/50">
MCP endpoint
</p>
<code className="mt-2 block break-all text-sm text-base-content">
{mcpUrl}
</code>
</div>
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">Connect Codex</h2>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
Codex supports hosted MCP servers from the CLI. Add OpenSEO, then
run the OAuth login flow.
</p>
<pre className="mt-4 overflow-x-auto rounded-lg border border-base-300 bg-base-200/50 p-4 text-xs leading-relaxed text-base-content">
<code>{`codex mcp add openseo --url ${mcpUrl}
codex mcp login openseo`}</code>
</pre>
<p className="mt-3 text-sm text-base-content/70 leading-relaxed">
After login, ask Codex to call <code>whoami</code>, then{" "}
<code>list_projects</code>. Use the returned <code>projectId</code>{" "}
for keyword, SERP, domain, backlink, and rank-tracking requests.
</p>
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">Connect Claude Code CLI</h2>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
Claude Code can add OpenSEO as an HTTP MCP server. Use the user
scope if you want it available across projects, or local scope if
this repository is the only place you need it.
</p>
<pre className="mt-4 overflow-x-auto rounded-lg border border-base-300 bg-base-200/50 p-4 text-xs leading-relaxed text-base-content">
<code>{`claude mcp add --transport http --scope user openseo ${mcpUrl}`}</code>
</pre>
<p className="mt-3 text-sm text-base-content/70 leading-relaxed">
If Claude Code prompts you to authorize the server, approve the
OpenSEO login flow. Then ask it to list MCP tools or call{" "}
<code>whoami</code> to confirm the connection.
</p>
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">
ChatGPT, Claude web, and Claude Desktop
</h2>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
Web AI clients handle MCP setup differently from coding tools. Use
the same endpoint above when adding a custom connector or MCP
server, then authorize OpenSEO when the client asks you to sign in.
</p>
<ul className="mt-4 space-y-3">
<li className="flex gap-2.5 text-sm text-base-content/70">
<span className="mt-[2px] shrink-0 text-base-content/40">
&mdash;
</span>
<span>
In ChatGPT, enable developer mode if needed, then create a
custom MCP connector from Apps & Connectors and use the endpoint
above.
</span>
</li>
<li className="flex gap-2.5 text-sm text-base-content/70">
<span className="mt-[2px] shrink-0 text-base-content/40">
&mdash;
</span>
<span>
In Claude web or Claude Desktop, add OpenSEO from Settings,
Connectors. Remote MCP servers should be added there, not by
editing the desktop JSON config.
</span>
</li>
</ul>
<p className="mt-3 text-sm text-base-content/70 leading-relaxed">
Start by asking the client to check <code>whoami</code> and{" "}
<code>list_projects</code>. Once it can see your projects, it can
help with research planning, SERP analysis, competitor comparisons,
and saving useful keywords back to OpenSEO.
</p>
<div className="mt-4 flex flex-wrap gap-4">
<a
href={CHATGPT_MCP_DOCS_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 text-sm font-medium text-base-content transition-colors hover:text-base-content/60"
>
ChatGPT MCP docs
<ArrowUpRight className="size-3.5" />
</a>
<a
href={CLAUDE_CONNECTORS_DOCS_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 text-sm font-medium text-base-content transition-colors hover:text-base-content/60"
>
Claude connector docs
<ArrowUpRight className="size-3.5" />
</a>
</div>
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">Available tools</h2>
<div className="mt-4 grid gap-3 md:grid-cols-2">
{mcpTools.map((tool) => (
<div
key={tool.name}
className="rounded-lg border border-base-300 bg-base-100 p-4"
>
<code className="text-sm font-medium text-base-content">
{tool.name}
</code>
<p className="mt-2 text-sm leading-relaxed text-base-content/70">
{tool.description}
</p>
</div>
))}
</div>
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">Prompts to try</h2>
<div className="mt-4 space-y-4">
{promptExamples.map((example) => (
<div
key={example.title}
className="rounded-lg border border-base-300 bg-base-100 p-4"
>
<h3 className="text-sm font-medium">{example.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-base-content/70">
{example.prompt}
</p>
</div>
))}
</div>
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">Sam: AI SEO teammate</h2>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
Sam is an experimental content workflow for Claude Code and other
coding agents. It combines keyword research, source discovery,
drafting, and QA for teams that want an agent to help produce
publishable SEO content.
</p>
<a
href={SAM_GITHUB_URL}
target="_blank"
rel="noreferrer"
className="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-base-content transition-colors hover:text-base-content/60"
>
View Sam on GitHub
<ArrowUpRight className="size-3.5" />
</a>
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">Roadmap</h2>
<ul className="mt-4 space-y-4">
{[
{
title: "In-app SEO Research Agent",
description:
"Ask questions and run research without leaving OpenSEO",
},
{
title: "Content Assistant",
description:
"Generate drafts using saved keywords and business context",
},
{
title: "More MCP clients",
description:
"Better setup guidance for ChatGPT, Claude web, OpenCode, and other clients as their MCP support matures",
},
].map((item) => (
<li key={item.title}>
<p className="flex gap-2.5 text-sm text-base-content/70">
<span className="mt-[2px] shrink-0 text-base-content/40">
&mdash;
</span>
<span>
<span className="font-medium text-base-content">
{item.title}
</span>
{item.description ? (
<>
<br />
{item.description}
</>
) : null}
</span>
</p>
</li>
))}
</ul>
</section>
</div>
</div>
);
}