feat(library): add FAQPage JSON-LD to the eight keyword-research spokes (#240)
This commit is contained in:
parent
56826f84d1
commit
d2bfc256dc
@ -8,6 +8,34 @@ import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const PATH = "/library/keyword-research/cluster-topical-hubs";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What's the best keyword clustering tool?",
|
||||
answer:
|
||||
"For SERP-overlap clustering at scale, paid tools exist, but for most sites, OpenSEO's research + an intent-grouping pass (the MCP prompt above) covers it. Judge tools by whether they cluster on SERP overlap; word-similarity clustering is a toy.",
|
||||
},
|
||||
{
|
||||
question: "Is there a free keyword clustering tool?",
|
||||
answer:
|
||||
"Not an unlimited one. The grouping step itself is free (the MCP prompt above does it), but it runs on researched keywords, and quality keyword data is the part that costs money everywhere. OpenSEO includes the clustering pass with research, so there's no separate clustering tool to buy; you can start for free, and paid plans start at $10/month.",
|
||||
},
|
||||
{
|
||||
question: "What is a keyword mapping template?",
|
||||
answer:
|
||||
"A sheet with one row per cluster: primary keyword, supporting keywords, intent, target URL, status. The keyword map above is the working example; copy the structure. Add a forecast column and it becomes a build order: size each cluster before you commit the quarter.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
||||
})),
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_marketing/library/keyword-research/cluster-topical-hubs",
|
||||
)({
|
||||
@ -21,6 +49,7 @@ export const Route = createFileRoute(
|
||||
ogType: "article",
|
||||
}),
|
||||
component: () => (
|
||||
<>
|
||||
<LibrarySpokePage
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description}
|
||||
@ -29,5 +58,11 @@ export const Route = createFileRoute(
|
||||
>
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</LibrarySpokePage>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@ -8,6 +8,39 @@ import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const PATH = "/library/keyword-research/gsc-programmatic-discovery";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "Can you use Google Search Console for keyword research?",
|
||||
answer:
|
||||
"Yes, and it is the most reliable source you have, because it reports queries that reached your site rather than estimating a market. Its limits are that it only shows terms you already rank for, and it hides queries below a privacy threshold. Use it for expansion and validation, and use a keyword tool for the demand you have not captured yet.",
|
||||
},
|
||||
{
|
||||
question: "What are striking-distance keywords?",
|
||||
answer:
|
||||
"Queries where you rank roughly between positions 11 and 30. They sit on page two, earn few clicks, and demonstrate that Google already treats your page as a plausible answer. They are the cheapest rankings to improve on most sites.",
|
||||
},
|
||||
{
|
||||
question: "How far back does Google Search Console data go?",
|
||||
answer:
|
||||
"Sixteen months. Most exports default to a far shorter window, so pulling the full range surfaces seasonal queries and historical rankings that a 90-day view hides.",
|
||||
},
|
||||
{
|
||||
question: "Why don't the clicks in the query table add up to the total?",
|
||||
answer:
|
||||
"Google anonymizes queries that too few people searched, so they never appear by name while their clicks still count in the total. The shortfall runs from a small fraction to most of the total depending on the size of the site. Reading the same pages at page level rather than query level recovers much of the count.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
||||
})),
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_marketing/library/keyword-research/gsc-programmatic-discovery",
|
||||
)({
|
||||
@ -20,6 +53,7 @@ export const Route = createFileRoute(
|
||||
ogType: "article",
|
||||
}),
|
||||
component: () => (
|
||||
<>
|
||||
<LibrarySpokePage
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description}
|
||||
@ -28,5 +62,11 @@ export const Route = createFileRoute(
|
||||
>
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</LibrarySpokePage>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@ -8,6 +8,40 @@ import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const PATH = "/library/keyword-research/intent-beyond-google";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "How do you do keyword research for Pinterest?",
|
||||
answer:
|
||||
"Use the platform's own search box and its guided-search suggestions, then place those terms in pin titles, pin descriptions, the text on the image, and your board and profile copy. Pinterest has to understand the account and the pin before it can match either to a query, so both levels need the vocabulary.",
|
||||
},
|
||||
{
|
||||
question: "Does SEO work for LinkedIn profiles?",
|
||||
answer:
|
||||
"Yes, and the profile matters more than the posting. The most frequent action on the platform is viewing a profile, so the headline and about section carry the weight a title tag carries on a website. Write them in the words a stranger would use for the problem you solve.",
|
||||
},
|
||||
{
|
||||
question: "How do you optimize for AI assistants?",
|
||||
answer:
|
||||
"Answer questions directly and completely enough to be quotable, and accept that queries an assistant can satisfy in a paragraph will return fewer clicks regardless of your rank. Weight your research toward queries needing a tool, a price, a login or a person, because those still send a visit.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"How do you measure traffic from platforms that send no referral data?",
|
||||
answer:
|
||||
"Watch branded search. Discovery on a closed platform tends to surface later as people searching your name, so a rising branded line in Search Console during a campaign is a useful directional signal.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
||||
})),
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_marketing/library/keyword-research/intent-beyond-google",
|
||||
)({
|
||||
@ -20,6 +54,7 @@ export const Route = createFileRoute(
|
||||
ogType: "article",
|
||||
}),
|
||||
component: () => (
|
||||
<>
|
||||
<LibrarySpokePage
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description}
|
||||
@ -28,5 +63,11 @@ export const Route = createFileRoute(
|
||||
>
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</LibrarySpokePage>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@ -8,6 +8,34 @@ import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const PATH = "/library/keyword-research/long-tail-question-mining";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What are long-tail keywords in SEO?",
|
||||
answer:
|
||||
"Specific multi-word queries with lower individual volume but higher combined traffic and clearer intent than head terms. They're the fastest way for a newer site to rank, because competition concentrates on head terms.",
|
||||
},
|
||||
{
|
||||
question: "How do I use long-tail keywords in content?",
|
||||
answer:
|
||||
"One intent per page. Make the long-tail query the H2 (or H1) verbatim where natural, answer it in the first paragraph, then earn depth below. Don't scatter twenty tails across one page; cluster related tails, then split by intent.",
|
||||
},
|
||||
{
|
||||
question: "Is there a free long-tail keyword generator?",
|
||||
answer:
|
||||
"Google gives you two: autocomplete and People Also Ask. Your Search Console is the third and best; it's your site's actual tail. OpenSEO connects your Search Console and expands what you find into full keyword lists. You can start for free; paid plans start at $10/month.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
||||
})),
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_marketing/library/keyword-research/long-tail-question-mining",
|
||||
)({
|
||||
@ -20,6 +48,7 @@ export const Route = createFileRoute(
|
||||
ogType: "article",
|
||||
}),
|
||||
component: () => (
|
||||
<>
|
||||
<LibrarySpokePage
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description}
|
||||
@ -28,5 +57,11 @@ export const Route = createFileRoute(
|
||||
>
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</LibrarySpokePage>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@ -8,6 +8,39 @@ import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const PATH = "/library/keyword-research/opportunity-sizing-forecasting";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "How accurate is SEO forecasting?",
|
||||
answer:
|
||||
"Directionally useful and precisely wrong, which is why the output should be a range. Volume figures are estimates, CTR curves are averages across wildly different SERPs, and ranking timelines depend on competitors who are also working. Forecast to compare opportunities against each other, not to promise a number.",
|
||||
},
|
||||
{
|
||||
question: "What is a good SEO ROI?",
|
||||
answer:
|
||||
"It depends on conversion value and payback window rather than a benchmark. The useful calculation is cost of the content and links against forecast revenue over 12 months, and whether that beats what the same budget would return in paid. SEO usually loses that comparison in month one and wins it by month nine, so the timeline matters as much as the multiple.",
|
||||
},
|
||||
{
|
||||
question: "Which SEO KPIs matter?",
|
||||
answer:
|
||||
"Leads and revenue are the two numbers a business treats as exact. Everything else, including rankings, sessions and impressions, is diagnostic: useful for explaining why the exact numbers moved, weak as a target in its own right.",
|
||||
},
|
||||
{
|
||||
question: "How do you calculate potential traffic from keywords?",
|
||||
answer:
|
||||
"Combined cluster volume, times the CTR for your expected position, gives estimated sessions. Multiply by conversion rate and conversion value for revenue. Run it three times at three positions so you finish with a range rather than a single figure nobody should trust.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
||||
})),
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_marketing/library/keyword-research/opportunity-sizing-forecasting",
|
||||
)({
|
||||
@ -20,6 +53,7 @@ export const Route = createFileRoute(
|
||||
ogType: "article",
|
||||
}),
|
||||
component: () => (
|
||||
<>
|
||||
<LibrarySpokePage
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description}
|
||||
@ -28,5 +62,11 @@ export const Route = createFileRoute(
|
||||
>
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</LibrarySpokePage>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@ -8,6 +8,34 @@ import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const PATH = "/library/keyword-research/positioning-to-demand";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "Should I invent a name for my category?",
|
||||
answer:
|
||||
"Only with a plan for the demand gap. An invented term has no search behind it on day one and may never earn any, so anything a stranger needs to find has to be findable through the vocabulary that already exists. Keep the invented term for the pitch, where you have someone's attention.",
|
||||
},
|
||||
{
|
||||
question: "What if my keyword has no search volume?",
|
||||
answer:
|
||||
"Treat it as a signal about the market rather than a limitation of the tool. Zero volume for a category term usually means people describe the problem differently, and the related terms that do have volume will show you how. Low volume is different from zero, and can be worth owning when the intent is strong.",
|
||||
},
|
||||
{
|
||||
question: "How do I find the words my customers use?",
|
||||
answer:
|
||||
"Sales calls, support tickets, and recorded interviews, which is the same source that produces good seed keywords. People describe problems in language that no keyword database originates, because the database only knows what has already been typed enough times to register.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
||||
})),
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_marketing/library/keyword-research/positioning-to-demand",
|
||||
)({
|
||||
@ -20,6 +48,7 @@ export const Route = createFileRoute(
|
||||
ogType: "article",
|
||||
}),
|
||||
component: () => (
|
||||
<>
|
||||
<LibrarySpokePage
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description}
|
||||
@ -28,5 +57,11 @@ export const Route = createFileRoute(
|
||||
>
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</LibrarySpokePage>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@ -8,6 +8,34 @@ import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const PATH = "/library/keyword-research/search-intent-mapping";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "Why is search intent important for SEO?",
|
||||
answer:
|
||||
"Because Google ranks pages that satisfy intent, not pages that mention keywords. A perfectly optimized page against the wrong intent can't win. Read the SERP and you'll see the intent Google has decided the query carries.",
|
||||
},
|
||||
{
|
||||
question: "What are buyer intent keywords?",
|
||||
answer:
|
||||
'Queries that signal purchase readiness: "pricing", "vs", "alternative", "best X for Y", "discount". They\'re low volume and high competition per click, but still usually your best ROI, because the searcher arrives pre-sold.',
|
||||
},
|
||||
{
|
||||
question: "How do I check the search intent of a keyword?",
|
||||
answer:
|
||||
"Search it. The current top 10 is Google's answer: if it's all listicles, the intent is commercial comparison; all docs and definitions, informational. OpenSEO also auto-labels intent on researched keywords in most countries.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
||||
})),
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_marketing/library/keyword-research/search-intent-mapping",
|
||||
)({
|
||||
@ -20,6 +48,7 @@ export const Route = createFileRoute(
|
||||
ogType: "article",
|
||||
}),
|
||||
component: () => (
|
||||
<>
|
||||
<LibrarySpokePage
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description}
|
||||
@ -28,5 +57,11 @@ export const Route = createFileRoute(
|
||||
>
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</LibrarySpokePage>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@ -8,6 +8,34 @@ import { buildPageSeo } from "@/lib/seo";
|
||||
|
||||
const PATH = "/library/keyword-research/seed-from-conversation";
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "How do I do keyword research for free?",
|
||||
answer:
|
||||
"Conversations for seeds (this page), Google autocomplete + People Also Ask for expansion, Search Console for validation. OpenSEO validates and expands what those surface; you can start for free, and paid plans start at $10/month.",
|
||||
},
|
||||
{
|
||||
question: "How do I find LSI keywords?",
|
||||
answer:
|
||||
'"LSI keywords" is tool-industry vocabulary for related phrasings. The fastest free sources are the People Also Ask box and the "related searches" footer. Better still: your customers\' own synonyms, which is exactly what conversation seeding harvests.',
|
||||
},
|
||||
{
|
||||
question: "How many seed keywords do I need?",
|
||||
answer:
|
||||
"5–15 strong seeds per topic. Past that you're expanding, not seeding. Feed them into the long-tail mining strategy next. The same customer vocabulary is worth pointing at your own positioning: see whether anyone searches for what you call yourself.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
"@type": "Question",
|
||||
name: faq.question,
|
||||
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
||||
})),
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_marketing/library/keyword-research/seed-from-conversation",
|
||||
)({
|
||||
@ -21,6 +49,7 @@ export const Route = createFileRoute(
|
||||
ogType: "article",
|
||||
}),
|
||||
component: () => (
|
||||
<>
|
||||
<LibrarySpokePage
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description}
|
||||
@ -29,5 +58,11 @@ export const Route = createFileRoute(
|
||||
>
|
||||
<Content components={{ ...defaultMdxComponents }} />
|
||||
</LibrarySpokePage>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqLd) }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user