website: Add MCP and Skills Docs

This commit is contained in:
Ben Senescu 2026-05-24 18:35:55 -04:00 committed by GitHub
parent 45234e6945
commit 0e75e07660
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1387 additions and 327 deletions

View File

@ -28,7 +28,7 @@ Example:
I can coach you through this. Are you new to SEO, or do you mostly want help using OpenSEO faster? I can coach you through this. Are you new to SEO, or do you mostly want help using OpenSEO faster?
Good starting points: Good starting points:
- Set up the onboarding checklist - Set up SEO project context
- Find keyword opportunities - Find keyword opportunities
- Map keywords to pages - Map keywords to pages
- Study a competitor - Study a competitor
@ -37,7 +37,7 @@ Good starting points:
## What each workflow does ## What each workflow does
- `onboarding-checklist`: sets up the workspace, verifies MCP, captures goals, positioning, and GSC exports. - `seo-project-setup`: sets up the workspace, verifies MCP, captures goals, positioning, and GSC exports.
- `keyword-research`: finds search opportunities from seed topics and evaluates volume, difficulty, CPC, intent, and SERPs. - `keyword-research`: finds search opportunities from seed topics and evaluates volume, difficulty, CPC, intent, and SERPs.
- `keyword-clustering`: groups keywords by intent and maps clusters to existing or proposed pages. - `keyword-clustering`: groups keywords by intent and maps clusters to existing or proposed pages.
- `competitive-landscape`: identifies who wins across a market and what content/backlink patterns are working. - `competitive-landscape`: identifies who wins across a market and what content/backlink patterns are working.
@ -90,11 +90,11 @@ When the user asks for execution:
Offer concise options based on context: Offer concise options based on context:
- "Let's run onboarding first so future work has context." - "Let's set up project context first."
- "Let's research keywords from your seed topics." - "Let's research keywords from your seed topics."
- "Let's cluster your GSC/query export into page targets." - "Let's cluster your GSC/query export into page targets."
- "Let's map the competitive landscape before choosing pages." - "Let's map the competitive landscape before choosing pages."
- "Let's deep-dive one competitor." - "Let's study one competitor."
- "Let's find link prospects for your best linkable asset." - "Let's find link prospects for your best linkable asset."
## Guardrails ## Guardrails

View File

@ -1,13 +1,13 @@
--- ---
name: onboarding-checklist name: seo-project-setup
description: Guide a new OpenSEO user through workspace setup, site goals, positioning context, MCP checks, SEO strategy, and Search Console data intake. description: Set up a durable local SEO workspace with project context, notes, goals, positioning, preferences, MCP checks, and Search Console data intake.
--- ---
# OpenSEO Onboarding Checklist # OpenSEO SEO Project Setup
## Goal ## Goal
Help the user set up a durable SEO workspace and gather enough context for future OpenSEO workflows to be useful. This is an intake and readiness workflow, not a full audit. Help the user set up a local SEO workspace for one website or SEO project. The folder is where the agent saves notes, goals, exports, briefs, reports, preferences, and project context over time. This is a workspace and context setup workflow, not a full audit.
## Tone ## Tone
@ -23,7 +23,7 @@ Suggest that the user choose or create a local folder for SEO work, for example:
- `~/Documents/SEO/<company-or-site>/` - `~/Documents/SEO/<company-or-site>/`
- A repo or workspace folder if SEO work should live beside website/content files - A repo or workspace folder if SEO work should live beside website/content files
Explain that keeping notes, exports, briefs, scraped pages, and reports in one folder helps the agent build context over time. Explain that keeping notes, exports, briefs, scraped pages, reports, and preferences in one folder helps the agent build context over time. Future SEO workflows can use that folder rather than starting from a blank conversation.
Recommended starter structure: Recommended starter structure:
@ -38,7 +38,7 @@ seo-workspace/
reports/ reports/
``` ```
Do not create folders unless the user asks. If file tools are available and the user asks, create a simple structure and a short `README.md` with the current goals and known sites. Do not create folders unless the user asks. If file tools are available and the user asks, create a simple structure and a short `README.md` with the current goals, known sites, and user preferences for how the agent should approach SEO for this project.
### 2. Collect website scope ### 2. Collect website scope
@ -159,5 +159,5 @@ Then summarize:
- Keep setup lightweight. The user should feel oriented, not assigned homework. - Keep setup lightweight. The user should feel oriented, not assigned homework.
- Do not pretend a GSC CSV has been uploaded unless you can see it. - Do not pretend a GSC CSV has been uploaded unless you can see it.
- Keep onboarding focused on setup and context unless the user asks for live research. - Keep project setup focused on setup and context unless the user asks for live research.
- If web search or scraping is used for positioning research, distinguish source evidence from inference. - If web search or scraping is used for positioning research, distinguish source evidence from inference.

View File

