Naar de inhoud

Configuratiereferentie

Alles wat pullpress.config.yml begrijpt: het ene bestand dat van je repo een CMS maakt.

Het configuratiebestand staat in de hoofdmap van je repository en heeft versiebeheer zoals al de rest. Anders dan contentwijzigingen (die een wijzigingsverzoek openen om na te kijken), is het opslaan van de configuratie een actie van een beheerder die rechtstreeks naar je standaardbranch commit, zodat een nieuw contentmodel meteen werkt (en gecontroleerd wordt voor het geschreven wordt). Het heeft een handvol sleutels op het hoogste niveau, een lijst collecties en een kleine set veldtypes.

Bewerk je liever in de app? De ingebouwde configuratie-editor heeft een visuele modus waarvan de kiezer voor veldtypes elk type groepeert met een beschrijving van één regel, en de YAML-modus controleert voor het opslaan: een tikfout in de syntax wordt aangewezen op de precieze regel en kolom, en structurele problemen worden per veldpad opgesomd. De bestandsnaam is pullpress.config.yml (een extensie .yaml werkt ook).

De configuratie-editor in de app in de visuele modus: tabbladen Visueel en YAML, de collecties Blog posts, Pages en Opening hours in een zijbalk, en de naam, het label, de map, het sjabloon voor de bestandsnaam en de lijst velden van de gekozen collectie als formulier
De configuratie-editor: hetzelfde bestand, visueel te bewerken of als gecontroleerde YAML.

Sleutels op het hoogste niveau

SleutelBeschrijving
versionVersie van het configuratieschema. Momenteel altijd 1.
media.folderPad in de repository waar geüploade afbeeldingen gecommit worden, bijvoorbeeld public/uploads.
media.public_pathURL-voorvoegsel waaronder de gepubliceerde site die map aanbiedt, bijvoorbeeld /uploads. Wordt gebruikt om correcte afbeeldingslinks in je Markdown te schrijven.
collectionsDe lijst contentcollecties waarmee redacteuren kunnen werken. Elke collectie heeft een naam, een label, een type en velden.
componentsOptioneel. Een map van herbruikbare, benoemde veldgroepen, waarnaar velden met type: component verwijzen. Definieer een groep één keer en hergebruik ze in meerdere collecties.

Collectietypes

Een collectie beschrijft één soort content. Haar type bepaalt hoe items overeenkomen met bestanden in de repository.

folder: één bestand per item

Elk item is een eigen Markdown-bestand in de ingestelde map. Redacteuren kunnen items maken, bewerken en verwijderen: ideaal voor blogberichten, nieuwsberichten of teamleden.

collections:
  - name: blog
    label: Blog posts
    type: folder
    folder: content/blog
    fields:
      - { name: title, label: Title, type: string, required: true }
      - { name: date,  label: Date,  type: date, default: now }
      - { name: body,  label: Body,  type: markdown }

Frontmatterformaten. Alle drie de formaten blijven behouden: YAML tussen regels ---, TOML tussen regels +++ (de standaard van Hugo) en een los JSON-object bovenaan het bestand. Bestaande items houden altijd hun formaat, de volgorde van de sleutels en de sleutels die PullPress niet beheert. YAML-frontmatter houdt bij het opslaan ook zijn # comments, anchors en blokstrings met |/>: één veld bewerken geeft een diff die alleen dat veld raakt. (TOML- en JSON-frontmatter houden waarden en volgorde van de sleutels, maar commentaarregels in TOML blijven niet behouden.) Zet format: toml (of json) op een collectie om nieuwe items in dat formaat te maken; de detectie vult dit voor je in als je bestaande content het gebruikt.

Sites met één map per item (Astro content collections, Hugo leaf bundles: content/my-post/index.md) zetten structure: bundle. Voeg media: colocated toe om de afbeeldingen van een item in diens eigen map te bewaren, met een relatieve verwijzing (./photo.jpg), zodat de beeldpijplijn van je generator ze oppikt. De detectie van de repository stelt dit allemaal voor je voor: ze herkent Astro content collections, Hugo leaf- en branch-bundles (plus contentDir en bestanden in data/), Jekyll-collecties uit _config.yml, contentmappen van Next.js en bundles genest per datum, en schrijft de gemaakte labels in de taal van je interface:

collections:
  - name: projects
    label: Projects
    type: folder
    folder: redesign/content
    structure: bundle      # one folder per entry: content/my-post/index.md
    media: colocated       # images live inside the entry folder (./photo.jpg)
    slug: "{{date}}-{{slug:title}}"
    fields:
      - { name: title, label: Title, type: string, required: true }
      - { name: date,  label: Date,  type: date, default: now }
      - { name: body,  label: Body,  type: markdown }

