DEV Community

Cover image for Four content QC scripts I run around directory deployments
MORINAGA
MORINAGA

Posted on

Four content QC scripts I run around directory deployments

The first AdSense rejection told me something uncomfortable: the quality checks I'd built were catching article-level problems but missing directory-level ones. Entries with near-identical sentences. Monetization blocks that never rendered in production because an env var hadn't been deployed. Structured data that was simply absent from pages I'd assumed had it. Each failure is fixable in isolation — the problem was I had no systematic check running before content shipped.

I added four scripts. Each catches a different class of problem. Three of them I invoke by hand — two around pushes that touch apps/*/src/data/ or content/articles/, and one after a deploy, because it inspects production HTML rather than the build. Three are pnpm scripts in the root package.json (audit:articles, audit:jsonld, affiliates:check) and the humanization linter I invoke with node directly. Only the JSON-LD audit is wired into a workflow at all, and there it runs after publishing with its failure explicitly swallowed. Calling any of this a gate would be generous.

audit-articles.mjs — frontmatter and article quality

The article auditor checks frontmatter completeness (title, description, tags, publish_to are required fields), word count (500 minimum as an error, a warning past 3,500), cliché phrases, and tag policy violations. The seo tag is prohibited and records an error — but whether that error is fatal depends on how the script is called. A bare repo-wide run prints the errors and still exits 0, so historical articles don't block anything. Running it in explicit path mode — node scripts/audit-articles.mjs content/articles/91-*.md — or with --strict makes errors fatal: it still prints every error and a summary, then exits 1.

The cliché list started with the obvious openers and superlatives that appear in virtually every AI-generated article introduction — excited preambles, urgency framings, breathless superlatives — and grew as I noticed additional patterns in AI-assisted drafts. It's now 13 phrases. The auditor flags them with line numbers so I can fix them in the draft without hunting.

lint-humanization.mjs — directory entry diversity

The humanization linter runs across all three data files: apps/ai-tools/src/data/models.json, apps/indie-games/src/data/games.json, and apps/oss-alternatives/src/data/saas.json. It counts how many times each phrase from a residue list appears across the whole dataset and fails if any phrase exceeds its threshold.

The most aggressive rules target specific verbatim sentences that appeared across dozens of model entries after the initial ETL run — these are set to max: 0 and must never appear in the final dataset. Other phrases get higher thresholds: "vendor lock-in" is allowed up to 24 times because it's a genuine descriptor that recurs naturally at scale, while "plays like" is limited to 12 because it's a game description pattern that can cluster.

The --strict flag doesn't touch the thresholds. All it does is make accumulated warnings fatal, so a run that would otherwise report and exit 0 exits 1 instead. I don't use --strict routinely — it fails on repetition I've decided to live with — but I run it before major content refreshes to see the phrase distribution across the whole dataset.

check-affiliates.mjs — affiliate CTAs that never rendered

The name oversells it. This one doesn't crawl outbound links at all — it's a deployment diagnostic. For each of the three sites it fetches ads.txt and checks a publisher ID is present, pulls the first detail page it can find in the sitemap, and then greps that page's HTML for three things: the affiliate CTA section ("Run this model on", "Find on other stores", "Self-host on"), an AdSense slot, and the Amazon block.

That's the failure mode it's built for. Every one of those blocks is gated behind an environment variable, so a PUBLIC_AMAZON_TAG that never made it into Cloudflare Pages means the block silently disappears from every page and nothing in the build complains. pnpm affiliates:check tells me in a few seconds whether the monetization markup actually survived the trip to production.

What it does not do is verify that the destination URLs still resolve. A partner page that restructured or a program that quietly closed would go unnoticed. That's the obvious next script to write, and I haven't written it.

audit-jsonld.mjs — structured data validity

The JSON-LD auditor works against the deployed sites, not the build output. For each of the three hosts it fetches the homepage plus the first couple of detail pages listed in the sitemap, extracts every <script type="application/ld+json"> block (flattening @graph where present), and checks two things: that each block parses as JSON, and that the @type values that page is supposed to carry are actually there — WebSite on a homepage, SoftwareApplication and BreadcrumbList on a model page, ItemList and BreadcrumbList on an alternatives page. It doesn't validate against the full Schema.org spec, and it doesn't inspect the contents of a block beyond @type, so an empty name or a missing @context would sail through. What it does catch is the block that fails to parse and the page type that's silently lost its schema.

That second case is why I wrote it. The first run found that all three homepages were missing their WebSite schema entirely — nothing had ever emitted it, no build step complained, and I'd assumed it was there. Adding the schema to each Base.astro was a five-minute fix; noticing it had been missing was the hard part, and that's the job I now delegate to a script.

The pattern that emerged

Each script covers a different failure domain:

Script What it checks When I run it
audit-articles.mjs Article metadata and prose quality By hand, on the articles I'm about to publish
lint-humanization.mjs Corpus-level phrase clustering By hand, after a content refresh
check-affiliates.mjs Monetization blocks present in production By hand, after a deploy
audit-jsonld.mjs Structured data types on live pages After the publish workflow, failure tolerated

None of them overlap, and between them they've caught real things — missing homepage schema, cliché phrases in drafts, monetization blocks that never made it into the deployed HTML, entries that had escaped the content quality gate checks. Whether any of those would have triggered an additional AdSense rejection or a drop in rich result coverage I don't know.

The honest summary is that the "when I run it" column is the weak part. Writing the checks was the easy half; the first three still depend on me remembering to type the command, which is the same failure mode that let the directory-level problems ship in the first place. The JSON-LD audit is the one exception — the publish workflow runs it for me, but only after the article is already out, and its failure is swallowed with an || echo, so it reports rather than blocks. Wiring the other three into the push path is the next job.


Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.

Top comments (0)