Skip to main content

Command Palette

Search for a command to run...

Why Most JavaScript SEO Libraries Miss the Most Important Check — And What Actually Works

Updated
5 min read
Why Most JavaScript SEO Libraries Miss the Most Important Check — And What Actually Works
A
Next.js Developer | I write about JavaScript problems & real-world solutions | Helping devs debug faster.

You install next-seo. You configure your titles, meta descriptions, Open Graph tags. Lighthouse gives you a green score. You ship.

Three weeks later, Google Search Console shows your blog posts ranking for nothing. Not low — nothing. You dig into the page, and technically everything looks correct. The meta tags are there. The canonical is set. The robots directive says index, follow.

What's missing? Your focus keyphrase isn't in your H2 headings. It appears once in the body, buried in paragraph six. Your intro paragraph doesn't mention it at all. Google saw the page, understood the structure, and had no strong signal about what the content was actually about.

Most JavaScript SEO libraries check structure. Almost none check content relevance. That gap is where rankings quietly die — and it's what I'll show you how to catch before it ships.

What "Structural" SEO Checks Actually Cover

Libraries like seo-analyzer — which is genuinely well-built — run checks against your rendered HTML. Title length, image alt attributes, canonical link presence, meta description, Open Graph tags. These are real checks that matter.

Here's what a basic seo-analyzer setup looks like:

// scripts/audit.js
const SeoAnalyzer = require('seo-analyzer');

new SeoAnalyzer()
  .inputHTMLString(renderedHtml)
  .addRule('titleLengthRule', { min: 50, max: 60 })
  .addRule('imgTagWithAltAttributeRule')
  .addRule('metaBaseRule', { list: ['description'] })
  .addRule('canonicalLinkRule')
  .outputObject((results) => console.log(results))
  .run();

This catches real problems — a missing canonical, an image without alt text, a title that's 90 characters long. Run this in CI and you'll catch an entire class of structural bugs before they reach Google.

But run it on a post where your focus keyphrase appears zero times in the first paragraph, never in an H2, and is absent from the URL slug — and it passes. Every check green. The content is invisible to Google for your target keyword, and your audit tool has no idea.

The Check That Actually Moves Rankings

Keyphrase distribution is what Yoast SEO checks in WordPress. It's what content teams use to validate posts before publishing. And until recently, there was no equivalent for JavaScript developers working outside WordPress.

The check involves four things: does the focus keyphrase appear in the title, in the first paragraph, in at least one H2, and in the URL slug? Miss two of those and your page will rank for something — just not what you intended.

Here's what this looks like with @power-seo/content-analysis, which I've been using after hitting this exact problem on a Next.js blog:

// scripts/seo-gate.ts
import { analyzeContent } from '@power-seo/content-analysis';

const result = analyzeContent({
  title: 'Best Running Shoes for Beginners',
  metaDescription: 'Expert guide to running shoes — tested and reviewed.',
  focusKeyphrase: 'running shoes for beginners',
  content: htmlBodyString, // your rendered body HTML
  slug: 'running-shoes-for-beginners',
});

const failures = result.results.filter((r) => r.status === 'poor');
if (failures.length > 0) {
  failures.forEach((f) => console.error('✗', f.description));
  process.exit(1);
}
console.log(`SEO gate passed — Score: \({result.score}/\){result.maxScore}`);

The output tells you exactly what's wrong:

✗ Focus keyphrase not found in introduction paragraph
✗ Focus keyphrase missing from H2 headings
SEO gate passed — Score: 38/55

This runs in about 2ms per page — fast enough to run on every keystroke in a CMS editor, or as a pre-merge GitHub Actions job. No DOM dependencies, safe in Next.js Server Components and Edge Runtime.

Putting a Gate in CI

The real value isn't running this manually — it's blocking a PR when content doesn't meet the bar. Here's the GitHub Actions setup:

```yaml
name: SEO Content Gate
on: [pull_request]
jobs:
  seo-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx tsx scripts/seo-gate.ts posts/my-new-post.mdx
```

When the keyphrase check fails, the merge is blocked. The post doesn't ship until the content actually signals what it's about. That's the same enforcement mechanism Yoast gives WordPress teams — now available in a TypeScript CI pipeline.

For structural audits after build, seo-analyzer still wins — its inputFolders() method scans an entire /public directory with one CLI command. The two tools cover different layers. Many teams run both: @power-seo/content-analysis pre-merge for keyphrase scoring, seo-analyzer post-build for structural checks.

What I Learned

  1. Structural SEO checks and content relevance checks are different problems. A library that passes your canonical and alt text checks can still let posts ship with zero keyphrase signal. Both layers need covering.

  2. The keyphrase gap is where rankings actually die. Title, meta, canonical — Google expects those. Keyphrase distribution in headings and the intro paragraph is what tells Google what the page is about.

  3. CI gates work for content quality the same way they work for code quality. A failing score blocks the merge. The problem never reaches production.

  4. Pick tools based on when in the pipeline they run. Pre-build content scoring and post-build structural audits serve different purposes — don't expect one tool to do both well.

If you want to try the keyphrase gate approach, the full Power SEO monorepo is here: Power SEO