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:
parent
bd402844fa
commit
a1ed6ece4b
@ -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",
|
||||||
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
55
web/src/lib/strategy-libraries.ts
Normal file
55
web/src/lib/strategy-libraries.ts
Normal 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",
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
87
web/src/routes/_marketing/library/index.tsx
Normal file
87
web/src/routes/_marketing/library/index.tsx
Normal 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">→</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">→</span>
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-[var(--color-brand-muted)]">
|
||||||
|
{strategy.description}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
Strategy Library
|
aria-label="Breadcrumb"
|
||||||
</p>
|
className="text-sm text-[var(--color-brand-muted)]"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/library"
|
||||||
|
className="font-medium text-[var(--color-brand-accent)]"
|
||||||
|
>
|
||||||
|
Strategy Library
|
||||||
|
</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)]"
|
>
|
||||||
>
|
→
|
||||||
→
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user