Maskinrommet håndhever nå publiseringsregler for samlinger med require_approval: true i publishing-traiten: - submitted_to-edge: kun roller i submission_roles (+ owner/admin) kan opprette. Metadata settes automatisk: status=pending, submitted_at=now. - belongs_to-edge til require_approval-samling: kun owner/admin. - Status-endring på submitted_to: kun owner/admin av samlingen. PublishingConfig utvidet med require_approval (default false) og submission_roles (default ["member"]). Nye hjelpefunksjoner: get_publishing_config, get_user_role_for_node, user_is_owner_or_admin. EdgeRow utvidet med source_id/target_id. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
29 KiB
Publisering — Fra privat tanke til offentlig artikkel
Kjerneflyt
En artikkel reiser fra privat til publisert via edges. Kildematerialet —
chatmeldinger, notater, transkripsjoner — er egne noder koblet med
source_material-edges.
To veier til en artikkel
1. Artikkel fra start. Bruker åpner artikkelverktøyet på arbeidsflaten og skriver. Artikkelen er en innholdsnode fra fødsel.
1. Privat artikkel Node med metadata.document. Bare din.
↓ (legg til intended_for → samling)
2. Under arbeid Tema-forhåndsvisning i editoren.
↓ (submitted_to erstatter intended_for)
3. Innsendt Redaktøren vurderer.
↓ (belongs_to erstatter submitted_to)
4. Publisert Maskinrommet rendrer HTML, genererer URL.
2. Artikkel fra kildemateriale. Bruker drar chatbobler, notater eller
transkripsjoner til artikkelverktøyet. Ny artikkelnode opprettes med
source_material-edges tilbake til kildene. Artikkelen følger deretter
publiseringsreisen over. Se arbeidsflaten.
Chatboble ←source_material── Artikkel-node
Notat ←source_material── ↑
Transkripsjon ←source_material── │
↓
intended_for → submitted_to → belongs_to → publisert
Tilbaketrekking: fjern belongs_to-edgen → artikkelen forsvinner fra
publikasjonen. Noden lever videre privat.
To publiseringsmodeller
Flyten ovenfor dekker den personlige modellen — du eier samlingen og publiserer direkte. Men en publikasjon med flere bidragsytere trenger redaksjonell kontroll: noen skriver, andre bestemmer hva som publiseres.
Forskjellen er én innstilling i publishing-traiten:
"publishing": {
"slug": "sidelinja-magasin",
"require_approval": true,
"submission_roles": ["member"]
}
| Innstilling | Personlig blogg | Redaksjonell publikasjon |
|---|---|---|
require_approval |
false |
true |
submission_roles |
— (ikke relevant) | ["member"] eller ["member", "reader"] |
| Hvem publiserer | Eieren selv | Owner/admin godkjenner |
| Edge ved publisering | belongs_to direkte |
submitted_to → godkjenning → belongs_to |
submission_roles styrer hvem som kan sende inn. member betyr at du
må være medlem av samlingen. reader åpner for at lesere også kan
foreslå innhold — åpen innsending.
Personlig publisering (require_approval: false)
Ole eier "Oles blogg" (samling med publishing-trait)
Ole skriver artikkel → legger til belongs_to-edge → publisert
Ingen mellomsteg. Maskinrommet rendrer HTML til CAS umiddelbart.
Redaksjonell publisering (require_approval: true)
Redaktøren eier "Nettmagasinet" (samling med publishing-trait)
Ole er medlem (member-edge til samlingen)
1. Ole skriver artikkel (privat node)
2. Ole sender inn → submitted_to-edge til Nettmagasinet
3. Redaktøren ser den → visning: noder med submitted_to til min samling
4. Diskusjon → kommunikasjonsnode med edges til artikkel + deltakere
5. Ole reviderer (samme node, nytt innhold)
6. Redaktøren godkjenner → submitted_to erstattes med belongs_to
7. Redaktøren planlegger → publish_at i edge-metadata
8. Maskinrommet rendrer → HTML til CAS ved publish_at
Artikkelen er alltid én node. Den reiser gjennom systemet ved at edges
endres. Materialet den bygger på er andre noder, sporbare gjennom grafen
via source_material-edges.
Innsending: submitted_to-edge
Ny edge-type for redaksjonell publisering:
artikkel ──submitted_to──→ samling (med publishing-trait)
Edge-metadata
{
"status": "pending",
"submitted_at": "2026-03-17T10:00:00Z"
}
Status-verdier
| Status | Betydning | Hvem endrer |
|---|---|---|
pending |
Venter på vurdering | Settes automatisk ved innsending |
in_review |
Under vurdering | Redaktør |
revision_requested |
Forfatter må revidere | Redaktør (legger til feedback) |
rejected |
Avvist | Redaktør |
approved |
Godkjent, klar for publisering | Redaktør |
Metadata ved tilbakemelding:
{
"status": "revision_requested",
"submitted_at": "2026-03-17T10:00:00Z",
"feedback": "Trenger sterkere intro. Kan du utdype kilden i avsnitt 3?",
"feedback_by": "uuid-redaktør",
"feedback_at": "2026-03-17T14:30:00Z"
}
Fra godkjent til publisert
Når redaktøren godkjenner:
submitted_to-edgen slettesbelongs_to-edge opprettes fra artikkel → samling- Valgfritt:
publish_ati edge-metadata for planlagt publisering
// belongs_to-edge ved planlagt publisering
{
"publish_at": "2026-04-01T08:00:00Z",
"approved_by": "uuid-redaktør",
"approved_at": "2026-03-20T16:00:00Z"
}
Maskinrommet sjekker periodisk for belongs_to-edges med publish_at
i fortiden som ikke er rendret ennå. Ved treff: render HTML → CAS →
oppdater RSS.
Umiddelbar publisering: publish_at settes ikke, maskinrommet rendrer
med en gang.
Avvisning
Redaktøren setter status til rejected. Artikkelen forblir Oles
private node — den forsvinner fra redaktørens visning, men Ole mister
ingenting. Ole kan revidere og sende inn på nytt (ny submitted_to-edge).
Redaktørens arbeidsflate
Redaktørens "innboks" er ikke en egen node — det er en visning:
"Noder med
submitted_to-edge til min samling, gruppert på status"
Dette er en spørring mot grafen, konsistent med prinsippet om at
visninger er spørringer, ikke containere. Samlingens kanban-trait
(hvis aktiv) kan drive denne visningen som et brett:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Innkomne │ │ Under │ │ Godkjent │ │ Planlagt │
│ │ │ vurdering│ │ │ │ │
│ Artikkel │ │ Artikkel │ │ Artikkel │ │ Artikkel │
│ fra Ole │ │ fra Lise │ │ fra Kari │ │ fra Arne │
│ │ │ │ │ │ │ 1. april │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
Drag-and-drop mellom kolonner endrer status i edge-metadata.
Siste kolonne ("Planlagt") setter publish_at.
Diskusjon om innsendt artikkel
Når redaktøren vil gi feedback utover et kort notat i edge-metadata, opprettes en kommunikasjonsnode — samme primitiv som brukes for chat og møter:
Kommunikasjonsnode (node_kind: "communication")
←── member ── Ole
←── member ── Redaktøren
←── belongs_to ── artikkelen (kontekst)
Samtalen lever som en vanlig tråd. Meldinger er noder med
belongs_to-edge til kommunikasjonsnoden. Når artikkelen er publisert
ligger samtalehistorikken igjen som arkiv — nyttig for revisjonshistorikk.
Håndhevelse i maskinrommet (implementert)
Maskinrommet validerer alle edge-operasjoner i create_edge og
update_edge. For publiseringssamlinger med require_approval: true:
belongs_to-edge til samlingen: Kun owner/admin kan opprette. Forsøk fra member/reader avvises.submitted_to-edge til samlingen: Tillatt for roller isubmission_roles. Maskinrommet sjekker at brukeren har riktig rolle-edge til samlingen.- Status-endring på
submitted_to: Kun owner/admin kan endre status (godkjenne, avvise, be om revisjon). Forfatter kan kun trekke tilbake (slette sinsubmitted_to-edge).
For samlinger med require_approval: false:
belongs_to-edge tillatt for owner/admin direkte.- Ingen
submitted_to-logikk.
Hvorfor edge-metadata, ikke workflow-noder
En alternativ modell er å gjøre hvert beslutningspunkt til sin egen node — en "innsending"-node, en "godkjenning"-node, etc. Det ville følge "alt er noder"-filosofien konsekvent.
Vi velger edge-metadata i stedet fordi:
- Status er en egenskap ved relasjonen, ikke en ting som eksisterer i seg selv. "Oles artikkel er innsendt til magasinet" beskriver forholdet mellom to noder, ikke en tredje entitet.
- Graf-hygiene. Tre-fire ekstra noder per artikkel for å representere tilstand skaper bloat uten informasjonsgevinst.
- Enklere spørringer. "Vis innsendte artikler" er én spørring mot edges. Med workflow-noder trenger du joins.
Unntaket er når et beslutningspunkt trenger egen kontekst — samtaler, deltakere, historikk. Da opprettes en kommunikasjonsnode (se "Diskusjon om innsendt artikkel" ovenfor). Men det er en samtale, ikke en workflow-tilstand.
Presentasjonselementer er noder
En ingress er en tekst. En overskrift er en tekst. Et forsidebilde er et bilde. Alt som vises om en artikkel på forsiden er en ting med egen forfatter, eget tidspunkt, og potensielt flere varianter. Det er noder, ikke felt.
Artikkel (innholdsnoden)
←── title ──── "Trikken kan bli gratis" (node, content)
←── title ──── "Gratis trikk fra 2027?" (node, content)
←── summary ── "Oslo bystyre vurderer..." (node, content)
←── summary ── "Fra 2027 kan trikken..." (node, content, AI-generert)
←── og_image ── Trikk i solnedgang (node, media)
←── og_image ── Bystyresalen (node, media)
Prinsipper
- Hvis det er en tekst om en annen tekst, er det en node. Ingress, undertittel, OG-beskrivelse, tweet-tekst, podcast-intro — alt som presenterer en annen node er selv en node.
nodes.titleer det interne displaynavnet. "Vegard", "Møte 15. mars", arbeidstittelen i redaksjonen. Det er ikke det som vises på forsiden til leserne.- Varianter er naturlige. Flere noder med samme edge-type til samme artikkel = flere varianter. Ingen spesiell mekanisme.
Edge-metadata for varianter
// title-edge fra tittelnode → artikkel
{
"variant": "editorial", // "editorial", "ai", "social", "rss"
"language": "no"
}
| Edge-type | Hva | Eksempel |
|---|---|---|
title |
Publisert overskrift | "Trikken kan bli gratis" |
subtitle |
Undertittel | "Bystyret splittet om finansiering" |
summary |
Ingress / forhåndsvisning | 1-2 setninger |
og_image |
Forsidebilde / OpenGraph-bilde | CAS-media-node |
og_description |
OG/meta-beskrivelse | Kort tekst for deling |
Automatisk A/B-testing
Når det finnes flere noder med samme edge-type til samme artikkel, er default å A/B-teste automatisk.
Mekanikk:
- Maskinrommet roterer varianter ved forside-rendering
- Logger hvilken variant som ble vist (impression) og om leseren klikket videre til artikkelen (konvertering)
- Normaliserer CTR mot tidspunkt — klikk kl. 08 mandag morgen har annen baseline enn kl. 22 lørdag kveld
- Etter statistisk signifikans → vinneren markeres, taperen deaktiveres
- Redaktøren kan alltid overstyre — pin en spesifikk variant
Edge-metadata under testing:
{
"variant": "editorial",
"ab_status": "testing", // "testing", "winner", "retired"
"impressions": 4821,
"clicks": 312,
"ctr": 0.0647,
"started_at": "2026-04-01T08:00:00Z"
}
Oppførsel:
- Én variant → ingen A/B, bare visning. Null overhead.
- To+ varianter → automatisk testing. Null konfigurasjon.
- Redaktøren trenger ikke vite at dette skjer. Skriver du én tittel får du én tittel. Skriver du to får du automatisk testing.
Hva med podcast-episoder?
Samme mønster. AI-analyse i podcastfabrikken genererer forslag til tittel, sammendrag, show notes og kapittler — disse er noder med edges til episoden:
Episode #42 (innholdsnode)
←── title ──── "Klimapolitikkens blinde flekk" (AI-generert)
←── title ──── "Ep. 42: Klimapolitikk" (redaksjonell)
←── summary ── AI-sammendrag (node, content)
←── summary ── Manuelt sammendrag (node, content)
←── show_notes ── AI-genererte show notes (node, content)
←── show_notes ── Redigerte show notes (node, content)
←── chapter ── "00:00 Intro" (node, metadata: { at: "00:00:00" })
←── chapter ── "05:23 Intervju" (node, metadata: { at: "00:05:23" })
Redaksjonen velger mellom AI-forslag og manuelt skrevne varianter. A/B-testing gir mening for titler og sammendrag i RSS-feeden. Kapittler er ikke A/B-testbare, men de er fortsatt noder fordi de har egne tidsstempler, kan redigeres uavhengig, og kan ha flere varianter (f.eks. AI-genererte vs. manuelt justerte).
Skriveopplevelse vs. forside-kontroll
Problemet
TipTap lar forfatteren formatere fritt — overskrifter, blockquotes,
inline-stiler, bilder. Det er bra for artikkelen. Men forsiden trenger
forutsigbar, enhetlig struktur. Hvis forfatteren åpner med en <h2>,
bruker kursiv overalt, eller legger inn en tabell i første avsnitt,
ser forside-kortet rotete ut.
Løsningen: forsiden leser aldri fra artikkelens dokument
Forsiden bruker kun presentasjonsnodene — aldri metadata.document:
| Forside-element | Kilde | Format |
|---|---|---|
| Overskrift | title-node |
Ren tekst, ingen formatering |
| Ingress | summary-node |
Ren tekst, ~200 tegn |
| Bilde | og_image-node |
CAS-media-node |
| Forfatter | created_by → person-node |
title på personnoden |
| Dato | publish_at i edge-metadata |
Tidsstempel |
Presentasjonsnodene har kun content-feltet (ren tekst). Ingen
metadata.document, ingen TipTap-formatering. Forfatteren skriver
tittel og ingress i egne, enkle tekstfelt — ikke i rik-editoren.
Det er en bevisst begrensning: forsiden er designerens domene.
Artikkelsiden: fri formatering, styrt av tema
Inne i artikkelen rendres metadata.document fritt — TipTap-struktur
er ønsket. Men forfatteren bestemmer struktur (overskrifter,
blockquotes, bilder, lister), temaet bestemmer stil (fonter, farger,
spacing, bredde). Tera-templaten wrapper dokumentet i tema-CSS som
normaliserer utseendet.
Forside: title-node.content + summary-node.content + og_image
→ ren tekst, tema-styrt layout, alltid forutsigbart
Artikkelside: metadata.document → Tera-template + tema-CSS
→ rik formatering, men temaet styrer utseende
Publiseringssteget i frontend
Når forfatteren publiserer, vises et publiseringspanel ved siden av (eller over) editoren:
┌─────────────────────────────────────────────┐
│ Publiser til: Nettmagasinet │
├─────────────────────────────────────────────┤
│ Tittel: [ Trikken kan bli gratis ] │
│ Ingress: [ Oslo bystyre vurderer gratis ] │
│ [ kollektivtransport fra 2027. ] │
│ Bilde: [ 🖼 Dra bilde hit / velg ] │
│ Slug: [ /om-trikken ] │
│ Publiser: [ Nå ▾ ] [ Publiser ] │
├─────────────────────────────────────────────┤
│ + Legg til variant (for A/B-testing) │
└─────────────────────────────────────────────┘
Hvert felt oppretter en presentasjonsnode med riktig edge-type. Feltene er enkle tekstfelt — ingen rik-editor, ingen formatering. "Legg til variant" oppretter en ekstra node av samme type.
Forfatteren som bare vil publisere fyller ut tittel og ingress og trykker publiser. Forfatteren som vil A/B-teste legger til en variant. Redaktøren kan overstyre begge deler.
Forhåndsvisning med publikasjonens tema
Forfatteren kan forhåndsvise artikkelen med målpublikasjonens utseende direkte i editoren:
┌─ Editor ──────────────────────────────────────┐
│ Visning: [ Redigering ▾ ] [ Trikkemagasinet ] │
│ │
│ (innholdet vises med temaets fonter, farger, │
│ bredde, spacing — WYSIWYG for publisert │
│ resultat) │
└────────────────────────────────────────────────┘
Teknisk: frontend henter theme_config fra samlingens
publishing-trait og appliserer CSS-variablene på editor-containeren.
Ingen rendering, ingen maskinrommet-kall — ren CSS-bytte i frontend.
Forutsetning: forfatteren har edge til samlingen — enten member_of
eller intended_for (se nedenfor).
intended_for-edge: artikkelens reise
Forfatteren markerer tidlig hvilken publikasjon artikkelen er ment
for. Det er en edge — intended_for — som uttrykker intensjon uten
å sende inn:
intended_for → submitted_to → belongs_to
(arbeidsfase) (vurdering) (publisert)
Én relasjon mellom artikkel og samling. Edge-typen er tilstanden:
| Edge-type | Fase | Hva skjer |
|---|---|---|
intended_for |
Skriving | Tema-forhåndsvisning i editor, samlingen vet ingenting |
submitted_to |
Vurdering | Redaktøren ser artikkelen, status-metadata styrer flyten |
belongs_to |
Publisert | Maskinrommet rendrer, artikkelen er live |
intended_for gir systemet kontekst fra starten:
- Hvilken tema-CSS som tilbys i editoren
- Hvilke presentasjonselementer samlingen krever (f.eks. Trikkemagasinet krever ingress + bilde, bloggen bare tittel)
- Hvem som potensielt vil motta artikkelen ved innsending
Forfatteren kan endre intended_for fritt — det er bare en
arbeidshypotese. Først ved eksplisitt innsending konverteres den
til submitted_to.
Temaer og forside-layout
Innebygde temaer
Hvert tema er et sett med Tera-templates (Jinja2-lignende, Rust-native)
og CSS-variabler. Tema velges i publishing-traiten:
"publishing": {
"slug": "nettmagasinet",
"theme": "avis",
"theme_config": {
"colors": { "primary": "#1a1a2e", "accent": "#e94560" },
"typography": {
"heading_font": "Georgia, serif",
"body_font": "system-ui, sans-serif"
},
"layout": { "max_width": "1200px" },
"logo_hash": "cas://sha256-def456"
},
"index_mode": "dynamic",
"index_cache_ttl": 300,
"stream_page_size": 20,
"featured_max": 4
}
Temaet setter alle defaults. theme_config overstyrer spesifikke
verdier via CSS-variabler (--color-primary, --font-heading, etc.).
Fungerer meningsfullt med bare "theme": "magasin" — null konfigurasjon.
| Tema | Karakter | Forside-layout |
|---|---|---|
| Avis | Tett, multi-kolonne, informasjonstung | Hero + sidebar + rutenett |
| Magasin | Store bilder, luft, editorial | Hero fullbredde + cards + kronologisk |
| Blogg | Enkel, én kolonne, personlig | Kronologisk liste, evt. pinned øverst |
| Tidsskrift | Akademisk, tekstdrevet, minimalt | Nummerliste med innholdsfortegnelse |
Temaer er kode (Tera + CSS) som lever i repoet. Nye temaer er en utvikleroppgave, ikke en brukeroppgave. Ingen page-builder, ingen plugin-arkitektur — innebygde temaer som ser bra ut.
Redaksjonell prioritering via slots
Redaktøren styrer forsiden gjennom slot-metadata på belongs_to-edgen:
// belongs_to-edge fra artikkel → publikasjon
{
"publish_at": "2026-04-01T08:00:00Z",
"slot": "hero",
"slot_order": 1,
"pinned": false
}
Tre plasser med økende automatisering:
| Plass | Antall | Styring | Visning |
|---|---|---|---|
hero |
Maks 1 | Manuell | Stor, dominant øverst |
featured |
Konfigurerbart (default 4) | Manuell | Fremhevet, mindre enn hero |
null (strøm) |
Ubegrenset | Automatisk | Kronologisk etter publish_at |
Gode defaults uten kurerering: Alt som publiseres havner i strømmen, sortert på dato. Ingen hero, ingen featured — forsiden er en ren kronologisk flyt som fungerer fra dag én uten redaksjonelt arbeid.
Når redaktøren griper inn (via POST /intentions/set_slot):
- Sett hero → maskinrommet setter
slot: "hero". Forrige hero flyttes automatisk tilbake til strøm (med mindre den er pinned). - Sett featured →
slot: "featured",slot_orderbestemmer rekkefølge. Overstiger antalletfeatured_max→ eldste ikke-pinned featured faller til strøm (FIFO). - Pin →
pinned: truei edge-metadata. Artikkel blir stående i slot uavhengig av alder og automatisk fjerning. - Krever owner/admin-tilgang til samlingen.
Forside-administrasjon i frontend
┌─────────────────────────────────────────┐
│ HERO │
│ [ Drag artikkel hit ] │
├──────────┬──────────┬──────────┬────────┤
│ FEATURED │ FEATURED │ FEATURED │ [ + ] │
├──────────┴──────────┴──────────┴────────┤
│ STRØM (automatisk, nyeste først) │
│ • Artikkel 5 — 1. april │
│ • Artikkel 4 — 28. mars [📌 Pin] │
│ • Artikkel 3 — 25. mars [⬆ Fremhev]│
└─────────────────────────────────────────┘
Drag-and-drop mellom plasser. Maskinrommet oppdaterer edge-metadata og regenererer forside (CAS eller cache-invalidering).
Sidetyper
| Side | Kilde | Rendering |
|---|---|---|
| Forside | Hero + featured + strøm | Statisk CAS eller dynamisk cache |
| Artikkelside | Én node | Statisk CAS (alltid) |
| Kategori/tag | Artikler med bestemt tag-edge | Dynamisk, maskinrommet, paginert |
| Arkiv | Alle artikler kronologisk | Dynamisk, maskinrommet, paginert |
| Søk | Fulltekstsøk i PG | Dynamisk, maskinrommet |
| Om-side | Node med page_role: "about" |
Statisk CAS |
Enkeltartikler rendres alltid til statisk CAS. Forsiden kan være
statisk CAS (magasin, lav frekvens) eller dynamisk med cache
(nyhetsavis, høy frekvens) — styrt av index_mode i trait-konfig.
Kategori-, arkiv- og søkesider er alltid dynamiske med paginering.
Skalering for store publikasjoner
Designet skal håndtere en nettavis med ~30.000 artikler over 30 år (~3 per dag). Implikasjoner:
Enkeltartikler i CAS: 30.000 HTML-filer á ~80KB = ~2.4 GB. Trivielt.
CAS-pruning beholder kun gjeldende html_hash per artikkel — eldre
versjoner prunes automatisk.
Forside-spørringer: Forsiden trenger aldri alle 30.000 artikler. Tre indekserte spørringer:
-- Hero (maks 1)
SELECT n.* FROM nodes n JOIN edges e ON e.source_id = n.id
WHERE e.target_id = $collection AND e.edge_type = 'belongs_to'
AND e.metadata->>'slot' = 'hero';
-- Featured (maks N)
SELECT n.* FROM nodes n JOIN edges e ON e.source_id = n.id
WHERE e.target_id = $collection AND e.edge_type = 'belongs_to'
AND e.metadata->>'slot' = 'featured'
ORDER BY (e.metadata->>'slot_order')::int;
-- Strøm (paginert)
SELECT n.* FROM nodes n JOIN edges e ON e.source_id = n.id
WHERE e.target_id = $collection AND e.edge_type = 'belongs_to'
AND e.metadata->>'slot' IS NULL
ORDER BY (e.metadata->>'publish_at')::timestamptz DESC
LIMIT 20 OFFSET $page;
Med indeks på (target_id, edge_type) og GIN-indeks på metadata
er dette raskt uansett samlingsstørrelse.
Forside-rendering (implementert):
index_mode: "static"— full HTML rendres til CAS viarender_index-jobb ved publisering. Samlingensmetadata.rendered_index.index_hashpeker til CAS-filen. Serveres medCache-Control: immutable. Passer for magasin/blogg med lav frekvens.index_mode: "dynamic"(default) — maskinrommet serverer on-demand med in-memory cache (IndexCache), invalidert ved publisering (belongs_to-endring).index_cache_ttl(default 300s) styrer cachens levetid. Passer for nyhetsavis med høy frekvens.- Tre separate indekserte PG-spørringer for hero/featured/strøm —
filtrerer på
sloti edge-metadata, bruker GIN-indeks.
Bulk re-rendering ved temaendring: Temaendring trigger batch-jobb
via jobbkøen. Maskinrommet paginerer 100 artikler om gangen, rendrer
til CAS, oppdaterer metadata.rendered.html_hash. Med ~100ms per
artikkel: ~50 min for 30.000. Ikke blokkerende — artikler serveres
med gammelt tema til de er re-rendret. renderer_version i metadata
identifiserer hvilke som gjenstår.
RSS-feed: Inneholder de rss_max_items nyeste (default 50).
Regenereres ved publisering. Trivielt uansett samlingsstørrelse.
URL-struktur
Uten eget domene
synops.no/pub/{samlings-slug}/{node-short-id}
Eksempel: synops.no/pub/mittmagasin/a7f3e2
samlings-sluger unik per publiseringssamlingnode-short-ider et kort derivat av node-id (stabilt, permanent)- Valgfri lesbar slug:
synops.no/pub/mittmagasin/om-trikken-og-tanker(lagret som metadata på noden, redirect ved endring)
Med eget domene
mittmagasin.no/a7f3e2
mittmagasin.no/om-trikken-og-tanker
Domenet kobles i samlingens trait-konfigurasjon. Caddy håndterer TLS og ruting automatisk.
HTML-rendering og CAS
Rendret HTML lagres i CAS (content-addressable storage), akkurat som andre avledede representasjoner (transkripsjoner, thumbnails).
Dokument (metadata.document) → Renderer → HTML (CAS)
Lydfil (CAS) → Whisper → Transkripsjon (content)
Noden peker på rendret resultat via metadata:
{
"metadata": {
"document": { /* TipTap/ProseMirror JSON */ },
"rendered": {
"html_hash": "a1b2c3d4e5f6...", // SHA-256 hex-digest, peker til CAS
"rendered_at": "2026-03-17T14:30:00Z",
"renderer_version": 1
}
}
}
Serving-modell
Caddy reverse-proxyer publiserings-URLer til maskinrommet. Maskinrommet slår opp CAS-hash fra node-metadata og streamer filen:
Leser → Caddy → maskinrommet (slug → hash oppslag) → CAS-fil fra disk
↑ Cache-Control: public, max-age=31536000, immutable
Maskinrommet eier mappingen mellom slug og CAS-hash — å duplisere den til filsystemet (symlinks) eller Caddy-konfig ville vært en ekstra synkroniseringsbyrde uten reell gevinst. CAS-hashen endres aldri, så Caddy og nettlesere cacher aggressivt.
For kategori-, arkiv- og søkesider serverer maskinrommet dynamisk HTML direkte (ingen CAS), med kortere cache-TTL.
Gevinster
- Deduplisering — rediger og angre → hashen peker tilbake til forrige versjon uten ekstra lagring
- Immutabel — en gitt hash er alltid samme HTML. Caches aggressivt, CDN-vennlig
- Pruning fungerer — gammel rendret HTML uten referanser ryddes bort som alt annet i CAS
- Revisjonshistorikk gratis — hver publisering genererer ny hash
- Bulk-regenerering —
renderer_versionlar maskinrommet finne alle noder med eldre versjon og re-rendere ved malendring - Én sannhetskilde — maskinrommet eier slug→hash-mappingen, ingen symlinks eller filsystem-synkronisering å vedlikeholde
Custom domain-mekanisme
- Bruker legger til domene i samlingens
publishing-trait:"publishing": { "custom_domain": "mittmagasin.no", ... } - Maskinrommet validerer at DNS peker til serveren
- Caddy registrerer domenet via on-demand TLS — sertifikat hentes automatisk ved første besøk
- Validerings-callback fra Caddy mot maskinrommet bekrefter at domenet er registrert
Caddy on-demand TLS-konfigurasjon
# Dynamiske custom domains for publiseringssamlinger
:443 {
tls {
on_demand {
ask http://maskinrommet:3100/internal/verify-domain
}
}
reverse_proxy maskinrommet:3100
}
Maskinrommet svarer 200 hvis domenet tilhører en samling med
publishing-trait, 404 ellers. Caddy henter kun sertifikat for
verifiserte domener.
RSS/Atom
En samling med rss-trait genererer feed automatisk:
synops.no/pub/{slug}/feed.xml
mittmagasin.no/feed.xml
Feeden genereres på nytt ved publisering/avpublisering. For podcast-samlinger
inkluderes <enclosure>-tags med lyd-URLer. Samme mønster som eksisterende
podcastfabrikken-konsept.
SEO og metadata
Ved rendering genererer maskinrommet:
<title>fra node-tittel<meta name="description">fra første avsnitt eller manuell oppsummering- OpenGraph-tags (tittel, beskrivelse, bilde)
<link rel="canonical">for å unngå duplikat-innhold- Strukturert data (JSON-LD) for artikler
<link rel="alternate" type="application/atom+xml">for feed
OG-defaults kan settes på samlingsnivå i publishing-traiten, med
mulighet for overstyring per node.
Tilgangskontroll
Publisert innhold serveres uten autentisering. Tilgangsmodellen:
- Samlingen har
visibility: open(eller spesifikk publishing-logikk) - Noder med
belongs_to-edge til publiseringssamlingen rendres som HTML - Noder uten slik edge er usynlige for offentligheten
- Kommentarer (hvis
comments-trait er aktiv) kan kreve innlogging eller tillate anonyme bidrag avhengig av konfigurasjon