files: vaste pagina’s

Een vaste set bestanden die je uitdrukkelijk oplijst. Redacteuren kunnen ze bewerken, maar er geen toevoegen of weghalen: geschikt voor losse pagina’s zoals Over ons of Contact. Elk bestand heeft zijn eigen fields (genest onder dat bestand, niet gedeeld op het niveau van de collectie), zodat elke pagina een andere vorm kan hebben.

collections:
  - name: pages
    label: Pages
    type: files
    files:
      - name: about
        label: About page
        file: content/about.md
        fields:
          - { name: title, label: Title, type: string, required: true }
          - { name: body,  label: Body,  type: markdown }
      - name: contact
        label: Contact page
        file: content/contact.md
        fields:
          - { name: heading, label: Heading, type: string, required: true }
          - { name: body,    label: Body,    type: markdown }

De detectie gebruikt dit ook voor sectiepagina’s: de _index.md van een Hugo branch bundle (de landingspagina van een sectie) is geen item van de folder-collectie, dus de detectie verzamelt ze allemaal in een gemaakte files-collectie “Section pages”, te bewerken zoals elke andere pagina, zonder handwerk.

data: één gestructureerd databestand

Eén YAML-, JSON- of TOML-bestand dat volledig via een formulier bewerkt wordt, zonder Markdown-body. Gebruik het voor gestructureerde data die je sjablonen rechtstreeks lezen: openingsuren, navigatiemenu’s, site-instellingen. Sleutels in het bestand die niet in de veldconfiguratie staan, overleven elke keer opslaan, in alle drie de formaten. YAML-bestanden houden ook hun commentaar; TOML- en JSON-bestanden houden hun sleutels en volgorde, maar worden opnieuw opgemaakt.

collections:
  - name: opening_hours
    label: Opening hours
    type: data
    file: data/opening-hours.yml
    fields:
      - name: hours
        label: Weekly hours
        type: list
        of:
          type: object
          fields:
            - { name: day,    label: Day,    type: select, options: [mon, tue, wed, thu, fri, sat, sun] }
            - { name: open,   label: Opens,  type: string }
            - { name: close,  label: Closes, type: string }
            - { name: closed, label: Closed all day, type: boolean, default: false }

Sommige databestanden zijn geen map maar één grote lijst: een array bovenaan het bestand, zoals de loader file() van Astro die leest (src/data/authors.json met [ …, … ]), of lijstbestanden van Hugo/Jekyll/Eleventy. Zet root: list en declareer precies één veld van het type list: de items ervan zijn de array op het hoogste niveau van het bestand, bewerkt als herhaalbaar formulier. Items die je niet aanraakt, worden letterlijk teruggeschreven (de volgorde van hun sleutels en eventuele extra sleutels blijven); TOML kan geen array op het hoogste niveau voorstellen, dus dit werkt voor bestanden .json, .yaml en .yml. De detectie stelt deze vorm automatisch voor.

