Kwisple's content model is modelled on Craft CMS 5 on purpose: sections, reusable entry types, a field library, Matrix, global sets, drafts and live preview work the way you know them. What changes is where things live. The model is TypeScript in your repository instead of project config YAML, your templates are React or Astro components instead of Twig, and the hosting, redirects, SEO, image transforms, forms and statistics that you assembled from plugins come with the platform.
This page maps one onto the other. The running example is RANSON.N, a Next.js site rebuilt from
a Craft 5 project; its cms.config.ts keeps Craft's field handles so the mapping stays visible.
Concepts
| Craft 5 | Kwisple | Notes |
|---|---|---|
Project config (config/project/*.yaml) | cms.config.ts, defineSchema({ sections, globals, forms, apps }) | Typed TypeScript in the site's repository. Versioned per site on the platform (cms_schema). See Schema as code. |
php craft project-config/apply | kwisple push (live), kwisple dev (development model on every save) | Additive changes apply at once, destructive ones need --force. See Workflow: dev, push and changes. |
allowAdminChanges off in production | always | Editors cannot change the model in the dashboard; it only changes through a push. |
| Content migrations | none needed for the model | A push never deletes content and reads are normalised against the current model. |
| Fields (global field library) | f.plainText(...), f.richText(...), … in cms/fields.ts | Reuse the same object; .with({ label, instructions, required, width }) is the per-layout override. See Fields reference. |
| Multi-instance fields with a handle override | the key in fields | The handle is the object key in each layout, so the same library field can sit under a different handle per entry type. |
| Field layout tabs | tabs: [{ name, fields }] on defineEntryType | Fields not listed land in the first tab. |
| Entry types (reusable in Craft 5) | defineEntryType | The same object in sections and Matrix fields. entry.type is the handle. |
| Sections: single / channel / structure | defineSection({ type: "single" | "channel" | "structure" }) | maxLevels for structures, defaultOrder for channels. |
| Entry URI format | uri on defineSection | Same tokens: {slug}, {parent.uri}, {postDate.year}, {postDate.month}. The homepage URI is __home__. See Localisation and URIs. |
| Section template setting | your view switch on entry.type | Next: src/views/entry-view.tsx; Astro: src/views/. |
| Matrix (nested entries) | f.matrix({ entryTypes }) | Blocks have stable ids, can be disabled, reordered and nested. See Blocks and relations. |
| Global sets | defineGlobalSet → client.globals() | Shown under Info op elke pagina (information on every page). |
| Sites and propagation | the site's locales + translation: "site" | "none" per field | Default locale unprefixed, others under /<locale>/…. One site per website, not one per language. |
craft.entries.section('news').limit(3) | client.entries({ section: "news", limit: 3 }) | Typed: Entry<typeof schema, "news">. See Querying entries. |
relatedTo | client.entries({ relatedTo }) | Backed by a relation index on publish. |
| Element routing (URI → element) | client.entryByPath(path) in the catch-all route | Next: [[...path]]/page.tsx; Astro: resolveEntry(Astro, client). |
Eager loading (.with([...])) | resolved relations in every answer | Entry, asset and link fields arrive resolved. |
| Entry statuses, post date, expiry date | Online vanaf / Offline vanaf (postDate, expiryDate) | The platform purges caches at both moments. |
| Drafts, provisional drafts, revisions | an autosaved draft per editor, a revision per publish | Restoring loads a revision into a draft. |
| Live Preview and preview targets | the site's Preview-URL + createPreviewRoute / the injected /api/preview + LivePreview | The dashboard posts kwisple:preview:refresh after autosave. See Live preview and revalidation. |
| Asset volumes | the client's media library | Images need alt text to be publishable; focal point per asset. |
| Users, user groups, permissions | agency team, client roles and the agency's access policy | Not part of your model. |
| Twig templates | React (Next.js) or Astro components | Server-rendered by default. |
| Dev mode, database dumps for local development | none needed | Your dev server reads the platform's API with a token; no local database. |
Plugins
| Craft plugin | Kwisple | How |
|---|---|---|
| Blitz (static page cache + refresh) | Next data cache with tag kwisple:all, or the Astro in-memory cache, purged by the signed publish webhook | Keep /api/revalidate deployed and set Webhook bij publiceren. Static builds rebuild instead. See Caching. |
| Retour (redirects, 404 log) | Doorverwijzingen (redirects) and the 404 log in the dashboard | A live entry whose URI changes gets a 301 automatically; handleNotFound (Next) and resolveEntry (Astro) follow redirects on a 404 and report the miss. |
| Imager-X (transforms) | imageProps(asset) with next/image, or <Image asset> in Astro | Responsive widths, AVIF/WebP, focal point as object-position, blur placeholder. No named transforms. |
| SEOmatic | the Zoekmachines tab on every entry + metadataFor(entry, site), <Seo> in Astro, sitemap/robots factories, JSON-LD builders | organizationJsonLd, webSiteJsonLd, breadcrumbJsonLd, serviceJsonLd. See SEO, performance and security headers. |
| Formie | defineForm in cms.config.ts + client.submitForm() from your server + the Formulieren inbox | You render the form; the platform validates declared forms, stores submissions, filters spam, mails the business, can confirm to the visitor and hold booking slots. See Forms, bookings and confirmations. |
| Cookie consent plugins | the platform's consent configuration (Cookies & privacy) + @kwisple/sdk/consent | Banner texts, categories, services and the consent log come from the dashboard. See Statistics and consent. |
| Analytics plugins | the platform's cookieless statistics, <Analytics> | No cookies, no third party. |
| Control-panel tools for one client | an app (defineApp) | A page of the client's own site, framed in the dashboard. See Apps overview. |
The RANSON.N model, side by side
RANSON.N's cms.config.ts starts with: "Content model of the RANSON.N site, mirroring the Craft 5
project (migrations/, config/project). Field handles follow Craft's." What it maps:
| In the Craft project | In cms.config.ts |
|---|---|
Single homepage with tabs Hero, Over ons, Diensten, Beeld | defineSection({ handle: "homepage", type: "single", uri: "__home__" }) with entry type homepage and the same tabs (plus Contact) |
Structure diensten | type: "structure", uri: "diensten/{slug}", maxLevels: 1; the structure order is the order on the site |
Dropdowns dienstIllustratie / realisatieDienst | f.select({ options: dienstOptions }) with the same values, translation: "none" |
| Table fields | f.table({ columns: { … } }) |
Texts inline in over-ons.twig, contact.twig, privacy.twig | fields on the singles overOns, contact, privacy: every visible sentence is content |
| Assets in the folder Realisaties with custom fields Dienst and Locatie en jaar | a single realisaties whose fotos matrix holds realisatie blocks with foto, realisatieDienst, realisatieContext, uitgelicht |
| Privacy statement with tables in rich text | a matrix inhoud of tekstblok (rich text) and tabel blocks |
Single Beschikbaarheid | global set beschikbaarheid (weekly schedule, appointment length, closed periods) |
| Business details | global set bedrijf, Bedrijfsgegevens in the dashboard, used for the Electrician JSON-LD |
Craft module actions site/contact/verstuur, site/boekingen/opslaan | Next route handlers api/contact and api/afspraak calling client.submitForm |
| SEO plugin settings | the built-in SEO tab and a site-level default description in lib/seo.ts |
| Control-panel brand tool | the app golfgenerator |
// RANSON.N: a Craft structure section and its entry type
const dienst = defineEntryType({
handle: "dienst",
name: "Dienst",
icon: "zap",
fields: {
dienstTekst: f.plainText({ label: "Korte beschrijving", multiline: true }),
dienstIllustratie: f.select({
label: "Illustratie",
options: dienstOptions,
default: "industrieel",
translation: "none",
}),
dienstIntro: f.plainText({ label: "Intro", multiline: true }),
// …
},
tabs: [
{ name: "Kaart", fields: ["dienstTekst", "dienstIllustratie"] },
{ name: "Detailpagina", fields: ["dienstIntro", "dienstStappen", "dienstCijfers"] },
{ name: "Beeld", fields: ["dienstBeeld", "dienstBeeldLabel", "dienstBeeldTekst"] },
],
});
defineSection({
handle: "diensten",
name: "Diensten",
type: "structure",
entryTypes: [dienst],
uri: "diensten/{slug}",
maxLevels: 1,
});A migration approach
- Create the site and the repository. Start from a starter
(
npm create kwisple-site@latest),kwisple login, thenkwisple linkand pick + Nieuwe website under the client. Only custom-code sites can be linked. - Translate project config into
cms.config.ts. OnedefineSectionper section, onedefineEntryTypeper entry type, onedefineGlobalSetper global set. Keep Craft's handles: your templates, your editors' habits and any import script stay recognisable. Put shared fields incms/fields.ts. - Move inline template text into fields. If editors could not change it in Craft because it was hard-coded in Twig, now is the moment. Keep navigation labels and short UI phrases in code if you prefer; RANSON.N did.
- Run
kwisple dev. The development model follows every save; editors see nothing yet. Fix what validation reports (reserved handles such astitle,slug,status,author; unknown tab fields; duplicate handles). - Port the templates. One view per entry type, switched on
entry.type; one component per Matrix block type, switched onblock.type(a missing block type is a TypeScript error). Keep your HTML and CSS: RANSON.N kept the Craft site's markup and class names. - Push the model.
kwisple push; the dashboard now has the sections, and every single gets its entry. - Bring the content over. For text content, a site kit is the supported
route: write the entries and global values as
cms/seed.jsonand runkwisple seed. A kit carries no assets and no relations between entries; upload images in the dashboard (with alt text) and set relations there. RANSON.N's import was a one-off script on the platform side before the site moved to its own repository. - Forms. Declare each form with
defineFormand submit from your own route handler withclient.submitForm. Notifications go to the business by mail; submissions land in the Formulieren inbox. - Redirects. Add old Craft URLs that changed under Doorverwijzingen (redirects), so search engines and old links land on the new pages.
- Go live. Connect the repository under Hosting & deploys, set the Site-URL and Webhook bij publiceren, add the domain and make it the Hoofdadres (primary address). See Hosting and deploys and Domains and DNS.
What has no counterpart
| Craft | Instead |
|---|---|
| Plugins in general | No plugin system. Generic capabilities come with the platform; one client's custom work is an app in that client's repository. |
| Custom fields on assets | Assets have alt text, focal point and dimensions, nothing else. Put the extra data next to the image in a Matrix block, as RANSON.N's realisatie block does. |
| Categories and tags as element types | A channel or structure section of their own, related with f.entries. |
| Users, addresses, Commerce products as content | Not part of a site's content model. |
| Tables inside rich text | f.table, or text and table blocks alternating in a Matrix. |
| Named image transforms | Responsive images from the asset with imageProps / <Image>. |
Twig, {% cache %}, template hooks | Your framework's components and caching (see Caching). |
| GraphQL API | The REST Delivery API and the typed SDK client. |
| A drag-and-drop form builder for editors | Forms are code: declared in cms.config.ts, rendered by your components. |
| Editing the model in the control panel | Only through kwisple push. |
| A local database per developer | Your dev server reads the platform with a token. |
Habits to unlearn
- Do not reach for a plugin. Check the platform first: redirects, SEO fields, sitemaps, JSON-LD, image optimisation, forms, consent and statistics are there.
- Do not cache by hand. Every SDK read is cached and purged on publish; add your own cache only for your own expensive work.
- Do not expose the token. The SDK runs on the server; forms post to your route handler,
which calls
client.submitForm. - Do not hard-code brand colours. Read them from the brand kit (
--brand-*variables), see Brand kit and privacy data.
Where to go next
- Schema as code and Fields reference.
- Workflow: dev, push and changes.
- Project structure (Next.js), which ends with how the RANSON.N repository is laid out.