Add strategy library index and navigation (#479)

* feat(web): add strategy library index

* fix(web): align strategy library review feedback

* fix(web): reorder resource links
This commit is contained in:
Ben Senescu 2026-08-11 22:53:02 -04:00 committed by GitHub
parent bd402844fa
commit a1ed6ece4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 350 additions and 150 deletions

View File

@ -96,9 +96,11 @@ function getLibraryPaths(dir = LIBRARY_ROUTES_DIR, segments = []) {
const slug = entry.name.replace(/\.tsx$/i, ""); const slug = entry.name.replace(/\.tsx$/i, "");
const pathSegments = slug === "index" ? segments : [...segments, slug]; const pathSegments = slug === "index" ? segments : [...segments, slug];
return pathSegments.length > 0 return [
? [`/library/${pathSegments.join("/")}`] pathSegments.length > 0
: []; ? `/library/${pathSegments.join("/")}`
: "/library",
];
}); });
} }

View File

@ -1,12 +1,14 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { DocsBody } from "fumadocs-ui/page"; import { DocsBody } from "fumadocs-ui/page";
import { buildBreadcrumbJsonLd } from "@/lib/seo";
const LIBRARY_PILLAR_PATH = "/library/keyword-research"; const KEYWORD_RESEARCH_LIBRARY_PATH = "/library/keyword-research";
type LibrarySpokePageProps = { type LibrarySpokePageProps = {
title: string; title: string;
description?: string; description?: string;
crumb: string; crumb: string;
path: string;
children: ReactNode; children: ReactNode;
}; };
@ -14,20 +16,37 @@ export function LibrarySpokePage({
title, title,
description, description,
crumb, crumb,
path,
children, children,
}: LibrarySpokePageProps) { }: LibrarySpokePageProps) {
const breadcrumbLd = buildBreadcrumbJsonLd([
{ name: "Strategy Library", path: "/library" },
{ name: "Keyword Research", path: KEYWORD_RESEARCH_LIBRARY_PATH },
{ name: crumb, path },
]);
return ( return (
<article className="mx-auto max-w-3xl text-neutral-900"> <article className="mx-auto max-w-3xl text-neutral-900">
<header className="mb-10 border-b border-[var(--color-border-subtle)] pb-8"> <header className="mb-10 border-b border-[var(--color-border-subtle)] pb-8">
<p className="text-sm text-[var(--color-brand-muted)]"> <nav
aria-label="Breadcrumb"
className="text-sm text-[var(--color-brand-muted)]"
>
<a <a
href={LIBRARY_PILLAR_PATH} href="/library"
className="font-medium text-[var(--color-brand-accent)]" className="font-medium text-[var(--color-brand-accent)]"
> >
Strategy Library Strategy Library
</a>{" "} </a>{" "}
/{" "}
<a
href={KEYWORD_RESEARCH_LIBRARY_PATH}
className="font-medium text-[var(--color-brand-accent)]"
>
Keyword Research
</a>{" "}
/ <span>{crumb}</span> / <span>{crumb}</span>
</p> </nav>
<h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-5xl"> <h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-5xl">
{title} {title}
</h1> </h1>
@ -43,6 +62,12 @@ export function LibrarySpokePage({
</DocsBody> </DocsBody>
<LibrarySpokeCta /> <LibrarySpokeCta />
<script
type="application/ld+json"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }}
/>
</article> </article>
); );
} }
@ -51,11 +76,11 @@ function LibrarySpokeCta() {
return ( return (
<section className="mt-14 rounded-xl border border-[var(--color-border-subtle)] bg-white p-6"> <section className="mt-14 rounded-xl border border-[var(--color-border-subtle)] bg-white p-6">
<p className="text-xl font-semibold tracking-tight text-neutral-950"> <p className="text-xl font-semibold tracking-tight text-neutral-950">
Run every play in this guide Run this strategy in OpenSEO
</p> </p>
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]"> <p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
OpenSEO connects your Search Console and expands your seeds. Open Run the MCP prompt in this guide with OpenSEO. OpenSEO is open source,
source, free to try, no credit card. free to try, and does not require a credit card.
</p> </p>
<div className="mt-5 flex flex-col gap-3 sm:flex-row"> <div className="mt-5 flex flex-col gap-3 sm:flex-row">
<a <a
@ -68,10 +93,10 @@ function LibrarySpokeCta() {
</span> </span>
</a> </a>
<a <a
href={LIBRARY_PILLAR_PATH} href={KEYWORD_RESEARCH_LIBRARY_PATH}
className="inline-flex h-10 items-center justify-center rounded-lg border border-[var(--color-border-subtle)] bg-white px-4 text-sm font-medium text-neutral-950 transition-colors hover:border-neutral-950" className="inline-flex h-10 items-center justify-center rounded-lg border border-[var(--color-border-subtle)] bg-white px-4 text-sm font-medium text-neutral-950 transition-colors hover:border-neutral-950"
> >
Back to the Strategy Library Back to Keyword Research
</a> </a>
</div> </div>
</section> </section>

View File

@ -15,7 +15,7 @@ export function SiteFooter({ className }: { className?: string }) {
OpenSEO OpenSEO
</Link> </Link>
<div className="mt-6 grid grid-cols-2 gap-8 md:grid-cols-4"> <div className="mt-6 grid grid-cols-2 gap-8 md:grid-cols-[repeat(auto-fit,minmax(9rem,1fr))]">
<div> <div>
<p className="font-semibold text-neutral-900">Features</p> <p className="font-semibold text-neutral-900">Features</p>
<div className="mt-2 flex flex-col gap-1.5"> <div className="mt-2 flex flex-col gap-1.5">
@ -41,10 +41,19 @@ export function SiteFooter({ className }: { className?: string }) {
<div> <div>
<p className="font-semibold text-neutral-900">Resources</p> <p className="font-semibold text-neutral-900">Resources</p>
<div className="mt-2 flex flex-col gap-1.5"> <div className="mt-2 flex flex-col gap-1.5">
<a href="/docs/mcp">MCP</a>
<a href="/docs/skills">Skills</a>
<Link to="/library">Strategy Library</Link>
<Link to="/open-source-seo">Why Open Source?</Link> <Link to="/open-source-seo">Why Open Source?</Link>
<Link to="/blogs">Blog</Link> <Link to="/blogs">Blog</Link>
<a href="/docs">Docs</a> <a href="/docs">Docs</a>
<a href="/docs/skills">Skills</a> </div>
</div>
<div>
<p className="font-semibold text-neutral-900">Free Tools</p>
<div className="mt-2 flex flex-col gap-1.5">
<Link to="/backlink-checker">Backlink Checker</Link>
</div> </div>
</div> </div>

View File

@ -112,7 +112,7 @@ export const featurePages = {
{ {
question: "Can I use OpenSEO as a free keyword research tool?", question: "Can I use OpenSEO as a free keyword research tool?",
answer: answer:
"Not unlimited: quality keyword data costs money everywhere, which is why the big SEO suites run $100/month and up. OpenSEO is the most affordable option; you can start for free, and paid plans start at $10/month with usage credits included. It's also open source, so you can self-host with your own DataForSEO account.", "Not unlimited: quality keyword data costs money everywhere, which is why the big SEO suites run $100/month and up. You can start OpenSEO for free; the paid plan is $10/month with usage credits included. It's also open source, so you can self-host with your own DataForSEO account.",
}, },
{ {
question: "Does OpenSEO show live search results?", question: "Does OpenSEO show live search results?",
@ -123,7 +123,7 @@ export const featurePages = {
guides: { guides: {
title: "The Keyword Research Strategy Library", title: "The Keyword Research Strategy Library",
description: description:
"Practitioner plays that treat keyword research as demand discovery, not a volume spreadsheet. Each guide is a full walkthrough with the copy-paste MCP prompt that runs it.", "Practitioner strategies for using keyword research to discover demand. Each guide includes a full walkthrough and a copy-paste MCP prompt.",
items: [ items: [
{ {
label: "Seed from conversation, not a volume report", label: "Seed from conversation, not a volume report",
@ -139,8 +139,7 @@ export const featurePages = {
}, },
{ {
label: "Search-intent mapping (hot / warm / cold)", label: "Search-intent mapping (hot / warm / cold)",
description: description: "Sort keywords by buying temperature before you write.",
"Label every keyword by buying temperature before you write.",
href: "/library/keyword-research/search-intent-mapping", href: "/library/keyword-research/search-intent-mapping",
}, },
{ {
@ -151,7 +150,7 @@ export const featurePages = {
}, },
], ],
cta: { cta: {
label: "Browse the full Strategy Library", label: "Browse all keyword research strategies",
href: "/library/keyword-research", href: "/library/keyword-research",
}, },
}, },

View File

@ -19,6 +19,21 @@ export function toCanonicalUrl(path: string): string {
return new URL(toCanonicalPath(path), `${SITE_URL}/`).href; return new URL(toCanonicalPath(path), `${SITE_URL}/`).href;
} }
export function buildBreadcrumbJsonLd(
items: Array<{ name: string; path: string }>,
) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
name: item.name,
item: toCanonicalUrl(item.path),
})),
};
}
type BuildSeoParams = { type BuildSeoParams = {
title: string; title: string;
path: string; path: string;

View File

@ -0,0 +1,55 @@
export type StrategyLibraryItem = {
title: string;
description: string;
href: string;
};
export const keywordResearchStrategies: StrategyLibraryItem[] = [
{
title: "Seed from conversation, not a volume report",
description:
"Harvest seed keywords from sales calls and support tickets using the language customers already use.",
href: "/library/keyword-research/seed-from-conversation",
},
{
title: "What are long-tail keywords, and how to mine them",
description:
"Find long-tail keywords in People Also Ask, autocomplete, and Search Console queries where your pages already rank.",
href: "/library/keyword-research/long-tail-question-mining",
},
{
title: "Search-intent mapping (hot / warm / cold)",
description:
"Sort keywords by buying temperature before you write, then build high-intent pages first.",
href: "/library/keyword-research/search-intent-mapping",
},
{
title: "Cluster keywords into topical hubs",
description:
"Group keywords by intent and build topical hubs without creating competing pages.",
href: "/library/keyword-research/cluster-topical-hubs",
},
{
title: "Programmatic discovery with Search Console",
description:
"Use MCP to find Search Console queries and pages with room to gain more clicks.",
href: "/library/keyword-research/gsc-programmatic-discovery",
},
{
title: "Opportunity sizing & forecasting",
description:
"Estimate a cluster's difficulty, traffic range, and payback scenarios before you invest.",
href: "/library/keyword-research/opportunity-sizing-forecasting",
},
{
title: "Intent beyond Google (Pinterest, AI, LinkedIn)",
description: "Research demand on Pinterest, LinkedIn, and AI assistants.",
href: "/library/keyword-research/intent-beyond-google",
},
{
title: "Map positioning to real demand",
description:
"Check whether your category language matches the terms customers search for.",
href: "/library/keyword-research/positioning-to-demand",
},
];

View File

@ -28,6 +28,7 @@ import { Route as MarketingPricingRouteImport } from './routes/_marketing/pricin
import { Route as MarketingOpenSourceSeoRouteImport } from './routes/_marketing/open-source-seo' import { Route as MarketingOpenSourceSeoRouteImport } from './routes/_marketing/open-source-seo'
import { Route as MarketingGoogleSearchConsoleMcpRouteImport } from './routes/_marketing/google-search-console-mcp' import { Route as MarketingGoogleSearchConsoleMcpRouteImport } from './routes/_marketing/google-search-console-mcp'
import { Route as MarketingBacklinkCheckerRouteImport } from './routes/_marketing/backlink-checker' import { Route as MarketingBacklinkCheckerRouteImport } from './routes/_marketing/backlink-checker'
import { Route as MarketingLibraryIndexRouteImport } from './routes/_marketing/library/index'
import { Route as MarketingFeaturesIndexRouteImport } from './routes/_marketing/features/index' import { Route as MarketingFeaturesIndexRouteImport } from './routes/_marketing/features/index'
import { Route as MarketingFeaturesSiteAuditRouteImport } from './routes/_marketing/features/site-audit' import { Route as MarketingFeaturesSiteAuditRouteImport } from './routes/_marketing/features/site-audit'
import { Route as MarketingFeaturesSavedKeywordsRouteImport } from './routes/_marketing/features/saved-keywords' import { Route as MarketingFeaturesSavedKeywordsRouteImport } from './routes/_marketing/features/saved-keywords'
@ -145,6 +146,11 @@ const MarketingBacklinkCheckerRoute =
path: '/backlink-checker', path: '/backlink-checker',
getParentRoute: () => MarketingRoute, getParentRoute: () => MarketingRoute,
} as any) } as any)
const MarketingLibraryIndexRoute = MarketingLibraryIndexRouteImport.update({
id: '/library/',
path: '/library/',
getParentRoute: () => MarketingRoute,
} as any)
const MarketingFeaturesIndexRoute = MarketingFeaturesIndexRouteImport.update({ const MarketingFeaturesIndexRoute = MarketingFeaturesIndexRouteImport.update({
id: '/features/', id: '/features/',
path: '/features/', path: '/features/',
@ -296,6 +302,7 @@ export interface FileRoutesByFullPath {
'/features/saved-keywords': typeof MarketingFeaturesSavedKeywordsRoute '/features/saved-keywords': typeof MarketingFeaturesSavedKeywordsRoute
'/features/site-audit': typeof MarketingFeaturesSiteAuditRoute '/features/site-audit': typeof MarketingFeaturesSiteAuditRoute
'/features/': typeof MarketingFeaturesIndexRoute '/features/': typeof MarketingFeaturesIndexRoute
'/library/': typeof MarketingLibraryIndexRoute
'/library/keyword-research/cluster-topical-hubs': typeof MarketingLibraryKeywordResearchClusterTopicalHubsRoute '/library/keyword-research/cluster-topical-hubs': typeof MarketingLibraryKeywordResearchClusterTopicalHubsRoute
'/library/keyword-research/gsc-programmatic-discovery': typeof MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute '/library/keyword-research/gsc-programmatic-discovery': typeof MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute
'/library/keyword-research/intent-beyond-google': typeof MarketingLibraryKeywordResearchIntentBeyondGoogleRoute '/library/keyword-research/intent-beyond-google': typeof MarketingLibraryKeywordResearchIntentBeyondGoogleRoute
@ -336,6 +343,7 @@ export interface FileRoutesByTo {
'/features/saved-keywords': typeof MarketingFeaturesSavedKeywordsRoute '/features/saved-keywords': typeof MarketingFeaturesSavedKeywordsRoute
'/features/site-audit': typeof MarketingFeaturesSiteAuditRoute '/features/site-audit': typeof MarketingFeaturesSiteAuditRoute
'/features': typeof MarketingFeaturesIndexRoute '/features': typeof MarketingFeaturesIndexRoute
'/library': typeof MarketingLibraryIndexRoute
'/library/keyword-research/cluster-topical-hubs': typeof MarketingLibraryKeywordResearchClusterTopicalHubsRoute '/library/keyword-research/cluster-topical-hubs': typeof MarketingLibraryKeywordResearchClusterTopicalHubsRoute
'/library/keyword-research/gsc-programmatic-discovery': typeof MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute '/library/keyword-research/gsc-programmatic-discovery': typeof MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute
'/library/keyword-research/intent-beyond-google': typeof MarketingLibraryKeywordResearchIntentBeyondGoogleRoute '/library/keyword-research/intent-beyond-google': typeof MarketingLibraryKeywordResearchIntentBeyondGoogleRoute
@ -378,6 +386,7 @@ export interface FileRoutesById {
'/_marketing/features/saved-keywords': typeof MarketingFeaturesSavedKeywordsRoute '/_marketing/features/saved-keywords': typeof MarketingFeaturesSavedKeywordsRoute
'/_marketing/features/site-audit': typeof MarketingFeaturesSiteAuditRoute '/_marketing/features/site-audit': typeof MarketingFeaturesSiteAuditRoute
'/_marketing/features/': typeof MarketingFeaturesIndexRoute '/_marketing/features/': typeof MarketingFeaturesIndexRoute
'/_marketing/library/': typeof MarketingLibraryIndexRoute
'/_marketing/library/keyword-research/cluster-topical-hubs': typeof MarketingLibraryKeywordResearchClusterTopicalHubsRoute '/_marketing/library/keyword-research/cluster-topical-hubs': typeof MarketingLibraryKeywordResearchClusterTopicalHubsRoute
'/_marketing/library/keyword-research/gsc-programmatic-discovery': typeof MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute '/_marketing/library/keyword-research/gsc-programmatic-discovery': typeof MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute
'/_marketing/library/keyword-research/intent-beyond-google': typeof MarketingLibraryKeywordResearchIntentBeyondGoogleRoute '/_marketing/library/keyword-research/intent-beyond-google': typeof MarketingLibraryKeywordResearchIntentBeyondGoogleRoute
@ -420,6 +429,7 @@ export interface FileRouteTypes {
| '/features/saved-keywords' | '/features/saved-keywords'
| '/features/site-audit' | '/features/site-audit'
| '/features/' | '/features/'
| '/library/'
| '/library/keyword-research/cluster-topical-hubs' | '/library/keyword-research/cluster-topical-hubs'
| '/library/keyword-research/gsc-programmatic-discovery' | '/library/keyword-research/gsc-programmatic-discovery'
| '/library/keyword-research/intent-beyond-google' | '/library/keyword-research/intent-beyond-google'
@ -460,6 +470,7 @@ export interface FileRouteTypes {
| '/features/saved-keywords' | '/features/saved-keywords'
| '/features/site-audit' | '/features/site-audit'
| '/features' | '/features'
| '/library'
| '/library/keyword-research/cluster-topical-hubs' | '/library/keyword-research/cluster-topical-hubs'
| '/library/keyword-research/gsc-programmatic-discovery' | '/library/keyword-research/gsc-programmatic-discovery'
| '/library/keyword-research/intent-beyond-google' | '/library/keyword-research/intent-beyond-google'
@ -501,6 +512,7 @@ export interface FileRouteTypes {
| '/_marketing/features/saved-keywords' | '/_marketing/features/saved-keywords'
| '/_marketing/features/site-audit' | '/_marketing/features/site-audit'
| '/_marketing/features/' | '/_marketing/features/'
| '/_marketing/library/'
| '/_marketing/library/keyword-research/cluster-topical-hubs' | '/_marketing/library/keyword-research/cluster-topical-hubs'
| '/_marketing/library/keyword-research/gsc-programmatic-discovery' | '/_marketing/library/keyword-research/gsc-programmatic-discovery'
| '/_marketing/library/keyword-research/intent-beyond-google' | '/_marketing/library/keyword-research/intent-beyond-google'
@ -663,6 +675,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MarketingBacklinkCheckerRouteImport preLoaderRoute: typeof MarketingBacklinkCheckerRouteImport
parentRoute: typeof MarketingRoute parentRoute: typeof MarketingRoute
} }
'/_marketing/library/': {
id: '/_marketing/library/'
path: '/library'
fullPath: '/library/'
preLoaderRoute: typeof MarketingLibraryIndexRouteImport
parentRoute: typeof MarketingRoute
}
'/_marketing/features/': { '/_marketing/features/': {
id: '/_marketing/features/' id: '/_marketing/features/'
path: '/features' path: '/features'
@ -824,6 +843,7 @@ interface MarketingRouteChildren {
MarketingFeaturesSavedKeywordsRoute: typeof MarketingFeaturesSavedKeywordsRoute MarketingFeaturesSavedKeywordsRoute: typeof MarketingFeaturesSavedKeywordsRoute
MarketingFeaturesSiteAuditRoute: typeof MarketingFeaturesSiteAuditRoute MarketingFeaturesSiteAuditRoute: typeof MarketingFeaturesSiteAuditRoute
MarketingFeaturesIndexRoute: typeof MarketingFeaturesIndexRoute MarketingFeaturesIndexRoute: typeof MarketingFeaturesIndexRoute
MarketingLibraryIndexRoute: typeof MarketingLibraryIndexRoute
MarketingLibraryKeywordResearchClusterTopicalHubsRoute: typeof MarketingLibraryKeywordResearchClusterTopicalHubsRoute MarketingLibraryKeywordResearchClusterTopicalHubsRoute: typeof MarketingLibraryKeywordResearchClusterTopicalHubsRoute
MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute: typeof MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute: typeof MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute
MarketingLibraryKeywordResearchIntentBeyondGoogleRoute: typeof MarketingLibraryKeywordResearchIntentBeyondGoogleRoute MarketingLibraryKeywordResearchIntentBeyondGoogleRoute: typeof MarketingLibraryKeywordResearchIntentBeyondGoogleRoute
@ -854,6 +874,7 @@ const MarketingRouteChildren: MarketingRouteChildren = {
MarketingFeaturesSavedKeywordsRoute: MarketingFeaturesSavedKeywordsRoute, MarketingFeaturesSavedKeywordsRoute: MarketingFeaturesSavedKeywordsRoute,
MarketingFeaturesSiteAuditRoute: MarketingFeaturesSiteAuditRoute, MarketingFeaturesSiteAuditRoute: MarketingFeaturesSiteAuditRoute,
MarketingFeaturesIndexRoute: MarketingFeaturesIndexRoute, MarketingFeaturesIndexRoute: MarketingFeaturesIndexRoute,
MarketingLibraryIndexRoute: MarketingLibraryIndexRoute,
MarketingLibraryKeywordResearchClusterTopicalHubsRoute: MarketingLibraryKeywordResearchClusterTopicalHubsRoute:
MarketingLibraryKeywordResearchClusterTopicalHubsRoute, MarketingLibraryKeywordResearchClusterTopicalHubsRoute,
MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute: MarketingLibraryKeywordResearchGscProgrammaticDiscoveryRoute:

View File

@ -59,10 +59,11 @@ function getMobileNavItems(githubStarCount: string) {
{ {
label: "Resources", label: "Resources",
links: [ links: [
{ label: "Blog", href: "/blogs" },
{ label: "Docs", href: "/docs" },
{ label: "MCP Setup", href: "/docs/mcp" }, { label: "MCP Setup", href: "/docs/mcp" },
{ label: "Skills", href: "/docs/skills" }, { label: "Skills", href: "/docs/skills" },
{ label: "Strategy Library", href: "/library" },
{ label: "Blog", href: "/blogs" },
{ label: "Docs", href: "/docs" },
], ],
}, },
{ {
@ -274,16 +275,6 @@ function MarketingLayout() {
function ResourcesDropdown() { function ResourcesDropdown() {
const resources = [ const resources = [
{
label: "Blog",
href: "/blogs",
description: "SEO articles and guides.",
},
{
label: "Docs",
href: "/docs",
description: "Setup, MCP, skills, and self-hosting guides.",
},
{ {
label: "MCP", label: "MCP",
href: "/docs/mcp", href: "/docs/mcp",
@ -294,6 +285,21 @@ function ResourcesDropdown() {
href: "/docs/skills", href: "/docs/skills",
description: "Focused OpenSEO workflows.", description: "Focused OpenSEO workflows.",
}, },
{
label: "Strategy Library",
href: "/library",
description: "Practical SEO strategies grouped by topic.",
},
{
label: "Blog",
href: "/blogs",
description: "SEO articles and guides.",
},
{
label: "Docs",
href: "/docs",
description: "Setup, MCP, skills, and self-hosting guides.",
},
]; ];
return ( return (

View File

@ -0,0 +1,87 @@
import { createFileRoute } from "@tanstack/react-router";
import { buildPageSeo } from "@/lib/seo";
import { keywordResearchStrategies } from "@/lib/strategy-libraries";
const PATH = "/library";
const description =
"Browse practical SEO strategies for finding search demand, mapping intent, and planning pages.";
const featuredStrategies = keywordResearchStrategies.slice(0, 4);
export const Route = createFileRoute("/_marketing/library/")({
head: () =>
buildPageSeo({
title: "SEO Strategy Library",
description,
path: PATH,
titleSuffix: "OpenSEO",
}),
component: StrategyLibraryIndexPage,
});
function StrategyLibraryIndexPage() {
return (
<article className="mx-auto max-w-5xl">
<header className="max-w-3xl">
<p className="text-sm font-medium text-[var(--color-brand-accent)]">
Resources
</p>
<h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
SEO Strategy Library
</h1>
<p className="mt-5 text-lg leading-8 text-[var(--color-brand-muted)]">
Find search demand and decide which pages to build. Strategies are
grouped by topic so you can start with the problem you need to solve.
</p>
</header>
<section className="mt-12">
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
Browse by topic
</h2>
<div className="mt-5 grid gap-4">
<a
href="/library/keyword-research"
className="block rounded-lg border border-[var(--color-border-subtle)] bg-white p-6 transition-colors hover:border-neutral-900"
>
<h3 className="text-2xl font-semibold tracking-tight text-neutral-950">
Keyword Research
</h3>
<p className="mt-3 max-w-xl text-sm leading-6 text-[var(--color-brand-muted)]">
Start with customer language, expand into long-tail demand, map
search intent, and decide which opportunities deserve a page.
</p>
<p className="mt-5 text-sm font-medium text-neutral-950">
View all {keywordResearchStrategies.length} strategies{" "}
<span aria-hidden="true">&rarr;</span>
</p>
</a>
</div>
</section>
<section className="mt-12">
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
Start with a strategy
</h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
Go straight to a workflow if you already know what you need to do.
</p>
<div className="mt-5 grid gap-4 md:grid-cols-2">
{featuredStrategies.map((strategy) => (
<a
key={strategy.href}
href={strategy.href}
className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-5 transition-colors hover:border-neutral-900"
>
<h3 className="text-base font-semibold text-neutral-950">
{strategy.title} <span aria-hidden="true">&rarr;</span>
</h3>
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
{strategy.description}
</p>
</a>
))}
</div>
</section>
</article>
);
}

View File

@ -6,6 +6,8 @@ import Content, {
import { LibrarySpokePage } from "@/components/library-page"; import { LibrarySpokePage } from "@/components/library-page";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const PATH = "/library/keyword-research/cluster-topical-hubs";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_marketing/library/keyword-research/cluster-topical-hubs", "/_marketing/library/keyword-research/cluster-topical-hubs",
)({ )({
@ -14,7 +16,7 @@ export const Route = createFileRoute(
title: title:
"Keyword Clustering: Turn a Keyword List into Topical Hubs (and Fix Cannibalization)", "Keyword Clustering: Turn a Keyword List into Topical Hubs (and Fix Cannibalization)",
description: frontmatter.description, description: frontmatter.description,
path: "/library/keyword-research/cluster-topical-hubs", path: PATH,
titleSuffix: "OpenSEO Library", titleSuffix: "OpenSEO Library",
ogType: "article", ogType: "article",
}), }),
@ -23,6 +25,7 @@ export const Route = createFileRoute(
title={frontmatter.title} title={frontmatter.title}
description={frontmatter.description} description={frontmatter.description}
crumb="Cluster keywords into topical hubs" crumb="Cluster keywords into topical hubs"
path={PATH}
> >
<Content components={{ ...defaultMdxComponents }} /> <Content components={{ ...defaultMdxComponents }} />
</LibrarySpokePage> </LibrarySpokePage>

View File

@ -6,6 +6,8 @@ import Content, {
import { LibrarySpokePage } from "@/components/library-page"; import { LibrarySpokePage } from "@/components/library-page";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const PATH = "/library/keyword-research/gsc-programmatic-discovery";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_marketing/library/keyword-research/gsc-programmatic-discovery", "/_marketing/library/keyword-research/gsc-programmatic-discovery",
)({ )({
@ -13,7 +15,7 @@ export const Route = createFileRoute(
buildPageSeo({ buildPageSeo({
title: "Search Console Keyword Research: Striking-Distance Queries", title: "Search Console Keyword Research: Striking-Distance Queries",
description: frontmatter.description, description: frontmatter.description,
path: "/library/keyword-research/gsc-programmatic-discovery", path: PATH,
titleSuffix: "OpenSEO Library", titleSuffix: "OpenSEO Library",
ogType: "article", ogType: "article",
}), }),
@ -22,6 +24,7 @@ export const Route = createFileRoute(
title={frontmatter.title} title={frontmatter.title}
description={frontmatter.description} description={frontmatter.description}
crumb="Programmatic discovery with Search Console" crumb="Programmatic discovery with Search Console"
path={PATH}
> >
<Content components={{ ...defaultMdxComponents }} /> <Content components={{ ...defaultMdxComponents }} />
</LibrarySpokePage> </LibrarySpokePage>

View File

@ -1,59 +1,9 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { buildPageSeo } from "@/lib/seo"; import { buildBreadcrumbJsonLd, buildPageSeo } from "@/lib/seo";
import { keywordResearchStrategies } from "@/lib/strategy-libraries";
const PATH = "/library/keyword-research"; const PATH = "/library/keyword-research";
const strategies = [
{
title: "Seed from conversation, not a volume report",
description:
"Harvest seed keywords from sales calls and support tickets: the phrasings tools never surface.",
href: "/library/keyword-research/seed-from-conversation",
},
{
title: "What are long-tail keywords, and how to mine them",
description:
"PAA fan-out, autocomplete harvesting, and the GSC queries you already half-rank for.",
href: "/library/keyword-research/long-tail-question-mining",
},
{
title: "Search-intent mapping (hot / warm / cold)",
description:
"Label every keyword by buying temperature before you write. Build the hot pages first.",
href: "/library/keyword-research/search-intent-mapping",
},
{
title: "Cluster keywords into topical hubs",
description:
"One page per intent, one hub per topic, plus the fix for keyword cannibalization.",
href: "/library/keyword-research/cluster-topical-hubs",
},
{
title: "Programmatic discovery with Search Console",
description:
"Query-mine your own GSC by MCP: striking-distance keywords, zero-click pages, dark queries.",
href: "/library/keyword-research/gsc-programmatic-discovery",
},
{
title: "Opportunity sizing & forecasting",
description:
"Size a cluster before you invest: difficulty, traffic ceiling, and modeled payback windows.",
href: "/library/keyword-research/opportunity-sizing-forecasting",
},
{
title: "Intent beyond Google (Pinterest, AI, LinkedIn)",
description:
"Where queries happen when they don't happen in a search box, AI assistants included.",
href: "/library/keyword-research/intent-beyond-google",
},
{
title: "Map positioning to real demand",
description:
"Check whether your category language matches what customers actually search.",
href: "/library/keyword-research/positioning-to-demand",
},
];
const faqs = [ const faqs = [
{ {
question: "How do you do keyword research for SEO?", question: "How do you do keyword research for SEO?",
@ -63,17 +13,17 @@ const faqs = [
{ {
question: "How do you do keyword research for free?", question: "How do you do keyword research for free?",
answer: answer:
"The discovery half runs on sources you already have: customer conversations, Google's autocomplete and People Also Ask, and your Search Console. Quality SEO data (volume, difficulty, live SERPs) is difficult to get, which is why SaaS tools run $100/month and up. OpenSEO is the most affordable option, starting at $10/month, and you can start for free.", "The discovery half runs on sources you already have: customer conversations, Google's autocomplete and People Also Ask, and your Search Console. Quality SEO data costs money, which is why the big SEO suites run $100/month and up. You can start OpenSEO for free; the paid plan starts at $10/month and includes $10 of usage. If you need more, you can buy top-up credits.",
}, },
{ {
question: "Can you do keyword research without Google Keyword Planner?", question: "Can you do keyword research without Google Keyword Planner?",
answer: answer:
"Yes, and for SEO you should. Planner groups variants and hides zero-ad-demand queries. Use it to sanity-check commercial value, not to discover topics.", "Yes. Keyword Planner combines close variants and reports approximate search-volume data designed for ad planning. Use it to sanity-check commercial value rather than as your only source for discovering topics.",
}, },
{ {
question: "What are the 3 types of keywords?", question: "What are the 3 types of keywords?",
answer: answer:
"By intent: informational, commercial/navigational, transactional. By shape: head, mid-tail, long-tail. The useful planning question is always intent first, shape second.", "There is no universal set of three. OpenSEO uses four intent types: informational, navigational, commercial, and transactional. By shape, terms are often grouped as head, mid-tail, and long-tail.",
}, },
{ {
question: "How do you do keyword research for a blog?", question: "How do you do keyword research for a blog?",
@ -92,12 +42,17 @@ const faqLd = {
})), })),
}; };
const breadcrumbLd = buildBreadcrumbJsonLd([
{ name: "Strategy Library", path: "/library" },
{ name: "Keyword Research", path: PATH },
]);
export const Route = createFileRoute("/_marketing/library/keyword-research/")({ export const Route = createFileRoute("/_marketing/library/keyword-research/")({
head: () => head: () =>
buildPageSeo({ buildPageSeo({
title: "How to Do Keyword Research: The Strategy Library", title: "How to Do Keyword Research: The Strategy Library",
description: description:
"Eight demand-discovery strategies drawn from interviews with working SEOs, each with a workflow you can run in OpenSEO.", "Eight demand-discovery strategies drawn from interviews with working SEOs, each with a workflow and an OpenSEO MCP prompt for its data-backed steps.",
path: PATH, path: PATH,
titleSuffix: "OpenSEO", titleSuffix: "OpenSEO",
}), }),
@ -108,76 +63,70 @@ function KeywordResearchLibraryPage() {
return ( return (
<article className="mx-auto max-w-5xl"> <article className="mx-auto max-w-5xl">
<header className="max-w-3xl"> <header className="max-w-3xl">
<p className="text-sm font-medium text-[var(--color-brand-accent)]"> <nav
aria-label="Breadcrumb"
className="text-sm text-[var(--color-brand-muted)]"
>
<a
href="/library"
className="font-medium text-[var(--color-brand-accent)]"
>
Strategy Library Strategy Library
</p> </a>{" "}
/ <span>Keyword Research</span>
</nav>
<h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl"> <h1 className="mt-3 text-4xl font-semibold leading-tight tracking-tight text-neutral-950 md:text-6xl">
The Keyword Research Strategy Library The Keyword Research Strategy Library
</h1> </h1>
<p className="mt-5 text-lg leading-8 text-[var(--color-brand-muted)]"> <p className="mt-5 text-lg leading-8 text-[var(--color-brand-muted)]">
Eight demand-discovery strategies drawn from interviews with working Eight demand-discovery strategies drawn from interviews with working
SEOs, each with a workflow you can run in OpenSEO. SEOs, each with a workflow and an OpenSEO MCP prompt for its
data-backed steps.
</p> </p>
</header> </header>
<section className="mt-12"> <section className="mt-12">
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950"> <h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
How to do keyword research: demand discovery, not a volume spreadsheet How to do keyword research through demand discovery
</h2> </h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]"> <p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
Most guides teach you to export a volume report and sort descending. Most guides teach you to export a volume report and sort descending.
These strategies start earlier, where demand originates: customer These strategies start earlier, where demand originates: customer
language, question mining, your own Search Console. They end with language, question mining, your own Search Console. They end with
pages mapped to intent, not keywords stuffed into paragraphs. Each pages mapped to intent, not keywords stuffed into paragraphs. Each
strategy includes a walkthrough and a copy-paste MCP prompt for the strategy includes a walkthrough and a copy-paste MCP prompt for its
workflow. data-backed steps.
</p> </p>
<div className="mt-6 grid gap-4 md:grid-cols-2"> <div className="mt-6 grid gap-4 md:grid-cols-2">
{strategies.map((strategy, index) => { {keywordResearchStrategies.map((strategy, index) => {
const number = String(index + 1).padStart(2, "0"); const number = String(index + 1).padStart(2, "0");
const body = ( const body = (
<> <>
<div className="flex items-start justify-between gap-3">
<span className="font-mono text-sm tabular-nums text-[var(--color-brand-accent)]"> <span className="font-mono text-sm tabular-nums text-[var(--color-brand-accent)]">
{number} {number}
</span> </span>
{strategy.href ? null : (
<span className="rounded-full border border-[var(--color-border-subtle)] px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider text-[var(--color-brand-muted)]">
Next up
</span>
)}
</div>
<h3 className="mt-3 text-base font-semibold text-neutral-950"> <h3 className="mt-3 text-base font-semibold text-neutral-950">
{strategy.title} {strategy.title}
{strategy.href ? (
<span <span
aria-hidden="true" aria-hidden="true"
className="ml-1 text-[var(--color-brand-accent)]" className="ml-1 text-[var(--color-brand-accent)]"
> >
&rarr; &rarr;
</span> </span>
) : null}
</h3> </h3>
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]"> <p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
{strategy.description} {strategy.description}
</p> </p>
</> </>
); );
return strategy.href ? ( return (
<a <a
key={strategy.title} key={strategy.href}
href={strategy.href} href={strategy.href}
className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-5 transition-colors hover:border-neutral-900" className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-5 transition-colors hover:border-neutral-900"
> >
{body} {body}
</a> </a>
) : (
<div
key={strategy.title}
className="rounded-lg border border-[var(--color-border-subtle)] bg-white p-5"
>
{body}
</div>
); );
})} })}
</div> </div>
@ -185,47 +134,50 @@ function KeywordResearchLibraryPage() {
<section className="mt-12"> <section className="mt-12">
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950"> <h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
What Google Keyword Planner hides (and what to use instead) How to supplement Google Keyword Planner
</h2> </h2>
<p className="mt-3 max-w-3xl text-sm leading-6 text-neutral-700"> <p className="mt-3 max-w-3xl text-sm leading-6 text-neutral-700">
Keyword Planner is an ads tool wearing an SEO costume: it buckets Keyword Planner is built for planning search-ad campaigns. Its
close variants into one number, rounds volumes into bands, and hides historical metrics combine close variants and report approximate
everything with no ad demand. That's fine for bidding. It's blinding monthly searches, so treat them as directional rather than a complete
for content strategy. content-discovery dataset.
</p> </p>
<p className="mt-3 max-w-3xl text-sm leading-6 text-neutral-700"> <p className="mt-3 max-w-3xl text-sm leading-6 text-neutral-700">
The strategies in this library replace it with three sources you can The strategies in this library supplement it with three sources you
inspect: your customers' language (strategy 01), Google's own question can inspect: your customers' language (strategy 01), Google's own
surfaces (strategy 02), and your Search Console reality (strategy 05). question surfaces (strategy 02), and your Search Console reality
Volume data still matters, but it's the <em>last</em> filter, not the (strategy 05). Volume data still matters, but it is most useful after
first. you understand the customer language and search intent.
</p> </p>
</section> </section>
<section className="mt-12"> <section className="mt-12">
<h2 className="text-2xl font-semibold tracking-tight text-neutral-950"> <h2 className="text-2xl font-semibold tracking-tight text-neutral-950">
Free keyword research tools for every strategy Free sources for keyword discovery
</h2> </h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]"> <p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--color-brand-muted)]">
Google's free surfaces (autocomplete, People Also Ask) plus your own Google's free surfaces (autocomplete, People Also Ask) plus your own
Search Console do the discovery. You can run each workflow in{" "} Search Console support the discovery. You can run the data-backed
parts of these workflows with{" "}
<a <a
href="/features/keyword-research" href="/features/keyword-research"
className="font-medium text-neutral-950 underline decoration-[var(--color-brand-accent)] underline-offset-4" className="font-medium text-neutral-950 underline decoration-[var(--color-brand-accent)] underline-offset-4"
> >
OpenSEO's keyword research OpenSEO's keyword research
</a> </a>{" "}
, connected to your live Search Console. Open source, self-hostable, and your connected Search Console. OpenSEO is open source and
and scriptable through the{" "} self-hostable, and its{" "}
<a <a
href="/docs/mcp" href="/docs/mcp"
className="font-medium text-neutral-950 underline decoration-[var(--color-brand-accent)] underline-offset-4" className="font-medium text-neutral-950 underline decoration-[var(--color-brand-accent)] underline-offset-4"
> >
MCP MCP
</a>{" "} </a>{" "}
so your AI assistant can run the whole workflow. Quality SEO data is lets a compatible AI assistant query both sources while it works
why the big suites run $100/month and up; OpenSEO starts at $10/month, through the workflow. Quality SEO data is why the big suites run
and you can start for free. $100/month and up; OpenSEO's paid plan starts at $10/month and includes
$10 of usage, with top-ups available if you need more. You can start
for free.
</p> </p>
</section> </section>
@ -253,8 +205,8 @@ function KeywordResearchLibraryPage() {
The Keyword Research Playbook The Keyword Research Playbook
</h2> </h2>
<p className="mt-2 max-w-xl text-sm leading-6 text-[var(--color-brand-muted)]"> <p className="mt-2 max-w-xl text-sm leading-6 text-[var(--color-brand-muted)]">
The strategies as a working PDF: checklists, the MCP prompts, and Eight interview-backed strategies and a seed-to-brief checklist in a
the keyword-map template. Ungated. working PDF. Ungated.
</p> </p>
</div> </div>
<a <a
@ -273,6 +225,11 @@ function KeywordResearchLibraryPage() {
// eslint-disable-next-line react/no-danger // eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
/> />
<script
type="application/ld+json"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }}
/>
</article> </article>
); );
} }

View File

@ -6,6 +6,8 @@ import Content, {
import { LibrarySpokePage } from "@/components/library-page"; import { LibrarySpokePage } from "@/components/library-page";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const PATH = "/library/keyword-research/intent-beyond-google";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_marketing/library/keyword-research/intent-beyond-google", "/_marketing/library/keyword-research/intent-beyond-google",
)({ )({
@ -13,7 +15,7 @@ export const Route = createFileRoute(
buildPageSeo({ buildPageSeo({
title: "Keyword Research Beyond Google: Pinterest, LinkedIn and AI", title: "Keyword Research Beyond Google: Pinterest, LinkedIn and AI",
description: frontmatter.description, description: frontmatter.description,
path: "/library/keyword-research/intent-beyond-google", path: PATH,
titleSuffix: "OpenSEO Library", titleSuffix: "OpenSEO Library",
ogType: "article", ogType: "article",
}), }),
@ -22,6 +24,7 @@ export const Route = createFileRoute(
title={frontmatter.title} title={frontmatter.title}
description={frontmatter.description} description={frontmatter.description}
crumb="Intent beyond Google" crumb="Intent beyond Google"
path={PATH}
> >
<Content components={{ ...defaultMdxComponents }} /> <Content components={{ ...defaultMdxComponents }} />
</LibrarySpokePage> </LibrarySpokePage>

View File

@ -6,6 +6,8 @@ import Content, {
import { LibrarySpokePage } from "@/components/library-page"; import { LibrarySpokePage } from "@/components/library-page";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const PATH = "/library/keyword-research/long-tail-question-mining";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_marketing/library/keyword-research/long-tail-question-mining", "/_marketing/library/keyword-research/long-tail-question-mining",
)({ )({
@ -13,7 +15,7 @@ export const Route = createFileRoute(
buildPageSeo({ buildPageSeo({
title: "What Are Long-Tail Keywords? How to Find and Use Them", title: "What Are Long-Tail Keywords? How to Find and Use Them",
description: frontmatter.description, description: frontmatter.description,
path: "/library/keyword-research/long-tail-question-mining", path: PATH,
titleSuffix: "OpenSEO Library", titleSuffix: "OpenSEO Library",
ogType: "article", ogType: "article",
}), }),
@ -22,6 +24,7 @@ export const Route = createFileRoute(
title={frontmatter.title} title={frontmatter.title}
description={frontmatter.description} description={frontmatter.description}
crumb="Long-tail & question mining" crumb="Long-tail & question mining"
path={PATH}
> >
<Content components={{ ...defaultMdxComponents }} /> <Content components={{ ...defaultMdxComponents }} />
</LibrarySpokePage> </LibrarySpokePage>

View File

@ -6,6 +6,8 @@ import Content, {
import { LibrarySpokePage } from "@/components/library-page"; import { LibrarySpokePage } from "@/components/library-page";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const PATH = "/library/keyword-research/opportunity-sizing-forecasting";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_marketing/library/keyword-research/opportunity-sizing-forecasting", "/_marketing/library/keyword-research/opportunity-sizing-forecasting",
)({ )({
@ -13,7 +15,7 @@ export const Route = createFileRoute(
buildPageSeo({ buildPageSeo({
title: "SEO Forecasting: Size a Keyword Opportunity Before You Build", title: "SEO Forecasting: Size a Keyword Opportunity Before You Build",
description: frontmatter.description, description: frontmatter.description,
path: "/library/keyword-research/opportunity-sizing-forecasting", path: PATH,
titleSuffix: "OpenSEO Library", titleSuffix: "OpenSEO Library",
ogType: "article", ogType: "article",
}), }),
@ -22,6 +24,7 @@ export const Route = createFileRoute(
title={frontmatter.title} title={frontmatter.title}
description={frontmatter.description} description={frontmatter.description}
crumb="Opportunity sizing & forecasting" crumb="Opportunity sizing & forecasting"
path={PATH}
> >
<Content components={{ ...defaultMdxComponents }} /> <Content components={{ ...defaultMdxComponents }} />
</LibrarySpokePage> </LibrarySpokePage>

View File

@ -6,6 +6,8 @@ import Content, {
import { LibrarySpokePage } from "@/components/library-page"; import { LibrarySpokePage } from "@/components/library-page";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const PATH = "/library/keyword-research/positioning-to-demand";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_marketing/library/keyword-research/positioning-to-demand", "/_marketing/library/keyword-research/positioning-to-demand",
)({ )({
@ -13,7 +15,7 @@ export const Route = createFileRoute(
buildPageSeo({ buildPageSeo({
title: "Does Your Positioning Have Search Demand Behind It?", title: "Does Your Positioning Have Search Demand Behind It?",
description: frontmatter.description, description: frontmatter.description,
path: "/library/keyword-research/positioning-to-demand", path: PATH,
titleSuffix: "OpenSEO Library", titleSuffix: "OpenSEO Library",
ogType: "article", ogType: "article",
}), }),
@ -22,6 +24,7 @@ export const Route = createFileRoute(
title={frontmatter.title} title={frontmatter.title}
description={frontmatter.description} description={frontmatter.description}
crumb="Map positioning to real demand" crumb="Map positioning to real demand"
path={PATH}
> >
<Content components={{ ...defaultMdxComponents }} /> <Content components={{ ...defaultMdxComponents }} />
</LibrarySpokePage> </LibrarySpokePage>

View File

@ -6,6 +6,8 @@ import Content, {
import { LibrarySpokePage } from "@/components/library-page"; import { LibrarySpokePage } from "@/components/library-page";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const PATH = "/library/keyword-research/search-intent-mapping";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_marketing/library/keyword-research/search-intent-mapping", "/_marketing/library/keyword-research/search-intent-mapping",
)({ )({
@ -13,7 +15,7 @@ export const Route = createFileRoute(
buildPageSeo({ buildPageSeo({
title: "What Is Search Intent? Mapping Keywords Hot, Warm, and Cold", title: "What Is Search Intent? Mapping Keywords Hot, Warm, and Cold",
description: frontmatter.description, description: frontmatter.description,
path: "/library/keyword-research/search-intent-mapping", path: PATH,
titleSuffix: "OpenSEO Library", titleSuffix: "OpenSEO Library",
ogType: "article", ogType: "article",
}), }),
@ -22,6 +24,7 @@ export const Route = createFileRoute(
title={frontmatter.title} title={frontmatter.title}
description={frontmatter.description} description={frontmatter.description}
crumb="Search-intent mapping" crumb="Search-intent mapping"
path={PATH}
> >
<Content components={{ ...defaultMdxComponents }} /> <Content components={{ ...defaultMdxComponents }} />
</LibrarySpokePage> </LibrarySpokePage>

View File

@ -6,6 +6,8 @@ import Content, {
import { LibrarySpokePage } from "@/components/library-page"; import { LibrarySpokePage } from "@/components/library-page";
import { buildPageSeo } from "@/lib/seo"; import { buildPageSeo } from "@/lib/seo";
const PATH = "/library/keyword-research/seed-from-conversation";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_marketing/library/keyword-research/seed-from-conversation", "/_marketing/library/keyword-research/seed-from-conversation",
)({ )({
@ -14,7 +16,7 @@ export const Route = createFileRoute(
title: title:
"Seed Keywords from Customer Conversations (Keyword Research Without a Paid Tool)", "Seed Keywords from Customer Conversations (Keyword Research Without a Paid Tool)",
description: frontmatter.description, description: frontmatter.description,
path: "/library/keyword-research/seed-from-conversation", path: PATH,
titleSuffix: "OpenSEO Library", titleSuffix: "OpenSEO Library",
ogType: "article", ogType: "article",
}), }),
@ -23,6 +25,7 @@ export const Route = createFileRoute(
title={frontmatter.title} title={frontmatter.title}
description={frontmatter.description} description={frontmatter.description}
crumb="Seed from conversation" crumb="Seed from conversation"
path={PATH}
> >
<Content components={{ ...defaultMdxComponents }} /> <Content components={{ ...defaultMdxComponents }} />
</LibrarySpokePage> </LibrarySpokePage>