diff --git a/.env.example b/.env.example index 15612ff2b..f4808b7d5 100644 --- a/.env.example +++ b/.env.example @@ -34,10 +34,10 @@ # GOOGLE_CLIENT_SECRET=replace-with-your-google-oauth-client-secret # POSTHOG_PUBLIC_KEY=replace-with-your-posthog-project-api-key # POSTHOG_HOST=https://us.i.posthog.com -# LOOPS_API_KEY=replace-with-your-loops-api-key -# LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID=replace-with-your-loops-verify-template-id -# LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID=replace-with-your-loops-reset-template-id -# LOOPS_TRANSACTIONAL_INVITATION_ID=replace-with-your-loops-invitation-template-id +# Note: this fork has no hosted transactional email provider wired up, so +# hosted mode also requires BYPASS_EMAIL_VERIFICATION=true — password reset +# and invitation emails are not sent in hosted mode. +# BYPASS_EMAIL_VERIFICATION=true # Optional in self-hosted modes. Required if you want AI features like SAM, # the in-app SEO agent. Create a key at https://openrouter.ai/settings/keys. diff --git a/.env.production.example b/.env.production.example index 20ab31f61..a768b62fa 100644 --- a/.env.production.example +++ b/.env.production.example @@ -20,24 +20,18 @@ DATABASE_PROVIDER=postgres # HYPERDRIVE_ORIGIN_USER= # HYPERDRIVE_ORIGIN_PASSWORD= -# Hosted auth. +# Hosted auth. Note: this fork has no hosted transactional email provider +# wired up, so hosted mode also requires BYPASS_EMAIL_VERIFICATION=true — +# password reset and invitation emails are not sent in hosted mode. # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= -# LOOPS_API_KEY= -# LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID= -# LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID= -# LOOPS_TRANSACTIONAL_INVITATION_ID= +# BYPASS_EMAIL_VERIFICATION=true # TURNSTILE_SECRET_KEY= # TURNSTILE_SITE_KEY= # Billing, analytics, agents. # AUTUMN_SECRET_KEY= # AUTUMN_WEBHOOK_SECRET= -# Dub referral conversion tracking (leads + sales). Also requires, in the Dub -# dashboard: openseo.so allowlisted under workspace Tracking settings, partner -# link destinations kept on openseo.so, and the native Stripe integration -# disconnected (sales are reported by the app; both would double count). -# DUB_API_KEY= # Long random secret shared only with scripts/erase-user-data.ts. # GDPR_ERASURE_SECRET= # POSTHOG_PUBLIC_KEY= diff --git a/alchemy.run.ts b/alchemy.run.ts index 9ae6192d4..4ac5520b0 100644 --- a/alchemy.run.ts +++ b/alchemy.run.ts @@ -274,18 +274,7 @@ const dataEnv = { OPENROUTER_MODEL: optionalVar("OPENROUTER_MODEL"), AUTUMN_SECRET_KEY: optionalSecret("AUTUMN_SECRET_KEY"), AUTUMN_WEBHOOK_SECRET: optionalSecret("AUTUMN_WEBHOOK_SECRET"), - DUB_API_KEY: optionalSecret("DUB_API_KEY"), GDPR_ERASURE_SECRET: optionalSecret("GDPR_ERASURE_SECRET"), - LOOPS_API_KEY: optionalSecret("LOOPS_API_KEY"), - LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID: optionalVar( - "LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID", - ), - LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID: optionalVar( - "LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID", - ), - LOOPS_TRANSACTIONAL_INVITATION_ID: optionalVar( - "LOOPS_TRANSACTIONAL_INVITATION_ID", - ), POSTHOG_PUBLIC_KEY: optionalVar("POSTHOG_PUBLIC_KEY"), POSTHOG_HOST: optionalVar("POSTHOG_HOST"), TURNSTILE_SECRET_KEY: optionalSecret("TURNSTILE_SECRET_KEY"), diff --git a/public/site.webmanifest b/public/site.webmanifest index 63f961f37..8ae74f878 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "OpenSEO", - "short_name": "OpenSEO", + "name": "Sightline", + "short_name": "Sightline", "icons": [ { "src": "/android-chrome-192x192.png", diff --git a/src/client/components/Markdown.tsx b/src/client/components/Markdown.tsx index fc566e39e..b92ee17c0 100644 --- a/src/client/components/Markdown.tsx +++ b/src/client/components/Markdown.tsx @@ -11,7 +11,7 @@ type Props = { /** * Shared Markdown renderer with explicit per-element Tailwind classes. * - * OpenSEO doesn't ship `@tailwindcss/typography`, so `prose` classes are + * Sightline doesn't ship `@tailwindcss/typography`, so `prose` classes are * no-ops — every block element is styled here instead. Tables use daisyUI's * `table table-sm` so model- and strategy-generated tables stay readable. * diff --git a/src/client/components/Sidebar.tsx b/src/client/components/Sidebar.tsx index 0f87002be..92e26f9b1 100644 --- a/src/client/components/Sidebar.tsx +++ b/src/client/components/Sidebar.tsx @@ -130,7 +130,7 @@ export function Sidebar({ projectId, onNavigate, onClose }: SidebarProps) { onClick={onNavigate} className="text-base font-semibold text-base-content" > - OpenSEO + Sightline {onClose ? (

- For more powerful AI workflows, use the OpenSEO MCP with your own agent + For more powerful AI workflows, use the Sightline MCP with your own agent like Claude Code or Hermes.

diff --git a/src/client/features/settings/ApiKeySettings.tsx b/src/client/features/settings/ApiKeySettings.tsx index 1448ded97..eb0714d65 100644 --- a/src/client/features/settings/ApiKeySettings.tsx +++ b/src/client/features/settings/ApiKeySettings.tsx @@ -18,9 +18,7 @@ export function ApiKeySettings() { const [createdKey, setCreatedKey] = useState(null); const mcpUrl = - typeof window === "undefined" - ? "https://app.openseo.so/mcp" - : `${window.location.origin}/mcp`; + typeof window === "undefined" ? "/mcp" : `${window.location.origin}/mcp`; const apiKeysQuery = useQuery({ queryKey: ["apiKeys"], diff --git a/src/client/layout/AppShell.tsx b/src/client/layout/AppShell.tsx index f4231f9b0..4f340b858 100644 --- a/src/client/layout/AppShell.tsx +++ b/src/client/layout/AppShell.tsx @@ -181,7 +181,7 @@ function MobileTopBar({ - OpenSEO + Sightline ); diff --git a/src/client/layout/AppShellParts.tsx b/src/client/layout/AppShellParts.tsx index c45176232..c89493274 100644 --- a/src/client/layout/AppShellParts.tsx +++ b/src/client/layout/AppShellParts.tsx @@ -19,7 +19,7 @@ function SeoApiStatusBanners({
- Setup needed: add your DataForSEO API key to use OpenSEO + Setup needed: add your DataForSEO API key to use Sightline features. See the quick steps on the{" "} - Add your DataForSEO API key to start using OpenSEO. + Add your DataForSEO API key to start using Sightline.

diff --git a/src/client/lib/error-messages.test.ts b/src/client/lib/error-messages.test.ts index 6f5b6d4b9..d6b146709 100644 --- a/src/client/lib/error-messages.test.ts +++ b/src/client/lib/error-messages.test.ts @@ -7,7 +7,7 @@ import { describe("getStandardErrorMessage", () => { it("maps known error codes to standard copy", () => { expect(getStandardErrorMessage(new Error("PAYMENT_REQUIRED"))).toBe( - "An active hosted subscription is required before you can use OpenSEO.", + "An active hosted subscription is required before you can use Sightline.", ); }); diff --git a/src/client/lib/error-messages.ts b/src/client/lib/error-messages.ts index 09973b362..49da9248b 100644 --- a/src/client/lib/error-messages.ts +++ b/src/client/lib/error-messages.ts @@ -4,9 +4,9 @@ import { isErrorCode, type ErrorCode } from "@/shared/error-codes"; const STANDARD_MESSAGES: Record = { UNAUTHENTICATED: "Please sign in and try again.", AUTH_CONFIG_MISSING: - "OpenSEO auth is not configured. Follow the README setup steps for Cloudflare Access.", + "Sightline auth is not configured. Follow the README setup steps for Cloudflare Access.", PAYMENT_REQUIRED: - "An active hosted subscription is required before you can use OpenSEO.", + "An active hosted subscription is required before you can use Sightline.", INSUFFICIENT_CREDITS: "You've run out of credits. Add more credits or upgrade your plan to continue.", FORBIDDEN: "You do not have access to this resource.", diff --git a/src/env.d.ts b/src/env.d.ts index 8816c0bfe..b48f763c9 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -35,15 +35,8 @@ declare namespace Cloudflare { }; GOOGLE_CLIENT_ID?: string; GOOGLE_CLIENT_SECRET?: string; - LOOPS_API_KEY?: string; - LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID?: string; - LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID?: string; - LOOPS_TRANSACTIONAL_INVITATION_ID?: string; AUTUMN_SECRET_KEY?: string; AUTUMN_WEBHOOK_SECRET?: string; - // Dub referral conversion tracking (hosted only); all Dub code no-ops - // when unset. - DUB_API_KEY?: string; // HMAC secret for the operator-only GDPR storage-erasure endpoint. GDPR_ERASURE_SECRET?: string; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index d237fbabc..0becf08e1 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -21,12 +21,6 @@ import { import { resolveSignInHostedOrganization } from "@/server/auth/default-hosted-organization"; import { onInvitationAccepted } from "@/server/auth/invited-member"; import { AuthRepository } from "@/server/auth/repositories/AuthRepository"; -import { captureDubReferralSignup } from "@/server/referrals/dub"; -import { - sendHostedPasswordResetEmail, - sendHostedVerificationEmail, - upsertHostedSignupContact, -} from "@/server/email/loops"; const hostedBaseUrlSchema = z .string() @@ -164,11 +158,10 @@ function createAuth() { requireEmailVerification: !bypassEmail, resetPasswordTokenExpiresIn: 60 * 60, revokeSessionsOnPasswordReset: true, - sendResetPassword: async ({ user, url }) => { - await sendHostedPasswordResetEmail({ - email: user.email, - resetUrl: url, - }); + sendResetPassword: async () => { + throw new Error( + "Hosted password reset email is not configured in this deployment.", + ); }, }, emailVerification: bypassEmail @@ -176,11 +169,10 @@ function createAuth() { : { sendOnSignUp: true, autoSignInAfterVerification: true, - sendVerificationEmail: async ({ user, url }) => { - await sendHostedVerificationEmail({ - email: user.email, - confirmationUrl: url, - }); + sendVerificationEmail: async () => { + throw new Error( + "Hosted verification email is not configured in this deployment.", + ); }, }, socialProviders: getSocialProviders(), @@ -225,12 +217,6 @@ function createAuth() { } return { data: user }; }, - after: async (user, ctx) => { - await syncHostedSignupContact(user); - if (isHostedAuthMode(env.AUTH_MODE)) { - await captureDubReferralSignup(user.id, ctx?.request); - } - }, }, }, session: { @@ -266,26 +252,6 @@ function createAuth() { let authInstance: ReturnType | null = null; -async function syncHostedSignupContact(user: { - id: string; - email: string; - name?: string | null; -}) { - try { - await upsertHostedSignupContact({ - userId: user.id, - email: user.email, - name: user.name, - }); - } catch (error) { - console.error("Failed to sync Loops profile after user creation:", { - userId: user.id, - email: user.email, - error, - }); - } -} - function getTrustedOrigins(baseUrl: string) { const trustedOrigins = [baseUrl]; @@ -364,19 +330,9 @@ function getGoogleSocialProviderConfig() { }; } -function hasHostedAuthEmailConfig() { - const loopsVars = [ - "LOOPS_API_KEY", - "LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID", - "LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID", - ]; - - return loopsVars.every((name) => { - const value: unknown = Reflect.get(env, name); - return typeof value === "string" && value.trim() !== ""; - }); -} - +// This fork has no hosted transactional email provider wired up, so hosted +// mode can only run with email verification bypassed — sendResetPassword and +// sendVerificationEmail above throw if hosted mode is ever used without it. export function hasHostedAuthConfig() { try { getHostedBaseUrl(); @@ -384,8 +340,7 @@ export function hasHostedAuthConfig() { getGoogleSocialProviderConfig(); return ( hasHostedTurnstileConfig(env) && - (Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true" || - hasHostedAuthEmailConfig()) + Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true" ); } catch { return false; diff --git a/src/middleware/ensure-user/delegated.ts b/src/middleware/ensure-user/delegated.ts index 7c56b0bae..84df53a10 100644 --- a/src/middleware/ensure-user/delegated.ts +++ b/src/middleware/ensure-user/delegated.ts @@ -16,7 +16,7 @@ const LOCAL_ADMIN_EMAIL = "admin@localhost"; // single user table means the OAuth `account` grant and every app table that // references `user.id` resolve the same way in all auth modes. function deriveUserName(email: string) { - return email.split("@")[0] || "OpenSEO"; + return email.split("@")[0] || "Sightline"; } async function ensureUserRecord(userId: string, userEmail: string) { diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 66178a009..6c878761b 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -37,7 +37,7 @@ export const Route = createRootRoute({ head: () => ({ meta: [ { - title: "OpenSEO", + title: "Sightline", }, { charSet: "utf-8", diff --git a/src/routes/_app/ai.tsx b/src/routes/_app/ai.tsx index ad45e910b..da9312ee7 100644 --- a/src/routes/_app/ai.tsx +++ b/src/routes/_app/ai.tsx @@ -52,10 +52,7 @@ export const Route = createFileRoute("/_app/ai")({ }); function AiPage() { - const origin = - typeof window === "undefined" - ? "https://app.openseo.so" - : window.location.origin; + const origin = typeof window === "undefined" ? "" : window.location.origin; const mcpUrl = `${origin}/mcp`; const prompt = getAgentSetupPrompt(origin); const [tab, setTab] = useState<"setup" | "skills">("setup"); @@ -65,7 +62,7 @@ function AiPage() {

Agent setup

- The most powerful way to use OpenSEO is through the AI agent you + The most powerful way to use Sightline is through the AI agent you already use. Set it up once, then ask it anything.

@@ -95,7 +92,7 @@ function AiPage() {

Set up your agent

- Paste the setup prompt into your agent to connect OpenSEO and + Paste the setup prompt into your agent to connect Sightline and install its SEO skills. It will guide you through any manual steps.

@@ -149,7 +146,7 @@ function AiPage() {

Update your skills

Already connected? Paste the update prompt into your agent to - get the latest OpenSEO skills while preserving your connection + get the latest Sightline skills while preserving your connection settings and personal edits.

diff --git a/src/routes/_app/billing.tsx b/src/routes/_app/billing.tsx index d64c0d484..a268e96ab 100644 --- a/src/routes/_app/billing.tsx +++ b/src/routes/_app/billing.tsx @@ -196,7 +196,7 @@ function BillingPage() {
    {[ - "Access to all OpenSEO features", + "Access to all Sightline features", "Includes $10.00 of Usage Credits each month", ].map((item) => (
  • - OpenSEO needs the DATAFORSEO_API_KEY secret before + Sightline needs the DATAFORSEO_API_KEY secret before keyword, domain, and SEO data workflows can run.

@@ -62,7 +62,7 @@ function DataforseoApiKeyHelpPage() {
  • In Cloudflare, go to Compute ->{" "} Workers & Pages - and open your OpenSEO Worker. + and open your Sightline Worker.
  • Open Settings. diff --git a/src/routes/_app/help/openrouter-api-key.tsx b/src/routes/_app/help/openrouter-api-key.tsx index 3f56edbcb..7f13dbe47 100644 --- a/src/routes/_app/help/openrouter-api-key.tsx +++ b/src/routes/_app/help/openrouter-api-key.tsx @@ -16,9 +16,9 @@ function OpenrouterApiKeyHelpPage() { Set up your OpenRouter API key

    - OpenSEO needs the OPENROUTER_API_KEY secret before AI + Sightline needs the OPENROUTER_API_KEY secret before AI features like SAM, the in-app SEO agent, can run. It is optional — - everything else in OpenSEO works without it. + everything else in Sightline works without it.

    @@ -64,7 +64,7 @@ function OpenrouterApiKeyHelpPage() {
  • -
  • Restart OpenSEO.
  • +
  • Restart Sightline.
  • @@ -78,7 +78,7 @@ function OpenrouterApiKeyHelpPage() {
  • In Cloudflare, go to Compute ->{" "} Workers & Pages - and open your OpenSEO Worker. + and open your Sightline Worker.
  • Open Settings. diff --git a/src/routes/_app/index.tsx b/src/routes/_app/index.tsx index c26beeaaf..5ba8adc35 100644 --- a/src/routes/_app/index.tsx +++ b/src/routes/_app/index.tsx @@ -76,7 +76,7 @@ function IndexRedirect() { return (
    { void refetch(); }} diff --git a/src/routes/_app/settings/index.tsx b/src/routes/_app/settings/index.tsx index e23ab891e..5ea756b5a 100644 --- a/src/routes/_app/settings/index.tsx +++ b/src/routes/_app/settings/index.tsx @@ -95,7 +95,7 @@ function PersonalSettings() {
    -

    Help improve OpenSEO

    +

    Help improve Sightline

    Share analytics and usage data.

    diff --git a/src/routes/_app/support.tsx b/src/routes/_app/support.tsx index a3bdce728..8bcd78e55 100644 --- a/src/routes/_app/support.tsx +++ b/src/routes/_app/support.tsx @@ -1,9 +1,5 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Check, Copy } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; -const SUPPORT_EMAIL = "ben@openseo.so"; const DISCORD_URL = "https://discord.gg/c9uGs3cFXr"; const GITHUB_URL = "https://github.com/every-app/open-seo"; @@ -12,15 +8,6 @@ export const Route = createFileRoute("/_app/support")({ }); function SupportPage() { - const [copied, setCopied] = useState(false); - - const handleCopy = async () => { - await navigator.clipboard.writeText(SUPPORT_EMAIL); - toast.success("Email copied to clipboard"); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - return (
    @@ -31,30 +18,12 @@ function SupportPage() { We want to hear from you

    - We want to talk to you! We're super open to feedback and want to learn - how you work so we can make OpenSEO better. + Sightline is built on the open-source OpenSEO project. For help with + the underlying project, the community channels below are a good + place to start.

    -
    -

    Email

    -

    - Send ideas, problems, questions, or feedback directly. -

    - -
    - ) : (
    -

    - By signing up, you agree to our{" "} - - Terms - {" "} - and{" "} - - Privacy Policy - - . -

    -

    Already have an account?{" "} OpenSEO

    Authorize MCP access

    - An MCP client is requesting access to your OpenSEO workspace. + An MCP client is requesting access to your Sightline workspace.

    diff --git a/src/routes/_authenticated.subscribe.tsx b/src/routes/_authenticated.subscribe.tsx index 37326f082..841ef7cdd 100644 --- a/src/routes/_authenticated.subscribe.tsx +++ b/src/routes/_authenticated.subscribe.tsx @@ -17,8 +17,6 @@ import { AUTUMN_PAID_PLAN_ID, } from "@/shared/billing"; -const SUPPORT_EMAIL = "ben@openseo.so"; - const PLAN_FEATURES = [ "Keyword research, backlinks, rank tracking, and site audits", "MCP server and agent skills for Claude, Cursor, and ChatGPT", @@ -136,7 +134,7 @@ function SubscribePage() {
    OpenSEO

    @@ -146,13 +144,6 @@ function SubscribePage() {

    This usually takes a few seconds.

    -

    - Taking longer?{" "} - - Email {SUPPORT_EMAIL} - - . -

    ); } @@ -163,7 +154,7 @@ function SubscribePage() {
    OpenSEO

    Billing unavailable

    @@ -223,15 +214,15 @@ function SubscribePage() {
    OpenSEO

    {isUpgradeFlow ? "Upgrade your plan" : firstName - ? `Welcome to OpenSEO, ${firstName}!` - : "Welcome to OpenSEO!"} + ? `Welcome to Sightline, ${firstName}!` + : "Welcome to Sightline!"}

    SEO on your terms. All your SEO tools in one place at a fair price. @@ -291,22 +282,11 @@ function SubscribePage() { )}

    - - - 30-day money-back guarantee - - - . Cancel anytime. Powered by Stripe. + Cancel anytime. Powered by Stripe.

    -

    - Questions? Email {SUPPORT_EMAIL}. -

    {isUpgradeFlow ? (
    diff --git a/src/routes/accept-invitation.$id.tsx b/src/routes/accept-invitation.$id.tsx index c4975ab8b..ae99e405d 100644 --- a/src/routes/accept-invitation.$id.tsx +++ b/src/routes/accept-invitation.$id.tsx @@ -39,7 +39,7 @@ function SignedOutInvitationCard({ invitationId }: { invitationId: string }) { return (

    - You’ve been invited to join an organization on OpenSEO. Sign in + You’ve been invited to join an organization on Sightline. Sign in with the email address that received the invitation to accept it.

    @@ -212,7 +212,7 @@ function InvitationCard({ {invitationQuery.data.organizationName} {" "} - on OpenSEO. + on Sightline.

    {actionError ?

    {actionError}

    : null}
    diff --git a/src/routes/auth-error.tsx b/src/routes/auth-error.tsx index c64c432a9..492190664 100644 --- a/src/routes/auth-error.tsx +++ b/src/routes/auth-error.tsx @@ -39,7 +39,7 @@ function AuthErrorPage() { } > - Back to OpenSEO + Back to Sightline diff --git a/src/routes/s/$token/index.tsx b/src/routes/s/$token/index.tsx index 123f2a7db..9b4569b54 100644 --- a/src/routes/s/$token/index.tsx +++ b/src/routes/s/$token/index.tsx @@ -11,11 +11,6 @@ import { loadSharePage } from "@/server/features/reports/sharePage"; // that endpoint bounces anyone who opens it top-level back here, so shared // content always carries this chrome. -const MARKETING_URL = "https://openseo.so/?utm_source=shared_report"; -// The marketing site's card. Absolute because a link preview crawler resolves -// og:image against nothing, and the app domain does not serve this asset. -const SOCIAL_CARD_URL = "https://openseo.so/social-card.jpg"; - // The share sheet on touch devices only; on desktop macOS anchors it to the // window rather than the button, so the clipboard is used instead. async function shareLink(title: string) { @@ -41,7 +36,7 @@ export const Route = createFileRoute("/s/$token/")({ staleTime: Infinity, head: ({ loaderData: data }) => { if (!data) return {}; - const title = `${data.title} · OpenSEO`; + const title = `${data.title} · Sightline`; return { meta: [ { title }, @@ -49,7 +44,7 @@ export const Route = createFileRoute("/s/$token/")({ // The raw endpoint sets X-Robots-Tag for the same reason. { name: "robots", content: "noindex, nofollow" }, { property: "og:type", content: "article" }, - { property: "og:site_name", content: "OpenSEO" }, + { property: "og:site_name", content: "Sightline" }, { property: "og:title", content: data.title }, { name: "twitter:title", content: data.title }, ...(data.description @@ -59,10 +54,6 @@ export const Route = createFileRoute("/s/$token/")({ ] : []), { property: "og:url", content: data.url }, - { property: "og:image", content: SOCIAL_CARD_URL }, - { property: "og:image:width", content: "1200" }, - { property: "og:image:height", content: "630" }, - { name: "twitter:card", content: "summary_large_image" }, ], }; }, @@ -81,14 +72,6 @@ function SharedReportMessage({

    {heading}

    {detail}

    - - Try OpenSEO -
    ); } @@ -123,7 +106,7 @@ function SharedReportPage() {

    {data.title}

    - Made with OpenSEO · Updated {formatRelativeTime(data.updatedAt)} + Made with Sightline · Updated {formatRelativeTime(data.updatedAt)}

    @@ -135,14 +118,6 @@ function SharedReportPage() { Share - - Try OpenSEO -
    ({ AuthRepository: mocks, })); -// Referral pin repair is fire-and-forget KV bookkeeping; mocking it keeps -// `cloudflare:workers` out of this module graph. -vi.mock("@/server/referrals/dub", () => ({ - markDubReferredOrganization: vi.fn(), -})); - describe("resolveSignInHostedOrganization", () => { beforeEach(() => { mocks.getLastActiveOrganizationId.mockResolvedValue(null); diff --git a/src/server/auth/default-hosted-organization.ts b/src/server/auth/default-hosted-organization.ts index c24e794d9..95a4c66c9 100644 --- a/src/server/auth/default-hosted-organization.ts +++ b/src/server/auth/default-hosted-organization.ts @@ -1,5 +1,4 @@ import { AuthRepository } from "@/server/auth/repositories/AuthRepository"; -import { markDubReferredOrganization } from "@/server/referrals/dub"; import { slugify, toHex } from "./org-slug"; type HostedUser = { @@ -19,7 +18,7 @@ type HostedOrganizationCreator = ( ) => Promise<{ id: string }>; function getDefaultHostedOrganizationName(user: HostedUser) { - const name = user.name?.trim() || user.email.split("@")[0] || "OpenSEO"; + const name = user.name?.trim() || user.email.split("@")[0] || "Sightline"; return `${name}'s organization`; } @@ -119,16 +118,6 @@ async function createActiveHostedOrganization( return { organizationId, role: membership?.role ?? "owner" }; } -// On every resolution, not just org creation: the signup-time referral pin -// can land after the org exists (email verification from another location, -// or BYPASS_EMAIL_VERIFICATION creating the session inside the signup -// transaction before user.create.after hooks flush), so later logins repair -// the org pin. No-ops without a user pin, and only ever pins an org the user -// founded — an invitee's membership never counts. -async function repairDubReferralPin(userId: string) { - await markDubReferredOrganization(userId); -} - // Picks the org a hosted user should be working in: their last-active org if // they still belong to it, else their most recently joined org (so a fresh // invite acceptance beats the signup-minted personal organization), else a @@ -140,14 +129,12 @@ export async function resolveActiveHostedOrganization( ): Promise { const existing = await findExistingActiveOrganization(userId); if (existing) { - await repairDubReferralPin(userId); return existing; } const created = await createActiveHostedOrganization( userId, createOrganization, ); - await repairDubReferralPin(userId); return created; } @@ -158,12 +145,7 @@ export async function resolveActiveHostedOrganization( export async function resolveExistingActiveHostedOrganization( userId: string, ): Promise { - const existing = await findExistingActiveOrganization(userId); - if (!existing) { - return null; - } - await repairDubReferralPin(userId); - return existing; + return findExistingActiveOrganization(userId); } // Sign-in (session-create hook) variant: same resolution, except a user with @@ -179,7 +161,6 @@ export async function resolveSignInHostedOrganization( ): Promise { const existing = await findExistingActiveOrganization(userId); if (existing) { - await repairDubReferralPin(userId); return existing; } @@ -188,10 +169,5 @@ export async function resolveSignInHostedOrganization( return null; } - const created = await createActiveHostedOrganization( - userId, - createOrganization, - ); - await repairDubReferralPin(userId); - return created; + return createActiveHostedOrganization(userId, createOrganization); } diff --git a/src/server/billing/autumn-webhook.ts b/src/server/billing/autumn-webhook.ts index dd8b09d0a..27ce20a5c 100644 --- a/src/server/billing/autumn-webhook.ts +++ b/src/server/billing/autumn-webhook.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { getRequiredEnvValue } from "@/server/lib/runtime-env"; import { captureServerError } from "@/server/lib/posthog"; -import { trackDubSalesForOrganization } from "@/server/referrals/dub"; import { syncAutumnCustomerStatus } from "./customer-status-sync"; import { verifySvixSignature } from "./svix"; @@ -74,10 +73,6 @@ export async function handleAutumnWebhookRequest(request: Request) { ); return json({ error: "Webhook processing failed" }, 500); } - - // Referral revenue attribution; internally fire-and-forget and can never - // fail the webhook response. - await trackDubSalesForOrganization(customerId); } return json({ received: true }); diff --git a/src/server/billing/customer-status-sync.ts b/src/server/billing/customer-status-sync.ts index e61a1c25e..90544d128 100644 --- a/src/server/billing/customer-status-sync.ts +++ b/src/server/billing/customer-status-sync.ts @@ -6,7 +6,6 @@ import { deriveBillingCustomerStatusSnapshot, type BillingCustomerStatusSnapshot, } from "./customer-status-model"; -import { syncBillingStatusToLoops } from "./loops-sync"; export async function syncAutumnCustomerStatus(customerId: string) { // getOrCreate is effectively a "get" here — a billing.updated webhook always @@ -14,7 +13,6 @@ export async function syncAutumnCustomerStatus(customerId: string) { const customer = await autumn.customers.getOrCreate({ customerId }); const snapshot = deriveBillingCustomerStatusSnapshot(customer); await upsertBillingCustomerStatus(snapshot); - await syncBillingStatusToLoops(snapshot); return snapshot; } diff --git a/src/server/billing/loops-contact-properties.test.ts b/src/server/billing/loops-contact-properties.test.ts deleted file mode 100644 index 94d60a560..000000000 --- a/src/server/billing/loops-contact-properties.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - getBillingLoopsContactProperties, - LOOPS_BILLING_PLAN_NONE, -} from "./loops-contact-properties"; - -describe("getBillingLoopsContactProperties", () => { - it("maps Autumn billing fields to Loops custom properties", () => { - expect( - getBillingLoopsContactProperties({ - paidPlanId: "base-plan", - paidPlanStatus: "active", - }), - ).toEqual({ - billingPlanId: "base-plan", - billingPlanStatus: "active", - }); - }); - - it("uses explicit none values when the customer has no paid plan", () => { - expect( - getBillingLoopsContactProperties({ - paidPlanId: null, - paidPlanStatus: null, - }), - ).toEqual({ - billingPlanId: LOOPS_BILLING_PLAN_NONE, - billingPlanStatus: LOOPS_BILLING_PLAN_NONE, - }); - }); -}); diff --git a/src/server/billing/loops-contact-properties.ts b/src/server/billing/loops-contact-properties.ts deleted file mode 100644 index caa27f72a..000000000 --- a/src/server/billing/loops-contact-properties.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { BillingCustomerStatusSnapshot } from "./customer-status-model"; - -export const LOOPS_BILLING_PLAN_NONE = "none"; - -export function getBillingLoopsContactProperties( - snapshot: Pick< - BillingCustomerStatusSnapshot, - "paidPlanId" | "paidPlanStatus" - >, -) { - return { - billingPlanId: snapshot.paidPlanId ?? LOOPS_BILLING_PLAN_NONE, - billingPlanStatus: snapshot.paidPlanStatus ?? LOOPS_BILLING_PLAN_NONE, - }; -} diff --git a/src/server/billing/loops-sync.ts b/src/server/billing/loops-sync.ts deleted file mode 100644 index 4b816b764..000000000 --- a/src/server/billing/loops-sync.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { eq } from "drizzle-orm"; -import { db } from "@/db"; -import { member, user } from "@/db/schema"; -import { - getContactNameParts, - updateLoopsContact, -} from "@/server/email/loops-client"; -import { getOptionalEnvValue } from "@/server/lib/runtime-env"; -import type { BillingCustomerStatusSnapshot } from "./customer-status-model"; -import { getBillingLoopsContactProperties } from "./loops-contact-properties"; - -export async function syncBillingStatusToLoops( - snapshot: BillingCustomerStatusSnapshot, -) { - const apiKey = await getOptionalEnvValue("LOOPS_API_KEY"); - - if (!apiKey) { - console.warn( - "Skipping Loops billing contact sync: LOOPS_API_KEY is not set", - ); - return; - } - - const contacts = await getOrganizationContacts(snapshot.organizationId); - const billingProperties = getBillingLoopsContactProperties(snapshot); - - for (const contact of contacts) { - await updateLoopsContact({ - apiKey, - payload: { - email: contact.email, - userId: contact.userId, - userGroup: "app-user", - ...getContactNameParts(contact.name), - ...billingProperties, - }, - logContext: { - action: "billing-contact-sync", - organizationId: snapshot.organizationId, - }, - }); - } -} - -async function getOrganizationContacts(organizationId: string) { - return db - .select({ - userId: user.id, - email: user.email, - name: user.name, - }) - .from(member) - .innerJoin(user, eq(member.userId, user.id)) - .where(eq(member.organizationId, organizationId)); -} diff --git a/src/server/email/loops-client.ts b/src/server/email/loops-client.ts deleted file mode 100644 index aecd3702d..000000000 --- a/src/server/email/loops-client.ts +++ /dev/null @@ -1,71 +0,0 @@ -const LOOPS_CONTACT_UPDATE_URL = "https://app.loops.so/api/v1/contacts/update"; - -type LoopsContactProperty = - | string - | number - | boolean - | null - | undefined - | Record; - -type LoopsContactUpdatePayload = { - email?: string; - userId?: string; - firstName?: string; - lastName?: string; - source?: string; - subscribed?: boolean; - userGroup?: string; - mailingLists?: Record; -} & Record; - -export async function updateLoopsContact({ - apiKey, - payload, - logContext, -}: { - apiKey: string; - payload: LoopsContactUpdatePayload; - logContext?: Record; -}) { - const response = await fetch(LOOPS_CONTACT_UPDATE_URL, { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify(payload), - signal: AbortSignal.timeout(10_000), - }); - - if (response.ok) { - return; - } - - const errorPayload = await response.json().catch(() => null); - console.warn("Loops contact update error:", { - status: response.status, - email: payload.email, - userId: payload.userId, - ...logContext, - errorPayload, - }); - - throw new Error(`Failed to update Loops contact (${response.status})`); -} - -export function getContactNameParts(name: string | null | undefined) { - const trimmedName = name?.trim(); - - if (!trimmedName) { - return {}; - } - - const [firstName, ...lastNameParts] = trimmedName.split(/\s+/); - const lastName = lastNameParts.join(" "); - - return { - firstName, - ...(lastName ? { lastName } : {}), - }; -} diff --git a/src/server/email/loops.ts b/src/server/email/loops.ts deleted file mode 100644 index 39afaa269..000000000 --- a/src/server/email/loops.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { env } from "cloudflare:workers"; -import { - getContactNameParts, - updateLoopsContact, -} from "@/server/email/loops-client"; - -const LOOPS_TRANSACTIONAL_URL = "https://app.loops.so/api/v1/transactional"; - -function getOptionalEnv(name: string) { - const value: unknown = Reflect.get(env, name); - const trimmed = typeof value === "string" ? value.trim() : ""; - - return trimmed || null; -} - -function getRequiredEnv(name: string) { - const value = getOptionalEnv(name); - - if (!value) { - throw new Error(`${name} is required in hosted mode`); - } - - return value; -} - -function getHostedAuthEmailConfig() { - return { - apiKey: getRequiredEnv("LOOPS_API_KEY"), - verificationTemplateId: getRequiredEnv( - "LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID", - ), - passwordResetTemplateId: getRequiredEnv( - "LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID", - ), - }; -} - -async function sendLoopsTransactionalEmail({ - apiKey, - email, - transactionalId, - dataVariables, -}: { - apiKey: string; - email: string; - transactionalId: string; - dataVariables: Record; -}) { - const response = await fetch(LOOPS_TRANSACTIONAL_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - transactionalId, - email, - addToAudience: false, - dataVariables, - }), - signal: AbortSignal.timeout(10_000), - }); - - if (response.ok) { - return; - } - - const errorPayload = await response.json().catch(() => null); - console.error("Loops transactional email error:", { - status: response.status, - email, - transactionalId, - errorPayload, - }); - - throw new Error( - `Failed to send Loops transactional email (${response.status})`, - ); -} - -export async function upsertHostedSignupContact({ - userId, - email, - name, -}: { - userId: string; - email: string; - name?: string | null; -}) { - const apiKey = getOptionalEnv("LOOPS_API_KEY"); - - if (!apiKey) { - console.warn( - "Skipping Loops signup contact sync: LOOPS_API_KEY is not set", - ); - return; - } - - await updateLoopsContact({ - apiKey, - payload: { - email, - userId, - source: "openseo-signup", - userGroup: "app-user", - ...getContactNameParts(name), - }, - logContext: { action: "signup-contact-sync" }, - }); -} - -export async function sendHostedVerificationEmail({ - email, - confirmationUrl, -}: { - email: string; - confirmationUrl: string; -}) { - const config = getHostedAuthEmailConfig(); - await sendLoopsTransactionalEmail({ - apiKey: config.apiKey, - email, - transactionalId: config.verificationTemplateId, - dataVariables: { - appName: "OpenSEO", - confirmationUrl, - }, - }); -} - -export async function sendHostedInvitationEmail({ - email, - inviteUrl, - organizationName, - inviterName, - inviterEmail, -}: { - email: string; - inviteUrl: string; - organizationName: string; - inviterName: string; - inviterEmail: string; -}) { - // Not part of getHostedAuthEmailConfig(): that trio gates hasHostedAuthConfig - // and adding a new required var there would brick existing deployments. - const apiKey = getRequiredEnv("LOOPS_API_KEY"); - const templateId = getRequiredEnv("LOOPS_TRANSACTIONAL_INVITATION_ID"); - await sendLoopsTransactionalEmail({ - apiKey, - email, - transactionalId: templateId, - dataVariables: { - appName: "OpenSEO", - inviteUrl, - organizationName, - inviterName, - inviterEmail, - }, - }); -} - -export async function sendHostedPasswordResetEmail({ - email, - resetUrl, -}: { - email: string; - resetUrl: string; -}) { - const config = getHostedAuthEmailConfig(); - await sendLoopsTransactionalEmail({ - apiKey: config.apiKey, - email, - transactionalId: config.passwordResetTemplateId, - dataVariables: { - appName: "OpenSEO", - resetUrl, - }, - }); -} diff --git a/src/server/features/ga4/services/Ga4Service.ts b/src/server/features/ga4/services/Ga4Service.ts index 3af8415ae..a210fc4c8 100644 --- a/src/server/features/ga4/services/Ga4Service.ts +++ b/src/server/features/ga4/services/Ga4Service.ts @@ -94,7 +94,7 @@ async function setProperty(input: { if (!grants.some((grant) => grant.accountId === input.accountId)) { throw new AppError( "NOT_FOUND", - "That Google account isn't connected to your OpenSEO account.", + "That Google account isn't connected to your Sightline account.", ); } diff --git a/src/server/features/google/GoogleAccountRepository.ts b/src/server/features/google/GoogleAccountRepository.ts index 694b0d44f..b98602feb 100644 --- a/src/server/features/google/GoogleAccountRepository.ts +++ b/src/server/features/google/GoogleAccountRepository.ts @@ -50,7 +50,7 @@ async function remove(input: AccountInput) { const { connections, grant, usage } = scope(input); // Atomic on D1 and Postgres: never leave a partial account removal. // Both deletes are scoped to the authenticated owner, including on retries - // after this Google identity has been linked to a different OpenSEO user. + // after this Google identity has been linked to a different Sightline user. await runBatch((tx) => [ tx.delete(connections).where(usage), tx.delete(account).where(grant), diff --git a/src/server/features/google/GoogleAccountService.test.ts b/src/server/features/google/GoogleAccountService.test.ts index 94b56ed03..46157536b 100644 --- a/src/server/features/google/GoogleAccountService.test.ts +++ b/src/server/features/google/GoogleAccountService.test.ts @@ -128,7 +128,7 @@ for (const provider of ["gsc", "ga4"] as const) { const table = provider === "gsc" ? "gsc_connections" : "ga4_connections"; const input = { provider, accountId: "google-a", userId: "u1" }; - it("deletes the authorization row so another OpenSEO user can claim the identity", async () => { + it("deletes the authorization row so another Sightline user can claim the identity", async () => { await grant(providerId); await mapping(provider, "p1"); expect((await rows("account"))[0]?.user_id).toBe("u1"); diff --git a/src/server/features/google/GoogleAccountService.ts b/src/server/features/google/GoogleAccountService.ts index 1e3a5520a..0753b12e2 100644 --- a/src/server/features/google/GoogleAccountService.ts +++ b/src/server/features/google/GoogleAccountService.ts @@ -13,7 +13,7 @@ async function getRemovalImpact(input: AccountInput) { async function remove(input: AccountInput) { // A user owns their personal Google authorization even when its projects // belong to other organizations. Removing it releases the provider identity - // for another OpenSEO user; it does not delete Google sign-in or other scopes. + // for another Sightline user; it does not delete Google sign-in or other scopes. await GoogleAccountRepository.remove(input); return { removed: true as const }; } diff --git a/src/server/features/gsc/services/GscService.ts b/src/server/features/gsc/services/GscService.ts index b3bb47fe2..631b1503c 100644 --- a/src/server/features/gsc/services/GscService.ts +++ b/src/server/features/gsc/services/GscService.ts @@ -150,7 +150,7 @@ async function setSite(input: { if (!grants.some((grant) => grant.accountId === input.accountId)) { throw new AppError( "NOT_FOUND", - "That Google account isn't connected to your OpenSEO account.", + "That Google account isn't connected to your Sightline account.", ); } diff --git a/src/server/features/rank-tracking/services/scheduledRankChecks.ts b/src/server/features/rank-tracking/services/scheduledRankChecks.ts index c24cd5783..6bbf4de93 100644 --- a/src/server/features/rank-tracking/services/scheduledRankChecks.ts +++ b/src/server/features/rank-tracking/services/scheduledRankChecks.ts @@ -173,7 +173,7 @@ export async function runScheduledRankChecks(env: Env) { projectId: config.projectId, billingCustomer: { userId: "system", - userEmail: "system@openseo.so", + userEmail: "system@sightline.internal", organizationId: config.organizationId, projectId: config.projectId, }, diff --git a/src/server/features/reports/services/ReportService.test.ts b/src/server/features/reports/services/ReportService.test.ts index 18710836f..b057534c6 100644 --- a/src/server/features/reports/services/ReportService.test.ts +++ b/src/server/features/reports/services/ReportService.test.ts @@ -275,7 +275,7 @@ describe("sharing", () => { vi.mocked(isHostedServerAuthMode).mockResolvedValue(false); await expect(share()).rejects.toThrow( - "Sharing is only available on hosted OpenSEO.", + "Sharing is only available on hosted Sightline.", ); expect(mocks.setShareToken).not.toHaveBeenCalled(); expect(mocks.getReport).not.toHaveBeenCalled(); diff --git a/src/server/features/reports/services/ReportService.ts b/src/server/features/reports/services/ReportService.ts index a98b70685..1fddfbf58 100644 --- a/src/server/features/reports/services/ReportService.ts +++ b/src/server/features/reports/services/ReportService.ts @@ -253,7 +253,7 @@ async function shareReport(params: ShareParams): Promise { if (!(await sharesEnabled())) { throw new AppError( "VALIDATION_ERROR", - "Sharing is only available on hosted OpenSEO.", + "Sharing is only available on hosted Sightline.", ); } diff --git a/src/server/features/sam/SamChatAgent.ts b/src/server/features/sam/SamChatAgent.ts index 5baf6380e..c4be51cbb 100644 --- a/src/server/features/sam/SamChatAgent.ts +++ b/src/server/features/sam/SamChatAgent.ts @@ -267,7 +267,7 @@ export class SamChatAgent extends Think { return withPgClient(async () => { const ctx = await this.loadSamContext(); if (!ctx) { - return "You are SAM, the SEO agent inside OpenSEO. This chat session no longer exists; tell the user to start a new chat."; + return "You are SAM, the SEO agent inside Sightline. This chat session no longer exists; tell the user to start a new chat."; } const context = await ProjectContextService.getProjectContext( ctx.project.id, @@ -349,8 +349,7 @@ export class SamChatAgent extends Think { } const baseUrl = - (await this.ctx.storage.get(PUBLIC_ORIGIN_KEY)) ?? - "https://app.openseo.so"; + (await this.ctx.storage.get(PUBLIC_ORIGIN_KEY)) ?? ""; // Delegated/self-host orgs have no member rows — implicit owner. In // hosted mode a missing member row means the user was removed from the // workspace; fail closed instead of letting the open socket keep diff --git a/src/server/features/sam/openseo-fact-sheet.md b/src/server/features/sam/openseo-fact-sheet.md deleted file mode 100644 index f20657ee8..000000000 --- a/src/server/features/sam/openseo-fact-sheet.md +++ /dev/null @@ -1,152 +0,0 @@ -# OpenSEO Fact Sheet - -This is the factual product reference for Sam, the OpenSEO onboarding agent. If a user asks about OpenSEO and the answer is not supported here, Sam should say it is not sure and point them to support instead of inventing details. - -## What OpenSEO is - -OpenSEO is an open-source SEO platform for keyword research, domain research, backlinks, rank tracking, site audits, Google Search Console, and AI-agent SEO workflows. - -OpenSEO is built for people who want useful SEO data without a bloated enterprise SEO suite. It can be used as a hosted app or self-hosted from the open-source codebase. - -OpenSEO is AI-native. It is designed to work with AI agents through MCP so users can ask an agent to run SEO research, inspect data, save findings, and continue work in the OpenSEO app. - -OpenSEO does not claim to fully automate SEO. The product positioning is that SEO still needs strategy and judgment; OpenSEO helps users and AI agents collaborate on that work with real data. - -## How OpenSEO helps with SEO strategy - -SEO and marketing are intertwined. Getting more organic traffic starts with clear positioning: knowing who the product is for, what problem it solves, and which narrow topics the site can credibly own before trying to compete for broad, high-volume searches. - -OpenSEO helps users turn that positioning into an SEO plan. It can surface relevant keywords, competitor gaps, Search Console opportunities, backlink context, and technical issues, but the goal is not to chase every keyword. The strongest early strategy is usually to build authority around a focused topic where the site has a real angle. - -As the site earns topical authority in Google and AI systems, it becomes easier to compete for broader, higher-volume searches. OpenSEO helps users see that path: start with specific, winnable topics; publish and improve useful pages; build supporting links and internal structure; track what moves; then expand into adjacent and more competitive terms. - -When explaining traffic growth, Sam should frame OpenSEO as a tool for making better SEO and marketing decisions, not as a magic traffic button. OpenSEO provides the data, workflows, and agent access; the user's positioning, content quality, distribution, and execution still matter. - -## Hosted plan and credits - -Hosted OpenSEO is free to try. Signing up requires no credit card, and new accounts include $0.50 of trial credits to test credit-using features before subscribing. - -The paid managed plan costs $10/month. - -The paid plan includes: - -- Keyword research, backlinks, rank tracking, and site audits. -- MCP server and agent skills for Claude, Cursor, ChatGPT-compatible clients, Codex, and other MCP clients. -- Google Search Console integration that does not use credits. -- $10.00 of usage credits each billing cycle. -- A 30-day money-back guarantee for the first charge. - -OpenSEO uses usage credits for features that query paid SEO data providers, especially DataForSEO. Credit-using workflows include keyword volume, competitor data, backlinks, rank tracking, and site audits. Projects, settings, and data that has already been fetched do not cost credits to view. - -Subscribers can purchase top-up credits if monthly credits run out. Top-up credits roll over and do not expire. Monthly included credits reset each billing cycle. Top-ups are only available on the paid plan; a free-tier user who runs out of trial credits subscribes to the paid plan to continue using credit-based features. - -Running out of credits never creates unexpected bills. Credit-using features stop working until the user has credits again. - -## Why OpenSEO for SEO consultants and agencies - -OpenSEO is a strong fit for SEO consultants, freelancers, and agencies managing SEO for clients. What you get: - -- You only pay for what you use. Billing runs on usage credits, so you are not forced into an expensive enterprise tier or charged per seat just to unlock basic work — no arbitrary upsells or features locked behind a paywall. This keeps costs predictable when you are running lean. -- You can run a project for every client. Set up as many projects as you need; you will not hit a per-project plan limit the way many SEO tools cap projects per tier. -- You tune rank tracking to fit your budget. Rank tracking is the cost that scales fastest as an agency grows, since it runs on a schedule across every client's keywords — but OpenSEO makes it fully configurable so you stay in control. You choose how many keywords and devices to track, how many SERP pages deep to check, and how often it runs (weekly or daily), and OpenSEO shows a live cost estimate before each tracker runs. Scheduled checks run through DataForSEO's task queue, which is much cheaper than live lookups, so it stays inexpensive: as a rough guide, tracking 100 keywords on one device type, five pages deep, on the default weekly schedule costs only about $1-2/month. Searching deeper, adding the second device type, or switching to daily checks raises the cost proportionally, and the in-app estimate always shows the current number before you commit. -- Your toolkit grows with the industry. OpenSEO works through MCP and AI agents, so as search shifts toward AI answers and AI-assisted workflows, you can have an agent run research, pull competitor data, and save findings into the right client project — without re-tooling. - -When answering this, Sam should speak directly to the user ("you" / "your clients") about what they get, not describe how OpenSEO is "positioned." Lead with these benefits in plain language and tie them to running an SEO practice. Sam should not invent specific competitor prices or exact rank-tracking rates; if asked for exact numbers it does not have, it should say so and suggest contacting `ben@openseo.so`. - -## Self-hosting - -OpenSEO is open source and can be self-hosted for free. - -Self-hosted users bring their own provider API keys and pay providers such as DataForSEO directly. Self-hosting is appropriate for users who want more control, privacy, customization, or provider-level billing. - -The open-source repository is at `https://github.com/every-app/open-seo`. - -## Data sources - -OpenSEO uses DataForSEO as its main SEO data provider. DataForSEO powers many paid SEO data workflows such as keyword metrics, domain research, backlinks, SERP data, and rank-tracking-related data. - -Google Search Console data comes from the user's connected Search Console property and does not use credits. - -## Google Search Console - -Hosted OpenSEO can connect to Google Search Console without requiring the user to create a Google Cloud project or OAuth client. - -Search Console access is read-only. OpenSEO requests read-only access and cannot change the user's Search Console account. - -Search Console features include: - -- Search performance data: clicks, impressions, CTR, and average position. -- Breakdown by query, page, country, device, and date. -- Up to 16 months of available Search Console history. -- URL inspection data such as index status, crawl information, canonical information, mobile checks, and rich-result checks. -- Up to 10 URLs per URL inspection call. - -Search Console tools use zero OpenSEO credits because Google does not charge users to read their own Search Console data. - -## OpenSEO and Claude (or other AI clients) - -OpenSEO and Claude are not competitors — they are meant to be used together. The short version: OpenSEO is the SEO data layer, and Claude (or Cursor, Codex, ChatGPT-compatible clients, etc.) is the AI client. - -OpenSEO exposes an MCP server, so Claude can call OpenSEO's keyword, SERP, competitor, backlink, rank-tracking, and Search Console tools directly. In practice, Claude does the talking and reasoning, and OpenSEO feeds it real SEO data through MCP. Claude on its own can reason about SEO but has no live keyword volumes, rankings, competitor data, or your Search Console numbers; OpenSEO is what gives it those. - -When a user asks to compare OpenSEO and Claude, or why they would use OpenSEO instead of Claude (or another AI chatbot), Sam should lead with this "they work together" framing and the data-layer point. Sam should not deflect, call it out of scope, or say comparing them would be a guess — connecting OpenSEO to Claude is a core, supported use case. Sam should not, however, rank or rate other AI products it does not have facts about. - -## MCP and AI agents - -OpenSEO exposes an MCP server so compatible AI clients can call OpenSEO tools. - -Hosted MCP endpoint: - -```txt -https://app.openseo.so/mcp -``` - -The first MCP connection sends the user through OpenSEO login and authorization. After authorization, the MCP client can call OpenSEO tools with the project context and account scopes the user approved. - -OpenSEO MCP works with MCP clients including Claude Code, Claude Desktop, Cursor, Codex CLI, Codex Desktop, and other clients that support remote MCP servers. - -OpenSEO MCP tools cover workflows such as: - -- Keyword research with volume, difficulty, CPC, intent, and trends. -- Live Google organic SERP inspection. -- Domain and page ranked keyword research for any domain, including competitors. -- SERP competitor comparisons. -- Local business, Maps, Local Finder, and Google Business Profile Q&A research. -- Saved keyword listing and saving. -- Rank tracker config and latest position reads. -- Domain organic footprint summaries for any domain, including competitors. -- Backlink and referring-domain overview data for any domain, including competitors. -- Google Search Console performance reads. -- Google URL inspection reads. - -OpenSEO also provides agent skills for workflows such as SEO project setup, SEO coaching, keyword research, competitive landscape analysis, competitor analysis, keyword clustering, and link prospecting. - -## App workflows - -OpenSEO's app includes these practical workflows: - -- Keyword research: expand seed topics into keyword ideas, compare search volume, difficulty, CPC, intent, and SERP context, then save useful opportunities. -- Domain overview: understand any domain's organic footprint and ranking keywords — including competitors and other third-party sites, not just the user's own site. Domains are looked up one at a time and use credits. -- Backlink research: inspect backlinks, referring domains, target URLs, link quality signals, and competitor link profiles. -- Rank tracking: track keyword positions over time. -- Site audit: crawl pages and inspect technical page-level signals such as status codes, titles, meta descriptions, headings, indexability, image alt coverage, links, response time, and optional Lighthouse findings. -- Saved keywords: organize keyword opportunities for content planning, tracking, or AI-agent workflows. -- Reports: agents connected over MCP save finished HTML reports into a project, where anyone in the workspace can read, print or export them from the Reports page in the sidebar. You cannot save reports yourself. Reports use no credits, and each project holds up to 10,000. -- AI and MCP setup: connect OpenSEO to agents and install OpenSEO skills. - -## What users can do after subscribing - -After subscribing, a hosted user can: - -- Set up Google Search Console from onboarding or the app. -- Use the OpenSEO app workflows, including keyword research, domain research, backlinks, rank tracking, and site audits. -- Research any domain — their own or a competitor's — with domain overview, ranked keywords, and backlink data (one domain at a time, using credits). -- Connect OpenSEO to an AI client through MCP. -- Install OpenSEO skills for agent-driven SEO workflows. -- Use the monthly included credits and buy top-up credits if needed. - -## Support and uncertainty - -If Sam is unsure about a product detail, current pricing, account-specific billing status, provider limits, or a feature not listed here, it should say it does not know from the product fact sheet and suggest contacting `ben@openseo.so`. - -Users who want advice from other OpenSEO users, the community, or the team can join the OpenSEO Discord at `https://discord.gg/c9uGs3cFXr`. diff --git a/src/server/features/sam/samChatTools.ts b/src/server/features/sam/samChatTools.ts index 01bd81788..1731536eb 100644 --- a/src/server/features/sam/samChatTools.ts +++ b/src/server/features/sam/samChatTools.ts @@ -60,7 +60,7 @@ import { import { whoamiTool } from "@/server/mcp/tools/whoami"; import { discoverSiteUrls, readPages, readSite } from "@/server/lib/scrape"; import { capToolOutput } from "@/server/features/sam/samToolOutput"; -import openSeoFactSheet from "@/server/features/sam/openseo-fact-sheet.md?raw"; +import sightlineFactSheet from "@/server/features/sam/sightline-fact-sheet.md?raw"; // Enough pages for SAM to work out what a business does, sells, and positions // against on its own. @@ -110,7 +110,7 @@ export function toModelOutput(result: CallToolResult): { return { summary, data }; } -// Adapt one OpenSEO tool into an AI SDK tool. The shared handler receives the +// Adapt one Sightline tool into an AI SDK tool. The shared handler receives the // same explicit auth context as the MCP transport, and runs through the same // instrumentation wrapper, so project scoping, credit metering, and the // mcp:tool_call telemetry (source "in_app_agent", null clientId) all match the @@ -299,7 +299,7 @@ function scrapeTools(projectDomain: string | null): ToolSet { /** * Builds SAM's tool surface as an AI SDK ToolSet: the full MCP toolset plus the - * free site-reading tools. Every tool the OpenSEO MCP server exposes is + * free site-reading tools. Every tool the Sightline MCP server exposes is * available except the ones a project-bound chat can't use (list_projects, * create_project) and get_project_context (already a context block). Auth and * billing context are passed directly to the shared tool handlers. DataForSEO @@ -353,9 +353,9 @@ export function buildSamMcpTools( // made the agent narrate hosted/self-hosted framing at signed-in users). get_product_info: tool({ description: - "The OpenSEO fact sheet: what the product does, plans/pricing, credit costs, integrations, MCP setup. Call before answering questions about OpenSEO itself. Uses no credits.", + "The Sightline fact sheet: what the product does, plans/pricing, credit costs, integrations, MCP setup. Call before answering questions about Sightline itself. Uses no credits.", inputSchema: z.object({}), - execute: () => Promise.resolve({ factSheet: openSeoFactSheet }), + execute: () => Promise.resolve({ factSheet: sightlineFactSheet }), }), ...scrapeTools(project.domain), whoami: adaptTool(whoamiTool), diff --git a/src/server/features/sam/samSkills.ts b/src/server/features/sam/samSkills.ts index 7e3ff3449..4034a1788 100644 --- a/src/server/features/sam/samSkills.ts +++ b/src/server/features/sam/samSkills.ts @@ -19,7 +19,7 @@ const skillFiles = import.meta.glob("/.agents/skills/*/SKILL.md", { // The skill bodies are written for external MCP clients (Claude Code); this // note reframes the surface so SAM skips the steps that don't apply in-app. -const SAM_SURFACE_NOTE = `> Surface note: you are SAM, running inside the OpenSEO app. You are already +const SAM_SURFACE_NOTE = `> Surface note: you are SAM, running inside the Sightline app. You are already > authenticated and scoped to the user's current project — skip any "verify the > MCP connection", "choose a project", or skill-install steps. You have no > local filesystem: skip local-folder and file steps. diff --git a/src/server/features/sam/samSystemPrompt.ts b/src/server/features/sam/samSystemPrompt.ts index 5d49a8532..1a1f75419 100644 --- a/src/server/features/sam/samSystemPrompt.ts +++ b/src/server/features/sam/samSystemPrompt.ts @@ -20,23 +20,23 @@ export function buildSamSystemPrompt( ): string { const market = LOCATIONS[project.locationCode] ?? "the project's market"; const sections = [ - "You are SAM, the SEO agent inside OpenSEO. You help the user research keywords, analyze domains and competitors, inspect SERPs, review backlinks, read rank tracking and Google Search Console data, and turn it all into clear next steps.", + "You are SAM, the SEO agent inside Sightline. You help the user research keywords, analyze domains and competitors, inspect SERPs, review backlinks, read rank tracking and Google Search Console data, and turn it all into clear next steps.", "Write in plain prose and Markdown. Lead with a one-sentence direct answer, then short paragraphs or bullets. Use Markdown tables for keyword or competitor data. Do not use decorative emoji or symbol markers.", "Talk like a sharp teammate in chat, not a consultant writing a briefing. Keep replies short. When you need something from the user, ask in one line — never preface it with why you need it or a numbered menu of what you'll do once you have it; they'll see what you do when you do it. Explain your process or reasoning only when the user asks.", "You have tools that pull real search data. Never state a metric, search volume, keyword difficulty, ranking, traffic estimate, or competitor figure you did not get from a tool. If a tool returns no data, say so plainly instead of guessing.", - "These tools are the same ones OpenSEO exposes over its MCP server. They already operate on the active project below — you don't pass or choose a project, so just call them directly for the current project.", + "These tools are the same ones Sightline exposes over its MCP server. They already operate on the active project below — you don't pass or choose a project, so just call them directly for the current project.", [ "Several tools (keyword research, domain overview, SERP results, backlinks, local SERP, ranked keywords, site audits, rank tracker runs) call paid data providers and cost the user credits. Be deliberate: gather what you need to answer well, but don't fan out redundant calls. When a request would require a large batch of paid lookups, briefly confirm with the user first.", "Before running paid research, check the research log in the project_context block. If the same question was answered within the last 30 days, present that conclusion and ask before spending credits again; if the entry is older, say the data may be stale and offer a refresh. When the user asks what to do next, treat the log as covered ground and propose work that is NOT in it.", ].join(" "), [ - "The project_context block is this project's shared memory — the same records the user sees and edits in the app and other OpenSEO agents read. It is read-only here; write with update_project_context, which takes several changes in one call.", + "The project_context block is this project's shared memory — the same records the user sees and edits in the app and other Sightline agents read. It is read-only here; write with update_project_context, which takes several changes in one call.", "Durable facts belong in the typed sections (business_overview: what the business does, who it's for, target market; current_goal; positioning; writing_preferences: voice, banned words, topics to avoid), with competitors and key pages as curated shortlists (addCompetitors / addKeyPages) and a custom section for anything durable that fits none of them.", 'Sections are short curated prose, not transcripts: rewrite a whole section to fold a new fact in, never paste raw tool output, and confirm an inference with the user before storing it as fact. When you finish a research arc, append a research log entry — ": . Verdict: ", conclusions and pointers (e.g. saved keyword tags) rather than data; the date is added for you.', ].join(" "), "When you run tools, narrate nothing — just call them, then synthesize the results into a concise, specific answer for THIS project. Prefer doing the work over describing what you could do.", - "You are talking to a signed-in user inside the OpenSEO app. Never pitch plans, upgrades, or hosted-vs-self-hosted — none of that belongs in this chat. When they need to do something in the app (like connecting Search Console), give them the link a tool attached rather than describing menus; do not invent app URLs.", - "For questions about OpenSEO itself (features, pricing, limits, integrations), call get_product_info and answer from it — do not invent product facts. If it does not cover the answer, say you are not sure and suggest ben@openseo.so.", + "You are talking to a signed-in user inside the Sightline app. Never pitch plans, upgrades, or hosted-vs-self-hosted — none of that belongs in this chat. When they need to do something in the app (like connecting Search Console), give them the link a tool attached rather than describing menus; do not invent app URLs.", + "For questions about Sightline itself (features, pricing, limits, integrations), call get_product_info and answer from it — do not invent product facts. If it does not cover the answer, say you are not sure rather than guessing.", `Active project: "${project.projectName}" (projectId: ${project.projectId}).`, project.domain ? `Project website: ${project.domain}. Default market: ${market} (location ${project.locationCode}, language ${project.languageCode}).` diff --git a/src/server/features/sam/sightline-fact-sheet.md b/src/server/features/sam/sightline-fact-sheet.md new file mode 100644 index 000000000..058eeb8e2 --- /dev/null +++ b/src/server/features/sam/sightline-fact-sheet.md @@ -0,0 +1,152 @@ +# Sightline Fact Sheet + +This is the factual product reference for Sam, the Sightline onboarding agent. If a user asks about Sightline and the answer is not supported here, Sam should say it is not sure and point them to support instead of inventing details. + +## What Sightline is + +Sightline is an open-source SEO platform for keyword research, domain research, backlinks, rank tracking, site audits, Google Search Console, and AI-agent SEO workflows. + +Sightline is built for people who want useful SEO data without a bloated enterprise SEO suite. It can be used as a hosted app or self-hosted from the open-source codebase. + +Sightline is AI-native. It is designed to work with AI agents through MCP so users can ask an agent to run SEO research, inspect data, save findings, and continue work in the Sightline app. + +Sightline does not claim to fully automate SEO. The product positioning is that SEO still needs strategy and judgment; Sightline helps users and AI agents collaborate on that work with real data. + +## How Sightline helps with SEO strategy + +SEO and marketing are intertwined. Getting more organic traffic starts with clear positioning: knowing who the product is for, what problem it solves, and which narrow topics the site can credibly own before trying to compete for broad, high-volume searches. + +Sightline helps users turn that positioning into an SEO plan. It can surface relevant keywords, competitor gaps, Search Console opportunities, backlink context, and technical issues, but the goal is not to chase every keyword. The strongest early strategy is usually to build authority around a focused topic where the site has a real angle. + +As the site earns topical authority in Google and AI systems, it becomes easier to compete for broader, higher-volume searches. Sightline helps users see that path: start with specific, winnable topics; publish and improve useful pages; build supporting links and internal structure; track what moves; then expand into adjacent and more competitive terms. + +When explaining traffic growth, Sam should frame Sightline as a tool for making better SEO and marketing decisions, not as a magic traffic button. Sightline provides the data, workflows, and agent access; the user's positioning, content quality, distribution, and execution still matter. + +## Hosted plan and credits + +Hosted Sightline is free to try. Signing up requires no credit card, and new accounts include $0.50 of trial credits to test credit-using features before subscribing. + +The paid managed plan costs $10/month. + +The paid plan includes: + +- Keyword research, backlinks, rank tracking, and site audits. +- MCP server and agent skills for Claude, Cursor, ChatGPT-compatible clients, Codex, and other MCP clients. +- Google Search Console integration that does not use credits. +- $10.00 of usage credits each billing cycle. +- A 30-day money-back guarantee for the first charge. + +Sightline uses usage credits for features that query paid SEO data providers, especially DataForSEO. Credit-using workflows include keyword volume, competitor data, backlinks, rank tracking, and site audits. Projects, settings, and data that has already been fetched do not cost credits to view. + +Subscribers can purchase top-up credits if monthly credits run out. Top-up credits roll over and do not expire. Monthly included credits reset each billing cycle. Top-ups are only available on the paid plan; a free-tier user who runs out of trial credits subscribes to the paid plan to continue using credit-based features. + +Running out of credits never creates unexpected bills. Credit-using features stop working until the user has credits again. + +## Why Sightline for SEO consultants and agencies + +Sightline is a strong fit for SEO consultants, freelancers, and agencies managing SEO for clients. What you get: + +- You only pay for what you use. Billing runs on usage credits, so you are not forced into an expensive enterprise tier or charged per seat just to unlock basic work — no arbitrary upsells or features locked behind a paywall. This keeps costs predictable when you are running lean. +- You can run a project for every client. Set up as many projects as you need; you will not hit a per-project plan limit the way many SEO tools cap projects per tier. +- You tune rank tracking to fit your budget. Rank tracking is the cost that scales fastest as an agency grows, since it runs on a schedule across every client's keywords — but Sightline makes it fully configurable so you stay in control. You choose how many keywords and devices to track, how many SERP pages deep to check, and how often it runs (weekly or daily), and Sightline shows a live cost estimate before each tracker runs. Scheduled checks run through DataForSEO's task queue, which is much cheaper than live lookups, so it stays inexpensive: as a rough guide, tracking 100 keywords on one device type, five pages deep, on the default weekly schedule costs only about $1-2/month. Searching deeper, adding the second device type, or switching to daily checks raises the cost proportionally, and the in-app estimate always shows the current number before you commit. +- Your toolkit grows with the industry. Sightline works through MCP and AI agents, so as search shifts toward AI answers and AI-assisted workflows, you can have an agent run research, pull competitor data, and save findings into the right client project — without re-tooling. + +When answering this, Sam should speak directly to the user ("you" / "your clients") about what they get, not describe how Sightline is "positioned." Lead with these benefits in plain language and tie them to running an SEO practice. Sam should not invent specific competitor prices or exact rank-tracking rates; if asked for exact numbers it does not have, it should say so rather than guessing. + +## Self-hosting + +Sightline is open source and can be self-hosted for free. + +Self-hosted users bring their own provider API keys and pay providers such as DataForSEO directly. Self-hosting is appropriate for users who want more control, privacy, customization, or provider-level billing. + +The open-source repository is at `https://github.com/every-app/open-seo`. + +## Data sources + +Sightline uses DataForSEO as its main SEO data provider. DataForSEO powers many paid SEO data workflows such as keyword metrics, domain research, backlinks, SERP data, and rank-tracking-related data. + +Google Search Console data comes from the user's connected Search Console property and does not use credits. + +## Google Search Console + +Hosted Sightline can connect to Google Search Console without requiring the user to create a Google Cloud project or OAuth client. + +Search Console access is read-only. Sightline requests read-only access and cannot change the user's Search Console account. + +Search Console features include: + +- Search performance data: clicks, impressions, CTR, and average position. +- Breakdown by query, page, country, device, and date. +- Up to 16 months of available Search Console history. +- URL inspection data such as index status, crawl information, canonical information, mobile checks, and rich-result checks. +- Up to 10 URLs per URL inspection call. + +Search Console tools use zero Sightline credits because Google does not charge users to read their own Search Console data. + +## Sightline and Claude (or other AI clients) + +Sightline and Claude are not competitors — they are meant to be used together. The short version: Sightline is the SEO data layer, and Claude (or Cursor, Codex, ChatGPT-compatible clients, etc.) is the AI client. + +Sightline exposes an MCP server, so Claude can call Sightline's keyword, SERP, competitor, backlink, rank-tracking, and Search Console tools directly. In practice, Claude does the talking and reasoning, and Sightline feeds it real SEO data through MCP. Claude on its own can reason about SEO but has no live keyword volumes, rankings, competitor data, or your Search Console numbers; Sightline is what gives it those. + +When a user asks to compare Sightline and Claude, or why they would use Sightline instead of Claude (or another AI chatbot), Sam should lead with this "they work together" framing and the data-layer point. Sam should not deflect, call it out of scope, or say comparing them would be a guess — connecting Sightline to Claude is a core, supported use case. Sam should not, however, rank or rate other AI products it does not have facts about. + +## MCP and AI agents + +Sightline exposes an MCP server so compatible AI clients can call Sightline tools. + +Hosted MCP endpoint: + +```txt +https://app.openseo.so/mcp +``` + +The first MCP connection sends the user through Sightline login and authorization. After authorization, the MCP client can call Sightline tools with the project context and account scopes the user approved. + +Sightline MCP works with MCP clients including Claude Code, Claude Desktop, Cursor, Codex CLI, Codex Desktop, and other clients that support remote MCP servers. + +Sightline MCP tools cover workflows such as: + +- Keyword research with volume, difficulty, CPC, intent, and trends. +- Live Google organic SERP inspection. +- Domain and page ranked keyword research for any domain, including competitors. +- SERP competitor comparisons. +- Local business, Maps, Local Finder, and Google Business Profile Q&A research. +- Saved keyword listing and saving. +- Rank tracker config and latest position reads. +- Domain organic footprint summaries for any domain, including competitors. +- Backlink and referring-domain overview data for any domain, including competitors. +- Google Search Console performance reads. +- Google URL inspection reads. + +Sightline also provides agent skills for workflows such as SEO project setup, SEO coaching, keyword research, competitive landscape analysis, competitor analysis, keyword clustering, and link prospecting. + +## App workflows + +Sightline's app includes these practical workflows: + +- Keyword research: expand seed topics into keyword ideas, compare search volume, difficulty, CPC, intent, and SERP context, then save useful opportunities. +- Domain overview: understand any domain's organic footprint and ranking keywords — including competitors and other third-party sites, not just the user's own site. Domains are looked up one at a time and use credits. +- Backlink research: inspect backlinks, referring domains, target URLs, link quality signals, and competitor link profiles. +- Rank tracking: track keyword positions over time. +- Site audit: crawl pages and inspect technical page-level signals such as status codes, titles, meta descriptions, headings, indexability, image alt coverage, links, response time, and optional Lighthouse findings. +- Saved keywords: organize keyword opportunities for content planning, tracking, or AI-agent workflows. +- Reports: agents connected over MCP save finished HTML reports into a project, where anyone in the workspace can read, print or export them from the Reports page in the sidebar. You cannot save reports yourself. Reports use no credits, and each project holds up to 10,000. +- AI and MCP setup: connect Sightline to agents and install Sightline skills. + +## What users can do after subscribing + +After subscribing, a hosted user can: + +- Set up Google Search Console from onboarding or the app. +- Use the Sightline app workflows, including keyword research, domain research, backlinks, rank tracking, and site audits. +- Research any domain — their own or a competitor's — with domain overview, ranked keywords, and backlink data (one domain at a time, using credits). +- Connect Sightline to an AI client through MCP. +- Install Sightline skills for agent-driven SEO workflows. +- Use the monthly included credits and buy top-up credits if needed. + +## Support and uncertainty + +If Sam is unsure about a product detail, current pricing, account-specific billing status, provider limits, or a feature not listed here, it should say it does not know from the product fact sheet rather than guessing. + +Sightline is built on the open-source OpenSEO project. Users who want advice from that project's wider community can join its Discord at `https://discord.gg/c9uGs3cFXr`. diff --git a/src/server/gdpr/storage-erasure.ts b/src/server/gdpr/storage-erasure.ts index 927917f99..0fa0562c7 100644 --- a/src/server/gdpr/storage-erasure.ts +++ b/src/server/gdpr/storage-erasure.ts @@ -1,10 +1,6 @@ import { getAuth } from "@/lib/auth"; import type { SamChatAgent } from "@/server/features/sam/SamChatAgent"; import { captureServerError } from "@/server/lib/posthog"; -import { - DUB_REFERRED_ORG_KV_PREFIX, - DUB_REFERRED_USER_KV_PREFIX, -} from "@/server/referrals/dub"; import { AI_SEARCH_PROMPT_CACHE_NAMESPACE, cacheObjectPrefix, @@ -162,7 +158,7 @@ async function revokeGoogleAccount( body: new URLSearchParams({ token: accessToken }), }); // Google uses invalid_token for an already-revoked token. Either response - // leaves OpenSEO without a live upstream grant once the local row is erased. + // leaves Sightline without a live upstream grant once the local row is erased. if (!response.ok && response.status !== 400) { throw new Error( `Google token revocation failed for ${account.providerId}/${account.accountId}: ${response.status}`, @@ -207,14 +203,6 @@ async function eraseStorage(env: Env, payload: GdprStorageErasurePayload) { // TTL, and clearing them would let an in-flight authenticated request // re-create the Autumn customer before the Postgres delete lands. - // Dub referral pins key on user/org ids and store a referral click - // identifier. The Dub-side customer record (pseudonymous external id) is - // removed via the erasure runbook. - await env.KV.delete(`${DUB_REFERRED_USER_KV_PREFIX}${payload.userId}`); - for (const organizationId of payload.organizationIds) { - await env.KV.delete(`${DUB_REFERRED_ORG_KV_PREFIX}${organizationId}`); - } - for (let index = 0; index < payload.r2Keys.length; index += 1_000) { await env.R2.delete(payload.r2Keys.slice(index, index + 1_000)); } diff --git a/src/server/lib/audit/discovery.ts b/src/server/lib/audit/discovery.ts index 1113f1af2..033f6f9b1 100644 --- a/src/server/lib/audit/discovery.ts +++ b/src/server/lib/audit/discovery.ts @@ -40,7 +40,7 @@ export interface RobotsResult { async function fetchRobotsTxtText(origin: string): Promise { try { const response = await fetch(`${origin}/robots.txt`, { - headers: { "User-Agent": "OpenSEO-Audit/1.0" }, + headers: { "User-Agent": "Sightline-Audit/1.0" }, signal: AbortSignal.timeout(10_000), }); @@ -173,7 +173,7 @@ async function fetchSitemapDocumentWithRetry(sitemapUrl: string): Promise<{ for (let attempt = 0; attempt <= SITEMAP_RETRIES; attempt++) { try { const response = await fetch(normalizedSitemapUrl, { - headers: { "User-Agent": "OpenSEO-Audit/1.0" }, + headers: { "User-Agent": "Sightline-Audit/1.0" }, signal: AbortSignal.timeout(SITEMAP_FETCH_TIMEOUT_MS), }); diff --git a/src/server/lib/audit/url-policy.ts b/src/server/lib/audit/url-policy.ts index c2fb5d35e..774128d96 100644 --- a/src/server/lib/audit/url-policy.ts +++ b/src/server/lib/audit/url-policy.ts @@ -263,7 +263,7 @@ export async function resolveStartUrlRedirects( response = await fetch(current, { method: "HEAD", redirect: "manual", - headers: { "User-Agent": "OpenSEO-Audit/1.0" }, + headers: { "User-Agent": "Sightline-Audit/1.0" }, signal: AbortSignal.timeout(START_URL_PROBE_TIMEOUT_MS), }); } catch { diff --git a/src/server/lib/dataforseo/endpoints.test.ts b/src/server/lib/dataforseo/endpoints.test.ts index 3046ce91a..6c6a21d12 100644 --- a/src/server/lib/dataforseo/endpoints.test.ts +++ b/src/server/lib/dataforseo/endpoints.test.ts @@ -329,7 +329,7 @@ describe("DataForSEO SDK-backed endpoints", () => { vi.stubGlobal("fetch", fetchMock); await fetchLlmResponse({ - userPrompt: "What is OpenSEO?", + userPrompt: "What is Sightline?", modelSlug: "perplexity", modelName: "sonar", webSearch: false, @@ -347,7 +347,7 @@ describe("DataForSEO SDK-backed endpoints", () => { ]); expect(parseDataforseoRequestBody(fetchMock.mock.calls[0]?.[1])).toEqual([ { - user_prompt: "What is OpenSEO?", + user_prompt: "What is Sightline?", model_name: "sonar", web_search: false, max_output_tokens: 1024, @@ -368,7 +368,7 @@ describe("fetchLlmResponse model_name validation", () => { await expect( fetchLlmResponse({ - userPrompt: "What is OpenSEO?", + userPrompt: "What is Sightline?", modelSlug: "claude", // DataForSEO dropped this from its catalog; it must never be dispatched. modelName: "claude-sonnet-4-0", diff --git a/src/server/lib/scrape.ts b/src/server/lib/scrape.ts index d8bd71865..80a4b0b45 100644 --- a/src/server/lib/scrape.ts +++ b/src/server/lib/scrape.ts @@ -11,7 +11,7 @@ const MAX_PAGES = 5; const PER_PAGE_CHAR_LIMIT = 4000; const FETCH_TIMEOUT_MS = 10_000; const MAX_RESPONSE_BYTES = 2_000_000; -const USER_AGENT = "OpenSEO-Onboarding/1.0 (+https://openseo.so)"; +const USER_AGENT = "Sightline-Onboarding/1.0"; type ScrapedPage = { url: string; diff --git a/src/server/mcp/api-key-auth.test.ts b/src/server/mcp/api-key-auth.test.ts index 15e9fb751..b1e4c90e1 100644 --- a/src/server/mcp/api-key-auth.test.ts +++ b/src/server/mcp/api-key-auth.test.ts @@ -209,7 +209,7 @@ describe("handleMcpApiKeyRequest", () => { expect(mocks.handleAuthenticatedOpenSeoMcpRequest).not.toHaveBeenCalled(); }); - it("leaves non-OpenSEO bearer tokens for OAuth", async () => { + it("leaves non-Sightline bearer tokens for OAuth", async () => { await expect( handleMcpApiKeyRequest( request({ Authorization: "Bearer oauth-access-token" }), @@ -220,7 +220,7 @@ describe("handleMcpApiKeyRequest", () => { expect(mocks.verifyApiKey).not.toHaveBeenCalled(); }); - it("leaves non-OpenSEO x-api-key values for OAuth", async () => { + it("leaves non-Sightline x-api-key values for OAuth", async () => { await expect( handleMcpApiKeyRequest( request({ diff --git a/src/server/mcp/context.test.ts b/src/server/mcp/context.test.ts index 2ee2d23ab..e2019e7dd 100644 --- a/src/server/mcp/context.test.ts +++ b/src/server/mcp/context.test.ts @@ -14,7 +14,7 @@ const applicationContext = { baseUrl: "https://open-seo.test", }; -describe("OpenSEO tool auth context", () => { +describe("Sightline tool auth context", () => { it("stores only application-specific identity in Workers OAuth props", () => { const props = createWorkersOAuthMcpProps(applicationContext); diff --git a/src/server/mcp/oauth-provider.test.ts b/src/server/mcp/oauth-provider.test.ts index 13653bd7e..7c9e7f657 100644 --- a/src/server/mcp/oauth-provider.test.ts +++ b/src/server/mcp/oauth-provider.test.ts @@ -171,7 +171,7 @@ async function invokeDefaultHandler( return rawResponse; } -describe("OpenSEO OAuth provider configuration", () => { +describe("Sightline OAuth provider configuration", () => { beforeEach(() => { mocks.options.length = 0; mocks.requests.length = 0; @@ -188,7 +188,7 @@ describe("OpenSEO OAuth provider configuration", () => { expect(mocks.options[0]?.resourceMetadata).toEqual({ resource: "https://app.openseo.so/mcp", scopes_supported: ["mcp"], - resource_name: "OpenSEO MCP", + resource_name: "Sightline MCP", }); expect(mocks.options[0]?.scopesSupported).toEqual([ "offline_access", diff --git a/src/server/mcp/oauth-provider.ts b/src/server/mcp/oauth-provider.ts index b0e325464..5eb7b4521 100644 --- a/src/server/mcp/oauth-provider.ts +++ b/src/server/mcp/oauth-provider.ts @@ -414,7 +414,7 @@ function createProvider(appFetch: AppFetch, resource: string) { resourceMetadata: { resource, scopes_supported: [MCP_SCOPE], - resource_name: "OpenSEO MCP", + resource_name: "Sightline MCP", }, tokenExchangeCallback: ({ props, requestedScope }) => { if (!requestedScope.includes(MCP_SCOPE)) { diff --git a/src/server/mcp/schemas.ts b/src/server/mcp/schemas.ts index 18a3ba461..d633bf4de 100644 --- a/src/server/mcp/schemas.ts +++ b/src/server/mcp/schemas.ts @@ -7,7 +7,7 @@ export const projectIdSchema = z .string() .min(1) .describe( - "Required. The OpenSEO project ID to scope this call to. Get one from list_projects.", + "Required. The Sightline project ID to scope this call to. Get one from list_projects.", ); export const locationCodeSchema = z diff --git a/src/server/mcp/server.ts b/src/server/mcp/server.ts index cd5018abc..a4236ea19 100644 --- a/src/server/mcp/server.ts +++ b/src/server/mcp/server.ts @@ -138,19 +138,11 @@ function registerOpenSeoTool( export function createOpenSeoMcpServer(authProps: McpProps) { const server = new McpServer( { - name: "OpenSEO MCP", - title: "OpenSEO", + name: "Sightline MCP", + title: "Sightline", version: "0.0.12", description: "SEO research tools for AI agents: keyword research and metrics, SERP and local SERP results, domain and backlink analysis, rank tracking, and Google Search Console performance.", - websiteUrl: "https://openseo.so", - icons: [ - { - src: "https://openseo.so/android-chrome-512x512.png", - mimeType: "image/png", - sizes: ["512x512"], - }, - ], }, { // The tool list is fixed per request and no list_changed notification @@ -159,7 +151,7 @@ export function createOpenSeoMcpServer(authProps: McpProps) { // Without the pre-declaration, registerTool defaults it to true. capabilities: { tools: { listChanged: false } }, instructions: - "OpenSEO research tools use credits. Proceed with normal focused research, but ask the user for confirmation before planned batches over 2,000 credits.", + "Sightline research tools use credits. Proceed with normal focused research, but ask the user for confirmation before planned batches over 2,000 credits.", }, ); diff --git a/src/server/mcp/tools/create-project.ts b/src/server/mcp/tools/create-project.ts index 3af27b144..5d13d3faf 100644 --- a/src/server/mcp/tools/create-project.ts +++ b/src/server/mcp/tools/create-project.ts @@ -103,7 +103,7 @@ export const createProjectTool = { config: { title: "Create project", description: - "Create a new project in the user's organization. Uses no credits — does not call DataForSEO. Provide a name, and optionally a domain and default market (locationCode/languageCode; a languageCode requires a locationCode). Returns the created {id, name, domain, locationCode, languageCode, url}; pass the returned `id` as `projectId` to other OpenSEO tools. Call list_projects first to avoid creating a duplicate.", + "Create a new project in the user's organization. Uses no credits — does not call DataForSEO. Provide a name, and optionally a domain and default market (locationCode/languageCode; a languageCode requires a locationCode). Returns the created {id, name, domain, locationCode, languageCode, url}; pass the returned `id` as `projectId` to other Sightline tools. Call list_projects first to avoid creating a duplicate.", inputSchema, outputSchema: { project: z diff --git a/src/server/mcp/tools/get-serp-results.ts b/src/server/mcp/tools/get-serp-results.ts index 70e140e15..96c905eac 100644 --- a/src/server/mcp/tools/get-serp-results.ts +++ b/src/server/mcp/tools/get-serp-results.ts @@ -65,7 +65,7 @@ export const getSerpResultsTool = { config: { title: "Get Google SERP results", description: - "Fetch live Google organic search results for 1-10 keywords. Use this to inspect who ranks for a query, verify competitors, compare SERPs across keywords, or gather source URLs before content planning. Returns the top `depth` result rows per keyword (default 20). Charges credits per keyword: ~5 each at the default depth 20, and each additional 10 of depth adds ~2.5. Does not save results to OpenSEO. Per-keyword errors don't fail the batch.", + "Fetch live Google organic search results for 1-10 keywords. Use this to inspect who ranks for a query, verify competitors, compare SERPs across keywords, or gather source URLs before content planning. Returns the top `depth` result rows per keyword (default 20). Charges credits per keyword: ~5 each at the default depth 20, and each additional 10 of depth adds ~2.5. Does not save results to Sightline. Per-keyword errors don't fail the batch.", inputSchema, outputSchema: { results: z.array( diff --git a/src/server/mcp/tools/google-analytics-tools.ts b/src/server/mcp/tools/google-analytics-tools.ts index a5899f372..edcfe6840 100644 --- a/src/server/mcp/tools/google-analytics-tools.ts +++ b/src/server/mcp/tools/google-analytics-tools.ts @@ -334,7 +334,7 @@ export const getGoogleAnalyticsOrganicLandingPagesTool = { config: { title: "Get Google Analytics organic landing pages", description: - "Read organic-search landing page sessions, engagement, key events, transactions, and revenue from the project's connected GA4 property. Defaults to the last 28 complete property days. Read-only and uses no OpenSEO credits.", + "Read organic-search landing page sessions, engagement, key events, transactions, and revenue from the project's connected GA4 property. Defaults to the last 28 complete property days. Read-only and uses no Sightline credits.", inputSchema: landingPageInputSchema, outputSchema: reportOutputSchema, annotations: { @@ -368,7 +368,7 @@ export const getGoogleAnalyticsPagePerformanceTool = { config: { title: "Get Google Analytics page performance", description: - "Read page views, users, engagement duration, and key events from the connected GA4 property. Organic Search is the default; set channel to all to include every channel. Read-only and uses no OpenSEO credits.", + "Read page views, users, engagement duration, and key events from the connected GA4 property. Organic Search is the default; set channel to all to include every channel. Read-only and uses no Sightline credits.", inputSchema: pagePerformanceInputSchema, outputSchema: reportOutputSchema, annotations: { @@ -405,7 +405,7 @@ export const getGoogleAnalyticsKeyEventsTool = { config: { title: "Get Google Analytics key events", description: - "Read active GA4 key events with counts and users by event or organic landing page. Previous-period comparison is available for the event breakdown. Read-only and uses no OpenSEO credits.", + "Read active GA4 key events with counts and users by event or organic landing page. Previous-period comparison is available for the event breakdown. Read-only and uses no Sightline credits.", inputSchema: keyEventsInputSchema, outputSchema: reportOutputSchema, annotations: { @@ -436,7 +436,7 @@ export const getSearchOpportunitiesTool = { config: { title: "Get search opportunities", description: - "Join Search Console pages ranking in positions 4–20 with GA4 organic landing-page outcomes, then score matched opportunities by demand, business value, and reachability. Unmatched pages remain visible and unscored. Read-only and uses no OpenSEO credits.", + "Join Search Console pages ranking in positions 4–20 with GA4 organic landing-page outcomes, then score matched opportunities by demand, business value, and reachability. Unmatched pages remain visible and unscored. Read-only and uses no Sightline credits.", inputSchema: opportunityInputSchema, outputSchema: opportunityOutputSchema, annotations: { @@ -472,7 +472,7 @@ export const getGoogleAnalyticsOrganicOverviewTool = { config: { title: "Get Google Analytics organic overview", description: - "Answer whether organic traffic is improving with top-line sessions, users, engagement, key events, transactions, revenue, an equal-length previous-period comparison, and a daily or weekly trend. Read-only and uses no OpenSEO credits.", + "Answer whether organic traffic is improving with top-line sessions, users, engagement, key events, transactions, revenue, an equal-length previous-period comparison, and a daily or weekly trend. Read-only and uses no Sightline credits.", inputSchema: overviewInputSchema, outputSchema: overviewOutputSchema, annotations: { @@ -510,7 +510,7 @@ export const getGoogleAnalyticsTrafficAcquisitionTool = { config: { title: "Get Google Analytics traffic acquisition", description: - "Compare session acquisition by channel group, source/medium, or campaign, including sessions, users, engagement, key events, transactions, and revenue. Previous-period comparison is available for channel group; source/medium also reports attribution-quality diagnostics. Read-only and uses no OpenSEO credits.", + "Compare session acquisition by channel group, source/medium, or campaign, including sessions, users, engagement, key events, transactions, and revenue. Previous-period comparison is available for channel group; source/medium also reports attribution-quality diagnostics. Read-only and uses no Sightline credits.", inputSchema: trafficAcquisitionInputSchema, outputSchema: reportOutputSchema, annotations: { @@ -547,7 +547,7 @@ export const getGoogleAnalyticsEcommercePerformanceTool = { config: { title: "Get Google Analytics ecommerce performance", description: - "Read item views, add-to-cart units, purchases, and item revenue by item, or transactions and purchase revenue by landing page. Returns a detected, none, or unknown activity state; landing pages can be limited to those with transactions. Organic Search is the default. Read-only and uses no OpenSEO credits.", + "Read item views, add-to-cart units, purchases, and item revenue by item, or transactions and purchase revenue by landing page. Returns a detected, none, or unknown activity state; landing pages can be limited to those with transactions. Organic Search is the default. Read-only and uses no Sightline credits.", inputSchema: ecommerceInputSchema, outputSchema: reportOutputSchema, annotations: { @@ -576,7 +576,7 @@ export const getGoogleAnalyticsSiteSearchTool = { config: { title: "Get Google Analytics site search", description: - "Read measured internal search terms with search events, users, sessions, engaged sessions, and engagement rate. Requires GA4 site-search measurement. Read-only and uses no OpenSEO credits.", + "Read measured internal search terms with search events, users, sessions, engaged sessions, and engagement rate. Requires GA4 site-search measurement. Read-only and uses no Sightline credits.", inputSchema: siteSearchInputSchema, outputSchema: reportOutputSchema, annotations: { @@ -614,7 +614,7 @@ export const getGoogleAnalyticsAudienceBreakdownTool = { config: { title: "Get Google Analytics audience breakdown", description: - "Read device, country, or new-versus-returning users, sessions, engagement, and key events. Previous-period comparison is available for device and new-versus-returning breakdowns. No demographic or user-level dimensions. Read-only and uses no OpenSEO credits.", + "Read device, country, or new-versus-returning users, sessions, engagement, and key events. Previous-period comparison is available for device and new-versus-returning breakdowns. No demographic or user-level dimensions. Read-only and uses no Sightline credits.", inputSchema: audienceInputSchema, outputSchema: reportOutputSchema, annotations: { @@ -644,7 +644,7 @@ export const getGoogleAnalyticsMeasurementHealthTool = { config: { title: "Get Google Analytics measurement health", description: - "Diagnose the connected property's data streams, web measurement IDs, enhanced-measurement settings, key events, and custom definitions. Read-only and uses no OpenSEO credits.", + "Diagnose the connected property's data streams, web measurement IDs, enhanced-measurement settings, key events, and custom definitions. Read-only and uses no Sightline credits.", inputSchema: measurementHealthInputSchema, outputSchema: measurementHealthOutputSchema, annotations: { diff --git a/src/server/mcp/tools/list-projects.ts b/src/server/mcp/tools/list-projects.ts index 8be9ca4a5..7e2bac935 100644 --- a/src/server/mcp/tools/list-projects.ts +++ b/src/server/mcp/tools/list-projects.ts @@ -41,7 +41,7 @@ export const listProjectsTool = { config: { title: "List projects", description: - "Lists the user's projects. Uses no credits — does not call DataForSEO. Use this whenever you need a `projectId` for another OpenSEO tool. Returns an array of {id, name, domain, locationCode, languageCode}; pass the `id` value as `projectId`. locationCode/languageCode are the project's default market — tools fall back to them when a call omits location/language args. When the user belongs to several organizations, each project is labeled with its organization and organizationId (pass that to create_project).", + "Lists the user's projects. Uses no credits — does not call DataForSEO. Use this whenever you need a `projectId` for another Sightline tool. Returns an array of {id, name, domain, locationCode, languageCode}; pass the `id` value as `projectId`. locationCode/languageCode are the project's default market — tools fall back to them when a call omits location/language args. When the user belongs to several organizations, each project is labeled with its organization and organizationId (pass that to create_project).", inputSchema: {} as Record, outputSchema: { projects: z.array( diff --git a/src/server/mcp/tools/list-saved-keywords.ts b/src/server/mcp/tools/list-saved-keywords.ts index 277f12da6..1e049688a 100644 --- a/src/server/mcp/tools/list-saved-keywords.ts +++ b/src/server/mcp/tools/list-saved-keywords.ts @@ -62,7 +62,7 @@ export const listSavedKeywordsTool = { config: { title: "List saved keywords", description: - "Lists keywords saved to a project (with cached metrics like search volume, difficulty, CPC, and tags if available). Uses no credits — reads from OpenSEO's database, no DataForSEO call. Use tag filters when the user asks for a saved segment; multiple tags match ANY tag.", + "Lists keywords saved to a project (with cached metrics like search volume, difficulty, CPC, and tags if available). Uses no credits — reads from Sightline's database, no DataForSEO call. Use tag filters when the user asks for a saved segment; multiple tags match ANY tag.", inputSchema, outputSchema: { rows: z.array(savedKeywordOutputSchema), diff --git a/src/server/mcp/tools/project-context.test.ts b/src/server/mcp/tools/project-context.test.ts index dec3c4eeb..8ee3d9316 100644 --- a/src/server/mcp/tools/project-context.test.ts +++ b/src/server/mcp/tools/project-context.test.ts @@ -100,7 +100,7 @@ describe("get_project_context", () => { name: "Monthly client check-in", description: "The monthly update we send retainer clients.", instructions: "Audience: the client's marketing lead.", - createdBy: "OpenSEO app", + createdBy: "Sightline app", createdByUserId: "user_1", createdAt: "2026-09-01T10:00:00.000Z", updatedAt: "2026-09-01T10:00:00.000Z", diff --git a/src/server/mcp/tools/project-context.ts b/src/server/mcp/tools/project-context.ts index 6d72b6ad2..aae0003f6 100644 --- a/src/server/mcp/tools/project-context.ts +++ b/src/server/mcp/tools/project-context.ts @@ -36,7 +36,7 @@ export const getProjectContextTool = { config: { title: "Get project context", description: - "Reads a project's shared memory: business overview, current goal, positioning, writing preferences, custom sections, competitors, key pages, and the recent research log. Uses no credits. Call this before SEO work to ground it in what the user already told OpenSEO, and check the research log before re-buying research. Sections listed as missing are the ones worth filling with update_project_context.", + "Reads a project's shared memory: business overview, current goal, positioning, writing preferences, custom sections, competitors, key pages, and the recent research log. Uses no credits. Call this before SEO work to ground it in what the user already told Sightline, and check the research log before re-buying research. Sections listed as missing are the ones worth filling with update_project_context.", inputSchema: getInputSchema, outputSchema: contextOutputSchema, annotations: { diff --git a/src/server/mcp/tools/search-console-tools.ts b/src/server/mcp/tools/search-console-tools.ts index d0e1397fb..64b03ba8b 100644 --- a/src/server/mcp/tools/search-console-tools.ts +++ b/src/server/mcp/tools/search-console-tools.ts @@ -78,7 +78,7 @@ async function missingSelfHostedGoogleClientResponse( if (hosted || configured) return null; return mcpResponse({ - text: `This self-hosted OpenSEO deployment is not configured for Search Console yet. Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and BETTER_AUTH_SECRET, then reconnect Search Console from the project's settings page. Setup docs: ${GSC_SELF_HOSTED_SETUP_DOCS_URL}`, + text: `This self-hosted Sightline deployment is not configured for Search Console yet. Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and BETTER_AUTH_SECRET, then reconnect Search Console from the project's settings page. Setup docs: ${GSC_SELF_HOSTED_SETUP_DOCS_URL}`, meta: buildProjectMeta(context, projectId), structuredContent: { ok: false, diff --git a/src/server/mcp/tools/site-audit-tools.ts b/src/server/mcp/tools/site-audit-tools.ts index dabb7e3a3..d5c6ee084 100644 --- a/src/server/mcp/tools/site-audit-tools.ts +++ b/src/server/mcp/tools/site-audit-tools.ts @@ -163,7 +163,7 @@ export const getAuditStatusTool = { config: { title: "Get site audit status", description: - "Check the progress of a site audit (phase, pages crawled, Lighthouse progress). Free — reads OpenSEO state and may reconcile a dead workflow by marking its audit failed. Omit auditId for the most recent audit.", + "Check the progress of a site audit (phase, pages crawled, Lighthouse progress). Free — reads Sightline state and may reconcile a dead workflow by marking its audit failed. Omit auditId for the most recent audit.", inputSchema: statusInputSchema, outputSchema: z .object({ @@ -238,7 +238,7 @@ export const getAuditIssuesTool = { config: { title: "Get site audit issues", description: - "Read the prioritized issue report from a completed site audit. Every issue carries a how_to_fix with concrete remediation steps an agent can act on. Free — reads OpenSEO state. Omit auditId for the most recent audit.", + "Read the prioritized issue report from a completed site audit. Every issue carries a how_to_fix with concrete remediation steps an agent can act on. Free — reads Sightline state. Omit auditId for the most recent audit.", inputSchema: issuesInputSchema, outputSchema: z .object({ @@ -364,7 +364,7 @@ export const getAuditPagesTool = { config: { title: "Get site audit pages", description: - "List crawled pages from a site audit with per-page SEO data (status, title, description, word count, indexability, crawl depth, link counts). Free — reads OpenSEO state. Omit auditId for the most recent audit.", + "List crawled pages from a site audit with per-page SEO data (status, title, description, word count, indexability, crawl depth, link counts). Free — reads Sightline state. Omit auditId for the most recent audit.", inputSchema: pagesInputSchema, outputSchema: z .object({ diff --git a/src/server/mcp/tools/tool-text-output.test.ts b/src/server/mcp/tools/tool-text-output.test.ts index 06890f568..19459b17d 100644 --- a/src/server/mcp/tools/tool-text-output.test.ts +++ b/src/server/mcp/tools/tool-text-output.test.ts @@ -19,7 +19,7 @@ import { makeGa4ReportResult } from "@/server/features/ga4/services/ga4-test-fix import type * as backlinksTargetModule from "@/server/lib/dataforseoBacklinksTarget"; // Verifies that each tool renders its actual row data into the text content -// block (not just a count), across the tools whose data comes from OpenSEO +// block (not just a count), across the tools whose data comes from Sightline // services rather than the DataForSEO client. Guards against a column wired to // the wrong field, which would render a table of only "—". diff --git a/src/server/mcp/tools/whoami.ts b/src/server/mcp/tools/whoami.ts index 472c9f023..34c55e912 100644 --- a/src/server/mcp/tools/whoami.ts +++ b/src/server/mcp/tools/whoami.ts @@ -23,7 +23,7 @@ export const whoamiTool = { config: { title: "Who am I", description: - "Confirms the connected OpenSEO account, server mode, token scopes, and current credit balance when the user asks to check their account or connection. Uses no credits — does not call DataForSEO.", + "Confirms the connected Sightline account, server mode, token scopes, and current credit balance when the user asks to check their account or connection. Uses no credits — does not call DataForSEO.", inputSchema: {} as Record, outputSchema: { userEmail: z.string(), diff --git a/src/server/mcp/transport.test.ts b/src/server/mcp/transport.test.ts index 2dcc6eac9..8509678b1 100644 --- a/src/server/mcp/transport.test.ts +++ b/src/server/mcp/transport.test.ts @@ -51,8 +51,8 @@ vi.mock("@/server/mcp/server", () => ({ createOpenSeoMcpServer: (props?: unknown) => { selfHostedAuthMocks.createOpenSeoMcpServer(props); return new McpServer({ - name: "OpenSEO MCP", - title: "OpenSEO", + name: "Sightline MCP", + title: "Sightline", version: "0.0.11", description: "SEO research tools for AI agents", websiteUrl: "https://openseo.so", diff --git a/src/server/mcp/transport.ts b/src/server/mcp/transport.ts index b59f61f01..68d1c8b26 100644 --- a/src/server/mcp/transport.ts +++ b/src/server/mcp/transport.ts @@ -98,7 +98,7 @@ async function handleLegacyJsonRequest(request: Request, props: McpProps) { // buffers the response and lets the finally below tear everything down // before the request completes. JSON mode silently drops server-to-client // requests (sampling/elicitation) and would hang the buffered response — - // no OpenSEO tool issues them. + // no Sightline tool issues them. const server = createOpenSeoMcpServer(props); const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, diff --git a/src/server/referrals/dub-sale.test.ts b/src/server/referrals/dub-sale.test.ts deleted file mode 100644 index f1275664c..000000000 --- a/src/server/referrals/dub-sale.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { AUTUMN_SEO_DATA_TOP_UP_PLAN_ID } from "@/shared/billing"; -import { type AutumnInvoice, buildDubSaleRequest } from "./dub-sale"; - -function invoice(overrides: Partial = {}): AutumnInvoice { - return { - planIds: ["base-plan"], - stripeId: "in_123", - processorType: "stripe", - status: "paid", - total: 10, - currency: "USD", - createdAt: 1756000000000, - ...overrides, - }; -} - -describe("buildDubSaleRequest", () => { - it("converts Autumn's dollar total to cents and keys on the Stripe invoice id", () => { - expect(buildDubSaleRequest(invoice(), "org_1")).toEqual({ - invoiceId: "in_123", - amount: 1000, - currency: "usd", - eventName: "Invoice paid", - paymentProcessor: "stripe", - metadata: { type: "subscription" }, - }); - }); - - it("labels top-up invoices via their plan id", () => { - const sale = buildDubSaleRequest( - invoice({ planIds: [AUTUMN_SEO_DATA_TOP_UP_PLAN_ID] }), - "org_1", - ); - expect(sale?.metadata).toEqual({ type: "top_up" }); - }); - - it("rounds fractional dollar totals to whole cents", () => { - expect( - buildDubSaleRequest(invoice({ total: 19.99 }), "org_1")?.amount, - ).toBe(1999); - }); - - it("skips unpaid invoices", () => { - expect( - buildDubSaleRequest(invoice({ status: "open" }), "org_1"), - ).toBeNull(); - }); - - it("skips zero-total invoices", () => { - expect(buildDubSaleRequest(invoice({ total: 0 }), "org_1")).toBeNull(); - }); - - it("skips non-USD invoices", () => { - vi.spyOn(console, "warn").mockImplementation(() => {}); - expect( - buildDubSaleRequest(invoice({ currency: "eur" }), "org_1"), - ).toBeNull(); - }); - - it("skips implausibly large totals that would signal a unit mismatch", () => { - vi.spyOn(console, "error").mockImplementation(() => {}); - expect(buildDubSaleRequest(invoice({ total: 1900 }), "org_1")).toBeNull(); - }); - - it("falls back to a stable synthetic key when stripeId is missing", () => { - const sale = buildDubSaleRequest( - invoice({ stripeId: "", processorType: "revenuecat" }), - "org_1", - ); - expect(sale?.invoiceId).toBe("org_1:1756000000000:10"); - expect(sale?.paymentProcessor).toBe("revenuecat"); - }); -}); diff --git a/src/server/referrals/dub-sale.ts b/src/server/referrals/dub-sale.ts deleted file mode 100644 index d5903c7e3..000000000 --- a/src/server/referrals/dub-sale.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { AUTUMN_SEO_DATA_TOP_UP_PLAN_ID } from "@/shared/billing"; - -// Leaf module (no cloudflare:workers import) so the mapping is unit-testable. - -export type AutumnInvoice = { - planIds: Array; - stripeId: string; - processorType: string; - status: string; - total: number; - currency: string; - createdAt: number; -}; - -// The largest legitimate invoice today is the $10/mo plan plus a $99 top-up; -// anything near this cap means the dollars-vs-cents assumption below broke. -const MAX_PLAUSIBLE_INVOICE_TOTAL_USD = 1000; - -/** Maps a paid Autumn invoice to a Dub /track/sale body, or null when the - * invoice shouldn't be tracked. Autumn's `total` is in major units (dollars); - * Dub wants minor units. Prices are USD-only, so skip anything else rather - * than carrying a per-currency minor-unit table. - * - * Refunds are not clawed back automatically: Autumn keeps a refunded - * invoice's status as "paid" and this response doesn't expose refund - * amounts, so refunded referral commissions must be adjusted manually in - * the Dub dashboard. */ -export function buildDubSaleRequest( - invoice: AutumnInvoice, - organizationId: string, -) { - if (invoice.status !== "paid" || invoice.total <= 0) return null; - if (invoice.currency.toLowerCase() !== "usd") { - console.warn("Skipping non-USD invoice for Dub sale tracking", { - organizationId, - currency: invoice.currency, - }); - return null; - } - if (invoice.total > MAX_PLAUSIBLE_INVOICE_TOTAL_USD) { - console.error("Skipping implausibly large invoice for Dub sale tracking", { - organizationId, - total: invoice.total, - }); - return null; - } - - return { - invoiceId: - invoice.stripeId || - `${organizationId}:${invoice.createdAt}:${invoice.total}`, - amount: Math.round(invoice.total * 100), - currency: "usd", - eventName: "Invoice paid", - paymentProcessor: - invoice.processorType === "stripe" || - invoice.processorType === "revenuecat" - ? invoice.processorType - : "custom", - metadata: { - type: invoice.planIds.includes(AUTUMN_SEO_DATA_TOP_UP_PLAN_ID) - ? "top_up" - : "subscription", - }, - }; -} diff --git a/src/server/referrals/dub.ts b/src/server/referrals/dub.ts deleted file mode 100644 index 8d9c7778a..000000000 --- a/src/server/referrals/dub.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { env, waitUntil } from "cloudflare:workers"; -import { parseCookies } from "better-auth/cookies"; -import { AuthRepository } from "@/server/auth/repositories/AuthRepository"; -import { autumn } from "@/server/billing/autumn"; -import { captureServerError } from "@/server/lib/posthog"; -import { buildDubSaleRequest } from "./dub-sale"; - -// Dub referral attribution (hosted only). Flow: -// 1. links.openseo.so/ redirects to openseo.so/?dub_id=; -// the marketing site persists it as a `dub_id` cookie on `.openseo.so`. -// 2. On signup we send a Dub lead and pin `referred-user:` in KV. -// The lead creates a pseudonymous Dub customer record (random name + -// our user id) — GDPR erasure deletes the KV pins here and the Dub-side -// record via the erasure runbook. -// 3. The pin is copied to `referred-org:` (= the Autumn customer id) -// on every session, so billing events need no member lookup. -// 4. Paid Autumn invoices for referred orgs are sent as Dub sales — from the -// billing webhook for promptness, and from a daily cron sweep because -// Autumn's `billing.updated` isn't documented to fire for renewals or -// one-time top-up purchases. -// Every entry point no-ops without DUB_API_KEY, so self-host and previews -// are inert. - -const DUB_API_URL = "https://api.dub.co"; -const DUB_COOKIE_NAME = "dub_id"; -export const DUB_REFERRED_USER_KV_PREFIX = "dub:referred-user:"; -export const DUB_REFERRED_ORG_KV_PREFIX = "dub:referred-org:"; -const TRACKED_SALE_KV_PREFIX = "dub:sale:"; -// User pin: matches Dub's 90-day attribution cookie. Org pin: refreshed on -// every session while the user pin lives, then ages out so the daily sweep -// doesn't grow forever. -const USER_PIN_TTL_SECONDS = 90 * 24 * 60 * 60; -const ORG_PIN_TTL_SECONDS = 400 * 24 * 60 * 60; -// Dub can answer "customer: null" for a referred org whose lead hasn't -// materialized yet; a short suppression window lets the next webhook or the -// daily cron retry. Dub's invoiceId idempotency makes re-sends safe. -const NOT_REFERRED_RETRY_TTL_SECONDS = 60 * 60; -// Only sweep recent invoices: everything older has either been tracked (and -// permanently marked) or has failed long enough that retrying is pointless. -// Keeps per-org KV reads and Dub calls bounded. -const SALE_SWEEP_WINDOW_MS = 45 * 24 * 60 * 60 * 1000; - -function getDubApiKey() { - const value: unknown = Reflect.get(env, "DUB_API_KEY"); - const trimmed = typeof value === "string" ? value.trim() : ""; - return trimmed || null; -} - -async function postDub(apiKey: string, path: string, body: unknown) { - return fetch(`${DUB_API_URL}${path}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify(body), - signal: AbortSignal.timeout(10_000), - }); -} - -/** Records a Dub lead and pins the referral in KV when the signup request - * carries a `dub_id` cookie. Must never fail the signup. */ -export async function captureDubReferralSignup( - userId: string, - request: Request | null | undefined, -) { - const apiKey = getDubApiKey(); - const cookieHeader = request?.headers.get("cookie"); - if (!apiKey || !cookieHeader) return; - - try { - const clickId = parseCookies(cookieHeader).get(DUB_COOKIE_NAME)?.trim(); - if (!clickId) return; - - await env.KV.put(`${DUB_REFERRED_USER_KV_PREFIX}${userId}`, clickId, { - expirationTtl: USER_PIN_TTL_SECONDS, - }); - - waitUntil( - (async () => { - // One retry: leads dedupe Dub-side on customerExternalId+eventName, - // and a lost lead leaves the org's sales unattributable. mode "wait" - // materializes the customer before a near-immediate checkout's sale. - let lastError: unknown; - for (let attempt = 0; attempt < 2; attempt += 1) { - try { - const response = await postDub(apiKey, "/track/lead", { - clickId, - eventName: "Sign up", - mode: "wait", - // Deliberately no name/email: keeps the Dub record pseudonymous. - customerExternalId: userId, - }); - if (response.ok) return; - lastError = new Error( - `Dub lead tracking failed (${response.status})`, - ); - } catch (error) { - lastError = error; - } - } - throw lastError; - })().catch(async (error: unknown) => { - console.error("Dub lead tracking failed", { userId, error }); - await captureServerError(error, { - source: "dub_lead", - user_id: userId, - }); - }), - ); - } catch (error) { - console.error("Dub referral capture failed", { userId, error }); - } -} - -/** Copies a user's referral pin onto the organization they FOUNDED, keyed by - * org id (= Autumn customer id) so billing events resolve it directly. - * Founded, not merely owned or joined: commission is only for net-new users - * creating their own workspace — an invitee (even one later promoted to - * owner) must never attribute an existing org's revenue to their referrer. - * Called on every session (only touches the DB when a user pin exists); - * must never fail session creation. */ -export async function markDubReferredOrganization(userId: string) { - if (!getDubApiKey()) return; - - try { - const clickId = await env.KV.get(`${DUB_REFERRED_USER_KV_PREFIX}${userId}`); - if (!clickId) return; - - const organizationId = - await AuthRepository.findFirstFoundedOrganizationIdForUser(userId); - if (!organizationId) return; - - await env.KV.put(`${DUB_REFERRED_ORG_KV_PREFIX}${organizationId}`, userId, { - expirationTtl: ORG_PIN_TTL_SECONDS, - }); - } catch (error) { - console.error("Dub referred-org marking failed", { userId, error }); - } -} - -async function trackDubSale( - apiKey: string, - userId: string, - sale: NonNullable>, -): Promise<"tracked" | "not_referred" | "failed"> { - const response = await postDub(apiKey, "/track/sale", { - ...sale, - customerExternalId: userId, - }); - - if (!response.ok) { - const detail = await response.text().catch(() => ""); - console.error("Dub sale tracking failed", { - status: response.status, - invoiceId: sale.invoiceId, - detail, - }); - return "failed"; - } - - // An unknown customer (the lead never landed, or hasn't materialized yet) - // returns 200 with `customer: null` and records nothing. - const payload = await response.json<{ customer: unknown }>(); - return payload.customer ? "tracked" : "not_referred"; -} - -function isAutumnNotFound(error: unknown) { - return ( - typeof error === "object" && - error !== null && - "statusCode" in error && - (error as { statusCode: unknown }).statusCode === 404 - ); -} - -async function sweepDubSalesForOrganization( - organizationId: string, - userId: string, -) { - const apiKey = getDubApiKey(); - if (!apiKey) return; - - let customer; - try { - customer = await autumn.customers.get({ - customerId: organizationId, - expand: ["invoices"], - }); - } catch (error) { - // Autumn customers are created lazily on first billing interaction, so a - // referred org that never used a paid feature (or was GDPR-erased) - // doesn't exist there. Not an error. - if (isAutumnNotFound(error)) return; - throw error; - } - - const oldestSweepable = Date.now() - SALE_SWEEP_WINDOW_MS; - for (const invoice of customer.invoices ?? []) { - if (invoice.createdAt < oldestSweepable) continue; - - const sale = buildDubSaleRequest(invoice, organizationId); - if (!sale) continue; - - // One bad invoice must not abort the org's remaining invoices. - try { - const trackedKey = `${TRACKED_SALE_KV_PREFIX}${sale.invoiceId}`; - if (await env.KV.get(trackedKey)) continue; - - const result = await trackDubSale(apiKey, userId, sale); - // "failed" is left unmarked so the next webhook or cron sweep retries. - if (result === "tracked") { - await env.KV.put(trackedKey, "1"); - } else if (result === "not_referred") { - await env.KV.put(trackedKey, "1", { - expirationTtl: NOT_REFERRED_RETRY_TTL_SECONDS, - }); - } - } catch (error) { - console.error("Dub sale tracking errored", { - organizationId, - invoiceId: sale.invoiceId, - error, - }); - } - } -} - -/** Billing-webhook entry point: fire-and-forget sale sweep for referred orgs. - * Must never fail the webhook response. */ -export async function trackDubSalesForOrganization(organizationId: string) { - try { - if (!getDubApiKey()) return; - - const userId = await env.KV.get( - `${DUB_REFERRED_ORG_KV_PREFIX}${organizationId}`, - ); - if (!userId) return; - - waitUntil( - sweepDubSalesForOrganization(organizationId, userId).catch( - async (error: unknown) => { - console.error("Dub sale sweep failed", { organizationId, error }); - await captureServerError(error, { - source: "dub_sale_sweep", - organization_id: organizationId, - }); - }, - ), - ); - } catch (error) { - console.error("Dub sale tracking skipped", { organizationId, error }); - } -} - -/** Daily cron: sweep every referred org. Catches revenue the webhook path - * misses (renewals, one-time top-ups, invoices that were still open). */ -export async function sweepDubReferredOrganizations() { - if (!getDubApiKey()) return; - - let cursor: string | undefined; - do { - const page = await env.KV.list({ - prefix: DUB_REFERRED_ORG_KV_PREFIX, - cursor, - }); - - for (const key of page.keys) { - const organizationId = key.name.slice(DUB_REFERRED_ORG_KV_PREFIX.length); - try { - const userId = await env.KV.get(key.name); - if (userId) { - await sweepDubSalesForOrganization(organizationId, userId); - } - } catch (error) { - console.error("Dub cron sweep failed for org", { - organizationId, - error, - }); - await captureServerError(error, { - source: "dub_cron_sweep", - organization_id: organizationId, - }); - } - } - - cursor = page.list_complete ? undefined : page.cursor; - } while (cursor); -} diff --git a/src/server/workflows/site-audit-workflow-helpers.ts b/src/server/workflows/site-audit-workflow-helpers.ts index 7d207aea1..6c47fbcf1 100644 --- a/src/server/workflows/site-audit-workflow-helpers.ts +++ b/src/server/workflows/site-audit-workflow-helpers.ts @@ -4,7 +4,7 @@ import { sha256Hex } from "@/server/lib/audit/ids"; import { normalizeUrl } from "@/server/lib/audit/url-utils"; import type { CrawlThrottle } from "@/server/lib/audit/crawl-throttle"; -const CRAWL_USER_AGENT = "OpenSEO-Audit/1.0"; +const CRAWL_USER_AGENT = "Sightline-Audit/1.0"; const MAX_HTML_BYTES = 1024 * 1024; /** diff --git a/src/serverFunctions/organization.ts b/src/serverFunctions/organization.ts index e4771762c..9480f0a9f 100644 --- a/src/serverFunctions/organization.ts +++ b/src/serverFunctions/organization.ts @@ -1,12 +1,11 @@ import { createServerFn } from "@tanstack/react-start"; import { getRequest } from "@tanstack/react-start/server"; import { z } from "zod"; -import { getAuth, getHostedBaseUrl } from "@/lib/auth"; +import { getAuth } from "@/lib/auth"; import { hasOrgPermission } from "@/lib/org-permissions"; import { consumeInvitationSendBudget } from "@/server/auth/invitation-send-limit"; import { requireOrgPermission } from "@/server/auth/org-gate"; import { AuthRepository } from "@/server/auth/repositories/AuthRepository"; -import { sendHostedInvitationEmail } from "@/server/email/loops"; import { AppError } from "@/server/lib/errors"; import { requireAuthenticatedContext } from "@/serverFunctions/middleware"; @@ -100,11 +99,8 @@ const sendInvitationSchema = z.object({ email: z.string().email() }); // Invite (or re-invite) a teammate. better-auth creates the pending // invitation — its server-side checks (inviter must be a member with invite -// permission, admin-only role lock, 20-pending cap) all still run — but the -// email is sent here rather than via the plugin's sendInvitationEmail -// callback: better-auth swallows throws from that callback, so a failed send -// would still read as "sent". Here it fails the call. resend: true re-mails -// an existing pending invitation with the same link and a refreshed expiry. +// permission, admin-only role lock, 20-pending cap) all still run. resend: +// true refreshes an existing pending invitation's link and expiry. export const sendTeamInvitation = createServerFn({ method: "POST" }) .middleware(requireAuthenticatedContext) .validator(sendInvitationSchema) @@ -128,29 +124,9 @@ export const sendTeamInvitation = createServerFn({ method: "POST" }) }, }); - const [inviter, memberships] = await Promise.all([ - AuthRepository.getHostedUser(context.userId), - AuthRepository.listMembershipsForUser(context.userId), - ]); - const organizationName = - memberships.find( - (membership) => membership.organizationId === context.organizationId, - )?.organizationName ?? "Organization"; - - try { - await sendHostedInvitationEmail({ - email: data.email, - inviteUrl: `${getHostedBaseUrl()}/accept-invitation/${invitation.id}`, - organizationName, - inviterName: inviter?.name?.trim() || context.userEmail, - inviterEmail: context.userEmail, - }); - } catch (error) { - // The invitation row exists and stays pending — surface the send - // failure so the inviter retries instead of assuming it landed. - console.error("Invitation email send failed:", error); - throw new AppError("UPSTREAM_UNAVAILABLE"); - } - + // This fork has no hosted transactional email provider wired up, so the + // invitation row is created but nobody is emailed — the invitation link + // has to be shared with the invitee out of band until hosted mode gets a + // mail provider. return { invitationId: invitation.id }; }); diff --git a/src/serverFunctions/reportTemplates.ts b/src/serverFunctions/reportTemplates.ts index c6a6afea7..cd0d869d6 100644 --- a/src/serverFunctions/reportTemplates.ts +++ b/src/serverFunctions/reportTemplates.ts @@ -43,7 +43,7 @@ export const saveReportTemplate = createServerFn({ method: "POST" }) description: data.description, instructions: data.instructions, // The client label for a template made in the app, not through MCP. - createdBy: "OpenSEO app", + createdBy: "Sightline app", createdByUserId: context.userId, }); return { ok: true as const, templateId, created }; diff --git a/src/serverFunctions/reports.ts b/src/serverFunctions/reports.ts index 72d047497..2b37b1cd4 100644 --- a/src/serverFunctions/reports.ts +++ b/src/serverFunctions/reports.ts @@ -98,8 +98,8 @@ export const getReport = createServerFn({ method: "POST" }) /** * Mints (or returns) the report's public link. The token alone comes back — - * the app builds `/s/` client-side, so the same server works on - * app.openseo.so and a self-hosted hostname without knowing either. + * the app builds `/s/` client-side, so the same server works + * on any hosted or self-hosted hostname without knowing which one it is. */ export const shareReport = createServerFn({ method: "POST" }) .middleware(requireProjectContext) diff --git a/src/serverFunctions/samAccess.ts b/src/serverFunctions/samAccess.ts index c7ca53b52..49868d0ef 100644 --- a/src/serverFunctions/samAccess.ts +++ b/src/serverFunctions/samAccess.ts @@ -7,7 +7,7 @@ import { import { requireProjectContext } from "@/serverFunctions/middleware"; const OPENROUTER_KEY_MISSING_MESSAGE = - "OPENROUTER_API_KEY is not set for this deployment yet. Add it to your environment, restart OpenSEO, then confirm here."; + "OPENROUTER_API_KEY is not set for this deployment yet. Add it to your environment, restart Sightline, then confirm here."; const projectScopedSchema = z.object({ projectId: z.string().min(1) }); diff --git a/src/shared/audit-issues.ts b/src/shared/audit-issues.ts index bce95fe24..b8bb62b10 100644 --- a/src/shared/audit-issues.ts +++ b/src/shared/audit-issues.ts @@ -22,7 +22,7 @@ export const AUDIT_ISSUE_TYPES = { explanation: "The site returned a bot challenge or access denial (e.g. a Cloudflare challenge or a 403) instead of the page. We report this honestly rather than pretending the page is broken — but it means this page could not be audited, and other crawlers like search engines may face similar friction.", howToFix: - 'If you own this site, allowlist the "OpenSEO-Audit" user agent in your WAF/bot-protection settings (on Cloudflare: a WAF custom rule that skips bot protection when the user agent contains "OpenSEO-Audit"; on some free tiers you may need to relax bot protection). Then re-run the audit.', + 'If you own this site, allowlist the "Sightline-Audit" user agent in your WAF/bot-protection settings (on Cloudflare: a WAF custom rule that skips bot protection when the user agent contains "Sightline-Audit"; on some free tiers you may need to relax bot protection). Then re-run the audit.', }, "rate-limited-page": { severity: "warning", @@ -30,7 +30,7 @@ export const AUDIT_ISSUE_TYPES = { explanation: "The server answered 429 Too Many Requests, so this page could not be audited. The crawler waits before retrying when the site's cooldown fits within the audit time limit.", howToFix: - 'Raise the rate limit for crawlers, or allowlist the "OpenSEO-Audit" user agent in your rate-limiting rules (on Cloudflare: a rate-limiting rule exception matching that user agent). Then re-run the audit. Re-running with fewer pages also helps if the limit is strict.', + 'Raise the rate limit for crawlers, or allowlist the "Sightline-Audit" user agent in your rate-limiting rules (on Cloudflare: a rate-limiting rule exception matching that user agent). Then re-run the audit. Re-running with fewer pages also helps if the limit is strict.', }, "crawl-rate-limited": { severity: "warning", @@ -38,7 +38,7 @@ export const AUDIT_ISSUE_TYPES = { explanation: "The site asked the crawler to wait longer than the audit time limit allowed. We stopped requesting pages. This report is incomplete; URLs we did not fetch are not recorded as broken or rate limited.", howToFix: - "Re-run the audit after the site's rate limit resets, or ask the site owner to allow the OpenSEO-Audit crawler.", + "Re-run the audit after the site's rate limit resets, or ask the site owner to allow the Sightline-Audit crawler.", }, "server-error": { severity: "critical", diff --git a/web/.env.example b/web/.env.example deleted file mode 100644 index 91373dc89..000000000 --- a/web/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -# Build-time vars (vite build inlines these). -VITE_TURNSTILE_SITE_KEY= - -# Runtime secrets — set with `wrangler secret put `, not here: -# DATAFORSEO_API_KEY base64("login:password") for api.dataforseo.com -# TURNSTILE_SECRET_KEY pairs with VITE_TURNSTILE_SITE_KEY; deploy the build -# containing the site key before setting this secret -# LOOPS_API_KEY newsletter subscribe endpoint diff --git a/web/.gitignore b/web/.gitignore deleted file mode 100644 index 734f35595..000000000 --- a/web/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -dist -.wrangler -.vinxi -.output -.vercel -.source -source.generated.ts -*.local -.dev.vars* diff --git a/web/.prettierignore b/web/.prettierignore deleted file mode 100644 index 560ae8c61..000000000 --- a/web/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -dist -.wrangler -.vinxi -.output -.source -source.generated.ts -pnpm-lock.yaml -routeTree.gen.ts -content/legal/terms-and-conditions.md diff --git a/web/.worktreeinclude b/web/.worktreeinclude deleted file mode 100644 index a1e2695dd..000000000 --- a/web/.worktreeinclude +++ /dev/null @@ -1 +0,0 @@ -web/open-seo diff --git a/web/content/blogs/best-open-source-seo-tools.md b/web/content/blogs/best-open-source-seo-tools.md deleted file mode 100644 index 0ed0653d9..000000000 --- a/web/content/blogs/best-open-source-seo-tools.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: "The Best Open Source SEO Tools in 2026" -description: "Open source SEO tools in 2026: OpenSEO, SerpBear, SEONaut, LibreCrawl, and SEOMachine — what each one does, what it costs to run, and how to self-host." -author: "OpenSEO Team" -date: "2026-06-05" ---- - -There are a lot of open source SEO projects on GitHub, but only a handful are mature enough to rely on. This guide covers those, plus a few honorable mentions worth watching or testing. - -In the past, open source SEO tools struggled because they lacked quality data. Beyond auditing, most SEO tasks have a cost, so most of the projects in this guide aren't totally free. For example, rank tracking requires SERP results from around the world. Coming up with a content strategy means you need accurate search volumes and backlink indexes. Most of the tools in this list rely on paid third-party services, but they still cost far less than the equivalent legacy SaaS tools. - -Note: OpenSEO publishes this guide, and OpenSEO is one of the tools listed, as the project has 2,000+ stars on GitHub. We have tried to make this useful even if you never touch it. We love open source and think it's the future of SEO tools: [openseo.so/open-source-seo](/open-source-seo). - -## The main tools at a glance - -| Tool | Stars | What it does | Cost to run | Self-hosting | -| ---------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------- | -------------------------------------------------- | ---------------------------------- | -| [OpenSEO](https://github.com/every-app/open-seo) | 2.1k | All-in-one: keyword research, rank tracking, backlinks, site audits, AI visibility | DataForSEO usage (your own API key, pay-as-you-go) | Docker or Cloudflare Workers | -| [SerpBear](https://github.com/towfiqi/serpbear) | 2.0k | Rank tracking | SERP API usage (provider of your choice) | Docker | -| [SEONaut](https://github.com/StJudeWasHere/seonaut) | 717 | Technical SEO and site audits | Free | Docker | -| [LibreCrawl](https://github.com/PhialsBasement/LibreCrawl) | 681 | Site crawling and SEO audits | Free | Desktop or web | -| [SEOMachine](https://github.com/TheCraigHewitt/seomachine) | 7.1k | SEO content writing inside Claude Code | Anthropic API usage | Clone the repo, run in Claude Code | - -_Star counts are updated monthly. Last updated June 5, 2026._ - -## OpenSEO - -OpenSEO is an open source alternative to Semrush and Ahrefs, built to be the only SEO tool that companies or small agencies need. It covers keyword research, rank tracking, backlinks, site audits, AI brand visibility, and an AI search prompt explorer. See them all on the [features page](/features). - -It relies on [DataForSEO](https://dataforseo.com), a paid service, which is the gold standard for SEO data with pay-as-you-go pricing. Many features cost money to run, but far less than a legacy SaaS seat, and the [costs are documented](/pricing). - -OpenSEO also has an [MCP server](/docs/mcp) and AI skills, so you can use Claude Code, Codex, or OpenClaw to handle tedious SEO work like a first pass of keyword research, then dig into the results in the OpenSEO UI. That is different from pointing an agent at DataForSEO's MCP directly. You can save data to your OpenSEO account through the MCP, like tags for clustering keywords, and you can ask the agent for a link to view its research in OpenSEO instead of trusting whatever it reports back. - -## SerpBear - -[SerpBear](https://docs.serpbear.com)'s main focus is rank tracking. You can track unlimited keywords for unlimited domains. - -The app is free; you pay by usage for the SERP requests you make to your chosen provider. SerpBear supports eight providers with different cost and functionality tradeoffs, so you can pick the one that fits. It also has a free Google Search Console integration and a free Keyword Research feature. Both pair well with rank tracking: you can see your real GSC data and find new keywords to target. - -One warning on Keyword Research. It relies on the [Google Ads API](https://docs.serpbear.com/miscellaneous/integrate-google-ads), which has an application and approval process before you can use it. - -## SEONaut & LibreCrawl - -SEONaut and LibreCrawl are both open source Screaming Frog alternatives for technical SEO: crawl a site, audit on-page SEO, and surface issues like broken links. Neither depends on a paid data provider, so they are the cheapest tools here to run, and an easy first install if technical SEO is your immediate need. - -The difference is how you run them. SEONaut is a self-hosted web app (Docker), with a hosted version and the full feature list at [seonaut.org/features](https://seonaut.org/features/). [LibreCrawl](https://librecrawl.com) runs as a free desktop or web crawler that analyzes links and exports SEO data; it is newer but moving fast. Try whichever fits your setup. - -## SEOMachine - -SEOMachine is a Claude Code workspace for creating long-form, SEO-optimized blog content, rather than a data platform. It works with Claude Code and an Anthropic API account, plus optional Google Analytics, Search Console, and DataForSEO integrations. To self-host, clone the repo, install its Python dependencies, add your business context, and open it in Claude Code. - -It gives you commands like /research, /write, and /optimize, along with a set of SEO agents to draft and refine content. If your bottleneck is producing content rather than pulling data, it solves a different problem than the tools above. - -## Honorable mentions - -These have fewer stars, or fill a narrower niche, than the main tools above. We have not put any of them through real work, so treat these as leads rather than recommendations. If you love one of them, email us and we will test it. Sorted by GitHub stars. - -- [openserp](https://github.com/karust/openserp) (745, active): a Go API and CLI that scrapes normalized SERP results from Google, Yandex, Baidu, Bing, DuckDuckGo, and Ecosia, self-hostable via Docker. -- [RustySEO](https://github.com/mascanho/RustySEO) (276, active): a cross-platform desktop SEO/GEO toolkit with a Rust crawling core, plus Google Analytics, Search Console, and PageSpeed integrations. It looked promising, but the Mac build would not run on our machine. -- [Greenflare](https://github.com/beb7/gflare-tk) (195, unmaintained): a lightweight Python technical-SEO crawler for Linux, Mac, and Windows. It is no longer maintained — last release 2021 — and its download site is down. -- [contentswift](https://github.com/hilmanski/contentswift) (159, unmaintained): a self-hostable content research tool that analyzes top-ranking SERP results to guide on-page optimization. The demo video is strong, but the repo has been dormant since 2023 and has no declared license. -- [SEO Panel](https://github.com/seopanel/Seo-Panel) (146, active): an older PHP control panel, around since 2010, for managing SEO across multiple sites — rank tracking, audits, sitemaps, backlink monitoring, and multi-user accounts. -- [elmo](https://github.com/elmohq/elmo) (124, active): an AI-visibility (AEO/GEO) tracker that monitors how ChatGPT, Claude, Gemini, and Perplexity mention a brand and cite its content, self-hostable via Docker Compose. -- [FreeCrawl-SEO-Tool](https://github.com/kemalai/FreeCrawl-SEO-Tool) (46, active): a free desktop SEO crawler aimed at large technical audits — 1M+ URLs, 150+ checks, JS rendering — that runs locally with no telemetry. Very new. -- [seo-tools-api](https://github.com/oguzhan18/seo-tools-api) (46, unmaintained): a NestJS REST API bundling meta-tag analysis, sitemap generation, SEO scoring, and rank and backlink checks. No declared license. -- [google-search-console-export-all](https://github.com/swalker-888/google-search-console-export-all) (8, unmaintained): a single-file Node.js script that bulk-exports all your Search Console data to CSV, bypassing the UI's row limits. No declared license. - -One clarification, since it shows up on other lists: seojuice.com is not open source. They publish open source SDKs for their APIs, but the core product is closed. - -## Try OpenSEO, and tell us what we missed - -If you want one open source tool that covers most of SEO, start with OpenSEO. Self-host it with Docker or Cloudflare Workers, or use the hosted version at [openseo.so](https://openseo.so) if you would rather not run it yourself. - -We will keep this guide current. If there is an open source SEO project you love that we did not cover, email us at ben@openseo.so and we will test it and consider adding it. diff --git a/web/content/blogs/dark-queries.md b/web/content/blogs/dark-queries.md deleted file mode 100644 index 104d9c4cb..000000000 --- a/web/content/blogs/dark-queries.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: "The Dark Query Problem: Why Search Console Hides Most of Your Searches" -description: "Google shows you the clicks but hides the questions behind them. Here's how to triangulate the searches you can't see using data you already have." -author: "Jeremy Rivera" -date: "2026-07-20" ---- - -Open any page report in Google Search Console. It will tell you, confidently, that this page earned 100 clicks last month. Scroll to the query table underneath, and Google will name maybe seven to fourteen of the searches that drove them. The other 86 clicks came from searches Google won't show you, anonymized away under privacy thresholds. - -Those are _dark queries_: real searches, from real people, that turned into real visits, and you have no idea what they were. - -You can't force Google to hand them over. But you can reconstruct most of them, and the data you need is already sitting in three tools you probably already use. If you want an agent to do the heavy lifting, connect [OpenSEO MCP](/docs/mcp) first so it can pull live keyword, ranking, SERP, and Search Console data. New to this? Start with the [SEO coach](/docs/skills/seo-coach). - -## Table of Contents - -- [The 100-clicks, 14-queries problem](#the-100-clicks-14-queries-problem) -- [Why the gap is getting worse, not better](#why-the-gap-is-getting-worse-not-better) -- [The fix: triangulate three data sources](#the-fix-triangulate-three-data-sources) -- [Do it with OpenSEO](#do-it-with-openseo) -- [What to do Monday morning](#what-to-do-monday-morning) - -## The 100-clicks, 14-queries problem - -It's not a rounding error you can wave off. Knowing you got a hundred clicks but only seeing fourteen of them is, as I've described it, "sticking your finger in the air." It's nice to know the number exists. But fourteen out of a hundred is not a usable level of data, not if you're trying to decide what to write next, which page to expand, or what your audience actually wants. - -![Google Search Console performance for one page: 81 clicks, 13.6K impressions, 0.6% CTR, average position 24.6, with a short Top Queries table.](/blog/dark-queries/gsc-queries.png) - -This is the quiet frustration underneath most content planning. I talked about it recently with Sonia Urquilla, who runs [SEO by Sonia](https://seobysonia.com/) and works almost entirely with founders and coaches who don't have a big analytics team. When I brought up the query gap, her reaction was immediate: - -> This is the one complaint that all of us always have. Everywhere I go, search or when I hear podcasts, we always complain about these numbers, the analytics. Please give us something that can help us solve that. - -For her clients, the gap isn't an academic annoyance. It's the difference between a content plan built on evidence and one built on a guess. - -## Why the gap is getting worse, not better - -Here's the part that should worry you: dark queries are the _old_ version of this problem. The new one is that entire channels are going dark. - -When a prospect asks ChatGPT for "an affordable keyword tool for a freelance SEO" and your brand gets recommended, that lead can arrive with no referrer, no query, and no row in any dashboard. Search Console at least admits it's hiding data from you. The AI answer layer doesn't even do that. - -> Sometimes my clients get a lead from ChatGPT, but I'm not able to see that data yet, to see what worked for them. I just want to condense that data. -> -> — Sonia Urquilla, SEO by Sonia - -So we're living with two layers of dark data now: the queries Search Console withholds, and the AI-driven visits that never announce themselves at all. You can't fully close either gap. But you can shrink the first one dramatically, and the discipline that does it is the same one you'll need for the second: stop treating any single tool as the source of truth, and start triangulating. - -## The fix: triangulate three data sources - -No single dataset can rebuild your dark queries. But you already hold three partial views of the same page, and each one knows something the others don't: - -- **Search Console** gives you _positioning_: the ~14 named queries, their average rank, impressions, and CTR. -- **Google Analytics** gives you the _real organic traffic_ and behavior for the landing page: sessions, engagement, conversions. -- **Rank tracking** gives you the _rough rankings_ for the broader keyword cluster the page could plausibly be catching. - -Line those three up against a single URL and the shape of the missing 86% starts to emerge. If a page shows 100 clicks, ranks on page one for a cluster of twenty related terms, and Analytics confirms a wave of engaged organic sessions, you can reasonably _infer_ which unnamed queries are doing the work, even though Google never spelled them out. It isn't certainty. It's triangulation: a defensible suggestion instead of a finger in the air. - -## Do it with OpenSEO - -This is exactly the workflow the [OpenSEO MCP](/docs/mcp) was built for. Because the Google Search Console connection runs _through_ OpenSEO, you don't have to wire up a separate integration. Connect it once and query all three sources from the same agent. - -### 1. Pull the honest positioning picture - -Ask your agent to grab the Search Console performance for one URL and flag the click-to-named-query gap. - -### 2. Widen the keyword cluster - -Use OpenSEO's ranked-keywords and keyword-research tools to find every term the page could plausibly be catching, not just the fourteen Google admitted to. The [keyword research skill](/docs/skills/keyword-research) makes this repeatable. - -### 3. Confirm against real traffic - -Bring in Google Analytics (or your choice tool) to check the inference against actual organic behavior for the landing page. - -### Full Prompt: See Dark Queries - -```text -1. Pull the positioning picture - -Look at [yourwebsite.com/page] in Search Console for the last 90 days. -List total clicks vs. the sum of clicks in the named-query table, and -show me the gap. Then give me the ranked keywords and their average -position for this exact URL. - -2. Widen the keyword cluster - -For that same URL, pull all keywords it ranks for in positions 1-20. -Cluster them by intent, then flag which clusters are NOT represented -in the named Search Console queries. Those are my likely dark-query -candidates. - -3. Confirm against real traffic - -Pull organic sessions and engagement for this landing page over the -same window. Where engaged-session volume is far higher than the named -clicks can explain, tell me which inferred query clusters most likely -account for the difference. Output as a document I can review. - -``` - -The output isn't a magic list of the exact hidden searches, that data genuinely doesn't exist on your side of Google's wall. What you get instead is a ranked, evidence-backed shortlist of what those dark queries almost certainly are, assembled from three sources that individually would have left you guessing. That's the difference between "I got a hundred clicks" and "here are the twelve topics actually pulling people to this page, and here's the one I should build the next article around." - -Expensive suites will happily charge you $100 to $200 a month and still hand you the same fourteen queries. The point of doing this in OpenSEO isn't a prettier dashboard, it's that the Search Console, ranking, and MCP pieces are already connected, cheap, and scriptable, so triangulation becomes a prompt instead of a project. - -## What to do Monday morning - -Pick your single most important page, the one you'd most hate to be guessing about. Run the three steps above against it. You'll walk away with a concrete list of topics to expand and, more usefully, a repeatable habit: never trust one tool's version of what your audience searched for. - -As Sonia's clients keep proving, the leads increasingly arrive from places nobody used to look. The SEOs who win the next few years won't be the ones with the most data. They'll be the ones who got good at reconstructing the data everyone else accepts as lost. - -If you want to explore this yourself, [OpenSEO](https://openseo.so/) is an affordable, open-source SEO tool that connects Search Console, ranking, and MCP data in one place, so you can start closing the dark-query gap today. diff --git a/web/content/blogs/how-to-prompt-claude-code-for-seo.md b/web/content/blogs/how-to-prompt-claude-code-for-seo.md deleted file mode 100644 index 8484bd29d..000000000 --- a/web/content/blogs/how-to-prompt-claude-code-for-seo.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: "How to Prompt Claude Code for SEO" -description: "Treat Claude Code like an SEO expert with infinite patience: ask every question, give it every idea, and control it so it serves your marketing strategy." -author: "Ben Senescu" -date: "2026-08-19" ---- - -One of the most frequently asked questions I get is how to take advantage of Claude Code for SEO. - -Most people asking are nervous about doing it right. But Claude Code is so good now that you should treat it like you have an SEO expert with infinite patience. Ask it every question. Give it every idea you have. - -Your job is to control that expert, and make sure what it does serves your marketing strategy. - -SEO is a deep topic. But working on it in tandem with your agent is much simpler than trying to read up on SEO first. Ask your questions, talk through your ideas, and refine from there. - -## A 60-second example - -> **You:** My site gets decent traffic but almost no signups. What should I look at? -> -> **Claude:** Six things worth checking: search intent mismatch, page titles, content gaps, internal links, page speed, backlinks... -> -> **You:** Which one of those actually moves signups for a two-person company, and what's the trade-off? -> -> **Claude:** Intent mismatch. Most of your traffic comes from how-to queries, and those readers were never going to sign up. The catch is that fixing it means writing for smaller, buyer-intent keywords, so your traffic number drops while your signups go up. - -The first answer is a list of ideas. The second is a decision you can act on. - -## How to set up Claude Code for SEO - -Two commands in Claude Code install the OpenSEO MCP server and all nine SEO agent skills: - -``` -/plugin marketplace add every-app/open-seo -/plugin install openseo@openseo -``` - -Codex CLI has [its own plugin](/docs/codex-plugin), and any other MCP client, Cursor included, can [connect directly](/docs/mcp). - -## Why do I need to pay for SEO data? - -Without a data source, an agent will invent search volumes that sound plausible and are wrong. Real numbers make everything below work. - -To acquire this data, someone has to crawl the whole internet and store everything people are searching for on Google. That's expensive, and it's why the big SEO suites run $100 a month and up. - -OpenSEO is $10 a month with $10 of usage credits included, and signing up is free with $0.50 of trial credits. Asking Claude questions costs nothing beyond your normal Claude usage. You only spend credits when the agent pulls real data, and a keyword lookup runs about five cents. Search Console data never costs credits, because it's your own data. - -## Your first SEO prompt - -Don't start by studying what the tools do. Ask: - -> What SEO skills do you have installed, and which should we use for my situation? - -The skills are packaged workflows for things like audits, keyword research, and link prospecting, and Claude will pick the right one and explain why. If it says it doesn't have any, run the two install commands above and ask again. - -## SEO prompts for Claude Code - -The example prompts below read like speech because we dictate ours. A rambling voice memo carries more context than a carefully typed line, and transcription typos don't matter. [Wispr Flow](https://wisprflow.ai) and [superwhisper](https://superwhisper.com) both do this well, and [Handy](https://handy.computer) and [FluidVoice](https://github.com/altic-dev/FluidVoice) are open source. - -**Push back on every recommendation.** Claude will give you lots of ideas. Make it show its work: the reasoning, the trade-off, and the one thing that matters most. - -> You gave me six options. Which one actually makes a difference for the business, and what's the best argument against it? - -**Give it every idea you have.** Half-formed is fine. It's much cheaper to have your expert poke holes in an idea than to build the wrong thing. - -> I have an idea: what if we made a comparison page for every competitor in our space? Poke holes in that before we commit to anything. - -**Describe your positioning in your own words.** Who your product is for, who it isn't for, and what makes someone pick you. Two sentences of that will steer keyword research better than any settings page. - -> We're better for solo founders who live in their code editor. The big suites are built for agencies with a full-time marketing team. Find keywords where OUR buyer is searching. - -**Name the deliverable, not the steps.** Say what done looks like and let Claude figure out how to get there. - -> Audit my site and give me the one thing worth doing this week, not a list of forty issues. - -**Show, don't describe.** A screenshot of a weird-looking page or a confusing report is a complete prompt. And if it's heading the wrong direction mid-task, interrupt it. You won't offend it. - -## How to get clarity from Claude - -You can't really mess this up: - -- **Confused?** Say "explain that like I'm new to SEO." That question is free and it never gets impatient. -- **Skeptical?** Ask "what data would prove this is working?" and have it check your own Search Console numbers. -- **Worried about breaking something?** Asking questions and pulling data changes nothing on your site. When Claude Code does edit something, it shows you the change first, and SEO changes are mostly text you can revert. - -You are not being graded on your prompts. A dumb question costs you a few seconds, and so does asking the same one twice. - -## How to get started with SEO in Claude Code - -Dictate a paragraph about your business: what you sell, who buys it, and what you want from search. Then ask: - -> Given all that, what should we look at first, and why that instead of the alternatives? - -Ten minutes later you'll have a short, prioritized list and the reasoning behind it. Push back on it, then refine from there. - -That works with or without OpenSEO connected. With it, the answers come with real numbers attached. - -## What each SEO skill does - -If you ran the two plugin commands above, you already have all ten skills, and the Codex plugin installs the same set. Anywhere else, [connect MCP](/docs/mcp) first and then [add the skill files](/docs/skills/setup). - -Each skill hands Claude a complete workflow. The ones you'll reach for most: - -- [SEO Audit](/docs/skills/seo-audit) audits your site and returns a one-page, plain-language report built around a single next action. -- [Keyword Research](/docs/skills/keyword-research) finds keywords worth targeting and explains why each one fits your business. -- [Competitive Landscape](/docs/skills/competitive-landscape) maps who is winning across your market and where the openings are. -- [Link Prospecting](/docs/skills/link-prospecting) finds qualified outreach prospects and the angle that makes each one relevant. -- [Local SEO](/docs/skills/local-seo) audits a Google Business Profile, compares it to local competitors, and shows where you drop out of Google Maps around a location. - -You don't have to memorize any of this. Ask Claude which one fits, or read the [skills docs](/docs/skills) for the full list. - -## FAQ - -### Do AI SEO agents actually work? - -For the legwork, yes: keyword research, audits, rank tracking, and competitor analysis are data problems, and an agent with real data handles them well. What doesn't work is autopilot. Google's spam policies name scaled AI content generation directly, and no agent can hand you a strategy. The judgment stays with you; that's the whole setup this guide describes. - -### Will AI replace SEO? - -No, but it's changing where the answers show up. More searches end in an AI-written answer instead of ten blue links, and those answers cite sources. The work of being the source worth citing is still SEO: crawlable pages, content that answers the question, and a reputation other sites vouch for. - -### Is SEO dead? - -Search demand didn't die; it's spreading across Google, AI Overviews, and chat assistants. What died is the version of SEO where you stuff keywords and wait. The questions that decide whether you get found, and now whether you get cited, are the same ones your agent can help you answer. - -### Can I do SEO myself? - -Yes, and that's the point of working this way. The parts that used to require an expensive expert, reading the data, spotting the priority, knowing what to check next, are exactly what the agent is good at. You bring the knowledge of your own business, which no agency has anyway. Once the business takes off, hiring an SEO expert can be worth it, but you don't need one to get started. - -### How much does SEO cost? - -Your time, plus data. The data runs $10 a month with [OpenSEO](/pricing), $100 and up with the big suites, or a $50 minimum deposit with a raw data provider if you [self-host](/open-source-seo). Content costs whatever your time is worth. You don't need an agency to start. - -### How long does SEO take? - -Months, not days. Google has to recrawl your pages, re-rank them, and build trust, so expect the first movement in weeks and real results in three to six months. That's why the method here leans on weekly rank tracking and your own Search Console data: you want evidence it's working long before the traffic shows up. - -### Is SEO worth it for a small business? - -It's worth it if your customers search for what you sell. Spend ten minutes checking before you invest: look up the keywords you'd want to rank for, and see whether there's volume and who currently holds the results. If nobody searches for your category, put the effort into another channel and revisit later. diff --git a/web/content/blogs/how-to-use-vellum-and-openseo-for-seo.md b/web/content/blogs/how-to-use-vellum-and-openseo-for-seo.md deleted file mode 100644 index 48cb62bd7..000000000 --- a/web/content/blogs/how-to-use-vellum-and-openseo-for-seo.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: "How to Use Vellum and OpenSEO for Your SEO Engine" -description: "Use Vellum and OpenSEO for keyword research, competitor analysis, live search results, and rank tracking in your content workflow." -author: "OpenSEO Team" -date: "2026-09-10" ---- - -Most teams know they need search traffic, but the traditional SEO workflow is broken. - -You open an expensive dashboard, spend an hour filtering columns, export a spreadsheet, clean up the data, and copy five numbers into a doc before writing a single word. By the time you sit down to outline, your mental bandwidth was burned on data triage instead of creating content that wins. - -Connecting OpenSEO to a personal AI assistant like [Vellum](http://vellum.ai) inverts that dynamic completely. - -Instead of treating search data like an external destination you have to visit, your assistant accesses live keyword metrics, competitor gaps, and SERP rankings directly inside your workspace. You bring the business positioning and editorial direction; your assistant pulls the live search ground truth and handles the execution. - -Here is how to set up and run an agentic SEO and Generative Engine Optimization (GEO) engine without the dashboard tax. - ---- - -## Why Use OpenSEO & Vellum Assistant? - -Pairing live search data with a personal AI assistant solves the two biggest bottlenecks in modern SEO: context loss and manual data triage. - -1. **Your business context:** A standalone LLM or chat window treats every prompt as a blank slate. Vellum maintains persistent memory across conversations, so it already understands your product, your target audience, and your existing content pillars. When OpenSEO feeds live keyword and ranking data into that context, the assistant evaluates opportunities based on what your business actually does, not generic search theory. -2. **No more manual data sorting:** Traditional SEO requires toggling between browser tabs, exporting CSVs, filtering spreadsheets, and copying figures into docs. With OpenSEO connected directly to Vellum via MCP, search intelligence lives inside your writing environment. You spot a competitor gap, analyze the SERP, and outline a counter-piece in one continuous thread. -3. **Turn data into action:** Dashboards only tell you what happened in the past. An assistant with live SEO tools turns data into immediate output: checking the live top 10 results, dissecting where incumbent pages are shallow, generating an outline that fills the gap, and tracking rank movements after you publish. - ---- - -## A 60-Second Example - -> **You:** Competitor X just took the #1 spot for "best ai assistants 2026." What are they winning on that we missed? - -**Vellum Assistant:** Let me pull their organic footprint and live SERP data through OpenSEO. Looking at the top 10 results, they're winning on three comparison keywords you don't rank for: local model execution, offline privacy, and pricing per seat. All three have low keyword difficulty (<25) and high search volume (>800/mo). - -**You:** Outline a new section for our breakdown covering those exact three angles, and cite the current ranking consensus. - -**Vellum Assistant:** Outline drafted. Here is where the incumbent pages are weak and the exact gap we can exploit. - -> - -The first answer pulls live search ground truth. The second is an editorial move executed in seconds. - ---- - -## How to Set Up Vellum + OpenSEO - -Connecting OpenSEO to Vellum takes two minutes: - -1. In Vellum, add the **OpenSEO plugin** from the [plugin catalog](https://www.vellum.ai/plugins/openseo) or [connect via MCP](https://openseo.so/docs/mcp). -2. Follow the OAuth prompt to sign in to your OpenSEO account and approve the connection. -3. Your assistant immediately gets access to all OpenSEO tools: keyword research, competitor analysis, live SERP lookups, backlink data, and rank tracking. - -No custom scripts, no API keys to manually wire up. Once connected, your assistant uses real search data automatically when you plan, draft, and track content. - ---- - -## Why Connect SEO Data to an Assistant? - -Without a live data source, AI models invent search volumes that sound plausible but are completely fabricated. Connecting OpenSEO gives your assistant real DataForSEO numbers directly in chat. - -Traditional SEO platforms like Ahrefs or Semrush charge $130 or more per month for a single seat, largely to pay for proprietary dashboard interfaces. OpenSEO is $10 a month with $10 of usage credits included, and signing up includes $0.50 of trial credits. - -Because an assistant only queries data when you need it, a typical keyword lookup or live SERP check costs about five cents. A lean team pays a fraction of enterprise SaaS pricing while getting search intelligence embedded directly where content is created. - ---- - -## 3 Core Workflows for Agentic Search - -Once OpenSEO is connected, three workflows replace the manual spreadsheet process. - -### 1. Conversational Competitor Gap Mining - -Finding competitor gaps manually takes hours of toggling filters, excluding branded terms, and cross-referencing published articles. - -With an assistant connected to OpenSEO, you do it in one prompt: - -> _"Look up competitor.com's organic footprint via OpenSEO. Filter out their branded terms, and show me the top 5 high-intent keywords with search volume over 500 and difficulty under 35 where we don't currently rank."_ - -The assistant queries OpenSEO, pulls the live ranking data, filters out irrelevant queries, and returns actionable targets with real volume and difficulty scores. - -From there, move straight into drafting: - -> _"Take keyword #2 and generate an outline targeting that exact search intent. What specific questions are competitors failing to answer?"_ - -The cycle between spotting an opportunity and structuring a draft drops from two hours to five minutes. - -### 2. Live SERP and GEO Analysis - -Traditional SEO stops at keyword volume. Generative Engine Optimization (GEO) requires understanding how search engines and AI models (Claude, ChatGPT, Perplexity, Google AI Overviews) synthesize answers on a topic. - -Before drafting any major piece, have your assistant inspect the live ranking landscape: - -> _"Pull the live top 10 Google results for [target query] via OpenSEO. Analyze the top ranking pages: What structure are they using? What specific questions or buyer requirements do they completely ignore? Where is the consensus weak?"_ - -OpenSEO crawls the live SERP and returns the actual pages winning today. Your assistant breaks down the patterns: - -- **If all ten ranking articles are superficial listicles** that omit pricing or benchmarks, your editorial wedge is transparent pricing teardowns and reproducible tests. -- **If incumbent pages miss recent feature updates**, that freshness hook becomes your opening thesis. - -Instead of writing generic summaries and hoping search engines notice, live SERP data identifies the analytical void on page one so you can write the definitive piece that fills it. - -### 3. Background Rank Tracking Without the Dashboard Tax - -Tracking keyword movements usually requires logging into external dashboards to stare at ranking charts. - -With OpenSEO in Vellum, rank tracking happens at the point of publication: - -> _"Add these 6 keywords to our OpenSEO rank tracker. Flag any movement greater than 3 positions."_ - -Rank checks run in the background. If an article climbs to position #3 or drops below the fold, your assistant catches it during routine checks and flags the delta directly in your feed. The feedback loop lives right where the work happens. - ---- - -## Guiding Principles for Prompting Your SEO Assistant - -- **Push back on every recommendation:** Your assistant will generate dozens of ideas. Make it show its reasoning, the trade-offs, and the primary metric that matters. -- **Describe your positioning clearly:** Tell your assistant who your product is for, who it isn't for, and why customers pick you. Two sentences of clear positioning steers keyword research better than complex dashboard filters. -- **Name the deliverable, not the steps:** Ask for what done looks like: _"Give me the top 3 high-volume search queries for this topic that have low competition"_, not a step-by-step tutorial. -- **Keep judgment with the human:** Search data and initial outlines are fast for an agent to pull, but unique perspective, honest benchmarks, and voice belong to you. - ---- - -## FAQ - -### What is OpenSEO? - -OpenSEO is an open-source, agent-native SEO platform and Model Context Protocol (MCP) server. It connects AI assistants directly to live search data, including keyword search volume, difficulty, competitor gap analysis, backlink profiles, site audits, and live SERP rankings. Instead of trapping data in proprietary dashboards, OpenSEO provides real search data through tools and guided skills so assistants and content teams can research and execute directly in chat or code editors. - -### What is Vellum? - -Vellum is an open-source personal AI assistant that lives on your computer. It has its own identity, persistent memory that carries across conversations, and its own email address. Unlike passive chat windows, Vellum works proactively in the background, connects to tools and services via plugins and MCP, and runs across Mac, iOS, Android, Slack, Telegram, and the web. - -### Why connect OpenSEO to an AI assistant instead of using a dashboard? - -The web app is useful for high-level overviews, but connecting OpenSEO over MCP lets your assistant use search data as live context while drafting, planning, and editing. Instead of copying numbers from a dashboard into a doc, your assistant checks keyword volume and live SERP structure mid-conversation. - -### How much does this setup cost compared to legacy SEO suites? - -Ahrefs and Semrush charge $130 or more per month for a single seat. OpenSEO is $10/month with $10 of usage credits included (and a free trial with $0.50 of credits). A typical keyword or SERP lookup costs around five cents, meaning small teams pay a fraction of enterprise SaaS pricing for real DataForSEO search data. - -### What is the difference between SEO and GEO in this workflow? - -Traditional SEO focuses on keyword volume, metadata, and ranking algorithms. GEO (Generative Engine Optimization) focuses on long tail queries, getting cited, and referenced by AI models like Claude, ChatGPT, and Google AI Overviews. Using OpenSEO's live SERP tools allows your assistant to analyze what sources AI engines currently cite for a topic and structure your content to become the authoritative reference. - -### Does pulling search data slow down content drafting? - -No, it does the opposite. By querying OpenSEO directly via chat prompts, competitive gap analysis and SERP structure reviews take seconds rather than the hours typically spent exporting CSVs and formatting spreadsheets. diff --git a/web/content/blogs/seo-for-startups.md b/web/content/blogs/seo-for-startups.md deleted file mode 100644 index 4b01f9862..000000000 --- a/web/content/blogs/seo-for-startups.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: "SEO for Startups: A Founder’s Handbook" -description: "A founder-focused handbook to SEO for startups: how to use search for market research, early rankings, and smarter growth without wasting time." -author: "OpenSEO Team" -date: "2026-04-22" ---- - -If you've started a company, you've probably considered investing some time into SEO for your startup, or search engine optimization. In this guide, I'll try to give you an overview of how you should be thinking about SEO as someone building something new. - -SEO compounds as your brand builds its reputation. It can also help with market research before you have a website. - -If you want an agent to help, connect [OpenSEO MCP](/docs/mcp) first so it can use live keyword, SERP, domain, backlink, and saved keyword data. If you are not sure where to start, use the [SEO coach](/docs/skills/seo-coach). If you are setting up SEO for a new company or site, begin with [SEO project setup](/docs/skills/seo-project-setup). - -## Table of Contents - -- [Mental Model: How does SEO affect your business?](#mental-model-how-does-seo-affect-your-business) -- [Market Research Techniques](#market-research-techniques) -- [How can you improve your site's rankings?](#how-can-you-improve-your-sites-rankings) -- [A simple founder SEO priority list](#a-simple-founder-seo-priority-list) - -![SEO for Startups infographic](/blog/seo-for-startups/infographic.png) - -## Mental Model: How does SEO affect your business? - -### Improving your site's rankings - -Google's goal is to show the website, or AI answer, most likely to satisfy the user's search. Your ranking improves when Google shows your website for a search term, the person clicks, and then finds what they were looking for. - -The major ways to improve SEO are still pretty simple: - -- Your website's domain name, title, and description engage customers -- You have quality content that satisfies the user's search -- Reputable websites link to your brand, increasing your authority - -Lots of guides and influencers will try to sell you shortcuts for SEO. For new companies, you will usually do better if you treat SEO as a side effect of good marketing rather than the main way to drive growth. If you understand your customer best, provide a unique offering, and have customers and thought leaders in your space recommending you, your ranking will improve faster than it will from chasing specific SEO tricks. - -### Market Research - -In 2026, when founders research a new market, they often reach for Claude or ChatGPT to do "Deep Research." These tools search the web, read Reddit conversations, and pull together industry reports and blog posts faster than most of us would by hand. - -That research is good for qualitative context. It helps you understand how people talk, what themes come up, and which companies are getting attention. SEO tools give you a different signal. They show what customers search for, which pages Google rewards, and where competitors get traffic. - -SEO tools can help answer questions like: - -- Which competitors in the space are the most popular and trusted? -- What are people searching when Google recommends my competitors? -- Which of my competitors' pages drive the most traffic? -- What blog posts or pages should you add to your website to help customers and drive traffic? -- What information does Google's algorithm think your customers are looking for? - -## Market Research Techniques - -The screenshots below show what this data looks like in OpenSEO. - -### Competitor Research - -Competitor research gives founders a fast read on market demand. - -With a tool like [OpenSEO](https://openseo.so/), you can enter a competitor's domain and see which keywords it ranks for, which pages are ranking, and where the estimated traffic seems to come from. - -That can shape your strategy. If a competitor gets traffic from a specific page type, buyers may care about that topic more than you realized. It can also reveal use cases, niches, or workflows that fit your product but do not show up in a few customer calls. - -For a focused workflow, use the [competitor analysis skill](/docs/skills/competitor-analysis). If you are still figuring out which domains matter, start with the broader [competitive landscape skill](/docs/skills/competitive-landscape). - -![OpenSEO domain overview showing top pages, keywords, and estimated traffic](/blog/seo-for-startups/domain-overview.png) - -### Keyword Research - -Keyword research shows how much search volume exists for certain terms, related searches, and whether a query is informational or commercial. Not every search is equally valuable. Simple answer-style searches may send fewer clicks because Google can answer them on the results page. Searches with comparison, workflow, or buying intent often work better because people still want detail, options, and proof. - -It also helps map a market. If you search a seed phrase like "best crm for," the suggested and related keywords can show you which audiences are worth attention: agencies, startups, consultants, real estate teams, and so on. - -For a repeatable workflow, use the [keyword research skill](/docs/skills/keyword-research). When you have a useful list of ideas, use the [keyword clustering skill](/docs/skills/keyword-clustering) to group them into page targets. - -![OpenSEO keyword research view showing related or suggested keywords for a seed term](/blog/seo-for-startups/keyword-research.png) - -### Backlink Analysis - -Backlink analysis shows which websites link to a specific website. - -When you look at who links to competitors, you start to see which publications, directories, newsletters, and communities matter in the market. You can also see why certain companies are treated as credible. Sometimes they earn attention through PR. Sometimes it is partnerships, original research, or a useful tool that people keep citing. - -Those links can tell you how attention moves through a market. - -![OpenSEO backlink analysis showing referring domains or top backlinks for a competitor](/blog/seo-for-startups/backlink-analysis.png) - -## How can you improve your site's rankings? - -You do not need to become an SEO expert to make progress. For most founders, covering the basics and avoiding obvious mistakes is enough. - -### Site Title & Description - -Your page title and meta description are the billboard Google shows searchers. - -They play a big role in whether someone clicks. A good title matches the search, tells the reader what they will get, and gives them a reason to choose your result. If someone searches for "inventory software for small manufacturers," that page should look like it was written for that need. - -### Google Search Console - -[Google Search Console](https://search.google.com/search-console/about) is your source of truth for how your site is performing in Google Search. - -It helps you see what queries you show up for, which pages get impressions and clicks, whether Google is indexing your pages, and what crawl or coverage issues need attention. - -If you're new to it, Google's docs are worth reading: - -- [Get started with Search Console](https://support.google.com/webmasters/answer/9128669) -- [Performance report](https://support.google.com/webmasters/answer/7576553) -- [Page indexing report](https://support.google.com/webmasters/answer/7440203) - -### Site Audits - -Site structure can get deep. There is a whole specialty built around it called "Technical SEO". If you're starting out, do not obsess over perfect audit scores. - -The main things to care about are: - -- Does your website load fast enough that users do not bounce? -- Can Google crawlers find all the pages you want indexed? -- Is the structure of the site clear enough that both users and search engines can navigate it easily? - -For most teams, a quick pass with [Google Search Console](https://search.google.com/search-console/about) or any site audit tool (e.g., Ahrefs, Screaming Frog) is enough to find the highest-priority issues. - -If you're non-technical, use the SEO tooling built into your CMS or hosting provider and fix the major problems. If you are technical, run an audit and resolve the highest-priority crawl, indexation, broken link, or performance issues. Your coding agent can help with the implementation. - -### Quality Content - -Google wants to show relevant pages that satisfy the search. If people land on your site and find what they wanted, your website will climb the rankings. - -There is no shortcut here. The page has to be useful. - -That does not mean every company needs a giant blog. It means the pages you do publish should do a clear job well. They might explain something your buyer needs to understand, compare options, answer a buying question, or solve a specific workflow problem. - -You can use AI to help write content. The bar is still the same: if the page is better than the alternatives for the search term, it has a chance to climb. If it is vague, padded, or interchangeable with everything already ranking, it probably will not. - -### Backlinks - -Backlinks are when other companies link to your website. They signal your website's authority on the topic that the linking page ranks for. - -There are plenty of sketchy offers for backlink exchanges and paid placements. Some may move a metric, but they rarely help the business. - -The backlinks that tend to matter most are the ones that make sense for your business regardless of their SEO benefit. Some examples: a niche publication, a respected directory, a relevant newsletter, a partner case study, or a useful piece of research that people cite. - -The best way to get those links is to give people a real reason to talk about you. Publish a strong customer story, create original data, build a useful free tool, or get listed in the places your market trusts. - -For a focused outreach workflow, use the [link prospecting skill](/docs/skills/link-prospecting). If you want an AI agent to help with this work, [set up OpenSEO MCP](/docs/mcp) so it can use live OpenSEO data. If you are new to the process, start with [SEO coach](/docs/skills/seo-coach). For a fresh project, use [SEO project setup](/docs/skills/seo-project-setup) before jumping into research. - -## A simple founder SEO priority list - -If you're still not sure who your customer is, rankings can wait. Use SEO tools for research first. - -When you're ready to start focusing on SEO, this is a good order: - -1. Make sure the product, positioning, and site messaging are clear -2. Run [SEO project setup](/docs/skills/seo-project-setup) so goals, scope, and data sources are clear -3. Set up [Google Search Console](https://search.google.com/search-console/about) and [OpenSEO MCP](/docs/mcp) -4. Fix major crawl, indexation, and performance problems -5. Identify the core searches that map to your product and customer pain points -6. Build or improve the pages that deserve to rank -7. Use [competitor analysis](/docs/skills/competitor-analysis) and [keyword research](/docs/skills/keyword-research) to find obvious gaps -8. Earn mentions and links by doing things worth citing - -## Final thought - -SEO is worth understanding as a founder because it shows what the market is asking for, what Google believes users want, and whether your company appears as a credible answer. - -Used well, it becomes both a research tool and a compounding distribution channel. Understand the customer, make useful pages, keep your site healthy, and give people a reason to talk about your company. - -If you want to explore this data yourself, [OpenSEO](https://openseo.so/) is an affordable SEO tool built by a founder for founders, without the usual bloat of tools made for SEO power users. diff --git a/web/content/blogs/two-surfaces-two-timelines.md b/web/content/blogs/two-surfaces-two-timelines.md deleted file mode 100644 index 2f51fb6fd..000000000 --- a/web/content/blogs/two-surfaces-two-timelines.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: "Two Surfaces, Two Timelines" -description: "Getting recommended by an LLM is two separate problems on different clocks. One of them you cannot move this quarter. The other is a finite list of pages you can edit this week." -author: "Jeremy Rivera" -date: "2026-09-04" ---- - -Last August I published six markdown companion files so AI crawlers would have something cheap and clean to read on my podcast site. Then I ran a control test on my own server and found that unscriptedseo.com was returning 429 to GPTBot on every request, from the same IP that was serving a browser 200 eight times in a row. - -I had spent a week optimising content for a door that was locked. - -That is the argument of this post. Getting recommended by an LLM is two problems, they run on different clocks, and most of what gets sold as a solution addresses the half you cannot move. - -If you want an agent to run the audits below, connect the [OpenSEO MCP](/docs/mcp) first so it can pull your live ranking and Search Console data. - -## Table of Contents - -## The two surfaces - -An assistant names your product from one of two places. - -The first is what the model already knows. That is where "everyone knows Ahrefs, Semrush, SE Ranking" lives. It is fixed at training time and updates on the model maker's schedule. - -The second is what the assistant fetches while it answers you. That is a live read of the open web, and you can work on it today. - -![A two panel diagram titled Two Surfaces, Two Timelines. The left panel, Training data, is greyed out and labelled years, no lever this quarter, describing where established tool names live. The right panel, Retrieved pages, is highlighted and labelled this month, entirely workable, describing a live index of roughly forty pages that decide any product category.](/blog/two-surfaces-two-timelines/two-surfaces-two-timelines.png) - -OpenAI documents the second surface plainly, and the wording repays a close read: - -> ChatGPT search typically rewrites your query into one or more targeted queries that it sends those providers. - -Two things follow. Your page is not being matched against what the user typed, it is being matched against a rewrite you never see. And OpenAI states directly that ["placement is not guaranteed"](https://help.openai.com/en/articles/9237897-chatgpt-search), along with the precondition: to make a website eligible for inclusion, allow OAI-Searchbot to crawl it. - -Google describes its own version, a [query fan-out technique](https://developers.google.com/search/docs/appearance/ai-features) that issues "multiple related searches across subtopics and data sources," and says there are no additional requirements to appear in AI Overviews or AI Mode. - -Perplexity separates the two surfaces explicitly in its [bot documentation](https://docs.perplexity.ai/guides/bots). PerplexityBot exists "to surface and link websites in search results" and is "not used to crawl content for AI foundation models," while Perplexity-User fetches a page live during an answer. - -The retrieval half comes down to a crawl, a rewrite, and a selection. You can be excluded at any of the three. - -## The overlap nobody agrees on - -The obvious next question is whether the pages that get cited are the pages that already rank. I went looking for a number and found three that disagree. - -![A bar chart titled The Studies Disagree, showing the share of AI Overview citations that also rank organically: Ahrefs 38 percent from 863,000 SERPs, Surfer SEO 52 percent from 405,576 AI Overviews, seoClarity 56 percent from 362,000 queries. Below, a second comparison shows the same Ahrefs measure at 76 percent in July 2025 falling to 38 percent in March 2026.](/blog/two-surfaces-two-timelines/ai-overview-organic-overlap-studies.png) - -[Ahrefs](https://ahrefs.com/blog/ai-overview-citations-top-10) put it at 38%, from 863,000 SERPs and 4 million citation URLs. [Surfer](https://surferseo.com/blog/ai-overviews-study/) measured 52% across 405,576 AI Overviews. [seoClarity](https://www.seoclarity.net/research/aio-rankings-overlap) reports 56% from the top 20, across 362,000 queries and 5.1 million citations. - -I do not think any of them is wrong. They cut at different top-N thresholds on different days. The seoClarity study also reports that 94% of queries showed *at least one* overlap, which is a much weaker claim than the 56% figure and gets quoted as though it were the same finding. - -The number that matters is none of those three. Ahrefs measured roughly 76% in July 2025 and 38% in March 2026 using their own method both times, and they attribute the fall to query fan-out. The overlap is a moving trend rather than a constant, so anything you build on a single overlap figure has a shelf life of about a quarter. - -Where citations concentrate is steadier and more useful. Research by Tom Wells of Peec AI, [published through Wix Studio's AI Search Lab](https://www.wix.com/studio/ai-search-lab/research/content-types-most-cited-by-llms), examined 1,056,727 citations across 75,000 answers and found listicles are the most-cited format at 21.9%, rising to 40% on commercial-intent queries. For "what tool should I use," the roundups are the corpus. - -## The half you cannot rush - -So what about the training side? This is where I expected a number and did not find one. - -The mechanism is well studied. *Dated Data*, from a Johns Hopkins team, shows that a model's [effective knowledge cutoff differs from its reported one](https://arxiv.org/abs/2403.12958), because CommonCrawl dumps carry meaningful amounts of older data and deduplication is imperfect. The boundary is fuzzy. That work does not tell you how long a new brand takes to cross it. - -As far as I can tell, nobody has published that figure. If you see a confident claim that it takes two years to enter the training data, ask where the number came from. - -What does exist is a control condition inside a paper about something else. Hyunseok Paeng's ["Injection Paradox"](https://arxiv.org/pdf/2606.09204), accepted at the ICML 2026 FAGEN workshop, needed a product with low brand recognition and chose the Edifier NeoBuds Pro 3. The line I keep returning to sits in the methodology: the product received **0 out of 100 recommendations from both GPT-4o-mini and Haiku when no corpus was provided**. Given a retrieval corpus, the same product reaches a 54% baseline in Claude Opus. - -That is a control result rather than the paper's headline, so I am careful about how much weight it carries. It is still the cleanest published demonstration I have found that for a product a model does not already know, retrieval is not an advantage on top of recognition. It is the whole route. - -## The court drew the same line - -The distinction has become load-bearing enough to turn up in the remedies opinion in *United States v. Google*. Judge Mehta ordered Google to make search index and user-interaction data available to qualified competitors, and in weighing publisher remedies the court considered letting publishers opt out of crawling "for inclusion in Google's search index and for training its GenAI models and products." - -Index and training, named separately, as two things a publisher might refuse independently. Google's own patent for [generative summaries](https://patents.google.com/patent/US11769017B1/en) describes selecting result documents using "query-dependent measure(s), query-independent measure(s), and/or user-dependent measure(s)" and then linking back to the documents that verify the summary. - -The industry is still arguing about whether these are one surface. The court and the patent office already treat them as two. - -## Check the door first - -Which brings me back to my own server, and to the check almost nobody runs. - -![A two panel comparison titled Refused at the Door. Left panel, browser user agent, 200, eight requests back to back, every one served, X-Powered-By PHP header present. Right panel highlighted, GPTBot user agent, 429, four requests 45 seconds apart after a two minute cooldown, no X-Powered-By header. Caption notes six sibling sites on the same host served GPTBot 200 under the identical test.](/blog/two-surfaces-two-timelines/gptbot-user-agent-block-test.png) - -Same IP. Browser user agent, eight requests back to back, 200 every time. GPTBot user agent, four requests spaced 45 seconds apart after a two minute cooldown, 429 every time. So this was not rate limiting. - -The detail that settles it: the 429 carried no `X-Powered-By` header and the 200 did, so PHP never ran. The request was refused at the LiteSpeed edge before WordPress saw it. It was not in robots.txt, not a plugin, not `.htaccess`, and six sibling sites on the same host served GPTBot 200 under the identical test. - -I had been publishing companion files for AI crawlers on a site that refused the crawler at the door. The static `.md` files still returned 200, because they never touch PHP, which was a useful accident rather than a plan. - -You cannot be selected from a set you were never admitted to. This is the one part of the stack that is binary, cheap to test, and almost never tested. - -## TL;DR - -- Getting recommended is two problems. Training data is a multi-year brand problem. Retrieved pages are a finite list you can work this month. -- All three major vendors document the retrieval half. OpenAI says outright that placement is not guaranteed and that crawl access is the precondition. -- Citation-to-organic overlap is measured at 38%, 52% and 56% by three credible studies, and the same team's figure fell from 76% to 38% in eight months. -- Listicles are 21.9% of all citations and 40% on commercial queries. -- For a brand the model does not know, retrieval is the only route, not an edge. -- None of it matters if you are returning 429 to the crawler. - -## Homework - -1. **Test your own door.** Curl your site with each AI crawler's user agent and with a browser user agent, from the same IP, interleaved. Compare headers as well as status codes. `X-Powered-By` told me more than the 429 did. -2. **Build the retrieval corpus.** Pull the top twenty pages for the two or three questions a buyer actually asks, phrased the way a person asks them. "Best SEO tools for beginners" and "cheapest SEO tool" retrieve different pages. -3. **Mark where you already appear**, and split the list into present, absent, and present-but-wrong. Present-but-wrong, where your pricing or description is stale, is the fastest fix and nobody looks for it. -4. **Write the sentence that earns your slot** for every page you are absent from. If you cannot write one, that page is not a target. - -### Full Prompt: Build the retrieval gap list - -``` -Using the OpenSEO MCP, for each of these buyer questions: - - - - - - - -1. Return the top 20 ranking URLs for each. -2. For each URL, fetch the page and report whether is mentioned, - and if so quote the exact sentence and note the stated price. -3. Flag any mention where the price or description is out of date. -4. Produce one table: URL, ranks for, mention status, stale claim, contact path. -5. Sort by ranking position, best first. -``` - -Optimising content while the crawler gets a 429 is an expensive way to feel productive. - ---- - -*Sources are linked inline. First-party crawler data is from my own servers, August 2026. The panel referenced here was recorded 28 August 2026 for The Unscripted SEO Interview Podcast with [Patrick Stox](https://unscriptedseo.com/patrick-stox-on-building-in-the-geo-era/), Ben Senescu of OpenSEO, and [Ben Wills](https://unscriptedseo.com/ben-wills-one-word-prompt-llm-testing/) of OppAlerts. Every cited URL was verified on 2 September 2026.* diff --git a/web/content/blogs/what-broke-the-99-dollar-ceiling.md b/web/content/blogs/what-broke-the-99-dollar-ceiling.md deleted file mode 100644 index 4e58362c9..000000000 --- a/web/content/blogs/what-broke-the-99-dollar-ceiling.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: "What Broke the $99 Ceiling" -description: "A decade of indie SEO tools died against a price comparison they could not win. The thing that finally opened the market was not a cheaper tool, it was the collapse of the twenty hours a week it took to use one." -author: "Jeremy Rivera" -date: "2026-09-04" ---- - -Everybody has spent fifteen years complaining that SEO tools cost too much, so you would expect a $10 alternative to be the headline. The price was never what stopped you. - -I spent years inside this problem. [Raven Tools](https://raventools.com/) brought me out of Homes.com and out to Tennessee, and back in 2008 to 2010, alongside [Moz](https://moz.com/), before [Ahrefs](https://ahrefs.com/) was a thought anybody had had, we were one of the better known SaaS tools in the space. I watched what happened to every indie tool that came after us, and it was always the same two questions. - -Is that already in Semrush or Ahrefs? And do I need to pay for this *on top of* Semrush? - -If you want an agent to run the audit at the end of this post, connect the [OpenSEO MCP](/docs/mcp) first so it can pull your live ranking and Search Console data. - -## Table of Contents - -## The ceiling nobody could price above - -Those two questions built a ceiling at $99 a month, and that ceiling killed a decade of good software. - -Price under $99 and you were a toy, useful but not something a team would build a process around. Price over $99 and you got compared to the full [Semrush](https://www.semrush.com/) suite on day one, and unless you had all of it, you lost. Every indie SEO tool had two options: play small forever, or jump to enterprise and skip the middle. - -![A bar chart titled The $99 Ceiling comparing monthly entry prices: OpenSEO at $10 where most users never pass that tier, Ahrefs at $99 and Semrush at $110 as baselines, with a horizontal line marking the $99 comparison trigger above which any tool gets measured against a full suite.](/blog/what-broke-the-99-dollar-ceiling/price-ceiling-openseo.png) - -That ceiling was real and it held for a decade. [Moz Pro has listed a $99 entry tier continuously since at least February 2016](https://moz.com/products/pro/pricing), which four separate archived snapshots confirm, and the tier above it drifted between $149 and $179 over the same period. The stability of the $99 line is the notable part. - -One correction to the story I used to tell, though. The market converged *on* $99 rather than starting there. Ahrefs Lite was $79 a month in December 2015 and Semrush Pro was $69.95 in mid-2015. Both climbed to roughly $99 by 2017 and stopped. So this was a ceiling the market found, not one it was born with. - -## What actually changed - -When Ben Senescu told me OpenSEO runs at $10 a month, my first read was that somebody had finally undercut the ceiling. That is not what happened, and the real answer is more useful to you. - -![A quote card reading: They aren't comparing against Semrush, because they've never used Semrush before. It was totally inaccessible to them at that price point. Attributed to Ben Senescu, founder of OpenSEO, from The Unscripted SEO Interview Podcast.](/blog/what-broke-the-99-dollar-ceiling/quote-never-used-semrush.png) - -His paying customers are not professional SEOs. They are entrepreneurs doing SEO for the first time, people who never had the $110 option, so they are not comparing anything. The reason they can do it now is that the work got cheap. - -Think about what an hour of SEO used to cost you in time. You wanted to change a meta title, so you downloaded a plugin, and you had to know [Yoast](https://yoast.com/) existed to know which plugin. Then you logged into WordPress, found the page, opened it, waited for it to load, made the change, saved it, and checked it. Ten minutes to edit one string. Everything else lived in spreadsheets, then a Google Doc, then another spreadsheet, then copy, paste, copy, paste. - -That was the real bill: ten to twenty hours a week. Against twenty hours a week, the difference between a $10 tool and a $100 tool was a rounding error, which is why cheap tools never found a market. They solved the small half of the problem. - -**Once the twenty hours collapses, the hundred dollars starts to matter.** That is why this market exists now and did not exist two years ago. - -Worth being accurate about the pricing, since it is the whole argument: OpenSEO is $10 a month and free to start, and it is not free in the sense people usually mean. Good SEO data costs money everywhere, which is why every serious suite lands near the same number. You either bring your own DataForSEO key or pay a small fee on top of the data you use. - -## Your constraint moved - -If you sat out SEO because the tooling was priced for agencies, the door is open. The useful takeaway is not "go buy a cheap tool." It is that your constraint moved and you probably have not moved with it. - -- **Execution used to be the bottleneck.** It is not any more. You can generate more work in an afternoon than you can evaluate in a week. -- **Judgement is the bottleneck now.** Which of these things is worth doing, and what do you leave out? No tool answers that. -- **The people who do well over the next two years** will be the ones who get good at saying no, rather than the ones who get good at prompting. - -Ben put this better than I would have, and he was talking about his own roadmap when he said it: - -![A quote card reading: I can do anything. What's the one thing I should do? Attributed to Ben Senescu, founder of OpenSEO, from The Unscripted SEO Interview Podcast.](/blog/what-broke-the-99-dollar-ceiling/quote-one-thing.png) - -He had roughly sixty open pull requests from strangers, and by his own account most of them were probably good work. He declared bankruptcy on the queue, published a roadmap explaining why, and stopped taking external code. That is uncomfortable to write and it is also the job now. - -## The cost nobody is pricing in - -I do not want to hand you a clean story, because this swap has a bill attached. - -When it takes ten minutes to edit a meta title, you think about whether the title is right. When it takes four seconds, you do not. You can produce a hundred pages that are each ninety-five percent correct and never find the five percent, because the errors do not cluster. They sit evenly across everything you made. Ben said the same about features, and it applies to content, redirects, schema, and most of the rest of this job. - -The discipline that friction used to enforce now has to come from you, which is a harder ask than it sounds, and I do not think many people have noticed the swap. - -## Do it with OpenSEO - -Here is the audit I would actually run this month, and it takes about twenty minutes. - -### 1. List what you would build if you had unlimited time - -Write it all down. Twenty items, forty, however many. This is the part that used to be constrained and is not. - -### 2. Mark which items a model can finish without you - -Be honest. Most drafting, most technical cleanup, most first-pass keyword work. - -### 3. Circle the ones that need your judgement - -Positioning. Who you are for. What to leave out. Which client to say no to. That list is short, and it is the only list that matters now. - -### Full Prompt: Find the work only you can do - -``` -Using the OpenSEO MCP, pull my current ranked keywords and top pages. - -Then, for each item on this list of planned work: - - - -1. Classify it as EXECUTABLE (a model can complete it end to end) or - JUDGEMENT (it needs a decision about positioning, audience, or tradeoffs). -2. For EXECUTABLE items, say what data you would need from OpenSEO to start. -3. For JUDGEMENT items, write the one question I have to answer first. -4. Return the JUDGEMENT list, shortest first. -``` - -Then go and do the judgement list yourself, carefully, and let the tooling take the rest. - -The tooling got cheap. Your attention did not. - ---- - -*Ben Senescu is the founder of OpenSEO. He joined me on The Unscripted SEO Interview Podcast on 13 August 2026; [the full conversation is here](https://unscriptedseo.com/ben-senescu-open-source-seo-99-ceiling/). Historical pricing was checked against archived vendor pages, and current pricing against each vendor's own pricing page, on 2 September 2026.* diff --git a/web/content/blogs/wrong-half-of-your-audience.md b/web/content/blogs/wrong-half-of-your-audience.md deleted file mode 100644 index 8d0131dcc..000000000 --- a/web/content/blogs/wrong-half-of-your-audience.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: "Ranking for the Wrong Half of Your Audience" -description: "Search intent has four buckets, and none of them tell you which customer you meant. Audit a keyword list by audience before you map it by temperature." -author: "Jeremy Rivera" -date: "2026-07-31" ---- - -Alex Zweydoff niched his agency into one industry on day one: property management, out of Orlando. Clients keep arriving with the same chart, where traffic and form fills both climb and no new business closes. - -He can call the diagnosis before the audit finishes. Whoever built the site optimized it for tenants. - -> They can throw a rock and hit ten tenants at the same time. They need owners who are looking for a trusted property manager. -> -> Alex Zweydoff, ClearLead Digital - -A property manager fills a vacancy without help from you. Zillow does that. The money comes from owners handing a property over to manage, so Alex scores his accounts on doors added to the client's portfolio, meaning signed management contracts. Tenant traffic can hit every target in the SEO report and move that number by zero. - -Both audiences search for property management and land on the same contact form, and only one of them pays. - -If you want an agent to run the audit below, connect the [OpenSEO MCP](/docs/mcp) first so it can pull your live ranking and Search Console data. - -## Table of Contents - -- [The variable intent mapping leaves out](#the-variable-intent-mapping-leaves-out) -- [Three stages of owner intent](#three-stages-of-owner-intent) -- [The rungs above the stages](#the-rungs-above-the-stages) -- [Do it with OpenSEO](#do-it-with-openseo) -- [What to do Monday morning](#what-to-do-monday-morning) - -## The variable intent mapping leaves out - -Sort "rental homes in Baldwin Park" and "property management companies Baldwin Park" through the standard four-type model and both come back commercial. Run them through the hot, warm, and cold map in our [search intent guide](/library/keyword-research/search-intent-mapping) and both look warm. That model describes what a searcher wants to do. It carries nothing about who the searcher is, and in a two-sided business that variable decides whether the click has any value at all. - -![OpenSEO keyword research results for property management in Orlando, where the intent column labels both tenant queries and owner queries as commercial, with an added audience column marking each row tenant or owner](/blog/wrong-half-of-your-audience/audience-intent-table-openseo.png) - -Any company with a supply side and a demand side inherits the problem. Recruiters and candidates. Hosts and guests. The keyword database cannot see the split, because both sides reach for the same nouns. - -## Three stages of owner intent - -Alex breaks the paying side into three stages, and the phrasing matters because it came off sales calls rather than out of a tool: - -1. An owner is thinking about renting out a property. -2. An owner is unhappy with the manager they have. -3. An owner should have hired a property manager two weeks ago. - -Those stages port to any service business. Someone weighing up the category, someone leaving a competitor, and someone in the middle of an emergency need different pages. The middle group tends to be the most profitable and the least targeted, because almost nobody writes for a customer who already bought from somebody else. - -## The rungs above the stages - -Alyssa Evans runs growth strategy for fintech and SaaS companies, and she has dropped the funnel shape for what she calls a content ladder. - -> You want to make sure that you're found at that very base level and working their way up. So as they get more information and it's more complicated, then they ask more questions, and then you're also showing up on that next level. -> -> Alyssa Evans, Grow Your Strategy - -Her reason for the shape is the answer layer. People type long questions into ChatGPT, so in her words "you can't just do one or two words anymore," and the base rung of most ladders now gets answered before anyone opens a browser tab. Skip the rungs above the base and the LLM handles the beginner question while a competitor takes the follow-up. - -Stack Alyssa's ladder on Alex's stages and the output is a grid instead of a list. Every cell holds one audience at one rung. Alex takes his grid below the metro into sub-communities, because an owner in Orlando searches Baldwin Park, the neighborhood, and not the city. The volume in those cells is thin and the competition for them is thinner, since the agencies chasing the metro term left them empty. - -![A three by three grid of owner buying stages against content ladder rungs, with two cells filled to show existing pages and seven left empty](/blog/wrong-half-of-your-audience/audience-rung-grid-openseo.png) - -## Do it with OpenSEO - -No keyword tool can label audience for you. That label lives in your sales calls, not in a database, so pull the vocabulary each side uses from your own recordings first. Our [seed keywords guide](/library/keyword-research/seed-from-conversation) covers the extraction. Then hand your agent the labels and let it sort what you already rank for. - -### 1. Label the existing footprint - -Every ranked keyword for the domain, tagged by audience, then by stage within the paying audience. - -### 2. Measure the split - -The share of your rankings, impressions, and clicks belonging to the side that never buys. On inherited accounts, Alex finds this number ugly. - -### 3. Find the rebuild list - -Paying-audience queries sitting in positions 11 to 30 are already halfway up. Those pages come first. - -### Full Prompt: Audit by Audience - -```text -My business serves two audiences: [A, who pays me] and [B, who does not]. -Here is how each one describes their problem in their own words: [paste]. - -1. Label the footprint - -Pull every keyword [mydomain.com] ranks for in positions 1-50. Label each -one A, B, or ambiguous. For the A keywords, add a buying stage: weighing -up the category, leaving a competitor, or urgent. - -2. Measure the split - -Report the share of my ranking keywords, impressions, and clicks that -belong to audience B. Then list my top 10 pages by traffic and name the -audience each page was written for. - -3. Find the rebuild list - -Show me the A keywords where I rank 11-30, sorted by the buying stage I -have the thinnest coverage for. Output as a document I can review. - -``` - -The keyword research runs the same way it always did. The [keyword research skill](/docs/skills/keyword-research) handles the mining, and the audience labels ride on top of it as one extra column. - -## What to do Monday morning - -Open your five highest-traffic pages and name the customer each one was written for. If two of them answer to the audience that does not pay, you have your explanation for why the traffic line and the revenue line stopped agreeing. - -Then run the prompt against the rest of the site. The number to watch is the share: how much of your organic footprint you built for people who were never going to buy from you. Alex's clients keep finding that their best page by every metric in the report was written for the customer who does not buy, which reorders the content calendar before a single new keyword gets mined. - -[OpenSEO](https://openseo.so/) is an affordable, open-source SEO tool that connects your ranking, keyword, and Search Console data in one place, so an audit like this runs as a prompt instead of a spreadsheet afternoon. diff --git a/web/content/docs/agent-setup.mdx b/web/content/docs/agent-setup.mdx deleted file mode 100644 index f567fbb99..000000000 --- a/web/content/docs/agent-setup.mdx +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: "Agent setup" -description: "Connect your AI agent to OpenSEO and keep its SEO skills up to date." ---- - -OpenSEO connects your agent to live SEO data through MCP. Skills give it reusable workflows for keyword research, audits, competitor research, and other SEO tasks. - -## Set up your agent - -Copy this prompt into Claude Code, Codex, Grok Bot, Hermes, or another agent. It asks your agent to use its supported installation method and guide you through any steps you need to do yourself. - - - -Approve the OpenSEO sign-in in your browser when prompted. Once setup is complete, ask your agent to use **SEO Coach** to help you choose what to do next. - -You can also copy this prompt from [Agent setup in OpenSEO](https://app.openseo.so/ai). For a self-hosted instance, use its Agent setup page so the prompt includes your own server address. - -Prefer to set it up yourself? Follow the instructions for your agent below. If the setup prompt already completed installation, skip to [Update your skills](#update-your-skills). - -### Claude Code - -Run inside Claude Code to install MCP and the public SEO skills: - -```text -/plugin marketplace add every-app/open-seo -/plugin install openseo@openseo -``` - -If Claude asks you to activate the plugin, run `/reload-plugins`. Follow the sign-in prompt, then try `/openseo:seo-coach`. See the [Claude Code plugin guide](/docs/claude-code-plugin) for troubleshooting. - -### Codex CLI - -Run in your terminal: - -```bash -codex plugin marketplace add every-app/open-seo -codex plugin add openseo@openseo -``` - -Reload or restart Codex if the plugin is not available yet, then sign in: - -```bash -codex mcp login openseo -``` - -Ask Codex to use SEO Coach. See the [Codex plugin guide](/docs/codex-plugin) for troubleshooting. For plugins installed through a desktop directory, use that directory's controls. - -### Hermes - -Use the setup prompt above to connect MCP. To install SEO Coach through the Hermes skills hub, run in your terminal: - -```bash -hermes skills install every-app/open-seo/plugins/openseo/skills/seo-coach -``` - -Repeat with the names of other [OpenSEO skills](/docs/skills) you want. Hub installations retain their source so Hermes can check for updates later. See [Hermes skill management](https://hermes-agent.nousresearch.com/docs/user-guide/features/skills). - -### Grok Bot - -Paste the setup prompt into your Bot. Open **Marketplace** in the sidebar to manage packaged skills and connectors. Find private skills under **Your plugins → Manage plugins and skills → Private skills**. Private skills are shared across your Bots. - -Follow the Bot's instructions for any steps you need to complete yourself. See [Grok Bot skills](https://docs.x.ai/grok-bot/skills-routines-and-automations). - -### Claude web/Desktop and other agents - -Follow the [MCP connection guide](/docs/mcp), then install skills through your agent's supported skill manager. Claude web/Desktop manages custom skills under **Customize → Skills**; its flow differs from the Claude Code plugin. - -For agents using the skills CLI, choose public OpenSEO skills interactively: - -```bash -npx skills add https://github.com/every-app/open-seo/tree/main/plugins/openseo/skills -``` - -Choose the current agent and your preferred installation scope. Skip this if the OpenSEO plugin already supplies your skills. See [manual skill setup](/docs/skills/setup) for other options. - -## Update your skills - -When OpenSEO releases improved skills, update the copies installed in your agent. Updating the MCP connection alone does not update those files. - -Paste this prompt into the agent where you use OpenSEO. It asks the agent to check how the skills were installed and preserve your connection settings and personal edits. - - - -To update manually, use the method that matches your installation below. - -### Update the Claude Code plugin - -Run inside Claude Code: - -```text -/plugin marketplace update openseo -/plugin update openseo@openseo -/reload-plugins -``` - -You can enable automatic updates in `/plugin` → **Marketplaces** → **openseo** → **Enable auto-update**. Third-party marketplaces have this off by default. Reload when Claude prompts you to activate an update. See [Claude's plugin update documentation](https://code.claude.com/docs/en/discover-plugins#configure-auto-updates). - -### Update the Codex CLI plugin - -Run in your terminal: - -```bash -codex plugin marketplace upgrade openseo -``` - -Reload or restart Codex if the updated skills are not available. If your CLI rejects a command, check `codex plugin --help` for the commands supported by your version. See [OpenAI's marketplace documentation](https://developers.openai.com/plugins/build/plugins). - -### Update Hermes hub skills - -Check for updates, then update each installed OpenSEO skill by name. For example: - -```bash -hermes skills check -hermes skills update seo-coach -``` - -Repeat for your other installed OpenSEO skills. Hermes skips locally edited skills by default. Review those edits before replacing them; only add `--force` if you want to discard them. See [Hermes's update lifecycle](https://hermes-agent.nousresearch.com/docs/user-guide/features/skills#update-lifecycle). - -### Update skills installed with the skills CLI - -List your installations, then update the OpenSEO skills by name. For example: - -```bash -npx skills list -npx skills update seo-coach seo-audit -``` - -Use the names you actually installed and choose the same project or global scope. Pass `-g` for global skills or `-p` for project skills. Back up personal edits before updating. Run the setup command to select any newly added workflows separately. See the [skills CLI documentation](https://github.com/vercel-labs/skills#skills-update). - -### Update Grok Bot or manually copied skills - -Use the update prompt above. For Grok Bot, open **Marketplace → Your plugins → Manage plugins and skills** to find the installed skill or plugin. Its public documentation does not currently specify an upstream skill-update command, so use the management controls available in your version. - -For manually copied skills, compare your installed copies with the [public OpenSEO skill folders](https://github.com/every-app/open-seo/tree/main/plugins/openseo/skills). Preserve personal edits and replace the complete skill folder, including supporting files. Use your agent's skill manager for uploaded skills, such as Claude's **Customize → Skills**. - -After updating, reload the skills or start a new session if needed. Ask the agent to confirm it can find the updated OpenSEO skills before starting your next SEO task. diff --git a/web/content/docs/claude-code-plugin.md b/web/content/docs/claude-code-plugin.md deleted file mode 100644 index 0b13d4877..000000000 --- a/web/content/docs/claude-code-plugin.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: "Install the OpenSEO plugin for Claude Code" -description: "Add OpenSEO MCP and Agent Skills to Claude Code with one marketplace and one install command." ---- - -The OpenSEO plugin bundles OpenSEO MCP and all ten SEO Agent Skills into one install. This is the preferred way to set up OpenSEO in Claude Code. - -## Install - -Run these two commands in Claude Code: - -```bash -/plugin marketplace add every-app/open-seo -/plugin install openseo@openseo -``` - -If the install summary says `Run /reload-plugins to activate.`, run that command. - -Claude Code connects OpenSEO MCP at `https://app.openseo.so/mcp` and enables ten skills: - -- SEO Project Setup -- SEO Coach -- SEO Audit -- Keyword Research -- Keyword Clustering -- Competitive Landscape -- Competitor Analysis -- Local SEO -- Link Prospecting -- SEO Report - -## Finish the login - -Claude Code should prompt you to log in to OpenSEO right after install. If it doesn't, run `/mcp` and approve the OpenSEO connection from there. - -## Run a skill - -Plugin skills are namespaced by the plugin name: - -``` -/openseo:seo-project-setup -/openseo:seo-coach -/openseo:seo-audit -/openseo:keyword-research -/openseo:keyword-clustering -/openseo:competitive-landscape -/openseo:competitor-analysis -/openseo:local-seo -/openseo:link-prospecting -``` - -## Claude Desktop - -Claude Desktop doesn't support this plugin format — plugins are a Claude Code feature. For Claude Desktop, [add OpenSEO as an MCP connector](/docs/mcp#claude-desktop) instead. - -## Update - -Run inside Claude Code: - -```text -/plugin marketplace update openseo -/plugin update openseo@openseo -/reload-plugins -``` - -Updates land in the cache immediately, but the running session keeps the old version until you run `/reload-plugins` or restart Claude Code. - -For other installation methods, see [Agent setup and skill updates](/docs/agent-setup#update-your-skills). - -## Remove - -```text -/plugin uninstall openseo@openseo -``` - -## Troubleshooting - -To check what's actually installed, run `/plugin list` rather than bare `/plugin` — `/plugin` alone opens an interactive panel that doesn't show plain text. - -If `/reload-plugins` reports `0 skills`, that's normal, not a failure — its summary only counts a plugin's `commands/` directory, not `skills/`. Confirm the skills loaded by running one directly, for example `/openseo:seo-audit`. - -If `/plugin uninstall openseo@openseo` reports "not installed in this project," you likely installed to a different scope than the one being checked (User, Project, or Local). Run `/plugin list` to see the actual scope, or sidestep the picker entirely with the shell form: `claude plugin uninstall openseo@openseo --scope user`. - -If plugin skills don't appear, clear the plugin cache with `rm -rf ~/.claude/plugins/cache` — this clears every installed plugin's cache, not just OpenSEO's, so reinstall anything else you have after — then restart Claude Code and reinstall the plugin. - -If the OpenSEO connection doesn't show as authenticated, run `/mcp`, select OpenSEO, and complete the login. - -## Other clients - -This plugin is for Claude Code. For Codex CLI, use the [OpenSEO plugin for Codex](/docs/codex-plugin) instead. For Cursor, Codex Desktop, Claude Desktop, or an API key setup, see [Set up OpenSEO MCP](/docs/mcp) and [Set up OpenSEO Agent Skills](/docs/skills/setup). diff --git a/web/content/docs/codex-plugin.md b/web/content/docs/codex-plugin.md deleted file mode 100644 index 70206230f..000000000 --- a/web/content/docs/codex-plugin.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: "Install the OpenSEO plugin for Codex" -description: "Add OpenSEO MCP and Agent Skills to Codex with one marketplace and one install command." ---- - -The OpenSEO plugin bundles OpenSEO MCP and all ten SEO Agent Skills into one install. This is the preferred way to set up OpenSEO in Codex CLI. - -## Install - -Run these commands in your terminal: - -```bash -codex plugin marketplace add every-app/open-seo -codex plugin add openseo@openseo -codex mcp login openseo -``` - -`codex mcp login` opens a browser to approve the OpenSEO connection. If it reports that `openseo` isn't found, restart Codex first — bundled MCP servers only register after a restart, not immediately after install — then run `codex mcp login openseo` again. - -Codex connects OpenSEO MCP at `https://app.openseo.so/mcp` and enables ten skills: - -- SEO Project Setup -- SEO Coach -- SEO Audit -- Keyword Research -- Keyword Clustering -- Competitive Landscape -- Competitor Analysis -- Local SEO -- Link Prospecting -- SEO Report - -## Run a skill - -Type `$` in Codex to see available skills, or ask Codex to run one by name, for example "run seo-project-setup" or "run seo-audit on example.com". - -## Update - -```bash -codex plugin marketplace upgrade openseo -``` - -Reload or restart Codex if the updated skills are not available. For other installation methods, see [Agent setup and skill updates](/docs/agent-setup#update-your-skills). - -## Remove - -```bash -codex plugin remove openseo@openseo -``` - -## Troubleshooting - -If the OpenSEO MCP server doesn't appear after restart, run `/mcp` in the Codex TUI to check its status, then run `codex mcp login openseo` again. - -If it still doesn't authenticate, log out first and retry: - -```bash -codex mcp logout openseo -codex mcp login openseo -``` - -If a `codex plugin` command reports "unrecognized subcommand," run `codex plugin --help` to see the subcommands your installed version actually supports — they've changed across versions (for example, `add`/`remove`, not `install`/`uninstall`). - -## Other clients - -This plugin is for Codex CLI. For Claude Code, use the [OpenSEO plugin for Claude Code](/docs/claude-code-plugin) instead. For Claude Desktop, Cursor, Codex Desktop, or an API key setup, see [Set up OpenSEO MCP](/docs/mcp) and [Set up OpenSEO Agent Skills](/docs/skills/setup). diff --git a/web/content/docs/mcp.md b/web/content/docs/mcp.md deleted file mode 100644 index be9ec9179..000000000 --- a/web/content/docs/mcp.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -title: "Set up OpenSEO MCP" -description: "Connect OpenSEO MCP to Claude, Codex, and other AI clients." ---- - -OpenSEO MCP lets compatible AI clients call OpenSEO tools for keyword research, SERP inspection, local business research, competitive search intelligence, domain research, backlink overview, saved keywords, rank tracking, shared project context, and Google Search Console performance and URL inspection. - -The hosted MCP server URL is: - -```txt -https://app.openseo.so/mcp -``` - -The first connection sends you through OpenSEO login. After authorization, your MCP client can call OpenSEO tools with the project context and account scopes you approved. For headless environments and CI, [connect with an API key](#connect-with-an-api-key) instead. - -For the most current setup UI and a copyable endpoint, open [Agent setup in OpenSEO](https://app.openseo.so/ai). - -For setup prompts, plugin commands, and skill updates, see [Agent setup](/docs/agent-setup). - -## Claude Code - -The [OpenSEO plugin](/docs/claude-code-plugin) is the preferred way to connect Claude Code — one install adds MCP and the public SEO skills together. Use the steps below only if you want MCP on its own. - -Use user scope to make OpenSEO available across projects. Use local scope for the current repository. - -```bash -claude mcp add --transport http --scope user openseo https://app.openseo.so/mcp -``` - -After adding the server, approve the OpenSEO login when prompted. - -## Claude Desktop - -1. Open Customize -> Connectors. -2. Click Add (or +), then choose Add custom connector. -3. Paste `https://app.openseo.so/mcp`. -4. Approve the OpenSEO login when prompted. - -Claude Desktop custom connectors are available on Free, Pro, Max, Team, and Enterprise plans. Free plans support one custom connector. - -## Cursor - -1. Open Cursor Settings -> Tools & Integrations -> MCP Tools. -2. Click New MCP Server. Cursor opens `mcp.json`. -3. Add: - -```json -{ - "mcpServers": { - "openseo": { - "url": "https://app.openseo.so/mcp" - } - } -} -``` - -4. Approve the OpenSEO login when prompted. - -## Codex CLI - -The [OpenSEO plugin](/docs/codex-plugin) is the preferred way to connect Codex CLI — one install adds MCP and the public SEO skills together. Use the steps below only if you want MCP on its own. - -Run this in your terminal: - -```bash -codex mcp add openseo --url https://app.openseo.so/mcp -``` - -Approve the login when prompted. - -## Codex Desktop - -1. Open Settings -> Integrations & MCP. -2. Click Add your own. -3. Paste `https://app.openseo.so/mcp`. -4. Approve the OpenSEO login when prompted. - -## Connect with an API key - -Use an API key in headless environments, CI, or clients where OAuth is inconvenient. API keys are personal: anything an agent does with your key acts as you in your workspace. - -In the [OpenSEO app](https://app.openseo.so/settings), open **Settings -> API keys**, create a key, and copy it when it appears. It won't be shown again. - -For Claude Code, run: - -```bash -claude mcp add --transport http --scope user openseo https://app.openseo.so/mcp --header "Authorization: Bearer oseo_YOUR_KEY" -``` - -For Cursor, add `headers` to the server entry in `mcp.json`: - -```json -{ - "mcpServers": { - "openseo": { - "url": "https://app.openseo.so/mcp", - "headers": { - "Authorization": "Bearer oseo_YOUR_KEY" - } - } - } -} -``` - -For Codex CLI, put the key in an environment variable and reference it: - -```bash -export OPENSEO_API_KEY=oseo_YOUR_KEY -codex mcp add openseo --url https://app.openseo.so/mcp --bearer-token-env-var OPENSEO_API_KEY -``` - -Any other client that supports custom HTTP headers can send `Authorization: Bearer oseo_YOUR_KEY` or `x-api-key: oseo_YOUR_KEY`. - -## Available tools - -OpenSEO MCP exposes tools for SEO research workflows: - -- Research keywords with volume, difficulty, and CPC. -- Fetch live Google organic SERP results for keywords. -- Find exact keyword, page, rank, volume, CPC, intent, and traffic rows for a domain or page. -- Compare SERP competitors across a supplied keyword set. -- Search local businesses near a coordinate, filtering by rating, review count, or claimed status. -- Fetch one Maps or Local Finder SERP, and read Google Business Q&A when needed. -- Audit a Google Business Profile: categories, rating, hours, photos, and claim status. -- Collect Google reviews (including reviews from other sites) and Google Business posts. -- Look up valid Google Business category slugs. -- Check Google Maps rank at each point of a grid around a business. -- Hydrate keywords with search volume, difficulty, intent, CPC, and trends. -- List saved keywords from an OpenSEO project. -- Save useful keywords back to OpenSEO. -- Read rank tracker configs and latest keyword positions. -- Summarize a domain's organic footprint. -- Find keywords a domain already ranks for. -- Check backlink and referring-domain overview data. -- Read first-party Google Search Console performance (clicks, impressions, CTR, position). -- Inspect index status, crawl, and canonical for specific URLs (up to 10 per call). -- Read and update a project's shared context: business, goal, positioning, writing preferences, competitors, key pages, and a research log (free, no credits). -- Save and read HTML reports on a project (free, no credits). -- List a project's report templates, and save a reusable report brief to the project (free, no credits). - -## What to do after setup - -Once OpenSEO MCP is connected, [set up OpenSEO Agent Skills](/docs/skills/setup). MCP gives your agent access to OpenSEO data. Skills are separate `SKILL.md` files that tell your agent how to use that data for specific SEO jobs. - -Start with one focused workflow instead of asking your agent to "do SEO" broadly. - -- Use [SEO project setup](/docs/skills/seo-project-setup) to save your goals, positioning, competitors, and key pages to your project context, so every other skill reuses them. -- Use [SEO coach](/docs/skills/seo-coach) if you are new to SEO or are not sure which workflow to run first. -- Use [keyword research](/docs/skills/keyword-research) to discover keyword opportunities. -- Use [competitive landscape](/docs/skills/competitive-landscape) to map a market before choosing competitors or pages. -- Use [competitor analysis](/docs/skills/competitor-analysis) to study one competitor. -- Use [keyword clustering](/docs/skills/keyword-clustering) to turn keywords into page groups. -- Use [link prospecting](/docs/skills/link-prospecting) to find outreach prospects for a linkable asset. - -## Troubleshooting - -If your client cannot connect, check that the server URL is exactly `https://app.openseo.so/mcp`. - -If Codex reports `Authorization server response missing required issuer: expected https://app.openseo.so`, upgrade Codex CLI or the Codex desktop app to 0.147.0 or later. Codex 0.143 through 0.146 drop the issuer from the OAuth callback. You can also [connect with an API key](#connect-with-an-api-key) instead of OAuth. - -If authorization fails, disconnect the OpenSEO server in your client, add it again, and repeat the login flow. - -If your agent cannot find a project, ask it to list OpenSEO projects first and use the returned project ID in later tool calls. diff --git a/web/content/docs/meta.json b/web/content/docs/meta.json deleted file mode 100644 index 72534aba8..000000000 --- a/web/content/docs/meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pages": [ - "agent-setup", - "mcp", - "claude-code-plugin", - "codex-plugin", - "skills", - "self-hosting" - ] -} diff --git a/web/content/docs/self-hosting/cloudflare.md b/web/content/docs/self-hosting/cloudflare.md deleted file mode 100644 index 5ab624d4d..000000000 --- a/web/content/docs/self-hosting/cloudflare.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: "Cloudflare Self-Hosting" -description: "Deploy OpenSEO to your own Cloudflare account for internet-facing, multi-device, or team use." ---- - -Host OpenSEO on Cloudflare for internet-facing self-hosting across multiple devices or with your team. One deploy command provisions everything, including the Cloudflare Access login gate. Works on Cloudflare's free plan. - -## Prerequisites - -- **Node 22.6 or newer** and **pnpm** (`corepack enable` sets it up). -- **A Cloudflare account with R2 enabled.** Activating R2 requires a payment method on file, even within its free tier — if you have never used R2, open `R2` in the Cloudflare dashboard once. -- **A DataForSEO account** — see [DataForSEO API key setup](/docs/self-hosting#dataforseo-api-key-setup). - -## 1) Clone your OpenSEO repo - -Fork `every-app/open-seo` on GitHub if you want a repo you control, then clone it locally: - -```bash -git clone https://github.com/YOUR_GITHUB_USER/open-seo.git -cd open-seo -corepack enable -pnpm install -``` - -If you do not need a fork, clone the upstream repo instead: - -```bash -git clone https://github.com/every-app/open-seo.git -cd open-seo -corepack enable -pnpm install -``` - -## 2) Log in to Cloudflare (once) - -```bash -pnpm alchemy login # answer yes to "Customize OAuth scopes?" and enable access:write -pnpm alchemy cloudflare bootstrap # deploys alchemy's state-store Worker to your account -``` - -Already logged in from before without the `access:write` scope? Run `pnpm alchemy login --configure` — a plain repeat login doesn't re-ask about scopes. - -## 3) Create `.env.selfhost` - -Copy the template and fill in the required values: - -```bash -cp .env.selfhost.example .env.selfhost -``` - -## 4) Deploy - -```bash -pnpm deploy:selfhost --yes -``` - -This provisions the D1 database, KV namespaces, and R2 bucket, applies the database migrations, deploys the Worker, and creates the Cloudflare Access application protecting it (allowing exactly `ACCESS_ALLOWED_EMAILS`). If the account has no Zero Trust team yet, one is created for you, named after your workers.dev subdomain. - -## 5) Validate setup - -1. Open the Worker URL printed at the end of the deploy. -2. Sign in with Cloudflare Access. -3. OpenSEO should load after login. - -If login fails, re-check `ACCESS_ALLOWED_EMAILS` and redeploy. - -## Connect the MCP server through Cloudflare Access - -Use the same Cloudflare Access application that protects your OpenSEO Worker. Managed OAuth is required for MCP clients and is not enabled by default. - -1. Open Cloudflare Zero Trust. -2. Go to `Access controls` -> `Applications`. -3. Find your OpenSEO application, then select `Edit`. -4. Go to `Additional settings` -> `OAuth`. -5. Turn on `Managed OAuth`. -6. In `Managed OAuth settings`, allow the redirect URIs your MCP clients use: - - Allow `localhost` / loopback clients for CLI and desktop agents (Codex CLI, Claude Code) that register `http://localhost:PORT/callback`. - - Add HTTPS redirect URIs for web connectors (a path may end in `/*`). - - Without this, clients can't finish [Dynamic Client Registration](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/) and log in but expose no tools. -7. Save. - -MCP clients should connect to: - -```text -https://YOUR_WORKER_HOSTNAME/mcp -``` - -## Give teammates access to OpenSEO - -Add the teammate to `ACCESS_ALLOWED_EMAILS` in `.env.selfhost` and redeploy. Everyone allowed through shares one OpenSEO workspace. - -## Updating to the latest OpenSEO version - -```bash -git pull # or: git fetch upstream && git merge upstream/main, if you forked -pnpm install -pnpm deploy:selfhost --yes -``` - -## More guides on GitHub - -- [Operations](https://github.com/every-app/open-seo/blob/main/docs/SELF_HOSTING_CLOUDFLARE_OPERATIONS.md): telemetry and other day-to-day tasks. -- [Legacy deployments](https://github.com/every-app/open-seo/blob/main/docs/SELF_HOSTING_CLOUDFLARE_LEGACY.md): maintenance for installs created with the retired Deploy-button or manual Wrangler flows. diff --git a/web/content/docs/self-hosting/docker.md b/web/content/docs/self-hosting/docker.md deleted file mode 100644 index 052b0bb23..000000000 --- a/web/content/docs/self-hosting/docker.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: "Docker Self-Hosting" -description: "Run OpenSEO locally with Docker Compose using the published GHCR image." ---- - -Run OpenSEO locally with Docker. - -In Docker mode, OpenSEO uses `AUTH_MODE=local_noauth` (no auth checks, local admin user `admin@localhost`). Only expose it behind your own auth-protected reverse proxy, tunnel, or private network. For internet-facing self-hosting, use [Cloudflare](/docs/self-hosting/cloudflare) instead. - -The default `compose.yaml` uses the published GHCR image: - -- `ghcr.io/every-app/open-seo:latest` - -## Prerequisites - -- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or Docker Engine + Docker Compose) -- A [DataForSEO API key](/docs/self-hosting#dataforseo-api-key-setup) - -## Quickstart - -Clone the repo, then: - -```bash -git clone https://github.com/every-app/open-seo.git -cd open-seo -cp .env.example .env -``` - -Set `DATAFORSEO_API_KEY` in `.env` using the [DataForSEO setup guide](/docs/self-hosting#dataforseo-api-key-setup), then start OpenSEO: - -```bash -docker compose up -d -``` - -Open `http://localhost:` (default `3001`). Each container start builds the app and may take 1-2 minutes; follow progress with `docker compose logs -f`. - -Optional env values: - -- `PORT` (defaults to `3001`) -- `ALLOWED_HOST` (single reverse-proxy hostname to allow in Vite preview) -- `AUTH_MODE=local_noauth` (already set in compose) -- `OPEN_SEO_IMAGE` (defaults to `ghcr.io/every-app/open-seo:latest`) - -If you are putting Docker behind a reverse proxy or a temporary tunnel, remember that Docker self-hosting runs with app auth disabled. Only expose it behind your own auth-protected reverse proxy, tunnel, or private network, and add the public hostname before restarting: - -```bash -ALLOWED_HOST=yourdomain.com docker compose up -d -``` - -You can also persist it in `.env`. - -## Telemetry - -OpenSEO collects anonymized telemetry for core usage events: heartbeats with aggregate counts (installs, users, projects, feature usage) tied to a random install ID, sent every 5 minutes during the first two hours after install, then at most once daily. Telemetry also includes failed setup check names and statuses, never values or error messages. No URLs, keywords, prompts, emails, or IP-derived location are collected, and idle installs send nothing. - -To disable it, set `OPENSEO_TELEMETRY_DISABLED=1` (or `DO_NOT_TRACK=1`) in `.env`, then run `docker compose up -d --force-recreate open-seo`. - -## Pin to a specific image tag - -Set `OPEN_SEO_IMAGE` in `.env` and restart: - -```bash -OPEN_SEO_IMAGE=ghcr.io/every-app/open-seo:v1.2.3 -docker compose up -d -``` - -## Build your own image locally - -If you are testing local code changes, build and run a local tag: - -```bash -docker build -f Dockerfile.selfhost -t open-seo:local . -OPEN_SEO_IMAGE=open-seo:local docker compose up -d -``` - -## Common commands - -Restart service after env changes: - -```bash -docker compose up -d open-seo -``` - -Pull latest published image and restart: - -```bash -docker compose pull && docker compose up -d -``` - -Stop: - -```bash -docker compose down -``` - -## Health and troubleshooting - -Startup checks appear in `docker compose logs` before the build. Once running, `/api/health` reports configuration and database status, and `docker compose ps` reports container health. - -## Troubleshooting environment variables - -To confirm Docker Compose is using the expected environment variables: - -```bash -docker compose config -``` - -Check that `AUTH_MODE=local_noauth`, and that `DATAFORSEO_API_KEY` is the base64 encoded value of your DataForSEO email and API password in this format: `email:password`. - -If you changed `.env`, recreate the container so Compose reapplies it: - -```bash -docker compose up -d --force-recreate open-seo -``` diff --git a/web/content/docs/self-hosting/index.md b/web/content/docs/self-hosting/index.md deleted file mode 100644 index 943e6214e..000000000 --- a/web/content/docs/self-hosting/index.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: "Self-Hosting OpenSEO" -description: "Run OpenSEO yourself with Docker or Cloudflare, bring your own DataForSEO API key, and pay only for what you use." ---- - -OpenSEO is free and open source. Self-hosting means the app costs $0. You bring your own DataForSEO API key and pay DataForSEO directly for API usage. - -There are two self-hosting paths: - -- **Simple: [Docker](/docs/self-hosting/docker)**, recommended for personal use on your own machine. Easiest way to get started. -- **Advanced: [Cloudflare](/docs/self-hosting/cloudflare)**, for internet-facing self-hosting across multiple devices or with your team. A SaaS-like experience with automatic database backups, and it works on Cloudflare's free plan. Slightly more setup if you're unfamiliar with Cloudflare. - -## DataForSEO API key setup - -OpenSEO uses [DataForSEO](https://dataforseo.com/?aff=255379) to fetch SEO data. DataForSEO is a paid third-party service unaffiliated with OpenSEO. You need an API key to connect OpenSEO to it. - -1. Go to [DataForSEO API Access](https://app.dataforseo.com/api-access?aff=255379). -2. Click "Send by email" to get your credentials. -3. Copy the longer credentials labelled "Base64" credentials. -4. Set this as `DATAFORSEO_API_KEY` in your environment: - - Docker: `.env` - - Cloudflare: as a Worker secret in the dashboard - - Local development: `.env.local` - -New DataForSEO accounts include $1 of free credit to test with, and the minimum top-up is $50. See [pricing](/pricing) for cost estimates. Self-hosted costs run slightly lower, since the hosted service adds a 28% fee on DataForSEO requests. - -## Optional features - -### Google Search Console - -Search Console is optional and works in self-hosted deployments using your own Google OAuth client. It takes about 10 minutes of one-time setup. See the [Google Search Console guide on GitHub](https://github.com/every-app/open-seo/blob/main/docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md). - -### AI features (SAM) - -AI features like SAM, the in-app SEO agent, are optional. Set the `OPENROUTER_API_KEY` environment variable to enable them. Create a key at [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys). diff --git a/web/content/docs/self-hosting/meta.json b/web/content/docs/self-hosting/meta.json deleted file mode 100644 index f6990f5f6..000000000 --- a/web/content/docs/self-hosting/meta.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "title": "Self-Hosting", - "pages": [ - "[Overview](/docs/self-hosting)", - "[Docker](/docs/self-hosting/docker)", - "[Cloudflare](/docs/self-hosting/cloudflare)" - ] -} diff --git a/web/content/docs/skills/competitive-landscape.mdx b/web/content/docs/skills/competitive-landscape.mdx deleted file mode 100644 index 83af52291..000000000 --- a/web/content/docs/skills/competitive-landscape.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "Competitive Landscape Agent Skill" -description: "Run competitive SEO research with your AI agent: map market leaders, content patterns, keyword coverage, backlinks, and gaps." ---- - - - -The Competitive Landscape Agent Skill maps the SEO market before you decide what to build. - -Your agent inspects the market from several angles: recurring ranking domains, content patterns, keyword coverage, and authority signals. - -You get a higher-level view of who is winning, why they are winning, and where your site has room to compete. - -## What this skill helps your agent do - -- Build a representative query set for a market or category. -- Inspect SERPs to find recurring domains. -- Separate direct product competitors from publishers, directories, communities, marketplaces, and resource sites. -- Compare organic footprints and keyword coverage across the strongest domains. -- Identify winning themes, content formats, authority patterns, and underserved angles. - -## When to use it - -Use this skill when the market is unclear, you are entering a new category, or you do not know which SEO competitors matter. - -It also helps when business competitors are not the same as SEO competitors. In many markets, the pages winning search traffic come from publishers, directories, forums, or comparison sites rather than direct product competitors. - -## What you get back - -Your agent should return market leaders, the most winnable opportunity area, the biggest ranking barrier, the query set it used, domain types, winning themes, and recommended next workflows. - -## How to get the best result - -- Provide a category, market, seed keyword set, or your own domain. -- Share known competitors if you have them. -- Tell the agent the country or language if search behavior is market-specific. -- Ask it to label domain types clearly so business competitors and SEO competitors do not get mixed together. - -## Use it with OpenSEO MCP - -Set up [OpenSEO MCP](/docs/mcp) so your agent can research keywords, inspect SERPs, compare domains, and review backlink overview data when authority matters. - -## Read the actual skill - -The source `SKILL.md` lives on GitHub: - -- [Competitive Landscape skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/competitive-landscape) - -## Related skills - -- [Competitor Analysis](/docs/skills/competitor-analysis) for a deep dive on one domain. -- [Keyword Research](/docs/skills/keyword-research) when you need to expand opportunity themes. -- [Keyword Clustering](/docs/skills/keyword-clustering) when you are ready to turn opportunities into page targets. diff --git a/web/content/docs/skills/competitor-analysis.mdx b/web/content/docs/skills/competitor-analysis.mdx deleted file mode 100644 index 2d2989205..000000000 --- a/web/content/docs/skills/competitor-analysis.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: "Competitor Analysis Agent Skill" -description: "Analyze one competitor's keywords, pages, backlinks, content themes, and SEO gaps with your AI agent." ---- - - - -The Competitor Analysis Agent Skill studies one competitor and turns the research into strategic takeaways. - -Your agent reviews the competitor's keywords, pages, backlinks, and content themes. It looks for the patterns that explain where the competitor gets organic visibility and which parts matter for your own site. - -You get a focused readout of what to learn from, what to avoid, and where your site may have a better angle. - -## What this skill helps your agent do - -- Review a competitor's organic footprint. -- Find ranking keyword themes and page opportunities. -- Compare backlink and authority signals when relevant. -- Inspect SERPs for important head-to-head keywords. -- Separate evidence from inference. -- Recommend stronger angles instead of copying competitor pages. - -## When to use it - -Use this skill when a specific competitor keeps appearing in search results, sales conversations, or customer comparisons. - -It helps before creating comparison pages, refreshing positioning, finding keyword gaps, or deciding whether a competitor's SEO advantage comes from content, links, brand, or a small number of strong pages. - -## What you get back - -Your agent should return a competitor snapshot, the biggest lesson, the best opportunity to beat them, keyword themes, content patterns, backlink notes, SERP observations, and priority actions. - -## How to get the best result - -- Give the agent one competitor domain. -- Share your own domain if you want a comparison. -- Mention the product category or topic if the competitor is broad. -- Ask the agent to filter for business fit, not just traffic. -- Ask for evidence and recommendations separately. - -## Use it with OpenSEO MCP - -Set up [OpenSEO MCP](/docs/mcp) so your agent can inspect domain overview data, ranking keywords, SERPs, and backlink context. - -## Read the actual skill - -The source `SKILL.md` lives on GitHub: - -- [Competitor Analysis skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/competitor-analysis) - -## Related skills - -- [Competitive Landscape](/docs/skills/competitive-landscape) if you still need to identify the right competitors. -- [Keyword Clustering](/docs/skills/keyword-clustering) to turn useful competitor keywords into page groups. -- [Link Prospecting](/docs/skills/link-prospecting) if competitor backlinks reveal outreach opportunities. diff --git a/web/content/docs/skills/index.md b/web/content/docs/skills/index.md deleted file mode 100644 index f1ef48453..000000000 --- a/web/content/docs/skills/index.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: "OpenSEO Agent Skills" -description: "Add OpenSEO Agent Skills to Claude Code, Codex, or another AI agent so it can run repeatable SEO workflows with live OpenSEO data." ---- - -OpenSEO Agent Skills let you hand repeatable SEO workflows to your AI agent. - -Run a slash command when you need keyword research, clustering, competitor analysis, link prospecting, or project setup. The skill gives your agent the workflow instructions. - -You stay in charge of strategy. Your agent uses OpenSEO data and the skill instructions to research the answer, then saves the result as a report in your project. - -## Set up OpenSEO Agent Skills - -On Claude Code, the [OpenSEO plugin](/docs/claude-code-plugin) installs MCP and every skill below in one step. On Codex CLI, the [OpenSEO plugin](/docs/codex-plugin) does the same. Use the manual steps here for other agents, or if you want to pick individual skills. - -1. [Set up OpenSEO MCP](/docs/mcp). -2. [Set up OpenSEO Agent Skills](/docs/skills/setup). - -MCP connects your agent to OpenSEO data. Skills tell your agent which SEO workflow to run. - -## Start here - -- [SEO Project Setup](/docs/skills/seo-project-setup): save your goals, positioning, competitors, and key pages to your project context, so every other skill reuses them. -- [SEO Coach](/docs/skills/seo-coach): choose the next workflow when you are new to SEO or unsure what to run first. - -## Audit workflows - -- [SEO Audit](/docs/skills/seo-audit): audit a site and get a one-page, plain-language report built around a single next action. - -## Research workflows - -- [Keyword Research](/docs/skills/keyword-research): find keywords worth targeting and explain why they fit. -- [Keyword Clustering](/docs/skills/keyword-clustering): turn keyword lists into page groups, content priorities, and cannibalization checks. -- [Competitive Landscape](/docs/skills/competitive-landscape): map who is winning across a market and where your openings are. -- [Competitor Analysis](/docs/skills/competitor-analysis): analyze one competitor and turn the research into strategic takeaways. -- [Local SEO](/docs/skills/local-seo): audit a Google Business Profile, compare it to local competitors, and map Maps visibility around a location. - -## Promotion workflows - -- [Link Prospecting](/docs/skills/link-prospecting): find qualified outreach prospects and the angle that makes each one relevant. - -## How the results reach you - -- [SEO Report](/docs/skills/seo-report): the shared report-writing skill every workflow above delivers through. Your agent writes one self-contained HTML page and saves it to your project's Reports page, where anyone can read or print it. - -## Learn more about skills - -OpenSEO uses the same `SKILL.md` pattern supported by modern AI agents. To learn how skills work in your agent, read: - -- [Claude Code skills documentation](https://docs.claude.com/en/docs/claude-code/skills) -- [OpenAI Skills documentation](https://help.openai.com/en/articles/20001066-skills-in-chatgpt) - -## Read the actual skills - -These pages explain what each skill is for. The source instructions live in GitHub: - -- [OpenSEO Agent Skills on GitHub](https://github.com/every-app/open-seo/tree/main/.agents/skills) diff --git a/web/content/docs/skills/keyword-clustering.mdx b/web/content/docs/skills/keyword-clustering.mdx deleted file mode 100644 index 296ebcb09..000000000 --- a/web/content/docs/skills/keyword-clustering.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "Keyword Clustering Agent Skill" -description: "Cluster keywords by intent and map them to existing or proposed pages with your AI agent." ---- - - - -The Keyword Clustering Agent Skill turns a messy keyword list into a page plan. - -Your agent sorts keywords, compares intent, and decides which terms belong on the same page. It uses the keyword set, SERP context, and your site context to recommend page groups. - -You get a content map: which keywords belong together, which pages should target them, where existing pages fit, and where cannibalization might become a problem. - -## What this skill helps your agent do - -- Group keywords by intent, SERP similarity, and page type. -- Separate terms that look similar but need different pages. -- Map clusters to existing URLs when they fit. -- Recommend new pages when no current page matches the search intent. -- Flag weak, off-strategy, or do-not-target keywords. -- Suggest saved keyword tags only after user confirmation. - -## When to use it - -Use this skill when you already have keywords but do not yet have a content plan. - -It helps when a keyword export is too large to sort by hand, when multiple pages might compete for the same intent, or when you need to turn research into page briefs. - -## What you get back - -Your agent should return clusters, primary keywords, secondary keywords, search intent, page targets, priorities, and notes about cannibalization or consolidation. - -## How to get the best result - -- Provide a keyword list, saved keyword tag, Search Console export, seed topic, or target domain. -- Share existing pages if you want the agent to map keywords to current URLs. -- Ask the agent to check SERP overlap for important borderline terms. -- Keep final tag changes user-approved. - -## Use it with OpenSEO MCP - -Set up [OpenSEO MCP](/docs/mcp) so your agent can read saved keywords, inspect SERPs, and use OpenSEO project context while clustering. - -## Read the actual skill - -The source `SKILL.md` lives on GitHub: - -- [Keyword Clustering skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/keyword-clustering) - -## Related skills - -- [Keyword Research](/docs/skills/keyword-research) when you need more candidate terms. -- [Competitor Analysis](/docs/skills/competitor-analysis) when competitor pages should inform the map. -- [SEO Coach](/docs/skills/seo-coach) if you are unsure whether clustering is the next step. diff --git a/web/content/docs/skills/keyword-research.mdx b/web/content/docs/skills/keyword-research.mdx deleted file mode 100644 index 8ebd1a7f9..000000000 --- a/web/content/docs/skills/keyword-research.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "Keyword Research Agent Skill" -description: "Find keyword opportunities from live SEO data with Claude Code, Codex, or another AI agent." ---- - - - -The Keyword Research Agent Skill finds keywords your site can target. - -Your agent pulls keyword ideas, checks difficulty, inspects SERPs, and filters terms against your company, website, and goals. - -You get a prioritized set of opportunities, with notes on why each keyword fits, how hard it may be to rank, and what to do next. - -## What this skill helps your agent do - -- Research keywords from seed topics, products, pages, competitors, or audience problems. -- Evaluate volume, difficulty, CPC, intent, and SERP competitiveness. -- Filter out irrelevant, duplicate, branded-only, or off-strategy terms. -- Decide which keywords are worth targeting now, saving for later, or sending into clustering. -- Ask before saving keywords back to OpenSEO. - -## When to use it - -Use this skill when you have a topic or market but do not yet know which searches are worth pursuing. - -It helps before writing new pages, planning a content roadmap, refreshing a landing page, or deciding whether a topic has enough search demand to matter. - -## What you get back - -Your agent should return a prioritized keyword shortlist, a longer opportunity table, SERP caveats, and a recommended next action. A good result explains why a keyword is useful, not just whether it has search volume. - -## How to get the best result - -- Give the agent one to five focused seed topics. -- Tell it the product, audience, or page type you care about. -- Mention the target country or language when it matters. -- Ask the agent to inspect SERPs for ambiguous or high-value terms. -- Confirm before saving selected keywords to OpenSEO. - -## Use it with OpenSEO MCP - -Set up [OpenSEO MCP](/docs/mcp) so your agent can research live keyword metrics, inspect Google SERPs, check saved keywords, and save selected terms with your approval. - -## Read the actual skill - -The source `SKILL.md` lives on GitHub: - -- [Keyword Research skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/keyword-research) - -## Related skills - -- [Keyword Clustering](/docs/skills/keyword-clustering) to turn selected keywords into page groups. -- [Competitive Landscape](/docs/skills/competitive-landscape) to understand the broader market. -- [Competitor Analysis](/docs/skills/competitor-analysis) to study one competitor's ranking keywords. diff --git a/web/content/docs/skills/link-prospecting.mdx b/web/content/docs/skills/link-prospecting.mdx deleted file mode 100644 index 40483cc73..000000000 --- a/web/content/docs/skills/link-prospecting.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: "Link Prospecting Agent Skill" -description: "Find relevant link prospects from SERPs, competitor backlinks, and topical publishers with your AI agent." ---- - - - -The Link Prospecting Agent Skill finds relevant outreach prospects for a page or asset. - -Your agent searches resource pages, articles, directories, and competitor backlink patterns. It uses OpenSEO data and web research to find sites with a real reason to mention your asset. - -You get a smaller set of qualified prospects, with notes on why each one fits and what outreach angle could make sense. - -## What this skill helps your agent do - -- Build prospecting queries from your asset, topic, audience, or competitors. -- Find resource pages, listicles, comparisons, directories, publishers, and topical articles. -- Use competitor backlink patterns to understand where attention comes from. -- Qualify prospects by topical relevance and outreach angle. -- Draft outreach messages tied to the specific page and reason. -- Avoid inventing contacts or treating spammy prospects as useful. - -## When to use it - -Use this skill when you already have something worth linking to: a useful guide, product page, original study, tool, template, dataset, or strong point of view. - -It also helps after competitor analysis if backlink patterns show publications, directories, communities, or partners that matter in your market. - -## What you get back - -Your agent should return the best outreach angle, highest-priority prospect type, a prospect table, contact-discovery notes, and a few reusable outreach drafts. - -## How to get the best result - -- Share the target URL or describe the linkable asset. -- Explain why someone would reference it. -- Provide competitors when competitor backlinks are useful. -- Ask the agent to prioritize relevance over volume. -- Require source URLs for any contact details it finds. - -## Use it with OpenSEO MCP - -Set up [OpenSEO MCP](/docs/mcp) so your agent can inspect SERPs, backlink overview data, and domain context before researching contact paths. - -## Read the actual skill - -The source `SKILL.md` lives on GitHub: - -- [Link Prospecting skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/link-prospecting) - -## Related skills - -- [Competitor Analysis](/docs/skills/competitor-analysis) when competitor backlink patterns should guide prospecting. -- [Competitive Landscape](/docs/skills/competitive-landscape) when you need to understand which publications or directories matter. -- [Keyword Research](/docs/skills/keyword-research) when prospecting queries need expansion. diff --git a/web/content/docs/skills/local-seo.mdx b/web/content/docs/skills/local-seo.mdx deleted file mode 100644 index 7698c4cd3..000000000 --- a/web/content/docs/skills/local-seo.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Local SEO Agent Skill" -description: "Audit a Google Business Profile, compare it to local competitors, and map Google Maps visibility around a location with your AI agent." ---- - - - -The Local SEO Agent Skill works out why a business does or does not show up in Google Maps and the local pack near its customers, and what to fix first. - -Your agent audits the Google Business Profile, compares it to the competitors that actually outrank it nearby, reads the review gap, and runs a rank grid to show where Maps visibility drops off around the location. - -You get a profile snapshot, a competitor comparison, a visibility map, and the one fix to do this week. - -## What this skill helps your agent do - -- Audit a Google Business Profile: categories, rating, hours, photos, and claim status. -- Pull the local competitors that rank nearby and compare profiles head-to-head. -- Analyze the review gap: volume, recency, ratings, and owner replies. -- Map Maps rankings across a grid of points around the business. -- Check Q&A and posting activity when profile basics are already competitive. -- Turn the evidence into a prioritized fix list. - -## When to use it - -Use this skill when rankings depend on a physical location or service area — storefronts, restaurants, clinics, trades, and any business that lives or dies by the local pack. - -It helps when a business ranks at its storefront but not across its service area, when a competitor keeps taking the map pack, or when you need to know whether the gap is categories, reviews, or engagement. - -## What you get back - -Your agent should return a profile snapshot, where visibility drops off on the grid, a signal-by-signal comparison against the best competitor, and a prioritized action list starting with the one fix for this week. - -## How to get the best result - -- Give the agent the business name, or better, its `cid` or `placeId`. -- Share the storefront coordinate — a grid centered on the wrong place is worse than no grid. -- Use one to three keywords customers actually search, not the brand name. -- Ask for the cost before running grids larger than 3x3; every grid point is a paid search. diff --git a/web/content/docs/skills/meta.json b/web/content/docs/skills/meta.json deleted file mode 100644 index 680c171ee..000000000 --- a/web/content/docs/skills/meta.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "title": "Agent Skills", - "pages": [ - "[Overview](/docs/skills)", - "[Install Agent Skills](/docs/skills/setup)", - "[Project Setup](/docs/skills/seo-project-setup)", - "[SEO Coach](/docs/skills/seo-coach)", - "[SEO Audit](/docs/skills/seo-audit)", - "[Keyword Research](/docs/skills/keyword-research)", - "[Keyword Clustering](/docs/skills/keyword-clustering)", - "[Competitive Landscape](/docs/skills/competitive-landscape)", - "[Competitor Analysis](/docs/skills/competitor-analysis)", - "[Local SEO](/docs/skills/local-seo)", - "[Link Prospecting](/docs/skills/link-prospecting)", - "[SEO Report](/docs/skills/seo-report)" - ] -} diff --git a/web/content/docs/skills/seo-audit.mdx b/web/content/docs/skills/seo-audit.mdx deleted file mode 100644 index e6358fe0a..000000000 --- a/web/content/docs/skills/seo-audit.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: "SEO Audit Agent Skill" -description: "Audit a website with your AI agent and get a one-page, plain-language SEO report built around a single next action." ---- - - - -The SEO Audit Agent Skill audits a site and writes a one-page HTML report that anyone can act on, including someone who has never done SEO. - -Your agent runs an OpenSEO site audit, pulls backlink and traffic-estimate data, and re-checks every finding against the live pages before reporting it. The report is built around one action the site owner can take this week, with the exact message or steps included, followed by a short list of small fixes and a section on what already works. - -The skill is plain `SKILL.md` instructions, so it runs in Claude Code, Codex, or any agent that supports the skills format. It delivers through the [SEO Report skill](/docs/skills/seo-report): the finished page is saved to your OpenSEO project and appears on the project's Reports page, where anyone on your team can open it or print it to PDF. - -## What this skill helps your agent do - -- Run a site audit and read the prioritized issues. -- Check backlinks and traffic estimates to find the real bottleneck, not just tag-level nitpicks. -- Verify each finding against the live page HTML before reporting it. -- Pick one do-this-week action and include the copy-paste mechanics for it. -- Propose a starting focus area: a handful of specific, winnable keywords and what to make for each. -- Explain every technical term in plain English the first time it appears. -- Save the result to your project as a single HTML page anyone on your team can open or print. - -## When to use it - -Use this skill when the report is for someone new to SEO: a client, a founder, a friend with a podcast or a photography site. It is also a good first skill if that beginner is you, because the report explains itself. - -It handles broken sites honestly. If the domain is down or half-crawled, the agent investigates why before writing, and the recommendation changes to match: an expired certificate, a dead domain with a live successor site, or a page-level block each lead to a different "one thing". - -## What you get back - -One report on your project's Reports page: a three-sentence summary, the single action to take this week with concrete steps, five to ten small fixes with the exact evidence for each, a starting focus area with three to five keyword ideas (healthy sites only), a list of what already works, and a note on how the review was done. In chat you get the link, the verdict, and the one thing to do. - -## How to get the best result - -- Give the agent one domain. -- Say who the report is for. -- Mention anything you already know, like a site move or an old domain. -- Ask to see the verified facts if you want to check the agent's work. - -## Use it with OpenSEO MCP - -Set up [OpenSEO MCP](/docs/mcp) so your agent can run the audit and read backlink data, traffic estimates, and keyword ideas. The site audit is included on every plan (up to 50 pages on the free plan); the backlink, domain, and keyword lookups spend credits, and the skill keeps that spend modest. - -## Read the actual skill - -The source `SKILL.md` lives on GitHub: - -- [SEO Audit skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/seo-audit) - -## Related skills - -- [SEO Report](/docs/skills/seo-report), the shared skill that writes and saves the report. -- [SEO Coach](/docs/skills/seo-coach) if you are not sure this is the right workflow to run first. -- [Keyword Research](/docs/skills/keyword-research) to go deeper than the report's starting keywords. -- [Link Prospecting](/docs/skills/link-prospecting) when the report's one thing is links and you want to go beyond the first outreach batch. -- [Competitor Analysis](/docs/skills/competitor-analysis) once the basics are handled and you want to study who outranks you. diff --git a/web/content/docs/skills/seo-coach.mdx b/web/content/docs/skills/seo-coach.mdx deleted file mode 100644 index 465391b83..000000000 --- a/web/content/docs/skills/seo-coach.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: "SEO Coach Agent Skill" -description: "Learn SEO basics, choose the right workflow, and decide what to do next with your AI agent." ---- - - - -The SEO Coach Agent Skill helps you decide what to do next. - -Your agent looks at your goal, experience level, available data, and project context. It explains the options in plain language and recommends the next step. - -You get a short plan: which workflow to run now, what information the agent needs, and why that step makes sense. - -## What this skill helps your agent do - -- Explain SEO concepts without turning the conversation into a course. -- Help you choose the next OpenSEO Agent Skill based on your goal. -- Distinguish strategy questions from execution work. -- Explain when to use OpenSEO MCP, saved project context, web search, browser review, or local files. -- Keep the next step small enough to act on. - -## When to use it - -Use SEO Coach when you are asking questions like: - -- "Where should I start with SEO?" -- "Should I research keywords or study competitors first?" -- "What does OpenSEO help my agent do?" -- "How do I use AI for SEO without getting generic content?" - -If you already know the exact workflow you want, go directly to that skill. - -## What you get back - -Your agent should give you a clear explanation, a recommended next workflow, and the minimum information it needs to continue. - -## How to get the best result - -- Tell the agent your SEO experience level. -- Share the site or project you are working on. -- Say whether you want strategy, execution help, or a tool explanation. -- Ask for a recommendation if you are choosing between multiple workflows. - -## Use it with OpenSEO MCP - -The coach can help before MCP is connected, but your agent needs [OpenSEO MCP](/docs/mcp) for live keyword, SERP, domain, backlink, and saved keyword data. - -## Read the actual skill - -The source `SKILL.md` lives on GitHub: - -- [SEO Coach skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/seo-coach) - -## Related skills - -- [SEO Project Setup](/docs/skills/seo-project-setup) when you need setup and project context. -- [SEO Audit](/docs/skills/seo-audit) when you have a site and want a plain-language report on what to do first. -- [Keyword Research](/docs/skills/keyword-research) when you are ready to find opportunities. -- [Competitive Landscape](/docs/skills/competitive-landscape) when the market is unclear. diff --git a/web/content/docs/skills/seo-project-setup.mdx b/web/content/docs/skills/seo-project-setup.mdx deleted file mode 100644 index ef2b6855b..000000000 --- a/web/content/docs/skills/seo-project-setup.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: "SEO Project Setup Agent Skill" -description: "Interview once and save your site's goals, positioning, competitors, and key pages to your OpenSEO project context." ---- - - - -The SEO Project Setup Agent Skill gives your agent the context it needs before doing SEO work for a site. - -Your agent asks about the website, goals, positioning, competitors, and key pages once, then saves the answers to your project's context in OpenSEO. - -Every other skill reads that context, so does SAM in the app, and you can edit it yourself on the project's Context page, in the sidebar under AI. It carries across sessions, machines, and agents. - -## What this skill helps your agent do - -- Confirm the right OpenSEO project and read what is already saved. -- Save the basics: what the business does, the current goal, positioning, and writing preferences. -- Save competitors and the pages that matter as structured entries. -- Connect Google Search Console, or organize CSV exports if you prefer files. -- Recommend the next OpenSEO Agent Skill to run. - -## When to use it - -Use this skill when you are starting SEO work for a website, client, product, or content program. - -Re-run it whenever the goal changes, or to fill in whatever another skill left empty. - -## What you get back - -A short project summary: sites in scope, goals, positioning, competitors and key pages saved, Search Console status, what is still missing, and the workflow to run next. - -## How to get the best result - -- Share your primary domain and any important subdomains. -- Tell the agent what SEO should support: signups, leads, revenue, awareness, or recovery from a traffic drop. -- Provide positioning notes, competitor names, or customer context if you have them. -- Name the pages that actually make you money, and any studies, tools, or templates worth links. -- Mention voice preferences and words to avoid if the agent will draft content. -- Connect Search Console, or add exports when a native connection is not an option. - -## Use it with OpenSEO MCP - -Set up [OpenSEO MCP](/docs/mcp) so your agent can read and write project context and use live SEO data in later workflows. The two context tools are free. - -## Read the actual skill - -The source `SKILL.md` lives on GitHub: - -- [SEO Project Setup skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/seo-project-setup) - -## Related skills - -- [SEO Coach](/docs/skills/seo-coach) if you are new to SEO or unsure where to start. -- [SEO Audit](/docs/skills/seo-audit) once setup is done and you want a plain-language report on what to do first. -- [Keyword Research](/docs/skills/keyword-research) when you are ready to find search opportunities. -- [Competitive Landscape](/docs/skills/competitive-landscape) when you need to understand the market first. diff --git a/web/content/docs/skills/seo-report.mdx b/web/content/docs/skills/seo-report.mdx deleted file mode 100644 index deb825df7..000000000 --- a/web/content/docs/skills/seo-report.mdx +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: "SEO Report Agent Skill" -description: "The report-writing skill every OpenSEO workflow delivers through: one self-contained HTML page, saved to your project, readable and printable by anyone." ---- - -The SEO Report Agent Skill is how the other OpenSEO workflows hand you their finished work. - -You do not run it on its own. When your agent finishes an audit, a keyword study, or a competitor analysis, this skill tells it how to write the result as one self-contained HTML page and save it to your project. The page shows up on the project's Reports page in OpenSEO, where anyone on your team can open it, read it on a phone, and print it to PDF. - -## What this skill helps your agent do - -- Write one HTML page instead of a wall of chat text, using a shared starter template so every report looks the same. -- Save it to the project with `save_report`, so it is tied to the site it is about and findable tomorrow. -- Check `list_reports` first and update the existing report when it is correcting its own run, instead of leaving you with three near-identical copies. -- Keep the report self-contained: all the styling is inside the file, nothing is fetched from the web, and it prints correctly with no network at all. -- Write a short summary alongside the page, so the agent, or the next agent, can pick the work back up without re-reading the whole report. -- Reply in chat with the link, the verdict, and the top action, and leave the tables where they belong. - -## What you get back - -A report page in your project with a title that names the subject and the period, like "badseo.dev SEO audit, Sep 2026". Open it from the Reports page in the sidebar. The menu beside it exports a PDF, opens the report full screen, or opens it in its own tab. - -Reports are free. They do not spend credits, and there is no practical limit on how many a project holds. The Reports page shows the 100 most recent. - -## Sharing a report - -On openseo.so, open a report, click Share and turn on Public link. Send the link to a client or a teammate; they do not need an OpenSEO account. It always shows the latest saved version, and it is not indexed by search engines. Turn the toggle off and the link stops working, stopping new visitors within a minute. - -## How to get the best result - -- Ask for a workflow, not for a report: "audit badseo.dev" produces one on its own. -- If you want the report corrected rather than duplicated, say so, and the agent will update the same page. -- Say who the report is for. The agent puts it in the byline and pitches the language to match. -- Reports have no version history and no undo, so a correction overwrites what was there. - -## Report templates - -A template is a reusable brief for a kind of report: who it is for, which sections it has and in what order, how it should sound, and how to sign off. Create one on the Templates page, reached from the Reports page in your project. Templates belong to a project; to reuse one somewhere else, ask your agent to list it in the first project and save a copy in the second. - -Your agent sees a project's templates and follows one when you name it, or when you ask for the kind of report it describes ("write the client-ready audit summary for badseo.dev"). A plain "audit this site" still uses the skill's own format. The template replaces the sections and the tone; it never changes how the page is built, so the report still prints and still opens on a phone. - -## Read the actual skill - -The source `SKILL.md`, including the starter template, lives on GitHub: - -- [SEO Report skill](https://github.com/every-app/open-seo/tree/main/.agents/skills/seo-report) - -## Related skills - -- [SEO Audit](/docs/skills/seo-audit), the workflow most people meet this skill through. -- [Competitor Analysis](/docs/skills/competitor-analysis) and [Keyword Research](/docs/skills/keyword-research), which deliver the same way. -- [SEO Coach](/docs/skills/seo-coach) if you are not sure which workflow to run first. diff --git a/web/content/docs/skills/setup.md b/web/content/docs/skills/setup.md deleted file mode 100644 index ef71b6145..000000000 --- a/web/content/docs/skills/setup.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: "Set up OpenSEO Agent Skills" -description: "Add OpenSEO skill files to your AI agent after connecting OpenSEO MCP." ---- - -OpenSEO Agent Skills are separate files from OpenSEO MCP. - -On Claude Code, skip the steps below and use the [OpenSEO plugin](/docs/claude-code-plugin) instead — it installs MCP and every skill in one step. On Codex CLI, use the [OpenSEO plugin](/docs/codex-plugin) the same way. - -First, [set up OpenSEO MCP](/docs/mcp). MCP gives your agent access to OpenSEO data. - -Then add the OpenSEO `SKILL.md` files you want your agent to use. Each skill gives your agent one SEO workflow. - -## Choose an installation option - -Pick the option that matches how you want to install the files. - -### Option 1: Install and choose interactively - -Use this if you want the installer to show the available skills and agents. - -```bash -npx skills add every-app/open-seo -``` - -### Option 2: Install all OpenSEO skills - -Use this if you want every OpenSEO skill. - -```bash -npx skills add every-app/open-seo --skill '*' -``` - -### Option 3: Install all skills for Claude Code only - -Use this if the skills should be available in Claude Code only. - -```bash -npx skills add every-app/open-seo --skill '*' --agent claude-code -``` - -### Option 4: Install all skills for OpenAI Codex only - -Use this if the skills should be available in Codex only. - -```bash -npx skills add every-app/open-seo --skill '*' --agent codex -``` - -### Option 5: Copy the skill files manually - -Use this if you prefer to copy files into your agent's skills folder. - -```bash -git clone https://github.com/every-app/open-seo.git - -# Codex -mkdir -p ~/.codex/skills -cp -R open-seo/plugins/openseo/skills/* ~/.codex/skills/ - -# Claude Code -mkdir -p ~/.claude/skills -cp -R open-seo/plugins/openseo/skills/* ~/.claude/skills/ -``` - -You can also review the source skills on GitHub: - -- [OpenSEO Agent Skills on GitHub](https://github.com/every-app/open-seo/tree/main/.agents/skills) - -Each skill page also links to its source `SKILL.md`. - -## Update installed skills - -Use the [update prompt or commands for your installation method](/docs/agent-setup#update-your-skills). Update the OpenSEO plugin if it supplies your skills; otherwise use the installer you originally chose or update your manual copies. - -## Run a skill - -After the skill files are available to your agent, run the matching slash command: - -- `/seo-project-setup` -- `/seo-coach` -- `/keyword-research` -- `/keyword-clustering` -- `/competitive-landscape` -- `/competitor-analysis` -- `/link-prospecting` -- `/local-seo` -- `/seo-audit` - -## Next step - -Start with [SEO Project Setup](/docs/skills/seo-project-setup) if this is a new SEO project, or [SEO Coach](/docs/skills/seo-coach) if you are not sure which workflow to run first. diff --git a/web/content/legal/privacy.md b/web/content/legal/privacy.md deleted file mode 100644 index 95dc7d9b2..000000000 --- a/web/content/legal/privacy.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Privacy Policy -description: How OpenSEO collects, uses, and stores personal data. ---- - -_Last updated: August 24, 2026_ - -Every App, Inc PRIVACY POLICY - -Every App, Inc (the “Company”) is committed to maintaining robust privacy protections for its users. Our Privacy Policy (“Privacy Policy”) is designed to help you understand how we collect, use and safeguard the information you provide to us and to assist you in making informed decisions when using our Service. - -For purposes of this Agreement, “Site” refers to the Company’s website, which can be accessed at https://openseo.so/. -“Service” refers to the Company’s hosted OpenSEO product accessed via the Site. -The terms “we,” “us,” and “our” refer to the Company. -“You” refers to you, as a user of our Site or our Service. -By accessing our Site or our Service, you accept our Privacy Policy and Terms of Use (found here: [Terms and Conditions](/terms-and-conditions)), and you consent to our collection, storage, use and disclosure of your Personal Information as described in this Privacy Policy. - -If you operate or use a self-hosted deployment of OpenSEO that is not run by Every App, Inc, the operator of that deployment controls the information processed there. This Privacy Policy applies to information collected by Every App, Inc through the Site and Every App-operated hosted services. - -I. INFORMATION WE COLLECT -We collect “Non-Personal Information” and “Personal Information.” **Non-Personal Information** includes aggregate or de-identified usage information such as referring/exit pages and URLs, page visits, browser type, operating system, device type, approximate location derived from IP address, and similar analytics information. **Personal Information** includes information such as your name, email address, password, account and session information, organization information, billing or customer identifiers, and other information that you submit to us through the Site or Service. - -1\. _Information collected via Technology_ -To receive marketing communications from us, you only need to submit your email address. To use the hosted version of the Service thereafter, you may need to submit further Personal Information, such as your name, email address, password, organization information, billing or customer identifiers, and other information you choose to provide through the Service. However, in an effort to improve the quality of the Service, we track information provided to us by your browser or by our software application when you view or use the Service, such as the website you came from (known as the “referring URL”), the pages you visit, the type of browser you use, the device from which you connected to the Service, your operating system, approximate location derived from IP address, the time and date of access, and other information related to how you use the Site or Service. In the authenticated hosted Service, PostHog may also collect session replay recordings to help us diagnose bugs and improve usability; form inputs and designated sensitive text are masked before recording. - -The public Site uses privacy-friendly analytics through Plausible Analytics, which is configured as cookieless analytics and proxied through our domain. If you arrive at the Site through a referral or partner link, we also set a first-party cookie (provided by Dub) that stores a referral click identifier for up to 90 days so that signups and purchases can be attributed to the referring partner. In the hosted version of the Service, we may use cookies or similar technologies that are necessary to authenticate users, maintain secure sessions, and protect accounts. -Where cookies are used in the hosted version of the Service, the Company may use both persistent and session cookies; persistent cookies remain on your computer after you close your session and until you delete them, while session cookies expire when you close your browser. -2\. _Information you provide us by registering for an account_ -In addition to the information provided automatically by your browser when you visit the Site, to use the hosted version of the Service you may need to create a personal profile. You can create a profile by registering with the Service and entering your name, email address, and password. If you provide your email address to receive marketing communications from us without creating an account, we collect and use that email address for that purpose. By registering, subscribing, or otherwise providing your information, you are authorizing us to collect, store and use your information in accordance with this Privacy Policy. - -3\. _Children’s Privacy_ -The Site and the Service are not directed to anyone under the age of 13\. As stated in our Terms of Use, you must be at least 18 years old to use the Site or Service. The Site does not knowingly collect or solicit information from anyone under the age of 13, or allow anyone under the age of 13 to sign up for the Service. In the event that we learn that we have gathered personal information from anyone under the age of 13 without the consent of a parent or guardian, we will delete that information as soon as possible. If you believe we have collected such information, please contact us at ben@openseo.so. - -II. HOW WE USE AND SHARE INFORMATION -_Personal Information:_ -Except as otherwise stated in this Privacy Policy, we do not sell, trade, rent or otherwise share for marketing purposes your Personal Information with third parties without your consent. We do share Personal Information with vendors who are performing services for the Company, such as providers that help us send email communications, operate analytics, measure advertising conversions, process billing, provide data requested through the Service, and host or support the infrastructure behind the Site and Service. These vendors may include Loops for email communications, Plausible for public-site analytics, PostHog for hosted analytics, session replay, and error monitoring, Dub for referral link and conversion attribution, Autumn and Stripe for hosted billing and payment processing, Cloudflare for hosting, storage, and access controls, DataForSEO for data requested through the Service, and OpenRouter and OpenAI for AI model access when you use AI assistant features of the Service. Those vendors use your Personal Information only at our direction and in accordance with our Privacy Policy. -In general, the Personal Information you provide to us is used to help us communicate with you and operate the Service. For example, we use Personal Information to create and secure accounts, authenticate users, provide technical support, process billing, send administrative or transactional emails, contact users in response to questions, solicit feedback from users, and inform users about product updates and promotional offers. -We may share Personal Information with outside parties if we have a good-faith belief that access, use, preservation or disclosure of the information is reasonably necessary to meet any applicable legal process or enforceable governmental request; to enforce applicable Terms of Service, including investigation of potential violations; address fraud, security or technical concerns; or to protect against harm to the rights, property, or safety of our users or the public as required or permitted by law. - -_Non-Personal Information_ -In general, we use Non-Personal Information to help us improve the Service and customize the user experience. We also aggregate Non-Personal Information in order to track trends and analyze use patterns on the Site. This Privacy Policy does not limit in any way our use or disclosure of Non-Personal Information, and we reserve the right to use and disclose such Non-Personal Information for lawful business purposes, including with service providers and analytics providers that help us operate and improve the Site and Service. -In the event we undergo a business transaction such as a merger, acquisition by another company, or sale of all or a portion of our assets, your Personal Information may be among the assets transferred. You acknowledge and consent that such transfers may occur and are permitted by this Privacy Policy, and that any acquirer of our assets may continue to process your Personal Information as set forth in this Privacy Policy. If our information practices change at any time in the future, we will post the policy changes to the Site so that you may opt out of the new information practices. We suggest that you check the Site periodically if you are concerned about how your information is used. - -III. GOOGLE USER DATA -You may choose to connect Google services, such as Google Search Console and Google Analytics, to the hosted Service, or to sign in with your Google account. When you do, the Service accesses Google user data through Google's APIs with your authorization. This section describes how we handle that data, in addition to the other terms of this Privacy Policy. Self-hosted deployments of OpenSEO connect to Google using the operator's own credentials, and this section does not apply to them. - -_What Google user data we access._ If you sign in with Google, we receive your basic account information (name, email address, and profile picture) to create and authenticate your account. If you connect Google Search Console, we access search performance data for the sites you authorize, such as queries, pages, clicks, impressions, and average positions, as well as URL inspection results. If you connect Google Analytics, we access report data for the property you select, such as traffic and acquisition metrics, landing page and page performance, key events, ecommerce outcomes, site search terms, and audience breakdowns by device and country, along with property configuration details such as your list of properties, data streams, and measurement settings. We also receive the email address of the connected Google account so we can show you which account is linked. All Google API scopes we request for these integrations are read-only; we cannot modify data in your Google account. - -_How we use it._ We use Google user data solely to provide the user-facing features of the Service that you request: showing connection status, generating SEO reports and insights, and returning data you ask for through the Service, including through AI assistants and MCP clients that you explicitly connect. We do not use Google user data for advertising, and we do not use it to develop, improve, or train generalized AI or machine learning models. - -_How we share it._ We do not sell Google user data, and we do not transfer it to data brokers or advertisers. Google user data leaves our systems only in these cases: (a) when you connect the Service to an AI assistant or other MCP client and request data through it, the results are returned to that client, whose own privacy terms then apply; (b) when you use the in-app AI assistant, relevant data may be processed by an AI model accessed through providers such as OpenRouter or OpenAI in order to generate your response; and (c) our infrastructure provider, Cloudflare, hosts the systems that process this data on our behalf. - -_How we protect it._ The OAuth tokens that grant access to your Google account are stored encrypted at rest, and all data is transferred over encrypted connections (TLS). Google Search Console and Google Analytics report data is retrieved on demand to answer your requests and is not stored in our databases. - -_Retention and deletion._ Because report data is not stored, there is no retained copy of it to delete. While an integration is connected, we retain the OAuth tokens and connection details (such as the selected site or property and the connected account email). Disconnecting an integration in your settings removes that connection and its details immediately, and the stored tokens for the Google account are deleted once none of your projects use it. You can also revoke OpenSEO's access at any time from your Google account's security settings at https://myaccount.google.com/permissions. Deleting your OpenSEO account revokes our access with Google and erases all associated data. - -_Limited Use._ OpenSEO's use and transfer to any other app of information received from Google APIs will adhere to the [Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy), including the Limited Use requirements. - -IV. HOW WE PROTECT INFORMATION -We implement security measures designed to protect your information from unauthorized access. Your account is protected by your account password and we urge you to take steps to keep your personal information safe by not disclosing your password and by logging out of your account after each use. We further protect your information from potential security breaches by implementing certain technological security measures including encryption, firewalls and secure socket layer technology. However, these measures do not guarantee that your information will not be accessed, disclosed, altered or destroyed by breach of such firewalls and secure server software. By using our Service, you acknowledge that you understand and agree to assume these risks. If you operate a self-hosted deployment of OpenSEO, you are responsible for the security measures and privacy practices of that deployment. - -V. YOUR RIGHTS REGARDING THE USE OF YOUR PERSONAL INFORMATION -You have the right at any time to prevent us from contacting you for marketing purposes. When we send a promotional communication to a user, the user can opt out of further promotional communications by following the unsubscribe instructions provided in each promotional e-mail. You can also indicate that you do not wish to receive marketing communications from us by contacting us at ben@openseo.so. Please note that notwithstanding the promotional preferences you indicate by either unsubscribing or contacting us, we may continue to send you administrative or transactional emails including, for example, account verification, password reset, billing, security, support, and periodic updates to our Privacy Policy. - -VI. LINKS TO OTHER WEBSITES -As part of the Service, we may provide links to or compatibility with other websites or applications. However, we are not responsible for the privacy practices employed by those websites or the information or content they contain. This Privacy Policy applies solely to information collected by us through the Site and the Service. Therefore, this Privacy Policy does not apply to your use of a third party website accessed by selecting a link on our Site or via our Service. To the extent that you access or use the Service through or on another website or application, then the privacy policy of that other website or application will apply to your access or use of that site or application. We encourage our users to read the privacy statements of other websites before proceeding to use them. - -VII. CHANGES TO OUR PRIVACY POLICY -The Company reserves the right to change this policy and our Terms of Service at any time. We will notify you of significant changes to our Privacy Policy by sending a notice to the primary email address specified in your account or by placing a prominent notice on our site. Significant changes will go into effect 30 days following such notification. Non-material changes or clarifications will take effect immediately. You should periodically check the Site and this privacy page for updates. - -VIII. CONTACT US -If you have any questions regarding this Privacy Policy or the practices of this Site, please contact us by sending an email to ben@openseo.so. -Last Updated: This Privacy Policy was last updated on August 24, 2026. - -[image1]: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAH0AAAB9CAYAAACPgGwlAAAaeklEQVR4Xu1dCbglRXVGs5rERGNcEpKYaBb3qDBz730zhohRVBJDFCIJGiMak4hLEuKCog/mdr8ZljFBRVA2l4ksEWHmver7xgFBEREDJIACgoACIyA7BmGQYXL+6q57T/9d1d13mTf3vbn/953v9q06p7ZT66nqrl12WaLYY/q8X2rNmD9rRuYfW7H591aUdOT5f+X5Bnm+tRWZe+X3tmacfA/uzdjMi9tHxe3twrPP8unklznMCcYEL4k6vw6lNqPk/lacbNtuFJn/k4px7Io1G36b0zDBdkarbfaUwv9WQSk7giJzXSMyr+I0TjAk9jvjjJ+SAv6MtOb7CoXeH22R3uBK28VH5nwJbwN+0y4/uUIq0oMemfokPYH8nvac6TN+lvMwQU1IAe4rBflooXDDdJUo8FSM4xzWMGhFnb2ksnxW0nK5J84wzcy9kcOaIICpI85+vLTCgwuFGCCZiH1laiZ5NYezPdCIzctlovclTkOYOocum9n0JA5nggy7t+d/S1rTw8WCy5O05nNCkymZsf+0e9bdtXOz8nFyrijuO82o82LnrtGNK0q+wX6MZUfOPU16gVlOY4Gkx2rOzP8ey++02G3Npl+pHk87hzaj2V1ZFpACPVzkf5I+J7d13eNU2QjfuYmC/tKF6dw0VqzqPFfCugjPmscqLTIPpQrs7NWT6GG36Q2/VtlDoVJPb3wKy+40kBa3R6FQFIn/PVNHfOk3tMxUO3mm+F1h/SNjbDjT878qhXknllMi82EoFu7Cc63wrMbYqidXUjEOFOUc5v5rgFdkjrLPMfcQ5phG1Pk7eU56EvAzF4jCf0G7oQII/+2cpxyNeO4x9lgZJ08uFIIiGExgXGE56x8lR4hi78azc5f/j1i5KDkaLT9zm23NJMt60imm4uQNQaVHyfukJb/LPqvwVdq2NqP55c59eZw8B+5T0dwLnZuDpP/npQJdzXnTFBqmlhSWtzf+btlECNYzzS+Kmxa3tvsvyjpSlHqihPFjLLV67iLfnnt6umwyN6WysMQhXHMfWrzjhTGnuco83/3XEL5D3NKw0Tb/CrdsYmnDhLvjlTTENvzIfAJjezeM2KzhtXuznRzAee3mWYYTDCuaf8lAFHgWZ1jRccyPLhvdsu0m23N/3HXPCt6GF3X2S3mTS6zntm2PeeUxyc853lGgufb0x7kew8W92ycv+Zks3VtdhXCAO5TueHN+0kt58p4pPzmX+RctMuNKIZM2o5H5W8cHBYvbHe5/akTp7J22tGSLm5DJ81aEiZn61Eyn5fhLMT392KlVnT+UivLniLMZd/5Bnv8ZvQHGaeme/6Kxau7ZCJdFfZA0vAw9S/a8xbk3ZuYaab6SWedmJ3iyWkCFdG6Ij8vC0aI37mBZxJlKC8VszvFhs0Pcl82YP+i6oYXFaWuxcm2zZ0/CAylUKE4K/GQoguMchLAqkJa8bsXq+T8KVQinpGyesmXl6rknMq+04guzIclOEnvu5rscJ6jR7vyJ5ls0wIQptBRjgwUyLwXyOXTZ2j0r+JOk4M/Ta3AGJl/CcxfHM0pqYkNnJvk3jtsBQ5Dk4YfsDkC+Ea9/KnoY7W53Av1xPSIV5Z2ad+whCjiFMwLKtWSp9eI2k5eTlq26wRCm4s5LhPdjHP5CEipjc2bulZw2huTzHcJ/Rc4tTs6WMjqoxzO7K4dvKTLrtdzYwmujjszDXQZRaivrfpWYhRTEPsJ7o3dcs3JmQyHsKpK4peDPwcxfft+KdTy6z8bqud3wm8Ypa3csBdNhZoChwWzCvIGTLMPbe+Cv3eT/HXBrxnN/hWfye4DDRi+oecYKMpY9IzBGXcW8QDqbNxf4CksDEy5PmAWSuH8kiutMxXMv5TCGgYS7ApUNxiKO00eNKPkgh+GAiSSWlfJ7faj7xhKOw8SwEVpm7jDYNbKnANDNwx8Tm6Zab3fl0gw9xO6A+O1fDI8LI7myFc8/i2W3J2BQkXxdymkpUJS8jWXFfUujnay0z6uTFzn3Rtu8Hn7ufzb0FcIcK8X7WridqWbjs+uWV0RJMycHC1pc7OanIrM7h5cjWcPL71qWW0igEku+f1BImyKehbfi2ee1rFWv1wAacfICcXsAu4VSTlGPN3k/hwebhfPfoZDE/o8nce/VPKLE14mijm/hbBp4VC3XyGp8ofC6FCX/yTKDApso7DYoJG0nFNKaT3eu1WM9j9+uDSMy387CyTWA5kzyJg5rh4/x0lI/xYkSej/zAb0CMMdj/C36+5dcWL6M2kbtbObcEoeF3WoNLFMxljM/Kp6zPwjPaeg5mCfb5OEyyS1tFwwwgXJiJBMfYT4H8V8rif0PdofduRCOy1y786fMPwqgIrk42G8UwDDGeXHkWrkDNpcyv9xMXqPl6er1km9BYMem9EyYTsS6sjU2DklIBr+g3eyypRBOVjjZhsf2gI6H/UYFyetbOE+W0iVkd2MJBhp049gi1vIMkV2rw5Fe4SduUrj9ka6zcxmRBFzDbFUQuYTDSQslOZB5Q5ACvJndNHBgwdq57XzC3FyIS+chMpubMLjIOh4VlMNi4OgUu/nQiDuv5bhsfHFyIfNWQRrWxRwONn+Yb+TQXWNaWMkjzFOFkALKTK0MZ8KEeZP9NLBRw/GUEWzoHAajZU/T9jEnkIaClslxYRXCrFWADZ/DYZ6RAvbxQoQlXTojM3QUjjJjvc28ZbCbMW4l4JkgMXCyheP0kT5aVQYtw35lkPL7OscJO0VjpvMa5i2DJ4yNzDMSYGbKkWGNyXwhYBuU5W2CY3Ms8xYwPf1Y4VvTVXSY7vBNFoFs65b5u8RHnjRgZpXKdSPL5MlcKoV/KssyJJzDi7KY4NVXvM8Yhq1j5hsamIRxRMxTBl8LF7eDmS8EWKOK8kXi2bEG82piXg3fWOqhrXXPuvuWYei2ma8MLC8V7qvMMxQ84+IdOHbEfCGoZUkvke3kAOarAo4Rczg6TaVmSs8EVFPVC4ueMuiR3lCqCZyqLYRTsmxjIL3c+4RODPcNHPqzRgQVOPOUQWTXc+YGUbgDWhOHVydN6D67/DjSLPMLPSnFDhzLMGDn53jrxB2CO1aVoyj5GvOVgeXrripKwYGGdod8CBwL8lrsMDljNx9gjfKEWVnwMOFi1s1HrHAqRsKcFaWfo919wA4axwvikzI+4NgUuwF2W5fCq1MBHVp4/avPsigFjvEWAqzYCnXAgQkex2G2ZT6HJs6s1XgLRBR3XTc89fYq8zFE7kR205AwTmM3hq04Lj/qbZzGqtnfZ16N7Bz8GnZ3wEYLlfMW3/HtEEh2W5WhpxRYR1KAlQXjUEhIiQEHe/GWB91txRsgNjx6UUDCvqjOKZZhAUVraxog//9Ler8PazcNHA3DshLpLqscvski84SAFQuV9YPMUwt7TJ/1hEET4VtaMY+DXYoRb78z2XFFKzslkydzSmg48Bi+7meeEDiegTapEGEuAbH5APP4gBbHCag6i66NFjhixP6LGbnlmcy22T8HGTq57GCXZzYfMA8gfdn3+2pDHebvEvOEwHIYq5mHkRaI/Q7Mw5hssf9ihrTsM9LVj7mhTjm2PCeGmCcElqtahuaQvu+VU9zdzONDOnmjBNeY+CF8zN7T1576t0ePMzDZxP69tQhKxWZ/H7gM+cRRCLxuLzuynUN2vLgnKK2vjiHGa2zIXiacoD+07Beu8mXZVG8DhWC3aekAJ04tMV8BxV2c2ecxjw+cSPee2QSDAS9PcJkyjw94vTuvhxrHwgaJSLqwvxlEboJyaFsAqM78CGBdhFYLFsIwkxcwX7QvBlQQRwIrE/NMqH/KXojIlS3z+KiZvs/XqyyxOYl13YUwX8+RTGjxU+lEnJkntHSIdW1RsW05oUVOWJWxzjFr7zDjhJYOyZLvMtY5ZuD+Q/oTWjLEOp+M5zsB5RSOo8fMMAi9+aQLt2kc++VrCjxnX3ZTjoexJrmyNi/A4V99673MYsF8IMRVhtcde35BRgPp87mXyTA037s+f3HX/f4HH+6663LwlWldyu2zy3r83cwwCJ171a3dxDkwT5Uih1G65sd/FCIKD9h8zwOFtGilu3hRqBosw/C5V8lolPGWuQ1CTfVpNVh+il+P6JN0K//c16+vlUgN9mPSymA/0Ae/eFnXH8/O/RVrNwXlfEoH6bywDAPhs3tIRscRIl1xkQ+d/m/ecGeBvy9Sn1EtmPsGIa0UrQDm06TBfkxVStf+UJovHpYJKR3dekiG4eQ0QjJ1lK4rHCqAzlcd+SrqKd3j2S+5bhTAfwfd6pg02I+pSuloBSF/B9cqHYWUXlZpHTg9GiGZukoLgfkGoZEq3QETKd9/H2mwHxMXMhPG7JC/A0/MfErX3SvmKKGw8Owmjdrd/ffJ+MC8IJ5XAHrSOAyNTOl63HGzSw3md1SHx9EolM7dvlY6I1TIDnh2+WYlhWR8YF5HuvKVNZx+yb4k6r7KMAz5Jm48k/aRBvsxVSldL9XYz6FOS9ddu2/G78D/NUIydbt3luNhaRjCLtwu+NoDe/RLobWxA7cwRxrsx1Sl9FGO6b5KzGHxf42QzKBKZ/dhCGcf8HWjf2KPfqkKvrGR5diPqUrpWlFcyUJyIaVrIwnLsLtviAjJjIPS7RE20fwxBY8+yUFbkUBlLUbLhfw1VSk9tJ7VSmGZUShdu/n8tP9YKB0vrKTfKS141CY9BrpJnKMyIwdIg/2YqpQO0utrhm/pGFJ6nXW6dtPLVfbTMj7wPMMnx+5DEY6YD6t03Zp9BVuWcA32Y6qjdJCexWswHyik9DIrns+dzcUhGR8WXunJJbDGnV/wmNDSJXyo0PfFxwktXcIHIiYtfWejyFw+9Jg+oUVGuFkSZ9sLHhNaumRn74Fviw9LZcsngPlBPkOHRp2ZrgP714mD96uroGf8ZeBwy8omZPMfHZl1u0zF5u+LHsOTzpjLCBc4y/iWUFWbGY4YbHL1xcFg5VRhFEr3pauscg9L9uaJwluqIyKf0h05sIxP6ZofYBmQ79QOfpmP42C/EOmKB2sd+4M02I/Jp3SQszGwZXOUhFel0qswPZ7DUh2lc2scVOlaKc5CGNqOHGela0MXl82oqPuuO3uMguoonS14gyo9dGqH+TgO9gvRQildp43LZlQ0skMUPqqjdK1YkE/pVSdZdHh8aof5OA4Gp8dRv0pncLghpet9DJYZFY210hkchqOyUzu+1lIWB6fH0UIpXe/uscyoqKt0/pLUKGhYpTu/qpMsbhz0vRzg49dxsF+I+lU6+zGFlO7L/6hJKb34HfJhqY7SuQBDmdbgePo9tbNYlB6KZxjC9we6Sm/NmDczw7BUR+ksM4jSq8DzgHFWep1zhUNS/ru8HoahKKT0sjPlwyg95M5+46x0Bz6MMirCt34XTOk++DLWr9LLKpBGKA6WCVG/SvdB81aVDYc9Ksop3Cq9+PHfoSiUMUy44Mf8oJDS9bitZ+RlZ931y5RaUeOsdF9DGCF173LtKT026zyME1oi5P2ePb5JzowTWkoU+AhkkXFCS4VY113gVAUzT2jxU259zsBnKFlgQoufyq4SsSgI4G6VKvK8JVPgmVDfVCjT9C7YAh8Ty7GOC8AnKnJCUWcv5vGBI5LZ4iHMM0F9+G5xYh4fcPtUXn81rkaxX46ke9jqXK+Jax9xhQRFeDrzTVANKf+PssJb7bkp5mMsX5P8JuuAryQLohkne+SVV++GQd9lP31dKTGB907YOld7A4Ur0XA0qh9wxOwfAssJncA8DKlUH+o+x2bdgtwJvkCQMfZk9Vz5vXbPHW0Dl30/V5NbFO5HrbjMzgHdPEeO2waYT0PG/4ccL+45Y//FDMnTx3RZsL+G7z7Xut2zDKWxlsO1HsxTCd9XJJknBFHcN/uRlQp1czexovS+a+i4Ytu2x2DG3SsHcx+zaHCZSblcxzwhsCzuc2GeWhDhq3RAUCbzhMCJaHoulss+N34L84IwtjG/ReD2p0a8/qnsNmqECtJ3sRGu0OA8WYrMvb7rvgurpri8oWiIXkw+jnq3QgXBCak7MZs64uzHS2KupsR8I3Q5PfylVXyX3RmWTyofvpWD/zjSm7p1Xsy8owYUg2HP3Yky1U6eiZVOk67iZDhLJ7sDtnLQZ9clvM11K7HvDr2he0pevkmL/TjzhNBYNfdsTpCEdybzAXiRErcVsjsDLcWFheun3HOoMjn4WpdGnbFT0nhuFh96wOOwqrFlEpsVzKthb6XEi4MeiPwJXEZ172EDcM8ryzNP30CNQ9esA626RlOjFXcO5UQJHcd8dSEFfbwnvMqMNrNLaEVB8zn3OPlC5v557e5D6BUw5qsLn8L6aVSFazqlEuJCRGYbCDw+wQDQjOaXM18IMoHZxJlrDah4fMnYE9Y2TJqYV8MNA908RGaj/o+73lmG4bs9CVTVy/ggCjqcw5FyupT5QsANza3stmZHheNQw0IKqc2JZJ4ySA2+kuXRpfdTYNg44DByJL0Ay2gU+BWFJogOzXZyAMtownKLZXzABBVrd5YXt+8zbxk88sG76QcGunSOSGhf5gsBw4RHHor/b+b1Qloyy/qo7OI55tXEvBqZwreyTJ7MxXUUH+j1tmElw7wh+G5nhDWU+UYCaWnv5cj6aamt6Y1P8bd48yPmzUEUjvvAMUsW+Vk3geqSrGex0yf+78ALmSwOtNpmT45XU9mdpI12shLHxGFDEN4HKO6b0cM0ZjqvacXzz2JZjZb3HKK5oR+F++wnUiZHM99IIRHczZEyTxV4PHVU18YMCH9i5QKzYo1UIcX4mNCiWdaHroysJNjPh5Vx8mSOKyVzMfNWgcOQyv4g82wXFCL2GF6qIIo/hMPJwvps7q6REoC/agJXNRYz4bOpHAbDTsJE4VVd6guO2viLvrMGGa1l/ipIWLdzOMyz3bBy9dwTm3RFJz5NVjamCf9ZwndLU10JjeWFZORbnBFLkVmv5X3ATJ7dfGhGs7tK+t6CNEh811A8NyIuDB399DQ4SMpuGqi8hTyl5fQ9bUhCxUAFwte4tbxGuvNmzsuHY27HVirzblfgdCVnCAljPgDjcea/QTL4qN5Fg2mzGE6WMVkx6HBGAR4TQ6bVYeCb+3Tz5OnFYEPAjJ7dHWy5UTjeGxUXAqKUt3JifC3UusskDt0hWprInco80gIuLITVo/2rllT9AJOnLK13st8waEWdvT1pd3SF5oU52/m5z7ppfwcpr09yWOL2AeZbUPi7MbNB87gM2QSrG36l0M/UNV+GjWcUw1KZlbUsbPqOf1C4gwpVY3IdNNee/jiZTH6H05qn3pnznDUTy9Ao+bHNmyi3OZO8qeu3iy2fE4th5ct2h8HX/Ujt/Yob45urzPOtG52bw/IKLVwyfKx2R5fbVHvsfjKXovfQcv2gRS2vH6CiSvwXFNOk8h8lj/i6cusnZZNzxBYsTN1ZflKbiK9MzUU5uR0JrNU5gZbwScoMosSv6pk2Ni/w24g7r5UM3uXcHewHkGizx0NbMQFj2TrAMord6qA1M/fGJp8F9NCKVZ3nsiwgeb0J/uyuJ2XS+r/G4YHKJso7BLD7BmpnwfDSWD23m167aj/eR2+tTl7UqrSGZWRt0eawUXTbgN0elvFTKug9hbgCBEOODmPZzKYn6f8AejbhvdZ3hk3c7+Aw0RuOncI1pOA/zYkG6VmydGUf79Vmc5Bzx6YIChhLQuemIbwHc7g1aUvTnuYx69J1szlM4vkX+5vuvn0GRp7qISVEnUM5rRbpyZmLkG72SuXMu91/34FIS56J8Vgi+GWLduevHQ9aI3aKcnJxvW3bdPLU2a+5467/3oJu3tf6MBkr9FZxvjdDhdPL1kZkXuWJAwdD3qPlxh7Z1V/XckZsZujQgbSwzXbZglsHCPZtWpnhhlo/gHP51vASmR9yXKMgrDgkbDMVmd05bgfh2xe8bimm/Wzvgv0BtXIBkG6OK43PbK469DG28J3uzGir3qjBRkVWuN0jwmgtU3HyBnsIQ53E1cOBD2hpmCR64uybMMvGwYkyU6+zrmGogIx9xhEqNXO3N2JhUqpWG9kxpy0cJ6juUamxRsszObGFGiUn6xYMY4+Ww0ENV5DAsiPnniaFd2NaYBifk3dq/irYVtieezoq2VQ090Ic52qs7vxOvxM/rMsxDKXn28yjcEM+oMTsPf+X6Y0gdN/uOTO7foTLIiuPvvcwxhqYtHAmVWavZH7YqMXvCukab3dudoszfRnAthDn7p4lnC/7xtlhgIqJypIp63y4SXz7YmLYiM3LdTowpNn1eZR8Sk/SHNwBSR+JzDTzLwmgheLT05xhRaexDFq3e5aC3EcK/kPye4O2zMn/uzAGagUI38OimHNaWBdHyducuwbi0+FLBfuB8N7aQs8Udfa2PJnFbCqee6kOP1Pu0eK2FuTcUTncs0NaCQp5TSlKjujnTMLiRWqBKuzL9wpCxkTPCVNuWQBaX9Zd2klj1z17lnhmQ+f5MHlEN8/uoogDnZUQ5lEcsEDXy+H3qLhkg32hzIgD+8XQR5UXGzDRQWvhwiAqHMdi4wcqSGYYspsocLMWwsjcnPlfHnqtKl27J/u7/zgG7ZaBPYshJmHJ21ER4O5apT2YEZlP+CxvUzPJqz156ZGkOZSmnQbNymvCzKZG27ye5QA7OctunBIlboSbtY1HyW12HI6LJk8HFb5n/E0naNlY/m3rFpszMBHMc6bAadqWxyJJdFXZSmDnA85wS+F6CorI3IelXJXtHDNmbOEK/wXsB0hcZ4rfQfK7HuHCDa8nSUufxxiMOFhGw87WYSAqG6YcyYpjp+vK+wVs8zW6fkvWdCqToapKMCxSJSexm9hVkvQUPBRNUAG0OjdO1yXMvqUSHOWbnA2C7HTQjFTA73NcZYR0YP3P4U3QB5o43ybdLhfucJT/asOwhInfoNu8E1QAs+cmdun67AVGTul59xPrvsU7wYiBmbNdj/PXM0ZHt8hY3vHtf08wZkArTF+XhiUveV/T7p2bU0SJp2W7eqdLa/00dsGkaz4ERhiYUvu1xS82/D8GKzaOy9/tKgAAAABJRU5ErkJggg== diff --git a/web/content/legal/terms-and-conditions.md b/web/content/legal/terms-and-conditions.md deleted file mode 100644 index 8be3c4efb..000000000 --- a/web/content/legal/terms-and-conditions.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: Terms and Conditions -description: Terms and conditions for openseo.so. ---- - -**Last revised on: 8/23/2026** - -The website located at https://openseo.so (the “**Site**”), including the hosted OpenSEO services made available through the Site, is a copyrighted work belonging to Every App, Inc (“**Company**”, “**us**”, “**our**”, and “**we**”). These Terms apply to your use of the Site and the hosted OpenSEO services made available through it. For the avoidance of doubt, these Terms do not govern any self-hosted or open-source version of OpenSEO, which is made available separately under the MIT License. Certain features of the Site may be subject to additional guidelines, terms, or rules, which will be posted on the Site in connection with such features. All such additional terms, guidelines, and rules are incorporated by reference into these Terms. - -THESE TERMS OF USE (THESE “**TERMS**”) SET FORTH THE LEGALLY BINDING TERMS AND CONDITIONS THAT GOVERN YOUR USE OF THE SITE. BY ACCESSING OR USING THE SITE, YOU ARE ACCEPTING THESE TERMS (ON BEHALF OF YOURSELF OR THE ENTITY THAT YOU REPRESENT), AND YOU REPRESENT AND WARRANT THAT YOU HAVE THE RIGHT, AUTHORITY, AND CAPACITY TO ENTER INTO THESE TERMS (ON BEHALF OF YOURSELF OR THE ENTITY THAT YOU REPRESENT). YOU MAY NOT ACCESS OR USE THE SITE OR ACCEPT THE TERMS IF YOU ARE NOT AT LEAST 18 YEARS OLD. IF YOU DO NOT AGREE WITH ALL OF THE PROVISIONS OF THESE TERMS, DO NOT ACCESS AND/OR USE THE SITE. - -**PLEASE BE AWARE THAT SECTION 10.2 CONTAINS PROVISIONS GOVERNING HOW TO RESOLVE DISPUTES BETWEEN YOU AND COMPANY. AMONG OTHER THINGS, SECTION 10.2 INCLUDES AN AGREEMENT TO ARBITRATE WHICH REQUIRES, WITH LIMITED EXCEPTIONS, THAT ALL DISPUTES BETWEEN YOU AND US SHALL BE RESOLVED BY BINDING AND FINAL ARBITRATION. SECTION 10.2 ALSO CONTAINS A CLASS ACTION AND JURY TRIAL WAIVER. PLEASE READ SECTION 10.2 CAREFULLY.** - -**UNLESS YOU OPT OUT OF THE AGREEMENT TO ARBITRATE WITHIN 30 DAYS: (1) YOU WILL ONLY BE PERMITTED TO PURSUE DISPUTES OR CLAIMS AND SEEK RELIEF AGAINST US ON AN INDIVIDUAL BASIS, NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY CLASS OR REPRESENTATIVE ACTION OR PROCEEDING AND YOU WAIVE YOUR RIGHT TO PARTICIPATE IN A CLASS ACTION LAWSUIT OR CLASS-WIDE ARBITRATION; AND (2) YOU ARE WAIVING YOUR RIGHT TO PURSUE DISPUTES OR CLAIMS AND SEEK RELIEF IN A COURT OF LAW AND TO HAVE A JURY TRIAL.** - -1. **Accounts** - 1. **Account Creation.** In order to use certain features of the Site, you must register for an account (“**Account**”) and provide certain information about yourself as prompted by the account registration form. You represent and warrant that: (a) all required registration information you submit is truthful and accurate; (b) you will maintain the accuracy of such information. You may delete your Account at any time, for any reason, by following the instructions on the Site. Company may suspend or terminate your Account in accordance with Section 8\. - - 2. **Account Responsibilities.** You are responsible for maintaining the confidentiality of your Account login information and are fully responsible for all activities that occur under your Account. You agree to immediately notify Company of any unauthorized use, or suspected unauthorized use of your Account or any other breach of security. Company cannot and will not be liable for any loss or damage arising from your failure to comply with the above requirements. - -2. **Access to the Site** - 1. **License.** Subject to these Terms, Company grants you a non-transferable, non-exclusive, revocable, limited license to use and access the Site and the hosted OpenSEO services for your personal use and your business purposes, including SEO work for your own websites and for your clients. You may export the reports and results you generate and use them in your own work and in client deliverables; you may not resell them as a standalone data product or use them to build a competing product or service. - - 2. **Certain Restrictions.** The rights granted to you in these Terms are subject to the following restrictions: (a) except as permitted by Section 2.1, you shall not license, sell, rent, lease, transfer, assign, distribute, host, or otherwise commercially exploit the Site, whether in whole or in part, or any content displayed on the Site; (b) you shall not modify, make derivative works of, disassemble, reverse compile or reverse engineer any part of the Site; (c) you shall not access the Site in order to build a similar or competitive website, product, or service; and (d) except as expressly stated herein, no part of the Site may be copied, reproduced, distributed, republished, downloaded, displayed, posted or transmitted in any form or by any means. Unless otherwise indicated, any future release, update, or other addition to functionality of the Site shall be subject to these Terms. All copyright and other proprietary notices on the Site (or on any content displayed on the Site) must be retained on all copies thereof. - - 3. **Modification.** Company reserves the right, at any time, to modify, suspend, or discontinue the Site (in whole or in part) with or without notice to you. You agree that Company will not be liable to you or to any third party for any modification, suspension, or discontinuation of the Site or any part thereof. - - 4. **No Support or Maintenance.** You acknowledge and agree that Company will have no obligation to provide you with any support or maintenance in connection with the Site. - - 5. **Ownership.** Excluding any User Content that you may provide (defined below), you acknowledge that all the intellectual property rights, including copyrights, patents, trade marks, and trade secrets, in the Site and its content are owned by Company or Company’s suppliers. Neither these Terms (nor your access to the Site) transfers to you or any third party any rights, title or interest in or to such intellectual property rights, except for the limited access rights expressly set forth in Section 2.1. Company and its suppliers reserve all rights not granted in these Terms. There are no implied licenses granted under these Terms. - - 6. **Feedback.** If you provide Company with any feedback or suggestions regarding the Site (“**Feedback**”), you hereby assign to Company all rights in such Feedback and agree that Company shall have the right to use and fully exploit such Feedback and related information in any manner it deems appropriate. Company will treat any Feedback you provide to Company as non-confidential and non-proprietary. You agree that you will not submit to Company any information or ideas that you consider to be confidential or proprietary. - - 7. **Subscriptions, Fees, and Refunds.** Access to certain features of the hosted OpenSEO service requires a paid subscription. Fees are billed in advance on a recurring basis and are non-refundable except as expressly provided in this Section or as required by applicable law. We offer a **30-day money-back guarantee**: if you are not satisfied with your paid subscription, you may request a full refund of your most recent subscription charge by emailing ben@openseo.so within thirty (30) days of that charge. The guarantee applies to subscription fees only and does not apply to separately purchased usage or top-up credits that have been consumed. Company reserves the right, in its sole discretion, to review each refund request and to decline a refund where it reasonably suspects fraud, abuse of the guarantee (including repeated subscribe-and-refund cycles), violation of these Terms or the Acceptable Use Policy, or other misuse of the Site or the refund policy. You may cancel your subscription at any time through your billing portal; your access will continue through the end of the then-current billing period, and no further charges will be made. - -3. **User Content** - 1. **User Content.** “**User Content**” means any and all information and content that a user submits to, or uses with, the Site (e.g., content in the user’s profile or postings). You are solely responsible for your User Content. You assume all risks associated with use of your User Content, including any reliance on its accuracy, completeness or usefulness by others, or any disclosure of your User Content that personally identifies you or any third party. You hereby represent and warrant that your User Content does not violate our Acceptable Use Policy (defined in Section 3.3). You may not represent or imply to others that your User Content is in any way provided, sponsored or endorsed by Company. Since you alone are responsible for your User Content, you may expose yourself to liability if, for example, your User Content violates the Acceptable Use Policy. Company is not obligated to backup any User Content, and your User Content may be deleted at any time without prior notice. You are solely responsible for creating and maintaining your own backup copies of your User Content if you desire. - - 2. **License.** You hereby grant (and you represent and warrant that you have the right to grant) to Company an irrevocable, nonexclusive, royalty-free and fully paid, worldwide license to reproduce, distribute, publicly display and perform, prepare derivative works of, incorporate into other works, and otherwise use and exploit your User Content, and to grant sublicenses of the foregoing rights, solely for the purposes of including your User Content in the Site. You hereby irrevocably waive (and agree to cause to be waived) any claims and assertions of moral rights or attribution with respect to your User Content. - - 3. **Acceptable Use Policy.** The following terms constitute our “**Acceptable Use Policy**”: - 1. You agree not to use the Site to collect, upload, transmit, display, or distribute any User Content (i) that violates any third-party right, including any copyright, trademark, patent, trade secret, moral right, privacy right, right of publicity, or any other intellectual property or proprietary right, (ii) that is unlawful, harassing, abusive, tortious, threatening, harmful, invasive of another’s privacy, vulgar, defamatory, false, intentionally misleading, trade libelous, pornographic, obscene, patently offensive, promotes racism, bigotry, hatred, or physical harm of any kind against any group or individual or is otherwise objectionable, (iii) that is harmful to minors in any way, or (iv) that is in violation of any law, regulation, or obligations or restrictions imposed by any third party. - - 2. In addition, you agree not to: (i) upload, transmit, or distribute to or through the Site any computer viruses, worms, or any software intended to damage or alter a computer system or data; (ii) send through the Site unsolicited or unauthorized advertising, promotional materials, junk mail, spam, chain letters, pyramid schemes, or any other form of duplicative or unsolicited messages, whether commercial or otherwise; (iii) use the Site to harvest, collect, gather or assemble information or data regarding other users, including e-mail addresses, without their consent; (iv) interfere with, disrupt, or create an undue burden on servers or networks connected to the Site, or violate the regulations, policies or procedures of such networks; (v) attempt to gain unauthorized access to the Site (or to other computer systems or networks connected to or used together with the Site), whether through password mining or any other means; (vi) harass or interfere with any other user’s use and enjoyment of the Site; or (vii) use software or automated agents or scripts to produce multiple accounts on the Site, or to generate automated searches, requests, or queries to (or to strip, scrape, or mine data from) the Site (provided, however, that we conditionally grant to the operators of public search engines revocable permission to use spiders to copy materials from the Site for the sole purpose of and solely to the extent necessary for creating publicly available searchable indices of the materials, but not caches or archives of such materials, subject to the parameters set forth in our robots.txt file). - - 4. **Enforcement.** We reserve the right (but have no obligation) to review, refuse and/or remove any User Content in our sole discretion, and to investigate and/or take appropriate action against you in our sole discretion if you violate the Acceptable Use Policy or any other provision of these Terms or otherwise create liability for us or any other person. Such action may include removing or modifying your User Content, terminating your Account in accordance with Section 8, and/or reporting you to law enforcement authorities. - -4. **Indemnification.** You agree to indemnify and hold Company (and its officers, employees, and agents) harmless, including costs and attorneys’ fees, from any claim or demand made by any third party due to or arising out of (a) your use of the Site, (b) your violation of these Terms, (c) your violation of applicable laws or regulations or (d) your User Content. Company reserves the right, at your expense, to assume the exclusive defense and control of any matter for which you are required to indemnify us, and you agree to cooperate with our defense of these claims. You agree not to settle any matter without the prior written consent of Company. Company will use reasonable efforts to notify you of any such claim, action or proceeding upon becoming aware of it. - -5. **Third-Party Links & Ads; Other Users** - 1. **Third-Party Links & Ads.** The Site may contain links to third-party websites and services, and/or display advertisements for third parties (collectively, “**Third-Party Links & Ads**”). Such Third-Party Links & Ads are not under the control of Company, and Company is not responsible for any Third-Party Links & Ads. Company provides access to these Third-Party Links & Ads only as a convenience to you, and does not review, approve, monitor, endorse, warrant, or make any representations with respect to Third-Party Links & Ads. You use all Third-Party Links & Ads at your own risk, and should apply a suitable level of caution and discretion in doing so. When you click on any of the Third-Party Links & Ads, the applicable third party’s terms and policies apply, including the third party’s privacy and data gathering practices. You should make whatever investigation you feel necessary or appropriate before proceeding with any transaction in connection with such Third-Party Links & Ads. - - 2. **Other Users.** Each Site user is solely responsible for any and all of its own User Content. Since we do not control User Content, you acknowledge and agree that we are not responsible for any User Content, whether provided by you or by others. We make no guarantees regarding the accuracy, currency, suitability, appropriateness, or quality of any User Content. Your interactions with other Site users are solely between you and such users. You agree that Company will not be responsible for any loss or damage incurred as the result of any such interactions. If there is a dispute between you and any Site user, we are under no obligation to become involved. - - 3. **Release.** You hereby release and forever discharge Company (and our officers, employees, agents, successors, and assigns) from, and hereby waive and relinquish, each and every past, present and future dispute, claim, controversy, demand, right, obligation, liability, action and cause of action of every kind and nature (including personal injuries, death, and property damage), that has arisen or arises directly or indirectly out of, or that relates directly or indirectly to, the Site (including any interactions with, or act or omission of, other Site users or any Third-Party Links & Ads). IF YOU ARE A CALIFORNIA RESIDENT, YOU HEREBY WAIVE CALIFORNIA CIVIL CODE SECTION 1542 IN CONNECTION WITH THE FOREGOING, WHICH STATES: “A GENERAL RELEASE DOES NOT EXTEND TO CLAIMS WHICH THE CREDITOR OR RELEASING PARTY DOES NOT KNOW OR SUSPECT TO EXIST IN HIS OR HER FAVOR AT THE TIME OF EXECUTING THE RELEASE, WHICH IF KNOWN BY HIM OR HER MUST HAVE MATERIALLY AFFECTED HIS OR HER SETTLEMENT WITH THE DEBTOR OR RELEASED PARTY.” - -6. **Disclaimers** - -THE SITE IS PROVIDED ON AN “AS-IS” AND “AS AVAILABLE” BASIS, AND COMPANY (AND OUR SUPPLIERS) EXPRESSLY DISCLAIM ANY AND ALL WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING ALL WARRANTIES OR CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, QUIET ENJOYMENT, ACCURACY, OR NON-INFRINGEMENT. WE (AND OUR SUPPLIERS) MAKE NO WARRANTY THAT THE SITE WILL MEET YOUR REQUIREMENTS, WILL BE AVAILABLE ON AN UNINTERRUPTED, TIMELY, SECURE, OR ERROR-FREE BASIS, OR WILL BE ACCURATE, RELIABLE, FREE OF VIRUSES OR OTHER HARMFUL CODE, COMPLETE, LEGAL, OR SAFE. IF APPLICABLE LAW REQUIRES ANY WARRANTIES WITH RESPECT TO THE SITE, ALL SUCH WARRANTIES ARE LIMITED IN DURATION TO 90 DAYS FROM THE DATE OF FIRST USE. - -SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT APPLY TO YOU. SOME JURISDICTIONS DO NOT ALLOW LIMITATIONS ON HOW LONG AN IMPLIED WARRANTY LASTS, SO THE ABOVE LIMITATION MAY NOT APPLY TO YOU. - -7. **Limitation on Liability** - -TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL COMPANY (OR OUR SUPPLIERS) BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY LOST PROFITS, LOST DATA, COSTS OF PROCUREMENT OF SUBSTITUTE PRODUCTS, OR ANY INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL OR PUNITIVE DAMAGES ARISING FROM OR RELATING TO THESE TERMS OR YOUR USE OF, OR INABILITY TO USE, THE SITE, EVEN IF COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ACCESS TO, AND USE OF, THE SITE IS AT YOUR OWN DISCRETION AND RISK, AND YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR DEVICE OR COMPUTER SYSTEM, OR LOSS OF DATA RESULTING THEREFROM. - -TO THE MAXIMUM EXTENT PERMITTED BY LAW, NOTWITHSTANDING ANYTHING TO THE CONTRARY CONTAINED HEREIN, OUR LIABILITY TO YOU FOR ANY DAMAGES ARISING FROM OR RELATED TO THESE TERMS (FOR ANY CAUSE WHATSOEVER AND REGARDLESS OF THE FORM OF THE ACTION), WILL AT ALL TIMES BE LIMITED TO A MAXIMUM OF FIFTY US DOLLARS. THE EXISTENCE OF MORE THAN ONE CLAIM WILL NOT ENLARGE THIS LIMIT. YOU AGREE THAT OUR SUPPLIERS WILL HAVE NO LIABILITY OF ANY KIND ARISING FROM OR RELATING TO THESE TERMS. - -SOME JURISDICTIONS DO NOT ALLOW THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE ABOVE LIMITATION OR EXCLUSION MAY NOT APPLY TO YOU. - -8. **Term and Termination.** Subject to this Section, these Terms will remain in full force and effect while you use the Site. We may suspend or terminate your rights to use the Site (including your Account) at any time for any reason at our sole discretion, including for any use of the Site in violation of these Terms. Upon termination of your rights under these Terms, your Account and right to access and use the Site will terminate immediately. You understand that any termination of your Account may involve deletion of your User Content associated with your Account from our live databases. Company will not have any liability whatsoever to you for any termination of your rights under these Terms, including for termination of your Account or deletion of your User Content. Even after your rights under these Terms are terminated, the following provisions of these Terms will remain in effect: Sections 2.2 through 2.6, Section 3 and Sections 4 through 10\. - -9. **Copyright Policy.** - -Company respects the intellectual property of others and asks that users of our Site do the same. In connection with our Site, we have adopted and implemented a policy respecting copyright law that provides for the removal of any infringing materials and for the termination, in appropriate circumstances, of users of our online Site who are repeat infringers of intellectual property rights, including copyrights. If you believe that one of our users is, through the use of our Site, unlawfully infringing the copyright(s) in a work, and wish to have the allegedly infringing material removed, the following information in the form of a written notification (pursuant to 17 U.S.C. § 512(c)) must be provided to our designated Copyright Agent: - -1. your physical or electronic signature; -2. identification of the copyrighted work(s) that you claim to have been infringed; -3. identification of the material on our services that you claim is infringing and that you request us to remove; -4. sufficient information to permit us to locate such material; -5. your address, telephone number, and e-mail address; -6. a statement that you have a good faith belief that use of the objectionable material is not authorized by the copyright owner, its agent, or under the law; and -7. a statement that the information in the notification is accurate, and under penalty of perjury, that you are either the owner of the copyright that has allegedly been infringed or that you are authorized to act on behalf of the copyright owner. - -Please note that, pursuant to 17 U.S.C. § 512(f), any misrepresentation of material fact (falsities) in a written notification automatically subjects the complaining party to liability for any damages, costs and attorney’s fees incurred by us in connection with the written notification and allegation of copyright infringement. - -The designated Copyright Agent for Company is: Every App, Inc -Designated Agent: Benjamin Senescu -Address of Agent: 34 Harding Street, Cambridge, MA 02141 US -Telephone: 4842529019 -Fax: n/a -Email: ben@openseo.so - -10. **General** - 1. **Changes.** These Terms are subject to occasional revision, and if we make any substantial changes, we may notify you by sending you an e-mail to the last e-mail address you provided to us (if any), and/or by prominently posting notice of the changes on our Site. You are responsible for providing us with your most current e-mail address. In the event that the last e-mail address that you have provided us is not valid, or for any reason is not capable of delivering to you the notice described above, our dispatch of the e-mail containing such notice will nonetheless constitute effective notice of the changes described in the notice. Continued use of our Site following notice of such changes shall indicate your acknowledgement of such changes and agreement to be bound by the terms and conditions of such changes. - - 2. **Dispute Resolution.** Please read the following arbitration agreement in this Section (the “**Arbitration Agreement**”) carefully. It requires you to arbitrate disputes with Company, its parent companies, subsidiaries, affiliates, successors and assigns and all of their respective officers, directors, employees, agents, and representatives (collectively, the “**Company Parties**”) and limits the manner in which you can seek relief from the Company Parties*.* - 1. **Applicability of Arbitration Agreement*.*** You agree that any dispute between you and any of the Company Parties relating in any way to the Site, the services offered on the Site (the “**Services**”) or these Terms will be resolved by binding arbitration, rather than in court, except that (1) you and the Company Parties may assert individualized claims in small claims court if the claims qualify, remain in such court and advance solely on an individual, non-class basis; and (2) you or the Company Parties may seek equitable relief in court for infringement or other misuse of intellectual property rights (such as trademarks, trade dress, domain names, trade secrets, copyrights, and patents). **This Arbitration Agreement shall survive the expiration or termination of these Terms and shall apply, without limitation, to all claims that arose or were asserted before you agreed to these Terms (in accordance with the preamble) or any prior version of these Terms.** This Arbitration Agreement does not preclude you from bringing issues to the attention of federal, state or local agencies. Such agencies can, if the law allows, seek relief against the Company Parties on your behalf. For purposes of this Arbitration Agreement, “**Dispute**” will also include disputes that arose or involve facts occurring before the existence of this or any prior versions of the Agreement as well as claims that may arise after the termination of these Terms. - - 2. **Informal Dispute Resolution.** There might be instances when a Dispute arises between you and Company. If that occurs, Company is committed to working with you to reach a reasonable resolution. You and Company agree that good faith informal efforts to resolve Disputes can result in a prompt, low‐cost and mutually beneficial outcome. You and Company therefore agree that before either party commences arbitration against the other (or initiates an action in small claims court if a party so elects), we will personally meet and confer telephonically or via videoconference, in a good faith effort to resolve informally any Dispute covered by this Arbitration Agreement (“**Informal Dispute Resolution Conference**”). If you are represented by counsel, your counsel may participate in the conference, but you will also participate in the conference. - -The party initiating a Dispute must give notice to the other party in writing of its intent to initiate an Informal Dispute Resolution Conference (“**Notice**”), which shall occur within 45 days after the other party receives such Notice, unless an extension is mutually agreed upon by the parties. Notice to Company that you intend to initiate an Informal Dispute Resolution Conference should be sent by email to: ben@openseo.so, or by regular mail to 34 Harding Street , Cambridge, Massachusetts 02141\. The Notice must include: (1) your name, telephone number, mailing address, e‐mail address associated with your account (if you have one); (2) the name, telephone number, mailing address and e‐mail address of your counsel, if any; and (3) a description of your Dispute. - -The Informal Dispute Resolution Conference shall be individualized such that a separate conference must be held each time either party initiates a Dispute, even if the same law firm or group of law firms represents multiple users in similar cases, unless all parties agree; multiple individuals initiating a Dispute cannot participate in the same Informal Dispute Resolution Conference unless all parties agree. In the time between a party receiving the Notice and the Informal Dispute Resolution Conference, nothing in this Arbitration Agreement shall prohibit the parties from engaging in informal communications to resolve the initiating party’s Dispute. Engaging in the Informal Dispute Resolution Conference is a condition precedent and requirement that must be fulfilled before commencing arbitration. The statute of limitations and any filing fee deadlines shall be tolled while the parties engage in the Informal Dispute Resolution Conference process required by this section. - -3. **Arbitration Rules and Forum.** These Terms evidence a transaction involving interstate commerce; and notwithstanding any other provision herein with respect to the applicable substantive law, the Federal Arbitration Act, 9 U.S.C. § 1 et seq., will govern the interpretation and enforcement of this Arbitration Agreement and any arbitration proceedings. If the Informal Dispute Resolution Process described above does not resolve satisfactorily within 60 days after receipt of your Notice, you and Company agree that either party shall have the right to finally resolve the Dispute through binding arbitration. The Federal Arbitration Act governs the interpretation and enforcement of this Arbitration Agreement. The arbitration will be conducted by JAMS, an established alternative dispute resolution provider. Disputes involving claims and counterclaims with an amount in controversy under $250,000, not inclusive of attorneys’ fees and interest, shall be subject to JAMS’ most current version of the Streamlined Arbitration Rules and procedures available at [http://www.jamsadr.com/rules-streamlined-arbitration/](http://www.jamsadr.com/rules-streamlined-arbitration/); all other claims shall be subject to JAMS’s most current version of the Comprehensive Arbitration Rules and Procedures, available at [http://www.jamsadr.com/rules-comprehensive-arbitration/](http://www.jamsadr.com/rules-comprehensive-arbitration/). JAMS’s rules are also available at [www.jamsadr.com](http://www.jamsadr.com/) or by calling JAMS at 800-352-5267. A party who wishes to initiate arbitration must provide the other party with a request for arbitration (the “**Request**”). The Request must include: (1) the name, telephone number, mailing address, e‐mail address of the party seeking arbitration and the account username (if applicable) as well as the email address associated with any applicable account; (2) a statement of the legal claims being asserted and the factual bases of those claims; (3) a description of the remedy sought and an accurate, good‐faith calculation of the amount in controversy in United States Dollars; (4) a statement certifying completion of the Informal Dispute Resolution process as described above; and (5) evidence that the requesting party has paid any necessary filing fees in connection with such arbitration. - -If the party requesting arbitration is represented by counsel, the Request shall also include counsel’s name, telephone number, mailing address, and email address. Such counsel must also sign the Request. By signing the Request, counsel certifies to the best of counsel’s knowledge, information, and belief, formed after an inquiry reasonable under the circumstances, that: (1) the Request is not being presented for any improper purpose, such as to harass, cause unnecessary delay, or needlessly increase the cost of dispute resolution; (2) the claims, defenses and other legal contentions are warranted by existing law or by a nonfrivolous argument for extending, modifying, or reversing existing law or for establishing new law; and (3) the factual and damages contentions have evidentiary support or, if specifically so identified, will likely have evidentiary support after a reasonable opportunity for further investigation or discovery. - -Unless you and Company otherwise agree, or the Batch Arbitration process discussed in Subsection 10.2(h) is triggered, the arbitration will be conducted in the county where you reside. Subject to the JAMS Rules, the arbitrator may direct a limited and reasonable exchange of information between the parties, consistent with the expedited nature of the arbitration. If the JAMS is not available to arbitrate, the parties will select an alternative arbitral forum. Your responsibility to pay any JAMS fees and costs will be solely as set forth in the applicable JAMS Rules. - -You and Company agree that all materials and documents exchanged during the arbitration proceedings shall be kept confidential and shall not be shared with anyone except the parties’ attorneys, accountants, or business advisors, and then subject to the condition that they agree to keep all materials and documents exchanged during the arbitration proceedings confidential. - -4. **Authority of Arbitrator.** The arbitrator shall have exclusive authority to resolve all disputes subject to arbitration hereunder including, without limitation, any dispute related to the interpretation, applicability, enforceability or formation of this Arbitration Agreement or any portion of the Arbitration Agreement, except for the following: (1) all Disputes arising out of or relating to the subsection entitled “Waiver of Class or Other Non-Individualized Relief,” including any claim that all or part of the subsection entitled “Waiver of Class or Other Non-Individualized Relief” is unenforceable, illegal, void or voidable, or that such subsection entitled “Waiver of Class or Other Non-Individualized Relief” has been breached, shall be decided by a court of competent jurisdiction and not by an arbitrator; (2) except as expressly contemplated in the subsection entitled “Batch Arbitration,” all Disputes about the payment of arbitration fees shall be decided only by a court of competent jurisdiction and not by an arbitrator; (3) all Disputes about whether either party has satisfied any condition precedent to arbitration shall be decided only by a court of competent jurisdiction and not by an arbitrator; and (4) all Disputes about which version of the Arbitration Agreement applies shall be decided only by a court of competent jurisdiction and not by an arbitrator. The arbitration proceeding will not be consolidated with any other matters or joined with any other cases or parties, except as expressly provided in the subsection entitled “Batch Arbitration.” The arbitrator shall have the authority to grant motions dispositive of all or part of any claim or dispute. The arbitrator shall have the authority to award monetary damages and to grant any non-monetary remedy or relief available to an individual party under applicable law, the arbitral forum’s rules, and these Terms (including the Arbitration Agreement). The arbitrator shall issue a written award and statement of decision describing the essential findings and conclusions on which any award (or decision not to render an award) is based, including the calculation of any damages awarded. The arbitrator shall follow the applicable law. The award of the arbitrator is final and binding upon you and us. Judgment on the arbitration award may be entered in any court having jurisdiction. - 5. **Waiver of Jury Trial.** EXCEPT AS SPECIFIED IN SECTION 10.2(A) YOU AND THE COMPANY PARTIES HEREBY WAIVE ANY CONSTITUTIONAL AND STATUTORY RIGHTS TO SUE IN COURT AND HAVE A TRIAL IN FRONT OF A JUDGE OR A JURY. You and the Company Parties are instead electing that all covered claims and disputes shall be resolved exclusively by arbitration under this Arbitration Agreement, except as specified in Section 10.2(a) above. An arbitrator can award on an individual basis the same damages and relief as a court and must follow these Terms as a court would. However, there is no judge or jury in arbitration, and court review of an arbitration award is subject to very limited review. - - 6. **Waiver of Class or Other Non-Individualized Relief.** YOU AND COMPANY AGREE THAT, EXCEPT AS SPECIFIED IN SUBSECTION 10.2(H) EACH OF US MAY BRING CLAIMS AGAINST THE OTHER ONLY ON AN INDIVIDUAL BASIS AND NOT ON A CLASS, REPRESENTATIVE, OR COLLECTIVE BASIS, AND THE PARTIES HEREBY WAIVE ALL RIGHTS TO HAVE ANY DISPUTE BE BROUGHT, HEARD, ADMINISTERED, RESOLVED, OR ARBITRATED ON A CLASS, COLLECTIVE, REPRESENTATIVE, OR MASS ACTION BASIS. ONLY INDIVIDUAL RELIEF IS AVAILABLE, AND DISPUTES OF MORE THAN ONE CUSTOMER OR USER CANNOT BE ARBITRATED OR CONSOLIDATED WITH THOSE OF ANY OTHER CUSTOMER OR USER. Subject to this Arbitration Agreement, the arbitrator may award declaratory or injunctive relief only in favor of the individual party seeking relief and only to the extent necessary to provide relief warranted by the party’s individual claim. Nothing in this paragraph is intended to, nor shall it, affect the terms and conditions under the Subsection 10.2(h) entitled “Batch Arbitration.” Notwithstanding anything to the contrary in this Arbitration Agreement, if a court decides by means of a final decision, not subject to any further appeal or recourse, that the limitations of this subsection, “Waiver of Class or Other Non-Individualized Relief,” are invalid or unenforceable as to a particular claim or request for relief (such as a request for public injunctive relief), you and Company agree that that particular claim or request for relief (and only that particular claim or request for relief) shall be severed from the arbitration and may be litigated in the state or federal courts located in the State of Massachusetts. All other Disputes shall be arbitrated or litigated in small claims court. This subsection does not prevent you or Company from participating in a class-wide settlement of claims. - - 7. **Attorneys’ Fees and Costs.** The parties shall bear their own attorneys’ fees and costs in arbitration unless the arbitrator finds that either the substance of the Dispute or the relief sought in the Request was frivolous or was brought for an improper purpose (as measured by the standards set forth in Federal Rule of Civil Procedure 11(b)). If you or Company need to invoke the authority of a court of competent jurisdiction to compel arbitration, then the party that obtains an order compelling arbitration in such action shall have the right to collect from the other party its reasonable costs, necessary disbursements, and reasonable attorneys’ fees incurred in securing an order compelling arbitration. The prevailing party in any court action relating to whether either party has satisfied any condition precedent to arbitration, including the Informal Dispute Resolution Process, is entitled to recover their reasonable costs, necessary disbursements, and reasonable attorneys’ fees and costs. - - 8. **Batch Arbitration.** To increase the efficiency of administration and resolution of arbitrations, you and Company agree that in the event that there are 100 or more individual Requests of a substantially similar nature filed against Company by or with the assistance of the same law firm, group of law firms, or organizations, within a 30 day period (or as soon as possible thereafter), the JAMS shall (1) administer the arbitration demands in batches of 100 Requests per batch (plus, to the extent there are less than 100 Requests left over after the batching described above, a final batch consisting of the remaining Requests); (2) appoint one arbitrator for each batch; and (3) provide for the resolution of each batch as a single consolidated arbitration with one set of filing and administrative fees due per side per batch, one procedural calendar, one hearing (if any) in a place to be determined by the arbitrator, and one final award (“**Batch Arbitration**”). - -All parties agree that Requests are of a “substantially similar nature” if they arise out of or relate to the same event or factual scenario and raise the same or similar legal issues and seek the same or similar relief. To the extent the parties disagree on the application of the Batch Arbitration process, the disagreeing party shall advise the JAMS, and the JAMS shall appoint a sole standing arbitrator to determine the applicability of the Batch Arbitration process (“**Administrative Arbitrator**”). In an effort to expedite resolution of any such dispute by the Administrative Arbitrator, the parties agree the Administrative Arbitrator may set forth such procedures as are necessary to resolve any disputes promptly. The Administrative Arbitrator’s fees shall be paid by Company. - -You and Company agree to cooperate in good faith with the JAMS to implement the Batch Arbitration process including the payment of single filing and administrative fees for batches of Requests, as well as any steps to minimize the time and costs of arbitration, which may include: (1) the appointment of a discovery special master to assist the arbitrator in the resolution of discovery disputes; and (2) the adoption of an expedited calendar of the arbitration proceedings. - -This Batch Arbitration provision shall in no way be interpreted as authorizing a class, collective and/or mass arbitration or action of any kind, or arbitration involving joint or consolidated claims under any circumstances, except as expressly set forth in this provision. - -9. **30-Day Right to Opt Out.** You have the right to opt out of the provisions of this Arbitration Agreement by sending a timely written notice of your decision to opt out to the following address: 34 Harding Street , Cambridge, Massachusetts 02141, or email to ben@openseo.so, within 30 days after first becoming subject to this Arbitration Agreement. Your notice must include your name and address and a clear statement that you want to opt out of this Arbitration Agreement. If you opt out of this Arbitration Agreement, all other parts of these Terms will continue to apply to you. Opting out of this Arbitration Agreement has no effect on any other arbitration agreements that you may currently have with us, or may enter into in the future with us. - 10. **Invalidity, Expiration.** Except as provided in the subsection entitled “Waiver of Class or Other Non-Individualized Relief”, if any part or parts of this Arbitration Agreement are found under the law to be invalid or unenforceable, then such specific part or parts shall be of no force and effect and shall be severed and the remainder of the Arbitration Agreement shall continue in full force and effect. You further agree that any Dispute that you have with Company as detailed in this Arbitration Agreement must be initiated via arbitration within the applicable statute of limitation for that claim or controversy, or it will be forever time barred. Likewise, you agree that all applicable statutes of limitation will apply to such arbitration in the same manner as those statutes of limitation would apply in the applicable court of competent jurisdiction. - 11. **Modification.** Notwithstanding any provision in these Terms to the contrary, we agree that if Company makes any future material change to this Arbitration Agreement, you may reject that change within 30 days of such change becoming effective by writing Company at the following address: 34 Harding Street , Cambridge, Massachusetts 02141, or email to ben@openseo.so. Unless you reject the change within 30 days of such change becoming effective by writing to Company in accordance with the foregoing, your continued use of the Site and/or Services, including the acceptance of products and services offered on the Site following the posting of changes to this Arbitration Agreement constitutes your acceptance of any such changes. Changes to this Arbitration Agreement do not provide you with a new opportunity to opt out of the Arbitration Agreement if you have previously agreed to a version of these Terms and did not validly opt out of arbitration. If you reject any change or update to this Arbitration Agreement, and you were bound by an existing agreement to arbitrate Disputes arising out of or relating in any way to your access to or use of the Services or of the Site, any communications you receive, any products sold or distributed through the Site, the Services, or these Terms, the provisions of this Arbitration Agreement as of the date you first accepted these Terms (or accepted any subsequent changes to these Terms) remain in full force and effect. Company will continue to honor any valid opt outs of the Arbitration Agreement that you made to a prior version of these Terms. - 3) **Export.** The Site may be subject to U.S. export control laws and may be subject to export or import regulations in other countries. You agree not to export, reexport, or transfer, directly or indirectly, any U.S. technical data acquired from Company, or any products utilizing such data, in violation of the United States export laws or regulations. - - 4) **Disclosures.** Company is located at the address in Section 10.8. If you are a California resident, you may report complaints to the Complaint Assistance Unit of the Division of Consumer Product of the California Department of Consumer Affairs by contacting them in writing at 400 R Street, Sacramento, CA 95814, or by telephone at (800) 952-5210. - - 5) **Electronic Communications.** The communications between you and Company use electronic means, whether you use the Site or send us emails, or whether Company posts notices on the Site or communicates with you via email. For contractual purposes, you (a) consent to receive communications from Company in an electronic form; and (b) agree that all terms and conditions, agreements, notices, disclosures, and other communications that Company provides to you electronically satisfy any legal requirement that such communications would satisfy if it were be in a hardcopy writing. The foregoing does not affect your non-waivable rights. - - 6) **Entire Terms.** These Terms constitute the entire agreement between you and us regarding the use of the Site. Our failure to exercise or enforce any right or provision of these Terms shall not operate as a waiver of such right or provision. The section titles in these Terms are for convenience only and have no legal or contractual effect. The word “including” means “including without limitation”. If any provision of these Terms is, for any reason, held to be invalid or unenforceable, the other provisions of these Terms will be unimpaired and the invalid or unenforceable provision will be deemed modified so that it is valid and enforceable to the maximum extent permitted by law. Your relationship to Company is that of an independent contractor, and neither party is an agent or partner of the other. These Terms, and your rights and obligations herein, may not be assigned, subcontracted, delegated, or otherwise transferred by you without Company’s prior written consent, and any attempted assignment, subcontract, delegation, or transfer in violation of the foregoing will be null and void. Company may freely assign these Terms. The terms and conditions set forth in these Terms shall be binding upon assignees. - - 7) **Copyright/Trademark Information.** Copyright © 2026 Every App, Inc. All rights reserved. All trademarks, logos and service marks (“**Marks**”) displayed on the Site are our property or the property of other third parties. You are not permitted to use these Marks without our prior written consent or the consent of such third party which may own the Marks. - - 8) **Contact Information:** - - Ben Senescu - - Address: - - 34 Harding Street - - Cambridge, Massachusetts 02141 - - Telephone: 4842529019 - - Email: ben@openseo.so diff --git a/web/content/marketing/google-search-console-mcp.mdx b/web/content/marketing/google-search-console-mcp.mdx deleted file mode 100644 index 265294353..000000000 --- a/web/content/marketing/google-search-console-mcp.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: "Google Search Console MCP Server: No Google Cloud Setup" -description: Connect Google Search Console to Claude, Cursor, or any MCP client — no Google Cloud project needed. Query clicks, impressions, CTR, position, and indexing. ---- - -Most Google Search Console (GSC) MCP servers are open source, but they take a lot of effort to set up. You need to create a Google Cloud project, enable an API, build an OAuth client, download a `credentials.json`, then keep a local server running. OpenSEO handles the Google connection for you. You sign up, connect your property, and start asking. - -## Setup: no Google Cloud project - -1. Sign up for OpenSEO ($10/month). -2. Go through the onboarding. When prompted, click **Connect Search Console** and pick your property. -3. Add the OpenSEO MCP endpoint to your client (steps below) and approve the OpenSEO login when prompted. - -The hosted MCP server URL is: - -```txt -https://app.openseo.so/mcp -``` - -### Claude Code - -```bash -claude mcp add --transport http --scope user openseo https://app.openseo.so/mcp -``` - -### Claude Desktop - -1. Open Customize → Connectors. -2. Click **Add** (or **+**), then choose **Add custom connector**. -3. Paste `https://app.openseo.so/mcp`. - -### Cursor - -1. Open Cursor Settings → Tools & Integrations → MCP Tools. -2. Click **New MCP Server** (this opens `mcp.json`) and add: - -```json -{ - "mcpServers": { - "openseo": { - "url": "https://app.openseo.so/mcp" - } - } -} -``` - -### Codex CLI - -```bash -codex mcp add openseo --url https://app.openseo.so/mcp -``` - -Any other MCP client works the same way: add `https://app.openseo.so/mcp` as a remote server and sign in. Full setup and troubleshooting live in the [MCP docs](/docs/mcp). - -### The DIY way (every other GSC MCP) - -1. Create a Google Cloud project -2. Enable the Search Console API -3. Configure the OAuth consent screen -4. Create OAuth client credentials -5. Download `credentials.json` -6. Install and run a local MCP server - -## Things to actually ask it - -- "Which queries am I ranking on page two for that I'm close to pushing onto page one?" -- "Show me pages where impressions went up but clicks stayed flat — probably a title or meta problem." -- "Are any of my blog posts competing with each other for the same query?" -- "Is `/pricing` indexed? If not, what's blocking it?" -- "Which queries lost the most clicks over the last 28 days?" - -## What your agent can read - -Two read-only tools over the property you connect. Both pull Google's own numbers, and neither uses credits. - -**Search performance.** Clicks, impressions, CTR, and average position for your site, sliced by query, page, country, device, and date. Pull up to 16 months of history in a single question. - -**URL inspection.** Whether a page is indexed and, if not, why: coverage status, last crawl, the canonical Google chose versus the one you declared, plus mobile and rich-result checks. Up to 10 URLs at a time. - -The agent can read and inspect. It cannot change anything in your account. - -## What it costs - -The Search Console tools use zero credits — Google doesn't charge you to read your own data, so reading it through OpenSEO never burns usage. They're included with the OpenSEO plan ($10/month, which includes monthly usage credits for the research tools), alongside the credit-based tools (keyword research, rank tracking, backlinks) that run through the same MCP endpoint. If you'd rather not pay at all, OpenSEO is open source and free to self-host — see the FAQ below. - -## OpenSEO vs. the alternatives - - - -## FAQ - -### What does it cost? - -The Search Console MCP tools are included with the $10/month OpenSEO plan (which comes with a 30-day money-back guarantee) and use zero credits. OpenSEO's other tools (keyword research, rank tracking, backlinks) use the usage credits included with the plan, through the same endpoint. Self-hosting is free. - -### Is it open source? - -Yes. OpenSEO is open source, so you can self-host the whole thing for free, including the Search Console MCP. The one-click connection here uses OpenSEO's hosted Google app. If you self-host, you bring your own Google OAuth client, the same Cloud-console step the hosted version saves you. Hosted means no setup; self-hosted means full control. - -### Is it read-only? - -Yes. OpenSEO requests read-only access (`webmasters.readonly`). Your agent can read performance data and inspect URLs, but it can't change your account. - -### Which AI clients work? - -Any MCP client, including Claude Code, Claude Desktop, Cursor, Codex, and OpenClaw. Add the OpenSEO MCP endpoint and sign in. - -### How fresh is the data? - -It's Google's own data, so the most recent few days can be incomplete. History runs back 16 months, the same as the Search Console interface. diff --git a/web/content/marketing/library/backlink-audit.mdx b/web/content/marketing/library/backlink-audit.mdx deleted file mode 100644 index 33ef7d0f4..000000000 --- a/web/content/marketing/library/backlink-audit.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: "The backlink audit: sort by first seen, then by relevance" -description: "The three newest links to a real site were from a casino domain and two link sellers, with spam scores of 45 to 70. Underneath them were the links that count. How to tell the two apart in an hour, and why relevance beats every third-party score." ---- - -## What is a backlink audit for? - -Two things, and they pull in different directions. The first is to find out which links are doing anything for the site, so you can get more like them. The second is to find out whether anything pointed at the site is a liability. Use the audit to identify links worth reading and target pages worth repairing. - -Here is what the newest links to seoarcade.com, a small SEO consultancy site, looked like on the day this was written, sorted by the date they were first seen: - -![OpenSEO backlinks table for seoarcade.com sorted by first seen: the three newest links come from a link seller, a PBN reseller and a casino domain with spam scores of 45, 60 and 70, followed by links from a marketing blog, a podcast recap and a related site with spam scores of 0 to 5](/library/link-building/backlink-profile-newest-links-openseo.png "Newest backlinks in OpenSEO") - -The three newest are from a link seller, a "premium PBN network service" and a casino domain, with spam scores of 45, 60 and 70 and anchor text that is an advert for buying links. Nobody at the site asked for them. The three below are a marketing blog citing a resource guide, a related site linking a services page, and a podcast recap. Spam scores 0 to 5, anchor text that describes the page. - -That is the whole audit in six rows. The junk arrives on its own and announces itself. The links that matter look like someone chose to make them. - -## The score is not the audit - -Every backlink tool carries an authority number for the linking domain. OpenSEO shows DataForSEO's domain rank alongside a spam score. They are useful for sorting. They are not the verdict. - -Bradley Benner, who builds links for a living and tests what works on his own sites, is blunt about what he learned: - -> it's not some stupid third party metric like DA or DR or trust flow or anything else that matters about whether a link is valuable or not. What matters is whether it's relevant. - -![A quote card reading: It's not some stupid third party metric like DA or DR or trust flow or anything else that matters about whether a link is valuable or not. What matters is whether it's relevant. Attributed to Bradley Benner of Semantic Mastery and Semantic Links, from The Unscripted SEO Interview Podcast.](/library/link-building/quote-whether-its-relevant.png "Bradley Benner on relevance over metrics") - -His episode on relevance over DR is [on the Unscripted SEO podcast](https://unscriptedseo.com/bradley-benner-on-relevance-over-dr/). The point holds in the table above: the casino domain has a domain rank of 40, higher than every legitimate link visible beneath it. Sort by rank and the worst link on the page floats to the top. - -So the audit has two passes. The first uses the scores to sort. The second reads the rows. - -## Pass one: sort and bucket - -Start with one page of the profile, using one row per referring domain. Turn off the spam filter so you can see the rows you are sorting. Then make four buckets. - -**Obvious junk.** Spam score above 30, or anchor text that is an advert, or a source domain whose name has nothing to do with anything. On the profile above, that bucket was three of the newest six and a long tail of `/all/2585/6.html`-shaped URLs on throwaway domains. You will recognise the pattern after ten rows. - -**Broken targets.** The overview for the site above reported 17 broken backlinks and 47 broken pages. Open the target URLs to check what is wrong. Restore a missing page or redirect it to a relevant replacement where one exists. This profile request returns live backlinks, so it is not a list of lost links. - -**Nofollow and syndication.** Podcast directories, aggregators, social profiles. Real, harmless, and not what you build a strategy on. Count them so they do not inflate the total in your head. - -**Everything else.** This is the bucket you audit properly. - -## Pass two: read the links that might matter - -For each row in the last bucket, the question is not "how strong is this domain" but "would a person on that page have a reason to click through." Jeremy Moser, who runs a link building agency, describes what his team looks for before the anchor text even comes up: whether the linking site is a real business with real traffic, and whether the page is about the same thing yours is. His words on where local businesses get the most from links: - -> The typical plays that we see working well for local is definitely more local publication. - -His episode is [here](https://unscriptedseo.com/jeremy-moser-it-was-always-just-good-seo/). A link from a town newspaper to a plumber is worth more than a link from a DR 70 marketing blog to the same plumber, and no score will tell you that. The row will, if you open it. - -Read the anchor and the surrounding text. Read what the linking page is about. Note whether your link is the only outbound link or one of two hundred. Twenty rows takes twenty minutes and tells you more than any composite. - -## Run this with the OpenSEO MCP - -Connect the [OpenSEO MCP](/docs/mcp). The overview costs about 50 credits for a domain and each page of 100 backlink rows about 30, so a full audit of a small site is a few hundred credits. The [link-prospecting](/docs/skills/link-prospecting) skill packages the outreach side of this. - -```text -Using the OpenSEO MCP on project [name], audit the backlinks for -[domain]: - -1. Call get_backlinks_overview with scope "domain". Report backlinks, - referring domains, rank, spam score, broken backlinks and broken - pages. - -2. Call get_backlinks_profile with scope "domain", mode - one_per_domain, hideSpam false, sorted by firstSeen descending, - page 1, 100 rows. Bucket every row for review: suspected junk - (spam score over 30 or advert-style anchor text), broken targets, - nofollow, or worth reading. Scores are sorting aids, not verdicts. - -3. For the "worth reading" bucket, list source domain, source page - title, anchor text and target page. Flag any target page that - returns an error. - -4. Start a new get_backlinks_profile request with scope "domain", - mode as_is, page 1, 100 rows, filters maxSpamScore 10 and - minDomainRank 20. In this sample, list domains linking to more - than one distinct target page. Say that other links may be absent - from the sample. - -5. Give me two lists: broken target pages to investigate for repair - or a relevant redirect, and ten linking pages to read in full. -``` - -Step 3 is the one that matters and the one an agent cannot finish for you. It can list the rows. Reading them is the audit. - -## The audit is a reading exercise - -A backlink profile is a list of decisions other people made about your site. The scores help you sort the list. They do not tell you what anyone decided or why. An hour with the rows does, and it ends with two things you can act on this week: the broken pages to investigate, and a short list of sites that already chose to link to you, which is where [more links usually come from](/library/link-building/how-to-get-backlinks). - -## Backlink audit FAQ - -### How do I do a backlink audit? - -Start with a page of backlink rows, one per referring domain, sorted by first seen, with spam filtering off. Sort them into suspected junk, broken targets, nofollow, and worth reading. Check broken pages before restoring them or choosing a relevant redirect. Read the remaining rows and decide whether a person on the source page would have a reason to click through. - -### What is a toxic backlink? - -A link from a page that exists only to sell or host links: link seller listings, PBN adverts, casino and pharmacy domains, and pages with hundreds of unrelated outbound links. They tend to have high spam scores and anchor text that reads like an advert. Google generally ignores them. - -### Is domain rank or DA a good measure of a backlink? - -It is a sorting aid, not a verdict. A spam domain can carry a higher rank than a relevant small site. Use the score to order the list, then judge each link on whether the linking page is about the same thing as yours and whether a real site published it. - -### Does OpenSEO show broken backlinks? - -The overview reports broken backlinks and broken target pages. Profile rows include broken status, dofollow or nofollow, domain rank, spam score and first-seen date. The profile request used here returns live backlinks, so it cannot list lost links. The free backlink checker shows the summary and top 15 links without an account. diff --git a/web/content/marketing/library/backlink-gap-analysis.mdx b/web/content/marketing/library/backlink-gap-analysis.mdx deleted file mode 100644 index d6434bc8f..000000000 --- a/web/content/marketing/library/backlink-gap-analysis.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: "Read a competitor's link profile before you copy it" -description: "A big backlink number usually means a small number of domains linking many times. How to read referring domains, spam score, and broken links, and find the gap that is actually worth chasing." ---- - -## Backlinks are the wrong unit - -Pull the link profile of an established competitor and the first number you see is the largest one. Here is a real profile from a national brand in the property restoration category. - -![A backlink profile panel showing 596,564 backlinks, 13,980 referring domains, 408,922 referring pages, rank 67, backlink spam score 23.0, 2,217 broken backlinks, 529 broken pages, and target spam score 0.0](/library/competitive-analysis/competitor-backlink-profile-openseo.png) - -596,564 backlinks. That is the number that goes in a slide, and it is close to meaningless on its own. - -The number underneath it is the one that matters: 13,980 referring domains. Divide the first by the second and you get roughly forty-three links per domain. Nearly every one of those 596,564 links is a repeat vote from a site that had already voted. Sitewide footers, template navigation, franchise directories, and blogroll modules generate link counts in the hundreds of thousands without generating any new endorsement. - -The realistic reading of that profile is not "they have 596,564 links I need to match." It is "they have earned links from about fourteen thousand distinct sites, and I need to understand which of those are reachable." - -Bradley Benner, who runs link building at Semantic Mastery, argues the third-party numbers deserve even less weight than that: - -> And so I totally believe 100% wholeheartedly that third-party metrics are useless. What we should be focusing on is relevance to strengthen those entity associations. - -He allows one legitimate use for them, and the ordering is the point: - -> If you want to use a third-party metric like DA, DR, Trust Flow, whatever, as kind of a secondary metric after determining that a potential link source is relevant to what it's going to be linking to, then fine. - -Relevance first, metric second. A link profile report is a shortlist generator, not a scoreboard. - -## The metric everyone chases and nobody can spend - -The other headline on that panel is rank, a 0-100 authority score. It is the number most often set as an objective, and it is the one least connected to outcomes. - -Ben Senescu, who founded OpenSEO, sees the pattern constantly among founders: - -> A lot of people want to increase their domain rating, entrepreneurs especially, because that's what everyone talks about and what companies are marketing. Like, we'll buy you all these backlinks to increase your domain rating. But is that going to get you any organic traffic? - -The question answers itself for most link campaigns. Authority scores are third-party inventions calculated from link graphs. Google does not read them. They correlate with ranking because both correlate with having a lot of good links, which means moving the score without earning the links moves nothing. - -Read the score as a rough tier, not a target. It tells you whether a competitor is in a different weight class or roughly your own, which is a genuinely useful thing to know before you decide whether to compete on links at all. - -## The two numbers nobody reads, and the opportunity in them - -Look at the bottom half of that same panel. Backlink spam score of 23.0. Broken backlinks: 2,217. Broken pages: 529. - -**Spam score** describes the neighbourhood a site's links come from. A profile in the low twenties is normal for a large brand that has accumulated directory listings and scraper mentions over two decades. What it tells you is that a meaningful share of the fourteen thousand referring domains are not endorsements you would want or could use. Filter them out before you treat the remainder as a target list. - -**Broken backlinks and broken pages** are the interesting ones. 2,217 live links point at URLs on that domain that no longer resolve, spread across 529 dead pages. Every one of those is a site that decided this topic was worth linking to, whose link now goes nowhere. - -That is the most reachable link opportunity on the board, and it does not require you to out-produce anybody. You find the dead page, you find who still links to it, you check whether you have something that genuinely serves the same purpose, and you tell them. It works because you are not asking for a favour. You are reporting a broken link on their site and offering the fix. - -It is also worth running this check against your own domain first. Sites that have been rebuilt more than once frequently discover a double-digit percentage of their own earned links landing on 404s, which is link equity you already paid for and can recover with redirects alone. - -## How to run a link gap in OpenSEO - -As with the keyword gap, OpenSEO does not ship a single "link gap" report that diffs two domains. You pull the [backlink profile](/features/backlink-checker) for each side and let the agent do the comparison, which lets you set the quality filters yourself rather than inheriting somebody's defaults. - -```text -Using the OpenSEO MCP, compare the link profiles of -[mydomain.com] and [competitor.com]. - -1. Pull the backlinks overview for both. Report backlinks, - referring domains, rank, and spam score side by side. Compute - links per referring domain for each, and tell me what that - ratio suggests about how their links were acquired. - -2. Pull their backlink profile grouped one per domain, filtered - to dofollow links with a low spam score. This is the - shortlist, not the full list. - -3. Cross-reference against my referring domains. Return the sites - linking to them and not to me, sorted by domain authority, and - for each one show which of their pages earned the link. - -4. Separately, pull their broken backlinks: live links pointing - at URLs on their domain that no longer resolve. For each, - show the linking site and the dead target. - -5. Run step 4 against my own domain too, and tell me how much of - my own earned link equity currently lands on a 404. - -For the step 3 list, judge each source on topical relevance to -my business first. Tell me which ones you would not approach and -why. -``` - -Step five is the one to run before any of the outreach ones. Recovering links you already earned is cheaper than earning new ones, and it is usually a redirect rather than a campaign. - -## What to do first - -Do not start with the competitor. Start with step five on your own domain. Then, on the competitor side, ignore the link total entirely and read three things: referring domains, links per domain, and broken backlinks. - -If their links-per-domain ratio is high, their advantage is thinner than the headline and their real domain count is within reach. If it is close to one, they have earned links from many distinct sites, which is a genuine moat and probably means competing on content depth rather than on links. - -Either way, the terms you would build pages for come out of [keyword gap analysis](/library/competitive-analysis/keyword-gap-analysis), and whether this domain is even the right one to study comes out of [finding your real competitors](/library/competitive-analysis/find-your-real-competitors). A link profile is the last thing to look at, not the first. - -## Competitor backlink analysis FAQ - -### How do you do a competitor backlink analysis? - -Pull their backlink overview for the headline metrics, then pull the link-level rows grouped one per referring domain so you see distinct sites rather than repeated links. Filter to relevant, low-spam, dofollow sources, then compare that list against your own referring domains. What remains is the reachable gap. - -### What is a good spam score? - -There is no universal threshold, and the score is a third-party estimate rather than a Google signal. Read it comparatively: a large old domain in the low twenties is unremarkable, while a young site with the same score has accumulated links it probably did not earn. Use it to filter a prospect list, not to judge a site. - -### Should I try to get every link my competitor has? - -No. A large share of any competitor's referring domains are directories, scrapers, and syndication that carry no value and would not accept you anyway. The reachable portion is usually a few hundred domains at most, and the relevant portion is smaller still. - -### Why does my competitor have hundreds of thousands of backlinks? - -Almost always because a small number of sites link many times each, through footers, sidebars, templates, or a franchise network. Divide backlinks by referring domains. A ratio far above one means the headline count is repetition rather than endorsement. - -### Are broken competitor backlinks worth chasing? - -They are among the best opportunities available, because the linking site has already demonstrated it will link to this topic and its link is currently broken. The approach works when you genuinely have a better replacement for the dead page, and fails when you are just asking for a swap. diff --git a/web/content/marketing/library/cluster-topical-hubs.mdx b/web/content/marketing/library/cluster-topical-hubs.mdx deleted file mode 100644 index 9d6264372..000000000 --- a/web/content/marketing/library/cluster-topical-hubs.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: "One page per intent, one hub per topic" -description: "A 500-row keyword list isn't a strategy. Keyword clustering turns it into a site structure and fixes the cannibalization that's splitting your rankings." ---- - -## What is keyword clustering? - -Keyword clustering is grouping keywords that deserve the _same page_ because they share one intent. "Keyword clustering", "keyword grouping", and "cluster keywords for seo" are one cluster, so they get one page. "Best keyword clustering tool" is a different intent, so it gets a different page. The unit of SEO is the cluster, not the individual keyword. - -## How to cluster keywords into topical hubs (without a $99/mo tool) - -1. **Start from your expanded list.** [Seeds](/library/keyword-research/seed-from-conversation) → [expansion](/library/keyword-research/long-tail-question-mining) → [intent labels](/library/keyword-research/search-intent-mapping). You want 100+ rows before clustering is worth it. -2. **Group by SERP overlap, not word similarity.** Two keywords belong together when Google already ranks the same pages for both. Word-match clustering ("keyword tool" + "keyword tool io") produces false groups. -3. **Name each cluster by its intent.** The name becomes the page's working title. If you can't name the intent in one line, the cluster is two. -4. **Arrange clusters into a hub.** The head-term cluster is the pillar page; each subtopic cluster is a spoke that links up. Internal links follow intent adjacency, not chronology. -5. **Map every cluster to one URL, and record it.** The keyword map (a simple sheet: cluster → URL → status) is the artifact that prevents future cannibalization. - -![Keyword map table showing clusters mapped to primary keywords, intent, and one target URL each, the structure that prevents keyword cannibalization](/library/keyword-research/keyword-clustering-topical-hub-openseo.png) - -## How to fix keyword cannibalization - -Cannibalization is the opposite problem: two of your pages compete for one intent, Google alternates between them, and both rank worse than either would alone. - -- **Find it:** in [Search Console](/google-search-console-mcp), group by query + page. Any query with 2+ of your URLs swapping impressions is a candidate. [Programmatic discovery](/library/keyword-research/gsc-programmatic-discovery) runs the same grouping across every page at once. -- **Fix it:** merge the weaker page into the stronger (301), or re-point the weaker page at a genuinely different intent and re-title it. -- **Prevent it:** the keyword map from step 5. No new page ships without checking which cluster it claims. - -Run this workflow with the [OpenSEO MCP](/docs/mcp): - -```text -Using the OpenSEO MCP + Search Console: pull my queries grouped by -query and page, flag queries where two of my URLs alternate, and -propose merge/redirect fixes. Then cluster my researched keywords -by shared intent and output a cluster → URL keyword map. -``` - -## Keyword clustering FAQ - -### What's the best keyword clustering tool? - -For SERP-overlap clustering at scale, paid tools exist, but for most sites, OpenSEO's research + an intent-grouping pass (the MCP prompt above) covers it. Judge tools by whether they cluster on SERP overlap; word-similarity clustering is a toy. - -### Is there a free keyword clustering tool? - -Not an unlimited one. The grouping step itself is free (the MCP prompt above does it), but it runs on researched keywords, and quality keyword data is the part that costs money everywhere. OpenSEO includes the clustering pass with research, so there's no separate clustering tool to buy; you can start for free, and paid plans start at $10/month. - -### What is a keyword mapping template? - -A sheet with one row per cluster: primary keyword, supporting keywords, intent, target URL, status. The keyword map above is the working example; copy the structure. Add a forecast column and it becomes a build order: [size each cluster](/library/keyword-research/opportunity-sizing-forecasting) before you commit the quarter. diff --git a/web/content/marketing/library/competitor-traffic-estimates.mdx b/web/content/marketing/library/competitor-traffic-estimates.mdx deleted file mode 100644 index b389c4fd1..000000000 --- a/web/content/marketing/library/competitor-traffic-estimates.mdx +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: "How accurate are competitor traffic estimates?" -description: "A domain overview reports an estimate, not a measurement. What inflates the headline number, how close-variant stacking works, and how to read a competitor's footprint without being fooled by its size." ---- - -## What a domain overview actually measures - -Every competitor traffic number you have ever seen is modelled. Nobody outside a company can see its analytics. What a tool does instead is find the keywords a domain ranks for, look up each keyword's estimated search volume, apply an assumed click-through rate for the position held, and add it all up. - -That method is genuinely useful and it is wrong in specific, knowable directions. If you understand the directions, the estimate becomes a good instrument. If you treat it as a measurement, it will lead you into fights you cannot win and away from ones you can. - -Melissa Popp of the agency Rickety Roo points at the largest blind spot, which no volume model can see at all: - -> And then you look at where traffic is actually coming from from competitors, and it's not going to their website. You know, people are starting on Instagram and TikTok. - -An organic traffic estimate covers Google organic. A competitor's real demand may be arriving somewhere the estimate has no visibility into, which cuts both ways: the number can overstate their search position and understate their business. - -## Read the headline number, then read what produced it - -Here is a real domain overview for a national brand in the property restoration category. - -![A domain overview showing 3,023,467 estimated organic traffic and 49,475 organic keywords, with a top-keywords table where the highest-traffic term is a carpet cleaning query](/library/competitive-analysis/competitor-domain-overview-openseo.png) - -Three million estimated visits a month, across just under fifty thousand keywords. If you run a restoration company, that is a terrifying number. - -Now read the table under it. The single highest-traffic term is a carpet cleaning query. Second is a commercial cleaning query. Work down the list and a very large share of the estimated traffic sits on carpet cleaning, general house cleaning, air duct cleaning, and dryer vent cleaning. - -That company is not beating you at restoration by three million visits a month. It also runs a cleaning services business, and the cleaning business is where a great deal of the traffic lives. The comparable number, restoration terms only, is a fraction of the headline. - -This is the first and most important correction: **a domain-level traffic estimate aggregates every business line a domain operates.** Before you benchmark against it, filter it down to the business you are actually in. - -## Close-variant stacking, and why keyword counts inflate - -The second correction is subtler and it inflates almost every competitor estimate you will ever read. - -![A ranked-keyword table where five different mold-related keywords all show identical search volume of 74,000 and identical estimated traffic of 22,496, all pointing at the same single URL](/library/competitive-analysis/close-variant-keyword-inflation-openseo.png) - -Look at the block in the middle. Five keywords, all reported at 74,000 monthly searches, all reported at 22,496 estimated traffic, nearly all pointing at the same URL. Above it, three more terms share an identical 165,000 and 26,730, again on one URL. - -Those are not five keywords and three keywords. Search volume data derives from Google Ads, which groups close variants together and reports the group's volume against every member. So one real pool of demand gets reported five times, and if the model then adds up per-keyword traffic estimates, that pool gets counted five times too. - -The practical consequences: - -- **Organic keyword counts are inflated.** "Ranks for 49,475 keywords" includes large families of near-duplicates. -- **Estimated traffic is inflated wherever stacking is heavy**, which tends to be in commercial service categories where buyers phrase the same request many ways. -- **Two tools will disagree** because they de-duplicate differently and assume different click curves. - -Gert Mellak of Solicom AI Consulting treats that disagreement as information rather than a problem: - -> The narrower the focus, the better you can use the data, because then you can start cross connecting data from different data points. Do Semrush and Ahrefs actually agree? Very often they might not. - -Where two estimates disagree wildly, the honest reading is that the true figure is uncertain, not that one tool is right. - -## Read pages, not keywords - -The fix for both problems is the same. Stop reading the keyword tab and read the pages tab. - -A page count cannot be inflated by close variants, because the five stacked mold terms resolve to one URL. Pages also map to work: you cannot build a keyword, but you can build a page. And grouping by page reveals the shape of a competitor's advantage, which is usually the actual finding. - -Ask three questions of the page list: - -1. **How concentrated is it?** If the top ten pages hold most of the estimated traffic, their moat is ten pages deep and you can see all of it. -2. **How many of those pages are in my business?** The restoration example collapses considerably under this question. -3. **How old and how good are they?** A dated page holding position three on a term you care about is an invitation. - -Brittany Trafis of Soarion Digital has watched the size advantage matter less than it used to: - -> We are working with brands who have a quarter of the budget of their big competitors in paid advertising, but now within AI search, they're starting to show up and have a higher presence than their larger brands. - -Budget and traffic estimates measure the same thing, which is how much a company has already spent. Neither measures whether a specific page can be beaten. - -## Do it with OpenSEO - -The [domain overview tool](/features/domain-overview) returns the footprint, and the ranked-keyword rows behind it are what let you check the estimate rather than trust it. Connect the [OpenSEO MCP](/docs/mcp) and run the whole correction in one pass. - -```text -Using the OpenSEO MCP, give me an honest read on [competitor.com]. - -1. Pull the domain overview: estimated organic traffic and - organic keyword count. Report these as the headline figures. - -2. Pull their ranked keywords sorted by traffic. Find rows that - share an identical search volume and identical traffic - estimate and resolve to the same URL. Report roughly how much - of the headline traffic sits in those stacked groups. - -3. Group the keywords by ranking URL. Give me their top 20 pages - by estimated traffic. - -4. Split those pages into "in my business" and "other business - line", where my business is [describe what you actually sell]. - Total each side. - -Finish with two numbers: the headline estimate, and your best -estimate for the part of it that competes with me. Say how -confident you are and why. -``` - -That final instruction matters. An estimate presented without a confidence statement invites the reader to treat it as a measurement, which is the failure this whole page exists to prevent. - -## What to do first - -Take the competitor whose traffic number most intimidates you and run step four only. Split their footprint into your business and everything else. In most markets the number that comes back is between a third and a tenth of the headline, and the gap you were planning around was never the gap. - -Then take the terms that survive into [keyword gap analysis](/library/competitive-analysis/keyword-gap-analysis), and check whether the domains you are comparing against are even [the right competitors](/library/competitive-analysis/find-your-real-competitors) before you commit a quarter to catching them. - -## Competitor traffic estimate FAQ - -### How accurate are competitor traffic estimates? - -They are directionally useful and numerically unreliable. They are modelled from ranked keywords, estimated search volumes, and assumed click-through rates, so they inherit every error in all three. Treat an estimate as an order of magnitude and a trend, never as a figure to plan revenue against. - -### Why do different SEO tools show different traffic for the same site? - -Because each one uses a different keyword index, a different volume source, a different way of de-duplicating close variants, and a different click curve. Disagreement between tools is normal and tells you the true figure is uncertain. - -### Why do several keywords show exactly the same search volume? - -Google Ads groups close variants such as plurals, word order changes, and near-synonyms, then reports the grouped volume for every term in the group. Tools that source volume from Google Ads inherit that grouping, which is why five phrasings of one request can all report an identical number. - -### Is estimated organic traffic the same as a website traffic checker? - -No. A traffic checker tries to estimate all visits from every source. An organic traffic estimate covers Google organic search only, and says nothing about direct, email, paid, social, or referral traffic, which for many businesses is the majority. diff --git a/web/content/marketing/library/find-your-real-competitors.mdx b/web/content/marketing/library/find-your-real-competitors.mdx deleted file mode 100644 index a47058524..000000000 --- a/web/content/marketing/library/find-your-real-competitors.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: "The competitors on your battlecard are not the ones in your SERPs" -description: "The domains you lose clicks to are rarely the companies you lose deals to. How to compare a keyword set, read the list that comes back, and work out which of those results you can actually take." ---- - -## How do you find out who your SEO competitors are? - -Ask a founder to name their competitors and you get the sales answer: the three companies that show up in deal reviews. That list is real, and it is the wrong input for a content plan. Search does not rank companies. It ranks pages. The page you have to beat for a term you care about is frequently published by someone who is not in your market at all. - -The fix is to stop naming competitors and start measuring them. Take a keyword set you genuinely want to own, compare every domain that appears across those SERPs, and rank them by how much of that result space they hold. What comes back is not a competitor list. It is a list of who currently stands between you and the clicks. - -Jeremy Moser, who runs the agency uSERP, describes this as the first thing his team fixes on a new engagement: - -> A lot of folks will come in and say... these are the main core central themes or topics that we want to rank for long-term. But then when you actually go to the search results and you look and see what's there, sometimes it's just not even worth it. - -The topic list and the SERP disagree more often than anyone expects. Measuring is what tells you which one to believe. - -## Why your SERP competitors are not your business competitors - -Here is a real comparison across five terms in the property restoration category: the head terms, plus one city-modified term. - -The top result by visibility was the national brand everyone in that industry names first. No surprise. But fifth on the list was Yelp. Tenth was Home Depot. Twelfth was YouTube. - -None of those three sell restoration services. All three occupy positions a restoration company would like to hold. A directory, a big-box retailer, and a video platform were collectively taking more of that result space than most of the actual restoration firms in the comparison. - -That changes what you build. You do not out-content Yelp with a better service page. You either target the terms where a directory does not belong, or you accept that a share of that SERP is structurally unavailable and price your forecast accordingly. - -The reverse surprise is just as useful. On the city-modified term, the domain sitting at position one was a small local operator nobody would have listed. The national brand with millions of visits a month was at position three, on its own category term, in a single city. Scale did not decide that result. Relevance did. - -## The three groups every competitor list splits into - -Once you have the measured list, sort it into three buckets before you plan anything. - -1. **Direct competitors** who sell what you sell and rank for what you want. These are the ones worth a full [competitor analysis](/docs/skills/competitor-analysis), because everything they rank for is a page you could plausibly build. -2. **Structural competitors** like directories, marketplaces, review sites, and video. You will rarely displace them, but you can often join them. If Yelp holds a position on your term, being well-placed inside Yelp is a cheaper win than trying to rank above it. -3. **Accidental competitors** who rank on one term because of a single strong page, usually a guide or a glossary entry. They are the softest targets on the list. One page beat you, so one better page can take it back. - -Most competitive analysis fails because it treats all three as group one. - -There is a fourth category worth holding in mind even though no tool will surface it. Jorge Chavez, who consults on sales process at Topaz Sales Consulting, makes the point that the competitor named in a deal review is usually not the one that wins: - -> You could ask anybody in any podcast you ever do, who's your biggest competitor? I suspect nine out of ten will tell you a name of a competitor... But the real answer is no change. - -Search has the same problem. Plenty of the demand you are measuring ends in nobody buying anything. A competitor list tells you who takes the clicks. It does not tell you which of those clicks were ever going to become customers, which is why the intent filter matters as much as the visibility ranking. - -## How to run this in OpenSEO - -Comparing domains across a keyword set is one of the [competitive research tools](/docs/mcp) in the MCP. - -![The OpenSEO competitive research tool group, listing Get SERP results, Find SERP competitors, Get ranked keywords, Get domain overview, Get domain keywords, Get backlinks overview, and Get backlinks profile](/library/competitive-analysis/competitive-research-mcp-tools-openseo.png) - -Note that Find SERP competitors runs through the MCP and the agent rather than through a page in the app. That is fine, and it is arguably the better shape: the useful version of this question involves a keyword set, an exclusion list, and a judgement call about each result, which is a conversation rather than a form. - -```text -Using the OpenSEO MCP, find the SERP competitors for [mydomain.com]. - -1. Compare these keywords: [list 5-15 terms you actually want to - own, including at least one modified the way a real buyer - would modify it]. Exclude my own domain from the results. - -2. Return the domains sorted by visibility, and for each one show - which of my keywords it ranks for and at what position. - -3. Sort the list into three groups: - - direct competitors who sell what I sell - - structural results (directories, marketplaces, review sites, - video, retailers) that I am unlikely to outrank - - accidental competitors ranking on a single strong page - -4. For group three, tell me which single URL is ranking and what - it would take to beat it. - -Flag anything in the list I would not have guessed. -``` - -That last line is the one that earns its keep. The value of this workflow is not confirmation. It is the names you did not expect. - -## Choosing the keyword set you compare on - -The output is only as good as the terms you feed it, and the most common mistake is feeding it your own vocabulary. If you compare on the words your marketing site uses, you get back the domains that also use your words, which is a much smaller and friendlier world than the one you actually sell into. - -Seed the comparison from customer language instead. The same discipline that governs [seeding keywords from conversation](/library/keyword-research/seed-from-conversation) governs this: use the terms buyers type, not the terms your category page was written around. And keep at least one modified term in the set. Head terms tell you who is big. Modified terms tell you who is beatable. - -## What to do first - -Pick five terms you would be happy to rank for next quarter, run the comparison, and read the list without editing it. Then answer one question for each unexpected domain: is this a company, a platform, or a page? - -Companies go into a full competitor analysis. Platforms go into a distribution plan. Pages go into your content backlog, and they are usually the fastest wins on the board. - -From there, the next step is measuring the distance. Once you know who ranks, [keyword gap analysis](/library/competitive-analysis/keyword-gap-analysis) tells you what they hold that you do not, and [reading their domain overview honestly](/library/competitive-analysis/competitor-traffic-estimates) tells you whether their advantage is as large as it looks. - -## Finding SEO competitors FAQ - -### How do you find your competitors' websites? - -Compare a set of keywords you want to rank for and look at which domains appear across those results. That measured list is more reliable than a list assembled from memory, because it is built from the SERPs you are actually competing in rather than from the companies you meet in sales calls. - -### What are the types of competitor analysis? - -For search work, three groupings matter more than any formal framework: direct competitors selling what you sell, structural results like directories and marketplaces that own positions by category rather than by merit, and accidental competitors ranking on one strong page. Each one calls for a different response. - -### Should you target the same keywords as your competitors? - -Only where you can plausibly win and the term matches something you sell. A competitor ranking for a term is evidence of demand, not evidence that you should chase it. Terms held by directories and marketplaces are often better joined than fought, and terms held by a competitor's single strong page are usually the cheapest to take. - -### Why does a small site outrank a big brand on some terms? - -Because relevance is scored per page, not per company. A page written specifically for one query, in one city, for one intent, routinely beats a national brand's generic category page on that query. This is the single most useful thing a measured competitor list will show you. diff --git a/web/content/marketing/library/gsc-programmatic-discovery.mdx b/web/content/marketing/library/gsc-programmatic-discovery.mdx deleted file mode 100644 index 1086e8558..000000000 --- a/web/content/marketing/library/gsc-programmatic-discovery.mdx +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: "Query-mine the Search Console you already own" -description: "Your own Search Console is the only keyword database built from people who reached you. The strategy for mining it: striking-distance keywords, zero-click pages, and the queries Google won't name." ---- - -## What is programmatic keyword discovery? - -Programmatic keyword discovery means treating your own Search Console as the keyword database, then working it in bulk instead of one query at a time. Every other source estimates demand from the outside. Search Console reports demand that already found you, on the pages that already exist, with the positions you already hold. It is the only first-party keyword data you own, and most sites read maybe two screens of it a month. - -The catch is that Google hands it to you in a shape designed for spot checks rather than strategy. Working it programmatically means pulling the whole surface through an API, joining it to your rankings, and asking questions the interface will not answer. - -## Why your query report is missing most of your clicks - -Open a page report and note the click total. Then add up the clicks in the query table underneath. The second number is smaller, often dramatically so. Google anonymizes queries below a privacy threshold, so the searches that drove most of your traffic never appear by name. - -The gap is worse than most people assume. On one of my own pages earning around a hundred clicks a month, the interface named five queries. Going through the API raised that to twelve. Out of a hundred clicks, I could name twelve of them, on a page I wrote and understood. - -That is the problem the [dark query post](/blogs/dark-queries) works through in detail. This page is the repeatable strategy that comes out of it. - -Grant Simmons, who has built SEO tooling for years, adds a confidence score to his estimates: - -> Most of my tools, I add in a confidence score, because the math is just math and reasoning is just reasoning. - -He is also skeptical that the gap can be closed outright, on the grounds that you are still dealing with garbage in and garbage out. Both things hold. You cannot recover the exact hidden queries, and you can still assemble a defensible picture of them from data you already hold. - -## How to find striking-distance keywords in Search Console - -Striking-distance keywords are the terms where you rank between roughly position 11 and 30. They earn few clicks and prove Google already considers your page a credible answer. Moving one of them onto page one is cheaper than earning a new ranking from nothing, which makes this the highest-return query mine on most sites. - -The workflow runs in four steps: - -1. Pull every query for the property over the longest window available, not the default three months. -2. Filter to positions 11 to 30 with impressions above your own noise floor. -3. Group those queries by the page currently ranking, which turns a keyword list into a page-by-page work queue. -4. Sort each page's group by impressions, because impressions measure demand that Google is already showing you losing. - -![A ranked-keyword list filtered to positions 10 through 13, showing the keyword, its position, and monthly search volume for each row](/library/keyword-research/gsc-striking-distance-openseo.png) - -The list above is one site's striking-distance band, filtered to positions 10 through 13. Nothing here is a head term, and that is the point: these are queries the site already half-ranks for, several of them worth a few hundred searches a month, all of them sitting one page away from traffic. - -Step three matters more than it looks. Sorting by query treats every row as its own project. Grouping by page tells you which single URL to improve to capture a dozen of them at once, which is the same logic behind [clustering keywords into topical hubs](/library/keyword-research/cluster-topical-hubs). - -## The 16-month window most people skip - -Search Console's default view is short, and the export most people take is shorter. Grant's practical advice is to stop working from the recent slice: - -> Generally you get a lot more information when you look at the 16 months of Google Search Console data, to have an understanding of, if the article has been around for that long, all the queries that have mapped, not just the most recent queries. I think that's often forgotten. - -Sixteen months is the full retention window Google offers. Pulling all of it changes what you can see: seasonal queries that vanish from a 90-day view, terms a page ranked for before a redesign, and the slow drift of a page away from the topic it was written for. A query that appeared for six months and stopped is a signal. A 90-day export shows you an absence instead. - -## Zero-click pages and the queries worth rewriting for - -The other half of the mine runs in the opposite direction. Look for pages with heavy impressions and almost no clicks. High impressions confirm Google shows the page for real demand. Near-zero clicks means the title and description are losing the auction, or the page is answering an intent the searcher did not have. - -Sort those by impressions and read the queries attached. When the queries do not match what the page is about, you have found a page drifting off its topic, and the fix is usually a split rather than a rewrite. When the queries match and the clicks still do not come, the fix is the title. Both are cheaper than new content, and both come from data you already own. - -## Do it with OpenSEO - -Because the Search Console connection runs through OpenSEO, your agent can query it alongside your ranking data without a second integration. Connect the [OpenSEO MCP](/docs/mcp) once, or read how the [Search Console MCP](/google-search-console-mcp) fits together, then run the workflow with this prompt. - -```text -Using the OpenSEO MCP, pull Search Console query data for -[mydomain.com] over the longest window available. - -1. Striking distance -Filter to queries in positions 11-30 with more than [50] impressions. -Group them by the page that ranks, then sort each group by total -impressions. Give me the top 10 pages by recoverable impressions. - -2. Zero-click pages -List pages with high impressions and a click-through rate under 1%. -For each, show the queries driving the impressions and tell me -whether they match the page's actual topic. - -3. The gap -For my top 10 pages by clicks, compare total clicks against the sum -of clicks in the named-query table. Report the difference as a -percentage. Those are the pages where I am planning blind. - -Output as a document I can review. -``` - -The output is a work queue ordered by what it would cost to fix, rather than a keyword list ordered by a volume number that was never about your site. - -## What to do first - -Run step one against your single most important page. If the striking-distance group has more than five queries in it, that page is the next thing you should edit, and you now know which ten words to edit it around. Then widen the window to sixteen months and see what used to work. - -From here, the queries you surface feed the rest of the library. Unnamed question queries belong in [long-tail question mining](/library/keyword-research/long-tail-question-mining). Grouped page queries belong in [topical hubs](/library/keyword-research/cluster-topical-hubs). Anything with buying language belongs in [intent mapping](/library/keyword-research/search-intent-mapping) before you write a word. - -## Search Console keyword research FAQ - -### Can you use Google Search Console for keyword research? - -Yes, and it is the most reliable source you have, because it reports queries that reached your site rather than estimating a market. Its limits are that it only shows terms you already rank for, and it hides queries below a privacy threshold. Use it for expansion and validation, and use a keyword tool for the demand you have not captured yet. - -### What are striking-distance keywords? - -Queries where you rank roughly between positions 11 and 30. They sit on page two, earn few clicks, and demonstrate that Google already treats your page as a plausible answer. They are the cheapest rankings to improve on most sites. - -### How far back does Google Search Console data go? - -Sixteen months. Most exports default to a far shorter window, so pulling the full range surfaces seasonal queries and historical rankings that a 90-day view hides. - -### Why don't the clicks in the query table add up to the total? - -Google anonymizes queries that too few people searched, so they never appear by name while their clicks still count in the total. The shortfall runs from a small fraction to most of the total depending on the size of the site. Reading the same pages at page level rather than query level recovers much of the count. diff --git a/web/content/marketing/library/how-to-get-backlinks.mdx b/web/content/marketing/library/how-to-get-backlinks.mdx deleted file mode 100644 index c0645b8d4..000000000 --- a/web/content/marketing/library/how-to-get-backlinks.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: "How to get backlinks: start from the pages that already earn them" -description: "On one site a free traffic calculator has links from 17 domains and a page about park cleanups has 270 backlinks from 29. Neither was pitched. How to read your own top pages before you write an outreach email, and the four plays that earned links on tape." ---- - -## Where do backlinks come from? - -Mostly from things that already exist. Before you plan an outreach campaign, look at which of your pages have earned links without one. The backlinks tool lists them, and the list is usually a surprise. - -Here is seoarcade.com, a small SEO consultancy site, by pages that attract links: - -![OpenSEO top pages by backlinks for seoarcade.com: the homepage with 710 backlinks from 151 referring domains, a park cleanup services page with 270 from 29, two podcast service pages, then a free SEO traffic estimate calculator with 34 backlinks from 17 domains, and a partner reports page with 30 from 22](/library/link-building/top-pages-by-backlinks-openseo.png "Top pages by backlinks in OpenSEO") - -Two rows stand out once you skip the homepage. A page describing a local park-cleanup service has 270 backlinks from 29 domains, more than any article on the site. And a free calculator that gives a rough estimate of potential SEO traffic has 34 backlinks from 17 domains, more referring domains than any piece of long-form content on the site. - -Neither page was the subject of an outreach campaign. One is an event that gave local sites a reason to mention it. The other is a tool. That is the pattern across most profiles: the pages that earn links are the ones that do something for the linking site's reader, and the pages that get pitched are the ones that do something for you. - -## Four plays that earned links on tape - -Guests on the Unscripted SEO podcast have described what worked for them with enough specificity to copy. Four of them, in rough order of effort. - -### 1. Build a tool instead of a post - -Christopher Gimmer built a SaaS company on this and says it without hedging: - -> people are more willing and likely to link out to a really good tool that actually solves a problem versus a yet another blog post with a wall of text. - -![A quote card reading: People are more willing and likely to link out to a really good tool that actually solves a problem versus yet another blog post with a wall of text. Attributed to Christopher Gimmer, founder of Snappa and GoodMetrics, from The Unscripted SEO Interview Podcast.](/library/link-building/quote-link-out-to-a-tool.png "Christopher Gimmer on linkable tools") - -His episode is [on the podcast](https://unscriptedseo.com/christopher-gimmer-bootstrapped-saas-ai-overviews/). The calculator in the table above is the small version of that idea. Steven Schneider, whose agency builds calculators for clients, adds the test that keeps it honest: - -> can a client rank for this keyword and can they actually find value in the calculator? Because, you know, there's tons of calculator keywords out there. - -His episode on calculators as link assets is [here](https://unscriptedseo.com/seo-calculators-assets-steven-schneider/). A tool nobody searches for earns links from nobody. Check the demand for the tool's name before building it; [keyword research](/library/keyword-research) covers how. - -### 2. Do something in the real world and let the web report it - -The park-cleanup page did not earn 270 backlinks by being well written. The business ran cleanups, and the city, the chamber of commerce, event listings and local news mentioned them. The site's owner described the mechanism on air: - -> we got a few links from City Hall and Chamber of Commerce, and we also walked away with multiple bags of trash from that park, and that felt good. - -Sponsorships, memberships, events and community work produce links from the sites that report on them, and those sites are exactly the local, relevant sources that [a backlink audit](/library/link-building/backlink-audit) says count for most. The link is a by-product. That is why it works. - -### 3. Map the businesses that share your customer - -Jeremy Rivera, the podcast's host, has a document he asks every client to write before any outreach, described here to Greg Digneo of Content Guppy: - -> I can recommend this SOP to sit down and create a Nexus document. And you can do it with Claude, but you're basically going to brainstorm out every non-direct competitor that wants to reach your same audience that has a a marketing budget. - -Greg's episode is [here](https://unscriptedseo.com/greg-digneo-on-building-an-audience-you-actually-own/). The nexus is every business that reaches your customer and does not compete with you. A realtor's nexus is inspectors, painters, movers and lenders. A SaaS company's is the tools its customers already pay for, the consultants who set them up, the newsletters they read. Each one is a site with a reason to mention you, because you are useful to their reader, and each mention is a link from a relevant domain. - -Write the list before you write a single email. The email to a nexus business is short, because you can name the reason the link helps their reader. The email to a random blog is long, because there is no reason. - -### 4. Make it personal enough that nobody else could have sent it - -The podcast's host built links for a software company the slow way, years ago, and told Mike Montague how it went: - -> I got 10 links for Raven Tools by writing 10 letters. - -Ten for ten. Handwritten, on paper, to people whose work he had read. Mike, who runs a human-first AI marketing practice, on why that still works: - -> nobody gets a printed book in the mail, nobody gets a handwritten note, and nobody understands their business well enough - -His episode is [here](https://unscriptedseo.com/mike-montague-ai-iron-man-not-terminator/). The lesson is not the postage. It is that ten letters to ten right people beat a thousand emails to a list, and the emails cost more once you count the reputation they spend. - -Katie Wagner, who spent fifteen years as a journalist before opening an agency, describes the receiving end of a pitch: - -> if you make it easier for them, that is a welcome thing - -Her episode is [here](https://unscriptedseo.com/katie-wagner-seo-journalism-are-the-answer-to-llms-and-google-too/). A pitch that includes the quote, the data and the image, so the writer has to do nothing but paste, gets used. A pitch that asks them to look at your site does not. - -## What to skip - -Buying links from the people who email you about buying links. The [backlink audit](/library/link-building/backlink-audit) shows what those look like on a real profile: casino domains and "premium PBN network" anchors, with spam scores in the 60s, that Google is built to ignore. Patrick Stox, formerly of Ahrefs and now consulting on his own, raised the newer risk when talking about AI search systems: - -> How long will they hold a grudge against you? Um, so I think you gotta be a little careful on that kind of stuff too. - -His episode is [here](https://unscriptedseo.com/patrick-stox-on-building-in-the-geo-era/). Google eventually forgave a cleaned-up profile. There is no evidence yet that the AI systems that now cite sources will. - -## Run this with the OpenSEO MCP - -Connect the [OpenSEO MCP](/docs/mcp). The [link-prospecting](/docs/skills/link-prospecting) skill packages the competitor side of this; the prompt below starts from your own profile, which is cheaper and usually better. - -In OpenSEO, open Backlinks, choose Top Pages, and sort by referring domains, highest first. Copy the first ten rows, including each URL and its referring-domain count, into your assistant with this prompt. - -```text -Using the OpenSEO MCP on project [name], find where my next links -should come from, for [domain]: - -1. Use the Top Pages rows I pasted to list my ten leading pages by - referring domains. Read each page and classify it as a tool, an - event or service page, an article, or a company page. Mark any - unreadable page as unverified. If I have not supplied the rows, - ask for them before continuing. - -2. Call get_backlinks_profile for the two non-homepage URLs with the - most referring domains in that list, using scope "page", - mode one_per_domain, hideSpam true. Read the linking pages and - suggest why each linked: cited data, used a tool, reported an - event, listed a partner, or something else. Mark uncertain reasons - as hypotheses and unreadable pages as unverified. - -3. From those reasons, tell me which of my pages could earn more of - the same kind of link, and what would have to be true for it to. - -4. Pull Search Console performance by query for the last 3 months and - list any query containing "calculator", "template", "checklist", - "tool" or "estimate" where I have impressions. Those are tools - people already want from me. - -5. Give me a list of 20 businesses that share my customer and do not - compete with me, from the referring domains I already have and the - domains linking to my two closest competitors. -``` - -Step 5 is the nexus document. Step 2 is what makes the outreach email short. - -## Earn the first ten before you pitch the hundredth - -The profile you already have tells you what people link to on your site and why. Build more of that: a tool someone searches for, a thing you did that a local site can report, a list of the businesses that share your customer, and ten personal notes to ten right people. Then count [referring domains](/library/link-building/referring-domains), not backlinks, and read the linking pages to understand why they chose to mention you. - -## Getting backlinks FAQ - -### How do I get backlinks for a new website? - -Start with the businesses that share your customer and do not compete with you: partners, suppliers, the tools your customers use, the communities they belong to. Ask for a mention where it helps their reader. Then build one thing worth linking to, a tool or a piece of data, and tell the ten people most likely to use it. - -### What is a linkable asset? - -A page that does something for another site's reader that a paragraph of prose cannot: a calculator, a template, a dataset, a checklist, a tool. On the site above, a traffic calculator has links from 17 domains; no article on the site has more than 10. - -### Does link building outreach still work? - -Personal outreach to a short list works. The podcast's host once got ten links from ten handwritten letters. Mass email to a purchased list mostly produces replies from people selling links, and those are the links that show up in an audit with spam scores in the 60s. - -### Should I buy backlinks? - -No. The sellers who email you produce links from casino and PBN domains that search engines ignore and that make your profile look manufactured. Practitioners on the podcast also warn that AI search systems may not forgive a manipulated profile the way Google eventually did. - -### How does OpenSEO help with link building? - -The backlinks tool shows which of your pages attract links and from where, which is the starting point above. It shows the same for any competitor, so you can list the domains that link to them and not to you. Use the app’s Top Pages table to compare referring-domain counts by page; the MCP provides backlink summaries and individual backlink rows. The link-prospecting skill packages the competitor workflow. diff --git a/web/content/marketing/library/human-in-the-loop-content.mdx b/web/content/marketing/library/human-in-the-loop-content.mdx deleted file mode 100644 index 3cc7aa418..000000000 --- a/web/content/marketing/library/human-in-the-loop-content.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: "Human in the loop content: the brief is the job" -description: "Most agencies run the loop backwards: AI writes the brief, a person writes the draft, AI edits. The practitioners with results do the opposite. What goes in a brief the model cannot write, what the model is good at once it has one, and the two checks that catch the draft that reads like everyone else's." ---- - -## How should AI be used for SEO content? - -Humans write the brief. The model writes the draft. Humans edit. That order, and only that order. - -It sounds like a slogan and it is a finding. Nick Eubanks, global CMO of Digistore24 and an advisor to six agencies, describes the version most teams settle into and why it fails: - -> when you look at the human in the loop process where you've got the start, have to create the brief with AI and a human writes the draft and then an AI does the proofreading and editing and then they publish it, I think it's completely backwards. - -And the version that works: - -> the best results that I've seen from a content perspective is where it's humans actually doing the brief still and then it's AI writing the draft and then it's humans doing the editing. - -![A quote card reading: The best results that I've seen from a content perspective is where it's humans actually doing the brief still and then it's AI writing the draft and then it's humans doing the editing. Attributed to Nick Eubanks, global CMO of Digistore24, from The Unscripted SEO Interview Podcast.](/library/ai-agent-seo/quote-humans-do-the-brief.png "Nick Eubanks on who does the brief") - -His episode is [on the Unscripted SEO podcast](https://unscriptedseo.com/nick-eubanks-distribution-is-the-last-moat/). The reason is simple once said. The brief is where the knowledge lives: what the reader needs, what the business knows that competitors do not, which claims are true. A model cannot supply any of that, and if it writes the brief it writes a generic one and every draft that follows is generic. The draft, by contrast, is mostly labour, and the model is good at labour. - -## What goes in the brief - -Malte Landwehr, who runs product and research at Peec AI and watches what gets cited, gives the test that decides whether a page deserves to exist: - -> if you can create it with a prompt, why would chat GPT or open AI or Google crawl index and rank it, right? They could just use that prompt on their own. - -And the mindset for the brief: - -> think about how you would do briefings for a new author who is very smart but doesn't know anything about your business and will try to bullshit you unless you give them very explicit instructions and then also have still someone double check it. - -His episode is [here](https://unscriptedseo.com/episode-recap-malte-landwehr-of-peec-ai-on-consensus-query-fan-outs-and-ai-content-guardrails/). Data and quotes are the two things the model cannot invent without lying, so they are the two things that make a page different from the ten pages already ranking. A brief that carries them produces a draft with something in it. A brief that says "write 1,500 words about X" produces the eleventh page. - -The rest of the brief is what you would tell a good freelance writer on their first day: - -- **Who reads it** and what they already know. The reader of this library owns a KPI and not the site; the reader of a plumber's page owns a leak. -- **What the reader should be able to do** after reading. One thing. -- **The data.** Your Search Console numbers, your tracker, your audit, a customer's actual words. Pulled through the [MCP](/docs/mcp) if you have it connected, pasted if not. -- **The quotes**, verbatim, with who said them and where. -- **What not to say.** The claims you cannot back, the competitors you do not name, the product features that do not exist yet. - -Jeremy Moser, who runs a content and link agency, on why the brief cannot be skipped: - -> AI works really good if you have the expertise to relay to it and feed through it. - -His episode is [here](https://unscriptedseo.com/jeremy-moser-it-was-always-just-good-seo/). The expertise is relayed in the brief. If you do not have it, the model does not either. - -## What the model is good at - -Once the brief exists, the model earns its place. Erika Braeger, manager of organic growth strategy at Tenspeed, names what it removes: - -> through all of my agency experience, what I consistently ran into is it it's the pre-work that can burn a lot of time, a lot of energy. - -Her episode is [here](https://unscriptedseo.com/erika-braeger-tenspeed-unscripted-seo/). The pre-work is pulling the data into one place, outlining against the brief, writing the first draft of every section, drafting the FAQ from the questions people ask, formatting. An afternoon becomes a minute, and the afternoon was never where the value was. - -Tom Malesic, founder of EZMarketing, describes what it did to his team's job titles: - -> Asking our team from being self creators of things whether it's you came up with a designer you handcrafted all that copy to become editors of things - -His episode is [here](https://unscriptedseo.com/tom-malesic-the-website-is-the-center-of-everything-unscripted-seo-podcast/). Editors, not writers. The role did not disappear; it moved to the two ends of the loop where the judgement is. - -## The edit: two checks that catch the generic draft - -The model's draft has a signature. It hedges, it lists in threes, it opens paragraphs with a throat-clearing phrase, it reaches for the same dozen adjectives every other draft reaches for. A reader may not name what is wrong with it, but they stop reading. - -Two checks, in order. - -**First, the fact pass.** Every number traces to a source in the brief. Every quote matches its transcript. Every product claim is true today, not last quarter. This is the check the model cannot run on itself, because it does not know which of its sentences it invented. On this site the rule is written into the review process: if a reader trusted every claim and opened the product right now, where would reality not match? - -**Second, the slop pass.** A list of the phrases and structures that mark a machine draft, applied to the text. OpenSEO keeps one in its repository and runs it on every page before it ships; the phrase catalogue is a few hundred entries and the structural one covers the rhythms, the false contrasts, the rhetorical questions the draft asks itself. Malte adds four measurements you can have the assistant compute against a set of your own human-written pages: perplexity, compression ratio, and Jaccard and cosine similarity between texts. A draft that scores far from the human set on any of them needs another pass. Here is what the rule and the catalogue look like in the source: - -![The OpenSEO repository's agent skills directory listed in a terminal, with the deslop skill's reference files and the first entries of its phrases catalogue: filler openers, binary contrasts, and vocabulary the skill removes from every draft before it ships](/library/ai-agent-seo/deslop-skill-catalogue-openseo.png "The deslop skill in the OpenSEO repository") - -Holly Homer, who runs organic and AI visibility at Digistore24, has a cheaper version of the second pass that works before you read a word: - -> one of my favorite things to do is is after I, you know, been working for a while with something is saying, what is the feedback I'm gonna give you about this? - -Her episode is [here](https://unscriptedseo.com/holly-homer-on-affiliate-marketing-ai-slop-and-being-replaced-by-your-own-words/). Mike Montague, who runs a human-first AI marketing practice, on why the whole loop matters to being cited at all: - -> if AI can write a blog for you, it already has that knowledge. So what why would it index it? Why would it cite your version if it can already write one that doesn't need to be cited? - -His episode is [here](https://unscriptedseo.com/mike-montague-ai-iron-man-not-terminator/). Run both passes and what remains is a draft that a person briefed, a model wrote, and a person checked. That is the loop. Adrian Nikolov, on who he hires now: - -> Of course, we still need editors, but the way I will hire an editor today is that I want one that knows AI, that uses AI constantly. - -His episode is [here](https://unscriptedseo.com/adrian-nikolov-organic-growth-engineering/). - -## Run this with the OpenSEO MCP - -Connect the [OpenSEO MCP](/docs/mcp). This prompt makes the assistant assemble the brief's data half and then refuse to draft until the human half exists. - -```text -Using the OpenSEO MCP on project [name], prepare a brief for a page -about [topic]: - -1. Pull Search Console performance for the last 3 months by query, - filtered to queries containing [topic word]. Show me the top 15 - with clicks, impressions and position, and the URL that ranks. - -2. Call get_keyword_metrics for the 15 plus five variants you think - are missing. Give me volume, difficulty and intent. - -3. Call get_serp_results for the top 3 terms and list the ranking - URLs with their titles. Tell me in one line what each page has - that the others do not. - -4. Stop. Ask me for: who the reader is, the one thing they should be - able to do afterwards, two to four verbatim quotes with sources, - and anything the page must not claim. Do not draft until I answer. - -5. After I answer, write the brief as a one-page document with those - answers at the top and the data underneath. Then, and only then, - draft the page from the brief. -``` - -Step 4 is the human in the loop, made mandatory. An assistant that skips it produces the backwards version Nick described, and it will skip it unless told not to. - -## The brief is where you are - -A page is worth reading because of what the writer knew. The model knows nothing about your reader, your data or your customers until you put it in the brief, and everything it drafts after that is only as good as what you put there. Write the brief, let it draft, edit it hard. [Skills, memory and the trace](/library/ai-agent-seo/skills-memory-and-the-trace) is how to make the second and third steps repeatable. - -## AI content FAQ - -### How do I use AI for SEO content without producing generic pages? - -Write the brief yourself: the reader, the one thing they should do, your own data, verbatim quotes, and what not to claim. Let the model draft from that. Then run a fact pass against the brief and a pass for machine-writing phrases before anything ships. Every skipped step shows in the result. - -### Should AI write the brief or the draft? - -The draft. A brief written by a model contains only what the model already knew, which is what every competing page already says. A brief written by a person carries the data and quotes that make the page different. The draft is labour; the brief is knowledge. - -### What is human in the loop content? - -A workflow where a person owns the two ends, briefing and editing, and a model does the middle. It is the opposite of the common pattern where the model briefs and edits while a person writes, which produces polished pages with nothing in them. - -### How do I check an AI draft? - -Two passes. Fact: every number, quote and product claim traces to the brief or the source. Slop: remove the phrases and structures that mark machine writing, such as filler openers, false contrasts and stacked rhetorical questions. OpenSEO keeps a catalogue of both in its repository and runs it on every page. - -### Does OpenSEO write content? - -No. The MCP pulls the data half of a brief: Search Console performance, keyword metrics, SERP results, competitors, audit findings. The assistant you connect writes from the brief you complete. The product's own pages follow the same loop and the review rules are public in its repository. diff --git a/web/content/marketing/library/index-bloat.mdx b/web/content/marketing/library/index-bloat.mdx deleted file mode 100644 index a2603346e..000000000 --- a/web/content/marketing/library/index-bloat.mdx +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: "Index bloat: when the fix is deleting pages" -description: "Homes.com removed 5 million pages from a 110-million-page site and recovered from Panda. On a small site the same instinct is usually wrong. How to tell which situation you are in before you delete anything." ---- - -## What is index bloat? - -More URLs eligible for indexing than the site has distinct things to say. It shows up as pagination, filter parameters, tag archives, per-answer or per-comment permalinks, and templated pages generated for combinations nobody searches for. Each one returns 200 and looks healthy in a crawl report. Nothing is broken. That is the difficulty: a crawler grades pages one at a time and index bloat is a property of the set. - -The remedy is unpopular because it means deleting work someone was paid to produce. It is also, at scale, one of the few interventions with a documented before and after. - -## The version where deleting 5 million pages was correct - -Grant Simmons was hired into Homes.com in 2014 after the site lost roughly 60% of its organic traffic to what turned out to be Panda. The diagnosis was not subtle: - -> We should clean up a lot of crap, a lot of thin pages that existed purely for just existing - -The scale of the cut is the part people do not believe: - -> it was a matter of getting rid of millions of pages from a 110 million page site, taking some really big risks in chopping. - -Five million pages came out of the index. Grant then did the thing every SEO fantasises about, which is get a straight answer from Google in person. At SMX London, with John Mueller in the audience, he described the site and the remediation: - -> He got on his laptop, he typed in and 20 seconds later he goes, yeah, you're under a Panda penalty. And I go, all right, well, they know that stuff. - -And the outcome: - -> we made those changes and when Panda was pushed out at that time, the rolling updates, we got back all our traffic and then some. - -His summary of why the incumbent agency had not fixed it is the transferable lesson: - -> They were not thinking to be aggressive. I've seen in those days recovery had to be aggressive. - -Grant's full episode covers entity SEO, measurement, and what he thinks reporting still gets wrong: [listen here](https://unscriptedseo.com/grant-simmons-on-entity-seo-llms-and-the-future-of-search-visibility/). - -Three things made deletion the right call at Homes.com and they are worth stating, because they are the conditions most sites fail to meet. The pages were near-duplicates generated by a template. There were millions of them, enough to change how the whole domain was assessed. And there was a measured traffic collapse to recover, not a hunch. - -## The version where deleting is a waste of a weekend - -Now the small-site case, from a crawl of seoarcade.com. - -The site runs a Q&A section. Every question has a page, and every individual answer to that question also gets its own permalink. Filtering the crawl to a single question about keyword forecasts returns **20 of the 867 crawled URLs**. Eight of those came back 200, all carrying the same title and between 824 and 1,127 words of overlapping text. - -![OpenSEO site audit pages tab filtered to one question slug, showing 20 of 867 crawled URLs, all sharing the title "How does my life change with keyword forecasts? -" with word counts between 824 and 1,127, four of them returning 429 and the rest returning 200](/library/site-audit/duplicate-answer-permalinks-openseo.png) - -Each of those eight sits in the sitemap and reports `isIndexable: true`. By any crawl-based definition this is index bloat, and the obvious move is to noindex the answer permalinks and ship it. - -Then you check what Google did with them, which is the step almost nobody takes. - -Search Console URL inspection on six URLs from that site returned **"URL is unknown to Google" for five of them**: the keyword-forecasts question page, two of its answer permalinks, a third answer permalink from a different question, and the parameter archive at `/?post_type=answer`. The only one that came back "Submitted and indexed" was an ordinary question page, which is the page that should be indexed. It reported a Google-selected canonical matching its declared canonical, three referring URLs, and a Q&A rich result. - -There was nothing to remove. The crawler found a mess and the index did not have one. - -## Check before you cut - -This is the difference between the two cases, and it costs nothing to establish. Google's URL Inspection API costs nothing and uses no OpenSEO credits, and it answers the only question that matters: does Google know this URL exists, and if it does, which URL did it pick as canonical? - -Four verdicts come back, and each implies a different action. - -- If the URL is unknown to Google, there is nothing to remove. Fix the sitemap so you stop submitting it, and move on. -- "Duplicate, Google chose a different canonical" also leaves you nothing to remove, because Google has already consolidated the set. Confirm the URL it picked is the one you want. -- The real bloat signal is "Crawled, currently not indexed" or "Discovered, currently not indexed". Google has seen the page and declined it, so either make the page worth indexing or take it out of the sitemap and the internal link graph. -- A page that comes back "Submitted and indexed" when you never wanted it indexed is the one case for `noindex` or removal. - -Patrick Stox, who built Site Audit and Page Inspect at Ahrefs, points out that the discovery side has more inputs than most people account for, which is why bloat keeps reappearing after you delete it: - -> But what are the data sources for these systems? Most people will tell you sitemaps, or crawling. Well, there's more. RSS feeds are a thing. - -Removing a URL from a sitemap does not remove it from an RSS feed, an internal link, or somebody else's page. If a URL keeps coming back after you deleted it, one of the other sources is still publishing it. Patrick's episode on building for the GEO era is on [the Unscripted SEO podcast](https://unscriptedseo.com/patrick-stox-on-building-in-the-geo-era/). - -## The workflow - -1. Crawl the site and read the pages report rather than the issues report. Bloat is not an issue type; it is a pattern in URLs, titles, and word counts. -2. Sort by title. Repeated titles across many URLs are where templated duplication lives. Sort by word count next, because the low end is where thin templated output collects. -3. Take a sample of 10 suspect URLs and inspect them in Search Console. -4. Act on the verdicts, not the crawl. Unknown and duplicate URLs need a sitemap fix. Indexed-but-unwanted URLs need `noindex` or a 410. -5. Re-inspect the same sample in 30 days. Index changes are slow and a re-check is the only way to know the change landed. - -## Run this with the OpenSEO MCP - -`get_audit_pages` gives you the crawl side and `inspect_urls` gives you Google's side, so an agent can join them in one pass. Both are free calls: they read stored audit state and Search Console rather than a paid data provider. - -```text -Using the OpenSEO MCP on project [name]: - -1. From the most recent site audit, pull all crawled pages. Group - them by exact title and show me every title used by more than - two URLs, with the URL count and the word-count range. - -2. Do the same for URL patterns: show me any path segment or query - parameter that appears on more than ten URLs (things like - /answer/, /page/, /tag/, ?filter=). - -3. Pick the ten worst-looking URLs from those groups, weighted - toward the largest group, and run inspect_urls on them. - -4. Report each URL with its coverage state and its Google-selected - canonical. Then sort them into: unknown to Google, canonicalised - away, crawled but not indexed, and indexed. - -5. Tell me the count in each bucket and what fraction of the - suspect set is in the index. Recommend an action per - bucket, and say plainly if the answer is that there is nothing - to remove. -``` - -Point five is the instruction that keeps this workflow honest. Ask an agent to find index bloat and it will find index bloat. Ask it what fraction of the suspects are in the index and you get a number that can come back as zero. - -## Deletion is a scale decision - -Both stories in this guide are true and they point in opposite directions, which is the useful part. - -At 110 million pages with a measured 60% traffic loss, removing 5 million near-duplicates was the intervention that worked, and the agency that would not be aggressive enough had already failed at it. At 867 pages with no traffic collapse, the same instinct would have burned a weekend removing URLs Google had never fetched. - -What separates them is not the size of the mess in the crawl report. It is whether the mess made it into the index and whether anything measurable got worse. Establish both before you delete a page, and the audit becomes evidence instead of an argument. - -If part of your crawl came back blocked, hold this workflow until that is fixed, because a blocked page reports no title and zero words and will look exactly like thin content. - -## Index bloat FAQ - -### How do I know if my site has index bloat? - -Compare the number of URLs you want indexed against the indexed count in Search Console's page indexing report. A large gap is the signal. Then inspect a sample of the surplus URLs, because a crawler can show you hundreds of near-duplicate pages that Google never fetched, which is not bloat. - -### Does index bloat hurt rankings? - -It can, at scale, when a large share of a domain's pages are thin or duplicated and the domain gets assessed as a whole. On a site of a few hundred pages the more common effects are wasted crawling and internal link equity spread across URLs that were never going to rank. Neither is urgent on its own. - -### Should I noindex or delete duplicate pages? - -Use `noindex` when the page has a purpose for users, such as a filtered listing or a paginated archive. Use a 410 or a 301 when the page has no purpose at all. Removing a URL from the sitemap alone does not deindex it; it only stops you asking for indexing. - -### How many pages should a website have indexed? - -As many as there are distinct things worth ranking, which for most small business sites is dozens rather than thousands. The count matters less than the ratio: if most of your indexed URLs get no impressions in a year, the set is larger than the site can support. - -### Can a site audit tool find index bloat? - -Not directly, because every bloated page returns 200 and passes its per-page checks. What a crawler gives you is the raw material, repeated titles, thin word counts, and recurring URL patterns. Pair it with Search Console URL inspection to find out which of those URLs Google holds. OpenSEO does both: crawls up to 50 pages on the free plan and 10,000 on the $10/month plan, and runs URL inspection against your connected property at no credit cost. diff --git a/web/content/marketing/library/intent-beyond-google.mdx b/web/content/marketing/library/intent-beyond-google.mdx deleted file mode 100644 index 551148cbc..000000000 --- a/web/content/marketing/library/intent-beyond-google.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: "Where the queries happen when they don't happen in Google" -description: "Pinterest, LinkedIn and the AI assistants all run on intent, and none of them appear in your keyword tool. The research method carries over; the surfaces do not." ---- - -## Where does keyword research happen outside Google? - -Keyword research assumes one box. A person types, an engine ranks, you measure the click. That model still describes a large share of demand, and it describes a shrinking share of discovery. People look things up inside Pinterest, inside LinkedIn, inside YouTube, and increasingly inside an assistant that answers without sending anyone anywhere. - -The discipline transfers cleanly. You are still asking what somebody wants, in what words, at what stage. What changes is that these surfaces publish no volume data, so the research runs on the platform's own autocomplete, on your first-party results, and on the vocabulary your customers already use. That last source is the one that works everywhere, which is why [seeding from conversation](/library/keyword-research/seed-from-conversation) matters more here than on Google. - -## Pinterest is a search engine wearing a social network's clothes - -Julia Bocchese works almost entirely in Pinterest and SEO, and she is direct about why the two belong together: - -> Most of the time when people go to Pinterest, they are searching for something. They're not really going there to follow people or to comment on pins. They're really searching for information, ideas, inspiration, products. - -That single observation changes what you optimize. On a social network, the account is the unit and the follower is the asset. On a search surface, the query is the unit, and Julia points out the practical consequence: the audience arriving from a Pinterest search has no idea who you are. - -> People aren't going on Pinterest to follow people. They're searching for information. So they likely don't know who you are. - -The optimization work looks familiar as a result. Keywords belong in pin titles, in pin descriptions, in the text rendered on the graphic itself, and in the board and profile copy, because the platform has to understand the account and the pin before it can match either to a search. It is the same two-sided job as a page title and a site structure, run on a surface with no Search Console. - -## LinkedIn ranks the profile, not the post - -Daniel Alfon makes an argument that sounds wrong until you check your own behaviour on the platform: - -> LinkedIn is the only platform where, Jeremy, you can be successful without posting content. That's amazing. Because if you want to be a successful YouTube or Instagram or TikTok, you have to upload content. - -His reasoning is that the most common action on LinkedIn is visiting somebody's profile. The profile is therefore the ranked, keyword-sensitive asset, and the question a visitor arrives with is narrow: is your specialty the specialty they are looking for. His advice follows from that. Take care of the profile, make sure it converts, and treat posting as the secondary activity it is. - -For keyword work this means the headline and the about section carry the load a title tag carries elsewhere. They should contain the words a stranger would use to describe the problem you solve, not the internal job title your company assigns you. - -## The assistants answer, and the click never arrives - -The third surface is changing fastest: ranking and traffic have come apart. Christopher Gimmer, running a site with a decade of authority behind it, describes the split: - -> Even though we're ranking number one for some of these keywords, the AI Overviews are answering some of those things, and so we're losing out on some of those clicks. - -He adds a second erosion that has nothing to do with AI. On commercial queries the paid block has grown to the point where position one is not position one on the page: - -> Even if you're ranking number one for that organic keyword, your link is actually the fifth one on the page. And if you're ranking number three, you're now like eight links down. - -![OpenSEO keyword research results with the intent column labelling each keyword informational, navigational, commercial or transactional](/library/keyword-research/intent-by-surface-openseo.png) - -The intent column is the closest native proxy for this. The rows labelled informational are the ones an assistant can satisfy without sending anyone anywhere, and the transactional and commercial rows are the ones that still end in a visit. Reading the list that way tells you which part of your research is exposed to the answer layer before you commit to any of it. - -Two lessons for research. First, rank is a weaker proxy for traffic than it used to be, which is why [forecasting](/library/keyword-research/opportunity-sizing-forecasting) should apply a CTR curve that reflects the SERP you face rather than a textbook one. Second, informational queries that an assistant can satisfy in a paragraph are worth less than they look in a volume column, and queries requiring a tool, a login, a price or a human are worth more. - -## How to research a surface that publishes no volume data - -None of these platforms will hand you a search volume. The method that works without one: - -1. Type your seed into the platform's own search box and record the autocomplete suggestions. That list is the platform telling you what people search there. -2. Read the top results and note the vocabulary they use in titles and opening lines, which is the language the platform has already rewarded. -3. Check your own first-party data for the platform, where it exists, before trusting any of the above. -4. Look for the branded lift. Discovery on a platform with no outbound links often shows up later as somebody searching your name in Google, which is the one measurable bridge back. - -Point four is the measurement most people skip. If a platform sends no clicks, watch whether branded search grows while you invest there. It is a directional signal you can monitor in [your own Search Console](/library/keyword-research/gsc-programmatic-discovery). - -## Do it with OpenSEO - -The keyword work still needs a demand baseline, and Google remains the best available proxy for what people want even when they ask somewhere else. - -```text -Using the OpenSEO MCP, research keywords for "[my topic]" and label -each result by intent. - -1. Split by surface -Flag which of these queries an AI assistant can fully answer in a -paragraph, and which need a tool, a price, a login or a human. Rank -the second group higher: those are the queries that still produce a -visit. - -2. Branded baseline -Pull my branded queries and their trend over the last 12 months. I am -going to invest in [platform], which sends no referral data, so I want -the branded line as my before-picture. - -3. Vocabulary -For my top 10 non-branded queries, give me the phrasings a person -would use searching inside Pinterest or LinkedIn rather than Google: -shorter, more visual, more role-based. - -Output as a document I can review. -``` - -## What to do first - -Take one seed and run it through the search box of whichever platform your customers use. Compare that autocomplete list against the keyword list you already have. The overlap tells you what carries across surfaces. The gap is the demand your current research has never seen, and it is usually phrased in words no keyword database would have suggested. - -## Keyword research beyond Google FAQ - -### How do you do keyword research for Pinterest? - -Use the platform's own search box and its guided-search suggestions, then place those terms in pin titles, pin descriptions, the text on the image, and your board and profile copy. Pinterest has to understand the account and the pin before it can match either to a query, so both levels need the vocabulary. - -### Does SEO work for LinkedIn profiles? - -Yes, and the profile matters more than the posting. The most frequent action on the platform is viewing a profile, so the headline and about section carry the weight a title tag carries on a website. Write them in the words a stranger would use for the problem you solve. - -### How do you optimize for AI assistants? - -Answer questions directly and completely enough to be quotable, and accept that queries an assistant can satisfy in a paragraph will return fewer clicks regardless of your rank. Weight your research toward queries needing a tool, a price, a login or a person, because those still send a visit. - -### How do you measure traffic from platforms that send no referral data? - -Watch branded search. Discovery on a closed platform tends to surface later as people searching your name, so a rising branded line in Search Console during a campaign is a useful directional signal. diff --git a/web/content/marketing/library/keyword-gap-analysis.mdx b/web/content/marketing/library/keyword-gap-analysis.mdx deleted file mode 100644 index 5f33d9614..000000000 --- a/web/content/marketing/library/keyword-gap-analysis.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: "Keyword gap analysis: subtract the brand terms first" -description: "A raw keyword gap between you and a competitor is mostly noise, because most ranked-keyword lists are mostly brand. How to strip brand from both sides and turn the remainder into a page plan." ---- - -## What is a keyword gap analysis? - -A keyword gap analysis compares the keywords a competitor ranks for against the keywords you rank for, and treats the difference as your opportunity list. It is the single most requested competitive report, and in its raw form it is close to useless. - -The reason is arithmetic. Pull the ranked keywords for a well-established competitor and a smaller site in the same market and the two numbers are not comparable. Here is a real pair from the property restoration category: the national brand ranks for 49,475 organic keywords. The regional operator ranks for 79. - -A raw gap analysis reports that as 49,396 opportunities. That number is not a plan. It is a way of feeling behind. - -## Why your ranked-keyword list is mostly your own name - -Look at what the smaller site actually ranks for, sorted by traffic. - -![A ranked-keyword list showing 457 estimated organic traffic and 79 organic keywords, where the top rows are all brand terms containing the company's own name](/library/competitive-analysis/brand-dominated-ranked-keywords-openseo.png) - -Every row at the top of that list is the company's own name in some arrangement. Below the fold it gets stranger: the site also ranks for several _other_ restoration companies' brand names, for a street address, and for a local civic app that happens to share a word with one of its city pages. - -Strip all of that out and the genuine service rankings are a short list: one city plus water damage at position 19, one city plus mold removal at position 18, one city plus fire damage at position 18. Three real footholds, not 79. - -That is the actual starting position, and it is much better news than the raw number, because three near-miss service pages is a quarter's worth of work rather than an impossible backlog. - -The same subtraction has to run on the competitor's side. Their 49,475 includes their brand, their franchise location pages, their glossary, and a large volume of close variants. The comparable figure on both sides is service terms only. - -## How to run a keyword gap in OpenSEO - -OpenSEO does not have a one-click "keyword gap" report that takes two domains and returns the difference. What it has are the two halves: you pull ranked keywords for each domain and the agent performs the join. In practice this is more flexible than a fixed report, because you control the filters on both sides, but it is worth knowing before you go looking for a button. - -The filter that does the real work is brand exclusion, which is a parameter on the ranked-keywords tool rather than something you have to post-process. - -```text -Using the OpenSEO MCP, run a keyword gap between [mydomain.com] -and [competitor.com]. - -1. Pull ranked keywords for both domains. On both sides, exclude - brand terms: [my brand and its variants] and [their brand, - their parent company, their location-page naming pattern]. - -2. Drop anything that is a competitor's brand name, a street - address, or an obvious mismatch. Show me what you dropped and - why, in a short list. - -3. Return three tables: - - terms they rank top 10 for and I do not rank for at all - - terms we both rank for where they are ahead of me - - terms I rank 11-30 for that they do not hold, which is - defensible ground rather than a gap - -4. For table one, group by the page on their site that ranks, so - I can see whether the gap is many pages or one strong page. - -Sort each table by search volume and tell me which table you -would work first. -``` - -Table three is the one most gap reports omit and the one most worth having. A gap analysis that only looks at what you are missing will walk you away from the terms you are quietly already winning. - -## Learn from the gap, do not copy it - -The output of this workflow is a list of things a competitor has that you do not. The obvious move is to build the same pages. It is usually the wrong move. - -Tom Malesic, who runs the agency EZMarketing, put the objection plainly when asked how to use competitor research without producing a copy of the competitor: - -> So for small business owners, their competitors are generally small business owners. And most small business owners' marketing is terrible. So I worry less about sounding like the competitor, and I worry more about creating the voice and sounding like the business that I'm representing. - -A gap tells you a topic has demand and that somebody satisfied it well enough to rank. It does not tell you that their page is good. Frequently the page holding position four is thin, dated, and beatable by anyone willing to answer the question properly. - -Gert Mellak of Solicom AI Consulting frames the useful version as a narrowing exercise rather than a copying one: - -> Let's really analyze those five. Let's go to competitors, let's see what's working for them, what can we learn from them? How can we go deeper on those five topics, angles, aspects? - -Five is closer to the right number than five hundred. A gap report's value is in the top of the list, not its length. - -## Turning the gap into pages - -Once the list is clean, it stops being a competitive artefact and becomes an ordinary content plan. Group the surviving terms by intent, then by the page that would satisfy them, exactly as you would with any other keyword set. The workflows in [clustering keywords into topical hubs](/library/keyword-research/cluster-topical-hubs) and [search-intent mapping](/library/keyword-research/search-intent-mapping) apply here without modification, because at this point the source of the keywords no longer matters. - -Two rules specific to gap-sourced keywords: - -1. **Check the ranking page before you commit.** If a competitor holds thirty terms with one page, you need one page, not thirty. Grouping by their URL is what reveals this. -2. **Do not chase terms your product does not serve.** A competitor with a broader product line will rank for things you have no business ranking for. The restoration comparison above is a good example: a large share of the national brand's traffic comes from general cleaning services, which is a different business. - -That second point is developed further in [reading a competitor's domain overview honestly](/library/competitive-analysis/competitor-traffic-estimates). - -## What to do first - -Run the gap on one competitor, with brand excluded on both sides, and stop at the first ten rows. Pick the one row where a competitor's page is visibly weaker than what you could write, and write that page. A gap analysis that produces one shipped page has outperformed the vast majority of gap analyses, which produce a spreadsheet. - -Then run it again in ninety days on the same competitor. The change between the two runs is more informative than either run alone. - -## Keyword gap analysis FAQ - -### What is a keyword gap in SEO? - -The set of keywords a competitor ranks for and you do not. Useful only after brand terms are removed from both sides, because otherwise the largest part of the difference is simply that you have different company names. - -### How do you do a content gap analysis for free? - -The discovery half costs nothing: read the competitor's pages, note what they cover that you do not, and check your own Search Console for questions you already receive impressions for. The part that costs money is the ranked-keyword data on the competitor's side, which is why quality SEO data sits behind a subscription in every tool. OpenSEO is open source and free to start, with a paid plan at $10/month that includes usage credits. - -### Should I target every keyword my competitor ranks for? - -No. A competitor's ranked-keyword list includes their brand, their variants, and terms served by product lines you do not have. The list worth acting on is usually a few dozen rows, not a few thousand. - -### What if my competitor ranks for thousands of keywords and I rank for almost none? - -Group their keywords by the page that ranks. A large keyword count is often a small number of strong pages, and a small number of strong pages is something you can compete with. The count itself is a poor measure of how far ahead they are. diff --git a/web/content/marketing/library/keyword-ranking-report.mdx b/web/content/marketing/library/keyword-ranking-report.mdx deleted file mode 100644 index b26017303..000000000 --- a/web/content/marketing/library/keyword-ranking-report.mdx +++ /dev/null @@ -1,137 +0,0 @@ ---- -title: "The keyword ranking report your CEO will read" -description: "A table of positions is not a report. Lead with the number the business tracks, group the movement into four buckets, explain the three rows that caused it, and say what happens next. One page, every week." ---- - -## What should a keyword ranking report show? - -The number the business already cares about, first, then rankings as the explanation for it. Most ranking reports do this backwards. They open with a table of keywords and positions, sometimes with green and red arrows, and leave the reader to work out whether anything in it matters. The reader is usually someone who does not do SEO, has ten minutes, and needs to decide whether to keep paying for this. A table does not help them decide. - -Grant Simmons, who ran search at Homes.com and now works with Fiat Growth, drew the line when we were talking about a well-liked Search Console tool: - -> it's still not reporting. It's insights. - -![A quote card reading: It's still not reporting. It's insights. Attributed to Grant Simmons of Fiat Growth, formerly Homes.com, from The Unscripted SEO Interview Podcast.](/library/rank-tracking/quote-not-reporting-insights.png "Grant Simmons on reporting versus insights") - -Insights are what a tool gives you. A report is what you give someone who has to make a decision with it. His full conversation on measurement is [on the Unscripted SEO podcast](https://unscriptedseo.com/grant-simmons-on-entity-seo-llms-and-the-future-of-search-visibility/). The structure below is built to be the second thing. - -## The structure - -### 1. One number, and what it did - -Open with the metric the business tracks, for the pages the tracker covers, against the previous period. Clicks from Search Console if that is what you have; leads or revenue on those pages if analytics is connected. One sentence: "Organic clicks on the twelve service pages we track were 1,240 this month, up from 980." That sentence is the report. Everything after it is support. - -![OpenSEO Search Console report for seoarcade.com, last 28 days against the previous period: clicks, impressions, click-through rate and average position with the change, then clicks, impressions, CTR and position by page](/library/rank-tracking/search-console-clicks-by-page-openseo.png "Search Console clicks by page in OpenSEO") - -Do not open with average position across all tracked keywords. It is a mean of unrelated things, it moves when you add a keyword, and nobody outside SEO knows whether 11.4 is good. - -### 2. Movement in four buckets - -Rankings come next, as counts rather than rows. Of the keywords tracked this period: - -- how many entered the top three -- how many entered page one -- how many dropped off page one -- how many did not move - -Label the comparison period next to these counts. The OpenSEO MCP currently compares ranks against a seven-day baseline, falling back to the earliest available snapshot when history is sparse. In a monthly report, present this as the latest weekly movement alongside the monthly click totals. It is not a month-over-month ranking comparison. - -### 3. The three rows that explain the number - -Now the table, cut to three rows: the biggest gain, the biggest loss, and the row where the position held but the clicks changed. Show keyword, position, previous position, ranking URL, and what is on the page. - -The third row is the one that earns the report its credibility. When a term holds position 4 and loses half its clicks, the reason is almost always something Google put above it: an AI overview, a local pack, a video block. A tracker that records SERP features shows it in a column, and a report that names it turns "clicks fell, not sure why" into "clicks fell because Google added an answer box, and here is what we do about it". - -### 4. Brand and non-brand, separately - -Split the tracked list and the clicks into brand and non-brand before you draw any conclusion. Brand terms rise when the company does marketing of any kind, and a report that counts them as SEO wins will get found out at the first hard question. Charlie Birch, founder of Humanize Collective, on what the number is supposed to be for: - -> the result that you ultimately want is not search volume, it's conversion and retention - -The non-brand line is the one SEO can claim. Report it on its own, even when it is the smaller number. Especially then. Expect the query rows to add up to less than the page total: Search Console withholds a share of queries for privacy, so some clicks on every page belong to queries you will never see. Say so in a footnote rather than letting the reader find the gap. [The Dark Query Problem](/blogs/dark-queries) covers how large that share is and how to read around it. Her episode is [here](https://unscriptedseo.com/charlie-birch-on-where-brand-strategy-meets-seo-what-every-founder-needs-to-know/). - -### 5. AI answers, with the caveat attached - -If you report visibility in AI assistants, say what the number is. Brittany Trafis, founder of Soarion Digital, on the tools that monitor it: - -> It is a monitoring tool. It is directional. do not think this data is Google Search Console level data. It is nowhere close - -Put the row in, label it directional, and do not let it sit next to the click count as if it were the same kind of fact. Her episode on treating AI search as its own channel is [on the podcast](https://unscriptedseo.com/brittany-trafis-ai-search-is-not-seo-2-0-unscripted-seo-podcast/). - -### 6. What we did, what we will do - -Two short lists. The work that shipped this period, and the work planned for next, each tied to a row in section 3 or a bucket in section 2. This is the section that gets the budget renewed, because it connects the numbers to decisions someone made. - -## What to leave out - -The full keyword table goes in an attachment, not the body. Anyone who wants it can open it; putting it in front of the reader costs the attention you need for section 1. - -Year-on-year comparisons need a warning label now. Google's results pages changed enough between 2024 and 2026 that a query's clicks last year and this year are often not the same kind of number, because the page they came from had a different shape. Compare periods, name what changed on the SERP in between, and resist the chart that makes it look continuous. - -And leave out any score you cannot explain in a sentence. A "visibility index" invites a conversation about how it is calculated instead of a conversation about what to do. - -## Run this with the OpenSEO MCP - -Connect Search Console to the project and the [OpenSEO MCP](/docs/mcp) to your assistant. The tracker read and the Search Console read use no credits, so the report itself is free to produce once the tracker has run. - -```text -Using the OpenSEO MCP on project [name], write this month's ranking -report for [company]. - -1. Call get_rank_tracker to list trackers, then read the relevant - ones with trackerId. Pull every tracked keyword with position, - previous position, and ranking URL. Label this section "Latest - weekly ranking movement": the comparison uses a seven-day - baseline, with an earliest-available fallback for sparse history. - Do not present it as monthly movement or assume a full week of - history when that is not established. - -2. Pull Search Console performance for the last 28 days and the 28 - days before, by page, filtered to the URLs from step 1. Give me - total clicks for both periods. That is the first sentence. - -3. Bucket the keywords: entered top 3, entered page 1, left page 1, - unchanged. Four counts. - -4. Show me three rows: the largest position gain, the largest loss, - and the keyword where position moved by 2 or less but Search - Console clicks on its URL changed by more than 30%. Include SERP - features for that third row. - -5. Pull Search Console query data for the same two 28-day periods, - filtered to the same URLs from step 1. Split reported queries into - brand (containing "[brand]") and non-brand. Report non-brand clicks - separately. Note that query totals can omit anonymized queries - and differ from page totals. - -6. Do not compute an average position across keywords. Do not - invent a score. Put the full table in a separate block at the end. -``` - -Step 4 is the only step that needs judgement, and the prompt hands it a rule instead. Adjust the thresholds to your site, but keep the rule written down so the report is consistent month to month. - -## A report is a request for a decision - -Every ranking report is asking the reader to do something: keep funding this, approve that fix, accept that a term is lost. Write it so the request is visible in the first sentence and supported by the fourth section, and the tracker underneath it becomes the most useful thing in your stack rather than the most ignored. If you have not built the tracker yet, [which keywords to track](/library/rank-tracking/which-keywords-to-track) is where to start. - -## Ranking report FAQ - -### What should an SEO ranking report include? - -The business metric for the tracked pages against the prior period, ranking movement as four counts (entered top 3, entered page 1, left page 1, unchanged), the three rows that explain the movement with SERP features noted, a brand and non-brand split, and a short list of work done and planned. The full keyword table goes in an attachment. - -### How often should I send a ranking report? - -Monthly for most businesses, weekly if a launch or migration is in progress. The tracker can check daily; the report should not, because week-to-week ranking noise is real and reporting it trains the reader to ignore the report. - -### Should a ranking report show average position? - -Not across all keywords. It is a mean of unrelated queries, changes whenever the list changes, and means nothing to a non-SEO reader. Report positions per keyword for the few rows that matter and movement counts for the rest. - -### How do I explain a ranking drop to a client or manager? - -Name the keyword, the previous and current position, the URL, and what is now on the results page, then say what you are doing about it. If the position held and clicks fell, show the SERP feature that took the click. A drop with a cause and a plan is a normal report; a drop with neither is a problem. - -### Can OpenSEO generate a ranking report? - -It provides the pieces: tracked positions with previous positions and SERP features, Search Console clicks by page and query at no credit cost, and an MCP so an assistant can assemble the report from a prompt like the one above. It does not produce a composite score, on purpose. diff --git a/web/content/marketing/library/local-rank-tracking.mdx b/web/content/marketing/library/local-rank-tracking.mdx deleted file mode 100644 index 7c26a4885..000000000 --- a/web/content/marketing/library/local-rank-tracking.mdx +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: "Local rank tracking: your position depends on where the searcher is standing" -description: "For a local business there is no single rank, only a rank at a point on the map. A nine-point grid around one Tennessee service company showed four different businesses holding number one, and the company itself nowhere in the set." ---- - -## Why a local business cannot have one rank - -Ask Google for a plumber from the north side of a town and from the south side, three kilometres apart, and you get two different map packs. Neither is wrong. Google weights proximity heavily for local queries, so the answer moves with the searcher, and a rank tracker that checks from one fixed point reports one of the many answers as if it were the answer. - -Melissa Popp, content strategy director at Rickety Roo, states the rule most local SEOs work from: - -> we know proximity and relevance are two of the most important factors for how you show up in map pack as a local business - -![A quote card reading: Proximity and relevance are two of the most important factors for how you show up in the map pack as a local business. Attributed to Melissa Popp, content strategy director at Rickety Roo, from The Unscripted SEO Interview Podcast.](/library/rank-tracking/quote-proximity-and-relevance.png "Melissa Popp on proximity and relevance") - -Her episode on fixing your Google Business Profile before anything else is [on the Unscripted SEO podcast](https://unscriptedseo.com/melissa-popp-go-fix-your-google-business-profile/). The implication for tracking is that a local business needs its position measured from several places, not one, or the number will describe the tracker's location rather than the customer's. - -## What a nine-point grid showed - -Here is the shape of the problem on a real business: an HVAC and plumbing company in a mid-sized Tennessee town. I ran a three-by-three grid, nine points three kilometres apart, centred on the town, on mobile, for two queries. - -For `plumber cookeville tn`, the number one result was a different business depending on the point. One plumber held the top spot across the north and west, a larger plumbing, heating and cooling company held the centre and the south, a third business took the east and south-east, and a fourth appeared at a single north-east point. Every point returned 18 to 20 results. - -![OpenSEO local rank grid for plumber cookeville tn: nine points three kilometres apart, with a different business at number one across the north-west, the centre and south, the east, and the north-east, and the target business matched at none of them](/library/rank-tracking/local-rank-grid-cookeville-openseo.png "Local rank grid in OpenSEO") - -For `hvac repair cookeville`, the split was cleaner: one company held number one at seven of nine points and a second company took two of the three points along the southern edge. - -So over a six-kilometre square, a searcher could be shown five or six different businesses at number one depending on which query they typed and where they typed it. A single-point tracker reporting "position 1" or "position 4" for this town would be reporting one of those cells and calling it the market. - -The company I was checking on appeared at none of the eighteen points. The grid's name-match returned nothing, and a listings search within 15 kilometres found no business under the name the company uses on its website. That is a finding in its own right, and it comes before any question of rank: the first thing a local grid tells you is whether you are in the set at all. If the profile exists under another name, that name needs to go into the tracker; if it does not exist, no amount of rank tracking will find it. - -## Storefront or service area - -Set up your Business Profile to match how you serve customers. [Google allows eligible hybrid businesses](https://support.google.com/business/answer/9157481) to show a storefront address and a service area when they both receive customers and travel or deliver to them. If customers do not visit your address, hide it and list the area you serve. - -Melissa describes the difficulty of competing across locations: - -> it can be a lot easier to rank an actual brick and mortar location than it can be a service area page because you open up the competitiveness - -Choose the profile settings that describe your business accurately. You can also build useful location pages for the neighbourhoods and towns you serve to reach customers through organic website results. [Map positioning to real demand](/library/keyword-research/positioning-to-demand) covers finding which of those towns search for it. - -## Which points to track - -Run a Maps grid to survey your visibility, then repeat it with the same settings to compare positions over time. Choose points where your customers search. Melissa's method for finding them: - -> there's also testing that needs to be involved there to figure out, know, like what areas, you know, looking in GA4 and other analytics tools, you know, where are people searching from down to at that city level - -That gives you two to five named places. Separately, OpenSEO rank trackers measure your website's organic rankings from a chosen location. They do not track your Business Profile's Maps position. A business serving three towns can use three small trackers to follow its organic results. Twenty keywords in three locations is 60 checks a week, which on the queued schedule is about $0.60 a month. - -The grid itself is priced per point: a 3x3 grid is nine Maps searches, a 5x5 is twenty-five, and the app shows the cost before it runs. Use the 3x3 for a quarterly survey and the 5x5 when you are deciding whether to open or list a second location. - -## What to do with a bad grid - -If you rank well at the centre and fall off two kilometres out, that is proximity doing what proximity does, and the fixes are the ones Melissa puts first: a complete, accurate profile in the right category, reviews you answer, and hours and contact details that are correct. If you rank badly everywhere, check the category and the name before anything else. And if you are absent, as the company above was, the tracker has done its job on the first run by telling you the problem is upstream of rankings. - -This is also a budget conversation. Melissa on the size of budget she can work with: - -> I can work with $500. It doesn't mean that we are going to have all the results we want in six months or a year. But I can move the needle with $500 - -A local grid and three small trackers are a few dollars of that, and they are what stops the other $490 being spent on the wrong town. - -## Run this with the OpenSEO MCP - -Connect the [OpenSEO MCP](/docs/mcp): use the grid for Maps positions and a rank tracker for organic website rankings. The [local-seo](/docs/skills/local-seo) skill packages this workflow. - -```text -Using the OpenSEO MCP on project [name] for [business name] at -[address or coordinates]: - -1. Search local businesses within 10 km for "[business name]" and - confirm the listing exists. Give me its exact title, its cid, and - its primary category. If nothing matches, stop and tell me. - -2. Run a 3x3 local rank grid, 2 km spacing, on mobile, for - "[main service] [town]", using the cid from step 1 as the target. - Show me the rank at each point and the business holding #1 at - each point. - -3. Repeat for "[second service] [town]". - -4. Summarise: at how many of the 9 points am I in the top 3, how - many in the top 10, and which competitors hold #1 where I do not. - -5. For organic website rankings, separately create one rank tracker - per location I name, weekly, mobile, depth 20. Estimate the cost - with 20 keywords each before adding anything. Explain that these - trackers do not measure Maps positions. -``` - -Step 1 is the one that would have saved the company above a quarter of guessing. Match on the cid, not the name, once you have it. - -## Local rank tracking FAQ - -### Why do my local rankings look different on my phone and my colleague's? - -Because you are standing in different places, or Google thinks you are. Proximity is one of the strongest signals for local queries, so two searchers a few kilometres apart routinely see different map packs. A tracker that checks from one point reports one of those views. - -### What is a local rank grid? - -A set of searches run from points on a grid around a location, usually 3x3 or 5x5, reporting where a business ranks at each point. It shows how far a business's visibility reaches and where competitors take over, which a single rank check cannot. - -### How many points should a local rank grid have? - -Nine, spaced to cover the area you serve, is enough to see the shape. Use twenty-five when you are making a location decision. Beyond that you are paying for resolution you will not act on. - -### Should a service-area business set up a service area or an address on Google? - -Use the settings that match how you serve customers. An eligible storefront that also visits or delivers to customers can show both an address and a service area. If customers do not visit your address, hide it and list your service area. - -### Does OpenSEO do local rank tracking? - -Yes. The MCP local rank grid measures Business Profile positions in Maps. Scheduled rank trackers measure organic website rankings from a chosen location. Both use credits. On the hosted app, scheduled and live rank-tracker checks require the $10/month plan, which includes $10 of credits. diff --git a/web/content/marketing/library/long-tail-question-mining.mdx b/web/content/marketing/library/long-tail-question-mining.mdx deleted file mode 100644 index 0b8a5e074..000000000 --- a/web/content/marketing/library/long-tail-question-mining.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: "The queries your competitors can't see" -description: "Long-tail keywords are where small sites win. What they are, how to find them without paying for a suite, and how to mine the question queries Google hands you for free." ---- - -## What are long-tail keywords? - -Long-tail keywords are longer, more specific search queries, usually three or more words, that individually get fewer searches but collectively make up most of Google's traffic. "Keyword research" is a head term; "how to do keyword research for a local restaurant" is long-tail. They bring lower volume, much lower competition, and clearer intent. - -## Long-tail vs short-tail keywords: why the tail converts - -A short-tail query is an audience; a long-tail query is a person with a problem. Someone searching "crm" is browsing. Someone searching "crm for solo real estate agents under $20" is buying. The tail trades volume for intent, and intent is what converts. - -## Long-tail keyword examples - -- **Head:** running shoes → **Tail:** best running shoes for flat feet and plantar fasciitis -- **Head:** keyword research → **Tail:** how to do keyword research without a paid tool -- **Head:** email marketing → **Tail:** email marketing laws for cold outreach in canada - -## How to find long-tail keywords (3 free methods) - -1. **Mine People Also Ask + autocomplete fan-out.** Type your head term, harvest every PAA question, then re-search each question and harvest again. Two levels deep gives you 30–50 real questions people ask. -2. **Pull your own Search Console queries.** Your GSC data is a long-tail generator that no tool can match. Filter [queries with impressions but no clicks](/blogs/dark-queries): those are tails you already half-rank for. The full strategy for working that data in bulk is [programmatic discovery with Search Console](/library/keyword-research/gsc-programmatic-discovery). -3. **Expand seeds in OpenSEO.** Feed your [seed list](/library/keyword-research/seed-from-conversation) into [keyword research](/features/keyword-research) and keep the specific, question-form results with clear intent regardless of volume. "0 volume" queries are measured, not dead. - -![OpenSEO keyword research results for the seed "running shoes for flat feet", returning long-tail variants like "best running shoes for flat feet men" with volume, difficulty, and intent columns](/library/keyword-research/long-tail-keyword-filter-openseo.png) - -Run this workflow with the [OpenSEO MCP](/docs/mcp): - -```text -Using the OpenSEO MCP: research keywords for the seed "[your topic]", -then return only queries with 4+ words or question form (how/what/why). -Group them by intent and suggest one page per group. -``` - -## Long-tail keyword FAQ - -### What are long-tail keywords in SEO? - -Specific multi-word queries with lower individual volume but higher combined traffic and clearer intent than head terms. They're the fastest way for a newer site to rank, because competition concentrates on head terms. - -### How do I use long-tail keywords in content? - -One intent per page. Make the long-tail query the H2 (or H1) verbatim where natural, answer it in the first paragraph, then earn depth below. Don't scatter twenty tails across one page; [cluster related tails](/library/keyword-research/cluster-topical-hubs), then split by [intent](/library/keyword-research/search-intent-mapping). - -### Is there a free long-tail keyword generator? - -Google gives you two: autocomplete and People Also Ask. Your Search Console is the third and best; it's your site's actual tail. OpenSEO connects your Search Console and expands what you find into full keyword lists. You can start for free; paid plans start at $10/month. diff --git a/web/content/marketing/library/opportunity-sizing-forecasting.mdx b/web/content/marketing/library/opportunity-sizing-forecasting.mdx deleted file mode 100644 index 72c1a7b60..000000000 --- a/web/content/marketing/library/opportunity-sizing-forecasting.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: "Size the opportunity before you commit the quarter" -description: "SEO forecasting turns a keyword cluster into a defensible traffic and revenue range, so you can argue for the work in the only two numbers a business treats as exact." ---- - -## What is SEO forecasting? - -SEO forecasting estimates the traffic and revenue a keyword cluster could return, before you spend anything building for it. The output is a range rather than a number, because every input is an estimate and pretending otherwise is how forecasts lose credibility on contact with reality. - -The arithmetic is short. Take the combined search volume for a cluster, apply a click-through-rate curve for the position you expect to reach, apply your conversion rate, then apply the value of a conversion. Present the result as three scenarios rather than one figure. Conservative assumes position eight and your worst converting month. Aggressive assumes position three and your best. Build the middle scenario from your typical conversion rate and the position you expect to reach. - -## How to size a keyword cluster before you invest - -Sizing runs on the cluster, not on the keyword. A single term tells you nothing about the page you would build, because the page will rank for dozens of variations. [Clustering first](/library/keyword-research/cluster-topical-hubs) gives you a unit worth forecasting. - -For each cluster, gather four inputs: - -1. Combined monthly volume across every keyword in the cluster. -2. The difficulty of the terms you would need to beat, which sets how long the climb takes. -3. Your realistic ceiling position, judged from what already ranks and what you can credibly publish. -4. The conversion value, taken from your own analytics rather than an industry average. - -Difficulty deserves more weight than it usually gets. A cluster worth 5,000 searches a month behind a wall of established domains is worth less than a 400-search cluster nobody has bothered to answer, because you will reach the second one this quarter and the first one never. That comparison is the entire point of sizing. - -## Why volume is the wrong number to lead with - -Sorting by volume selects for the terms your competitors have already spent years defending. It also selects against the terms that convert. The pattern repeats across niches: the highest-volume keyword in a market is usually informational, and the terms carrying real buying intent sit an order of magnitude below it. - -![OpenSEO keyword research for a Nashville kayak rental topic, showing search volume alongside keyword difficulty, CPC, competition, and intent](/library/keyword-research/keyword-difficulty-volume-openseo.png) - -OpenSEO keeps volume and difficulty separate because they answer different questions. In the run above, the 10-search term has a difficulty score of 28, while the 6,600-search term scores 15. Neither number establishes value on its own. Use an expected rank, click-through rate, conversion rate, and conversion value to compare the business case instead of treating volume or difficulty as an opportunity score. - -Paul Baterina ran the test that shows what disciplined sizing buys you. Working on long-tail category pages, he built a proper control group rather than shipping on instinct: - -> We're four weeks in, we're seeing a 25% lift on the test group versus the control group. The control group is actually down 5%. So that's already a signal that I could roll this out to all the long tail pages, which we have about 40,000 of those. - -Four weeks and one controlled test turned a guess about 40,000 pages into a decision with evidence under it. That is what a forecast is for. It is not a promise about traffic, it is a way to find out whether the bet is worth placing at scale. - -## Report in the numbers the business already trusts - -Traffic is the number SEOs reach for and the number executives discount, because it does not appear on any statement they are accountable for. Ernesto Ortiz puts the problem at the level of measurement rather than tactics: - -> You've used it as a proxy for so long. You've got to have better metrics, and you need to have a clear way to evaluate business results. - -Forecast in sessions if that is all you have, then translate. A cluster forecast that ends at "an estimated 900 sessions a month" invites a shrug. The same forecast carried through a 2% conversion rate and a $400 average order value ends at a number that gets a budget approved or rejected on its merits, which is the outcome you want either way. - -The same arithmetic works in reverse for pages you already own. Take your current position, apply the CTR curve for position three, and the difference is the traffic your existing rankings are leaving on the table. That figure is often larger than anything new content would produce, and it is cheaper to capture. [Search Console discovery](/library/keyword-research/gsc-programmatic-discovery) finds those pages. - -## Do it with OpenSEO - -Connect the [OpenSEO MCP](/docs/mcp) and the sizing runs against live difficulty and volume data instead of a stale export. - -```text -Using the OpenSEO MCP, research keywords for "[my topic]" and cluster -the results by intent. - -1. Size each cluster -For every cluster, report combined monthly volume, median keyword -difficulty, and the domains currently holding the top 3. Flag any -cluster where the top 3 are all sites I cannot realistically outrank. - -2. Forecast a range -For the 5 most winnable clusters, model traffic at position 3, -position 5 and position 8 using a standard CTR curve. Show me -conservative, moderate and aggressive scenarios. - -3. Convert to money -Apply a [2]% conversion rate and a [$400] conversion value to each -scenario. Rank the clusters by moderate-scenario revenue per unit of -difficulty, not by volume. - -Output as a document I can review. -``` - -Ranking by revenue per unit of difficulty is the step that changes the plan. It routinely promotes a cluster sitting eighth by volume to first by value, which is the whole argument the rest of this library makes. - -## What to do first - -Take the cluster you were about to write for and run the three scenarios against it. Then run them against one cluster you dismissed for having low volume. If the low-volume cluster wins on revenue per unit of difficulty, you have found the order your content calendar should have been in. - -## SEO forecasting FAQ - -### How accurate is SEO forecasting? - -Directionally useful and precisely wrong, which is why the output should be a range. Volume figures are estimates, CTR curves are averages across wildly different SERPs, and ranking timelines depend on competitors who are also working. Forecast to compare opportunities against each other, not to promise a number. - -### What is a good SEO ROI? - -It depends on conversion value and payback window rather than a benchmark. The useful calculation is cost of the content and links against forecast revenue over 12 months, and whether that beats what the same budget would return in paid. SEO usually loses that comparison in month one and wins it by month nine, so the timeline matters as much as the multiple. - -### Which SEO KPIs matter? - -Leads and revenue are the two numbers a business treats as exact. Everything else, including rankings, sessions and impressions, is diagnostic: useful for explaining why the exact numbers moved, weak as a target in its own right. - -### How do you calculate potential traffic from keywords? - -Combined cluster volume, times the CTR for your expected position, gives estimated sessions. Multiply by conversion rate and conversion value for revenue. Run it three times at three positions so you finish with a range rather than a single figure nobody should trust. diff --git a/web/content/marketing/library/positioning-to-demand.mdx b/web/content/marketing/library/positioning-to-demand.mdx deleted file mode 100644 index 88482a817..000000000 --- a/web/content/marketing/library/positioning-to-demand.mdx +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: "Check that somebody searches for what you call it" -description: "Positioning that no one searches for is a marketing problem disguised as an SEO problem. Validate the words before you build a company around them." ---- - -## Does your positioning have any search demand behind it? - -Most keyword research starts after the naming is done. The category is chosen, the tagline is written, the site is built, and only then does anyone check whether the words on the homepage match anything a person types. By that point the words are expensive to change. - -The check itself takes minutes. Take the term you use to describe what you sell, and look up whether it has search volume, what the results for it show, and what people search instead. Three outcomes are possible. The term has demand and matching results, so you compete for it. The term has demand but the results answer a different question, so your positioning collides with an intent you cannot win. Or the term has no demand at all, which is the case worth planning for, because it is the most common and the least discussed. - -## The name with no market behind it - -George Little now runs a consultancy with a clear offering. An earlier version of the same company did not survive, and the reason was linguistic: - -> I had another version of this company that did not succeed a couple of years ago that was basically trying to push fractional creative director. No market for it, no search around it. Speaking of SEO, that's really a good place to start. Is anyone even Googling this? And they weren't. So I realized that what they were Googling was agencies and freelancers and this kind of consultant in between. - -Running his term through the tool now shows what he found the hard way: - -![OpenSEO keyword research for "fractional creative director", returning a total of nine keywords, with the term itself at 140 searches a month and the surrounding results labelled informational or navigational](/library/keyword-research/positioning-demand-validation-openseo.png) - -Nine keywords in the entire universe. The term itself does 140 searches a month, and almost everything around it is informational or navigational: people looking up what fractional working means, or searching for jobs. The one term with real volume is a company name. There is no row here belonging to somebody trying to hire this role, which is the demand a business built on the phrase would have needed. - -"Is anyone even Googling this" is the whole strategy in five words. Notice what the check gave him beyond a yes or no. It also returned the vocabulary the market does use: agencies, freelancers, and the space between them. A term with no volume is not only a warning. It is a pointer to the words that carry the demand instead. - -This is the failure mode behind a lot of category creation. Inventing a name for what you do feels like differentiation, and it reads as differentiation on a pitch deck. In a search box it reads as nothing at all. You can still use the invented term inside the relationship, in sales calls and in messaging where you have someone's attention. What you cannot do is expect it to be the door people walk through. - -## The opposite case: language that already has a wave behind it - -The inverse is more useful, and rarer, because it involves matching demand you did not have to create. Holly Homer learned SEO by accident this way. She wrote a review of a product from a late-night infomercial, and by her own account she had no idea what she was doing: - -> I parroted the keywords from the infomercial not knowing what I was doing and just would give my two cents. - -Her blog ran three to five thousand page views a week. The post took it to a hundred thousand. - -> It was because I hit the first page. Actually, it was the first result for pajama jean review at the time. And what I found out later was that Pajama Jeans was doing this massive radio campaign. And so people were going to Google and looking for pajama jean information, and they were landing on my blog. - -She won by using the exact language a national ad campaign was pushing into people's heads at the moment it was pushing it. She did not invent a term or educate a market. She matched vocabulary that already existed and rode demand somebody else paid to create. - -Both stories point the same direction. The words that work are the ones already in circulation, and finding them is [seeding from conversation](/library/keyword-research/seed-from-conversation) applied to your own positioning rather than your content calendar. - -## How to validate a name before you commit - -Run this before the logo, and certainly before the domain purchase: - -1. Look up your candidate term and its obvious variants for search volume. Zero volume is a finding, not a dead end. -2. Search the term and read the top ten. If the results answer a question you do not answer, the term is claimed by a different intent. -3. Look up the terms your customers used in the last ten sales conversations. That list is the demand-side vocabulary. -4. Compare. Where your language and their language differ, theirs wins for anything a stranger has to find. - -Step four is where people flinch, because the customer's phrasing is usually less distinctive than the one the founders like. Keep the distinctive term for the pitch and the relationship. Use their words on the pages that have to be found by somebody who has never met you. - -## Do it with OpenSEO - -```text -Using the OpenSEO MCP, research "[my category term]" and these -variants: [list]. Report search volume, difficulty, and intent for each. - -For any term returning little or no volume, tell me what related terms -DO carry volume, and what the top 10 results for those terms are -selling. I want the vocabulary the market uses instead of mine. - -Then compare that list against how my customers describe the problem -in their own words: [paste from sales calls or support tickets]. -Show me where my language and their language diverge, and which of -their phrasings I should own on the pages a stranger has to find. -``` - -## What to do first - -Take the phrase on your homepage that describes what you sell, and look up its volume. If the number is zero, the sentence is doing no acquisition work, and the words that would are sitting in your last ten sales calls. That is a cheap discovery to make now and an expensive one to make in a year. - -## Positioning and search demand FAQ - -### Should I invent a name for my category? - -Only with a plan for the demand gap. An invented term has no search behind it on day one and may never earn any, so anything a stranger needs to find has to be findable through the vocabulary that already exists. Keep the invented term for the pitch, where you have someone's attention. - -### What if my keyword has no search volume? - -Treat it as a signal about the market rather than a limitation of the tool. Zero volume for a category term usually means people describe the problem differently, and the related terms that do have volume will show you how. Low volume is different from zero, and can be worth owning when the intent is strong. - -### How do I find the words my customers use? - -Sales calls, support tickets, and recorded interviews, which is the same source that produces good seed keywords. People describe problems in language that no keyword database originates, because the database only knows what has already been typed enough times to register. diff --git a/web/content/marketing/library/referring-domains.mdx b/web/content/marketing/library/referring-domains.mdx deleted file mode 100644 index 134eae43d..000000000 --- a/web/content/marketing/library/referring-domains.mdx +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: "Referring domains, not backlinks: the count that moves rankings" -description: "One site shows 2,393 backlinks and 308 referring domains, and 872 of those backlinks come from a single domain its owner also runs. How to report the number of distinct sources and spot a profile dominated by a few sites." ---- - -## What is a referring domain? - -A website that links to yours at least once. The backlink count is every individual link; the referring domain count is how many different sites those links come from. A site with 2,000 backlinks from 20 domains has 20 votes, cast repeatedly. A site with 300 backlinks from 300 domains has 300 votes. - -Here is the difference on a real site. seoarcade.com shows 2,393 backlinks and 308 referring domains. The top referring domain, unscriptedseo.com, accounts for 872 of those backlinks on its own, and the site's owner runs both: - -![OpenSEO referring domains table for seoarcade.com: unscriptedseo.com 872 backlinks from 831 pages, castos.com 515, semanticjuice.com 145, jeremyriveraseo.com 80, then a long tail of podcast directories and partner sites, each with rank, spam score and first-seen date](/library/link-building/referring-domains-openseo.png "Referring domains in OpenSEO") - -The first four domains supply 1,612 backlinks, two thirds of the total. Two of the four are the owner's own properties and one is the podcast host. A report that says "2,393 backlinks" is describing a site with a few hundred real endorsements and a lot of repetition. That is normal and it is not a problem. It only becomes one when the big number is the one that gets reported. - -## Why count distinct sources? - -Referring-domain count helps distinguish broad coverage from repeated links on a few sites. It is useful context, but it does not tell you how much any individual link helps rankings. Look at who is linking and why before treating an increase as progress. - -Rob Bonham puts the endpoint of that logic plainly: - -> whoever has the bigger footprint and the more people talking about them across the web in a positive way, that is the ultimate and all be all of winning and dominating the SERPs. - -![A quote card reading: Whoever has the bigger footprint and the more people talking about them across the web in a positive way, that is the ultimate and all be all of winning and dominating the SERPs. Attributed to Rob Bonham, fractional SEO consultant, from The Unscripted SEO Interview Podcast.](/library/link-building/quote-bigger-footprint.png "Rob Bonham on footprint") - -His episode on SEO in the agentic age is [on the Unscripted SEO podcast](https://unscriptedseo.com/rob-bonham-seo-agentic-age/). Footprint is a count of places, not a count of links. - -## Which referring domains count for more - -Not all 308 are equal, and the difference is not the domain's authority score. The idea that comes up most on the podcast is distance to seed: how many links stand between a site and the sites Google trusts most in its field. Jeremy Rivera, the podcast's host, explains it to guests the same way each time, here to Christopher Gimmer of Snappa: - -> if you are truly in that niche, then you are connected to them by one, two or three hops. You know, like it's a like Kevin Bacon for for backlinks. - -Christopher's read on what that produced across the last two years of updates: - -> anyone who's had like a legitimate product within a a specific space tend to be doing much better than just a a pure content site. - -His episode is [here](https://unscriptedseo.com/christopher-gimmer-bootstrapped-saas-ai-overviews/). Google keeps a set of trusted sites per field, and a link counts for more the fewer hops it is from one. A referring domain that is itself linked from the trade press in your industry is close. A referring domain with a high score earned in an unrelated field is far away, whatever its number says. - -You cannot see the seed list. You can see, in the referring domains table, which of your linking sites are themselves the kind of site people in your field cite. Those rows are worth more than their rank column shows. - -## What to do with the table - -Three things, in this order. - -**Report referring domains, not backlinks.** In the monthly report, the line is "308 referring domains, up from 290" and not "2,393 backlinks". Then add one row: how many of the new domains are ones you had nothing to do with. - -**Find the concentration.** Sort by backlinks and look at the top five. If they are your own properties, your host, or a directory that lists every page, they are context, not endorsements. The site above has three of the four in that category. Knowing that stops you from reading a jump in backlinks as a jump in authority when it was a new template link on a site you own. - -**Check first-seen dates.** In the referring-domains table, the date describes the domain. In backlink rows, it describes that individual link; a recent backlink does not prove a domain is new. The growth chart on the overview shows both lines over a year; on this site, backlinks climbed from around 1,900 to 2,393 over the year while referring domains went from around 110 to 308, most of that in the last two months, which is a different story from the backlink line alone. When the two lines diverge, the referring domains line is the one that describes what changed. - -![OpenSEO backlinks overview for seoarcade.com: 2,393 backlinks, 308 referring domains, 2,137 referring pages, rank 41, spam score 5.0, 17 broken backlinks and 47 broken pages, with a year of backlink and referring-domain growth and a new-versus-lost chart](/library/link-building/backlink-overview-openseo.png "Backlinks overview in OpenSEO") - -## Run this with the OpenSEO MCP - -Connect the [OpenSEO MCP](/docs/mcp). The overview is about 50 credits per domain and includes the referring domain breakdown; a page of 100 backlink rows is about 30. - -```text -Using the OpenSEO MCP on project [name], report on referring domains -for [domain]: - -1. Call get_backlinks_overview with scope "domain". Give me backlinks, - referring domains, and the ratio between them. - -2. From the top referring domains in that response, list the top 10 - by backlink count with their share of total backlinks. Flag any - that are my own properties, my hosting or podcast platform, or a - directory. - -3. Call get_backlinks_profile with scope "domain", mode - one_per_domain, sorted by firstSeen descending, page 1, 100 rows, - hideSpam true. List the domains represented by backlinks first - discovered in the last 30 days in this sample, with anchor text - and target page. Do not call them newly acquired domains: the - date belongs to the selected backlink, not the domain. - -4. For each sampled domain, check whether its linking page is about - the same topic as its target. Mark any you cannot verify. - -5. Report the current referring-domain total, then the number of - sampled domains with recent backlinks that appear relevant. Label - the sample size and filters. Do not infer a monthly acquisition - count from this sample. -``` - -Step 5 is the number the business should see. Step 2 is what stops you from over-reading it. - -## One number, and the story behind it - -A backlink total goes up when you add a link to your own footer. A referring domain total goes up when someone new decides your page is worth pointing at. Report the second, know what the top of the list is made of, and spend the link building effort on [sites that have not linked to you yet](/library/link-building/how-to-get-backlinks). - -## Referring domains FAQ - -### What is the difference between backlinks and referring domains? - -A backlink is one link from one page. A referring domain is one website that links to you, however many times. Two thousand backlinks can come from twenty sites. Referring domains is the count of distinct sources and the better measure of how many sites endorse yours. - -### How many referring domains do I need? - -Enough to match the pages that outrank you for the terms you want, from sites in the same field. Check the competitors' profiles for the number; a [backlink gap analysis](/library/competitive-analysis/backlink-gap-analysis) shows the domains that link to them and not to you. - -### Are links from my own other websites worth anything? - -A little, and less each time. They are real links, but a search engine can see common ownership and hosting, and the diversity that moves rankings comes from sites you do not control. Count them separately so they do not inflate the total. - -### What is distance to seed? - -The number of link hops between a site and the sites a search engine trusts most in a field. A link from a site that is itself cited by the industry's authorities is close to the seed and counts for more than a link from a high-scoring site in an unrelated field. You cannot see the seed list; you can judge whether a linking site is one your industry cites. - -### Does OpenSEO show referring domains? - -Yes. The backlinks overview reports the referring domain count and the top referring domains with their backlink count, rank, spam score and first-seen date, and the growth chart shows backlinks and referring domains over the last year. The free backlink checker shows the summary without an account. diff --git a/web/content/marketing/library/run-seo-from-your-ai-assistant.mdx b/web/content/marketing/library/run-seo-from-your-ai-assistant.mdx deleted file mode 100644 index 92fdf9388..000000000 --- a/web/content/marketing/library/run-seo-from-your-ai-assistant.mdx +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: "Run SEO from your AI assistant: the MCP workflow" -description: "Connect one server and the assistant you already use can read Search Console, pull keyword data, check rankings and crawl a site, in the same conversation. What MCP is, the first five prompts that pay for the setup, and the row in the data that shows why a human still reads the output." ---- - -## What is MCP, and why does it matter for SEO? - -MCP, the Model Context Protocol, is the standard that lets an AI assistant call tools. Without it, an assistant knows what it was trained on and what you paste in. With it, the assistant can ask a server a question and get the answer as data: the queries your site ranked for last month, the search volume for a term, the position of a keyword on mobile in one city, the pages a crawl found with no title. - -For SEO the difference is the whole job. Most of the work is not writing; it is moving between five tabs, exporting from one and pasting into another, then thinking. An assistant with the data in the conversation does the moving and leaves you the thinking. - -Dan Kurtz, who runs an agency built around agentic customers, describes what his day looks like once the servers are connected: - -> every morning boot the computer up, open up Claude, and it goes using MCP connectors, goes into all of my accounts, scrapes everything. So at a top level I can see everything that needs to be done for clients - -![A quote card reading: Every morning boot the computer up, open up Claude, and it goes using MCP connectors, goes into all of my accounts, scrapes everything. So at a top level I can see everything that needs to be done for clients. Attributed to Dan Kurtz of MissPepper AI, from The Unscripted SEO Interview Podcast.](/library/ai-agent-seo/quote-morning-boot-claude.png "Dan Kurtz on the morning MCP run") - -His episode is [on the Unscripted SEO podcast](https://unscriptedseo.com/dan-kurtz-bots-as-a-customer-segment-markdown-agentic-flywheel/). Adrian Nikolov, whose agency runs its client diagnostics the same way, on the first step of every engagement: - -> That's a completely automated diagnostic that runs through all the MCPs. It crawls the website with screaming frog. pulls all of the relevant keyword data with data for SEO - -His episode is [here](https://unscriptedseo.com/adrian-nikolov-organic-growth-engineering/). Malte Landwehr, who runs product at Peec AI, on what that does to the tools themselves: - -> It's more likely that I tell Claude to create a linear ticket or tell me the status of a linear ticket than me actually logging into linear. - -His episode is [here](https://unscriptedseo.com/episode-recap-malte-landwehr-of-peec-ai-on-consensus-query-fan-outs-and-ai-content-guardrails/). Read the data through one server, write through another, and the assistant is the thing in between. - -## The setup, once - -The [OpenSEO MCP](/docs/mcp) connects to Claude Code, Claude Desktop, Codex and Cursor. You add one server, sign in, and the assistant gains the tools: Search Console reads, keyword metrics, SERP results, domain and backlink data, rank tracking, site audit, local rank grids. Search Console and audit reads use no credits; research calls that hit a data provider do, and each tool says so before it runs. - -Then install the [agent skills](/docs/skills/setup). A skill is a `SKILL.md` file that tells the assistant how to use the tools for one job, so you do not have to explain the workflow each time. There are skills for [keyword research](/docs/skills/keyword-research), [competitor analysis](/docs/skills/competitor-analysis), [site audit](/docs/skills/seo-audit), [local SEO](/docs/skills/local-seo) and [link prospecting](/docs/skills/link-prospecting), plus an [SEO coach](/docs/skills/seo-coach) that picks the workflow if you are not sure which you need. - -Start with [project setup](/docs/skills/seo-project-setup). It saves your goals, positioning, competitors and key pages to the project, and every later prompt reads them. - -## The first five prompts - -Five prompts, in order, that return more than they cost. Each one is a full strategy elsewhere in this library; these are the short forms. - -**1. What am I already ranking for that I am not getting?** Search Console by query, last three months, positions 4 to 20, sorted by impressions. This is the list a [rank tracker](/features/rank-tracking) is built from, and it costs nothing. - -**2. Which of my pages compete with each other?** Search Console by query and page. Flag queries with multiple URLs for review. Check whether those pages serve the same intent and whether the overlap hurts performance before changing links or merging them. [Cluster keywords into topical hubs](/library/keyword-research/cluster-topical-hubs) covers it. - -**3. What do the sites that outrank me have that I do not?** Domain overview and ranked keywords for the top three competitors, minus yours. [Keyword gap analysis](/library/competitive-analysis/keyword-gap-analysis). - -**4. What is broken?** A site audit, then the issues sorted by severity. [The technical SEO audit checklist](/library/site-audit/technical-seo-audit-checklist). - -**5. Where should my links come from?** Your own top pages by referring domains, and the reason each one earned them. The [backlink checker](/features/backlink-checker) lists them. - -None of the five needs a keyword typed by a human. All five need a human to read the answer. - -## The row that shows why - -Here is the first prompt run against a real site through the MCP. The Search Console call returned the queries seoarcade.com ranked for over 28 days, and the top rows are what you would expect: the brand name, a few guest names, `seo for home services` at position 17 with 1,545 impressions. - -![A terminal session in Claude Code calling the OpenSEO MCP get_search_console_performance tool for seoarcade.com over 28 days: the top rows are the brand name and real queries with clicks, impressions and position, and then a query that is the full text of an AI prompt beginning "you are a keyword analysis bot at an adtech company", with 31 impressions at position 10.2](/library/ai-agent-seo/mcp-search-console-query-pull-openseo.png "Search Console through the OpenSEO MCP") - -Then there is a query with 31 impressions at position 10.2 whose text begins "you are a keyword analysis bot at an adtech company. your task is to analyze the provided keyword" and runs for two thousand characters. That is not a person. It is another company's AI agent, pasting its own system prompt into Google, and Google recorded the site as ranking for it. - -An assistant that summarises the table will count that row. A person who reads it will laugh and exclude it. Mason McCumber, an AI automation specialist, has the right description of the tool you are working with: - -> The best way I can describe it is it's a teenager on steroids. Like, it's hopped up on G-Force and it's ready to go. But at the same time, like, it's a teenager at heart. - -His episode is [on the Unscripted Small Business podcast](https://unscriptedsmallbusiness.com/mason-mccumber-on-ai-agents-agentic-workflows-building-smarter-business-systems-show-notes/). The data arrives faster than you could fetch it. The judgement about what it means did not get faster, and it is still yours. [What to automate and what to keep](/library/ai-agent-seo/what-to-automate) is about where that line goes. - -## Run this with the OpenSEO MCP - -Connect Search Console to the project and the [OpenSEO MCP](/docs/mcp) to your assistant. This prompt is the first of the five above, with the filter step the row above makes necessary. - -```text -Using the OpenSEO MCP on project [name]: - -1. Call get_project_context and tell me what the project already - knows: goal, positioning, competitors, key pages. If it is empty, - stop and ask me the four questions the seo-project-setup skill - asks. - -2. Pull Search Console performance for the last 3 months by query, - all rows. Exclude queries containing the brand name and any query - longer than 80 characters or containing "you are a" or a numeric - ID followed by a colon. Tell me how many rows you excluded and - show me three of them. - -3. Keep queries at average position 4 to 20 with at least 100 - impressions. Sort by impressions. Show me the top 20 with clicks, - impressions and position. - -4. Pull the same period by query and page, and for the 20, tell me - which URL ranks and whether any query shows two of my URLs. - -5. Do not recommend content yet. Tell me which three of the 20 you - would want to look at first and why, in one line each. -``` - -Step 2's exclusion count is the tell. On the site above it removes a dozen rows that would otherwise sit in the striking-distance list forever, at position 10, waiting for clicks that no human will ever send. - -## The assistant fetches; you decide - -MCP turns the assistant from a writer into an analyst with access. That is most of the value of the tools you were paying for, in a window you already have open. What it does not turn the assistant into is the person who knows that a two-thousand-character query is a bot, that a position-2 term with no clicks has an answer box above it, or that the client's boss will only read the first sentence. Those stay with you, and the rest of this library is about keeping them there. - -## MCP for SEO FAQ - -### What is MCP in SEO? - -The Model Context Protocol, a standard that lets an AI assistant call external tools and receive data. An SEO MCP server gives the assistant Search Console, keyword, SERP, backlink, rank tracking and audit data inside the conversation, so it can fetch and analyse instead of only writing from what you paste. - -### Which AI assistants work with the OpenSEO MCP? - -Claude Code, Claude Desktop, Codex and Cursor, through one server configuration. The docs carry the setup for each. Agent skills, which are SKILL.md files describing SEO workflows, install alongside it. - -### Does using the MCP cost credits? - -Search Console reads, URL inspection and audit reads use no credits. Calls that fetch from a data provider, such as keyword metrics, SERP results, domain and backlink data, and rank checks, use credits, and each tool states its cost before it runs. The hosted app includes credits with the $10 plan; self-hosted deployments pay their provider directly. - -### Can an AI agent do SEO on its own? - -It can fetch, filter, sort and draft on its own. It cannot tell a bot query from a human one, judge whether a ranking is worth having, or know what the business will act on. The workflows here keep a person at the steps where that judgement happens. - -### What is the first thing to run after connecting the MCP? - -Project setup, then a Search Console pull of queries at positions 4 to 20 with real impressions, with a filter that removes bot queries and junk. It costs nothing and it produces the list every other workflow starts from. diff --git a/web/content/marketing/library/search-console-vs-rank-tracker.mdx b/web/content/marketing/library/search-console-vs-rank-tracker.mdx deleted file mode 100644 index 9ad4ab74d..000000000 --- a/web/content/marketing/library/search-console-vs-rank-tracker.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: "Is Search Console a rank tracker? Where the free data stops" -description: "Search Console tells you an average position across searchers, devices, and countries, for the queries it chooses to show you. That is enough for a lot of sites. Here is how to tell whether yours is one of them." ---- - -## Can you check keyword rankings for free? - -Yes, for your own site, in Google Search Console. It costs nothing, it is Google's own record rather than a third party's guess, and it reports the metric a rank tracker cannot: clicks. For a site that wants to know whether SEO is working, Search Console is the first answer and sometimes the only one needed. - -What it does not give you is a rank. It gives you an average position, and the difference matters more than the name suggests. - -## What Search Console's position is - -Every time your site appears in a result set, Google records the position of your highest result. The number in the report is the mean of those positions across every impression in the date range, every device, every country, and every query that rolls up into the row you are looking at. - -So a query at "position 8.4" might be position 3 on desktop in one state and position 14 on mobile everywhere else. A page at "position 12" might be at 5 for one query and 30 for another. The average is true and it hides the shape. - -Three other properties of the data shape what you can do with it: - -- **Recent data can be preliminary.** The [24-hour view](https://developers.google.com/search/blog/2024/12/recent-data-search-console) shows data within hours. Finalized reports arrive later, and recent figures can change. -- **It keeps 16 months.** Grant Simmons, who ran search at Homes.com and now works with Fiat Growth, points out that this is the part people waste: "generally you get a lot more information when you look at the 16 months of Google search console data to have an understanding of if the article has been around for that long all the queries that have mapped not just the most recent queries I think that's often forgotten." His episode on measurement is [on the Unscripted SEO podcast](https://unscriptedseo.com/grant-simmons-on-entity-seo-llms-and-the-future-of-search-visibility/). - ![A quote card reading: You get a lot more information when you look at the 16 months of Google Search Console data. Attributed to Grant Simmons of Fiat Growth, formerly Homes.com, from The Unscripted SEO Interview Podcast.](/library/rank-tracking/quote-sixteen-months-search-console.png "Grant Simmons on 16 months of Search Console data") - -- **It does not show every query.** A share of your clicks arrive on queries Search Console never reports, for privacy reasons. I have a post that earns about a hundred clicks a month, and the query report explains twelve of them. [The Dark Query Problem](/blogs/dark-queries) is about what to do with the rest. - -There is one more thing worth knowing if you compare Search Console across time. In the autumn of 2025, Google stopped serving 100 results on a single page. Rank trackers had been fetching that page for years to record deep positions, and every fetch counted as an impression for every site on it. When the page went away, desktop impressions fell across the industry at once. If your impressions chart has a cliff that month, that is the cliff, and it was never people. - -## What a rank tracker adds - -A tracker asks Google a specific question on a schedule you set: for this keyword, from this location, on this device, where does this domain appear, and what else is on the page? It records the answer as a number, not an average. - -A tracker puts these measurements together for each scheduled check: - -1. **A single position** for one keyword on one device in one place, comparable week to week. -2. **The ranking URL for that check.** Search Console also reports URLs when you group by query and page; a tracker records which URL appeared in the specific search it ran. -3. **The previous position**, so movement is a number in a column rather than a chart you squint at. -4. **SERP features** on the page: an AI overview, a local pack, a video carousel, a shopping block. When your position holds and your clicks fall, this column is usually the reason. -5. **Competitors' positions**, for terms you do not rank for at all. Search Console has nothing to say about a query you never appeared for. - -Here is what the difference looks like on a real tracker. The 25 keywords below were chosen because Search Console reported seoarcade.com at average positions between 2 and 20 for them. After one mobile check from the US, three had a position; the other 22 were not in the top 40 at all. The averages were true. The positions were not there. - -![OpenSEO rank tracker results for seoarcade.com after one mobile check: 25 tracked keywords sorted by position, three with a position, ranking URL and SERP features, the rest showing no position inside the top 40](/library/rank-tracking/rank-tracker-positions-openseo.png "Rank tracker results in OpenSEO") - -The cost is that every one of those answers is a paid request. In OpenSEO, scheduled checks run through a queue that prices out at a fraction of a cent per keyword per device, so a 25-keyword weekly tracker costs about $0.25 a month; a one-off live check costs around three and a half times more per keyword. On the hosted app, rank checks need the $10 plan, which includes $10 of credits each month. Search Console reads in OpenSEO cost nothing at all. - -## When Search Console alone is enough - -More often than tool vendors admit. Search Console is enough when all of the following are true: - -- You have one location and one language, so the average is not blending markets. -- You can read direction from a chart and do not need a position to the decimal. -- Your questions are about your own pages, not a competitor's. -- The pages you care about are ones Google already shows, so the query rows exist. - -A ten-page consultancy site, a regional service business with one office, a founder checking whether the launch post is gaining: Search Console, filtered to the right page and device, answers the question. Adding a tracker adds a bill and a second dashboard without adding a decision. - -## When you need the tracker - -Four situations, and one of them is usually what tips it. - -- **You are local.** Position depends on where the searcher is standing, and Search Console's country filter is far too coarse. [Local rank tracking](/library/rank-tracking/local-rank-tracking) covers this. -- **You report upward.** "Average position improved from 14.2 to 13.1" is a sentence nobody can act on. "Nine of the twenty terms tied to the pricing page moved onto page one" is. Search Console cannot produce the second sentence without a lot of manual work each week. -- **You need to know why clicks fell while position held.** That is the SERP features column. -- **You want a competitor in the same table.** Search Console is a first-party tool and will never report someone else's site. - -## Use both, in the right order - -The workable pattern is Search Console first and the tracker second. Search Console picks the keywords, because it knows which queries already show your site and how far down; [which keywords to track](/library/rank-tracking/which-keywords-to-track) is that process. The tracker then follows those terms with the precision Search Console lacks. Then Search Console comes back at the end, because clicks are the number the tracker cannot produce, and a report that shows positions moving without showing what the clicks did is half a report. - -## Run this with the OpenSEO MCP - -Connect Search Console to the project, then connect the [OpenSEO MCP](/docs/mcp). The first three steps use no credits. - -```text -Using the OpenSEO MCP on project [name]: - -1. Pull Search Console performance for the last 28 days by query and - device. Show me the 20 queries with the most impressions, with - position on mobile and on desktop side by side. Flag any query - where the two positions differ by more than 5. - -2. Pull the same 20 queries by query and page. For any query that - returns two of my URLs, list both with their impressions. - -3. Pull the last 16 months by date for the whole property and tell - me whether impressions dropped by more than 30% in any single - week, with the week. - -4. Call get_rank_tracker for this project and list the tracked - keywords with their latest mobile position and previous position. - For each one that is also in the Search Console list, put the two - positions in one row: tracker position, Search Console average. - -5. Tell me which keywords I am tracking that Search Console shows no - impressions for, and which Search Console queries with more than - 500 impressions I am not tracking. -``` - -Step 4 is where the difference shows. The tracker position and the Search Console average for the same term will not match, and the gap between them is the blending this page is about. Step 5 keeps the two lists honest with each other over time. - -## The free data is the ground truth for clicks - -Everything a tracker reports is a description of a results page. Search Console is a record of what people did on it. When the two disagree about whether things are going well, the clicks are right and the positions are the explanation. Set them up in that relationship from the start and you will read both correctly. - -## Search Console and rank tracking FAQ - -### Is Google Search Console a rank tracker? - -Not in the usual sense. It reports an average position per query or page, blended across devices, countries, and dates, for your own verified site only. Its 24-hour view shows recent preliminary data; finalized reports arrive later. A rank tracker records one position per keyword, device, and location on a schedule, and can include sites you do not own. - -### Why does my Search Console position not match the rank tracker? - -Because they measure different things. Search Console averages your best position across every impression in the period, so a term that is 3 in one state and 15 elsewhere reports as something in between. A tracker reports the position from one place on one device on one day. A gap of several positions between them is normal. - -### How accurate is Search Console average position? - -It is Google's own count of where your result appeared, so the impressions and clicks are as accurate as any data you will get. The position is accurate as an average; it is not a rank. Filter by device and country and the average becomes much closer to what a searcher sees. - -### How do I check keyword rankings for free? - -Open Search Console, go to Performance, filter to the page or query you care about, and switch on the average position metric. Filter to a single device and country to make the number meaningful. That covers your own site; for a competitor's rankings you need a tool that fetches results, which costs money wherever you do it. - -### Does OpenSEO use credits to read Search Console? - -No. Search Console and URL inspection reads are free in OpenSEO, in the app and through the MCP. Rank tracking checks use credits because they fetch live results, and on the hosted app they require the $10/month plan, which includes $10 of credits. diff --git a/web/content/marketing/library/search-intent-mapping.mdx b/web/content/marketing/library/search-intent-mapping.mdx deleted file mode 100644 index 2556b0517..000000000 --- a/web/content/marketing/library/search-intent-mapping.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "Rank for the searches that are ready to buy" -description: 'Search intent is the "why" behind a query. Map every keyword hot, warm, or cold before you write a word, and you''ll stop producing content that ranks but never converts.' ---- - -## What is search intent in SEO? - -Search intent is the goal a searcher has when they type a query: to learn, to compare, to find a site, or to buy. Google ranks the page that best satisfies that intent, regardless of who "deserves" it. Optimizing a page against the wrong intent is the most common reason content ranks nowhere. - -## The 4 types of search intent - -- **Informational:** "[what are seed keywords](/library/keyword-research/seed-from-conversation)" → wants an answer -- **Commercial:** "[best keyword clustering tool](/library/keyword-research/cluster-topical-hubs)" → wants a comparison -- **Transactional:** "openseo pricing" → wants to act -- **Navigational:** "google search console login" → wants a specific place - -## Hot, warm, cold: the practitioner's intent map - -The 4-type taxonomy is fine for textbooks. For planning a content calendar, temperature is faster: - -- **Hot: ready now.** Transactional and high-commercial queries. Buyer intent keywords like "X vs Y", "X pricing", "best X for [niche]". Build these pages first: smallest traffic, largest revenue. -- **Warm: comparing.** Commercial-investigation queries. They know the problem, they're shortlisting solutions. Comparison pages, use-case pages, and alternative pages that state the tradeoffs. -- **Cold: learning.** Informational queries. Highest volume, longest payback. Their job is trust and retargeting, not conversion. Measure them accordingly. - -![OpenSEO keyword research results for a local SEO services seed with the intent column labeling each keyword informational or commercial](/library/keyword-research/search-intent-mapping-openseo.png) - -Run this workflow with the [OpenSEO MCP](/docs/mcp): - -```text -Using the OpenSEO MCP: research keywords for "[your topic]" and group -results into hot (transactional/commercial), warm (comparison), and -cold (informational). Recommend which 5 hot pages to build first. -``` - -## Search intent FAQ - -### Why is search intent important for SEO? - -Because Google ranks pages that satisfy intent, not pages that mention keywords. A perfectly optimized page against the wrong intent can't win. Read the SERP and you'll see the intent Google has decided the query carries. - -### What are buyer intent keywords? - -Queries that signal purchase readiness: "pricing", "vs", "alternative", "best X for Y", "discount". They're low volume and high competition per click, but still usually your best ROI, because the searcher arrives pre-sold. - -### How do I check the search intent of a keyword? - -Search it. The current top 10 is Google's answer: if it's all listicles, the intent is commercial comparison; all docs and definitions, informational. OpenSEO also auto-labels intent on [researched keywords](/features/keyword-research) in most countries. diff --git a/web/content/marketing/library/seed-from-conversation.mdx b/web/content/marketing/library/seed-from-conversation.mdx deleted file mode 100644 index b2a1f5d13..000000000 --- a/web/content/marketing/library/seed-from-conversation.mdx +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "Seed from conversation, not a volume report" -description: 'Every keyword tool starts with a box that says "enter a keyword", and that first word decides everything downstream. Get it from your customers'' mouths, not from a tool''s suggestions.' ---- - -## What is a seed keyword? - -A seed keyword is the starting term you feed a research tool to generate the full set of related queries. Weak seeds (industry jargon) produce weak results. Strong seeds, the words real customers use when they describe their problem, surface keywords your competitors' tools never see. - -## Why customer conversations beat keyword tools for seed keywords - -Tools recycle each other's databases. Your sales calls, support tickets, and podcast interviews contain phrasings that have never been typed into a tool but get typed into Google every day. The searcher who says "my website doesn't show up when I google my own company" is not searching "seo services." Seed from the first phrasing and you own a lane; seed from the second and you're bidding against everyone. - -## How to build a seed keyword list without a paid tool (5 steps) - -1. **Harvest verbatims.** Pull the exact problem-phrases from your last 10 sales calls, support threads, or reviews. Copy the words, not your summary of them. -2. **Strip to the seed.** "We can't tell if our blog is doing anything" → seeds: "is my blog working", "blog roi", "measure blog traffic". -3. **Add the jargon translation.** For every customer phrase, note the industry term too. You'll need both sides of the vocabulary to catch both audiences. -4. **Validate in OpenSEO.** Feed each seed into [keyword research](/features/keyword-research). You want _any_ measured volume plus clear intent, not big numbers. Zero-volume seeds with real intent still convert. -5. **Keep the losers list.** Seeds with nothing behind them go in a "watch" list. Customer language often precedes search demand by months. - -![Validating a conversation-sourced seed keyword list in OpenSEO's keyword research tool, showing volume and intent for each seed](/library/keyword-research/seed-keyword-list-validation-openseo.png) - -Run this workflow with the [OpenSEO MCP](/docs/mcp): - -```text -Using the OpenSEO MCP: here are 8 phrases my customers actually said: -[paste verbatims]. Extract seed keywords from each, research them, -and tell me which have measured demand vs. which go on the watch list. -``` - -## Seed keyword FAQ - -### How do I do keyword research for free? - -Conversations for seeds (this page), Google autocomplete + People Also Ask for expansion, Search Console for validation. OpenSEO validates and expands what those surface; you can start for free, and paid plans start at $10/month. - -### How do I find LSI keywords? - -"LSI keywords" is tool-industry vocabulary for related phrasings. The fastest free sources are the People Also Ask box and the "related searches" footer. Better still: your customers' own synonyms, which is exactly what conversation seeding harvests. - -### How many seed keywords do I need? - -5–15 strong seeds per topic. Past that you're expanding, not seeding. Feed them into [the long-tail mining strategy](/library/keyword-research/long-tail-question-mining) next. The same customer vocabulary is worth pointing at your own positioning: see [whether anyone searches for what you call yourself](/library/keyword-research/positioning-to-demand). diff --git a/web/content/marketing/library/seo-audit-report-template.mdx b/web/content/marketing/library/seo-audit-report-template.mdx deleted file mode 100644 index c31987db6..000000000 --- a/web/content/marketing/library/seo-audit-report-template.mdx +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: "Write an SEO audit report the client will actually act on" -description: "Most audit reports get read once and filed. A structure that connects each finding to a page, a cost, and a business number, plus what to leave out and why the summary belongs at the front." ---- - -## What should an SEO audit report contain? - -Six things, in this order: what you checked, what is broken now, what it costs, what you propose, who does it, and how you will know it worked. Everything else in a typical audit deck is evidence, and evidence belongs in an appendix. - -The reason most audit reports fail is not that they are wrong. They are usually correct and unusable. A 40-page document listing 1,180 findings across 27 issue types is a faithful description of a crawl, and it gives the person paying for it no way to decide anything. - -Grant Simmons, who ran search at Homes.com through its CoStar acquisition and now works with Fiat Growth, names this as the unsolved problem of the whole business: - -> I think the biggest challenge that most agencies have and most individual consultants have is reporting. - -And he is specific about what is missing from it: - -> I still think it's a primary agency issue where there's no consistency in tying efforts, outcomes back to KPIs for the business. - -That is the test to apply to your own audit report. Point at any row in it and ask which business number moves if the row gets fixed. If you cannot answer for most rows, you have written a crawl summary. Grant's full conversation on entity SEO and measurement is on [the Unscripted SEO podcast](https://unscriptedseo.com/grant-simmons-on-entity-seo-llms-and-the-future-of-search-visibility/). - -## The structure - -### 1. Scope, in two sentences - -What was crawled, how many pages, on what date, and what was excluded. Then the one line most audits omit: whether the crawl succeeded. If the site's bot protection blocked part of the crawl, that belongs in the first paragraph, not a footnote, because it bounds every number that follows. - -### 2. The critical tier, with named owners - -Only the issues that stop a page being reached, rendered, or understood. On a healthy site this is a short list, sometimes empty. Each row gets a URL, the specific detail, and the fix. Do not summarise this section. The client should be able to forward it to a developer without editing it. - -![OpenSEO site audit report for unscriptedseo.com grouped into CRITICAL, WARNING and INFO tiers, showing one critical issue type with 35 pages, nine warning types led by missing meta description on 279 pages, and five informational types, with an Export control above the list](/library/site-audit/audit-report-by-severity-openseo.png) - -### 3. Warnings, grouped by cause and not by URL - -This is the section that decides whether the report gets used. Two hundred and seventy-nine pages missing a meta description is one line, one owner, and one template change. Written out as 279 rows it becomes a document nobody opens twice. - -Name the cause for each group. "The post template does not output a meta description" is a sentence a developer can act on. "279 pages are missing meta descriptions" is a number a client can only feel bad about. - -### 4. What it costs, in the client's units - -Convert each group into the unit the reader already tracks: revenue on the affected pages for ecommerce, form fills for lead generation, sessions for a publisher. Pull the figure from Search Console or analytics rather than from the audit, because the audit does not know which pages matter. - -Grant puts a number on the uncertainty rather than hiding it: - -> most of my tools, I add in a confidence score, because the math is just math and reasoning is just reasoning. - -An estimate with a stated confidence survives being challenged in a meeting. An estimate presented as a fact does not survive the first person who asks how you got it. - -### 5. The proposal, ordered by pages fixed per hour of work - -Order by return, which means severity is an input rather than the sort key. A template change that repairs 279 pages outranks four broken links even though the broken links score higher on severity. Severity is a property of the issue. Return is a property of the fix. - -### 6. The measurement plan - -Two or three specific numbers, with the date you will read them and the date you expect movement: indexed page count, clicks on the affected URL set, crawl errors in Search Console. Write down what would count as the change not working, before you start. - -## What to leave out - -The informational tier does not belong in the body of the report. Twenty-six long titles and 82 heading-order skips are real, and putting them in front of a decision-maker costs you the attention you need for the four things that matter. Export them, attach the file, and reference it in one line. - -The same goes for a composite score. Any tool that emits one number out of 100 creates a conversation about that number: the client asks why it is 62 rather than 80, you explain the weighting, and nobody discusses the broken links. OpenSEO does not compute a site health score at all. The only scores it stores are Lighthouse's own performance, SEO, accessibility, and best-practice numbers for the sampled pages, and those are worth reporting as what they are, a page-level measurement of ten pages, rather than promoting into a headline. - -Chris Green of Torque Partnership makes the case for why report hygiene is not cosmetic, in the context of bot traffic corrupting analytics: - -> it pollutes all of my other data, which then ruins decision making, which then eventually potentially ruins my budget and my remit. - -A wrong number in an audit report travels the same route. It gets copied into a deck, the deck sets a budget, and the budget outlives everyone's memory of where the number came from. Chris's episode is [here](https://unscriptedseo.com/chris-green-do-googles-patents-still-hold-up-in-an-agentic-web/). - -## Run this with the OpenSEO MCP - -The report structure above is a prompt. Because `get_audit_issues` returns a `how_to_fix` string on every issue, an agent can write section 2 verbatim rather than paraphrasing remediation advice it half-remembers. - -```text -Using the OpenSEO MCP on project [name], write me an SEO audit -report for [client] from the most recent site audit. - -Structure it exactly like this: - -1. SCOPE: pages crawled, date, and whether any pages came back with - fetchClass other than "ok". If any did, say so in the first - paragraph and state how many findings are therefore unverified. - -2. CRITICAL: every issue at severity "critical". URL, the detail - from the issue row, and the how_to_fix text unedited. No - summarising. - -3. WARNINGS BY CAUSE: pull severity "warning", group by issueType, - and for each group write one sentence naming the likely cause - (template, migration, editorial). Sort groups by page count. - -4. COST: for the top three groups, list the affected URLs and tell - me which of them appear in Search Console with clicks or - impressions. Flag any group where none of the pages get traffic. - -5. PROPOSAL: order the groups by pages fixed per unit of effort, - not by severity. Say which team owns each one. - -6. MEASUREMENT: three numbers to re-check in 30 days. - -Put the informational tier in an appendix as counts only. Do not -invent a score. -``` - -Step 4 is what turns a crawl into a report. The audit knows which pages are broken; Search Console knows which broken pages anybody was ever going to visit. [Programmatic discovery with Search Console](/library/keyword-research/gsc-programmatic-discovery) covers pulling that side. - -## An audit report is a proposal for work - -An audit report has a job that is easy to lose sight of: getting someone to authorise work. It is a sales document for a set of tasks, and it succeeds when the tasks get scheduled. - -That reframes every editorial choice in it. The 1,180-row export is honest and it does not persuade anybody. Four named fixes with owners, costs, and a re-check date do. Keep the export, attach it, and write the four. - -When the proposed fix is deletion rather than repair, the report needs a different shape again, because you are asking permission to remove pages someone paid to create. [Index bloat](/library/site-audit/index-bloat) covers how to make that case with evidence. - -## SEO audit report FAQ - -### What should be in an SEO audit report? - -Scope and crawl reliability, the critical issues with named owners and exact fixes, warnings grouped by underlying cause rather than by URL, an estimate of what each group costs in the client's own metric, a prioritised proposal, and a short measurement plan with dates. Raw findings go in an appendix. - -### How long should an SEO audit report be? - -The body should be short enough to read in a meeting, which in practice means four to eight pages. Length lives in the appendix. A long body signals that the findings were not triaged, which is the work the client is paying for. - -### Should an SEO audit report include a score? - -Not a composite one. A single number out of 100 invites an argument about the weighting and displaces the conversation about which fixes get scheduled. Report counts by severity, which are checkable, and let the proposal carry the judgement. Lighthouse category scores are fine to include as long as you say how many pages they cover. - -### How do you present SEO audit findings to a client? - -Lead with the decision you want, not the evidence you gathered. Open with the proposal and the cost, keep the critical issues as a forwardable list, group everything else by cause, and put the full export behind a link. Say plainly which numbers are estimates. - -### Is there a free SEO audit report template? - -The structure on this page is the template, and it is more useful than a formatted document because it tells you what goes in each section and what to leave out. OpenSEO can produce every section from a real crawl using the prompt above; it is open source and free to start, with paid plans from $10/month for larger crawls. diff --git a/web/content/marketing/library/skills-memory-and-the-trace.mdx b/web/content/marketing/library/skills-memory-and-the-trace.mdx deleted file mode 100644 index 17bd8a230..000000000 --- a/web/content/marketing/library/skills-memory-and-the-trace.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: "Skills, memory and the trace: make the good run repeatable" -description: "The perfect output you got once is worth nothing if the agent cannot do it again. Three habits from people who run SEO through agents every day: save the workflow as a skill, give the agent a memory it reads every run, and make it write down every step it took. Plus the two checks that catch the confident wrong answer." ---- - -## Why does the agent do it differently every time? - -Because you asked it to. A chat box with a blank prompt is the highest-variance way to use a model. The same instruction, pasted on two days, produces two different routes to two different outputs, and the second one is not worse because the model got worse; it is different because nothing told it to be the same. - -Mason McCumber, an AI automation specialist, has the sentence for it: - -> So repeatability is scalability. And that's the number one thing people are forgetting to realize. They're doing something and they're doing it once. - -And the cost of not fixing it: - -> If we get the perfect output from like some sort of AI model and we can't repeat that you'll you're just kind of battling that Chasing that one high that you got that one time - -![A quote card reading: Repeatability is scalability. And that's the number one thing people are forgetting to realize. They're doing something and they're doing it once. Attributed to Mason McCumber of BuildWithMM, from The Unscripted SEO Interview Podcast.](/library/ai-agent-seo/quote-repeatability-is-scalability.png "Mason McCumber on repeatability") - -His episode is [on the Unscripted Small Business podcast](https://unscriptedsmallbusiness.com/mason-mccumber-on-ai-agents-agentic-workflows-building-smarter-business-systems-show-notes/). Three habits fix it. None of them is clever. - -## 1. Save the workflow as a skill - -A skill is a file that tells the assistant how to do one job: what to pull, in what order, what to check, what the output looks like. OpenSEO ships a set of them for the SEO jobs the MCP supports, and they are the reason the same prompt produces the same shape of work on Monday and on Thursday: - -![The OpenSEO agent skills listed in a terminal: competitive-landscape, competitor-analysis, keyword-clustering, keyword-research, link-prospecting, local-seo, seo-audit, seo-coach and seo-project-setup, each a SKILL.md file the assistant reads before doing that job](/library/ai-agent-seo/openseo-agent-skills-openseo.png "The OpenSEO agent skills") - -Erika Braeger, manager of organic growth strategy at Tenspeed, builds them for her team and is precise about what they are for: - -> the innovation that I work on for my strategy team is clot skills that take away that pre-work so the strategist has all of that time back to focus on what really matters - -Her episode is [here](https://unscriptedseo.com/erika-braeger-tenspeed-unscripted-seo/). The skill does the fetching and arranging. The strategist does the thinking. And the skill does it the same way every time, which is the point. - -Patrick Stox, formerly of Ahrefs and now consulting on his own, on where to start: - -> anything that, you know, is taking too much of your time that you're having to do repeatedly, go just try and you know, build a skill, try and improve it. - -His episode is [here](https://unscriptedseo.com/patrick-stox-on-building-in-the-geo-era/). If you have pasted the same prompt three times, it is a skill. Write it down, name it, and stop pasting. - -## 2. Give it a memory it reads every run - -An agent forgets between conversations, and inside a long one. Mason's fix is a plain-text store the agent reads before it starts: - -> The way that we do it is we do it with Obsidian. Obsidian is a markdown platform that manages uh all of our files. So we make logs of transcripts. We make logs of every single thing that we're working on and that we're doing. - -Markdown, because every model reads it and every editor opens it. Logs of transcripts, decisions, what the client said, what you decided last month and why. OpenSEO's version is the project context: business overview, goal, positioning, competitors, key pages, and a research log so the assistant does not re-buy research it already bought. The [project setup](/docs/skills/seo-project-setup) skill fills it once; every other skill reads it. - -Holly Homer, who runs organic and AI visibility at Digistore24, describes the loop that memory makes possible: - -> where the magic happens is when you are building things that have feedback loops. And so you're like, no, that sucked for these s five reasons. You know, go ahead and redo it now. - -Her episode is [here](https://unscriptedseo.com/holly-homer-on-affiliate-marketing-ai-slop-and-being-replaced-by-your-own-words/). Feedback you give once and the agent forgets is a complaint. Feedback written into the memory it reads every run is a rule. - -## 3. Make it write down every step - -The third habit is the one almost nobody does, and Mason is specific about why it matters: - -> You have to tell it to leave clear traces of everything it does. Cause AI a lot of times will just go. It just goes and then doesn't leave a good trace on how it got to the perfect destination. - -A trace is a log: which tool it called, with what arguments, what came back, what it did with it. It costs a few lines per step. It buys you two things. When the output is right, the trace is the skill you should have written. When the output is wrong, the trace is where you find the step it invented. - -Bradley Benner, who builds AI operating systems for contractors, runs the same discipline before anything executes: - -> I always work in plan mode to begin with, anyways. And so it ends up a a sometimes a very lengthy discussion back and forth before we ever agree to a build plan - -His episode is [here](https://unscriptedseo.com/bradley-benner-on-relevance-over-dr/). Plan, agree, then run, with the plan written down. The trace is the same idea after the fact. - -## The two checks the trace makes possible - -Knight Lancaster, an attorney and CPA in Nashville, on why the confident answer is the dangerous one: - -> the old saying, know, often wrong but never in doubt. You get a confident response, but the second you say, hey, I don't think that's right, I mean, you don't have to push that hard. - -His episode is [on the Unscripted Small Business podcast](https://unscriptedsmallbusiness.com/knight-lancaster-navigating-finances-marketing-law-in-tennessee/). He has watched lawyers file briefs with invented citations, and his diagnosis is not the model: - -> I really think the fundamental issue is it's just a gap in the process. - -The gap is the last check. Two of them, both cheap once the trace exists. - -**The source check.** Every number in the output traces to a tool call in the log. If the agent says a keyword has 1,600 searches, the trace shows `get_keyword_metrics` returning 1,600. If it does not, the number came from the model's memory of the world, which is not a data source. Knight again, on why reading the first five facts is not enough: - -> when somebody hands you a fact sheet that has 27 facts on them and you look at the first four or five and they look right and the rest of them sound good you know you can kind of go sleep mentally - -**The domain check.** Chris Garrett, who has been a programmer and a content marketer for thirty years, sets the boundary: - -> AI tools are really helpful in domains that you're well versed in, but don't try to use it in a domain that you don't know very well because you don't spot the inaccuracies and the hallucinations. - -His episode is [here](https://unscriptedseo.com/chris-garrett-on-the-over-caffeinated-human-brain/). If you cannot tell whether a step in the trace is wrong, the workflow needs someone who can, or it needs to stay smaller than your expertise. - -Erika, after building a library of skills for her team, still ends every run the same way: - -> I still find even with all of my Clawed skills or Claude usage, I still need to run a thorough QA. - -And a practical limit Dan Kurtz hit that no amount of memory prevents: - -> Until you go to bulk update all of your metas and then your hosting provider shuts off that IP range. - -His episode is [here](https://unscriptedseo.com/dan-kurtz-bots-as-a-customer-segment-markdown-agentic-flywheel/). The agent can do anything to your site through an API. Your host may have opinions about how fast. - -## Run this with the OpenSEO MCP - -Connect the [OpenSEO MCP](/docs/mcp) and install the [skills](/docs/skills/setup). This prompt turns a one-off run into a repeatable one and produces the trace. - -```text -Using the OpenSEO MCP on project [name]: - -1. Call get_project_context. If the goal, positioning, competitors or - key pages are missing, run the seo-project-setup skill and ask me - for them before doing anything else. - -2. Run the [keyword-research | seo-audit | competitor-analysis] skill - for [topic or domain]. As you go, keep a log: every tool called, - the arguments, the row count that came back, and what you did with - it. Print the log at the end. - -3. For every number in your summary, cite the log line it came from. - Any number without a log line, mark as "not from data". - -4. Tell me which two steps you would change if you ran this again, - and write the whole run as a SKILL.md I can save, with the steps - in order and the checks at the end. - -5. Call update_project_context to add one line to the research log: - what was run, when, and where the output is. -``` - -Step 3 is the source check, automated. Step 4 is the skill you should have had. Step 5 is the memory the next run reads. - -## The run is the asset - -The output of an agent run is one document. The skill, the memory and the trace are the things that make the next hundred runs cheaper and the wrong ones catchable. Save all three, or you are paying for the same afternoon every week and trusting a confident voice you cannot audit. [Run SEO from your AI assistant](/library/ai-agent-seo/run-seo-from-your-ai-assistant) is where the first run starts; this is how it becomes the second. - -## Skills and memory FAQ - -### What is an agent skill? - -A file, usually SKILL.md, that tells an AI assistant how to do one job: which tools to call in what order, what to check, and what the output should look like. OpenSEO ships skills for keyword research, competitor analysis, site audit, local SEO, link prospecting, reporting and project setup, so the assistant runs each workflow the same way every time. - -### Why does ChatGPT or Claude give different answers to the same prompt? - -Because a blank chat is designed to vary; nothing constrains the route from prompt to answer. Saving the workflow as a skill, giving the assistant a memory it reads before each run, and asking it to log each step all reduce that variance. A skill in particular runs the same steps in the same order. - -### How do I give an AI agent memory? - -A plain-text store it reads before it starts: markdown files of decisions, transcripts and rules, or a structured project context like OpenSEO's, which holds goal, positioning, competitors, key pages and a research log. Feedback written into that store becomes a rule; feedback given once in chat is forgotten. - -### How do I know if an AI SEO report is accurate? - -Make the assistant log every tool call and cite the log line for every number. A figure with no tool call behind it came from the model rather than the data. Then read the whole thing, not the first five rows, and only trust it in a domain where you would spot a wrong answer yourself. - -### Does OpenSEO have project memory? - -Yes. Each project holds a shared context: business overview, current goal, positioning, writing preferences, competitors, key pages and a research log. The MCP reads it with get_project_context and updates it with update_project_context, and the seo-project-setup skill fills it in on the first run. diff --git a/web/content/marketing/library/technical-seo-audit-checklist.mdx b/web/content/marketing/library/technical-seo-audit-checklist.mdx deleted file mode 100644 index d3704f913..000000000 --- a/web/content/marketing/library/technical-seo-audit-checklist.mdx +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: "The technical SEO audit checklist that ends in fixes, not findings" -description: "A crawl of one 318-page site returned 1,180 findings. Thirty-five were critical, and thirty-two of those traced back to four missing URLs. How to run a technical audit that comes out the other end as a work order instead of a spreadsheet." ---- - -## What should a technical SEO audit checklist actually produce? - -A work order: a list of things a developer or an editor can pick up on Monday and finish. Most audits produce a count, a score out of 100, or a red-amber-green table, and none of those is a task. - -Almost every audit stops short of that. The crawler finds the problem, names it, counts the pages, and hands the interpretation back to you. That last step is where audits die, because interpretation is the expensive part and it gets deferred until nobody remembers why the crawl was run. - -Here is what the gap looks like on a real site. A crawl of unscriptedseo.com covered 318 pages and produced **1,180 issues**. Read as a number, that site is a disaster. Read by severity, it is not: 35 critical, 642 warning, 503 informational. All 35 critical rows are the same issue type, broken internal links. Thirty-two of them come from eleven hub pages pointing at the same four URLs that no longer exist. - -![OpenSEO site audit summary for unscriptedseo.com showing 318 pages crawled and 1,180 issues found, split into 35 critical, 642 warning and 503 info, with the Broken internal link group expanded to show its explanation, a How to fix instruction, and the affected URLs each listing its target URL and 404 status](/library/site-audit/audit-issue-how-to-fix-openseo.png) - -Restoring or redirecting four URLs clears most of the critical tier. The other 1,145 rows are real, and none of them are why that site is not ranking. - -## Sort by severity before you read a single URL - -The audit engine assigns every issue one of three severities, and the split is deliberate rather than cosmetic. Of the 27 issue types OpenSEO checks, only four are critical: a crawler that got blocked, a 5xx server error, a broken internal link, and a missing title tag. Fourteen are warnings and nine are informational. - -That ratio tells you how to read any audit. If a report has 1,000 rows and no critical ones, the site has housekeeping to do and no emergency. If it has 12 critical rows, start there and ignore everything else until they are gone. - -The severity tiers map to three different kinds of work: - -1. Critical issues stop a page from being seen or understood. They go to whoever can deploy. -2. Warnings describe pages competing with each other or leaking equity. Duplicate titles, redirect chains, orphan pages. These go into a content or template ticket, and they are usually template-level rather than page-level. -3. Informational issues are style. A title running to 64 characters is worth fixing when you next touch the page and is worth nobody's sprint. - -Group the report by issue type first and by page second. Twenty-six "title too long" rows are one templating decision, not twenty-six tasks. - -## Every issue carries its own fix - -The differentiator in this workflow is small and it changes the economics. `get_audit_issues` returns a `how_to_fix` on every single row, written for that issue type, and the app shows the same text inline when you expand a group. - -For the broken internal link above, the instruction reads: _"Update the link to point at the correct live URL, or remove it. If the target was moved, prefer linking directly to the new URL rather than relying on a redirect."_ For a blocked page it names the user agent to allowlist. For thin content it gives you three options and tells you which one applies when the content exists but is rendered by JavaScript. - -Most audit tools stop at the finding and leave the remediation to a knowledge base article you have to go and find. Carrying the fix in the payload is what lets an agent take the report and open pull requests against it, which is the difference between an audit you read and an audit that gets done. - -Patrick Stox spent years building exactly this kind of tooling at Ahrefs before moving on, and his own summary of the work is unglamorous: - -> And yeah, lots of charts, lots of reports. Page Inspect, site structure, obviously a huge impact on Site Audit. - -His advice for the repetitive parts of the job is the argument for automating remediation rather than diagnosis: - -> Get it to where it's good enough that you're like, I can trust that, or at least 95% of the way there, which takes a lot of the burden off you. - -He said that about a redirect-matching script he wrote after one migration too many. It applies the same way to an audit: the crawl is not the slow part, the triage is, and triage is the part a machine can carry to 95%. His full conversation on building tools in the AI-search era is on [the Unscripted SEO podcast](https://unscriptedseo.com/patrick-stox-on-building-in-the-geo-era/). - -## Check what the crawler was actually served - -Before you trust any part of the report, confirm the crawler saw the same page a browser sees. This is the check that invalidates every other check when it fails, and it is the one most checklists leave out. - -Two things commonly go wrong. The site's bot protection challenges the crawler, in which case the pages come back flagged rather than audited, and you are reading a report about 80 pages while believing you are reading one about 800. Or the edge serves something different to a non-browser client. - -Chris Green, who runs Torque Partnership and spends his time in access logs, has watched the second failure happen: - -> So if you use a CDN like Akamai or Fastly or Cloudflare, maybe that network will automatically like mark down your website and serve it a different content type. Like all of that's possible and relatively straightforward, but even those automated processes are not foolproof. I've seen it get it wrong quite recently. - -The practical version of this check is two columns in the pages report. If `fetchClass` is anything other than `ok`, or if a page you know has 2,000 words comes back with a word count of zero, the crawl is describing the edge and not the site. Chris's episode on patents and the agentic web is [here](https://unscriptedseo.com/chris-green-do-googles-patents-still-hold-up-in-an-agentic-web/). - -## The workflow - -1. Crawl. The free plan covers 50 pages per crawl, which is enough for most brochure sites; larger crawls run to 10,000 pages on the $10/month plan. -2. Check `fetchClass` before anything else. If pages came back blocked, fix access and re-crawl. Everything downstream is wrong otherwise. -3. Read the critical tier only. Fix it or schedule it. -4. Group warnings by issue type and ask which template produced each group. -5. Leave the informational tier in the export and attach it to the next content refresh. - -## Run this with the OpenSEO MCP - -The whole loop runs through the [OpenSEO MCP](/docs/mcp), which means an agent can crawl, triage, and draft the tickets in one pass. - -```text -Using the OpenSEO MCP on project [name]: - -1. Run a site audit on [https://example.com] with a page budget of - [200]. Poll until it finishes. - -2. Before reading any issues, list the pages with fetchClass other - than "ok". If there are more than a handful, stop and tell me - the site blocked the crawl. - -3. Pull the issues at severity "critical". For each one, give me - the URL, the specific detail (target URL and status for a broken - link, status code for an error), and the how_to_fix text as - written. - -4. Now pull severity "warning" and group it by issueType, not by - URL. For each group tell me whether it looks like a per-page - mistake or a template producing the same fault everywhere. - -5. Write the output as a task list ordered by how many pages each - task fixes. Put the informational tier at the bottom as one - line. - -Do not give me a score. -``` - -That last instruction matters more than it looks. Ask for a score and you get a number to argue about; ask for a task list and you get work. - -## Where the checklist stops being a checklist - -Two of the most valuable audit outcomes will never appear as a row in any tool's issue list, because they are decisions rather than defects. - -The first is deletion. A crawl of 800 near-identical pages reports 800 healthy pages. Nothing is broken, and the site still has a problem. That is the subject of [index bloat](/library/site-audit/index-bloat). - -The second is priority against revenue. An audit ranks by severity, which is a property of the issue. Your business ranks by which pages earn, which is a property of the page. Cross the critical list against your [Search Console performance data](/library/keyword-research/gsc-programmatic-discovery) and the two or three fixes that matter separate themselves from the thirty that are merely correct. - -## Technical SEO audit checklist FAQ - -### What is included in a technical SEO audit? - -At minimum: crawlability and access, status codes, canonical and indexability signals, titles and meta descriptions, heading structure, internal linking including broken links and orphan pages, duplicate content, image alt coverage, and server response time. OpenSEO checks 27 issue types across those areas and can optionally run Lighthouse on a sample of up to 10 pages for performance and accessibility findings. - -### How often should you run a technical SEO audit? - -Run one before and after any migration, template change, or platform upgrade, because those are the events that create critical issues. Outside of that, a quarterly crawl is enough for a stable site. Auditing monthly on a site nobody is changing produces the same report every month and trains everyone to ignore it. - -### What is the difference between a technical SEO audit and an SEO audit? - -A technical audit asks whether search engines can reach, render, and understand your pages. A broader SEO audit adds content quality, keyword coverage, and links. The technical layer comes first because a content problem on a page Google cannot fetch is not the problem you have. - -### Why do SEO audits produce so many issues? - -Because most issue types are page-level and most sites are templated, so a single template fault multiplies by the number of pages using it. A report of 1,180 issues across 318 pages usually describes a dozen underlying causes. Group by issue type before you count anything. - -### Is there a free technical SEO audit tool? - -Partly. Google Search Console reports coverage and indexing for your own verified property at no cost, and it is the more reliable source for anything Google-specific. A crawler adds the on-page and internal-link picture that Search Console does not give you. OpenSEO is open source and free to start, with 50-page crawls on the free plan and 10,000-page crawls on the $10/month plan. diff --git a/web/content/marketing/library/what-to-automate.mdx b/web/content/marketing/library/what-to-automate.mdx deleted file mode 100644 index 1fa65efce..000000000 --- a/web/content/marketing/library/what-to-automate.mdx +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: "What to automate and what to keep: the dispatcher rule" -description: "A scheduled rank check runs without anyone watching. A decision about which keywords go in it does not. How to split SEO work into what a schedule does, what an agent does, and what stays with a person, with a rule from someone who builds these systems for a living." ---- - -## Which SEO tasks should be automated? - -The ones with a fixed input, a fixed output and no judgement in between. Everything else needs either an agent with a person reading its work, or a person. - -That sounds obvious and it is where most SEO automation goes wrong, because the temptation is to automate the interesting part. Mason McCumber, an AI automation specialist, describes the agent as a truck driver and you as the dispatcher, and the failure mode in one line: - -> The driver's up so high in the truck and it just drove over someone's grass, but it doesn't know it just drove over someone's grass. - -His episode is [on the Unscripted Small Business podcast](https://unscriptedsmallbusiness.com/mason-mccumber-on-ai-agents-agentic-workflows-building-smarter-business-systems-show-notes/). You are the dispatcher. The agent drives. You tell it where to go and you tell it when it went over the grass, and the second part is not optional, because the agent has a hard time noticing on its own. - -## Three layers, not two - -Most advice splits work into "automate" and "do by hand". The useful split has three layers. - -**Schedules.** Fixed input, fixed output, no model involved. A rank tracker checking 25 keywords every Monday. A Search Console property that syncs on its own. A monthly audit you start with one call. These run without an agent and without a person, and they are the cheapest thing in SEO. The only decision is what goes in, and that decision is made once. - -**Agents.** Variable input, structured output, a person reads it. Summarise what moved in this week's tracker check. Bucket the audit's 1,180 findings by cause. Draft the report from the numbers. The agent does in a minute what took an afternoon, and the person reads the result the way an editor reads a draft. - -**People.** Deciding what to track, what to build, what to say to the client, what to ignore. Anything where being wrong costs more than being slow. - -Mason is blunt about the first layer, and it is the opposite of what the market is selling: - -> if there's a repeated task that you have to keep doing, it doesn't have to be AI. Everyone's putting AI in everything. AI is not for everything. - -Patrick Stox, formerly of Ahrefs, on where the second layer starts: - -> anything that, you know, is taking too much of your time that you're having to do repeatedly, go just try and you know, build a skill, try and improve it. - -His episode is [here](https://unscriptedseo.com/patrick-stox-on-building-in-the-geo-era/). - -Here is the first layer on a real tracker. Twenty-five keywords, mobile, checking the top 40 results, weekly. The estimate before it runs, from the app: - -![OpenSEO rank tracker cost estimate through the MCP for the seoarcade.com tracker: 25 keywords on mobile at depth 40, a live check and the queued weekly check priced separately, with the monthly recurring cost](/library/ai-agent-seo/mcp-rank-tracker-estimate-openseo.png "Rank tracker cost estimate through the OpenSEO MCP") - -That is a schedule. Nobody prompts it. It runs on Monday and the results wait. The agent's job is Tuesday: read what changed and say so. The person's job was two weeks ago, choosing the 25. Choosing them is that decision; reading the [rank tracker](/features/rank-tracking) and writing three sentences is the agent's Tuesday. - -## What not to automate - -Two things, and they are the two most people try first. - -**A process that is not written down.** Bradley Benner, who runs a link building service, on why automation fails in businesses that are not ready for it: - -> If if they don't have clear processes in their own business, you can't automate it because then you're just automating a sloppy process - -His episode is [here](https://unscriptedseo.com/bradley-benner-on-relevance-over-dr/). If you cannot write the steps down for a new hire, you cannot hand them to an agent. The agent will do something, confidently, and you will not know which step it invented. - -**The decision itself.** Kate Hendrickson of The Strategy Lane, on where the human sits: - -> AIs can't be like a Waymo. Like we can't just let it drive itself. We still have to be the driver of a lot of that. - -![A quote card reading: AIs can't be like a Waymo. We can't just let it drive itself. We still have to be the driver of a lot of that. Attributed to Kate Hendrickson of The Strategy Lane, from The Unscripted SEO Interview Podcast.](/library/ai-agent-seo/quote-cant-be-a-waymo.png "Kate Hendrickson on staying the driver") - -Her episode is [on the Unscripted Small Business podcast](https://unscriptedsmallbusiness.com/kate-hendrickson-markets-through-conversations/). She and Mason disagree on the metaphor and agree on the point. Whether you call yourself the driver or the dispatcher, there is a person in the loop at the moment a choice is made, and the systems that skip that person produce work that is fast and wrong. - -One more thing to know about the market: a lot of what is sold as an "SEO agent" is a schedule and a prompt in a wrapper. That is fine, as long as you know that is what you bought and a person still reads the output. - -## Putting the three layers on OpenSEO - -Map the product to the layers and the plan writes itself. - -| Layer | In OpenSEO | Who acts | -| -------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| Schedule | Rank tracker (daily, weekly, monthly), the connected Search Console property | Nobody. It runs. | -| Agent | The MCP tools: read the tracker, read Search Console, bucket audit issues, pull competitors, draft the report | The assistant, with a person reading | -| Person | Which keywords, which pages, which competitors, what to build, what to tell the client | You | - -The schedule layer costs credits per check and the app shows the price before you commit. The agent layer costs nothing for reads (Search Console, tracker results, audit results) and credits for research. The person layer costs the most, which is the reason to keep it small and to give it good inputs. - -## Run this with the OpenSEO MCP - -Connect the [OpenSEO MCP](/docs/mcp). This prompt sets up the schedule layer once and defines the agent's weekly job; the person's job is answering the questions in step 1. - -```text -Using the OpenSEO MCP on project [name]: - -1. Ask me three questions and wait: which 20 to 40 keywords to - track, which device, and what day of the week I read reports. - -2. Call get_rank_tracker. If a tracker exists for my domain, add the - keywords to it; otherwise create one: weekly, the device I named, - depth 40. Call estimate_rank_tracker_cost first and show me the - monthly figure before adding anything. - -3. Call run_site_audit if the last audit is older than 30 days, and - tell me when it will finish. - -4. Write down, as a saved prompt I can paste every week: pull the - tracker results, bucket the movement (entered top 3, entered page - 1, left page 1, unchanged), pull Search Console clicks for the - tracked pages this week versus last, and write three sentences. - -5. Tell me which of the things you just did will run again without - me, which need me to paste the weekly prompt, and which need me to - decide something. -``` - -Step 5 is the three layers, said back to you by the agent. If it puts a decision in the "runs without me" list, that is the grass. - -## Automate the schedule, read the agent, keep the decision - -The tracker runs. The audit runs when asked. The agent reads them and writes the draft. You choose the keywords, read the draft and decide. The work that used to fill the week now fills an hour, and the hour is the part that was always worth paying for. [Human in the loop content](/library/ai-agent-seo/human-in-the-loop-content) applies the same split to writing. - -## SEO automation FAQ - -### What SEO tasks can be automated? - -Anything with a fixed input and output and no judgement: scheduled rank checks, Search Console pulls, site audits started on a fixed day, data exports. An agent can add a second layer, summarising and drafting from that data, as long as a person reads the result. Choosing what to track, what to build and what to tell a client stays with a person. - -### What is the difference between SEO automation and an AI SEO agent? - -Automation runs the same job on a schedule with no model involved, like a weekly rank check. An agent takes variable input and produces structured output using a language model, like reading the check and writing the summary. Many products sold as agents are a schedule plus a prompt; that is useful, but it still needs a reader. - -### Can I automate SEO reporting? - -The data collection, yes: the tracker and Search Console pulls run on their own and use no credits to read. The draft, yes, with an agent and a saved prompt. The sentence that says what it means for the business should still be written or at least read by a person before it goes out. - -### How much does scheduled rank tracking cost in OpenSEO? - -The app estimates before anything runs. As a reference, 25 keywords on mobile checking the top 40 results weekly comes to about $0.25 a month in credits; 100 keywords about a dollar. Scheduled checks go through a queue that is cheaper than one-off live checks. Rank checks on the hosted app need the $10/month plan. - -### Should I automate content production? - -Not end to end. The strategy on [human in the loop content](/library/ai-agent-seo/human-in-the-loop-content) covers the split that works: a person writes the brief, the agent drafts, a person edits. Automating the brief or the edit produces content that reads like every other automated site. diff --git a/web/content/marketing/library/which-keywords-to-track.mdx b/web/content/marketing/library/which-keywords-to-track.mdx deleted file mode 100644 index 992cffadd..000000000 --- a/web/content/marketing/library/which-keywords-to-track.mdx +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: "Which keywords to track, and how many" -description: "A tracker with 500 keywords in it gets ignored by the second month. Pick 20 to 50 from Search Console, tie each one to a page that earns money, and price the list before you add it." ---- - -## How many keywords should you track? - -Fewer than you want to. A rank tracker is a report you have to read every week, and the number of rows you will read is the number you should track. For most sites that is 20 to 50 keywords. An agency running many clients might track a few hundred per site. Nobody reads 500. - -The instinct to track everything comes from a reasonable place: if the tool can do it, why not? Two reasons. Tracking costs money on every check, so 500 keywords on a daily schedule is a real line item. And a list that long hides the ten terms that matter inside 490 that do not, which is how a tracker ends up as a dashboard nobody opens. - -Charlie Birch, founder of Humanize Collective, makes the underlying point about metrics in general: - -> you can always find a big number to point at, you know, out of context or in a sliver, and be like, yay, big numbers, but like big numbers of what - -![A quote card reading: You can always find a big number to point at, out of context or in a sliver, and be like, yay, big numbers. But big numbers of what? Attributed to Charlie Birch, founder of Humanize Collective, from The Unscripted SEO Interview Podcast.](/library/rank-tracking/quote-big-numbers-of-what.png "Charlie Birch on big numbers") - -A 500-row tracker is a big number of what. Her conversation on where brand strategy meets SEO is on [the Unscripted SEO podcast](https://unscriptedseo.com/charlie-birch-on-where-brand-strategy-meets-seo-what-every-founder-needs-to-know/). - -## Where the list comes from - -Not from a brainstorm, and not from the keyword research tool's volume column. The list comes from Search Console, because Search Console already knows which queries Google shows your site for and how far down. Those are the only terms where a rank change can be read against a click change, which is the point of tracking. - -Here is what that looked like on a real site. Three months of Search Console data for seoarcade.com, a small SEO consultancy site, came back with 76 clicks across the top 400 queries, 52 of them on the brand name. That is not much, and the impressions told a different story: - -- `seo for home services`: 4,558 impressions, average position 17.3, two clicks -- `keyword forecasting`: 3,694 impressions, position 20.3, one click -- `white label keyword research`: 3,131 impressions, position 1.8, one click -- `local seo for home services`: 2,861 impressions, position 14.3, two clicks -- `seo sops`: 2,503 impressions, position 11.9, two clicks -- `authority link building`: 1,984 impressions, position 13.1, no clicks -- `international seo keyword research`: 1,917 impressions, position 10.1, one click - -Those are the terms worth tracking. Each one has demand Google is already willing to show the site for, an average position within reach of page one, and a page that exists. A position change on any of them will show up in clicks within weeks, which makes the tracker readable: this moved, and here is what it did. - -One caution before you trust the averages. When these 25 terms went into a tracker and it ran a single mobile check from the US, only three of them came back inside the top 40. The Search Console averages were real, but they were blended across desktop, states, and weeks, and the position a phone in one place sees is a different number. That gap is the subject of [Is Search Console a rank tracker?](/library/rank-tracking/search-console-vs-rank-tracker), and it is the reason to track these terms rather than assume them. - -The third row is the odd one. Position 1.8 on 3,131 impressions and one click means the page is ranking for a query it does not satisfy, or the SERP has a feature above it that takes the click. Search Console cannot tell you which. A tracker that records SERP features per keyword can, and that is a reason to track a term you already rank for. - -## The four buckets - -Build the list in four groups and cap each one. - -**Money pages.** Five to fifteen terms, one or two per page that produces leads or revenue. These are the rows the business cares about, whether or not they move this month. - -**Striking distance.** Ten to twenty terms from Search Console at positions 4 to 20 with meaningful impressions. This is where movement happens and where the tracker earns its keep. Re-pull this bucket quarterly, because the terms change. - -**Brand, as a control.** One or two brand terms. They should sit at position 1. If they move, something is wrong with the site or the listing, and you want to know before anything else. - -**A competitor's term.** One or two terms you do not rank for yet but a named competitor does. They cost the same to track and they turn the report from "how are we doing" into "how are we doing against them". - -That is 20 to 40 rows, and every one of them means something to the person reading the report. - -## Clean the list before it goes in - -Search Console's query list carries junk, and it is worth knowing what it looks like so you do not track it. The same seoarcade.com pull included queries like `12371: multiple keyword serps report` and several that were the full text of an AI agent's prompt, complete with markdown headers. Those are tools and agents hitting Google, not people. They will show impressions and never a click. Leave them out. - -Also decide, per keyword, whether case matters. `OpenSEO` and `openseo` return the same results; `Apple` and `apple` may not. OpenSEO lowercases tracked keywords by default and tracks a cased keyword separately if you ask, which is right for brand names and wrong for almost everything else. - -## Price the list before you add it - -OpenSEO estimates cost before any check runs. On the seoarcade.com tracker, 25 keywords, mobile only, checking the top 40 results, came back as 225 credits for a one-off live check and 63 credits per scheduled weekly check, which is 252 credits or about $0.25 a month. The $10 plan includes $10 of credits, so a list of this size uses under 3% of them. Doubling the depth to 100 results, adding desktop, or moving to daily checks each raise the cost in proportion, and the estimate updates before you commit. - -![OpenSEO rank tracker configuration for seoarcade.com: United States, national targeting, mobile only, weekly, top 40 results, with the cost shown before saving as about $0.0025 per keyword per check and about $0.50 a month for 50 keywords](/library/rank-tracking/rank-tracker-cost-estimate-openseo.png "Rank tracker cost estimate in OpenSEO") - -## Run this with the OpenSEO MCP - -Connect Search Console to the project first, because the whole workflow reads from it, and connect the [OpenSEO MCP](/docs/mcp) so an agent can do the pull, the filter, and the add in one pass. - -```text -Using the OpenSEO MCP on project [name]: - -1. Pull Search Console performance for the last 3 months by query, - all rows. Exclude queries containing the brand name, queries - longer than 80 characters, and anything that looks like a tool - ID or a prompt rather than a search. - -2. Keep queries with average position between 4 and 20 and at least - 100 impressions. Sort by impressions. Show me the top 30 with - position, impressions, and clicks. - -3. Pull the same period by query and page, and for each of the 30 - tell me which URL Google is showing. Flag any query where two - URLs both appear. - -4. Call get_rank_tracker for this project. If a tracker exists for - the site's domain, use it; otherwise create one with schedule - weekly, devices mobile, depth 40. - -5. Call estimate_rank_tracker_cost with additionalKeywordCount set - to the number of terms I approve, and show me the per-check and - monthly numbers before adding anything. -``` - -Step 3 is the one people skip. If two of your URLs both show for a query, the tracker will report whichever one Google picked that day, and the row will look volatile when the site is competing with itself. [Cluster keywords into topical hubs](/library/keyword-research/cluster-topical-hubs) covers fixing that. - -## The list is a decision, not a setting - -Every keyword in the tracker is a claim that someone will act on its movement. If you cannot say what you would do differently when a row goes from 14 to 8, or from 8 to 14, it does not belong in the list. That test cuts most first drafts in half and makes what is left worth reading. - -Once the list is in, the next question is what to do with the numbers each week. [The keyword ranking report your CEO will read](/library/rank-tracking/keyword-ranking-report) covers that. - -## Tracking keywords FAQ - -### How many keywords should I track for SEO? - -Twenty to fifty for a typical small or mid-size site: the terms tied to pages that earn money, the striking-distance queries from Search Console, one or two brand terms as a control, and a couple of competitor terms. Track more only if someone will read the extra rows. - -### Which keywords should I track first? - -The ones Search Console already shows your site for at positions 4 to 20 with real impressions. They have demand, they are within reach of page one, and a position change will show up in clicks quickly enough to learn from. - -### Should I track keywords I already rank number one for? - -A few. Brand terms belong in the tracker as an early-warning control, and a term where you rank near the top with a low click-through rate is worth tracking for its SERP features, since a feature above your result explains the missing clicks better than the position does. - -### Does it cost more to track more keywords? - -Yes, in proportion. In OpenSEO the cost of a check is the number of keywords times the number of devices times the depth of results inspected, and scheduled checks run through a cheaper queue than one-off live checks. The app shows an estimate before you add keywords or start a run. On the hosted app, rank checks need the $10/month plan, which includes $10 of credits. - -### How often should I re-pick the keywords? - -Quarterly for the striking-distance bucket, since those queries shift as pages move. The money-page bucket changes only when the business does. diff --git a/web/content/marketing/open-source-seo.md b/web/content/marketing/open-source-seo.md deleted file mode 100644 index cb7e38262..000000000 --- a/web/content/marketing/open-source-seo.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: The Future of SEO Software Is Open Source -description: Open source puts the power into the hands of the users. It forces companies to price their services fairly and act in the best interest of the community. ---- - -Open source is the future of the internet. - -Open source means that the code for an application is freely available. Anyone can use the code how they wish. - -Historically, SEO tools have been able to raise prices and let the user experience degrade because people had no other options. Open source forces companies to do what's best for the community. Otherwise, angry users can fork the codebase and provide a better service. - -Open source puts the power in the hands of the users. - -## Benefits of open source SEO - -### Self-hosting benefits - -If you already self-host open source products, you already know the many benefits including: - -- It's fun -- Learning -- Saving money -- Privacy and controlling your own data - -### Why does open source matter even if you're using OpenSEO's SaaS? - -#### Open source products are higher quality - -Open source products can be much higher quality than closed source alternatives. - -Most apps suffer from "paper cuts", or small annoying bugs that are not worth it for the company to fix. If a user encounters one of these problems, they can fix the code themselves and contribute back to the application. - -#### Open source puts the user first - -A company open sourcing their code puts a flag in the sand saying, "We are going to be the best place to use this software even though users have other options." This mentality creates a healthy pressure which has lots of benefits for users. - -##### Fair pricing - -Anyone can [self-host OpenSEO](https://github.com/every-app/open-seo) and use it at cost for themselves. - -Since our code is open source, other companies could try to provide the same service for cheaper. This is a check, so we cannot just keep charging more and more. We need to offer the hosted service at a fair price so that customers are happy to pay for it because it gives them so much value. - -##### Transparency - -Anyone can read the code. This keeps the company in check to make sure it is prioritizing things like security. - -##### Community driven - -The best ideas from the community will bubble back into the application for everyone to benefit. - -## Build a custom SEO tool on top of OpenSEO - -Now that AI agents like Claude are making coding more accessible and inexpensive, many SEO agencies are deciding to build custom SEO tools for their companies' use cases. - -This allows them to save money and tailor the tool to their own workflows. Many are building these from scratch. - -I propose that they should instead build on top of OpenSEO. Here's why: - -### Don't re-invent the wheel - -There is no point in rebuilding keyword research, backlinks, or rank tracking from scratch. Even if you want to change something about our product, making those changes will be easier than starting from zero. - -### Software engineering best practices - -Coding agents do best when they have good examples to reference. There are many patterns in place to make it easier to add new features. You will not need to reinvent the wheel and go through the same months of QA, tweaking, and debugging that we have. - -We have built systems to verify that features work and are high quality, which you can benefit from. - -### Security - -Since we have a hosted product, we take great care to ensure every change is secure. You get to benefit from this and reference our documentation for how to self-host your custom tool securely too. - -## Does open source mean free? - -In SEO, data quality is extremely important. It is very expensive to store historical data to see trends for the whole internet or to run computers all over the world to see what position a page ranks for on Google. - -Because of this, many SEO workflows require data that costs money. Otherwise, the SEO tool would not be very useful. - -But, since the code for the application is open source, it means that you are not locked into a single data provider. If a better, more affordable data provider comes along, the project can switch. - -## Where does OpenSEO get its data? - -OpenSEO uses [DataForSEO](https://dataforseo.com/) as its main data source. They have been in business for almost 10 years and are considered the gold standard for pay-by-usage SEO data. They have quality SEO data for almost every workflow and they are very reliable. - -There are other data providers for more specific tasks like SERP, or search engine results page, which we may support in the future, but for simplicity's sake, DataForSEO is our main provider right now. - -## Can you really replace your SEO tool with OpenSEO? - -It depends. - -### For beginners - -If you're new to SEO, OpenSEO should definitely be the first tool you select. Good design means the tool is approachable to both beginners and experts. When you connect OpenSEO with an AI agent, it can help coach you through the basics. OpenSEO will grow with you. - -### For experts - -If you love your current solution, you should probably just stick with that. But, if you think your current tool is bloated, poorly designed, or too expensive, OpenSEO is striving to be an all-in-one replacement for tools like [Semrush](https://www.semrush.com/) and [Ahrefs](https://ahrefs.com/). - -## AI native: More than a replacement for other SEO tools - -OpenSEO is both open source and more affordable than alternatives. But, we do not plan for these to be the only things different about the product. - -Many AI-native SEO tools strive to automate SEO as a job function. This is hype. SEO is about coming up with a better strategy than your competitors. If every company is using the same AI agent product, there is no edge. - -OpenSEO aims to be the best way to collaborate with your AI agent on SEO tasks. OpenSEO was created after AI agents became powerful. The product is not bogged down by features and workflows that only make sense in a pre-AI world. - -Right now, this means OpenSEO has an [MCP server](/docs/mcp) which you can use with any other AI product like Claude, Codex, or OpenClaw. For example, you can ask your agent to do keyword research. But instead of blindly trusting its judgment, you can ask it for a link to view the data in OpenSEO. - -In the future, this collaboration will get even more powerful. You will be able to ask your agent to create a custom dashboard for your business or client with the specific data that you value. Or, you will be able to create bespoke, reusable workflows for routine tasks. - -While AI and SEO is very noisy right now, it is definitely the future. We're going to do everything we can to cut through the noise and empower SEOs and entrepreneurs with simple, powerful tools to pursue their SEO strategies. diff --git a/web/content/marketing/roadmap.md b/web/content/marketing/roadmap.md deleted file mode 100644 index 2494c9240..000000000 --- a/web/content/marketing/roadmap.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Roadmap -description: Features we plan to add to OpenSEO over the next three months. ---- - -Have a feature request that belongs here? [Tell us on Discord](https://discord.gg/c9uGs3cFXr). - -## Evergreen - -- Fix bugs -- Improve onboarding - - Easy whether your new to SEO or a Pro -- Improve the UX - -## Highest Priority - -- Add teammates to an account -- Community skill library - - "Advanced" features available in other tools make the tools less approachable to people new to SEO. - - We'll make a community library for these more advanced workflows. -- Web Bot Auth for bot-protected sites for Audit -- Improve In App Agent -- Consistent design across OpenSEO, built on a shared component library -- IndexNow support - -## Soon - -- Prompt tracking for AI search visibility -- Google Business Profile integration -- Google Maps geo-grid rank tracking -- Support multi-user for Docker self hosting - - Guides for popular platforms like Coolify and Railway. -- Configurable daily and weekly email reports - - Scheduled Site Audits - - GSC Reports - - Rank Tracking Alerts -- Custom reports - - We'll expose a Claude Skill so that you can customize to your company or clients. -- Share reports with clients -- Content writing workflows for Agents -- Agent readiness checks in site audits -- Bing Webmaster Tools integration - -## Not planned - -- Content Optimization features like "Keyword Density" -- Advanced Site Audit Features like JS Rendering -- Keyword Gap Analysis Page - - We will support this as a skill. You can ask Claude to turn it into a report. - -Have a feature request that belongs here? [Tell us on Discord](https://discord.gg/c9uGs3cFXr). diff --git a/web/package.json b/web/package.json deleted file mode 100644 index b7e5933c9..000000000 --- a/web/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "open-seo-landing", - "private": true, - "type": "module", - "sideEffects": false, - "scripts": { - "dev": "vite dev", - "build": "vite build && node scripts/generate-sitemap.js", - "build:app": "vite build", - "sitemap": "node scripts/generate-sitemap.js", - "start": "wrangler dev", - "preview": "vite preview", - "types:check": "fumadocs-mdx && tsc --noEmit", - "format:check": "prettier --check .", - "format:write": "prettier --write .", - "deploy": "npm run build && wrangler deploy", - "deploy:prod": "npm run build && wrangler deploy", - "deploy:preview": "CLOUDFLARE_ENV=preview npm run build && wrangler deploy" - }, - "dependencies": { - "@tanstack/react-router": "^1.168.10", - "@tanstack/react-router-devtools": "^1.166.11", - "@tanstack/react-start": "^1.167.16", - "fumadocs-core": "^15.5.1", - "fumadocs-mdx": "^11.6.5", - "fumadocs-ui": "^15.5.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "vite": "^7.3.1", - "zod": "^3.24.0" - }, - "devDependencies": { - "@cloudflare/vite-plugin": "^1.13.3", - "@tailwindcss/vite": "^4.1.18", - "@types/mdx": "^2.0.13", - "@types/node": "^22.10.2", - "@types/react": "^19.2.13", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.3", - "prettier": "^3.6.2", - "srvx": "^0.11.2", - "tailwindcss": "^4.1.18", - "typescript": "^5.9.3", - "vite-tsconfig-paths": "^6.0.5", - "wrangler": "^4.67.0" - } -} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml deleted file mode 100644 index 10df550dc..000000000 --- a/web/pnpm-lock.yaml +++ /dev/null @@ -1,6148 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@tanstack/react-router': - specifier: ^1.168.10 - version: 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/react-router-devtools': - specifier: ^1.166.11 - version: 1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/react-start': - specifier: ^1.167.16 - version: 1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - fumadocs-core: - specifier: ^15.5.1 - version: 15.8.5(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - fumadocs-mdx: - specifier: ^11.6.5 - version: 11.10.1(fumadocs-core@15.8.5(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - fumadocs-ui: - specifier: ^15.5.1 - version: 15.8.5(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1) - react: - specifier: ^19.1.0 - version: 19.2.4 - react-dom: - specifier: ^19.1.0 - version: 19.2.4(react@19.2.4) - vite: - specifier: ^7.3.1 - version: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - zod: - specifier: ^3.24.0 - version: 3.25.76 - devDependencies: - '@cloudflare/vite-plugin': - specifier: ^1.13.3 - version: 1.26.1(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))(workerd@1.20260301.1)(wrangler@4.71.0) - '@tailwindcss/vite': - specifier: ^4.1.18 - version: 4.2.1(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - '@types/mdx': - specifier: ^2.0.13 - version: 2.0.13 - '@types/node': - specifier: ^22.10.2 - version: 22.19.15 - '@types/react': - specifier: ^19.2.13 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@vitejs/plugin-react': - specifier: ^5.1.3 - version: 5.1.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - prettier: - specifier: ^3.6.2 - version: 3.8.1 - srvx: - specifier: ^0.11.2 - version: 0.11.8 - tailwindcss: - specifier: ^4.1.18 - version: 4.2.1 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vite-tsconfig-paths: - specifier: ^6.0.5 - version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - wrangler: - specifier: ^4.67.0 - version: 4.71.0 - -packages: - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@cloudflare/kv-asset-handler@0.4.2': - resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} - engines: {node: '>=18.0.0'} - - '@cloudflare/unenv-preset@2.15.0': - resolution: {integrity: sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==} - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 - peerDependenciesMeta: - workerd: - optional: true - - '@cloudflare/vite-plugin@1.26.1': - resolution: {integrity: sha512-CyebmOYAjnobU9DCn/4cUb3xipWSsqCQO+Syc0AteEl7q/A72RTvs2PHQt/O8Qj6hUutslQbmvswdL5KuDwuhw==} - peerDependencies: - vite: ^6.1.0 || ^7.0.0 - wrangler: ^4.71.0 - - '@cloudflare/workerd-darwin-64@1.20260301.1': - resolution: {integrity: sha512-+kJvwociLrvy1JV9BAvoSVsMEIYD982CpFmo/yMEvBwxDIjltYsLTE8DLi0mCkGsQ8Ygidv2fD9wavzXeiY7OQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - - '@cloudflare/workerd-darwin-arm64@1.20260301.1': - resolution: {integrity: sha512-PPIetY3e67YBr9O4UhILK8nbm5TqUDl14qx4rwFNrRSBOvlzuczzbd4BqgpAtbGVFxKp1PWpjAnBvGU/OI/tLQ==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-linux-64@1.20260301.1': - resolution: {integrity: sha512-Gu5vaVTZuYl3cHa+u5CDzSVDBvSkfNyuAHi6Mdfut7TTUdcb3V5CIcR/mXRSyMXzEy9YxEWIfdKMxOMBjupvYQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20260301.1': - resolution: {integrity: sha512-igL1pkyCXW6GiGpjdOAvqMi87UW0LMc/+yIQe/CSzuZJm5GzXoAMrwVTkCFnikk6JVGELrM5x0tGYlxa0sk5Iw==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-windows-64@1.20260301.1': - resolution: {integrity: sha512-Q0wMJ4kcujXILwQKQFc1jaYamVsNvjuECzvRrTI8OxGFMx2yq9aOsswViE4X1gaS2YQQ5u0JGwuGi5WdT1Lt7A==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} - - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - - '@floating-ui/react-dom@2.1.8': - resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - - '@formatjs/intl-localematcher@0.6.2': - resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - - '@mdx-js/mdx@3.1.1': - resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} - - '@oozcitak/dom@2.0.2': - resolution: {integrity: sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==} - engines: {node: '>=20.0'} - - '@oozcitak/infra@2.0.2': - resolution: {integrity: sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==} - engines: {node: '>=20.0'} - - '@oozcitak/url@3.0.0': - resolution: {integrity: sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==} - engines: {node: '>=20.0'} - - '@oozcitak/util@10.0.0': - resolution: {integrity: sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==} - engines: {node: '>=20.0'} - - '@orama/orama@3.1.18': - resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} - engines: {node: '>= 20.0.0'} - - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - - '@poppinss/dumper@0.6.5': - resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} - - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - - '@radix-ui/number@1.1.1': - resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} - - '@radix-ui/primitive@1.1.3': - resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - - '@radix-ui/react-accordion@1.2.12': - resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-arrow@1.1.7': - resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-collapsible@1.1.12': - resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-collection@1.1.7': - resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-context@1.1.2': - resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dialog@1.1.15': - resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-direction@1.1.1': - resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dismissable-layer@1.1.11': - resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-focus-guards@1.1.3': - resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-focus-scope@1.1.7': - resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-id@1.1.1': - resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-navigation-menu@1.2.14': - resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popover@1.1.15': - resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-popper@1.2.8': - resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-portal@1.1.9': - resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-presence@1.1.5': - resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-roving-focus@1.1.11': - resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-scroll-area@1.2.10': - resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-slot@1.2.4': - resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-tabs@1.1.13': - resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-use-callback-ref@1.1.1': - resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-escape-keydown@1.1.1': - resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-previous@1.1.1': - resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-rect@1.1.1': - resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-size@1.1.1': - resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-visually-hidden@1.2.3': - resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/rect@1.1.1': - resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - - '@rolldown/pluginutils@1.0.0-beta.40': - resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} - - '@rolldown/pluginutils@1.0.0-rc.3': - resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} - - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} - cpu: [x64] - os: [win32] - - '@shikijs/core@3.23.0': - resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} - - '@shikijs/engine-javascript@3.23.0': - resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} - - '@shikijs/engine-oniguruma@3.23.0': - resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} - - '@shikijs/langs@3.23.0': - resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} - - '@shikijs/rehype@3.23.0': - resolution: {integrity: sha512-GepKJxXHbXFfAkiZZZ+4V7x71Lw3s0ALYmydUxJRdvpKjSx9FOMSaunv6WRLFBXR6qjYerUq1YZQno+2gLEPwA==} - - '@shikijs/themes@3.23.0': - resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} - - '@shikijs/transformers@3.23.0': - resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} - - '@shikijs/types@3.23.0': - resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} - engines: {node: '>=18'} - - '@speed-highlight/core@1.2.14': - resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@tailwindcss/node@4.2.1': - resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} - - '@tailwindcss/oxide-android-arm64@4.2.1': - resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.2.1': - resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.2.1': - resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.2.1': - resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': - resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': - resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.2.1': - resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.2.1': - resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.2.1': - resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.2.1': - resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': - resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.2.1': - resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.2.1': - resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} - engines: {node: '>= 20'} - - '@tailwindcss/vite@4.2.1': - resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 - - '@tanstack/history@1.161.6': - resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} - engines: {node: '>=20.19'} - - '@tanstack/react-router-devtools@1.166.11': - resolution: {integrity: sha512-WYR3q4Xui5yPT/5PXtQh8i03iUA7q8dONBjWpV3nsGdM8Cs1FxpfhLstW0wZO1dOvSyElscwTRCJ6nO5N8r3Lg==} - engines: {node: '>=20.19'} - peerDependencies: - '@tanstack/react-router': ^1.168.2 - '@tanstack/router-core': ^1.168.2 - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - peerDependenciesMeta: - '@tanstack/router-core': - optional: true - - '@tanstack/react-router@1.168.10': - resolution: {integrity: sha512-/RmDlOwDkCug609KdPB3U+U1zmrtadJpvsmRg2zEn8TRCKRNri7dYZIjQZbNg8PgUiRL4T6njrZBV1ChzblNaA==} - engines: {node: '>=20.19'} - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - - '@tanstack/react-start-client@1.166.25': - resolution: {integrity: sha512-FvD279zzneUtsfhaTv2c29qhE1Z3wHy3dt3cCjn9LzWZehOgn5Ij78s0YpmQaQ8lSF3YL7CySE3pDk9XHE6YeA==} - engines: {node: '>=22.12.0'} - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - - '@tanstack/react-start-server@1.166.25': - resolution: {integrity: sha512-bPLADxlplvcnAcnZvBjJl2MzgUnB85d7Mu5aEkYoOFxhz0WiG6mZp7BDadIJuCd33NYMirsd3XrjfCHNzrMTyg==} - engines: {node: '>=22.12.0'} - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - - '@tanstack/react-start@1.167.16': - resolution: {integrity: sha512-vHIhn+FTWfAVhRus1BZEaBZPhnYL+StDuMlShslIBPEGGTCRt11BxNUfV/iDpr7zbxw36Snj7zGfI7DwfjjlDQ==} - engines: {node: '>=22.12.0'} - hasBin: true - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - vite: '>=7.0.0' - - '@tanstack/react-store@0.9.3': - resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - '@tanstack/router-core@1.168.9': - resolution: {integrity: sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g==} - engines: {node: '>=20.19'} - hasBin: true - - '@tanstack/router-devtools-core@1.167.1': - resolution: {integrity: sha512-ECMM47J4KmifUvJguGituSiBpfN8SyCUEoxQks5RY09hpIBfR2eswCv2e6cJimjkKwBQXOVTPkTUk/yRvER+9w==} - engines: {node: '>=20.19'} - peerDependencies: - '@tanstack/router-core': ^1.168.2 - csstype: ^3.0.10 - peerDependenciesMeta: - csstype: - optional: true - - '@tanstack/router-generator@1.166.24': - resolution: {integrity: sha512-vdaGKwuH+r+DPe6R1mjk+TDDmDH6NTG7QqwxHqGEvOH4aGf9sPjhmRKNJZqQr8cPIbfp6u5lXyZ1TeDcSNMVEA==} - engines: {node: '>=20.19'} - - '@tanstack/router-plugin@1.167.12': - resolution: {integrity: sha512-StEHcctCuFI5taSjO+lhR/yQ+EK63BdyYa+ne6FoNQPB3MMrOUrz2ZVnbqILRLkh2b+p2EfBKt65sgAKdKygPQ==} - engines: {node: '>=20.19'} - hasBin: true - peerDependencies: - '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.168.10 - vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' - vite-plugin-solid: ^2.11.10 - webpack: '>=5.92.0' - peerDependenciesMeta: - '@rsbuild/core': - optional: true - '@tanstack/react-router': - optional: true - vite: - optional: true - vite-plugin-solid: - optional: true - webpack: - optional: true - - '@tanstack/router-utils@1.161.6': - resolution: {integrity: sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw==} - engines: {node: '>=20.19'} - - '@tanstack/start-client-core@1.167.9': - resolution: {integrity: sha512-2ETQO/bxiZGsoTdPxZb7xR8YqCy5l4kv/QPkwIXuvx/A4BjufngXfgISjXUicXsFRIBZeiFnBzp9A38UMsS2iA==} - engines: {node: '>=22.12.0'} - hasBin: true - - '@tanstack/start-fn-stubs@1.161.6': - resolution: {integrity: sha512-Y6QSlGiLga8cHfvxGGaonXIlt2bIUTVdH6AMjmpMp7+ANNCp+N96GQbjjhLye3JkaxDfP68x5iZA8NK4imgRig==} - engines: {node: '>=22.12.0'} - - '@tanstack/start-plugin-core@1.167.17': - resolution: {integrity: sha512-OkorpOobGOEDVr72QUmkzKjbawKC05CSz+1B3OObB/AxBIIw+lLLhTXbV45QkX2LZA7dcRvPJYZGOH1pkFqA1g==} - engines: {node: '>=22.12.0'} - peerDependencies: - vite: '>=7.0.0' - - '@tanstack/start-server-core@1.167.9': - resolution: {integrity: sha512-vKkslQIihoDDVumF73VXT7PVFmN7Nea0nKhZx7gMbc0m09yPQYYR1dn86/dz14k6/7cDkJ+qKXa09rlVlN/i9Q==} - engines: {node: '>=22.12.0'} - hasBin: true - - '@tanstack/start-storage-context@1.166.23': - resolution: {integrity: sha512-3vEdiYRMx+r+Q7Xqxj3YmADPIpMm7fkKxDa8ITwodGXiw+SBJCGkpBXGUWjOXyXkIyqGHKM5UrReTcVUTkmaug==} - engines: {node: '>=22.12.0'} - - '@tanstack/store@0.9.3': - resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} - - '@tanstack/virtual-file-routes@1.161.7': - resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==} - engines: {node: '>=20.19'} - hasBin: true - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - - '@types/estree-jsx@1.0.5': - resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/mdx@2.0.13': - resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/node@22.19.15': - resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - - '@types/unist@2.0.11': - resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - '@vitejs/plugin-react@5.1.4': - resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} - engines: {node: '>=14'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - - astring@1.9.0: - resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} - hasBin: true - - babel-dead-code-elimination@1.0.12: - resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} - - bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} - engines: {node: '>=6.0.0'} - hasBin: true - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - blake3-wasm@2.1.5: - resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - caniuse-lite@1.0.30001777: - resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - - character-reference-invalid@2.0.1: - resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - - cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - - cheerio@1.2.0: - resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} - engines: {node: '>=20.18.1'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - collapse-white-space@2.1.0: - resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - compute-scroll-into-view@3.1.1: - resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cookie-es@2.0.1: - resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} - - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} - engines: {node: '>=18'} - - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decode-named-character-reference@1.3.0: - resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} - engines: {node: '>=0.3.1'} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - electron-to-chromium@1.5.307: - resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} - - encoding-sniffer@0.2.1: - resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} - - enhanced-resolve@5.20.0: - resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} - engines: {node: '>=10.13.0'} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} - - error-stack-parser-es@1.0.5: - resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - - esast-util-from-estree@2.0.0: - resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} - - esast-util-from-js@2.0.1: - resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estree-util-attach-comments@3.0.0: - resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} - - estree-util-build-jsx@3.0.1: - resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} - - estree-util-is-identifier-name@3.0.0: - resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - - estree-util-scope@1.0.0: - resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} - - estree-util-to-js@2.0.0: - resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} - - estree-util-value-to-estree@3.5.0: - resolution: {integrity: sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==} - - estree-util-visit@2.0.0: - resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fumadocs-core@15.8.5: - resolution: {integrity: sha512-hyJtKGuB2J/5y7tDfI1EnGMKlNbSXM5N5cpwvgCY0DcBJwFMDG/GpSpaVRzh3aWy67pAYDZFIwdtbKXBa/q5bg==} - peerDependencies: - '@mixedbread/sdk': ^0.19.0 - '@oramacloud/client': 1.x.x || 2.x.x - '@tanstack/react-router': 1.x.x - '@types/react': '*' - algoliasearch: 5.x.x - lucide-react: '*' - next: 14.x.x || 15.x.x - react: 18.x.x || 19.x.x - react-dom: 18.x.x || 19.x.x - react-router: 7.x.x - waku: ^0.26.0 - peerDependenciesMeta: - '@mixedbread/sdk': - optional: true - '@oramacloud/client': - optional: true - '@tanstack/react-router': - optional: true - '@types/react': - optional: true - algoliasearch: - optional: true - lucide-react: - optional: true - next: - optional: true - react: - optional: true - react-dom: - optional: true - react-router: - optional: true - waku: - optional: true - - fumadocs-mdx@11.10.1: - resolution: {integrity: sha512-WoEzzzoKncXl7PM++GRxEplAb73y3A4ow+QdTYybhVtoYXgJzvTzkLc5OIlNQm72Dv+OxSAx7uk11zTTOX9YMQ==} - hasBin: true - peerDependencies: - '@fumadocs/mdx-remote': ^1.4.0 - fumadocs-core: ^14.0.0 || ^15.0.0 - next: ^15.3.0 - react: '*' - vite: 6.x.x || 7.x.x - peerDependenciesMeta: - '@fumadocs/mdx-remote': - optional: true - next: - optional: true - react: - optional: true - vite: - optional: true - - fumadocs-ui@15.8.5: - resolution: {integrity: sha512-9pyB+9rOOsrFnmmZ9xREp/OgVhyaSq2ocEpqTNbeQ7tlJ6JWbdFWfW0C9lRXprQEB6DJWUDtDxqKS5QXLH0EGA==} - peerDependencies: - '@types/react': '*' - next: 14.x.x || 15.x.x - react: 18.x.x || 19.x.x - react-dom: 18.x.x || 19.x.x - tailwindcss: ^3.4.14 || ^4.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - next: - optional: true - tailwindcss: - optional: true - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-nonce@1.0.1: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} - - github-slugger@2.0.0: - resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - - goober@2.1.18: - resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} - peerDependencies: - csstype: ^3.0.10 - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - h3@2.0.1-rc.16: - resolution: {integrity: sha512-h+pjvyujdo9way8qj6FUbhaQcHlR8FEq65EhTX9ViT5pK8aLj68uFl4hBkF+hsTJAH+H1END2Yv6hTIsabGfag==} - engines: {node: '>=20.11.1'} - hasBin: true - peerDependencies: - crossws: ^0.4.1 - peerDependenciesMeta: - crossws: - optional: true - - hast-util-to-estree@3.1.3: - resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} - - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - - hast-util-to-jsx-runtime@2.3.6: - resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} - - hast-util-to-string@3.0.1: - resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - htmlparser2@10.1.0: - resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - image-size@2.0.2: - resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} - engines: {node: '>=16.x'} - hasBin: true - - inline-style-parser@0.2.7: - resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - - is-alphabetical@2.0.1: - resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - - is-alphanumerical@2.0.1: - resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-decimal@2.0.1: - resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-hexadecimal@2.0.1: - resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - - isbot@5.1.37: - resolution: {integrity: sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ==} - engines: {node: '>=18'} - - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} - hasBin: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - - lightningcss-android-arm64@1.31.1: - resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.31.1: - resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.31.1: - resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.31.1: - resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.31.1: - resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.31.1: - resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.31.1: - resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.31.1: - resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.31.1: - resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.31.1: - resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.31.1: - resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.31.1: - resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} - engines: {node: '>= 12.0.0'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - - lru-cache@11.2.6: - resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} - engines: {node: 20 || >=22} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - markdown-extensions@2.0.0: - resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} - engines: {node: '>=16'} - - markdown-table@3.0.4: - resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - - mdast-util-find-and-replace@3.0.2: - resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - - mdast-util-from-markdown@2.0.3: - resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} - - mdast-util-gfm-autolink-literal@2.0.1: - resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} - - mdast-util-gfm-footnote@2.1.0: - resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} - - mdast-util-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - - mdast-util-gfm-table@2.0.0: - resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - - mdast-util-gfm-task-list-item@2.0.0: - resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - - mdast-util-gfm@3.1.0: - resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - - mdast-util-mdx-expression@2.0.1: - resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} - - mdast-util-mdx-jsx@3.2.0: - resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} - - mdast-util-mdx@3.0.0: - resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} - - mdast-util-mdxjs-esm@2.0.1: - resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - - mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - - mdast-util-to-hast@13.2.1: - resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - - mdast-util-to-markdown@2.1.2: - resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - - mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - - micromark-core-commonmark@2.0.3: - resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - - micromark-extension-gfm-autolink-literal@2.1.0: - resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} - - micromark-extension-gfm-footnote@2.1.0: - resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} - - micromark-extension-gfm-strikethrough@2.1.0: - resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} - - micromark-extension-gfm-table@2.1.1: - resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} - - micromark-extension-gfm-tagfilter@2.0.0: - resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - - micromark-extension-gfm-task-list-item@2.1.0: - resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} - - micromark-extension-gfm@3.0.0: - resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - - micromark-extension-mdx-expression@3.0.1: - resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} - - micromark-extension-mdx-jsx@3.0.2: - resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} - - micromark-extension-mdx-md@2.0.0: - resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} - - micromark-extension-mdxjs-esm@3.0.0: - resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} - - micromark-extension-mdxjs@3.0.0: - resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} - - micromark-factory-destination@2.0.1: - resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - - micromark-factory-label@2.0.1: - resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} - - micromark-factory-mdx-expression@2.0.3: - resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} - - micromark-factory-space@2.0.1: - resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - - micromark-factory-title@2.0.1: - resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - - micromark-factory-whitespace@2.0.1: - resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-chunked@2.0.1: - resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - - micromark-util-classify-character@2.0.1: - resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - - micromark-util-combine-extensions@2.0.1: - resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - - micromark-util-decode-numeric-character-reference@2.0.2: - resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - - micromark-util-decode-string@2.0.1: - resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-events-to-acorn@2.0.3: - resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} - - micromark-util-html-tag-name@2.0.1: - resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - - micromark-util-normalize-identifier@2.0.1: - resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - - micromark-util-resolve-all@2.0.1: - resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-subtokenize@2.1.0: - resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - - micromark@4.0.2: - resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - - miniflare@4.20260301.1: - resolution: {integrity: sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog==} - engines: {node: '>=18.0.0'} - hasBin: true - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - next-themes@0.4.6: - resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} - peerDependencies: - react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - - node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-to-yarn@3.0.1: - resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - oniguruma-parser@0.12.1: - resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - - oniguruma-to-es@4.3.4: - resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} - - parse-entities@4.0.2: - resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - - parse5-htmlparser2-tree-adapter@7.1.0: - resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - - parse5-parser-stream@7.1.2: - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} - engines: {node: '>=4'} - - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} - - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} - engines: {node: '>=14'} - hasBin: true - - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} - peerDependencies: - react: ^19.2.4 - - react-medium-image-zoom@5.4.1: - resolution: {integrity: sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - - react-remove-scroll-bar@2.3.8: - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-remove-scroll@2.7.2: - resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react-style-singleton@2.2.3: - resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} - engines: {node: '>=0.10.0'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} - engines: {node: '>= 4'} - - recma-build-jsx@1.0.0: - resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} - - recma-jsx@1.0.1: - resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - recma-parse@1.0.0: - resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} - - recma-stringify@1.0.0: - resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} - - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.1.0: - resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - - rehype-recma@1.0.0: - resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} - - remark-gfm@4.0.1: - resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - - remark-mdx@3.1.1: - resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} - - remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - - remark-rehype@11.1.2: - resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - - remark-stringify@11.0.0: - resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - - remark@15.0.1: - resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - rou3@0.8.1: - resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - scroll-into-view-if-needed@3.1.0: - resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - seroval-plugins@1.5.2: - resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - - seroval@1.5.2: - resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} - engines: {node: '>=10'} - - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - shiki@3.23.0: - resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - srvx@0.11.15: - resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==} - engines: {node: '>=20.16.0'} - hasBin: true - - srvx@0.11.8: - resolution: {integrity: sha512-2n9t0YnAXPJjinytvxccNgs7rOA5gmE7Wowt/8Dy2dx2fDC6sBhfBpbrCvjYKALlVukPS/Uq3QwkolKNa7P/2Q==} - engines: {node: '>=20.16.0'} - hasBin: true - - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - - style-to-js@1.1.21: - resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} - - style-to-object@1.0.14: - resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - - tailwind-merge@3.5.0: - resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} - - tailwindcss@4.2.1: - resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} - - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - undici@7.18.2: - resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} - engines: {node: '>=20.18.1'} - - undici@7.24.7: - resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} - engines: {node: '>=20.18.1'} - - unenv@2.0.0-rc.24: - resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - - unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - - unist-util-is@6.0.1: - resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} - - unist-util-position-from-estree@2.0.0: - resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-parents@6.0.2: - resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - - unist-util-visit@5.1.0: - resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - - unplugin@2.3.11: - resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} - engines: {node: '>=18.12.0'} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - use-callback-ref@1.3.3: - resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - use-sidecar@1.1.3: - resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vfile-message@4.0.3: - resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - vite-tsconfig-paths@6.1.1: - resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} - peerDependencies: - vite: '*' - - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitefu@1.1.3: - resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - vite: - optional: true - - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - workerd@1.20260301.1: - resolution: {integrity: sha512-oterQ1IFd3h7PjCfT4znSFOkJCvNQ6YMOyZ40YsnO3nrSpgB4TbJVYWFOnyJAw71/RQuupfVqZZWKvsy8GO3fw==} - engines: {node: '>=16'} - hasBin: true - - wrangler@4.71.0: - resolution: {integrity: sha512-j6pSGAncOLNQDRzqtp0EqzYj52CldDP7uz/C9cxVrIgqa5p+cc0b4pIwnapZZAGv9E1Loa3tmPD0aXonH7KTkw==} - engines: {node: '>=20.0.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20260226.1 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xmlbuilder2@4.0.3: - resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==} - engines: {node: '>=20.0'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - youch-core@0.3.3: - resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - - youch@4.1.0-beta.10: - resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -snapshots: - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.0': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.28.6': {} - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.28.6': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - - '@babel/parser@7.29.0': - dependencies: - '@babel/types': 7.29.0 - - '@babel/parser@7.29.2': - dependencies: - '@babel/types': 7.29.0 - - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@cloudflare/kv-asset-handler@0.4.2': {} - - '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260301.1)': - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260301.1 - - '@cloudflare/vite-plugin@1.26.1(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))(workerd@1.20260301.1)(wrangler@4.71.0)': - dependencies: - '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260301.1) - miniflare: 4.20260301.1 - unenv: 2.0.0-rc.24 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - wrangler: 4.71.0 - ws: 8.18.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - workerd - - '@cloudflare/workerd-darwin-64@1.20260301.1': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20260301.1': - optional: true - - '@cloudflare/workerd-linux-64@1.20260301.1': - optional: true - - '@cloudflare/workerd-linux-arm64@1.20260301.1': - optional: true - - '@cloudflare/workerd-windows-64@1.20260301.1': - optional: true - - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@emnapi/runtime@1.8.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.25.12': - optional: true - - '@esbuild/aix-ppc64@0.27.3': - optional: true - - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.25.12': - optional: true - - '@esbuild/android-arm64@0.27.3': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - - '@esbuild/android-arm@0.25.12': - optional: true - - '@esbuild/android-arm@0.27.3': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - - '@esbuild/android-x64@0.25.12': - optional: true - - '@esbuild/android-x64@0.27.3': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - - '@esbuild/darwin-arm64@0.25.12': - optional: true - - '@esbuild/darwin-arm64@0.27.3': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - - '@esbuild/darwin-x64@0.25.12': - optional: true - - '@esbuild/darwin-x64@0.27.3': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - - '@esbuild/freebsd-arm64@0.25.12': - optional: true - - '@esbuild/freebsd-arm64@0.27.3': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - - '@esbuild/freebsd-x64@0.25.12': - optional: true - - '@esbuild/freebsd-x64@0.27.3': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - - '@esbuild/linux-arm64@0.25.12': - optional: true - - '@esbuild/linux-arm64@0.27.3': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - - '@esbuild/linux-arm@0.25.12': - optional: true - - '@esbuild/linux-arm@0.27.3': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - - '@esbuild/linux-ia32@0.25.12': - optional: true - - '@esbuild/linux-ia32@0.27.3': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - - '@esbuild/linux-loong64@0.25.12': - optional: true - - '@esbuild/linux-loong64@0.27.3': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - - '@esbuild/linux-mips64el@0.25.12': - optional: true - - '@esbuild/linux-mips64el@0.27.3': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - - '@esbuild/linux-ppc64@0.25.12': - optional: true - - '@esbuild/linux-ppc64@0.27.3': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - - '@esbuild/linux-riscv64@0.25.12': - optional: true - - '@esbuild/linux-riscv64@0.27.3': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - - '@esbuild/linux-s390x@0.25.12': - optional: true - - '@esbuild/linux-s390x@0.27.3': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/linux-x64@0.27.3': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.27.3': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.27.3': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.27.3': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.27.3': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.27.3': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.27.3': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.27.3': - optional: true - - '@esbuild/win32-arm64@0.27.7': - optional: true - - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.27.3': - optional: true - - '@esbuild/win32-ia32@0.27.7': - optional: true - - '@esbuild/win32-x64@0.25.12': - optional: true - - '@esbuild/win32-x64@0.27.3': - optional: true - - '@esbuild/win32-x64@0.27.7': - optional: true - - '@floating-ui/core@1.7.5': - dependencies: - '@floating-ui/utils': 0.2.11 - - '@floating-ui/dom@1.7.6': - dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 - - '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@floating-ui/dom': 1.7.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@floating-ui/utils@0.2.11': {} - - '@formatjs/intl-localematcher@0.6.2': - dependencies: - tslib: 2.8.1 - - '@img/colour@1.1.0': {} - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true - - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.8.1 - optional: true - - '@img/sharp-win32-arm64@0.34.5': - optional: true - - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@mdx-js/mdx@3.1.1': - dependencies: - '@types/estree': 1.0.8 - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdx': 2.0.13 - acorn: 8.16.0 - collapse-white-space: 2.1.0 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - estree-util-scope: 1.0.0 - estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.6 - markdown-extensions: 2.0.0 - recma-build-jsx: 1.0.0 - recma-jsx: 1.0.1(acorn@8.16.0) - recma-stringify: 1.0.0 - rehype-recma: 1.0.0 - remark-mdx: 3.1.1 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - source-map: 0.7.6 - unified: 11.0.5 - unist-util-position-from-estree: 2.0.0 - unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@oozcitak/dom@2.0.2': - dependencies: - '@oozcitak/infra': 2.0.2 - '@oozcitak/url': 3.0.0 - '@oozcitak/util': 10.0.0 - - '@oozcitak/infra@2.0.2': - dependencies: - '@oozcitak/util': 10.0.0 - - '@oozcitak/url@3.0.0': - dependencies: - '@oozcitak/infra': 2.0.2 - '@oozcitak/util': 10.0.0 - - '@oozcitak/util@10.0.0': {} - - '@orama/orama@3.1.18': {} - - '@poppinss/colors@4.1.6': - dependencies: - kleur: 4.1.5 - - '@poppinss/dumper@0.6.5': - dependencies: - '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.2.0 - supports-color: 10.2.2 - - '@poppinss/exception@1.2.3': {} - - '@radix-ui/number@1.1.1': {} - - '@radix-ui/primitive@1.1.3': {} - - '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/rect': 1.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/rect': 1.1.1 - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.14 - - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - - '@radix-ui/rect@1.1.1': {} - - '@rolldown/pluginutils@1.0.0-beta.40': {} - - '@rolldown/pluginutils@1.0.0-rc.3': {} - - '@rollup/rollup-android-arm-eabi@4.59.0': - optional: true - - '@rollup/rollup-android-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-x64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.59.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.59.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.59.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.59.0': - optional: true - - '@shikijs/core@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.4 - - '@shikijs/engine-oniguruma@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - - '@shikijs/rehype@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@types/hast': 3.0.4 - hast-util-to-string: 3.0.1 - shiki: 3.23.0 - unified: 11.0.5 - unist-util-visit: 5.1.0 - - '@shikijs/themes@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - - '@shikijs/transformers@3.23.0': - dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/types': 3.23.0 - - '@shikijs/types@3.23.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - - '@sindresorhus/is@7.2.0': {} - - '@speed-highlight/core@1.2.14': {} - - '@standard-schema/spec@1.1.0': {} - - '@tailwindcss/node@4.2.1': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.0 - jiti: 2.6.1 - lightningcss: 1.31.1 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.2.1 - - '@tailwindcss/oxide-android-arm64@4.2.1': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.2.1': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.2.1': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.2.1': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.2.1': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.2.1': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.2.1': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.2.1': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.2.1': - optional: true - - '@tailwindcss/oxide@4.2.1': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.1 - '@tailwindcss/oxide-darwin-arm64': 4.2.1 - '@tailwindcss/oxide-darwin-x64': 4.2.1 - '@tailwindcss/oxide-freebsd-x64': 4.2.1 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 - '@tailwindcss/oxide-linux-x64-musl': 4.2.1 - '@tailwindcss/oxide-wasm32-wasi': 4.2.1 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 - - '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': - dependencies: - '@tailwindcss/node': 4.2.1 - '@tailwindcss/oxide': 4.2.1 - tailwindcss: 4.2.1 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - - '@tanstack/history@1.161.6': {} - - '@tanstack/react-router-devtools@1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-devtools-core': 1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@tanstack/router-core': 1.168.9 - transitivePeerDependencies: - - csstype - - '@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@tanstack/history': 1.161.6 - '@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-core': 1.168.9 - isbot: 5.1.37 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@tanstack/react-start-client@1.166.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-core': 1.168.9 - '@tanstack/start-client-core': 1.167.9 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@tanstack/react-start-server@1.166.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@tanstack/history': 1.161.6 - '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-core': 1.168.9 - '@tanstack/start-client-core': 1.167.9 - '@tanstack/start-server-core': 1.167.9 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - crossws - - '@tanstack/react-start@1.167.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': - dependencies: - '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/react-start-client': 1.166.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/react-start-server': 1.166.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-utils': 1.161.6 - '@tanstack/start-client-core': 1.167.9 - '@tanstack/start-plugin-core': 1.167.17(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - '@tanstack/start-server-core': 1.167.9 - pathe: 2.0.3 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - transitivePeerDependencies: - - '@rsbuild/core' - - crossws - - supports-color - - vite-plugin-solid - - webpack - - '@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@tanstack/store': 0.9.3 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) - - '@tanstack/router-core@1.168.9': - dependencies: - '@tanstack/history': 1.161.6 - cookie-es: 2.0.1 - seroval: 1.5.2 - seroval-plugins: 1.5.2(seroval@1.5.2) - - '@tanstack/router-devtools-core@1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3)': - dependencies: - '@tanstack/router-core': 1.168.9 - clsx: 2.1.1 - goober: 2.1.18(csstype@3.2.3) - optionalDependencies: - csstype: 3.2.3 - - '@tanstack/router-generator@1.166.24': - dependencies: - '@tanstack/router-core': 1.168.9 - '@tanstack/router-utils': 1.161.6 - '@tanstack/virtual-file-routes': 1.161.7 - prettier: 3.8.1 - recast: 0.23.11 - source-map: 0.7.6 - tsx: 4.21.0 - zod: 3.25.76 - transitivePeerDependencies: - - supports-color - - '@tanstack/router-plugin@1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@tanstack/router-core': 1.168.9 - '@tanstack/router-generator': 1.166.24 - '@tanstack/router-utils': 1.161.6 - '@tanstack/virtual-file-routes': 1.161.7 - chokidar: 3.6.0 - unplugin: 2.3.11 - zod: 3.25.76 - optionalDependencies: - '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - transitivePeerDependencies: - - supports-color - - '@tanstack/router-utils@1.161.6': - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - ansis: 4.2.0 - babel-dead-code-elimination: 1.0.12 - diff: 8.0.4 - pathe: 2.0.3 - tinyglobby: 0.2.15 - transitivePeerDependencies: - - supports-color - - '@tanstack/start-client-core@1.167.9': - dependencies: - '@tanstack/router-core': 1.168.9 - '@tanstack/start-fn-stubs': 1.161.6 - '@tanstack/start-storage-context': 1.166.23 - seroval: 1.5.2 - - '@tanstack/start-fn-stubs@1.161.6': {} - - '@tanstack/start-plugin-core@1.167.17(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0 - '@babel/types': 7.29.0 - '@rolldown/pluginutils': 1.0.0-beta.40 - '@tanstack/router-core': 1.168.9 - '@tanstack/router-generator': 1.166.24 - '@tanstack/router-plugin': 1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - '@tanstack/router-utils': 1.161.6 - '@tanstack/start-client-core': 1.167.9 - '@tanstack/start-server-core': 1.167.9 - cheerio: 1.2.0 - exsolve: 1.0.8 - pathe: 2.0.3 - picomatch: 4.0.4 - source-map: 0.7.6 - srvx: 0.11.15 - tinyglobby: 0.2.15 - ufo: 1.6.3 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - vitefu: 1.1.3(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) - xmlbuilder2: 4.0.3 - zod: 3.25.76 - transitivePeerDependencies: - - '@rsbuild/core' - - '@tanstack/react-router' - - crossws - - supports-color - - vite-plugin-solid - - webpack - - '@tanstack/start-server-core@1.167.9': - dependencies: - '@tanstack/history': 1.161.6 - '@tanstack/router-core': 1.168.9 - '@tanstack/start-client-core': 1.167.9 - '@tanstack/start-storage-context': 1.166.23 - h3-v2: h3@2.0.1-rc.16 - seroval: 1.5.2 - transitivePeerDependencies: - - crossws - - '@tanstack/start-storage-context@1.166.23': - dependencies: - '@tanstack/router-core': 1.168.9 - - '@tanstack/store@0.9.3': {} - - '@tanstack/virtual-file-routes@1.161.7': {} - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.29.0 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.29.0 - - '@types/debug@4.1.12': - dependencies: - '@types/ms': 2.1.0 - - '@types/estree-jsx@1.0.5': - dependencies: - '@types/estree': 1.0.8 - - '@types/estree@1.0.8': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdx@2.0.13': {} - - '@types/ms@2.1.0': {} - - '@types/node@22.19.15': - dependencies: - undici-types: 6.21.0 - - '@types/react-dom@19.2.3(@types/react@19.2.14)': - dependencies: - '@types/react': 19.2.14 - - '@types/react@19.2.14': - dependencies: - csstype: 3.2.3 - - '@types/unist@2.0.11': {} - - '@types/unist@3.0.3': {} - - '@ungap/structured-clone@1.3.0': {} - - '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@rolldown/pluginutils': 1.0.0-rc.3 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - transitivePeerDependencies: - - supports-color - - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - - ansis@4.2.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - argparse@2.0.1: {} - - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - - ast-types@0.16.1: - dependencies: - tslib: 2.8.1 - - astring@1.9.0: {} - - babel-dead-code-elimination@1.0.12: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - bail@2.0.2: {} - - baseline-browser-mapping@2.10.0: {} - - binary-extensions@2.3.0: {} - - blake3-wasm@2.1.5: {} - - boolbase@1.0.0: {} - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001777 - electron-to-chromium: 1.5.307 - node-releases: 2.0.36 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - - caniuse-lite@1.0.30001777: {} - - ccount@2.0.1: {} - - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - character-entities@2.0.2: {} - - character-reference-invalid@2.0.1: {} - - cheerio-select@2.1.0: - dependencies: - boolbase: 1.0.0 - css-select: 5.2.2 - css-what: 6.2.2 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - - cheerio@1.2.0: - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.2.2 - encoding-sniffer: 0.2.1 - htmlparser2: 10.1.0 - parse5: 7.3.0 - parse5-htmlparser2-tree-adapter: 7.1.0 - parse5-parser-stream: 7.1.2 - undici: 7.24.7 - whatwg-mimetype: 4.0.0 - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - class-variance-authority@0.7.1: - dependencies: - clsx: 2.1.1 - - clsx@2.1.1: {} - - collapse-white-space@2.1.0: {} - - comma-separated-tokens@2.0.3: {} - - compute-scroll-into-view@3.1.1: {} - - convert-source-map@2.0.0: {} - - cookie-es@2.0.1: {} - - cookie@1.1.1: {} - - css-select@5.2.2: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - - css-what@6.2.2: {} - - cssesc@3.0.0: {} - - csstype@3.2.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decode-named-character-reference@1.3.0: - dependencies: - character-entities: 2.0.2 - - dequal@2.0.3: {} - - detect-libc@2.1.2: {} - - detect-node-es@1.1.0: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - - diff@8.0.4: {} - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - electron-to-chromium@1.5.307: {} - - encoding-sniffer@0.2.1: - dependencies: - iconv-lite: 0.6.3 - whatwg-encoding: 3.1.1 - - enhanced-resolve@5.20.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - - entities@4.5.0: {} - - entities@6.0.1: {} - - entities@7.0.1: {} - - error-stack-parser-es@1.0.5: {} - - esast-util-from-estree@2.0.0: - dependencies: - '@types/estree-jsx': 1.0.5 - devlop: 1.1.0 - estree-util-visit: 2.0.0 - unist-util-position-from-estree: 2.0.0 - - esast-util-from-js@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - acorn: 8.16.0 - esast-util-from-estree: 2.0.0 - vfile-message: 4.0.3 - - esbuild@0.25.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - - escalade@3.2.0: {} - - escape-string-regexp@5.0.0: {} - - esprima@4.0.1: {} - - estree-util-attach-comments@3.0.0: - dependencies: - '@types/estree': 1.0.8 - - estree-util-build-jsx@3.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - estree-walker: 3.0.3 - - estree-util-is-identifier-name@3.0.0: {} - - estree-util-scope@1.0.0: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - - estree-util-to-js@2.0.0: - dependencies: - '@types/estree-jsx': 1.0.5 - astring: 1.9.0 - source-map: 0.7.6 - - estree-util-value-to-estree@3.5.0: - dependencies: - '@types/estree': 1.0.8 - - estree-util-visit@2.0.0: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/unist': 3.0.3 - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - exsolve@1.0.8: {} - - extend@3.0.2: {} - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - fsevents@2.3.3: - optional: true - - fumadocs-core@15.8.5(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@formatjs/intl-localematcher': 0.6.2 - '@orama/orama': 3.1.18 - '@shikijs/rehype': 3.23.0 - '@shikijs/transformers': 3.23.0 - github-slugger: 2.0.0 - hast-util-to-estree: 3.1.3 - hast-util-to-jsx-runtime: 2.3.6 - image-size: 2.0.2 - negotiator: 1.0.0 - npm-to-yarn: 3.0.1 - path-to-regexp: 8.3.0 - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) - remark: 15.0.1 - remark-gfm: 4.0.1 - remark-rehype: 11.1.2 - scroll-into-view-if-needed: 3.1.0 - shiki: 3.23.0 - unist-util-visit: 5.1.0 - optionalDependencies: - '@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@types/react': 19.2.14 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - supports-color - - fumadocs-mdx@11.10.1(fumadocs-core@15.8.5(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)): - dependencies: - '@mdx-js/mdx': 3.1.1 - '@standard-schema/spec': 1.1.0 - chokidar: 4.0.3 - esbuild: 0.25.12 - estree-util-value-to-estree: 3.5.0 - fumadocs-core: 15.8.5(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - js-yaml: 4.1.1 - lru-cache: 11.2.6 - picocolors: 1.1.1 - remark-mdx: 3.1.1 - remark-parse: 11.0.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - unified: 11.0.5 - unist-util-visit: 5.1.0 - zod: 4.3.6 - optionalDependencies: - react: 19.2.4 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - transitivePeerDependencies: - - supports-color - - fumadocs-ui@15.8.5(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1): - dependencies: - '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - class-variance-authority: 0.7.1 - fumadocs-core: 15.8.5(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - lodash.merge: 4.6.2 - next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - postcss-selector-parser: 7.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-medium-image-zoom: 5.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - scroll-into-view-if-needed: 3.1.0 - tailwind-merge: 3.5.0 - optionalDependencies: - '@types/react': 19.2.14 - tailwindcss: 4.2.1 - transitivePeerDependencies: - - '@mixedbread/sdk' - - '@oramacloud/client' - - '@tanstack/react-router' - - '@types/react-dom' - - algoliasearch - - lucide-react - - react-router - - supports-color - - waku - - gensync@1.0.0-beta.2: {} - - get-nonce@1.0.1: {} - - get-tsconfig@4.13.7: - dependencies: - resolve-pkg-maps: 1.0.0 - - github-slugger@2.0.0: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - globrex@0.1.2: {} - - goober@2.1.18(csstype@3.2.3): - dependencies: - csstype: 3.2.3 - - graceful-fs@4.2.11: {} - - h3@2.0.1-rc.16: - dependencies: - rou3: 0.8.1 - srvx: 0.11.15 - - hast-util-to-estree@3.1.3: - dependencies: - '@types/estree': 1.0.8 - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - estree-util-attach-comments: 3.0.0 - estree-util-is-identifier-name: 3.0.0 - hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - style-to-js: 1.1.21 - unist-util-position: 5.0.0 - zwitch: 2.0.4 - transitivePeerDependencies: - - supports-color - - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-to-jsx-runtime@2.3.6: - dependencies: - '@types/estree': 1.0.8 - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - style-to-js: 1.1.21 - unist-util-position: 5.0.0 - vfile-message: 4.0.3 - transitivePeerDependencies: - - supports-color - - hast-util-to-string@3.0.1: - dependencies: - '@types/hast': 3.0.4 - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - html-void-elements@3.0.0: {} - - htmlparser2@10.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 7.0.1 - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - image-size@2.0.2: {} - - inline-style-parser@0.2.7: {} - - is-alphabetical@2.0.1: {} - - is-alphanumerical@2.0.1: - dependencies: - is-alphabetical: 2.0.1 - is-decimal: 2.0.1 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-decimal@2.0.1: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-hexadecimal@2.0.1: {} - - is-number@7.0.0: {} - - is-plain-obj@4.1.0: {} - - isbot@5.1.37: {} - - jiti@2.6.1: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - jsesc@3.1.0: {} - - json5@2.2.3: {} - - kleur@4.1.5: {} - - lightningcss-android-arm64@1.31.1: - optional: true - - lightningcss-darwin-arm64@1.31.1: - optional: true - - lightningcss-darwin-x64@1.31.1: - optional: true - - lightningcss-freebsd-x64@1.31.1: - optional: true - - lightningcss-linux-arm-gnueabihf@1.31.1: - optional: true - - lightningcss-linux-arm64-gnu@1.31.1: - optional: true - - lightningcss-linux-arm64-musl@1.31.1: - optional: true - - lightningcss-linux-x64-gnu@1.31.1: - optional: true - - lightningcss-linux-x64-musl@1.31.1: - optional: true - - lightningcss-win32-arm64-msvc@1.31.1: - optional: true - - lightningcss-win32-x64-msvc@1.31.1: - optional: true - - lightningcss@1.31.1: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.31.1 - lightningcss-darwin-arm64: 1.31.1 - lightningcss-darwin-x64: 1.31.1 - lightningcss-freebsd-x64: 1.31.1 - lightningcss-linux-arm-gnueabihf: 1.31.1 - lightningcss-linux-arm64-gnu: 1.31.1 - lightningcss-linux-arm64-musl: 1.31.1 - lightningcss-linux-x64-gnu: 1.31.1 - lightningcss-linux-x64-musl: 1.31.1 - lightningcss-win32-arm64-msvc: 1.31.1 - lightningcss-win32-x64-msvc: 1.31.1 - - lodash.merge@4.6.2: {} - - longest-streak@3.1.0: {} - - lru-cache@11.2.6: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - markdown-extensions@2.0.0: {} - - markdown-table@3.0.4: {} - - mdast-util-find-and-replace@3.0.2: - dependencies: - '@types/mdast': 4.0.4 - escape-string-regexp: 5.0.0 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - mdast-util-from-markdown@2.0.3: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.2 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-decode-string: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-autolink-literal@2.0.1: - dependencies: - '@types/mdast': 4.0.4 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.2 - micromark-util-character: 2.1.1 - - mdast-util-gfm-footnote@2.1.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - micromark-util-normalize-identifier: 2.0.1 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-strikethrough@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-table@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-task-list-item@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm@3.1.0: - dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-expression@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-jsx@3.2.0: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - parse-entities: 4.0.2 - stringify-entities: 4.0.4 - unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.3 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx@3.0.0: - dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdxjs-esm@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-phrasing@4.1.0: - dependencies: - '@types/mdast': 4.0.4 - unist-util-is: 6.0.1 - - mdast-util-to-hast@13.2.1: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - - mdast-util-to-markdown@2.1.2: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - longest-streak: 3.1.0 - mdast-util-phrasing: 4.1.0 - mdast-util-to-string: 4.0.0 - micromark-util-classify-character: 2.0.1 - micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.1.0 - zwitch: 2.0.4 - - mdast-util-to-string@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - - micromark-core-commonmark@2.0.3: - dependencies: - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-factory-destination: 2.0.1 - micromark-factory-label: 2.0.1 - micromark-factory-space: 2.0.1 - micromark-factory-title: 2.0.1 - micromark-factory-whitespace: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-html-tag-name: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-autolink-literal@2.1.0: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-footnote@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-strikethrough@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-table@2.1.1: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-tagfilter@2.0.0: - dependencies: - micromark-util-types: 2.0.2 - - micromark-extension-gfm-task-list-item@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm@3.0.0: - dependencies: - micromark-extension-gfm-autolink-literal: 2.1.0 - micromark-extension-gfm-footnote: 2.1.0 - micromark-extension-gfm-strikethrough: 2.1.0 - micromark-extension-gfm-table: 2.1.1 - micromark-extension-gfm-tagfilter: 2.0.0 - micromark-extension-gfm-task-list-item: 2.1.0 - micromark-util-combine-extensions: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-mdx-expression@3.0.1: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - micromark-factory-mdx-expression: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-events-to-acorn: 2.0.3 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-mdx-jsx@3.0.2: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - micromark-factory-mdx-expression: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-events-to-acorn: 2.0.3 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - vfile-message: 4.0.3 - - micromark-extension-mdx-md@2.0.0: - dependencies: - micromark-util-types: 2.0.2 - - micromark-extension-mdxjs-esm@3.0.0: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-util-character: 2.1.1 - micromark-util-events-to-acorn: 2.0.3 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-position-from-estree: 2.0.0 - vfile-message: 4.0.3 - - micromark-extension-mdxjs@3.0.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - micromark-extension-mdx-expression: 3.0.1 - micromark-extension-mdx-jsx: 3.0.2 - micromark-extension-mdx-md: 2.0.0 - micromark-extension-mdxjs-esm: 3.0.0 - micromark-util-combine-extensions: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-destination@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-label@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-mdx-expression@2.0.3: - dependencies: - '@types/estree': 1.0.8 - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-events-to-acorn: 2.0.3 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-position-from-estree: 2.0.0 - vfile-message: 4.0.3 - - micromark-factory-space@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-types: 2.0.2 - - micromark-factory-title@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-whitespace@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-chunked@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-classify-character@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-combine-extensions@2.0.1: - dependencies: - micromark-util-chunked: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-decode-numeric-character-reference@2.0.2: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-decode-string@2.0.1: - dependencies: - decode-named-character-reference: 1.3.0 - micromark-util-character: 2.1.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-symbol: 2.0.1 - - micromark-util-encode@2.0.1: {} - - micromark-util-events-to-acorn@2.0.3: - dependencies: - '@types/estree': 1.0.8 - '@types/unist': 3.0.3 - devlop: 1.1.0 - estree-util-visit: 2.0.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - vfile-message: 4.0.3 - - micromark-util-html-tag-name@2.0.1: {} - - micromark-util-normalize-identifier@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-resolve-all@2.0.1: - dependencies: - micromark-util-types: 2.0.2 - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-subtokenize@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - - micromark@4.0.2: - dependencies: - '@types/debug': 4.1.12 - debug: 4.4.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-combine-extensions: 2.0.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-encode: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - transitivePeerDependencies: - - supports-color - - miniflare@4.20260301.1: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.18.2 - workerd: 1.20260301.1 - ws: 8.18.0 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - negotiator@1.0.0: {} - - next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - node-releases@2.0.36: {} - - normalize-path@3.0.0: {} - - npm-to-yarn@3.0.1: {} - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - oniguruma-parser@0.12.1: {} - - oniguruma-to-es@4.3.4: - dependencies: - oniguruma-parser: 0.12.1 - regex: 6.1.0 - regex-recursion: 6.0.2 - - parse-entities@4.0.2: - dependencies: - '@types/unist': 2.0.11 - character-entities-legacy: 3.0.0 - character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.3.0 - is-alphanumerical: 2.0.1 - is-decimal: 2.0.1 - is-hexadecimal: 2.0.1 - - parse5-htmlparser2-tree-adapter@7.1.0: - dependencies: - domhandler: 5.0.3 - parse5: 7.3.0 - - parse5-parser-stream@7.1.2: - dependencies: - parse5: 7.3.0 - - parse5@7.3.0: - dependencies: - entities: 6.0.1 - - path-to-regexp@6.3.0: {} - - path-to-regexp@8.3.0: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@2.3.2: {} - - picomatch@4.0.3: {} - - picomatch@4.0.4: {} - - postcss-selector-parser@7.1.1: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prettier@3.8.1: {} - - property-information@7.1.0: {} - - react-dom@19.2.4(react@19.2.4): - dependencies: - react: 19.2.4 - scheduler: 0.27.0 - - react-medium-image-zoom@5.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - react-refresh@0.18.0: {} - - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - - react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): - dependencies: - get-nonce: 1.0.1 - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - - react@19.2.4: {} - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - - readdirp@4.1.2: {} - - recast@0.23.11: - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - - recma-build-jsx@1.0.0: - dependencies: - '@types/estree': 1.0.8 - estree-util-build-jsx: 3.0.1 - vfile: 6.0.3 - - recma-jsx@1.0.1(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - estree-util-to-js: 2.0.0 - recma-parse: 1.0.0 - recma-stringify: 1.0.0 - unified: 11.0.5 - - recma-parse@1.0.0: - dependencies: - '@types/estree': 1.0.8 - esast-util-from-js: 2.0.1 - unified: 11.0.5 - vfile: 6.0.3 - - recma-stringify@1.0.0: - dependencies: - '@types/estree': 1.0.8 - estree-util-to-js: 2.0.0 - unified: 11.0.5 - vfile: 6.0.3 - - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.1.0: - dependencies: - regex-utilities: 2.3.0 - - rehype-recma@1.0.0: - dependencies: - '@types/estree': 1.0.8 - '@types/hast': 3.0.4 - hast-util-to-estree: 3.1.3 - transitivePeerDependencies: - - supports-color - - remark-gfm@4.0.1: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 - micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-mdx@3.1.1: - dependencies: - mdast-util-mdx: 3.0.0 - micromark-extension-mdxjs: 3.0.0 - transitivePeerDependencies: - - supports-color - - remark-parse@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - micromark-util-types: 2.0.2 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-rehype@11.1.2: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.1 - unified: 11.0.5 - vfile: 6.0.3 - - remark-stringify@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.2 - unified: 11.0.5 - - remark@15.0.1: - dependencies: - '@types/mdast': 4.0.4 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - resolve-pkg-maps@1.0.0: {} - - rollup@4.59.0: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 - fsevents: 2.3.3 - - rou3@0.8.1: {} - - safer-buffer@2.1.2: {} - - scheduler@0.27.0: {} - - scroll-into-view-if-needed@3.1.0: - dependencies: - compute-scroll-into-view: 3.1.1 - - semver@6.3.1: {} - - semver@7.7.4: {} - - seroval-plugins@1.5.2(seroval@1.5.2): - dependencies: - seroval: 1.5.2 - - seroval@1.5.2: {} - - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.7.4 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - - shiki@3.23.0: - dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/engine-javascript': 3.23.0 - '@shikijs/engine-oniguruma': 3.23.0 - '@shikijs/langs': 3.23.0 - '@shikijs/themes': 3.23.0 - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - source-map-js@1.2.1: {} - - source-map@0.6.1: {} - - source-map@0.7.6: {} - - space-separated-tokens@2.0.2: {} - - srvx@0.11.15: {} - - srvx@0.11.8: {} - - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - - style-to-js@1.1.21: - dependencies: - style-to-object: 1.0.14 - - style-to-object@1.0.14: - dependencies: - inline-style-parser: 0.2.7 - - supports-color@10.2.2: {} - - tailwind-merge@3.5.0: {} - - tailwindcss@4.2.1: {} - - tapable@2.3.0: {} - - tiny-invariant@1.3.3: {} - - tinyexec@1.0.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - trim-lines@3.0.1: {} - - trough@2.2.0: {} - - tsconfck@3.1.6(typescript@5.9.3): - optionalDependencies: - typescript: 5.9.3 - - tslib@2.8.1: {} - - tsx@4.21.0: - dependencies: - esbuild: 0.27.7 - get-tsconfig: 4.13.7 - optionalDependencies: - fsevents: 2.3.3 - - typescript@5.9.3: {} - - ufo@1.6.3: {} - - undici-types@6.21.0: {} - - undici@7.18.2: {} - - undici@7.24.7: {} - - unenv@2.0.0-rc.24: - dependencies: - pathe: 2.0.3 - - unified@11.0.5: - dependencies: - '@types/unist': 3.0.3 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 6.0.3 - - unist-util-is@6.0.1: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position-from-estree@2.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-visit@5.1.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - unplugin@2.3.11: - dependencies: - '@jridgewell/remapping': 2.3.5 - acorn: 8.16.0 - picomatch: 4.0.4 - webpack-virtual-modules: 0.6.2 - - update-browserslist-db@1.2.3(browserslist@4.28.1): - dependencies: - browserslist: 4.28.1 - escalade: 3.2.0 - picocolors: 1.1.1 - - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): - dependencies: - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): - dependencies: - detect-node-es: 1.1.0 - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.14 - - use-sync-external-store@1.6.0(react@19.2.4): - dependencies: - react: 19.2.4 - - util-deprecate@1.0.2: {} - - vfile-message@4.0.3: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.3 - - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - transitivePeerDependencies: - - supports-color - - typescript - - vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.8 - rollup: 4.59.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 22.19.15 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.31.1 - tsx: 4.21.0 - - vitefu@1.1.3(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)): - optionalDependencies: - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) - - webpack-virtual-modules@0.6.2: {} - - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - workerd@1.20260301.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260301.1 - '@cloudflare/workerd-darwin-arm64': 1.20260301.1 - '@cloudflare/workerd-linux-64': 1.20260301.1 - '@cloudflare/workerd-linux-arm64': 1.20260301.1 - '@cloudflare/workerd-windows-64': 1.20260301.1 - - wrangler@4.71.0: - dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260301.1) - blake3-wasm: 2.1.5 - esbuild: 0.27.3 - miniflare: 4.20260301.1 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 - optionalDependencies: - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - ws@8.18.0: {} - - xmlbuilder2@4.0.3: - dependencies: - '@oozcitak/dom': 2.0.2 - '@oozcitak/infra': 2.0.2 - '@oozcitak/util': 10.0.0 - js-yaml: 4.1.1 - - yallist@3.1.1: {} - - youch-core@0.3.3: - dependencies: - '@poppinss/exception': 1.2.3 - error-stack-parser-es: 1.0.5 - - youch@4.1.0-beta.10: - dependencies: - '@poppinss/colors': 4.1.6 - '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.14 - cookie: 1.1.1 - youch-core: 0.3.3 - - zod@3.25.76: {} - - zod@4.3.6: {} - - zwitch@2.0.4: {} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml deleted file mode 100644 index b6b592e2e..000000000 --- a/web/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -minimumReleaseAge: 11520 -minimumReleaseAgeExclude: - - "@every-app/*" diff --git a/web/public/android-chrome-192x192.png b/web/public/android-chrome-192x192.png deleted file mode 100644 index cf4056156..000000000 Binary files a/web/public/android-chrome-192x192.png and /dev/null differ diff --git a/web/public/android-chrome-512x512.png b/web/public/android-chrome-512x512.png deleted file mode 100644 index 3e3943846..000000000 Binary files a/web/public/android-chrome-512x512.png and /dev/null differ diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png deleted file mode 100644 index fe3bb8c96..000000000 Binary files a/web/public/apple-touch-icon.png and /dev/null differ diff --git a/web/public/avatars/fed-avatar.jpg b/web/public/avatars/fed-avatar.jpg deleted file mode 100644 index e32b429ae..000000000 Binary files a/web/public/avatars/fed-avatar.jpg and /dev/null differ diff --git a/web/public/avatars/samik-avatar.jpg b/web/public/avatars/samik-avatar.jpg deleted file mode 100644 index 2709c97ec..000000000 Binary files a/web/public/avatars/samik-avatar.jpg and /dev/null differ diff --git a/web/public/avatars/tom-avatar.jpeg b/web/public/avatars/tom-avatar.jpeg deleted file mode 100644 index 4e6c18f29..000000000 Binary files a/web/public/avatars/tom-avatar.jpeg and /dev/null differ diff --git a/web/public/blog/dark-queries/gsc-queries.png b/web/public/blog/dark-queries/gsc-queries.png deleted file mode 100644 index c9f4741a2..000000000 Binary files a/web/public/blog/dark-queries/gsc-queries.png and /dev/null differ diff --git a/web/public/blog/seo-for-startups/backlink-analysis.png b/web/public/blog/seo-for-startups/backlink-analysis.png deleted file mode 100644 index c0c903b3e..000000000 Binary files a/web/public/blog/seo-for-startups/backlink-analysis.png and /dev/null differ diff --git a/web/public/blog/seo-for-startups/domain-overview.png b/web/public/blog/seo-for-startups/domain-overview.png deleted file mode 100644 index b7ab0e173..000000000 Binary files a/web/public/blog/seo-for-startups/domain-overview.png and /dev/null differ diff --git a/web/public/blog/seo-for-startups/infographic.png b/web/public/blog/seo-for-startups/infographic.png deleted file mode 100644 index bbac27c2e..000000000 Binary files a/web/public/blog/seo-for-startups/infographic.png and /dev/null differ diff --git a/web/public/blog/seo-for-startups/keyword-research.png b/web/public/blog/seo-for-startups/keyword-research.png deleted file mode 100644 index 3056e3a9e..000000000 Binary files a/web/public/blog/seo-for-startups/keyword-research.png and /dev/null differ diff --git a/web/public/blog/two-surfaces-two-timelines/ai-overview-organic-overlap-studies.png b/web/public/blog/two-surfaces-two-timelines/ai-overview-organic-overlap-studies.png deleted file mode 100644 index 658e0f8b3..000000000 Binary files a/web/public/blog/two-surfaces-two-timelines/ai-overview-organic-overlap-studies.png and /dev/null differ diff --git a/web/public/blog/two-surfaces-two-timelines/gptbot-user-agent-block-test.png b/web/public/blog/two-surfaces-two-timelines/gptbot-user-agent-block-test.png deleted file mode 100644 index 9778bb2fe..000000000 Binary files a/web/public/blog/two-surfaces-two-timelines/gptbot-user-agent-block-test.png and /dev/null differ diff --git a/web/public/blog/two-surfaces-two-timelines/two-surfaces-two-timelines.png b/web/public/blog/two-surfaces-two-timelines/two-surfaces-two-timelines.png deleted file mode 100644 index 020890486..000000000 Binary files a/web/public/blog/two-surfaces-two-timelines/two-surfaces-two-timelines.png and /dev/null differ diff --git a/web/public/blog/what-broke-the-99-dollar-ceiling/price-ceiling-openseo.png b/web/public/blog/what-broke-the-99-dollar-ceiling/price-ceiling-openseo.png deleted file mode 100644 index 2b0d12026..000000000 Binary files a/web/public/blog/what-broke-the-99-dollar-ceiling/price-ceiling-openseo.png and /dev/null differ diff --git a/web/public/blog/what-broke-the-99-dollar-ceiling/quote-never-used-semrush.png b/web/public/blog/what-broke-the-99-dollar-ceiling/quote-never-used-semrush.png deleted file mode 100644 index ed697e6a7..000000000 Binary files a/web/public/blog/what-broke-the-99-dollar-ceiling/quote-never-used-semrush.png and /dev/null differ diff --git a/web/public/blog/what-broke-the-99-dollar-ceiling/quote-one-thing.png b/web/public/blog/what-broke-the-99-dollar-ceiling/quote-one-thing.png deleted file mode 100644 index 66ff4f4a5..000000000 Binary files a/web/public/blog/what-broke-the-99-dollar-ceiling/quote-one-thing.png and /dev/null differ diff --git a/web/public/blog/wrong-half-of-your-audience/audience-intent-table-openseo.png b/web/public/blog/wrong-half-of-your-audience/audience-intent-table-openseo.png deleted file mode 100644 index d0e2ef11f..000000000 Binary files a/web/public/blog/wrong-half-of-your-audience/audience-intent-table-openseo.png and /dev/null differ diff --git a/web/public/blog/wrong-half-of-your-audience/audience-rung-grid-openseo.png b/web/public/blog/wrong-half-of-your-audience/audience-rung-grid-openseo.png deleted file mode 100644 index 5a51cb0a2..000000000 Binary files a/web/public/blog/wrong-half-of-your-audience/audience-rung-grid-openseo.png and /dev/null differ diff --git a/web/public/demo-poster.webp b/web/public/demo-poster.webp deleted file mode 100644 index 5c962342d..000000000 Binary files a/web/public/demo-poster.webp and /dev/null differ diff --git a/web/public/demo.mp4 b/web/public/demo.mp4 deleted file mode 100644 index d14243621..000000000 Binary files a/web/public/demo.mp4 and /dev/null differ diff --git a/web/public/favicon-16x16.png b/web/public/favicon-16x16.png deleted file mode 100644 index 7fbaf3103..000000000 Binary files a/web/public/favicon-16x16.png and /dev/null differ diff --git a/web/public/favicon-32x32.png b/web/public/favicon-32x32.png deleted file mode 100644 index 44638020f..000000000 Binary files a/web/public/favicon-32x32.png and /dev/null differ diff --git a/web/public/favicon.ico b/web/public/favicon.ico deleted file mode 100644 index 1495fdde3..000000000 Binary files a/web/public/favicon.ico and /dev/null differ diff --git a/web/public/library/ai-agent-seo/deslop-skill-catalogue-openseo.png b/web/public/library/ai-agent-seo/deslop-skill-catalogue-openseo.png deleted file mode 100644 index 174ec8c67..000000000 Binary files a/web/public/library/ai-agent-seo/deslop-skill-catalogue-openseo.png and /dev/null differ diff --git a/web/public/library/ai-agent-seo/mcp-rank-tracker-estimate-openseo.png b/web/public/library/ai-agent-seo/mcp-rank-tracker-estimate-openseo.png deleted file mode 100644 index 380a5b494..000000000 Binary files a/web/public/library/ai-agent-seo/mcp-rank-tracker-estimate-openseo.png and /dev/null differ diff --git a/web/public/library/ai-agent-seo/mcp-search-console-query-pull-openseo.png b/web/public/library/ai-agent-seo/mcp-search-console-query-pull-openseo.png deleted file mode 100644 index 17ee6f186..000000000 Binary files a/web/public/library/ai-agent-seo/mcp-search-console-query-pull-openseo.png and /dev/null differ diff --git a/web/public/library/ai-agent-seo/openseo-agent-skills-openseo.png b/web/public/library/ai-agent-seo/openseo-agent-skills-openseo.png deleted file mode 100644 index 4b6cc944d..000000000 Binary files a/web/public/library/ai-agent-seo/openseo-agent-skills-openseo.png and /dev/null differ diff --git a/web/public/library/ai-agent-seo/quote-cant-be-a-waymo.png b/web/public/library/ai-agent-seo/quote-cant-be-a-waymo.png deleted file mode 100644 index 8075922a9..000000000 Binary files a/web/public/library/ai-agent-seo/quote-cant-be-a-waymo.png and /dev/null differ diff --git a/web/public/library/ai-agent-seo/quote-humans-do-the-brief.png b/web/public/library/ai-agent-seo/quote-humans-do-the-brief.png deleted file mode 100644 index 30866747d..000000000 Binary files a/web/public/library/ai-agent-seo/quote-humans-do-the-brief.png and /dev/null differ diff --git a/web/public/library/ai-agent-seo/quote-morning-boot-claude.png b/web/public/library/ai-agent-seo/quote-morning-boot-claude.png deleted file mode 100644 index d720ebe5b..000000000 Binary files a/web/public/library/ai-agent-seo/quote-morning-boot-claude.png and /dev/null differ diff --git a/web/public/library/ai-agent-seo/quote-repeatability-is-scalability.png b/web/public/library/ai-agent-seo/quote-repeatability-is-scalability.png deleted file mode 100644 index 970fac5cb..000000000 Binary files a/web/public/library/ai-agent-seo/quote-repeatability-is-scalability.png and /dev/null differ diff --git a/web/public/library/competitive-analysis/brand-dominated-ranked-keywords-openseo.png b/web/public/library/competitive-analysis/brand-dominated-ranked-keywords-openseo.png deleted file mode 100644 index dc54dd44f..000000000 Binary files a/web/public/library/competitive-analysis/brand-dominated-ranked-keywords-openseo.png and /dev/null differ diff --git a/web/public/library/competitive-analysis/close-variant-keyword-inflation-openseo.png b/web/public/library/competitive-analysis/close-variant-keyword-inflation-openseo.png deleted file mode 100644 index e4d664d9d..000000000 Binary files a/web/public/library/competitive-analysis/close-variant-keyword-inflation-openseo.png and /dev/null differ diff --git a/web/public/library/competitive-analysis/competitive-research-mcp-tools-openseo.png b/web/public/library/competitive-analysis/competitive-research-mcp-tools-openseo.png deleted file mode 100644 index 344821a43..000000000 Binary files a/web/public/library/competitive-analysis/competitive-research-mcp-tools-openseo.png and /dev/null differ diff --git a/web/public/library/competitive-analysis/competitor-backlink-profile-openseo.png b/web/public/library/competitive-analysis/competitor-backlink-profile-openseo.png deleted file mode 100644 index a3daa5d96..000000000 Binary files a/web/public/library/competitive-analysis/competitor-backlink-profile-openseo.png and /dev/null differ diff --git a/web/public/library/competitive-analysis/competitor-domain-overview-openseo.png b/web/public/library/competitive-analysis/competitor-domain-overview-openseo.png deleted file mode 100644 index b06575511..000000000 Binary files a/web/public/library/competitive-analysis/competitor-domain-overview-openseo.png and /dev/null differ diff --git a/web/public/library/keyword-research/gsc-striking-distance-openseo.png b/web/public/library/keyword-research/gsc-striking-distance-openseo.png deleted file mode 100644 index e099d5a58..000000000 Binary files a/web/public/library/keyword-research/gsc-striking-distance-openseo.png and /dev/null differ diff --git a/web/public/library/keyword-research/intent-by-surface-openseo.png b/web/public/library/keyword-research/intent-by-surface-openseo.png deleted file mode 100644 index 25498d342..000000000 Binary files a/web/public/library/keyword-research/intent-by-surface-openseo.png and /dev/null differ diff --git a/web/public/library/keyword-research/keyword-clustering-topical-hub-openseo.png b/web/public/library/keyword-research/keyword-clustering-topical-hub-openseo.png deleted file mode 100644 index 1668a414b..000000000 Binary files a/web/public/library/keyword-research/keyword-clustering-topical-hub-openseo.png and /dev/null differ diff --git a/web/public/library/keyword-research/keyword-difficulty-volume-openseo.png b/web/public/library/keyword-research/keyword-difficulty-volume-openseo.png deleted file mode 100644 index c4d6a0461..000000000 Binary files a/web/public/library/keyword-research/keyword-difficulty-volume-openseo.png and /dev/null differ diff --git a/web/public/library/keyword-research/keyword-research-playbook.pdf b/web/public/library/keyword-research/keyword-research-playbook.pdf deleted file mode 100644 index fc05e20b4..000000000 Binary files a/web/public/library/keyword-research/keyword-research-playbook.pdf and /dev/null differ diff --git a/web/public/library/keyword-research/long-tail-keyword-filter-openseo.png b/web/public/library/keyword-research/long-tail-keyword-filter-openseo.png deleted file mode 100644 index c4948db4c..000000000 Binary files a/web/public/library/keyword-research/long-tail-keyword-filter-openseo.png and /dev/null differ diff --git a/web/public/library/keyword-research/positioning-demand-validation-openseo.png b/web/public/library/keyword-research/positioning-demand-validation-openseo.png deleted file mode 100644 index 55022e410..000000000 Binary files a/web/public/library/keyword-research/positioning-demand-validation-openseo.png and /dev/null differ diff --git a/web/public/library/keyword-research/search-intent-mapping-openseo.png b/web/public/library/keyword-research/search-intent-mapping-openseo.png deleted file mode 100644 index 311a2b720..000000000 Binary files a/web/public/library/keyword-research/search-intent-mapping-openseo.png and /dev/null differ diff --git a/web/public/library/keyword-research/seed-keyword-list-validation-openseo.png b/web/public/library/keyword-research/seed-keyword-list-validation-openseo.png deleted file mode 100644 index 9c1c598fc..000000000 Binary files a/web/public/library/keyword-research/seed-keyword-list-validation-openseo.png and /dev/null differ diff --git a/web/public/library/link-building/backlink-overview-openseo.png b/web/public/library/link-building/backlink-overview-openseo.png deleted file mode 100644 index ce0d38ba1..000000000 Binary files a/web/public/library/link-building/backlink-overview-openseo.png and /dev/null differ diff --git a/web/public/library/link-building/backlink-profile-newest-links-openseo.png b/web/public/library/link-building/backlink-profile-newest-links-openseo.png deleted file mode 100644 index 422ad6c47..000000000 Binary files a/web/public/library/link-building/backlink-profile-newest-links-openseo.png and /dev/null differ diff --git a/web/public/library/link-building/quote-bigger-footprint.png b/web/public/library/link-building/quote-bigger-footprint.png deleted file mode 100644 index 89e3adcf2..000000000 Binary files a/web/public/library/link-building/quote-bigger-footprint.png and /dev/null differ diff --git a/web/public/library/link-building/quote-link-out-to-a-tool.png b/web/public/library/link-building/quote-link-out-to-a-tool.png deleted file mode 100644 index 90d6e16c0..000000000 Binary files a/web/public/library/link-building/quote-link-out-to-a-tool.png and /dev/null differ diff --git a/web/public/library/link-building/quote-whether-its-relevant.png b/web/public/library/link-building/quote-whether-its-relevant.png deleted file mode 100644 index e3ed38d30..000000000 Binary files a/web/public/library/link-building/quote-whether-its-relevant.png and /dev/null differ diff --git a/web/public/library/link-building/referring-domains-openseo.png b/web/public/library/link-building/referring-domains-openseo.png deleted file mode 100644 index dca3faa0d..000000000 Binary files a/web/public/library/link-building/referring-domains-openseo.png and /dev/null differ diff --git a/web/public/library/link-building/top-pages-by-backlinks-openseo.png b/web/public/library/link-building/top-pages-by-backlinks-openseo.png deleted file mode 100644 index ef127cb71..000000000 Binary files a/web/public/library/link-building/top-pages-by-backlinks-openseo.png and /dev/null differ diff --git a/web/public/library/rank-tracking/local-rank-grid-cookeville-openseo.png b/web/public/library/rank-tracking/local-rank-grid-cookeville-openseo.png deleted file mode 100644 index ee17ed53f..000000000 Binary files a/web/public/library/rank-tracking/local-rank-grid-cookeville-openseo.png and /dev/null differ diff --git a/web/public/library/rank-tracking/quote-big-numbers-of-what.png b/web/public/library/rank-tracking/quote-big-numbers-of-what.png deleted file mode 100644 index f61fa65cd..000000000 Binary files a/web/public/library/rank-tracking/quote-big-numbers-of-what.png and /dev/null differ diff --git a/web/public/library/rank-tracking/quote-not-reporting-insights.png b/web/public/library/rank-tracking/quote-not-reporting-insights.png deleted file mode 100644 index ca8c442fb..000000000 Binary files a/web/public/library/rank-tracking/quote-not-reporting-insights.png and /dev/null differ diff --git a/web/public/library/rank-tracking/quote-proximity-and-relevance.png b/web/public/library/rank-tracking/quote-proximity-and-relevance.png deleted file mode 100644 index 695cc1c56..000000000 Binary files a/web/public/library/rank-tracking/quote-proximity-and-relevance.png and /dev/null differ diff --git a/web/public/library/rank-tracking/quote-sixteen-months-search-console.png b/web/public/library/rank-tracking/quote-sixteen-months-search-console.png deleted file mode 100644 index d8fa27ac9..000000000 Binary files a/web/public/library/rank-tracking/quote-sixteen-months-search-console.png and /dev/null differ diff --git a/web/public/library/rank-tracking/rank-tracker-cost-estimate-openseo.png b/web/public/library/rank-tracking/rank-tracker-cost-estimate-openseo.png deleted file mode 100644 index 1aabe1daf..000000000 Binary files a/web/public/library/rank-tracking/rank-tracker-cost-estimate-openseo.png and /dev/null differ diff --git a/web/public/library/rank-tracking/rank-tracker-positions-openseo.png b/web/public/library/rank-tracking/rank-tracker-positions-openseo.png deleted file mode 100644 index e85cddeb9..000000000 Binary files a/web/public/library/rank-tracking/rank-tracker-positions-openseo.png and /dev/null differ diff --git a/web/public/library/rank-tracking/search-console-clicks-by-page-openseo.png b/web/public/library/rank-tracking/search-console-clicks-by-page-openseo.png deleted file mode 100644 index 5fcec78bb..000000000 Binary files a/web/public/library/rank-tracking/search-console-clicks-by-page-openseo.png and /dev/null differ diff --git a/web/public/library/site-audit/audit-issue-how-to-fix-openseo.png b/web/public/library/site-audit/audit-issue-how-to-fix-openseo.png deleted file mode 100644 index 6a8162a96..000000000 Binary files a/web/public/library/site-audit/audit-issue-how-to-fix-openseo.png and /dev/null differ diff --git a/web/public/library/site-audit/audit-report-by-severity-openseo.png b/web/public/library/site-audit/audit-report-by-severity-openseo.png deleted file mode 100644 index 4c6198001..000000000 Binary files a/web/public/library/site-audit/audit-report-by-severity-openseo.png and /dev/null differ diff --git a/web/public/library/site-audit/duplicate-answer-permalinks-openseo.png b/web/public/library/site-audit/duplicate-answer-permalinks-openseo.png deleted file mode 100644 index 0f278b324..000000000 Binary files a/web/public/library/site-audit/duplicate-answer-permalinks-openseo.png and /dev/null differ diff --git a/web/public/robots.txt b/web/public/robots.txt deleted file mode 100644 index fa239f786..000000000 --- a/web/public/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: -Sitemap: https://openseo.so/sitemap.xml diff --git a/web/public/site.webmanifest b/web/public/site.webmanifest deleted file mode 100644 index 63f961f37..000000000 --- a/web/public/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "OpenSEO", - "short_name": "OpenSEO", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#0a0f0d", - "background_color": "#0a0f0d", - "display": "standalone" -} diff --git a/web/public/social-card.jpg b/web/public/social-card.jpg deleted file mode 100644 index 1c13d8d67..000000000 Binary files a/web/public/social-card.jpg and /dev/null differ diff --git a/web/scripts/generate-sitemap.js b/web/scripts/generate-sitemap.js deleted file mode 100644 index dd77d85ec..000000000 --- a/web/scripts/generate-sitemap.js +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env node - -import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { FEATURE_PAGE_SLUGS } from "../src/lib/feature-page-slugs.js"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const DIST_DIR = join(__dirname, "../dist/client"); -const BLOG_CONTENT_DIR = join(__dirname, "../content/blogs"); -const DOCS_CONTENT_DIR = join(__dirname, "../content/docs"); -const LIBRARY_ROUTES_DIR = join(__dirname, "../src/routes/_marketing/library"); - -const DEFAULT_SITE_URL = "https://openseo.so"; -const SITE_URL = (process.env.SITE_URL ?? DEFAULT_SITE_URL).replace(/\/+$/, ""); - -const STATIC_PATHS = [ - "/", - "/pricing", - "/privacy", - "/terms-and-conditions", - "/blogs", - "/docs", - "/features", - "/features/mcp", - "/backlink-checker", - "/open-source-seo", - "/google-search-console-mcp", - "/roadmap", - "/support", - ...Object.values(FEATURE_PAGE_SLUGS).map((slug) => `/features/${slug}`), -]; - -function getContentEntries( - contentDir, - basePath, - dir = contentDir, - segments = [], -) { - if (!existsSync(dir)) { - return []; - } - - return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { - if (entry.name.startsWith(".")) { - return []; - } - - const entryPath = join(dir, entry.name); - - if (entry.isDirectory()) { - return getContentEntries(contentDir, basePath, entryPath, [ - ...segments, - entry.name, - ]); - } - - if (!entry.isFile() || !/\.(md|mdx)$/i.test(entry.name)) { - return []; - } - - const slug = entry.name.replace(/\.(md|mdx)$/i, ""); - const pathSegments = slug === "index" ? segments : [...segments, slug]; - - return [ - { - path: - pathSegments.length > 0 - ? `${basePath}/${pathSegments.join("/")}` - : basePath, - lastmod: statSync(entryPath).mtime.toISOString(), - }, - ]; - }); -} - -function getLibraryPaths(dir = LIBRARY_ROUTES_DIR, segments = []) { - if (!existsSync(dir)) { - return []; - } - - return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { - if (entry.name.startsWith(".")) { - return []; - } - - if (entry.isDirectory()) { - return getLibraryPaths(join(dir, entry.name), [...segments, entry.name]); - } - - if (!entry.isFile() || !/\.tsx$/i.test(entry.name)) { - return []; - } - - const slug = entry.name.replace(/\.tsx$/i, ""); - const pathSegments = slug === "index" ? segments : [...segments, slug]; - - return [ - pathSegments.length > 0 - ? `/library/${pathSegments.join("/")}` - : "/library", - ]; - }); -} - -function toCanonicalUrl(path) { - if (path === "/") { - return `${SITE_URL}/`; - } - - return `${SITE_URL}${path.replace(/\/+$/, "")}`; -} - -function main() { - if (!existsSync(DIST_DIR) || !statSync(DIST_DIR).isDirectory()) { - throw new Error(`Build output directory does not exist: ${DIST_DIR}`); - } - - const entries = new Map(); - for (const path of [...STATIC_PATHS, ...getLibraryPaths()]) { - entries.set(path, { path, lastmod: null }); - } - for (const entry of [ - ...getContentEntries(BLOG_CONTENT_DIR, "/blogs"), - ...getContentEntries(DOCS_CONTENT_DIR, "/docs"), - ]) { - entries.set(entry.path, entry); - } - - const sorted = Array.from(entries.values()).sort((a, b) => - toCanonicalUrl(a.path).localeCompare(toCanonicalUrl(b.path)), - ); - - const sitemapBody = sorted - .map(({ path, lastmod }) => { - const loc = toCanonicalUrl(path); - const lastmodTag = lastmod ? `\n ${lastmod}` : ""; - return ` \n ${loc}${lastmodTag}\n `; - }) - .join("\n"); - - const sitemapXml = `\n\n${sitemapBody}\n\n`; - - const sitemapPath = join(DIST_DIR, "sitemap.xml"); - writeFileSync(sitemapPath, sitemapXml); - - console.log(`Generated sitemap with ${sorted.length} URLs at ${sitemapPath}`); -} - -main(); diff --git a/web/source.config.ts b/web/source.config.ts deleted file mode 100644 index 1db0582cc..000000000 --- a/web/source.config.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - defineConfig, - defineCollections, - frontmatterSchema, - metaSchema, -} from "fumadocs-mdx/config/zod-3"; -import { z } from "zod"; - -const pageSchema = frontmatterSchema as any; - -export const blog = defineCollections({ - type: "doc", - dir: "content/blogs", - schema: pageSchema.extend({ - author: z.string(), - date: z.string(), - }), -}); - -export const docs = defineCollections({ - type: "doc", - dir: "content/docs", - schema: pageSchema, -}); - -export const docsMeta = defineCollections({ - type: "meta", - dir: "content/docs", - schema: metaSchema, -}); - -export const legal = defineCollections({ - type: "doc", - dir: "content/legal", - schema: pageSchema, -}); - -export default defineConfig(); diff --git a/web/src/components/agent-prompt.tsx b/web/src/components/agent-prompt.tsx deleted file mode 100644 index bfaae0564..000000000 --- a/web/src/components/agent-prompt.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock"; -import { - agentUpdatePrompt, - getAgentSetupPrompt, -} from "../../../src/client/features/ai-mcp/agentSetupPrompt"; - -export function AgentPrompt({ kind }: { kind: "setup" | "update" }) { - const prompt = - kind === "setup" - ? getAgentSetupPrompt("https://app.openseo.so") - : agentUpdatePrompt; - - return ( - -
    -        {prompt}
    -      
    -
    - ); -} diff --git a/web/src/components/backlink-checker-tool.tsx b/web/src/components/backlink-checker-tool.tsx deleted file mode 100644 index 21e0ec3ba..000000000 --- a/web/src/components/backlink-checker-tool.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import { type FormEvent, useEffect, useRef, useState } from "react"; - -// Public site key. Cloudflare's always-passing test key is the dev-only -// fallback; a prod build without VITE_TURNSTILE_SITE_KEY renders no widget. -const TURNSTILE_SITE_KEY = - (import.meta.env.VITE_TURNSTILE_SITE_KEY as string | undefined) || - (import.meta.env.DEV ? "1x00000000000000000000AA" : ""); - -type TurnstileApi = { - render( - container: HTMLElement, - options: { - sitekey: string; - appearance?: string; - callback: (token: string) => void; - "expired-callback"?: () => void; - }, - ): string; - reset(widgetId?: string): void; -}; - -type BacklinkRow = { - domainFrom: string | null; - urlFrom: string | null; - urlTo: string | null; - pageTitle: string | null; - anchor: string | null; - dofollow: boolean | null; - domainRank: number | null; -}; - -type CheckResult = { - target: string; - summary: { - rank: number | null; - backlinks: number | null; - referringDomains: number | null; - brokenBacklinks: number | null; - }; - topBacklinks: BacklinkRow[]; -}; - -function formatCount(value: number | null): string { - return typeof value === "number" ? value.toLocaleString("en-US") : "—"; -} - -/** Small info icon that reveals an explanation on hover or focus. */ -function InfoTip({ - tip, - align = "center", -}: { - tip: string; - align?: "center" | "right"; -}) { - return ( - - - - - ); -} - -export function BacklinkCheckerTool() { - const [target, setTarget] = useState(""); - const [status, setStatus] = useState<"idle" | "loading" | "done" | "error">( - "idle", - ); - const [errorMessage, setErrorMessage] = useState(""); - const [result, setResult] = useState(null); - - const widgetContainerRef = useRef(null); - const widgetIdRef = useRef(null); - const tokenRef = useRef(""); - - useEffect(() => { - if (!TURNSTILE_SITE_KEY) return; - const w = window as unknown as { - turnstile?: TurnstileApi; - onloadTurnstileCallback?: () => void; - }; - const renderWidget = () => { - if (!w.turnstile || !widgetContainerRef.current) return; - if (widgetIdRef.current !== null) return; - widgetIdRef.current = w.turnstile.render(widgetContainerRef.current, { - sitekey: TURNSTILE_SITE_KEY, - // Invisible unless Turnstile decides the visitor needs a challenge. - appearance: "interaction-only", - callback: (token) => { - tokenRef.current = token; - }, - "expired-callback": () => { - tokenRef.current = ""; - }, - }); - }; - - if (w.turnstile) { - renderWidget(); - return; - } - w.onloadTurnstileCallback = renderWidget; - if (!document.querySelector("script[data-turnstile]")) { - const script = document.createElement("script"); - script.src = - "https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback"; - script.async = true; - script.dataset.turnstile = "true"; - document.head.appendChild(script); - } - }, []); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setStatus("loading"); - setErrorMessage(""); - try { - const res = await fetch("/api/backlink-check", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - target, - turnstileToken: tokenRef.current || undefined, - }), - }); - const data = (await res.json().catch(() => ({}))) as - | CheckResult - | { error?: string }; - if (!res.ok) { - throw new Error( - (data as { error?: string }).error || "Something went wrong", - ); - } - setResult(data as CheckResult); - setStatus("done"); - } catch (err) { - setStatus("error"); - setErrorMessage( - err instanceof Error ? err.message : "Something went wrong", - ); - } finally { - // Tokens are single-use; get a fresh one for the next check. - tokenRef.current = ""; - const w = window as unknown as { turnstile?: TurnstileApi }; - if (w.turnstile && widgetIdRef.current !== null) { - w.turnstile.reset(widgetIdRef.current); - } - } - }; - - return ( -
    -
    -
    - - setTarget(e.target.value)} - placeholder="example.com" - disabled={status === "loading"} - className="h-11 min-w-0 flex-1 rounded-lg border border-[var(--color-border-subtle)] bg-white px-3.5 text-base text-neutral-900 placeholder:text-neutral-500 transition focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900" - /> - -
    -
    -

    - Free · No signup · Instant results -

    - {status === "error" && ( -

    {errorMessage}

    - )} - - - {status === "done" && result ? : null} -
    - ); -} - -function CheckResults({ result }: { result: CheckResult }) { - const { summary, topBacklinks } = result; - const total = summary.backlinks; - const hasMore = typeof total === "number" && total > topBacklinks.length; - - const metrics = [ - { - label: "Domain rank", - value: formatCount(summary.rank), - tip: "DataForSEO's 0-100 strength score for a domain's link profile. Similar idea to Ahrefs DR or Moz DA, but each tool uses its own index and formula, so numbers differ between tools.", - }, - { - label: "Backlinks", - value: formatCount(summary.backlinks), - tip: "Total individual links pointing at this domain, counting multiple links from the same website.", - }, - { - label: "Referring domains", - value: formatCount(summary.referringDomains), - tip: "Unique websites that link to this domain at least once.", - }, - { - label: "Broken backlinks", - value: formatCount(summary.brokenBacklinks), - tip: "Links pointing at pages on this domain that no longer load, such as deleted pages returning 404.", - }, - ]; - - return ( -
    -

    - Backlink profile for{" "} - - {result.target} - -

    - -
    - {metrics.map((metric, index) => ( -
    1 && "border-t border-[var(--color-border-subtle)]", - index > 0 && "md:border-l md:border-t-0", - ] - .filter(Boolean) - .join(" ")} - > -
    - {metric.label} - 1 ? "right" : "center"} - /> -
    -
    - {metric.value} -
    -
    - ))} -
    - - {topBacklinks.length > 0 ? ( -
    - - - - - - - - - - - {topBacklinks.map((row) => ( - - - - - - - ))} - -
    - Rank - - Referring page - Anchor and target - - - Type - -
    - {formatCount(row.domainRank)} - -

    - {row.pageTitle ?? row.domainFrom ?? "—"} -

    - {row.urlFrom ? ( - - {row.urlFrom} - - ) : null} -
    -

    - {row.anchor ?? "—"} -

    - {row.urlTo ? ( -

    - {row.urlTo} -

    - ) : null} -
    - - {row.dofollow ? "Follow" : "Nofollow"} - -
    -
    - ) : ( -

    - No live backlinks found for this domain in the index yet. -

    - )} - -
    -

    - {hasMore ? ( - <> - Showing the top {topBacklinks.length} backlinks, one per referring - domain, strongest domains first.{" "} - - {formatCount(total)} total backlinks - {" "} - are in the index for this domain. - - ) : ( - <> - Explore the full picture: referring domains, anchors, new and lost - links, and spam signals. - - )} -

    - -
    -
    - ); -} diff --git a/web/src/components/blog-layout.tsx b/web/src/components/blog-layout.tsx deleted file mode 100644 index b56ae6388..000000000 --- a/web/src/components/blog-layout.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Link } from "@tanstack/react-router"; -import type { ReactNode } from "react"; - -const navLinks = [ - { label: "Features", to: "/features" }, - { label: "Blog", to: "/blogs" }, - { label: "Docs", to: "/docs" }, - { label: "Pricing", to: "/pricing" }, -] as const; - -export function BlogLayout({ children }: { children: ReactNode }) { - return ( -
    -
    -
    - - OpenSEO - - - -
    -
    - - {children} -
    - ); -} diff --git a/web/src/components/comparison-table.tsx b/web/src/components/comparison-table.tsx deleted file mode 100644 index 6ba3b080e..000000000 --- a/web/src/components/comparison-table.tsx +++ /dev/null @@ -1,203 +0,0 @@ -type Tone = "positive" | "negative" | "neutral"; - -type Cell = { - text: string; - tone?: Tone; - code?: boolean; -}; - -type Column = { - name: string; - highlight?: boolean; -}; - -const COLUMNS: Column[] = [ - { name: "OpenSEO", highlight: true }, - { name: "DIY open-source repos" }, - { name: "Data-pipeline tools" }, -]; - -const ROWS: { label: string; cells: Cell[] }[] = [ - { - label: "Setup", - cells: [ - { text: "Simple, guided onboarding", tone: "positive" }, - { text: "~30 min in the Google Cloud console" }, - { text: "Account + connector setup" }, - ], - }, - { - label: "Google Cloud project", - cells: [ - { text: "Not needed", tone: "positive" }, - { text: "Required", tone: "negative" }, - { text: "Usually not needed", tone: "positive" }, - ], - }, - { - label: "Cost to run", - cells: [ - { - text: "Included in the $10/mo plan, zero credits (free to self-host)", - tone: "positive", - }, - { text: "Free (your time + your own quota)" }, - { text: "Paid or limited free tier", tone: "negative" }, - ], - }, - { - label: "Read-only and safe", - cells: [ - { text: "webmasters.readonly", tone: "positive", code: true }, - { text: "Depends on the scopes you grant" }, - { text: "Varies" }, - ], - }, - { - label: "Built for SEO", - cells: [ - { - text: "Also does keyword, rank, and backlink research", - tone: "positive", - }, - { text: "Search Console only", tone: "negative" }, - { text: "Reporting and analytics focus" }, - ], - }, - { - label: "Self-host option", - cells: [ - { text: "Yes", tone: "positive" }, - { text: "Yes", tone: "positive" }, - { text: "No", tone: "negative" }, - ], - }, -]; - -export function ComparisonTable() { - return ( -
    -
    - - - - - ))} - - - - {ROWS.map((row) => ( - - - {row.cells.map((cell, i) => { - const highlight = COLUMNS[i]?.highlight; - return ( - - ); - })} - - ))} - -
    - {COLUMNS.map((col) => ( - - {col.name} -
    - {row.label} - - -
    -
    -
    - ); -} - -function CellContent({ cell, highlight }: { cell: Cell; highlight?: boolean }) { - const tone = cell.tone ?? "neutral"; - const textClass = - tone === "negative" - ? "text-neutral-400" - : highlight && tone === "positive" - ? "font-medium text-neutral-900" - : "text-neutral-700"; - - return ( -
    - - - - - {cell.code ? ( - - {cell.text} - - ) : ( - cell.text - )} - -
    - ); -} - -function ToneIcon({ tone }: { tone: Tone }) { - if (tone === "positive") { - return ( - - ); - } - if (tone === "negative") { - return ( - - ); - } - return null; -} diff --git a/web/src/components/content-index.tsx b/web/src/components/content-index.tsx deleted file mode 100644 index 68bfb2620..000000000 --- a/web/src/components/content-index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Link } from "@tanstack/react-router"; - -type ContentIndexItem = { - title: string; - description?: string; - slugs: string[]; - url: string; -}; - -type ContentIndexProps = { - eyebrow: string; - title: string; - description: string; - emptyLabel: string; - items: ContentIndexItem[]; - route: "/docs/$"; -}; - -export function ContentIndex({ - eyebrow, - title, - description, - emptyLabel, - items, - route, -}: ContentIndexProps) { - return ( -
    -

    {eyebrow}

    -

    - {title} -

    -

    - {description} -

    - - {items.length === 0 ? ( -

    {emptyLabel}

    - ) : ( -
    - {items.map((item) => ( - -

    - {item.title} -

    - {item.description ? ( -

    - {item.description} -

    - ) : null} - - ))} -
    - )} -
    - ); -} diff --git a/web/src/components/content-post.tsx b/web/src/components/content-post.tsx deleted file mode 100644 index 1d968effe..000000000 --- a/web/src/components/content-post.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Link } from "@tanstack/react-router"; -import { DocsBody } from "fumadocs-ui/page"; -import defaultMdxComponents from "fumadocs-ui/mdx"; -import { Suspense } from "react"; -import { RunSkillCallout } from "@/components/run-skill-callout"; -import { AgentPrompt } from "@/components/agent-prompt"; - -type ContentPostProps = { - backLabel: string; - backTo: "/docs"; - title: string; - description?: string; - Content: React.ComponentType; -}; - -export const mdxComponents = { - ...defaultMdxComponents, - RunSkillCallout, - AgentPrompt, -}; - -export function ContentPost({ - backLabel, - backTo, - title, - description, - Content, -}: ContentPostProps) { - return ( -
    -
    -
    - - - {backLabel} - -
    -

    - {title} -

    - {description ? ( -

    - {description} -

    - ) : null} -
    - - - - - -
    - ); -} diff --git a/web/src/components/feature-page.tsx b/web/src/components/feature-page.tsx deleted file mode 100644 index 9ea7c5b2f..000000000 --- a/web/src/components/feature-page.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import type { FeaturePage } from "@/lib/feature-pages"; - -type FeaturePageProps = { - page: FeaturePage; -}; - -export function FeaturePageTemplate({ page }: FeaturePageProps) { - return ( -
    -
    -

    - {page.eyebrow} -

    -

    - {page.title} -

    -

    - {page.description} -

    - -
    - - - - {page.featuredLink ? ( -
    -

    - - {page.featuredLink.title} - -

    -

    - {page.featuredLink.description} -

    -
    - ) : null} - -
    -

    - What you can do -

    -
      - {page.workflows.map((workflow, index) => ( -
    1. - - {String(index + 1).padStart(2, "0")} - -

      - {workflow.title} -

      -

      - {workflow.description} -

      -
    2. - ))} -
    -
    - - {page.showMetrics ? : null} - -
    - - -
    - - {page.guides ? : null} - -
    -

    - Related features -

    -
    - {page.related.map((item) => ( - - {item.label} - - - ))} -
    -
    - -
    -

    - FAQ -

    -
    - {page.faqs.map((faq) => ( -
    -

    - {faq.question} -

    -

    - {faq.answer} -

    -
    - ))} -
    -
    - -
    -

    - Try OpenSEO -

    -

    - The open source alternative to bloated, expensive, legacy SEO tools. -

    - -
    -
    - ); -} - -function FeatureImage({ page }: FeaturePageProps) { - return ( -
    - {page.imageAlt} -
    - {page.eyebrow} in OpenSEO. -
    -
    - ); -} - -function MetricsSection({ page }: FeaturePageProps) { - return ( -
    -

    - Data you can act on -

    -
    - {page.metrics.map((metric, index) => ( -
    0 && "border-t border-[var(--color-border-subtle)]", - index % 2 === 1 && - "sm:border-l sm:border-[var(--color-border-subtle)]", - index > 1 && "sm:border-t", - index > 0 && - "md:border-l md:border-t-0 md:border-[var(--color-border-subtle)]", - ] - .filter(Boolean) - .join(" ")} - > -
    - {metric.label} -
    -
    - {metric.value} -
    -
    - ))} -
    -
    - ); -} - -function GuidesSection({ - guides, -}: { - guides: NonNullable; -}) { - return ( -
    -

    - {guides.title} -

    -

    - {guides.description} -

    -
    - {guides.items.map((item) => ( - -

    - {item.label} - -

    -

    - {item.description} -

    -
    - ))} -
    - -
    - ); -} - -function ListSection({ title, items }: { title: string; items: string[] }) { - return ( -
    -

    - {title} -

    -
      - {items.map((item) => ( -
    • -
    • - ))} -
    -
    - ); -} diff --git a/web/src/components/landing-page.css b/web/src/components/landing-page.css deleted file mode 100644 index cda427832..000000000 --- a/web/src/components/landing-page.css +++ /dev/null @@ -1,703 +0,0 @@ -/** - * Landing-page styles. - * - * Scoped under `.itc` and kept unlayered (plain CSS) so these component - * classes win over Tailwind's layered utilities, matching the page's design. - * Palette vars that exactly match the global @theme tokens (app.css) alias - * them instead of redefining the hex; vars with bespoke values stay local. - */ - -.itc { - --canvas: var(--color-surface); - --surface: var(--color-surface-raised); - --surface-2: #ebe7e1; - --ink: var(--color-brand); - --ink-muted: #626260; - --ink-subtle: #7b7b78; - --ink-tertiary: #9c9fa5; - --hairline: #d3cec6; - --hairline-soft: #ebe7e1; - --orange: var(--color-brand-accent); - --inverse: #000000; - --inverse-surface: #313130; - --inverse-muted: #9c9fa5; - /* Testimonial band (light: deep greige band, white cards, charcoal text) */ - --q-bg: #e3dccd; - --q-card: #ffffff; - --q-card-border: #d0c9bb; - --q-text: #111111; - --q-muted: #7b7b78; - --q-avatar-bg: #111111; - --q-avatar-fg: #ffffff; - --q-mark: #b8ac96; - /* Dark "emphasis" surfaces: featured pricing card + MCP terminal */ - --feat-bg: #221f1b; - --feat-text: #ffffff; - --term-bg: #221f1b; - font-family: - "Inter", - ui-sans-serif, - system-ui, - -apple-system, - sans-serif; - background: var(--canvas); - color: var(--ink); - font-size: 16px; - line-height: 1.5; - font-weight: 400; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; -} -.itc *, -.itc *::before, -.itc *::after { - box-sizing: border-box; -} -.itc img, -.itc video { - display: block; - max-width: 100%; -} -/* Zero-specificity reset so component classes below always win. */ -.itc :where(a) { - color: inherit; - text-decoration: none; -} - -.itc-container { - max-width: 1232px; - margin: 0 auto; - padding: 0 24px; -} -.itc-narrow { - max-width: 760px; -} - -.itc-section { - padding: 96px 0; -} -.itc-section-open-source { - background: #f8f6f2; - border-top: 1px solid var(--hairline); - border-bottom: 1px solid var(--hairline); -} -.itc-section-demo { - background: var(--canvas); -} -.itc-section-demo + .itc-footer { - margin-top: 0; -} -@media (max-width: 767px) { - .itc-section { - padding: 64px 0; - } -} - -/* ── Type scale (Inter standing in for Saans; weight 500 carries display) ── */ -.itc-display-xl { - font-size: clamp(40px, 6.5vw, 72px); - font-weight: 500; - line-height: 1.05; - letter-spacing: -0.028em; - margin: 0; - text-wrap: balance; -} -.itc-display-lg { - font-size: clamp(32px, 4.4vw, 52px); - font-weight: 500; - line-height: 1.1; - letter-spacing: -0.026em; - margin: 0; - text-wrap: balance; -} -.itc-display-md { - font-size: clamp(28px, 3.4vw, 40px); - font-weight: 500; - line-height: 1.15; - letter-spacing: -0.02em; - margin: 0; - text-wrap: balance; -} -.itc-card-title { - font-size: 22px; - font-weight: 500; - line-height: 1.25; - letter-spacing: -0.013em; - margin: 0; -} -.itc-subhead { - font-size: 20px; - font-weight: 400; - line-height: 1.4; - letter-spacing: -0.01em; -} -.itc-body-lg { - font-size: 18px; - line-height: 1.5; - letter-spacing: -0.005em; -} -.itc-body-sm { - font-size: 14px; - line-height: 1.5; -} -.itc-caption { - font-size: 12px; - line-height: 1.4; -} -.itc-eyebrow { - font-size: 14px; - font-weight: 500; - line-height: 1.3; - letter-spacing: 0; - margin: 0 0 16px; - color: var(--ink-muted); -} -.itc-muted { - color: var(--ink-muted); -} -.itc-subtle { - color: var(--ink-subtle); -} -@media (max-width: 479px) { - .itc-subhead { - font-size: 18px; - } -} - -/* ── Buttons: 8px radius, 10x18 padding, never pill ── */ -.itc-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - min-height: 44px; - padding: 10px 18px; - border-radius: 8px; - border: 1px solid transparent; - font-family: inherit; - font-size: 15px; - font-weight: 500; - line-height: 1.2; - letter-spacing: 0; - text-decoration: none; - cursor: pointer; - transition: - background-color 0.15s ease, - border-color 0.15s ease; -} -.itc-btn:focus-visible { - outline: 2px solid var(--ink); - outline-offset: 2px; -} -.itc-btn-lg { - min-height: 54px; - padding: 15px 28px; - font-size: 17px; - border-radius: 10px; -} -.itc-btn-primary { - background: var(--ink); - color: #ffffff; -} -.itc-btn-primary:hover { - background: #000000; -} -.itc-btn-secondary { - background: var(--surface); - color: var(--ink); - border-color: var(--hairline); -} -.itc-btn-secondary:hover { - border-color: var(--ink); -} -.itc-btn-fin { - background: var(--orange); - color: #ffffff; -} -.itc-btn-fin:hover { - background: #e64d00; -} -.itc-btn-fin:focus-visible { - outline-color: var(--orange); -} -.itc-arrow { - transition: transform 0.15s ease; -} -.itc-btn:hover .itc-arrow, -.itc-textlink:hover .itc-arrow { - transform: translateX(2px); -} -@media (prefers-reduced-motion: reduce) { - .itc-arrow, - .itc-btn, - .itc-textlink { - transition: none; - } - .itc-btn:hover .itc-arrow, - .itc-textlink:hover .itc-arrow { - transform: none; - } -} - -.itc-textlink { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 15px; - font-weight: 500; - color: var(--ink); - text-decoration: none; - border-radius: 4px; -} -.itc-textlink:hover { - text-decoration: underline; - text-underline-offset: 4px; -} -.itc-textlink:focus-visible { - outline: 2px solid var(--ink); - outline-offset: 2px; -} - -/* ── Surfaces: white lift on cream, hairline borders, no shadows ── */ -.itc-card { - background: var(--surface); - border: 1px solid var(--hairline); - border-radius: 12px; -} -.itc-mockup { - background: var(--surface); - border: 1px solid var(--hairline); - border-radius: 16px; - padding: 12px; -} -@media (min-width: 768px) { - .itc-mockup { - padding: 20px; - } -} -.itc-mockup-media { - border-radius: 8px; - border: 1px solid var(--hairline-soft); - overflow: hidden; - background: var(--canvas); -} - -/* ── Hero ── */ -.itc-hero { - align-items: center; - display: flex; - min-height: calc(100svh - 150px); - padding-top: clamp(104px, 12vh, 176px); - padding-bottom: clamp(96px, 12vh, 144px); - position: relative; - text-align: center; -} -.itc-hero::before { - content: ""; - position: absolute; - inset: 0; - pointer-events: none; - transition: background-color 0.18s ease; -} -.itc-hero .itc-container { - position: relative; - z-index: 1; -} -/* Out of flow so the hero's vertical centering ignores the laurel and the - headline block sits exactly where it did without it. */ -.itc-hero-ph-laurel { - color: var(--ink); - display: inline-flex; - position: absolute; - bottom: calc(100% + 28px); - left: 50%; - transform: translateX(-50%); - opacity: 0.85; - transition: opacity 0.18s ease; -} -.itc-hero-ph-laurel:hover { - opacity: 1; -} -.itc-hero-ph-laurel svg { - height: 44px; - width: auto; -} -@media (max-width: 767px) { - .itc-hero-ph-laurel { - bottom: calc(100% + 20px); - } - .itc-hero-ph-laurel svg { - height: 38px; - } -} -.itc-hero-title { - color: var(--ink); - transition: color 0.18s ease; -} -.itc-hero-subtitle { - transition: - background-color 0.18s ease, - border-color 0.18s ease, - color 0.18s ease; -} -@media (max-width: 767px) { - .itc-hero { - min-height: calc(100svh - 120px); - padding-top: 72px; - padding-bottom: 88px; - } -} -.itc-hero-ctas { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 12px; - margin-top: 32px; -} -.itc-hero-cta-group { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; -} -.itc-hero-cta-note { - margin: 0; - color: var(--ink-subtle); - font-size: 13px; - line-height: 1.4; -} -@media (max-width: 479px) { - .itc-hero-ctas { - flex-direction: column; - align-items: stretch; - } - .itc-hero-cta-group { - align-items: stretch; - } - .itc-hero-cta-note { - text-align: center; - } -} - -/* ── Testimonial band (colors driven by --q-* palette vars) ── */ -.itc-inverse { - background: var(--q-bg); - color: var(--q-text); -} -.itc-testimonials { - padding: 72px 0; -} -.itc-testimonials-title { - max-width: none; - text-align: center; - white-space: nowrap; -} -.itc-quote-mark { - color: var(--q-mark); - font-size: 48px; - font-weight: 500; - line-height: 0.5; -} -.itc-quote-grid { - display: grid; - gap: 20px; -} -@media (min-width: 768px) { - .itc-quote-grid { - grid-template-columns: repeat(3, 1fr); - } -} -@media (max-width: 767px) { - .itc-testimonials { - padding: 56px 0; - } - .itc-testimonials-title { - white-space: normal; - } -} -.itc-quote-card { - margin: 0; - background: var(--q-card); - border: 1px solid var(--q-card-border); - border-radius: 12px; - padding: 28px; - display: flex; - flex-direction: column; - gap: 18px; -} -.itc-quote-text { - margin: 0; - font-size: 17px; - font-weight: 400; - line-height: 1.5; - letter-spacing: -0.01em; - color: var(--q-text); - text-wrap: pretty; - flex: 1; -} -.itc-quote-attr { - display: flex; - align-items: center; - gap: 13px; -} -.itc-quote-avatar { - display: inline-flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 9999px; - background: var(--q-avatar-bg); - color: var(--q-avatar-fg); - font-size: 14px; - font-weight: 500; - flex: none; - overflow: hidden; -} -.itc-quote-avatar-img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} -.itc-quote-meta { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 1px; - line-height: 1.3; -} -.itc-quote-name { - font-size: 14px; - font-weight: 500; - color: var(--q-text); -} -.itc-quote-handle { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 13px; - color: var(--q-muted); - text-decoration: none; -} -.itc-quote-handle:hover { - color: var(--q-text); -} -.itc-quote-handle:focus-visible { - outline: 2px solid var(--q-text); - outline-offset: 3px; - border-radius: 4px; -} - -/* ── Product grid ── */ -.itc-feature-grid { - display: grid; - gap: 24px; - margin-top: 24px; -} -@media (min-width: 768px) { - .itc-feature-grid { - grid-template-columns: repeat(3, 1fr); - } -} -.itc-feature-card { - display: flex; - flex-direction: column; - padding: 12px; - text-decoration: none; - transition: border-color 0.15s ease; -} -.itc-feature-card:hover { - border-color: var(--ink); -} -.itc-feature-card:focus-visible { - outline: 2px solid var(--ink); - outline-offset: 2px; -} -.itc-feature-card-body { - padding: 16px 8px 8px; -} -.itc-feature-list-grid { - display: grid; - gap: 16px; - margin-top: 28px; -} -@media (min-width: 768px) { - .itc-feature-list-grid { - grid-template-columns: repeat(4, 1fr); - } -} -.itc-feature-list-card { - min-height: 128px; - padding: 20px; - transition: border-color 0.15s ease; -} -.itc-feature-list-card:hover { - border-color: var(--ink); -} -.itc-feature-list-card:focus-visible { - outline: 2px solid var(--ink); - outline-offset: 2px; -} -.itc-feature-more-header { - align-items: center; - display: flex; - justify-content: flex-end; - gap: 16px; - margin-top: 24px; -} - -/* ── MCP (the single orange moment) ── */ -.itc-mcp-section { - background: #eee8de; - border-top: 1px solid var(--hairline); - padding: 84px 0 92px; -} -.itc-hero + .itc-mcp-section { - margin-top: 24px; -} -.itc-mcp-grid { - display: grid; - gap: 48px; - align-items: center; -} -@media (min-width: 980px) { - .itc-mcp-grid { - grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); - gap: 64px; - } -} -@media (max-width: 767px) { - .itc-mcp-section { - padding: 64px 0; - } - .itc-hero + .itc-mcp-section { - margin-top: 28px; - } -} -.itc-terminal { - background: var(--term-bg); - border-radius: 16px; - overflow: hidden; - min-width: 0; -} -.itc-terminal-bar { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - border-bottom: 1px solid #2a2a29; -} -.itc-terminal-dot { - width: 10px; - height: 10px; - border-radius: 9999px; - background: #313130; -} -.itc-terminal-label { - margin-left: 8px; - font-size: 12px; - color: var(--inverse-muted); -} -.itc-terminal pre { - margin: 0; - padding: 20px 20px 24px; - overflow-x: auto; - font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace; - font-size: 13px; - line-height: 1.7; - color: #d6d3ce; -} -.itc-terminal .t-orange { - color: var(--orange); -} -.itc-terminal .t-bright { - color: #ffffff; -} -.itc-terminal .t-dim { - color: var(--inverse-muted); -} -.itc-agent-icons { - display: flex; - flex-wrap: wrap; - gap: 12px; - margin-top: 28px; -} -.itc-agent-icon { - align-items: center; - background: var(--surface-2); - border-radius: 10px; - display: inline-flex; - height: 44px; - justify-content: center; - overflow: hidden; - width: 44px; -} -.itc-agent-icon svg { - height: 28px; - width: 28px; -} - -/* ── Footer ── */ -.itc-footer { - background: #eee8de; - border-top: 1px solid var(--hairline); -} -.itc-sitefooter { - font-size: 14px; - color: var(--ink-muted); -} -.itc-sitefooter a { - color: var(--ink-muted); - text-decoration: none; - transition: color 0.15s ease; -} -.itc-sitefooter a:hover { - color: var(--ink); -} -.itc-newsletter input { - border-color: var(--hairline) !important; - border-radius: 8px !important; -} -.itc-newsletter input:focus { - border-color: var(--ink) !important; - box-shadow: none !important; -} -.itc-newsletter button { - border-radius: 8px !important; - background: var(--ink) !important; -} - -/* ── Floating Discord pill (quiet messenger-style nod) ── */ -.itc-discord { - position: fixed; - right: 20px; - bottom: 20px; - z-index: 40; - display: inline-flex; - align-items: center; - gap: 8px; - background: var(--ink); - color: #ffffff; - border-radius: 9999px; - padding: 12px 18px; - font-size: 14px; - font-weight: 500; - text-decoration: none; - transition: background-color 0.15s ease; -} -.itc-discord:hover { - background: #000000; -} -.itc-discord:focus-visible { - outline: 2px solid var(--ink); - outline-offset: 2px; -} -@media (max-width: 767px) { - .itc-discord span { - display: none; - } - .itc-discord { - padding: 12px; - } -} diff --git a/web/src/components/landing-page.tsx b/web/src/components/landing-page.tsx deleted file mode 100644 index f786bf57c..000000000 --- a/web/src/components/landing-page.tsx +++ /dev/null @@ -1,913 +0,0 @@ -/** - * Canonical marketing landing page. - * - * The page uses a soft cream canvas, restrained hairline surfaces, charcoal - * type, and a single orange emphasis moment around the MCP section. - */ - -import { type ReactNode, type SVGProps } from "react"; -import { NewsletterSignup } from "@/components/newsletter-signup"; -import { ProductHuntLaurel } from "@/components/product-hunt-laurel"; -import { SiteFooter } from "@/components/site-footer"; -import { featurePages } from "@/lib/feature-pages"; -import "./landing-page.css"; - -const SIGNUP_URL = "https://app.openseo.so/sign-up"; -const PRODUCT_HUNT_URL = - "https://www.producthunt.com/products/openseo?launch=openseo"; -const GITHUB_URL = "https://github.com/every-app/open-seo"; -const DISCORD_URL = "https://discord.gg/c9uGs3cFXr"; - -type Testimonial = { - quote: string; - name: string; - initial: string; - handle: string; - href: string; - network: "x" | "linkedin" | "web"; - avatarSrc?: string; -}; - -const TESTIMONIALS: Testimonial[] = [ - { - quote: - "All of the value, none of the bloat. OpenSEO is a no-brainer compared to the expensive alternatives!", - name: "Fed", - initial: "F", - handle: "@foliofed", - href: "https://x.com/foliofed", - network: "x", - avatarSrc: "/avatars/fed-avatar.jpg", - }, - { - quote: - "I've been using OpenSEO for the past 3 months, Ben keeps launching features to make it the best. I use it every day to find where my competitors are ranking.", - name: "Samik", - initial: "S", - handle: "Subclip", - href: "https://www.subclip.app/", - network: "web", - avatarSrc: "/avatars/samik-avatar.jpg", - }, - { - quote: - "It's so straightforward and incredibly easy to get started. OpenSEO gives you the complete setup, stripped of all the fluff that you get elsewhere.", - name: "Tom Raine", - initial: "T", - handle: "LinkedIn", - href: "https://www.linkedin.com/in/tom-raine-hk/", - network: "linkedin", - avatarSrc: "/avatars/tom-avatar.jpeg", - }, -]; - -// ─── Icons (inline SVG only, per project convention) ───────────────── - -type IconProps = { size?: number; className?: string }; - -function strokeProps(size: number, className?: string) { - return { - width: size, - height: size, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - strokeWidth: 2, - strokeLinecap: "round" as const, - strokeLinejoin: "round" as const, - className, - "aria-hidden": true, - }; -} - -function IconArrowRight({ size = 16, className }: IconProps) { - return ( - - - - - ); -} - -function IconGithub({ size = 16, className }: IconProps) { - return ( - - ); -} - -function IconX({ size = 14, className }: IconProps) { - return ( - - ); -} - -function IconLinkedIn({ size = 14, className }: IconProps) { - return ( - - ); -} - -function IconLink({ size = 14, className }: IconProps) { - return ( - - - - - ); -} - -function IconDiscord({ size = 18, className }: IconProps) { - return ( - - ); -} - -// ─── Shared bits ───────────────────────────────────────────────────── - -function Container({ - children, - className = "", -}: { - children: ReactNode; - className?: string; -}) { - return
    {children}
    ; -} - -function ArrowCta({ - href = SIGNUP_URL, - className = "itc-btn itc-btn-primary", - children = "Start enjoying SEO", - size = "md", -}: { - href?: string; - className?: string; - children?: ReactNode; - size?: "md" | "lg"; -}) { - return ( - - {children} - - - ); -} - -// ─── Hero ──────────────────────────────────────────────────────────── - -function Hero() { - return ( -
    - - - - -

    - The modern, open source SEO platform. -

    -

    - Without quality data, AI gives generic advice. OpenSEO is built for - you and your AI agent to work together on SEO strategy + content - tailored to your business. -

    -
    -
    - -

    No credit card required

    -
    -
    -
    -
    - ); -} - -// ─── Testimonial (true-black inverse strip) ────────────────────────── - -function Testimonial() { - return ( -
    - -
    -

    - Trusted by hundreds of customers worldwide -

    -
    - {TESTIMONIALS.map((t) => ( -
    - -
    {t.quote}
    -
    - - - {t.name} - - {t.network === "x" ? ( - - ) : t.network === "linkedin" ? ( - - ) : ( - - )} - {t.handle} - - -
    -
    - ))} -
    -
    -
    -
    - ); -} - -// ─── Product ───────────────────────────────────────────────────────── - -const FEATURE_CARDS = [ - { - page: featurePages.keywordResearch, - blurb: "Find ideas, demand, difficulty, intent, and live SERPs.", - }, - { - page: featurePages.domainOverview, - blurb: "Estimate organic traffic and ranking keywords.", - }, - { - page: featurePages.backlinks, - blurb: "Inspect backlinks, referring domains, and link quality.", - }, - { - page: featurePages.rankTracking, - blurb: "Track keyword positions over time.", - }, - { - page: featurePages.siteAudit, - blurb: "Crawl pages and surface technical issues.", - }, - { - page: featurePages.aiBrandVisibility, - blurb: "Review AI mentions, citations, and prompts.", - }, - { - page: featurePages.aiSearchPrompts, - blurb: "Compare prompts across supported AI models.", - }, - { - page: featurePages.savedKeywords, - blurb: "Organize ideas for content and tracking.", - }, -]; - -function DemoVideo() { - return ( - - ); -} - -function ProductSection() { - return ( -
    - -
    -

    See OpenSEO in action

    -

    - Keyword research, competitor analysis, backlinks, rank tracking, - technical audits, and AI-search visibility, all on real DataForSEO - data and connected to each other. -

    -
    - -
    -
    - -
    -
    - -
    - {FEATURE_CARDS.map(({ page, blurb }) => ( - -

    - {page.eyebrow} -

    -

    - {blurb} -

    -
    - ))} -
    - - -
    -
    - ); -} - -// ─── MCP: the page's one Fin Orange moment ─────────────────────────── - -type McpClient = { - name: string; - Icon: (props: SVGProps) => ReactNode; -}; - -const MCP_CLIENTS: McpClient[] = [ - { name: "Claude", Icon: ClaudeIcon }, - { name: "Codex", Icon: CodexIcon }, - { name: "OpenClaw", Icon: OpenClawIcon }, - { name: "OpenCode", Icon: OpenCodeIcon }, - { name: "Gemini", Icon: GeminiIcon }, -]; - -function McpSection() { - return ( -
    - -
    -
    -

    - Model Context Protocol -

    -

    Get superpowers with the MCP

    -

    - Give your agent real SEO data instead of guesses. It can research - keywords, competitors, backlinks, and Google Search Console - performance, then you can review the work in OpenSEO. -

    -
    - {MCP_CLIENTS.map(({ name, Icon }) => ( - - - ))} -
    - -
    - -
    -
    -
    -
    -              
    -                › find and cluster keywords
    -                for openseo.so
    -                {"\n\n"}
    -                
    -                  ⏺ openseo.keyword_research(seed: "open source seo")
    -                
    -                {"\n"}
    -                {"  "}keyword{"                      "}volume{"     "}kd{"\n"}
    -                {"  "}open source seo{"              "}
    -                1,300
    -                {"      "}
    -                12
    -                {"\n"}
    -                {"  "}open source seo tools{"        "}
    -                720
    -                {"        "}
    -                9
    -                {"\n"}
    -                {"  "}self-hosted seo platform{"     "}
    -                210
    -                {"        "}
    -                4
    -                {"\n\n"}
    -                ✓
    -                
    -                  {" "}
    -                  Saved 3 keywords to your workspace.
    -                
    -                {"\n"}
    -                ↳
    -                 View data in app: 
    -                app.openseo.so/keywords
    -              
    -            
    -
    -
    -
    -
    - ); -} - -function ClaudeIcon(props: SVGProps) { - return ( - - - - ); -} - -function CodexIcon(props: SVGProps) { - return ( - - - - ); -} - -function OpenClawIcon(props: SVGProps) { - return ( - - - - - - - - - - - - - - - - - - ); -} - -function OpenCodeIcon(props: SVGProps) { - return ( - - - - - - ); -} - -function GeminiIcon(props: SVGProps) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -// ─── Open source ───────────────────────────────────────────────────── - -function OpenSourceSection() { - return ( -
    - -
    -

    100% open source

    -

    - People should have the option to self-host and customize their - tools. If you ever hear someone talking about building their own - tool from scratch, tell them to build on top of OpenSEO. -

    -
    - - -
    -
    - ); -} - -// ─── Footer ────────────────────────────────────────────────────────── - -function Footer() { - return ( -
    - -
    -
    -

    - Stay in the loop -

    -

    - Product updates, new features, and the occasional - behind-the-scenes. -

    -
    -
    - -
    -
    - -
    - -
    - -

    - © 2026 Every App, Inc. -

    -
    -
    - ); -} - -// ─── Page ──────────────────────────────────────────────────────────── - -export function LandingPage() { - return ( -
    - - - - - -
    - ); -} diff --git a/web/src/components/legal-page.tsx b/web/src/components/legal-page.tsx deleted file mode 100644 index f695cc45c..000000000 --- a/web/src/components/legal-page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { HomeLayout } from "fumadocs-ui/layouts/home"; -import { DocsBody } from "fumadocs-ui/page"; -import type { ReactNode } from "react"; -import { SiteFooter } from "@/components/site-footer"; -import { baseOptions } from "@/lib/layout.shared"; - -type LegalPageProps = { - title: string; - description?: string; - children: ReactNode; -}; - -export function LegalPage({ title, description, children }: LegalPageProps) { - return ( - -
    -
    -

    - {title} -

    - {description ? ( -

    - {description} -

    - ) : null} -
    - - - {children} - - -
    - -
    -
    -
    - ); -} diff --git a/web/src/components/library-page.tsx b/web/src/components/library-page.tsx deleted file mode 100644 index eae2d082a..000000000 --- a/web/src/components/library-page.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import type { ReactNode } from "react"; -import { DocsBody } from "fumadocs-ui/page"; -import { buildBreadcrumbJsonLd } from "@/lib/seo"; - -const KEYWORD_RESEARCH_LIBRARY = { - name: "Keyword Research", - path: "/library/keyword-research", -}; - -type LibraryRef = { - name: string; - path: string; -}; - -type LibrarySpokePageProps = { - title: string; - description?: string; - crumb: string; - path: string; - library?: LibraryRef; - children: ReactNode; -}; - -export function LibrarySpokePage({ - title, - description, - crumb, - path, - library = KEYWORD_RESEARCH_LIBRARY, - children, -}: LibrarySpokePageProps) { - const breadcrumbLd = buildBreadcrumbJsonLd([ - { name: "Strategy Library", path: "/library" }, - { name: library.name, path: library.path }, - { name: crumb, path }, - ]); - - return ( -
    -
    - -

    - {title} -

    - {description ? ( -

    - {description} -

    - ) : null} -
    - - - {children} - - - - -