Kwisple
Browse the docs

Content model

Coming from Craft CMS

A map from Craft CMS 5 concepts and plugins to Kwisple, a migration approach based on the RANSON.N rebuild, and what has no counterpart.

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 5KwispleNotes
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/applykwisple 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 productionalwaysEditors cannot change the model in the dashboard; it only changes through a push.
Content migrationsnone needed for the modelA push never deletes content and reads are normalised against the current model.
Fields (global field library)f.plainText(...), f.richText(...), … in cms/fields.tsReuse the same object; .with({ label, instructions, required, width }) is the per-layout override. See Fields reference.
Multi-instance fields with a handle overridethe key in fieldsThe handle is the object key in each layout, so the same library field can sit under a different handle per entry type.
Field layout tabstabs: [{ name, fields }] on defineEntryTypeFields not listed land in the first tab.
Entry types (reusable in Craft 5)defineEntryTypeThe same object in sections and Matrix fields. entry.type is the handle.
Sections: single / channel / structuredefineSection({ type: "single" | "channel" | "structure" })maxLevels for structures, defaultOrder for channels.
Entry URI formaturi on defineSectionSame tokens: {slug}, {parent.uri}, {postDate.year}, {postDate.month}. The homepage URI is __home__. See Localisation and URIs.
Section template settingyour view switch on entry.typeNext: 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 setsdefineGlobalSet → client.globals()Shown under Info op elke pagina (information on every page).
Sites and propagationthe site's locales + translation: "site" | "none" per fieldDefault 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.
relatedToclient.entries({ relatedTo })Backed by a relation index on publish.
Element routing (URI → element)client.entryByPath(path) in the catch-all routeNext: [[...path]]/page.tsx; Astro: resolveEntry(Astro, client).
Eager loading (.with([...]))resolved relations in every answerEntry, asset and link fields arrive resolved.
Entry statuses, post date, expiry dateOnline vanaf / Offline vanaf (postDate, expiryDate)The platform purges caches at both moments.
Drafts, provisional drafts, revisionsan autosaved draft per editor, a revision per publishRestoring loads a revision into a draft.
Live Preview and preview targetsthe site's Preview-URL + createPreviewRoute / the injected /api/preview + LivePreviewThe dashboard posts kwisple:preview:refresh after autosave. See Live preview and revalidation.
Asset volumesthe client's media libraryImages need alt text to be publishable; focal point per asset.
Users, user groups, permissionsagency team, client roles and the agency's access policyNot part of your model.
Twig templatesReact (Next.js) or Astro componentsServer-rendered by default.
Dev mode, database dumps for local developmentnone neededYour dev server reads the platform's API with a token; no local database.

Plugins

Craft pluginKwispleHow
Blitz (static page cache + refresh)Next data cache with tag kwisple:all, or the Astro in-memory cache, purged by the signed publish webhookKeep /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 dashboardA 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 AstroResponsive widths, AVIF/WebP, focal point as object-position, blur placeholder. No named transforms.
SEOmaticthe Zoekmachines tab on every entry + metadataFor(entry, site), <Seo> in Astro, sitemap/robots factories, JSON-LD buildersorganizationJsonLd, webSiteJsonLd, breadcrumbJsonLd, serviceJsonLd. See SEO, performance and security headers.
FormiedefineForm in cms.config.ts + client.submitForm() from your server + the Formulieren inboxYou 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 pluginsthe platform's consent configuration (Cookies & privacy) + @kwisple/sdk/consentBanner texts, categories, services and the consent log come from the dashboard. See Statistics and consent.
Analytics pluginsthe platform's cookieless statistics, <Analytics>No cookies, no third party.
Control-panel tools for one clientan 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 projectIn cms.config.ts
Single homepage with tabs Hero, Over ons, Diensten, BeelddefineSection({ handle: "homepage", type: "single", uri: "__home__" }) with entry type homepage and the same tabs (plus Contact)
Structure dienstentype: "structure", uri: "diensten/{slug}", maxLevels: 1; the structure order is the order on the site
Dropdowns dienstIllustratie / realisatieDienstf.select({ options: dienstOptions }) with the same values, translation: "none"
Table fieldsf.table({ columns: { … } })
Texts inline in over-ons.twig, contact.twig, privacy.twigfields on the singles overOns, contact, privacy: every visible sentence is content
Assets in the folder Realisaties with custom fields Dienst and Locatie en jaara single realisaties whose fotos matrix holds realisatie blocks with foto, realisatieDienst, realisatieContext, uitgelicht
Privacy statement with tables in rich texta matrix inhoud of tekstblok (rich text) and tabel blocks
Single Beschikbaarheidglobal set beschikbaarheid (weekly schedule, appointment length, closed periods)
Business detailsglobal set bedrijf, Bedrijfsgegevens in the dashboard, used for the Electrician JSON-LD
Craft module actions site/contact/verstuur, site/boekingen/opslaanNext route handlers api/contact and api/afspraak calling client.submitForm
SEO plugin settingsthe built-in SEO tab and a site-level default description in lib/seo.ts
Control-panel brand toolthe 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

  1. Create the site and the repository. Start from a starter (npm create kwisple-site@latest), kwisple login, then kwisple link and pick + Nieuwe website under the client. Only custom-code sites can be linked.
  2. Translate project config into cms.config.ts. One defineSection per section, one defineEntryType per entry type, one defineGlobalSet per global set. Keep Craft's handles: your templates, your editors' habits and any import script stay recognisable. Put shared fields in cms/fields.ts.
  3. 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.
  4. Run kwisple dev. The development model follows every save; editors see nothing yet. Fix what validation reports (reserved handles such as title, slug, status, author; unknown tab fields; duplicate handles).
  5. Port the templates. One view per entry type, switched on entry.type; one component per Matrix block type, switched on block.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.
  6. Push the model. kwisple push; the dashboard now has the sections, and every single gets its entry.
  7. Bring the content over. For text content, a site kit is the supported route: write the entries and global values as cms/seed.json and run kwisple 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.
  8. Forms. Declare each form with defineForm and submit from your own route handler with client.submitForm. Notifications go to the business by mail; submissions land in the Formulieren inbox.
  9. Redirects. Add old Craft URLs that changed under Doorverwijzingen (redirects), so search engines and old links land on the new pages.
  10. 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

CraftInstead
Plugins in generalNo plugin system. Generic capabilities come with the platform; one client's custom work is an app in that client's repository.
Custom fields on assetsAssets 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 typesA channel or structure section of their own, related with f.entries.
Users, addresses, Commerce products as contentNot part of a site's content model.
Tables inside rich textf.table, or text and table blocks alternating in a Matrix.
Named image transformsResponsive images from the asset with imageProps / <Image>.
Twig, {% cache %}, template hooksYour framework's components and caching (see Caching).
GraphQL APIThe REST Delivery API and the typed SDK client.
A drag-and-drop form builder for editorsForms are code: declared in cms.config.ts, rendered by your components.
Editing the model in the control panelOnly through kwisple push.
A local database per developerYour 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

Last updated 2026-10-30