Our work

Kunstbrein: a news site that fills itself, with the editorial rules in code

A Dutch-language AI news site that reads fifty sources every morning, decides for itself which story is worth telling, writes the article with a context layer for Flemish business owners, and publishes it without anyone clicking merge. The gates that stop a faulty article are written in code, and they are tested.

An instruction in a prompt is a request. A gate is a gate. That is why Kunstbrein's quality control lives in code, not in the brief we hand the model.

Challenge

Content marketing does not scale with manual work, and AI content nobody checks costs you more authority than it earns. So the question was never whether AI can write articles, because anyone can demonstrate that in five minutes. The question was whether you can build a system you dare to let publish without a final edit, every day, without anything ever going live that contradicts its own source.

Approach

Everything that can live in code, lives in code. Fetching, deduplicating, clustering and ranking happen without a language model, so those steps cannot invent anything and cost nothing. The model only comes in where judgement is needed: the writing and the fact check. On top of that, five gates that refuse instead of warn, 300 tests, and git as the single source of truth for the content.

50

sources fetched, deduplicated, clustered and ranked every morning, entirely without a language model

50%

of written articles never make it: 22 written, 11 published across 16 logged runs

300

tests across the pipeline, the gates and the clock, all green at the time of writing this case

4

automated jobs a day: fetch, write, verify, top up, plus a newsletter twice a week

Fifty sources, every morning, without a model

At 07:25 the pipeline fetches 47 RSS and Atom feeds plus three pages that offer no feed and are therefore polled. Those fifty sources are not equal: 41 may supply content, nine sit behind a paywall and may only tip that something is happening. Every source also has a role, from primary source through press and policy to analysis and dissent.

What happens next happens in code. The feeds are normalised, deduplicated against everything already online, and clustered: the same story at five publishers becomes one candidate instead of five. The ranking weighs the number of independent sources (logarithmically, because ten sources do not count ten times), the diversity of roles (a primary source plus analysis plus press is a real story, five copies of the same press release is not), the freshness, and the presence of SME-relevant terms. Pure research news takes a penalty. That last dial arrived after "Midjourney buys an astrology app" could become a candidate.

Doing all of that without a model is not frugality but reliability. What lives in code cannot invent anything, is reproducible and can be tested. At this volume, having a language model rank eighty headlines a day would also cost more than the writing itself.

The gates: quality lives in code, not in the prompt

This is the heart of the project. Five layers stand between a written article and publication, all in code and all tested:

  • Verbatim overlap. After writing, the article is matched word by word against the source text. Any run of eight or more words that matches literally and does not sit inside a marked quote is a rejection. Facts are free, phrasings are not. One refinement came out of measurement: a shorter run that contains a number is a factual description, not plagiarism, because "of 10 gigawatts in southern Ohio" cannot be phrased differently without losing the fact. Ten words in a row without a number is never coincidence.
  • Fact checking with two yardsticks. A summary bullet is a fact and has to appear in the sources. A context consequence is an interpretation and by definition may not appear in the sources, because the brief explicitly asks for an angle the sources do not have. Treating those two the same would either reject every piece of analysis or let every invented factual claim through.
  • Duplicate gate. Headline overlap against recently published work, with a threshold. On 30 July it rejected an article because it reconstructed the same AI agent breach as a piece from four days earlier. Rightly so.
  • Entity and link check. Invented source URLs, publishers or names do not get through. The frontmatter has to point at what the fetching step actually brought in.
  • Lengths, with a target and a failure point. The model writes towards the ceiling, so three characters too many is not a quality problem but probability. There is now a limit the brief asks for and a hard boundary where the gate rejects. In between, the article gets a style note, and style gets fixed but never rejects.

The trap underneath is the content schema. A gate that is looser than the schema lets through an article that breaks the build, and the pipeline merges its own pull request. So a test reads the maxima from the content schema and compares them with the failure points in code. Anyone who ever raises a boundary and forgets the schema gets a red test instead of a site that is down.

Half of it gets rejected, and that is the point

Across 16 logged writing runs, 22 articles were written and 11 published. The rejections: 10 on the fact check, 6 on length, 3 on plagiarism, 2 as duplicates and 1 on a broken model response. That 50% is not a defect, that is the system working.

The fact check exists because something did slip through. The source said three models were involved, one of them not yet released. The article turned that into a model name nobody had announced. All gates were green, and the body of the article had it right. The error sat in the compression layer, in the summary, and that is exactly where nuance dies. Since then every bullet in that layer is matched against the sources individually.

The site is its own clock

The pipeline first hung on the GitHub Actions scheduler. That is deliberately not a guarantee: scheduled events land in a shared queue. Measured on this repo, the fetch step ran 2 hours and 32 minutes late on 26 July, the writing step 2 hours and 2 minutes late, and on 27 and 28 July nothing ran at all. Moving to odd minutes did not help, and a watchdog hanging off that same scheduler is only half a fix.

