🪟 Windows TippsID.3 GTI: VW stellt den stärksten Serien-GTI aller Zeiten vor(16.09.2026 um 10:00 Uhr)
🪟 Windows TippsDoppelte Power: HMX 6 mit 2x RTX 5090 von ZOTAC GAMING(16.09.2026 um 10:25 Uhr)
🪟 Windows TippsAnthbot N8 im Test: Mähroboter mit Fangkorb für Gras und Laub(16.09.2026 um 10:30 Uhr)
🪟 Windows TippsGenesung, Erholung, Entspannung – Aufgaben der Beleuchtung(16.09.2026 um 10:30 Uhr)
🪟 Windows TippsID.3 GTI: VW stellt den stärksten Serien-GTI aller Zeiten vor(16.09.2026 um 10:00 Uhr)
🪟 Windows TippsDoppelte Power: HMX 6 mit 2x RTX 5090 von ZOTAC GAMING(16.09.2026 um 10:25 Uhr)
🪟 Windows TippsAnthbot N8 im Test: Mähroboter mit Fangkorb für Gras und Laub(16.09.2026 um 10:30 Uhr)
🪟 Windows TippsGenesung, Erholung, Entspannung – Aufgaben der Beleuchtung(16.09.2026 um 10:30 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 16 Min Lesezeit
0

GitHub Actions SEO: gate PRs on broken links and schema

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Originally published on , so the check below is worth adapting rather than skipping if you are on Astro instead.



The page renders without error, silently sending its ranking signal to the wrong URL.






Malformed JSON-LD that silently forfeits rich-result eligibility



Nestlé measured that pages appearing as rich results in Google Search have an 82% higher click-through rate than non-rich-result pages, a figure cited in measured 58 clicks per 100 queries for rich results against 41 for standard results. A single malformed property in the JSON-LD block, a date string in the wrong format, or a missing required field silently disqualifies the page from rich-result consideration. The structured data is rendered in the HTML; it just does not validate.



Lighthouse runs wraps lychee, a link checker written in Rust. The lychee project benchmarks it at 576 links in about 60 seconds on the analysis-tools-dev/static-analysis repository; throughput varies by repo size and link distribution, but most blogs with a few dozen posts complete in well under two minutes. It reads Markdown files directly and does not require a running server, so it can complete before any build step.




CODE
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






fail: true exits with a non-zero code on any broken link, which fails the job. jobSummary: true writes the full report to the GitHub Actions job summary, accessible from the PR's check status.



Add a .lycheeignore at the repo root for URLs to exclude, one regex per line:




CODE
# Localhost references in code blocks
http://localhost
# Web archive links
https://web.archive.org









Job 2: meta, canonical, and OG tags - parse built HTML after next build



There is no off-the-shelf action for meta-tag validation on a Next.js App Router site, so this job builds the site and runs a short Node script against the HTML output. The script checks each page for a <meta name="description">, a <link rel="canonical"> that matches the page's own URL, and basic Open Graph tags.



After validating, the job uploads the build as an artifact. The JSON-LD and Lighthouse jobs download it instead of rebuilding, so all three validate the same output and CI time does not multiply with each additional check:




CODE
  meta-tags:
name: Meta and canonical tags
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'

- run: npm ci

- name: Cache Next.js build
uses: actions/cache@v6
with:
path: .next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}

- name: Build
run: npx next build
env:
NODE_ENV: production

- name: Check meta and canonical tags
run: node scripts/check-meta.mjs

- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: next-build
path: |
.next/
public/
retention-days: 1






Setting process.exitCode = 1 instead of calling process.exit(1) immediately lets the script report every failure across all pages in a single run rather than stopping at the first hit. Create scripts/check-meta.mjs in your repo:




CODE
// 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;
}

async function checkPage(htmlPath) {
const slug = htmlPath.replace(BLOG_DIR + '/', '').replace('/page.html', '');
const html = await readFile(htmlPath, 'utf8');
const expectedUrl = `${SITE_URL}/blog/${slug}/`;
let ok = true;

const description =
html.match(/<meta[^>]+name="description"[^>]+content="([^"]+)"/i)?.[1] ??
html.match(/<meta[^>]+content="([^"]+)"[^>]+name="description"/i)?.[1] ??
null;

if (!description) {
console.error(`[FAIL] Missing meta description: /blog/${slug}/`);
process.exitCode = 1;
ok = false;
}

const canonical =
html.match(/<link[^>]+rel="canonical"[^>]+href="([^"]+)"/i)?.[1] ??
html.match(/<link[^>]+href="([^"]+)"[^>]+rel="canonical"/i)?.[1] ??
null;

