Merge pull request #165 from bensenescu/codex/update-ai-tab

Update the AI tab for MCP setup and usage guidance
This commit is contained in:
Ben Senescu 2026-05-08 15:30:18 -04:00 committed by GitHub
commit 36b60f240f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 504 additions and 186 deletions

View File

@ -0,0 +1,34 @@
import type { SVGProps } from "react";
export function ClaudeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 257"
preserveAspectRatio="xMidYMid"
aria-hidden="true"
{...props}
>
<path
fill="#D97757"
d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"
/>
</svg>
);
}
export function CodexIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
aria-hidden="true"
{...props}
>
<path d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z" />
</svg>
);
}

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,
matchSegment: "/prompt-explorer",
},
{
to: "/p/$projectId/ai" as const,
label: "AI & Agents",
] as const;
const aiNavItem = linkOptions({
to: "/ai" as const,
label: "AI & MCP",
icon: Bot,
matchSegment: "/ai",
},
] as const;
});
function getProjectNavItems(projectId: string) {
return linkOptions(
@ -114,7 +115,7 @@ export function getProjectNavGroups(projectId: string) {
},
{
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 AppSettingsRouteImport } from './routes/_app/settings'
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 DotwellKnownOauthAuthorizationServerRouteImport } from './routes/[.]well-known/oauth-authorization-server'
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 ProjectPProjectIdBacklinksRouteImport } from './routes/_project/p/$projectId/backlinks'
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 ProjectPProjectIdRankTrackingIndexRouteImport } from './routes/_project/p/$projectId/rank-tracking/index'
import { Route as ProjectPProjectIdAuditIndexRouteImport } from './routes/_project/p/$projectId/audit/index'
@ -119,6 +119,11 @@ const AppBillingRoute = AppBillingRouteImport.update({
path: '/billing',
getParentRoute: () => AppRouteRoute,
} as any)
const AppAiRoute = AppAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => AppRouteRoute,
} as any)
const DotwellKnownOpenidConfigurationRoute =
DotwellKnownOpenidConfigurationRouteImport.update({
id: '/.well-known/openid-configuration',
@ -207,11 +212,6 @@ const ProjectPProjectIdAuditRoute = ProjectPProjectIdAuditRouteImport.update({
path: '/audit',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdAiRoute = ProjectPProjectIdAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const DotwellKnownOauthAuthorizationServerApiAuthRoute =
DotwellKnownOauthAuthorizationServerApiAuthRouteImport.update({
id: '/api/auth',
@ -250,6 +250,7 @@ export interface FileRoutesByFullPath {
'/verify-email': typeof VerifyEmailRoute
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren
'/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute
'/ai': typeof AppAiRoute
'/billing': typeof AppBillingRoute
'/settings': typeof AppSettingsRoute
'/support': typeof AppSupportRoute
@ -263,7 +264,6 @@ export interface FileRoutesByFullPath {
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/autumn/$': typeof ApiAutumnSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
'/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute
@ -285,6 +285,7 @@ export interface FileRoutesByTo {
'/verify-email': typeof VerifyEmailRoute
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren
'/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute
'/ai': typeof AppAiRoute
'/billing': typeof AppBillingRoute
'/settings': typeof AppSettingsRoute
'/support': typeof AppSupportRoute
@ -297,7 +298,6 @@ export interface FileRoutesByTo {
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/autumn/$': typeof ApiAutumnSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
'/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
@ -321,6 +321,7 @@ export interface FileRoutesById {
'/verify-email': typeof VerifyEmailRoute
'/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren
'/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute
'/_app/ai': typeof AppAiRoute
'/_app/billing': typeof AppBillingRoute
'/_app/settings': typeof AppSettingsRoute
'/_app/support': typeof AppSupportRoute
@ -335,7 +336,6 @@ export interface FileRoutesById {
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/autumn/$': typeof ApiAutumnSplatRoute
'/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute
'/_project/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/_project/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
'/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/_project/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute
@ -359,6 +359,7 @@ export interface FileRouteTypes {
| '/verify-email'
| '/.well-known/oauth-authorization-server'
| '/.well-known/openid-configuration'
| '/ai'
| '/billing'
| '/settings'
| '/support'
@ -372,7 +373,6 @@ export interface FileRouteTypes {
| '/api/auth/$'
| '/api/autumn/$'
| '/.well-known/oauth-authorization-server/api/auth'
| '/p/$projectId/ai'
| '/p/$projectId/audit'
| '/p/$projectId/backlinks'
| '/p/$projectId/brand-lookup'
@ -394,6 +394,7 @@ export interface FileRouteTypes {
| '/verify-email'
| '/.well-known/oauth-authorization-server'
| '/.well-known/openid-configuration'
| '/ai'
| '/billing'
| '/settings'
| '/support'
@ -406,7 +407,6 @@ export interface FileRouteTypes {
| '/api/auth/$'
| '/api/autumn/$'
| '/.well-known/oauth-authorization-server/api/auth'
| '/p/$projectId/ai'
| '/p/$projectId/backlinks'
| '/p/$projectId/brand-lookup'
| '/p/$projectId/domain'
@ -429,6 +429,7 @@ export interface FileRouteTypes {
| '/verify-email'
| '/.well-known/oauth-authorization-server'
| '/.well-known/openid-configuration'
| '/_app/ai'
| '/_app/billing'
| '/_app/settings'
| '/_app/support'
@ -443,7 +444,6 @@ export interface FileRouteTypes {
| '/api/auth/$'
| '/api/autumn/$'
| '/.well-known/oauth-authorization-server/api/auth'
| '/_project/p/$projectId/ai'
| '/_project/p/$projectId/audit'
| '/_project/p/$projectId/backlinks'
| '/_project/p/$projectId/brand-lookup'
@ -581,6 +581,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppBillingRouteImport
parentRoute: typeof AppRouteRoute
}
'/_app/ai': {
id: '/_app/ai'
path: '/ai'
fullPath: '/ai'
preLoaderRoute: typeof AppAiRouteImport
parentRoute: typeof AppRouteRoute
}
'/.well-known/openid-configuration': {
id: '/.well-known/openid-configuration'
path: '/.well-known/openid-configuration'
@ -693,13 +700,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectPProjectIdAuditRouteImport
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': {
id: '/.well-known/oauth-authorization-server/api/auth'
path: '/api/auth'
@ -739,6 +739,7 @@ declare module '@tanstack/react-router' {
}
interface AppRouteRouteChildren {
AppAiRoute: typeof AppAiRoute
AppBillingRoute: typeof AppBillingRoute
AppSettingsRoute: typeof AppSettingsRoute
AppSupportRoute: typeof AppSupportRoute
@ -747,6 +748,7 @@ interface AppRouteRouteChildren {
}
const AppRouteRouteChildren: AppRouteRouteChildren = {
AppAiRoute: AppAiRoute,
AppBillingRoute: AppBillingRoute,
AppSettingsRoute: AppSettingsRoute,
AppSupportRoute: AppSupportRoute,
@ -794,7 +796,6 @@ const ProjectPProjectIdRankTrackingRouteWithChildren =
)
interface ProjectPProjectIdRouteRouteChildren {
ProjectPProjectIdAiRoute: typeof ProjectPProjectIdAiRoute
ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren
ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute
ProjectPProjectIdBrandLookupRoute: typeof ProjectPProjectIdBrandLookupRoute
@ -808,7 +809,6 @@ interface ProjectPProjectIdRouteRouteChildren {
const ProjectPProjectIdRouteRouteChildren: ProjectPProjectIdRouteRouteChildren =
{
ProjectPProjectIdAiRoute: ProjectPProjectIdAiRoute,
ProjectPProjectIdAuditRoute: ProjectPProjectIdAuditRouteWithChildren,
ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute,
ProjectPProjectIdBrandLookupRoute: ProjectPProjectIdBrandLookupRoute,

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

@ -0,0 +1,352 @@
import { createFileRoute } from "@tanstack/react-router";
import { ArrowUpRight, Check, ChevronDown, Copy } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { ClaudeIcon, CodexIcon } from "@/client/features/ai-mcp/AgentIcons";
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="Add with the CLI"
icon={<ClaudeIcon className="size-5" />}
>
<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.
</p>
</Collapsible>
<Collapsible
id="claude-desktop"
title="Claude Desktop"
subtitle="Add a custom connector"
icon={<ClaudeIcon className="size-5" />}
>
<ol className="ml-5 list-decimal space-y-1.5 text-sm text-base-content/70 leading-relaxed">
<li>
Open <span className="text-base-content">Settings</span> {" "}
<span className="text-base-content">Connectors</span>.
</li>
<li>
Click{" "}
<span className="font-medium text-base-content">
Add custom connector
</span>
.
</li>
<li>Paste the MCP URL above and click Add.</li>
<li>Approve the OpenSEO login when prompted.</li>
</ol>
<p className="text-xs text-base-content/55 leading-relaxed">
Requires a Claude Pro, Max, Team, or Enterprise plan.
</p>
</Collapsible>
<Collapsible
id="codex"
title="Codex"
subtitle="Add with the CLI"
icon={<CodexIcon className="size-5" />}
>
<p className="text-sm text-base-content/70">
Run this in your terminal:
</p>
<CodeBlock code={`codex mcp add openseo --url ${mcpUrl}`} />
<p className="text-sm text-base-content/70">
Approve the login when prompted.
</p>
</Collapsible>
<Collapsible
id="codex-desktop"
title="Codex Desktop"
subtitle="Settings → Integrations & MCP"
icon={<CodexIcon className="size-5" />}
>
<ol className="ml-5 list-decimal space-y-1.5 text-sm text-base-content/70 leading-relaxed">
<li>
Open{" "}
<span className="text-base-content">
Settings Integrations & MCP
</span>
.
</li>
<li>
Click{" "}
<span className="font-medium text-base-content">
Add your own
</span>
.
</li>
<li>Paste the MCP URL above.</li>
<li>Approve the OpenSEO login when prompted.</li>
</ol>
</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,
icon,
children,
}: {
id: string;
title: string;
subtitle?: string;
icon?: React.ReactNode;
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 min-w-0 items-center gap-3">
{icon ? (
<span className="flex size-5 shrink-0 items-center justify-center text-base-content">
{icon}
</span>
) : null}
<div className="flex min-w-0 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>
</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,158 +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 DATAFORSEO_MCP_DOCS_URL =
"https://dataforseo.com/help-center/setting-up-the-official-dataforseo-mcp-server-simple-guide";
export const Route = createFileRoute("/_project/p/$projectId/ai")({
component: AiPage,
});
function AiPage() {
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-2xl">
<h1 className="text-2xl font-semibold">AI & Agents</h1>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
OpenSEO is brand new. Our goal is to build the most powerful and
intuitive tool to help your websites rank better. AI will play a large
role in that.
</p>
<p className="mt-3 text-sm text-base-content/70 leading-relaxed">
Lots of other SEO tools have an overwhelming number of
&ldquo;features&rdquo;. We think many of these can be hidden by
default, only shown to you when you want them.
</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>
{/* Sam */}
<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 turns your Claude Code or favorite coding agent into a powerful
SEO teammate. Sam has a specialized workflow for writing great
content, but you can teach Sam about your SEO workflow and needs and
it will customize itself for you.
</p>
<p className="mt-3 text-sm text-base-content/70 leading-relaxed">
Sam handles keyword research, source discovery, and drafting with
a built-in QA loop that checks quality and accuracy before you
publish.
</p>
<p className="mt-3 text-sm text-base-content/70 leading-relaxed">
Works with DataForSEO MCP for real SEO data. Model-agnostic use it
with Claude Code, GPT, or others.
</p>
<ul className="mt-4 space-y-2">
{[
"Keyword research & topic analysis",
"Source discovery & verification",
"Content drafting with QA",
].map((item) => (
<li
key={item}
className="flex gap-2.5 text-sm text-base-content/70"
>
<span className="mt-[2px] shrink-0 text-base-content/40">
&mdash;
</span>
{item}
</li>
))}
</ul>
<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 hover:text-base-content/60 transition-colors"
>
View on GitHub
<ArrowUpRight className="size-3.5" />
</a>
</section>
{/* Use any coding agent */}
<section className="mt-12">
<h2 className="text-xl font-semibold">Use any coding agent</h2>
<p className="mt-2 text-sm text-base-content/70 leading-relaxed">
Pair your preferred agent with the DataForSEO MCP server to access
keyword volumes, SERP results, backlink profiles, and more.
</p>
<p className="mt-3 text-sm text-base-content/60">
Claude Code &middot; Cursor &middot; Windsurf &middot; Codex
&middot; any MCP-compatible agent
</p>
<a
href={DATAFORSEO_MCP_DOCS_URL}
target="_blank"
rel="noreferrer"
className="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-base-content hover:text-base-content/60 transition-colors"
>
DataForSEO MCP setup guide
<ArrowUpRight className="size-3.5" />
</a>
</section>
{/* Roadmap */}
<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 integrations",
},
].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>
);
}