Een blok is twee dingen: een schema dat zegt wat het opslaat, en een component dat het rendert. Verder niets. Je schrijft het in je eigen repo, in TypeScript, via dezelfde `defineBlock`-API die de ingebouwde blokken gebruiken — als een kernblok iets kan wat het jouwe niet kan, is dat een bug in de API.
Het hele ding
import { defineBlock, registerBlocks, CORE_BLOCK_DEFS, Editable, useEdit, inlineFormat, Grid, TextInput } from "@vinumcms/react";import { safeHref, type Block } from "@vinumcms/core"; type CalloutBlock = Block & { type: "callout"; body: string; href: string }; const callout = defineBlock<CalloutBlock>({ type: "callout", label: "Callout", group: "This site", create: () => ({ body: "Something worth saying", href: "" }), sanitize: (raw) => ({ body: typeof raw.body === "string" ? raw.body : "", href: safeHref(raw.href), }), Settings: ({ block, patch }) => ( <Grid cols={1}> <TextInput label="Link" value={block.href} onChange={(v) => patch({ href: v })} /> </Grid> ), Render: ({ block, set }) => ( <aside className="callout"> <Txt value={block.body} onChange={(v) => set({ body: v })} /> </aside> ),}); export const BLOCKS = [...CORE_BLOCK_DEFS, callout];registerBlocks(BLOCKS);Dat is een compleet, bewerkbaar blok: het verschijnt in het palet, het slaat op, het saneert, en de tekst wordt op de pagina zelf getypt.
Bewerkbare tekst
`Editable` is wat een veld bewerkbaar maakt op het canvas. Je kiest er per veld voor — een veld dat je als platte tekst rendert, is alleen via het instellingenpaneel te bereiken, en dat is meestal niet wat je wilt.
function Txt({ value, onChange }: { value: string; onChange: (v: string) => void }) { const ed = useEdit(); if (!ed.edit) return <>{inlineFormat(value)}</>; return <Editable value={value} onChange={onChange} />;}`set` doet niets in weergavestand, dus koppel het onvoorwaardelijk. `inlineFormat` rendert de syntaxis `vet cursief markering link` — vergeet het en bezoekers zien letterlijke sterretjes.
Vier regels die geen stijlvoorkeuren zijn
Registreer in een module die `root.tsx` importeert. Blokdefinities bevatten functies, dus ze kunnen niet mee via loaderdata, en de editor rendert blokken in de browser. Registreer je alleen op de server, dan is het palet in de admin leeg.
`sanitize` is een vertrouwensgrens. Het krijgt de JSON die de editor heeft gepost — en dat is alles wat iemand met een editoraccount kan versturen. Dwing elk veld af naar het juiste type, gooi nooit een fout, en laat nooit een waarde ongemoeid door. Haal URL's door `safeHref` en `safeSrc`.
Gebruik een type-alias, geen interface. `type CalloutBlock = Block & {…}` — een interface krijgt geen impliciete indexsignatuur en is daardoor niet toewijsbaar aan het open `Block`-type.
Een bloktype verwijderen wist content. `sanitizeBlocks` laat blokken vallen waarvan het type niet geregistreerd is, dus een blok uit het palet halen haalt het bij de eerstvolgende opslag van elke pagina weg. Behandel het als een contentmigratie.
Knoppen die je gratis krijgt
Declareer `variants` en de admin rendert de keuzemenu's, de sanitizer valideert ze, en elk thema stylet het resultaat al — zonder dat je je renderer aanpast:
variants: ["align", "tone"],`align` zet de tekst links, gecentreerd of rechts. `tone` bepaalt op welk paar van achtergrond en voorgrond het blok staat. Je kiest voor de woordenschat; je kunt er niets aan toevoegen, en dat is wat voorkomt dat een blok een combinatie oplevert die geen enkel thema heeft gedefinieerd.
Waar sanitize zijn geld verdient
Het draait bij elke opslag, dus het is meteen je migratiepad: voeg een veld met een fallback toe en oude blokken pikken het op zodra iemand ze weer opslaat. Een blok waarvan de content zijn code overleeft, moet defensief lezen — deze site heeft zijn eigen productiedeploy ooit onderuitgehaald door een veld te renderen dat oudere opgeslagen records niet hadden.
Verder
De volledige naslag — herhaalde velden met toevoegen en verwijderen ter plekke, instellingen die op één element slaan, de mediakiezer en de API voor e-mailblokken — staat in `docs/BLOCKS.md` in de repository.