Skip to main content

On This Page

GitHub Actions SEO: How to Gate PRs on Broken Links and Schema Validation

3 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

The fix is a GitHub Actions SEO workflow that gates every blog PR automatically. Four jobs check broken links, meta and canonical correctness, JSON-LD validity, and a Lighthouse performance budget; the merge button stays red until all four pass.

Why This Matters

Code review catches logic bugs but not SEO bugs—a broken canonical doesn’t throw a build error, a dead external link doesn’t fail a type check, and malformed JSON-LD doesn’t appear in a diff. These issues ship quietly and surface weeks later in Search Console. Automated CI gates prevent silent ranking degradation by enforcing technical correctness before merging.

Key Insights

    • Broken links rot silently: External links that resolve during editorial review may 404 by the time a post ships; lychee-action scans Markdown files directly without building the site (576 links in ~60 seconds per benchmarks).
    • Canonical mismatches split ranking signals: In Next.js App Router sites, pages can inherit canonicals from parent layouts (e.g., /blog/ instead of /blog/your-post-slug/), sending ranking signals to the wrong URL without any visible error.
    • JSON-LD validation is separate from Lighthouse: Lighthouse runs ~8 SEO audits per page but never validates structured data content; Schemar wraps the Schema Markup Validator to catch malformed properties or missing required fields that forfeit rich-result eligibility.
    • Rich results boost CTR significantly: Nestlé measured 82% higher click-through rate for rich-result pages vs non-rich-result pages; Milestone Internet study of 4.5 million queries found 58 clicks per 100 queries for rich results vs 41 for standard results.

Working Examples

# .github/workflows/blog-seo.yml
name: Blog SEO checks
on:
  pull_request:
    paths:
      - 'content/blog/**'
      - '.github/workflows/blog-seo.yml'

jobs:
  broken-links:
    name: Broken links
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Check links
        uses: lycheeverse/lychee-action@v2
        with:
          args: --verbose --no-progress 'content/blog/**/*.md'
          fail: true
          jobSummary: true
# scripts/check-meta.mjs
import { readdir, readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';

const SITE_URL = process.env.SITE_URL ?? 'https://yoursite.com';
const BLOG_DIR = resolve('.next/server/app/blog');

async function walk(dir) {
  const entries = await readdir(dir, { withFileTypes: true });
  const files = [];
  for (const entry of entries) {
    const full = join(dir, entry.name);
    if (entry.isDirectory()) {
      files.push(...await walk(full));
    } else if (entry.name === 'page.html') {
      files.push(full);
    }
  }
 return files;
b}
async function checkPage(htmlPath) {
bconst slug = htmlPath.replace(BLOG_DIR + '/', '').replace('/page.html', '');
bconst html = await readFile(htmlPath, 'utf8');
bconst expectedUrl = `${SITE_URL}/blog/${slug}/`;
blet ok = true;
bconst description =
bhtml.match(/<meta[^>]+name="description"[^>]+content="([^"]+)"/i)?.[1] ??bhtml.match(/<meta[^>]+content="([^"]+)"[^>]+name="description"/i)?.[1] ??bnull;if (!description) {console.error(`[FAIL] Missing meta description: /blog/${slug}/`);process.exitCode = b1;ok = false;}// ... canonical and og:title checks omitted for brevity ...}

Practical Applications

  • [Use case] Lyra’s autonomous blog writer uses this CI gate—agent drafts fast with fact-checking before opening the PR—then human review handles voice and accuracy. Pitfall : Skipping branch protection rules after setting up checks makes everything advisory; without requiring status checks to pass before merging ,the merge button stays active even when jobs fail .

References:

Continue reading

Next article

Building Your First AWS Network with Terraform: 4 Failures That Teach More Than Success

Related Content