@ -13,7 +13,7 @@ Easy to self-host and extend, but we have a managed version too:
- [Why Use This](#why-use-this) - [Why Use This](#why-use-this)
- [Main SEO Workflows](#main-seo-workflows) - [Main SEO Workflows](#main-seo-workflows)
- [OpenSEO MCP](#openseo-mcp) - [OpenSEO MCP](#openseo-mcp)
- [OpenSEO Skills](#openseo-skills) - [OpenSEO Agent Skills](#openseo-agent-skills)
- [Roadmap](#roadmap) - [Roadmap](#roadmap)
- [Community](#community) - [Community](#community)
- [Pricing / Costs (Free + API costs)](#pricing--costs) - [Pricing / Costs (Free + API costs)](#pricing--costs)
@ -88,9 +88,9 @@ claude mcp add --transport http --scope user openseo http://localhost:3001/mcp
Approve the OpenSEO login when your agent asks. Approve the OpenSEO login when your agent asks.
## OpenSEO Skills ## OpenSEO Agent Skills
OpenSEO skills are reusable workflows for Codex and Claude Code. They guide your agent through SEO tasks and can use the OpenSEO MCP for live keyword, SERP, backlink, and domain data. OpenSEO Agent Skills are reusable workflows for Codex and Claude Code. They guide your agent through SEO tasks and can use the OpenSEO MCP for live keyword, SERP, backlink, and domain data.
### Installation Options ### Installation Options
@ -132,11 +132,11 @@ mkdir -p ~/.claude/skills
cp -R open-seo/.agents/skills/* ~/.claude/skills/ cp -R open-seo/.agents/skills/* ~/.claude/skills/
``` ```
Start with `/onboarding-checklist`. It will ask about your project and help configure your workspace. Start with `/seo-project-setup`. It will ask about your project and help configure your workspace.
### Available Skills ### Available Skills
- `onboarding-checklist` - `seo-project-setup`
- `seo-coach` - `seo-coach`
- `keyword-research` - `keyword-research`
- `keyword-clustering` - `keyword-clustering`

View File

@ -7,3 +7,4 @@ dist
source.generated.ts source.generated.ts
pnpm-lock.yaml pnpm-lock.yaml
routeTree.gen.ts routeTree.gen.ts
content/legal/terms-and-conditions.md

87
web/content/docs/mcp.md Normal file
View File

@ -0,0 +1,87 @@
---
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, domain research, backlink overview, saved keywords, and rank tracking.
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 the most current setup UI and a copyable endpoint, open [AI & MCP in OpenSEO](https://app.openseo.so/ai).
## Claude Code
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 Settings -> Connectors.
2. Click Add custom connector.
3. Paste `https://app.openseo.so/mcp`.
4. Approve the OpenSEO login when prompted.
Claude Desktop custom connectors require a Claude plan that supports custom connectors.
## Codex CLI
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.
## 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.
- 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.
## 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 capture your SEO goals and website context in a local workspace.
- 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 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.

View File

@ -0,0 +1,3 @@
{
"pages": ["mcp", "skills"]
}

View File

@ -0,0 +1,53 @@
---
title: "Competitive Landscape Agent Skill"
description: "Run competitive SEO research with your AI agent: map market leaders, content patterns, keyword coverage, backlinks, and gaps."
---
<RunSkillCallout command="/competitive-landscape" />
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.

View File

@ -0,0 +1,55 @@
---
title: "Competitor Analysis Agent Skill"
description: "Analyze one competitor's keywords, pages, backlinks, content themes, and SEO gaps with your AI agent."
---
<RunSkillCallout command="/competitor-analysis" />
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.

View File

@ -0,0 +1,46 @@
---
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 return a recommendation, plan, or shortlist.
## Set up OpenSEO Agent 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): set up a durable project workspace so your agent can reuse goals, context, exports, and preferences across sessions.
- [SEO Coach](/docs/skills/seo-coach): choose the next workflow when you are new to SEO or unsure what to run first.
## 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.
## Promotion workflows
- [Link Prospecting](/docs/skills/link-prospecting): find qualified outreach prospects and the angle that makes each one relevant.
## 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)

View File

@ -0,0 +1,54 @@
---
title: "Keyword Clustering Agent Skill"
description: "Cluster keywords by intent and map them to existing or proposed pages with your AI agent."
---
<RunSkillCallout command="/keyword-clustering" />
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.

View File

@ -0,0 +1,54 @@
---
title: "Keyword Research Agent Skill"
description: "Find keyword opportunities from live SEO data with Claude Code, Codex, or another AI agent."
---
<RunSkillCallout command="/keyword-research" />
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.

View File

@ -0,0 +1,55 @@
---
title: "Link Prospecting Agent Skill"
description: "Find relevant link prospects from SERPs, competitor backlinks, and topical publishers with your AI agent."
---
<RunSkillCallout command="/link-prospecting" />
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.

View File

@ -0,0 +1,14 @@
{
"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)",
"[Keyword Research](/docs/skills/keyword-research)",
"[Keyword Clustering](/docs/skills/keyword-clustering)",
"[Competitive Landscape](/docs/skills/competitive-landscape)",
"[Competitor Analysis](/docs/skills/competitor-analysis)",
"[Link Prospecting](/docs/skills/link-prospecting)"
]
}

View File

@ -0,0 +1,58 @@
---
title: "SEO Coach Agent Skill"
description: "Learn SEO basics, choose the right workflow, and decide what to do next with your AI agent."
---
<RunSkillCallout command="/seo-coach" />
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, 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.
- [Keyword Research](/docs/skills/keyword-research) when you are ready to find opportunities.
- [Competitive Landscape](/docs/skills/competitive-landscape) when the market is unclear.

View File

@ -0,0 +1,55 @@
---
title: "SEO Project Setup Agent Skill"
description: "Create a local SEO workspace where your AI agent can save project context, notes, goals, exports, and preferences over time."
---
<RunSkillCallout command="/seo-project-setup" />
The SEO Project Setup Agent Skill gives your agent the context it needs before doing SEO work for a site.
Your agent organizes the website, goals, competitors, files, and preferences once. It creates a working folder for notes, exports, briefs, and project context.
That workspace carries context across sessions, so future keyword research, clustering, competitor analysis, and content planning are more specific to your site.
## What this skill helps your agent do
- Pick or create a working folder for one website or SEO project.
- Save the basics: domain, goals, audience, positioning, competitors, and target markets.
- Organize exports and notes so they are easy to reuse in later sessions.
- Capture how you want the agent to approach SEO for this project.
- 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.
It helps when SEO work will span multiple sessions. Keep project context in one folder and let the agent build on it.
## What you get back
Your agent should help set up a simple workspace structure and a short project summary. The summary should explain what site is in scope, what goals matter, what files or exports are available, and what workflow should run next.
## How to get the best result
- Run the skill from the folder where you want SEO work to live.
- 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.
- Add Google Search Console exports when available.
- Tell the agent any preferences you want it to remember for future SEO work.
## Use it with OpenSEO MCP
Set up [OpenSEO MCP](/docs/mcp) so your agent can confirm the right OpenSEO project and use live SEO data in later workflows.
## 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.
- [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.

View File

@ -0,0 +1,84 @@
---
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.
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/.agents/skills/* ~/.codex/skills/
# Claude Code
mkdir -p ~/.claude/skills
cp -R open-seo/.agents/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`.
## 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`
## 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.

View File

@ -7,7 +7,9 @@ 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. 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 is something that compounds over time as your brand builds its reputation and it can be a great channel for potential customers to find you. But SEO tools can also be your secret weapon for market research before you even have a website. 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 ## Table of Contents
@ -22,7 +24,7 @@ SEO is something that compounds over time as your brand builds its reputation an
### Improving your site's rankings ### 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 actually finds what they were looking for. 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: The major ways to improve SEO are still pretty simple:
@ -34,13 +36,13 @@ Lots of guides and influencers will try to sell you shortcuts for SEO. For new c
### Market Research ### Market Research
In 2026, when founders are researching a new market, they're probably reaching for Claude or ChatGPT to do "Deep Research." These tools are useful. They search the web, read through Reddit conversations, and pull together industry reports and blog posts much faster than most of us would manually. 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 kind of research is great for qualitative context. It helps you understand how people talk, what themes come up repeatedly, and which companies are getting attention. But SEO tools can give you a different kind of signal. They can help you cut through claims and marketing copy and get a better sense of how customers in the space are actually behaving. 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.
In the [Market Research Techniques](#market-research-techniques) section, we'll go into how you can use SEO tools to answer questions like: SEO tools can help answer questions like:
- Which competitors in the space are really the most popular and trusted? - Which competitors in the space are the most popular and trusted?
- What are people searching when Google recommends my competitors? - What are people searching when Google recommends my competitors?
- Which of my competitors' pages drive the most traffic? - 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 blog posts or pages should you add to your website to help customers and drive traffic?
@ -48,44 +50,49 @@ In the [Market Research Techniques](#market-research-techniques) section, we'll
## Market Research Techniques ## Market Research Techniques
To make this more concrete, I'll reference a few OpenSEO screenshots below so you can see what this kind of data actually looks like in practice. The screenshots below show what this data looks like in OpenSEO.
### Competitor Research ### Competitor Research
This is one of the more useful ways founders can use SEO data. 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. 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 own strategy in a very practical way. If a competitor is getting a lot of traction from a specific kind of page, that may be a sign that buyers care about that topic more than you realized. It can also help you spot use cases, niches, or workflows that fit your product but are not yet obvious from talking to a few customers. 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](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/85c0c3bf-fe49-4e18-998b-5a5017304100/public) ![OpenSEO domain overview showing top pages, keywords, and estimated traffic](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/85c0c3bf-fe49-4e18-998b-5a5017304100/public)
### Keyword Research ### Keyword Research
Keyword research lets you see how much search volume exists for certain terms, explore related keywords, and get a feel for what kinds of searches are informational versus commercial. That matters because not every search is equally valuable. Some simple answer-style searches may send fewer clicks because Google can answer them right on the results page with an AI answer. Searches with comparison, workflow, or buying intent are often more useful because people still want detail, options, and proof.
It is also a good way to map a market. If you search a seed phrase like "best crm for," the suggested and related keywords can quickly show you which audiences are worth paying attention to: agencies, startups, consultants, real estate teams, and so on. 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](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/f8739457-4cc0-4f99-8c7a-7c74b35f0d00/public) ![OpenSEO keyword research view showing related or suggested keywords for a seed term](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/f8739457-4cc0-4f99-8c7a-7c74b35f0d00/public)
### Backlink Analysis ### Backlink Analysis
Backlink analysis lets you see which websites link to a specific website. 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 actually matter in the market. You can also get a feel for why certain companies are seen as credible. Sometimes they are earning attention through PR. Sometimes it is partnerships. Sometimes it is original research or a genuinely useful tool that people keep citing. 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.
That kind of visibility can tell you a lot about the structure of a market and where attention is really coming from. Those links can tell you how attention moves through a market.
![OpenSEO backlink analysis showing referring domains or top backlinks for a competitor](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/d9a2a5ea-6aa4-469e-10b5-e413febe4200/public) ![OpenSEO backlink analysis showing referring domains or top backlinks for a competitor](https://imagedelivery.net/ysLOa6bzFaM49Jxok-TAlw/d9a2a5ea-6aa4-469e-10b5-e413febe4200/public)
## How can you improve your site's rankings? ## How can you improve your site's rankings?
You do not need to become an SEO expert to make progress here. For most founders, it is enough to cover the basics well and avoid obvious mistakes. 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 ### Site Title & Description
Your page title and meta description are the billboard Google shows searchers. Your page title and meta description are the billboard Google shows searchers.
They play a big role in whether someone clicks. A good title usually matches the search clearly, 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 exact need. 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
@ -101,7 +108,7 @@ If you're new to it, Google's docs are worth reading:
### Site Audits ### Site Audits
Structuring your website optimally can get very deep, and there is a whole specialty built around it called "Technical SEO". If you're just getting started, you probably shouldn't obsess over perfect audit scores. 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: The main things to care about are:
@ -111,46 +118,49 @@ The main things to care about are:
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. 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, using the SEO tooling built into your CMS or hosting provider and fixing the major problems is usually good enough. If you are technical, running an audit and resolving the highest-priority crawl, indexation, broken link, or performance issues is a worthwhile way to spend an hour or two since your coding agent can likely do the implementation. 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 ### 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. 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 real shortcut around this. The page has to be useful. 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. 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, though: 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 else already ranking, it probably will not. 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
Backlinks are when other companies link to your website. They signal your website's authority on the topic that the linking page ranks for. 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 out there for backlink exchanges and paid placements. Some of those tactics may move a metric here or there, but they aren't going to get your business over the hump. 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 genuinely useful piece of research that people cite. 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 usually to give people a real reason to talk about you. That might mean publishing a strong customer story, creating original data, building a unique free tool, or getting your company listed in the places your market already trusts. 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 ## A simple founder SEO priority list
If you're still not sure who your customer is yet, trying to improve your ranking isn't worth it yet. Instead use SEO tools for research. 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: 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 1. Make sure the product, positioning, and site messaging are clear
2. Set up [Google Search Console](https://search.google.com/search-console/about) 2. Run [SEO project setup](/docs/skills/seo-project-setup) so goals, scope, and data sources are clear
3. Fix major crawl, indexation, and performance problems 3. Set up [Google Search Console](https://search.google.com/search-console/about) and [OpenSEO MCP](/docs/mcp)
4. Identify the core searches that map to your product and customer pain points 4. Fix major crawl, indexation, and performance problems
5. Build or improve the pages that deserve to rank 5. Identify the core searches that map to your product and customer pain points
6. Use competitor and keyword research to find obvious gaps 6. Build or improve the pages that deserve to rank
7. Earn mentions and links by doing things worth citing 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 ## Final thought
SEO is worth understanding as a founder because it helps you see what the market is asking for, what Google believes users want, and whether your company is showing up as a credible answer. 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 can become both a research tool and a compounding distribution channel. The core idea is still pretty simple: understand the customer, make genuinely useful pages, keep your site technically healthy, and give people a reason to talk about your company. 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. 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.

View File

@ -1,3 +1,3 @@
minimumReleaseAge: 11520 minimumReleaseAge: 11520
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- '@every-app/*' - "@every-app/*"

View File

@ -2,6 +2,7 @@ import {
defineConfig, defineConfig,
defineCollections, defineCollections,
frontmatterSchema, frontmatterSchema,
metaSchema,
} from "fumadocs-mdx/config/zod-3"; } from "fumadocs-mdx/config/zod-3";
import { z } from "zod"; import { z } from "zod";
@ -16,6 +17,18 @@ export const guide = defineCollections({
}), }),
}); });
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({ export const legal = defineCollections({
type: "doc", type: "doc",
dir: "content/legal", dir: "content/legal",

View File

@ -0,0 +1,62 @@
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 (
<div className="max-w-3xl mx-auto px-6 py-12 md:py-24">
<p className="text-sm font-medium text-fd-muted-foreground">{eyebrow}</p>
<h1 className="mt-3 text-4xl font-bold tracking-tight text-fd-foreground md:text-5xl">
{title}
</h1>
<p className="mt-4 max-w-2xl text-lg leading-8 text-fd-muted-foreground">
{description}
</p>
{items.length === 0 ? (
<p className="mt-10 text-fd-muted-foreground">{emptyLabel}</p>
) : (
<div className="mt-10 divide-y divide-fd-border border-y border-fd-border">
{items.map((item) => (
<Link
key={item.url}
to={route}
params={{ _splat: item.slugs.join("/") }}
className="group block py-6"
>
<h2 className="text-xl font-semibold text-fd-foreground transition-colors group-hover:text-fd-primary">
{item.title}
</h2>
{item.description ? (
<p className="mt-2 text-sm leading-6 text-fd-muted-foreground">
{item.description}
</p>
) : null}
</Link>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,55 @@
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";
type ContentPostProps = {
backLabel: string;
backTo: "/docs";
title: string;
description?: string;
Content: React.ComponentType;
};
export const mdxComponents = {
...defaultMdxComponents,
RunSkillCallout,
};
export function ContentPost({
backLabel,
backTo,
title,
description,
Content,
}: ContentPostProps) {
return (
<article className="mx-auto w-full min-w-0 max-w-3xl px-6 py-12 text-fd-foreground md:py-24">
<header className="mb-8">
<div className="mb-4">
<Link
to={backTo}
className="inline-flex items-center gap-2 text-sm font-medium text-fd-muted-foreground transition-colors hover:text-fd-primary"
>
<span aria-hidden="true">&larr;</span>
<span>{backLabel}</span>
</Link>
</div>
<h1 className="mb-4 break-words text-4xl font-bold text-fd-foreground md:text-5xl">
{title}
</h1>
{description ? (
<p className="max-w-2xl text-lg leading-8 text-fd-muted-foreground md:text-xl">
{description}
</p>
) : null}
</header>
<Suspense>
<DocsBody className="min-w-0 text-fd-foreground [&_figure]:max-w-full [&_h2]:text-fd-foreground [&_h3]:text-fd-foreground [&_li]:text-fd-foreground/90 [&_p]:text-fd-foreground/90 [&_pre]:!min-w-0 [&_pre]:!w-full [&_strong]:text-fd-foreground">
<Content />
</DocsBody>
</Suspense>
</article>
);
}

View File

@ -133,9 +133,7 @@ export function FeaturePageTemplate({ page }: FeaturePageProps) {
</section> </section>
<section className="mt-12 rounded-lg border border-neutral-200 bg-neutral-50 p-5"> <section className="mt-12 rounded-lg border border-neutral-200 bg-neutral-50 p-5">
<h2 className="text-lg font-semibold text-neutral-900"> <h2 className="text-lg font-semibold text-neutral-900">Try OpenSEO</h2>
Try OpenSEO
</h2>
<p className="mt-2 text-sm leading-relaxed text-neutral-600"> <p className="mt-2 text-sm leading-relaxed text-neutral-600">
The open source alternative to bloated, expensive, legacy SEO tools. The open source alternative to bloated, expensive, legacy SEO tools.
</p> </p>

View File

@ -0,0 +1,26 @@
type RunSkillCalloutProps = {
command: string;
};
export function RunSkillCallout({ command }: RunSkillCalloutProps) {
const slashCommand = command.startsWith("/") ? command : `/${command}`;
return (
<div className="my-6 overflow-hidden rounded-lg border border-fd-border bg-fd-card">
<div className="flex items-center gap-1.5 border-b border-fd-border px-4 py-2">
<span className="size-2 rounded-full bg-fd-muted-foreground/30" />
<span className="size-2 rounded-full bg-fd-muted-foreground/30" />
<span className="size-2 rounded-full bg-fd-muted-foreground/30" />
<span className="ml-2 text-xs font-medium text-fd-muted-foreground">
Run this skill in your agent
</span>
</div>
<div className="flex items-center gap-3 p-4">
<span className="font-mono text-sm text-fd-muted-foreground">&gt;</span>
<code className="font-mono text-base font-semibold text-fd-foreground">
{slashCommand}
</code>
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
import { notFound } from "@tanstack/react-router"; import { notFound } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start"; import { createServerFn } from "@tanstack/react-start";
import { guideSource } from "@/lib/source"; import { docsSource, guideSource } from "@/lib/source";
export const getGuidePost = createServerFn({ method: "GET" }) export const getGuidePost = createServerFn({ method: "GET" })
.inputValidator((slugs: string[]) => slugs) .inputValidator((slugs: string[]) => slugs)
@ -27,3 +27,54 @@ export const getGuidePosts = createServerFn({ method: "GET" }).handler(
})); }));
}, },
); );
function getContentPost(source: typeof docsSource, slugs: string[]) {
const page = source.getPage(slugs);
if (!page) throw notFound();
return {
path: page.path,
title: page.data.title,
description: page.data.description,
url: page.url,
};
}
function getContentPosts(source: typeof docsSource) {
const topLevelOrder = new Map([
["mcp", 0],
["skills", 1],
]);
const pages = source.getPages();
return pages
.map((page: (typeof pages)[number]) => ({
title: page.data.title,
description: page.data.description,
url: page.url,
slugs: page.slugs,
}))
.sort((a, b) => {
const depth = a.slugs.length - b.slugs.length;
if (depth !== 0) return depth;
const order =
(topLevelOrder.get(a.slugs[0] ?? "") ?? Number.MAX_SAFE_INTEGER) -
(topLevelOrder.get(b.slugs[0] ?? "") ?? Number.MAX_SAFE_INTEGER);
if (order !== 0) return order;
return a.title.localeCompare(b.title);
});
}
export const getDocsPost = createServerFn({ method: "GET" })
.inputValidator((slugs: string[]) => slugs)
.handler(async ({ data: slugs }) => getContentPost(docsSource, slugs));
export const getDocsPosts = createServerFn({ method: "GET" }).handler(
async () => getContentPosts(docsSource),
);
export const getDocsPageTree = createServerFn({ method: "GET" }).handler(
async () => docsSource.getPageTree() as any,
);

View File

@ -84,9 +84,12 @@ export const featurePages = {
"MCP access so AI agents can research and save keywords for you.", "MCP access so AI agents can research and save keywords for you.",
], ],
related: [ related: [
{ label: "Keyword Clustering", href: "/docs/skills/keyword-clustering" },
{
label: "Keyword Research",
href: "/docs/skills/keyword-research",
},
{ label: "Rank Tracking", href: "/features/rank-tracking" }, { label: "Rank Tracking", href: "/features/rank-tracking" },
{ label: "Saved Keywords", href: "/features/saved-keywords" },
{ label: "OpenSEO MCP", href: "/features/mcp" },
], ],
faqs: [ faqs: [
{ {
@ -230,8 +233,11 @@ export const featurePages = {
"MCP support lets an AI agent pull backlink context during SEO research.", "MCP support lets an AI agent pull backlink context during SEO research.",
], ],
related: [ related: [
{
label: "Link Prospecting",
href: "/docs/skills/link-prospecting",
},
{ label: "Domain Overview", href: "/features/domain-overview" }, { label: "Domain Overview", href: "/features/domain-overview" },
{ label: "Site Audit", href: "/features/site-audit" },
{ label: "OpenSEO MCP", href: "/features/mcp" }, { label: "OpenSEO MCP", href: "/features/mcp" },
], ],
faqs: [ faqs: [
@ -303,9 +309,12 @@ export const featurePages = {
"Open-source and self-hostable for teams that want control over their SEO stack.", "Open-source and self-hostable for teams that want control over their SEO stack.",
], ],
related: [ related: [
{
label: "Competitor Analysis",
href: "/docs/skills/competitor-analysis",
},
{ label: "Keyword Research", href: "/features/keyword-research" }, { label: "Keyword Research", href: "/features/keyword-research" },
{ label: "Backlink Checker", href: "/features/backlink-checker" }, { label: "Backlink Checker", href: "/features/backlink-checker" },
{ label: "Rank Tracking", href: "/features/rank-tracking" },
], ],
faqs: [ faqs: [
{ {
@ -376,9 +385,12 @@ export const featurePages = {
"OpenSEO can expose ranking data to AI agents through MCP.", "OpenSEO can expose ranking data to AI agents through MCP.",
], ],
related: [ related: [
{ label: "Keyword Clustering", href: "/docs/skills/keyword-clustering" },
{
label: "Competitor Analysis",
href: "/docs/skills/competitor-analysis",
},
{ label: "Keyword Research", href: "/features/keyword-research" }, { label: "Keyword Research", href: "/features/keyword-research" },
{ label: "Saved Keywords", href: "/features/saved-keywords" },
{ label: "OpenSEO MCP", href: "/features/mcp" },
], ],
faqs: [ faqs: [
{ {

View File

@ -10,8 +10,25 @@ export function baseOptions(): BaseLayoutProps {
}, },
links: [ links: [
{ {
text: "Guides", text: "Resources",
url: "/guides", url: "/guides",
items: [
{
text: "Guides",
description: "Founder-focused SEO articles.",
url: "/guides",
},
{
text: "MCP",
description: "Connect OpenSEO to AI clients.",
url: "/docs/mcp",
},
{
text: "Skills",
description: "Focused OpenSEO workflows.",
url: "/docs/skills",
},
],
}, },
{ {
text: "GitHub", text: "GitHub",

View File

@ -2,7 +2,7 @@ import { loader } from "fumadocs-core/source";
// Keep this server runtime import explicit. In this app's SSR build, the // Keep this server runtime import explicit. In this app's SSR build, the
// public Vite runtime entry resolves to the browser variant for sourceAsync. // public Vite runtime entry resolves to the browser variant for sourceAsync.
import { fromConfig } from "../../node_modules/fumadocs-mdx/dist/runtime/vite/server.js"; import { fromConfig } from "../../node_modules/fumadocs-mdx/dist/runtime/vite/server.js";
import { guide } from "../../source.generated"; import { docs, docsMeta, guide } from "../../source.generated";
import type * as Config from "../../source.config"; import type * as Config from "../../source.config";
const serverCreate = fromConfig<typeof Config>(); const serverCreate = fromConfig<typeof Config>();
@ -11,3 +11,22 @@ export const guideSource = loader({
source: await serverCreate.sourceAsync(guide, {} as Record<string, never>), source: await serverCreate.sourceAsync(guide, {} as Record<string, never>),
baseUrl: "/guides", baseUrl: "/guides",
}); });
export const docsSource = loader({
source: await serverCreate.sourceAsync(docs, docsMeta),
baseUrl: "/docs",
plugins: [
{
transformPageTree: {
folder(node, folderPath) {
if (folderPath !== "skills") return node;
return {
...node,
index: undefined,
};
},
},
},
],
});

View File

@ -13,9 +13,11 @@ import { Route as TermsAndConditionsRouteImport } from './routes/terms-and-condi
import { Route as PrivacyRouteImport } from './routes/privacy' import { Route as PrivacyRouteImport } from './routes/privacy'
import { Route as MarketingRouteImport } from './routes/_marketing' import { Route as MarketingRouteImport } from './routes/_marketing'
import { Route as GuidesIndexRouteImport } from './routes/guides/index' import { Route as GuidesIndexRouteImport } from './routes/guides/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as MarketingIndexRouteImport } from './routes/_marketing/index' import { Route as MarketingIndexRouteImport } from './routes/_marketing/index'
import { Route as JsScriptDotjsRouteImport } from './routes/js/script[.]js' import { Route as JsScriptDotjsRouteImport } from './routes/js/script[.]js'
import { Route as GuidesSplatRouteImport } from './routes/guides/$' import { Route as GuidesSplatRouteImport } from './routes/guides/$'
import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as ApiSubscribeRouteImport } from './routes/api/subscribe' import { Route as ApiSubscribeRouteImport } from './routes/api/subscribe'
import { Route as ApiEventRouteImport } from './routes/api/event' import { Route as ApiEventRouteImport } from './routes/api/event'
import { Route as MarketingPricingRouteImport } from './routes/_marketing/pricing' import { Route as MarketingPricingRouteImport } from './routes/_marketing/pricing'
@ -49,6 +51,11 @@ const GuidesIndexRoute = GuidesIndexRouteImport.update({
path: '/guides/', path: '/guides/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const DocsIndexRoute = DocsIndexRouteImport.update({
id: '/docs/',
path: '/docs/',
getParentRoute: () => rootRouteImport,
} as any)
const MarketingIndexRoute = MarketingIndexRouteImport.update({ const MarketingIndexRoute = MarketingIndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@ -64,6 +71,11 @@ const GuidesSplatRoute = GuidesSplatRouteImport.update({
path: '/guides/$', path: '/guides/$',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const DocsSplatRoute = DocsSplatRouteImport.update({
id: '/docs/$',
path: '/docs/$',
getParentRoute: () => rootRouteImport,
} as any)
const ApiSubscribeRoute = ApiSubscribeRouteImport.update({ const ApiSubscribeRoute = ApiSubscribeRouteImport.update({
id: '/api/subscribe', id: '/api/subscribe',
path: '/api/subscribe', path: '/api/subscribe',
@ -145,8 +157,10 @@ export interface FileRoutesByFullPath {
'/pricing': typeof MarketingPricingRoute '/pricing': typeof MarketingPricingRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
'/docs/$': typeof DocsSplatRoute
'/guides/$': typeof GuidesSplatRoute '/guides/$': typeof GuidesSplatRoute
'/js/script.js': typeof JsScriptDotjsRoute '/js/script.js': typeof JsScriptDotjsRoute
'/docs/': typeof DocsIndexRoute
'/guides/': typeof GuidesIndexRoute '/guides/': typeof GuidesIndexRoute
'/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute '/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute
'/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute '/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute
@ -165,9 +179,11 @@ export interface FileRoutesByTo {
'/pricing': typeof MarketingPricingRoute '/pricing': typeof MarketingPricingRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
'/docs/$': typeof DocsSplatRoute
'/guides/$': typeof GuidesSplatRoute '/guides/$': typeof GuidesSplatRoute
'/js/script.js': typeof JsScriptDotjsRoute '/js/script.js': typeof JsScriptDotjsRoute
'/': typeof MarketingIndexRoute '/': typeof MarketingIndexRoute
'/docs': typeof DocsIndexRoute
'/guides': typeof GuidesIndexRoute '/guides': typeof GuidesIndexRoute
'/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute '/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute
'/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute '/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute
@ -188,9 +204,11 @@ export interface FileRoutesById {
'/_marketing/pricing': typeof MarketingPricingRoute '/_marketing/pricing': typeof MarketingPricingRoute
'/api/event': typeof ApiEventRoute '/api/event': typeof ApiEventRoute
'/api/subscribe': typeof ApiSubscribeRoute '/api/subscribe': typeof ApiSubscribeRoute
'/docs/$': typeof DocsSplatRoute
'/guides/$': typeof GuidesSplatRoute '/guides/$': typeof GuidesSplatRoute
'/js/script.js': typeof JsScriptDotjsRoute '/js/script.js': typeof JsScriptDotjsRoute
'/_marketing/': typeof MarketingIndexRoute '/_marketing/': typeof MarketingIndexRoute
'/docs/': typeof DocsIndexRoute
'/guides/': typeof GuidesIndexRoute '/guides/': typeof GuidesIndexRoute
'/_marketing/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute '/_marketing/features/ai-brand-visibility': typeof MarketingFeaturesAiBrandVisibilityRoute
'/_marketing/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute '/_marketing/features/ai-search-prompts': typeof MarketingFeaturesAiSearchPromptsRoute
@ -212,8 +230,10 @@ export interface FileRouteTypes {
| '/pricing' | '/pricing'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
| '/docs/$'
| '/guides/$' | '/guides/$'
| '/js/script.js' | '/js/script.js'
| '/docs/'
| '/guides/' | '/guides/'
| '/features/ai-brand-visibility' | '/features/ai-brand-visibility'
| '/features/ai-search-prompts' | '/features/ai-search-prompts'
@ -232,9 +252,11 @@ export interface FileRouteTypes {
| '/pricing' | '/pricing'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
| '/docs/$'
| '/guides/$' | '/guides/$'
| '/js/script.js' | '/js/script.js'
| '/' | '/'
| '/docs'
| '/guides' | '/guides'
| '/features/ai-brand-visibility' | '/features/ai-brand-visibility'
| '/features/ai-search-prompts' | '/features/ai-search-prompts'
@ -254,9 +276,11 @@ export interface FileRouteTypes {
| '/_marketing/pricing' | '/_marketing/pricing'
| '/api/event' | '/api/event'
| '/api/subscribe' | '/api/subscribe'
| '/docs/$'
| '/guides/$' | '/guides/$'
| '/js/script.js' | '/js/script.js'
| '/_marketing/' | '/_marketing/'
| '/docs/'
| '/guides/' | '/guides/'
| '/_marketing/features/ai-brand-visibility' | '/_marketing/features/ai-brand-visibility'
| '/_marketing/features/ai-search-prompts' | '/_marketing/features/ai-search-prompts'
@ -276,8 +300,10 @@ export interface RootRouteChildren {
TermsAndConditionsRoute: typeof TermsAndConditionsRoute TermsAndConditionsRoute: typeof TermsAndConditionsRoute
ApiEventRoute: typeof ApiEventRoute ApiEventRoute: typeof ApiEventRoute
ApiSubscribeRoute: typeof ApiSubscribeRoute ApiSubscribeRoute: typeof ApiSubscribeRoute
DocsSplatRoute: typeof DocsSplatRoute
GuidesSplatRoute: typeof GuidesSplatRoute GuidesSplatRoute: typeof GuidesSplatRoute
JsScriptDotjsRoute: typeof JsScriptDotjsRoute JsScriptDotjsRoute: typeof JsScriptDotjsRoute
DocsIndexRoute: typeof DocsIndexRoute
GuidesIndexRoute: typeof GuidesIndexRoute GuidesIndexRoute: typeof GuidesIndexRoute
} }
@ -311,6 +337,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof GuidesIndexRouteImport preLoaderRoute: typeof GuidesIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/docs/': {
id: '/docs/'
path: '/docs'
fullPath: '/docs/'
preLoaderRoute: typeof DocsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_marketing/': { '/_marketing/': {
id: '/_marketing/' id: '/_marketing/'
path: '/' path: '/'
@ -332,6 +365,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof GuidesSplatRouteImport preLoaderRoute: typeof GuidesSplatRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/docs/$': {
id: '/docs/$'
path: '/docs/$'
fullPath: '/docs/$'
preLoaderRoute: typeof DocsSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/api/subscribe': { '/api/subscribe': {
id: '/api/subscribe' id: '/api/subscribe'
path: '/api/subscribe' path: '/api/subscribe'
@ -467,8 +507,10 @@ const rootRouteChildren: RootRouteChildren = {
TermsAndConditionsRoute: TermsAndConditionsRoute, TermsAndConditionsRoute: TermsAndConditionsRoute,
ApiEventRoute: ApiEventRoute, ApiEventRoute: ApiEventRoute,
ApiSubscribeRoute: ApiSubscribeRoute, ApiSubscribeRoute: ApiSubscribeRoute,
DocsSplatRoute: DocsSplatRoute,
GuidesSplatRoute: GuidesSplatRoute, GuidesSplatRoute: GuidesSplatRoute,
JsScriptDotjsRoute: JsScriptDotjsRoute, JsScriptDotjsRoute: JsScriptDotjsRoute,
DocsIndexRoute: DocsIndexRoute,
GuidesIndexRoute: GuidesIndexRoute, GuidesIndexRoute: GuidesIndexRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@ -66,7 +66,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
/> />
</head> </head>
<body className="flex flex-col min-h-screen bg-fd-background text-fd-foreground"> <body className="flex flex-col min-h-screen bg-fd-background text-fd-foreground">
<RootProvider>{children}</RootProvider> <RootProvider search={{ enabled: false }}>{children}</RootProvider>
<Scripts /> <Scripts />
</body> </body>
</html> </html>

View File

@ -3,6 +3,35 @@ import { useState } from "react";
import { SiteFooter } from "@/components/site-footer"; import { SiteFooter } from "@/components/site-footer";
import { featureGroups } from "@/lib/feature-pages"; import { featureGroups } from "@/lib/feature-pages";
const githubStarCount = "2.1k";
const mobileNavItems = [
{
label: "Product",
links: [
{ label: "Features", href: "/features" },
{ label: "Pricing", href: "/pricing" },
],
},
{
label: "Resources",
links: [
{ label: "Guides", href: "/guides" },
{ label: "MCP Setup", href: "/docs/mcp" },
{ label: "Skills", href: "/docs/skills" },
],
},
{
label: "Community",
links: [
{
label: `GitHub ${githubStarCount}`,
href: "https://github.com/every-app/open-seo",
},
],
},
];
function GitHubIcon({ size = 18 }: { size?: number }) { function GitHubIcon({ size = 18 }: { size?: number }) {
return ( return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"> <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
@ -11,11 +40,50 @@ function GitHubIcon({ size = 18 }: { size?: number }) {
); );
} }
function MenuIcon({ size = 28 }: { size?: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
aria-hidden="true"
>
<path d="M4 7h16" />
<path d="M4 12h16" />
<path d="M4 17h16" />
</svg>
);
}
function CloseIcon({ size = 28 }: { size?: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
aria-hidden="true"
>
<path d="M6 6l12 12" />
<path d="M18 6L6 18" />
</svg>
);
}
export const Route = createFileRoute("/_marketing")({ export const Route = createFileRoute("/_marketing")({
component: MarketingLayout, component: MarketingLayout,
}); });
function MarketingLayout() { function MarketingLayout() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return ( return (
<main <main
className="bg-white text-neutral-900 min-h-screen" className="bg-white text-neutral-900 min-h-screen"
@ -23,40 +91,103 @@ function MarketingLayout() {
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
}} }}
> >
<div className="max-w-3xl mx-auto px-6 py-16 md:py-24"> <div className="mx-auto w-full max-w-6xl px-4 pt-6 sm:px-6 md:pt-8">
<nav className="flex items-center justify-between gap-4 mb-20"> <div className="relative mx-auto max-w-5xl">
<nav className="grid min-h-14 grid-cols-[1fr_auto] items-center gap-3 rounded-full border border-neutral-200 bg-white/95 px-4 py-2.5 shadow-sm shadow-neutral-900/5 md:grid-cols-[1fr_auto_1fr] md:px-5">
<Link <Link
to="/" to="/"
className="text-sm font-semibold hover:opacity-80 transition-opacity" className="text-sm font-semibold hover:opacity-80 transition-opacity"
> >
OpenSEO OpenSEO
</Link> </Link>
<div className="flex items-center gap-4 sm:gap-5">
<div className="hidden items-center justify-center gap-5 md:flex">
<FeatureDropdown /> <FeatureDropdown />
<ResourcesDropdown />
<Link <Link
to="/pricing" to="/pricing"
className="text-sm text-neutral-600 hover:text-neutral-900 transition-colors" className="text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900"
> >
Pricing Pricing
</Link> </Link>
</div>
<div className="flex items-center justify-end gap-2 sm:gap-3">
<button
type="button"
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
aria-expanded={mobileMenuOpen}
onClick={() => setMobileMenuOpen((open) => !open)}
className="inline-flex h-9 w-9 items-center justify-center rounded-full text-neutral-900 transition-colors hover:bg-neutral-100 md:hidden"
>
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
</button>
<a <a
href="https://github.com/every-app/open-seo" href="https://github.com/every-app/open-seo"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="GitHub" aria-label={`GitHub, ${githubStarCount} stars`}
className="inline-flex items-center gap-1.5 text-sm text-neutral-600 hover:text-neutral-900 transition-colors" className="hidden h-9 items-center gap-1.5 px-2 text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:inline-flex"
> >
<GitHubIcon size={16} /> <GitHubIcon size={16} />
<span className="hidden sm:inline">GitHub</span> <span>GitHub</span>
<span className="text-neutral-500">{githubStarCount}</span>
</a> </a>
<a <a
href="https://app.openseo.so/sign-in" href="https://app.openseo.so/sign-in"
className="hidden sm:inline-flex h-8 px-3.5 text-sm font-medium border border-neutral-300 text-neutral-900 rounded-md hover:border-neutral-900 transition-colors items-center" className="hidden h-9 items-center rounded-full border border-neutral-300 px-4 text-sm font-medium text-neutral-900 transition-colors hover:border-neutral-900 md:inline-flex"
> >
Sign in Sign in
</a> </a>
</div> </div>
</nav> </nav>
{mobileMenuOpen ? (
<div className="absolute left-0 right-0 top-full z-30 mt-3 rounded-2xl border border-neutral-200 bg-white p-3 shadow-xl shadow-neutral-900/10 md:hidden">
<div className="grid grid-cols-2 gap-2">
<a
href="https://app.openseo.so/sign-in"
onClick={() => setMobileMenuOpen(false)}
className="flex h-11 items-center justify-center rounded-xl bg-neutral-950 px-3 text-sm font-semibold text-white transition-colors hover:bg-neutral-800"
>
Try OpenSEO
</a>
<a
href="https://app.openseo.so/sign-in"
onClick={() => setMobileMenuOpen(false)}
className="flex h-11 items-center justify-center rounded-xl border border-neutral-200 px-3 text-sm font-semibold text-neutral-800 transition-colors hover:border-neutral-300 hover:bg-neutral-50"
>
Sign in
</a>
</div>
<div className="mt-3 space-y-3">
{mobileNavItems.map((section) => (
<div key={section.label}>
<p className="px-2 text-xs font-semibold uppercase tracking-wide text-neutral-500">
{section.label}
</p>
<div className="mt-1 space-y-1">
{section.links.map((item) => (
<a
key={item.href}
href={item.href}
onClick={() => setMobileMenuOpen(false)}
className="flex min-h-10 items-center rounded-xl px-2 text-sm font-semibold text-neutral-800 transition-colors hover:bg-neutral-50 hover:text-neutral-950"
>
{item.label}
</a>
))}
</div>
</div>
))}
</div>
</div>
) : null}
</div>
</div>
<div className="max-w-3xl mx-auto px-6 py-16 md:py-24">
<Outlet /> <Outlet />
<MarketingFooter /> <MarketingFooter />
</div> </div>
@ -64,18 +195,73 @@ function MarketingLayout() {
); );
} }
function ResourcesDropdown() {
const resources = [
{
label: "Guides",
href: "/guides",
description: "Founder-focused SEO articles.",
},
{
label: "MCP",
href: "/docs/mcp",
description: "Connect OpenSEO to AI clients.",
},
{
label: "Skills",
href: "/docs/skills",
description: "Focused OpenSEO workflows.",
},
];
return (
<div className="relative group">
<a
href="/guides"
className="text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:hidden"
>
Resources
</a>
<button
type="button"
className="hidden text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:inline-flex"
>
Resources
</button>
<div className="pointer-events-none absolute left-1/2 top-full z-20 hidden w-[260px] -translate-x-1/2 pt-4 opacity-0 transition md:block group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
<div className="rounded-lg border border-neutral-200 bg-white p-3 shadow-xl shadow-neutral-900/10">
{resources.map((resource) => (
<a
key={resource.href}
href={resource.href}
className="block rounded-md px-3 py-2.5 transition-colors hover:bg-neutral-50"
>
<span className="block text-sm font-semibold text-neutral-900">
{resource.label}
</span>
<span className="mt-0.5 block text-xs leading-relaxed text-neutral-600">
{resource.description}
</span>
</a>
))}
</div>
</div>
</div>
);
}
function FeatureDropdown() { function FeatureDropdown() {
return ( return (
<div className="relative group"> <div className="relative group">
<Link <Link
to="/features" to="/features"
className="text-sm text-neutral-600 transition-colors hover:text-neutral-900 md:hidden" className="text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:hidden"
> >
Features Features
</Link> </Link>
<button <button
type="button" type="button"
className="hidden text-sm text-neutral-600 transition-colors hover:text-neutral-900 md:inline-flex" className="hidden text-sm font-semibold text-neutral-600 transition-colors hover:text-neutral-900 md:inline-flex"
> >
Features Features
</button> </button>

View File

@ -5,7 +5,9 @@ import { buildPageSeo } from "@/lib/seo";
const page = featurePages.aiBrandVisibility; const page = featurePages.aiBrandVisibility;
export const Route = createFileRoute("/_marketing/features/ai-brand-visibility")({ export const Route = createFileRoute(
"/_marketing/features/ai-brand-visibility",
)({
head: () => head: () =>
buildPageSeo({ buildPageSeo({
title: "AI Brand Visibility Tool", title: "AI Brand Visibility Tool",

View File

@ -1,6 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import type { ReactNode, SVGProps } from "react";
import { useState } from "react";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const mcpDescription = const mcpDescription =
@ -16,24 +14,16 @@ const toolCategories = [
}, },
{ {
title: "Get SERP results", title: "Get SERP results",
description: "See SERP results for a keyword.", description: "See live Google organic results for a keyword.",
},
{
title: "Get rank tracking positions",
description: "Read tracked keyword positions.",
},
{
title: "Get saved keywords",
description: "Pull saved keywords from OpenSEO.",
}, },
{ {
title: "Save keywords", title: "Save keywords",
description: "Save keywords back to OpenSEO.", description: "Keep useful ideas organized in your OpenSEO project.",
}, },
], ],
}, },
{ {
label: "Domain", label: "Competitive research",
tools: [ tools: [
{ {
title: "Get domain overview", title: "Get domain overview",
@ -51,6 +41,24 @@ const toolCategories = [
}, },
] as const; ] as const;
const workflows = [
{
title: "Research with live SEO data",
description:
"Give Codex, Claude, and other MCP clients access to OpenSEO keyword, SERP, domain, backlink, saved keyword, and rank-tracking data.",
},
{
title: "Keep the agent focused",
description:
"Ask for a specific workflow like competitor analysis or keyword clustering instead of a vague SEO recommendation.",
},
{
title: "Save useful output",
description:
"Let the agent save promising keywords back to OpenSEO so human review and rank tracking can happen in the same workspace.",
},
];
export const Route = createFileRoute("/_marketing/features/mcp")({ export const Route = createFileRoute("/_marketing/features/mcp")({
head: () => head: () =>
buildPageSeo({ buildPageSeo({
@ -67,35 +75,65 @@ function McpPage() {
<> <>
<p className="text-sm font-medium text-neutral-500">OpenSEO MCP</p> <p className="text-sm font-medium text-neutral-500">OpenSEO MCP</p>
<h1 className="mt-3 text-3xl font-bold tracking-tight leading-tight"> <h1 className="mt-3 text-3xl font-bold tracking-tight leading-tight">
OpenSEO MCP for your AI agent OpenSEO MCP for AI SEO workflows
</h1> </h1>
<p className="mt-4 text-neutral-700 leading-relaxed"> <p className="mt-4 text-neutral-700 leading-relaxed">
Connect OpenSEO to your AI agent so your MCP client can call OpenSEO Connect OpenSEO to your AI agent so it can research keywords, inspect
tools for keyword research, SERP results, domain overview, domain SERPs, compare competitors, summarize backlink context, save keyword
keywords, backlink overview, saved keywords, and rank-tracking opportunities, and review rank-tracking data with live SEO context.
positions.
</p> </p>
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<a <a
href="#setup" href="/docs/mcp"
className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-neutral-900 underline decoration-neutral-300 underline-offset-4 transition-colors hover:text-neutral-600" className="inline-flex h-10 items-center justify-center rounded-md bg-neutral-900 px-5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
> >
Skip to setup Set up OpenSEO MCP
<span aria-hidden="true"></span>
</a> </a>
<a
href="/docs/skills"
className="inline-flex h-10 items-center justify-center rounded-md border border-neutral-300 px-5 text-sm font-medium text-neutral-900 transition-colors hover:border-neutral-900"
>
View OpenSEO skills
</a>
</div>
<section className="mt-12"> <section className="mt-12">
<h2 className="text-xl font-semibold">What it does</h2> <h2 className="text-xl font-semibold">What it does</h2>
<p className="mt-2 text-sm leading-relaxed text-neutral-600"> <p className="mt-2 text-sm leading-relaxed text-neutral-600">
MCP lets compatible AI clients call OpenSEO research tools directly. MCP lets compatible AI clients call OpenSEO research tools directly.
After connecting OpenSEO through a supported MCP client, you can ask After connecting OpenSEO through a supported MCP client, your agent
your agent to research keywords, inspect search results, compare can perform structured SEO research with data from your OpenSEO
domains, and save useful ideas back to your workspace. project instead of relying on generic guesses.
</p> </p>
</section> </section>
<section className="mt-12"> <section className="mt-12">
<h2 className="text-xl font-semibold">Available tools</h2> <h2 className="text-xl font-semibold">Useful workflows</h2>
<ol className="mt-6 space-y-6">
{workflows.map((workflow, index) => (
<li
key={workflow.title}
className="grid grid-cols-[2.25rem_1fr] gap-x-4"
>
<span className="pt-[2px] font-mono text-sm tabular-nums text-neutral-400">
{String(index + 1).padStart(2, "0")}
</span>
<div>
<h3 className="text-sm font-semibold text-neutral-900">
{workflow.title}
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-neutral-600">
{workflow.description}
</p>
</div>
</li>
))}
</ol>
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">Available tool groups</h2>
<div className="mt-5 grid gap-x-8 gap-y-8 md:grid-cols-2"> <div className="mt-5 grid gap-x-8 gap-y-8 md:grid-cols-2">
{toolCategories.map((category) => ( {toolCategories.map((category) => (
<div key={category.label}> <div key={category.label}>
@ -119,213 +157,24 @@ function McpPage() {
</div> </div>
</section> </section>
<section id="setup" className="mt-12"> <section className="mt-12 rounded-lg border border-neutral-200 bg-neutral-50 p-5">
<h2 className="text-xl font-semibold">Set up OpenSEO MCP</h2> <h2 className="text-lg font-semibold text-neutral-900">
Setup lives in Docs
</h2>
<p className="mt-2 text-sm leading-relaxed text-neutral-600"> <p className="mt-2 text-sm leading-relaxed text-neutral-600">
The first connection sends you through OpenSEO login. After The MCP server URL, Claude setup, Codex setup, and troubleshooting
authorization, your MCP client can call OpenSEO tools using the steps are maintained in the docs so this feature page can stay focused
authorized OpenSEO project context and scopes. The MCP server URL is{" "} on what OpenSEO MCP makes possible.
<code>https://app.openseo.so/mcp</code>. For the most current setup UI </p>
and a copyable endpoint, open{" "} <div className="mt-4">
<a <a
href="https://app.openseo.so/ai" href="/docs/mcp"
className="font-medium text-neutral-900 underline decoration-neutral-300 underline-offset-4 transition-colors hover:text-neutral-600" className="inline-flex h-9 items-center justify-center rounded-md bg-neutral-900 px-4 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
> >
AI & MCP in OpenSEO Open MCP docs
</a> </a>
.
</p>
<div className="mt-5 rounded-lg border border-neutral-200 bg-white px-4 py-3.5">
<h3 className="text-sm font-semibold text-neutral-900">
Don&apos;t see your agent or AI app?
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-neutral-600">
Copy the link to this page into your AI tool and ask it how to
configure OpenSEO MCP for your tool of choice.
</p>
</div>
<p className="mt-5 text-sm leading-relaxed text-neutral-600">
Follow the setup steps for Claude, Claude Code, Codex, or MCP clients
that support remote HTTP MCP servers and the required authorization
flow.
</p>
<div className="mt-5 divide-y divide-neutral-200 overflow-hidden rounded-lg border border-neutral-200 bg-neutral-50">
<SetupOption
id="claude-code"
title="Claude Code"
subtitle="Add with the CLI"
icon={<ClaudeIcon className="size-5" />}
>
<p className="text-sm leading-relaxed text-neutral-600">
Use user scope if you want OpenSEO available across projects, or
local scope for the current repository.
</p>
<CodeBlock
code={`claude mcp add --transport http --scope user openseo https://app.openseo.so/mcp`}
/>
</SetupOption>
<SetupOption
id="claude-desktop"
title="Claude Desktop"
subtitle="Add a custom connector"
icon={<ClaudeIcon className="size-5" />}
>
<ol className="ml-5 list-decimal space-y-1.5 text-sm leading-relaxed text-neutral-600">
<li>Open Settings -&gt; Connectors.</li>
<li>Click Add custom connector.</li>
<li>
Paste <code>https://app.openseo.so/mcp</code>.
</li>
<li>Approve the OpenSEO login when prompted.</li>
</ol>
<p className="text-xs leading-relaxed text-neutral-500">
Requires a Claude plan that supports custom connectors.
</p>
</SetupOption>
<SetupOption
id="codex"
title="Codex"
subtitle="Add with the CLI"
icon={<CodexIcon className="size-5" />}
>
<p className="text-sm leading-relaxed text-neutral-600">
Add OpenSEO as a hosted MCP server, then run the OAuth login flow.
</p>
<CodeBlock
code={`codex mcp add openseo --url https://app.openseo.so/mcp
codex mcp login openseo`}
/>
</SetupOption>
<SetupOption
id="codex-desktop"
title="Codex Desktop"
subtitle="Settings -> Integrations & MCP"
icon={<CodexIcon className="size-5" />}
>
<ol className="ml-5 list-decimal space-y-1.5 text-sm leading-relaxed text-neutral-600">
<li>Open Settings -&gt; Integrations &amp; MCP.</li>
<li>Click Add your own.</li>
<li>
Paste <code>https://app.openseo.so/mcp</code>.
</li>
<li>Approve the OpenSEO login when prompted.</li>
</ol>
</SetupOption>
</div> </div>
</section> </section>
</> </>
); );
} }
function SetupOption({
id,
title,
subtitle,
icon,
children,
}: {
id: string;
title: string;
subtitle: string;
icon?: ReactNode;
children: ReactNode;
}) {
const [open, setOpen] = useState(false);
const contentId = `setup-${id}`;
return (
<section>
<button
type="button"
aria-expanded={open}
aria-controls={contentId}
onClick={() => setOpen((value) => !value)}
className="flex w-full items-center justify-between gap-3 px-4 py-3.5 text-left transition-colors hover:bg-neutral-100"
>
<span className="flex min-w-0 items-center gap-3">
{icon ? (
<span className="flex size-5 shrink-0 items-center justify-center text-neutral-900">
{icon}
</span>
) : (
<span className="flex size-5 shrink-0 items-center justify-center rounded-full border border-neutral-300 text-[10px] font-semibold text-neutral-500">
AI
</span>
)}
<span className="flex min-w-0 flex-col gap-0.5">
<h3 className="text-sm font-medium text-neutral-900">{title}</h3>
<span className="text-xs text-neutral-500">{subtitle}</span>
</span>
</span>
<svg
aria-hidden="true"
viewBox="0 0 16 16"
className={`size-4 shrink-0 text-neutral-400 transition-transform ${
open ? "rotate-180" : ""
}`}
>
<path
d="M4 6l4 4 4-4"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
/>
</svg>
</button>
{open ? (
<div id={contentId} className="space-y-3 px-4 pb-4 pt-1">
{children}
</div>
) : null}
</section>
);
}
function CodeBlock({ code }: { code: string }) {
return (
<pre className="overflow-x-auto rounded-lg border border-neutral-200 bg-white p-4 text-xs leading-relaxed text-neutral-800">
<code>{code}</code>
</pre>
);
}
function ClaudeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 257"
preserveAspectRatio="xMidYMid"
aria-hidden="true"
{...props}
>
<path
fill="#D97757"
d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"
/>
</svg>
);
}
function CodexIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
aria-hidden="true"
{...props}
>
<path d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z" />
</svg>
);
}

56
web/src/routes/docs/$.tsx Normal file
View File

@ -0,0 +1,56 @@
import { createFileRoute } from "@tanstack/react-router";
import { createClientLoader } from "fumadocs-mdx/runtime/vite";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { ContentPost, mdxComponents } from "@/components/content-post";
import { baseOptions } from "@/lib/layout.shared";
import { getDocsPageTree, getDocsPost } from "@/lib/content.functions";
import { buildPageSeo } from "@/lib/seo";
import { docs } from "../../../source.generated";
export const Route = createFileRoute("/docs/$")({
loader: async ({ params }: { params: { _splat?: string } }) => {
const slugs = params._splat?.split("/") ?? [];
const page = await getDocsPost({ data: slugs });
await clientMdxLoader.preload(page.path);
return {
...page,
pageTree: await getDocsPageTree(),
};
},
head: ({ loaderData }: { loaderData?: unknown }) => {
const data = loaderData as
| { title?: string; description?: string; url?: string }
| undefined;
return buildPageSeo({
title: data?.title ?? "OpenSEO Docs",
description: data?.description,
path: data?.url ?? "/docs",
titleSuffix: "OpenSEO",
});
},
component: DocsPost,
});
const clientMdxLoader = createClientLoader(docs, {
id: "docs",
component({ default: MDX }) {
return <MDX components={mdxComponents} />;
},
});
function DocsPost() {
const data = Route.useLoaderData();
const Content = clientMdxLoader.getComponent(data.path);
return (
<DocsLayout tree={data.pageTree} {...baseOptions()}>
<ContentPost
backLabel="Back to Docs"
backTo="/docs"
title={data.title}
description={data.description}
Content={Content}
/>
</DocsLayout>
);
}

View File

@ -0,0 +1,40 @@
import { createFileRoute } from "@tanstack/react-router";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { ContentIndex } from "@/components/content-index";
import { baseOptions } from "@/lib/layout.shared";
import { getDocsPageTree, getDocsPosts } from "@/lib/content.functions";
import { buildPageSeo } from "@/lib/seo";
const docsDescription =
"OpenSEO setup and reference docs for MCP, AI clients, and workflow configuration.";
export const Route = createFileRoute("/docs/")({
head: () =>
buildPageSeo({
title: "OpenSEO Docs",
description: docsDescription,
path: "/docs",
}),
component: DocsIndex,
loader: async () => ({
pages: await getDocsPosts(),
pageTree: await getDocsPageTree(),
}),
});
function DocsIndex() {
const { pages, pageTree } = Route.useLoaderData();
return (
<DocsLayout tree={pageTree} {...baseOptions()}>
<ContentIndex
eyebrow="Docs"
title="OpenSEO Docs"
description={docsDescription}
emptyLabel="No docs yet. Check back soon."
items={pages}
route="/docs/$"
/>
</DocsLayout>
);
}

View File

@ -32,7 +32,10 @@ function GuideIndex() {
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
{guides.map((guide) => ( {guides.map((guide) => (
<article key={guide.url} className="border-b pb-8 last:border-b-0"> <article
key={guide.url}
className="border-b pb-8 last:border-b-0"
>
<Link <Link
to="/guides/$" to="/guides/$"
params={{ _splat: guide.slugs.join("/") }} params={{ _splat: guide.slugs.join("/") }}