Module-architectuur & uitwerking
Werk-model: de lagen, de bestanden en de doctrine, 1-op-1 geverifieerd tegen de code. Stand 3 juni 2026. Repo schwung-contentmotor → contentmotor.schwung.ai · publiceert naar Schwung.ai → schwung.ai/blog.
De keten — van idee tot live
cron-auto-suggest.jsschedule 0 7 * * 1,3,4,5 (ma/wo/do/vr 09:00 NL). Draait per project alleen als settings.cadans_per_week vandaag triggert; roteert bron-type. Maakt idee + run, triggert generate-background.
generate.jsMens postt kern + invalshoek (+ inspiratie-doc). Maakt idee + run, triggert generate-background.
auto-idee-generator.jsAlleen in de auto-stroom. selectDomein() trekt random een van de 5 domeinen (gelijke kans); laadt domein-bewust (alleen het getrokken domein + onderwerpen-matrix, niet alle merk-bestanden); selectBronType() rouleert doctrine/scan_data/klantcases; reageert tegen een levende take, uitwisselbaarheidstest, geen plagiaat, dedup tegen recente titels. Levert {kern, invalshoek, vijand, domein, template}.
generate-background.js → runPipeline()Sonnet · skelet (H1, koppen, these)
Sonnet + web_search · feiten mét bron
Sonnet · body, Frankwatching-toon
Opus · tegenlees-poort
Sonnet · LinkedIn, title-tag, beeld-trigger
schema.org JSON-LD
pipeline.js: schoonModelMarkdown() stript front-matter / H1 / <mark> / em-dash · bronnenblok deterministisch uit feiten · anthropic.js = Sonnet 4.6 (draft) / Opus 4.7 (polish) · template-instructie uit _lib/templates/<type>.md (artikel·faq·definitie·vergelijking).
doctrine-loader.js leest alleen wat in project.yaml staat, met prompt-cacheFrankwatching-denkstuk op de canon; zin/onzin- + tegenlees-poort; geen em-dash; lengte 1.000-1.500
toon/ambacht · duiding boven beschrijving
schrijven/
terrein + inhoud
= merkREFLECTIE-vorm — NIET voor blog
DOCTRINE-HERKOMST.md: deze map is een kopie/snapshot van de merkscan-doctrine, bewust ontkoppeld; handmatige sync. Runtime leest de motor uitsluitend deze map.
preview.html + status.js (mail met token-link)publish.js→ publiceren
revise.jstekst · structuur & argument · volledig + nieuw onderzoek
revise.js → revise-background.jsrunRevisiePipeline · polish, behoudend
runRevisiePipeline · polish met volle vrijheid (koppen, these, titel)
runPipeline · hele pijplijn opnieuw, vers onderzoek; feedback in invalshoek
Titel komt via een TITEL:-regel terug in concept_output, zodat publicatie + mail de nieuwe kop oppakken.
adapters/output-github.jsDirect commit (geen PR) src/blog/<slug>.md + front-matter (triggerWoord, kicker, keywords) + cover-png naar Schwung.ai.
Eleventy → schwung.ai/blog (~90s)
link-linkedin.jsLinkedIn-URL terugkoppelen aan artikel
Bestand-map — functions (14)
| Function | Type | Doet | Hangt af van |
|---|---|---|---|
cron-auto-suggest.js | ⚙️ cron | Dagelijkse auto-stroom; cadans-check; maakt idee+run; triggert pijplijn | supabase, auto-idee-generator |
generate.js | 👤 | Handmatig idee → run → trigger background | supabase |
generate-background.js | 🔁 | Draait runPipeline, slaat run op, preview-mail | pipeline, supabase, mail |
revise.js | 👤 | Maakt revisie-run (scope), triggert background (token) | supabase, preview-token |
revise-background.js | 🔁 | runRevisiePipeline (tekst/structuur) of runPipeline (volledig) | pipeline, supabase, mail |
publish.js | 🚀 | Commit naar GitHub (token), bouwt front-matter, published-mail | supabase, output-github, preview-token, mail, doctrine-loader |
link-linkedin.js | — | LinkedIn-post-URL koppelen aan gepubliceerd artikel | octokit, supabase, preview-token, doctrine-loader, output-github |
status.js | — | Polling: volledige run-data voor de preview | supabase |
queue.js | — | AI-voorgestelde ideeën (queued/generating/draft) | supabase |
history.js | — | Alle ideeën (incl. gepubliceerd/verworpen) | supabase |
settings.js | 🔧 | GET/PUT project-settings (cadans, woordcount, toon, AI-bronnen) | supabase |
extract-text.js | — | Tekst uit een geüpload inspiratie-document | — |
refresh-sitemap-cache.js | ⚙️ cron | Wekelijks (0 4 * * 1): ververst de schwungreclame.nl cross-link-cache | sitemap-cache |
debug.js | — | Inspecteert filesystem, env-vars en doctrine-loader | node:fs |
Bestand-map — _lib (engine + helpers)
| Module | Type | Rol |
|---|---|---|
pipeline.js | 🔁 | runPipeline (6 stappen) + runRevisiePipeline (scope) + schoonModelMarkdown + splitTitelEnBody + bronnenblok |
doctrine-loader.js | 🔁 | loadProject / loadDoctrine (leest project.yaml) / buildSystemPrompt (cache_control) |
anthropic.js | 🔁 | callClaude; Sonnet 4.6 / Opus 4.7; web_search; kosten-telemetry; extractJson |
schema-builder.js | 🔁 | buildSchemaOrg (Article/FAQPage/Definitie) + validatie |
auto-idee-generator.js | 🔧 | bedenkAutoIdee, selectDomein, selectBronType, shouldTriggerToday |
adapters/output-github.js | 🔁 | publishToGithub (direct commit of PR) + slugify + front-matter-builder |
sitemap-cache.js | ⚙️ | loadVerifiedCrossLinks uit externe_paginas_cache (7-daagse refresh) |
preview-token.js | ⚙️ | HMAC-token: beveiligt revise / publish / link-linkedin |
supabase.js | ⚙️ | DB-client + helpers (getRun, insertRun, updateRun, insertIdee, updateIdee, getProject) |
mail.js | ⚙️ | sendPreviewMail / sendPublishedMail / getNotifyEmail |
templates/*.md | 🔧 | 4 template-types: artikel (denkstuk-default), faq, definitie, vergelijking |
blog-template/* | 🔁 | Eleventy-installatieseed (blog-artikel.njk, blog.njk, blog.11tydata.js) voor Schwung.ai |
Data & extern ⚙️
Supabase — 5 tabellen
projects— slug, naam, actief, settings (cadans_per_week, word_count, toon_spectrum, ai_bron_*)ideeen— kern, bron, status, template, social_keuzes, inspiratieruns— pipeline_status, pagina_markdown, concept_output, onderzoek_output, schema_org, kostenauto_runs— logboek van de cron-stroomexterne_paginas_cache— geverifieerde schwungreclame.nl-links (cross-links)
Externe diensten
- Anthropic — Sonnet 4.6 (concept/onderzoek/draft/format) · Opus 4.7 (polish) ·
web_search· prompt-cache - GitHub — Schwung-nick/Schwung.ai (publicatie via Octokit)
- Netlify — host, 2 cron-schedules, Eleventy-build
- LinkedIn · mail (preview/published)
De regels (doctrine in het kort)
Wat kan los — voor herbruikbaarheid
De grote scheidslijn zit al in het ontwerp: een project-agnostische engine tegenover een per-project doctrine/config. Kandidaten om los te trekken:
| Blok | Nu | Los te trekken als |
|---|---|---|
| Engine 🔁 | pipeline.js + doctrine-loader.js + anthropic.js + schema-builder.js | @schwung/contentmotor-core — package dat elk project draait |
| Doctrine 🔧 | doctrines/<project>/ + project.yaml | Config-contract: nieuwe klant = nieuwe doctrine-map, zelfde engine |
| Output-adapter 🔁 | adapters/output-github.js | Adapter-interface: GitHub, WordPress, of andere bestemming |
| Publicatie-pakket | _lib/blog-template/* (Eleventy) | Los "blog-thema"-pakket voor elke Eleventy-site |
| Review-UI 👤 | preview.html + status/revise/publish | Losse review-app over projecten heen |
| Functions ⚙️ | netlify/functions/* | Dunne HTTP-wrappers om de engine — vervangbaar per host |
Kortom: de motor is al bijna een core + plug-ins-model. De winst van lostrekken zit in de engine (1× onderhouden, N projecten) en de adapters (andere bestemmingen zonder de engine te raken).