Bestanden in CSV en TSV (Jekyll _data/*.csv, exports uit een spreadsheet) werken net zo: ze zijn altijd tabellen, dus laat een data-collectie met root: list naar het bestand wijzen en som de kolommen op als string-velden van het ene veld list. De kopregel en de volgorde van de kolommen blijven bij het opslaan behouden; elke waarde gaat heen en terug als tekst.

collections:
  - name: authors
    label: Authors
    type: data
    file: src/data/authors.json   # the file's root is [ … ], not { … }
    root: list
    fields:                       # exactly one field, of type list
      - name: items
        label: Authors
        type: list
        of:
          type: object
          fields:
            - { name: name,   label: Name,   type: string, required: true }
            - { name: avatar, label: Avatar, type: image }

folder met data-items: geen Markdown-body

Soms is elk item pure gestructureerde data zonder artikeltekst: auteurs, teamleden, producten, lijsten met links. Geef een folder-collectie een data-extension (json, yaml, yml of toml) en elk item wordt één databestand dat redacteuren als formulier invullen: hetzelfde maken, bewerken, verwijderen, nakijken en dezelfde geschiedenis als bij Markdown, alleen zonder body. Zo werken collecties met type: 'data' in Astro precies, en de detectie stelt ze voor je voor.

collections:
  - name: authors
    label: Authors
    type: folder
    folder: src/content/authors
    extension: json          # each entry is a .json data file (no body)
    slug: "{{slug:name}}"
    fields:
      - { name: name,   label: Name,   type: string, required: true }
      - { name: bio,    label: Bio,    type: text }
      - { name: avatar, label: Avatar, type: image }

Het hele bestand is de data (het formaat volgt de extensie), en net als bij een data-collectie blijven de volgorde van de sleutels en sleutels die niet in je lijst velden staan, bij elke keer opslaan behouden. Er is geen body-veld, dus voeg er geen toe; relaties kunnen ook naar deze collecties wijzen.

Collectie-opties

Naast name, label, folder en fields neemt een folder-collectie deze optionele sleutels. De detectie vult de meeste voor je in.

SleutelBeschrijving
structureflat (standaard) houdt één bestand per item; bundle gebruikt één map per item (folder/slug/index.md).
mediasite (standaard) stuurt uploads naar de mediamap; colocated bewaart ze in de eigen bundlemap van het item, met een verwijzing als ./photo.jpg. colocated vraagt structure: bundle.
extensionBestandstype van items: md (standaard), markdown of mdx voor Markdown-pagina’s; html, adoc of org voor content die geen Markdown is (de frontmatter blijft behouden; declareer de body als type: code zodat die als platte tekst bewerkt wordt); of json, yaml, yml of toml om van elk item een databestand zonder body te maken (een map met data-items, zoals Astro data collections).
formatFrontmatterformaat voor nieuwe items: yaml (standaard), toml of json. Bestaande items houden het formaat dat ze al gebruiken.
nestedtrue verzamelt ook items uit submappen (tot vijf niveaus diep), gegroepeerd per map. Standaard uit.
localesExtra taalcodes (bijvoorbeeld [fr, en]) die elk item per taal bewerkbaar maken, ingedeeld volgens de sleutel i18n.
i18nWaar vertalingen staan: suffix (standaard) gebruikt bestanden naast elkaar (post.fr.md); subfolder gebruikt één map per taal (Hugo content/<lang>/, Astro/Starlight docs/<lang>/). Met subfolder laat je folder wijzen naar de map van de standaardtaal en noem je dat segment in defaultLocale; bijvoorbeeld folder: content/en, defaultLocale: en, locales: [fr, de] zet Franse items in content/fr.
slugSjabloon voor de bestandsnaam van nieuwe items. Standaard {{slug:title}}. Tokens: {{slug:field}} (veld als slug), {{field}}, {{date}}, {{year}}, {{month}}, {{day}}.
sortHoe de lijst items gesorteerd is: { field, order } met order asc of desc. Standaard op het datumveld, nieuwste eerst.

Een files-collectie somt in plaats daarvan elk bestand op met zijn eigen fields (en een optioneel format per bestand); een data-collectie wijst naar één file en deelt één set fields.

Veldtypes

Elk veld in een collectie wordt een formulierelement in de editor. De gewone opties label, required en default werken op elk type. Twee andere helpen je redacteuren: een tekst help toont een hint onder het veld, en een placeholder toont voorbeeldtekst in een leeg invoerveld. Verplichte velden krijgen een rood sterretje en bij het indienen een vriendelijk overzicht van wat er ontbreekt.

Alleen-lezen velden. Voeg readOnly: true toe om een veld met zijn bewaarde waarde te tonen zonder dat redacteuren het kunnen wijzigen: de naam van een instellingendocument, een vergrendelde slug, een externe id. Het is vergrendeld in zowel de webeditor als de iOS-app, en de server zet bij elke keer opslaan de bewaarde waarde terug, zodat een alleen-lezen veld nooit kan verschuiven, hoe een wijziging ook binnenkomt.

- name: title
  type: string
  label: Name
  readOnly: true            # shown, but editors can't change it

Tekst per taal. De interface past zich aan de gekozen taal van elke redacteur aan, en jij kunt hetzelfde doen voor je eigen teksten in label, help en placeholder. De kale sleutel ziet iedereen; voeg een achtervoegsel _<language> toe (een van en, nl, fr, de, es) om die te vervangen voor redacteuren die die taal gebruiken. Er wordt niets automatisch vertaald: je schrijft elke variant zelf.

- name: title
  type: string
  label: Titel              # shown to everyone
  help: De titel van je pagina
  help_en: Your page title  # only for editors using English
TypeBeschrijvingVoorbeeld
stringEén regel tekst.{ name: title, type: string, required: true }
textPlatte tekst over meerdere regels, als tekstvak.{ name: summary, type: text }
markdownOpgemaakte tekst in de rustige editor, bewaard als Markdown.{ name: body, type: markdown }
numberEen geheel of decimaal getal.{ name: price, type: number }
booleanEen schakelaar ja/nee.{ name: featured, type: boolean, default: false }
dateEen kalenderdatum. default: now vult vandaag in.{ name: date, type: date, default: now }
datetimeEen datum met een tijdstip.{ name: published_at, type: datetime }
selectEén keuze uit een vaste lijst, via options.{ name: category, type: select, options: [news, recipes] }
multiselectMeerdere keuzes uit een vaste lijst, als vinkjes.{ name: categories, type: multiselect, options: [news, events] }
urlEen webadres, gecontroleerd als https://…{ name: website, type: url }
emailEen gecontroleerd e-mailadres.{ name: contact, type: email }
colorEen kleurkiezer, bewaard als #rrggbb.{ name: accent, type: color }
imageEen geüploade afbeelding, gecommit naar de mediamap.{ name: cover, type: image }
listEen herhaalbare lijst items; of is een velddefinitie voor het type item.{ name: tags, type: list, of: { type: string } }
objectEen groep geneste velden, gedefinieerd met fields.{ name: cta, type: object, fields: [...] }
relationEen verwijzing naar een item in een andere folder-collectie (bewaart de slug).{ name: author, type: relation, collection: team }
blocksStapelbare secties met verschillende vormen (elk item draagt een sleutel _type; overschrijf de sleutel met typeKey).{ name: sections, type: blocks, blocks: [...] }
hiddenNiet zichtbaar voor redacteuren; wordt meegeschreven met een vaste waarde (bijvoorbeeld een layoutsleutel).{ name: layout, type: hidden, default: post }
codeEen codeblok in monospace, met een optionele taalhint.{ name: snippet, type: code, language: js }
mapEen locatie, bewerkt als breedte- en lengtegraad (bewaard als 'lat,lng').{ name: location, type: map }
computeEen alleen-lezen waarde afgeleid van een sjabloon; elk {{token}} is de naam van een veld op hetzelfde niveau.{ name: ref, type: compute, template: '{{brand}}-{{sku}}' }
keyvalueWillekeurige paren sleutel/waarde, bijvoorbeeld extra metadata.{ name: meta, type: keyvalue }
seoSEO-paneel: titel + beschrijving (met lengtehints), canonieke URL, OG-afbeelding en noindex, met een voorbeeld van het zoekresultaat en van de JSON-LD.{ name: seo, type: seo }
componentKlapt een herbruikbare veldgroep uit de map components op het hoogste niveau uit.{ name: cta, type: component, component: callToAction }

list en object zijn te combineren: een list van object geeft je herhaalbare groepen velden, zoals de openingsuren per week in het voorbeeld hierboven.

Validatieregels

Naast required kan een veld precies de beperkingen overnemen die het schema van je generator oplegt, zodat fouten in de editor met een vriendelijke melding opgevangen worden, en niet door een mislukte build. Getalvelden nemen min, max en integer: true; tekstvelden (string, text, markdown, code) nemen minLength, maxLength en een reguliere expressie in pattern; list en multiselect nemen minItems en maxItems. Overtredingen verschijnen tijdens het schrijven bij het veld (een optioneel veld mag leeg blijven; de regels gelden zodra er iets in staat), en dezelfde regels draaien bij elke keer opslaan op de server.

- { name: title,  type: string, required: true, maxLength: 70 }
- { name: rating, type: number, min: 1, max: 5, integer: true }
- { name: sku,    type: string, pattern: "^[A-Z]{2}-\\d{4}$" }
- { name: tags,   type: list, of: { type: string }, minItems: 1, maxItems: 8 }

blocks definiëren

Met een veld blocks stapelen redacteuren secties met verschillende vormen (een hero, een citaat, een galerij…). Som de sectietypes op onder blocks; elk is een benoemde groep fields (met een optioneel label), net als een object:

- name: sections
  label: Page sections
  type: blocks
  blocks:
    - name: hero
      label: Hero
      fields:
        - { name: heading, type: string, required: true }
        - { name: image,   type: image }
    - name: quote
      label: Pull quote
      fields:
        - { name: text,   type: text, required: true }
        - { name: source, type: string }

Elk item dat een redacteur toevoegt, legt onder een sleutel _type vast welk blok het is (hero, quote, …), zodat je generator weet hoe hij elke sectie moet weergeven. Verwacht je schema een andere sleutel (de z.discriminatedUnion('type', …) van Astro, of content die uit Decap CMS komt, dat ze als type bewaart), zet dan typeKey op het veld (typeKey: type) en PullPress leest, schrijft en controleert die sleutel. De import uit Decap zet dit automatisch, zodat geïmporteerde items ongewijzigd blijven werken.

Voorwaardelijke velden (showIf)

Elk veld kan showIf declareren om alleen te verschijnen als een veld op hetzelfde niveau overeenkomt. Verborgen velden worden leeggemaakt (zodat ze nooit in de frontmatter belanden), en de regel required van een voorwaardelijk veld geldt alleen zolang het zichtbaar is.

fields:
  - { name: hasCta, label: Add a call to action?, type: boolean }
  - name: ctaUrl
    label: Button URL
    type: url
    required: true
    showIf: { field: hasCta, truthy: true }   # equals / oneOf also work

Geneste collecties

Zet nested: true op een folder-collectie om ook items uit submappen te verzamelen (tot vijf niveaus diep) en ze gegroepeerd per map te tonen: handig voor documentatiebomen. Zoeken en de kiezers voor relaties pikken de geneste items automatisch op.

collections:
  - name: docs
    label: Documentation
    type: folder
    folder: content/docs
    nested: true            # gather entries from subfolders too
    fields:
      - { name: title, type: string, required: true }
      - { name: body,  type: markdown }

Herbruikbare veldcomponenten

Definieer één keer een benoemde veldgroep onder de map components op het hoogste niveau en zet ze in elke collectie met een veld component. Bij het laden klapt ze uit tot een gewone objectgroep, zodat een wijziging aan de groep elke collectie bijwerkt die ze gebruikt.

components:
  callToAction:
    - { name: label, type: string }
    - { name: url,   type: url }
collections:
  - name: pages
    type: folder
    folder: content/pages
    fields:
      - { name: title, type: string, required: true }
      - { name: cta,   type: component, component: callToAction }

Waar media staan (en waarom niet in S3)

Uploads worden in je repository gecommit, naast de content die ernaar verwijst. Dat is een bewuste keuze, geen ontbrekende functie: de repo blijft de enige bron van waarheid, afbeeldingen reizen met hun berichten mee door branches, nakijken en geschiedenis, en wie de repo kloont, heeft de volledige site, zonder tweede systeem om te back-uppen of voor te betalen.

Een CDN of verkleinen van afbeeldingen nodig? Laat je generator of host dat bij de build doen (de beeldpijplijnen van Astro en Next.js, de image CDN van Netlify, fetch-URL’s van Cloudinary). PullPress bewaart het origineel in Git; je build maakt het snel. Uploads worden voor het versturen in de browser verkleind tot webformaat, zelfs een telefoonfoto van 20–50 MB, wat repo’s in de praktijk ruim klein houdt. Externe objectopslag (S3 en co.) zou de garantie “je repo is alles” breken, dus die plannen we niet.

AI-vindbaarheid (llms.txt)

PullPress kan een llms.txt in de hoofdmap van je repository maken: een index in platte tekst van je site (de naam, de URL en per folder-collectie een lijst titels) die AI-antwoordmachines lezen om je site correct samen te vatten en te citeren. Ze wordt deterministisch opgebouwd uit je contentmodel (geen AI-aanroep, er verlaat niets je repo).

Beheerders vinden dit in het paneel Instellingen → AI-vindbaarheid (GEO) van een site, dat ook een score voor gereedheid toont: of je site-URL ingesteld is, of llms.txt er staat, of je gepubliceerde content en velden voor beschrijving/SEO hebt, en (als je site-URL bereikbaar is) of de homepage een meta description, Open Graph-tags en JSON-LD toont. Klik op llms.txt bijwerken om het bestand (opnieuw) te maken; het wordt gecommit zoals elke andere wijziging: langs het nakijken, of op sites die direct publiceren rechtstreeks naar je standaardbranch.

Dit vult brandvoice.md hieronder aan: de merkstem bepaalt hoe AI voor je schrijft, terwijl llms.txt AI helpt te vinden en citeren wat je al publiceerde. Het beheert geen robots.txt of sitemap; laat die aan je generator over.

Merkstem voor AI-agents

Zet een optionele brandvoice.md in de hoofdmap van je repository om de toon van de site, gewenste en verboden woorden en het schrijfniveau te beschrijven. AI-agents die via MCP gekoppeld zijn, kunnen het lezen (via de tool get_style_guide) en volgen bij het opstellen van content. Omdat het een gewoon bestand in je repo is, heeft het versiebeheer en wordt het nagekeken zoals elke andere content, en per klant uniek.