if (!canonical || canonical !== expectedUrl) {
console.error(`[FAIL] Canonical mismatch: /blog/${slug}/`);
console.error(` Expected: ${expectedUrl}`);
console.error(` Found: ${canonical ?? 'missing'}`);
process.exitCode = 1;
ok = false;
}

const ogTitle =
html.match(/<meta[^>]+property="og:title"[^>]+content="([^"]+)"/i)?.[1] ??
html.match(/<meta[^>]+content="([^"]+)"[^>]+property="og:title"/i)?.[1] ??
null;

if (!ogTitle) {
console.error(`[FAIL] Missing og:title: /blog/${slug}/`);
process.exitCode = 1;
ok = false;
}

if (ok) console.log(`[OK] /blog/${slug}/`);
}

const files = await walk(BLOG_DIR).catch(() => []);

if (files.length === 0) {
console.error('[FAIL] No HTML found in .next/server/app/blog - run next build first');
process.exitCode = 1;
} else {
await Promise.all(files.map(checkPage));
}






walk recurses the App Router build directory and collects every page.html file. Next.js 15 App Router writes pre-rendered pages to .next/server/app/blog/<slug>/page.html, so the slug is extracted directly from the path. checkPage reads each file, runs all three checks without short-circuiting, and logs every failure before the process exits. Set SITE_URL via the environment (or hardcode your domain) to match the canonical your generateMetadata produces.






Job 3: JSON-LD linting - schemar posts pass/fail results as a sticky PR comment



. Passing that array straight to message posts raw JSON on the PR. The actions/github-script step in between maps each result to a one-line pass/fail row before it reaches the sticky comment, which is the same shape johnnyreilly's own writeup of the action uses for its PR comments.






Job 4: Lighthouse budget - serve the build locally, assert on LCP, CLS, and INP



.



Without this step, the entire setup is advisory: the checks run and report, but nothing actually blocks the merge. This is the step most workflow tutorials omit.






Surfacing failures inline with sticky PR comments



The Schemar job's sticky comment puts JSON-LD results directly on the PR without navigating to the Actions run page. For the other three jobs, the GitHub job summary (via jobSummary: true on lychee, and console output on the meta-tag script) provides the detailed report accessible from each check status link.



Make the meta-tag script output specific enough to act on immediately:




CODE
[FAIL] Missing meta description: /blog/new-post-slug/
[FAIL] Canonical mismatch: /blog/new-post-slug/
Expected: https://yoursite.com/blog/new-post-slug/
Found: https://yoursite.com/blog/









Keeping it zero-maintenance: .lycheeignore, pinned action versions, and caching the build



Three habits prevent the workflow from becoming a source of noise.




  • Pin action versions to major version tags (@v2, @v7, @v12). Moving tags like @latest break without warning when upstream ships a breaking change. Check release pages when onboarding a new action; marocchino/sticky-pull-request-comment is at v3, for example.

  • Share the build output. The cache step in the meta-tags job preserves .next/cache between workflow runs, and the artifact upload carries the final output to the JSON-LD and Lighthouse jobs - one build per PR, three jobs consuming it.

  • Keep .lycheeignore current. As the blog grows, more code-block URLs and archived-page references need exclusion. A stale file generates false failures that train the team to dismiss CI output; update it when adding an exclusion-worthy URL.






Where the green check ends and editorial judgment begins






What four passing jobs actually confirm - and what they cannot



A green run confirms:




  • No external link in the PR's Markdown files returns a 4xx or 5xx response

  • Every generated page has a meta description, a self-referencing canonical, and Open Graph tags

  • The structured data on the new post validates against Schema.org

  • The new post clears Core Web Vitals thresholds under lab conditions



What it does not confirm: whether the facts are correct, whether the post answers the question it sets up, or whether the prose is worth reading. CI has no opinion about those things.



This is the same division that makes , CI gates the technical surface, and human review handles editorial judgment. All three pass before the post ships.



For teams using a also benefits directly: the broken-link job confirms that any new cross-links added to a post actually resolve before they ship.






I'm building Lyra, an autonomous blog writer that writes in your blog's voice, fact-checks every claim, and opens a pull request you review. This post comes from her blog, where we publish what we learn running the pipeline. Happy to answer questions in the comments.

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
‘Pacing’ won’t eliminate the risk of AI doom. Here’s what could | David Krueger
1 Quelle
The Liftoff Scenario That Terrifies A.I. Doomsayers
1 Quelle
How to Use AI to Plan a Trip: Better Prompts for Travel Recommendations
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten GitHub Actions SEO: gate PRs on broken links and schema

Thematisch verwandte Begriffe: GitHub, Actions, gate, broken · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...