Since 28 July the roles are reversed. The Cloudflare Worker that already serves the site is now also the alarm clock: its cron triggers start the pipeline through an API call that fires immediately, and GitHub Actions is only the workbench. There was a real technical obstacle underneath, because the Astro adapter builds an entry point that only accepts visitor traffic and leaves no room for a scheduled task. A separate entry point alongside Astro's solves that, and because such a move is version-sensitive, the build script verifies everything it depends on. If something is off, the build fails, and a failed build is a skipped deploy instead of a broken site.

That same migration produced the most expensive lesson of the project. The newsletter cron was set to a value that means Tuesday and Friday in crontab and in GitHub Actions. Cloudflare counts weekdays differently, so the newsletter went out a day early. There was a test, called "starts on Tuesday and Friday morning", but it checked whether the right string sat in the table. It tested the typing, not the assumption, while its name suggested otherwise. Cloudflare's weekday numbering now sits in the code as a table and the tests check what actually comes out. A test that does not live up to its own title is worse than no test.

Git is the content database

There is no CMS. An article only exists after a commit, which sounds like a limitation but is an advantage: every article has a diff, a history and a rollback, the sitemap gets a real per-page lastmod, and there is no database that can quietly corrupt. Deduplication runs against the source URLs in what is already published, so there is no separate status table that can drift out of sync with reality.

The site itself is Astro 7 on Cloudflare Workers, with all articles, categories and dossiers prerendered: no waiting and no database call to read an article. D1 only handles what is genuinely state, such as the newsletter list, the hit counter for "most read" and the alerts. Search runs on Pagefind, without an external search service. The OG image per article is drawn during postbuild with the category and the headline in it. Fonts are self-hosted, the CSP is enforcing, GA4 only loads after consent, and the admin sits behind Cloudflare Access with real token validation: if the keys no longer check out, that admin closes rather than opens.

The context layer is the original contribution

An AI answer does not cite a site that says the same thing as five others. So the core of every Kunstbrein article is not the news but the reading of it: three to four concrete profiles, such as an SME running a chatbot through an external supplier or a marketer comparing AI subscriptions on price, each with the consequence spelled out. That is the text that exists nowhere else, and therefore the only part an AI answer has to point to Kunstbrein for.

Around it sits the accountability that AI answers reward. Every article shows its sources with publisher, title, timestamp and link, without nofollow, because a source credit is the opposite of a paid link. Every article states that it was assembled with AI assistance, under the editorial responsibility of Frederiek Pascal. The editorial charter is a real page and is at the same time the target of publishingPrinciples in the schema.org graph. Every article is a NewsArticle whose citations are built from the mandatory sources field: no source is no article, enforced by the schema. On top of that, a deliberate AI crawler policy, an llms.txt and a sitemap with a real per-page lastmod.

What it costs

One to three dollars a month in model costs. That low figure does not come from a cheap model but from the architecture: the model only does the writing and the fact check, and everything else happens in code. Everything runs through the Message Batches API at half the token price, because nobody is waiting for the answer at ten past eight in the morning. The flow works in rounds: first all main calls as one batch, then the gates in code, then all repair calls together as the next batch. A batch that is not done after forty-five minutes gets cancelled and the run fails safely: no article, no pull request, try again tomorrow.

Those costs are actually measured, not estimated: a script pulls the usage and the cost from the provider itself. Our own tally from the logs only counted the articles that made it to publication, and a rejected article costs exactly the same tokens. Beyond that: Cloudflare Workers, D1 and email routing within the existing plan, and Resend for delivery. No CMS licence, no search service, no image bank.

What we do not claim

A case that only shows the good side is a brochure. So:

  • This is not "AI writes, human checks". There is no final edit before publication. The gates sit before the merge, and quality control after the fact happens in the admin, where an article can be archived or feedback can trigger a rewrite. That is a deliberate choice.
  • No traffic or ranking claims. Measurement only started at launch. The checkpoint sits at three and six months: does Kunstbrein show up in AI answers to questions from the target audience, and does traffic reach our own AI pages. If not, it stops, with an archive that stays online.
  • The oldest articles are build-up material from the phase before the pipeline went live, and they are shorter. So we always split figures on average article length at that date, otherwise you are measuring two things at once.

Fifty sources, every morning

Fetching, deduplicating, clustering and ranking happen without a language model, so it cannot invent anything and it costs nothing.

Half of it gets rejected

22 articles written, 11 published. An article that contradicts its own source or copies it verbatim does not get in.

The site is its own clock

The same Cloudflare Worker that serves the pages starts the pipeline, after GitHub's scheduler skipped three days in a row.

Tools used

Claude Code (building with AI)
Claude Sonnet through the Message Batches API
Astro 7, prerendered, no CMS
Cloudflare Workers, D1 and cron triggers
GitHub Actions as the workbench, pull requests as the publishing path
Pagefind for search, satori and resvg for the OG images
Resend for the newsletter, with self-hosted double opt-in

Project in pictures

Your project as the next case?

We work with Belgian SMEs that want to grow.

Through Shopify, performance marketing and AI.

Client reviews

What clients experience

Why entrepreneurs love working with ClickForest.

Excellent on Google5.0★★★★★based on 23 reviews