GSID CMS — handbook
Reference documentation for the GSID CMS, split into two tracks by audience:
- Designers — theme authors. You write Twig + Tailwind inside a sandbox, deploy via git tag.
- Operators — admins configuring tenants in the SPA. Modules, payment gateways, mail-from settings, hosts, security defaults.
If you're new, the glossary at the end covers vocabulary the rest of the docs assume.
Designers track
Read in this order:
- getting-started.md — directory layout, the Twig sandbox boundary, how a theme is built and deployed.
- twig-helpers.md — every Twig function a theme can call. The sandbox blocks everything else.
- field-types.md — payload shape and rendered markup
for every
field()type (html, text, image, gallery, video, slideshow, compare, embed, divider). - forms.md — server-rendered forms end to end: template helpers, the protection stack, mail-from resolution, what the editor configures, what arrives in the inbox.
- per-page-modules.md — the
cms_module()opt-in JS+CSS system (carousel, lightbox, slideshow, video). How to add new ones. Note: these are frontend JS modules, not the backend modules the operators track describes — see the glossary. - tailwind-and-styles.md —
styles.src.css,@sourceglobs, the deploy-time Tailwind compile. - multi-locale.md (designer half) — locale-prefixed
URLs, the
page_translations()switcher,seo_hreflang(), which routes are NOT locale-prefixed. Skip if every website you touch is single-locale. - do-and-dont.md — prescriptive rules. Read this last; it makes more sense once you've read the rest.
Working with Claude on a theme? See theme-authoring.md — Claude-facing instructions: which docs to read for which task, hard rules, sandbox-error checklist, escalation patterns. Drop a 1-line CLAUDE.md in the theme repo pointing at it so Claude reads it on every session.
What designers do NOT do
- Write PHP. Themes run inside a Twig sandbox; arbitrary PHP isn't reachable.
- Add new Twig tags, filters, or functions. They're allowlisted in
cms-core/src/Theme/ThemeSandboxPolicy.php— additions need a cms-core release, not a theme deploy. - Add new field types. Same constraint — see field-types.md → adding a new field type.
- Touch the database. The page's data arrives in the
pageTwig variable; that's the entire input. - Add bundlers, npm dependencies, or a build step beyond Tailwind. Theme
JS, when needed, is a plain ES module shipped in the tarball and loaded
via
theme_asset('app.js').
Operators track
Read in this order:
- operators/index.md — admin tour, login, active-website concept, the per-website Settings tab.
- operators/security.md — form-protection stack (rate limit, signed schema, time-gate, CSRF, honeypot), mail-from refusal model, JWT auth.
- operators/modules/index.md — what backend modules are, how the toggle + dependencies work, how to enable one.
- operators/modules/blog.md — articles
- tags: scheduled publishing, translations, RSS, theme helpers. No per-website settings; everything lives on the entities.
- operators/modules/catalog.md — products, categories, tags, tax classes, variants. No per-website settings; everything lives on the entities.
- operators/modules/customer.md — registration, login, address book, customer-channel mail sender.
- operators/modules/cart.md — anonymous
- persistent carts. No settings.
- operators/modules/checkout.md — shipping, payment gateways (COD / Mollie / Stripe), order-notification recipients, order-channel mail sender, packing slips, and invoices and credit notes.
- operators/modules/tax.md: the VAT engine, VAT by destination for the Netherlands and Belgium, the VAT profile, 0% for businesses abroad with VIES, the VAT return figures, and proof of transport.
- operators/modules/shipping.md: carriers, delivery zones by country or postcode, rate tables, and refusing an order you cannot deliver.
- operators/modules/delivery-slot.md: delivery moments, the day and time window a shopper picks at checkout, the cut-off and lead time, closed dates, and what a theme with its own checkout template has to add.
- operators/modules/discount.md: promo codes, percentage / fixed / free-shipping / BOGO, validity window, capped redemptions, frozen onto the order.
- operators/modules/catalog-discount.md: automatic price rules, sales and volume tiers, no code entered. Stacks with the promo codes above.
- operators/modules/giftcard.md: gift cards sold as a product, mailed on payment, spent as a balance at checkout, and why a card is not a discount.
- operators/modules/portfolio.md: photo collections backed by media folders.
- operators/modules/faq.md: questions and answers, optional categories, search, and why each language keeps its own list instead of translating one.
- operators/modules/newsletter.md: double-opt-in capture and throttled bulk campaigns.
- operators/modules/review-request.md: one email per order some days after shipping, asking for a (Google) review, who is not asked, and the rules a shop must keep.
- operators/modules/abandoned-cart.md: one email some hours after a shopper left their cart, who is reached, and how the link puts the cart back.
- operators/modules/vehicle-lookup.md: license plate to brand/model/year on a form field.
- operators/modules/seo.md: the head tags, structured data and sitemaps every site has by default, and what switching them off does (and does not) do.
- operators/cookie-consent.md: the cookie wall, what the banner records, what Reject actually stops, and when to bump the policy version.
- operators/under-construction.md: holding a site behind a "coming soon" page while it is built, who still reaches it, and why that is a different switch from hiding it from search engines.
- operators/bol-sync.md: the bol.com integration, stock up, orders down, why SKU must be the EAN, and the dry run to check it before the first real sync, orders priced at what the customer paid on bol, and re-pricing older imports.
- multi-locale.md (operator half): what changes when a website has more than one locale: URL shape flips, the Translations panel, translation groups, which routes do NOT get a locale prefix.
What operators do NOT do
- Edit theme templates. That's a designer job — operators see the rendered result in the inline editor and edit content inside the fields the template defines.
- Configure mail transport (SMTP, DKIM, SPF). That's an infra job — the
admin only sets the per-website From: address; the relay itself
is configured in
cms-public's environment. - Run database migrations. Migrations are append-only and shipped with cms-core releases; deploys run them automatically.
Where things live
| Repo | What it owns |
|---|---|
gsid/cms-core (Composer bundle) |
Field renderer, Twig helpers, sandbox policy, shared partials, Website + Page + Media + MenuItem entities. Also the optional backend modules (blog, catalog, customer, cart, checkout, shipping, delivery moments, discount, catalog discounts, gift cards, landing pages, portfolio, FAQ, newsletter, review requests, abandoned cart emails, vehicle lookup, VAT engine, bol.com, SEO). Each ships entities, controllers, mailers, theme templates, sandbox additions. |
gsid/cms-public (Symfony app) |
Public site renderer, theme deploy worker, static frontend JS+CSS modules. |
gsid/cms-api (Symfony app) |
Admin REST API, JWT auth, page render endpoint for the editor. |
gsid/cms-admin (Vue 3 SPA) |
The editor UI. Edits land in cms-api; previews come from cms-api's render. |
gsid/cms-worker (Symfony app) |
Consumes the Messenger queue: theme deploys, video conversion, newsletter batches, backups, bol.com sync, the scheduled jobs. Nothing user-facing, but a stopped worker is why a queued job never finishes. |
<your theme repo> (e.g. ceems/pmumelanie) |
The Twig templates + Tailwind source for one website's theme. |
A theme's tarball is downloaded from Gitea (or cloned from another git
host, for a theme repository given as an HTTPS URL) on deploy and extracted into
/data/cms/themes/<themeCode>/<version>/. Activation flips
Website.themeVersion — version dirs are immutable.
Glossary
The codebase uses two senses of "module" that aren't interchangeable. Both docs tracks reference them, so it's worth pinning down:
- Backend module: an optional feature shipped by
gsid/cms-core(blog, catalog, customer, cart, checkout, shipping, delivery moments, discount, catalog discounts, gift cards, landing pages, portfolio, FAQ, newsletter, review requests, abandoned cart emails, vehicle lookup, VAT engine, bol.com, SEO). Toggled per-website on the admin's Modules tab. Each one contributes Twig helpers, entities, controllers, routes, and (sometimes) per-website settings. Documented in operators/modules/. - Per-page frontend module — a small JS+CSS pair in
cms-public/public/assets/that opts into being shipped on a specific page viacms_module('<name>')from a Twig template. Today:carousel,lightbox,slideshow,imagerail,video,reviews,portfolio,faq,variant_picker,address_autocomplete,vehicle. Documented in per-page-modules.md.
Other terms used throughout:
- Theme — Twig templates + Tailwind source for one website. Lives in its own git repo, deployed by tag. Designers own the theme; operators trigger deploys via the admin.
- Website (a.k.a. tenant) — one row in the
websitetable. Owns hosts, pages, theme, modules, mail-from, cookie wall. - Page — one row in the
pagetable, per locale per slug per website. Carries fields (the editable content blocks the template renders) + SEO metadata + nav placement. - Field — an editable content block on a page. Type drives the editor UI and the rendered markup; see field-types.md.
- Inline editor — the iframe-based editor in the admin SPA. Shows the
page rendered by cms-api with
data-cms-field-namemarkers; the SPA attaches contentEditable + cog dialogs on top.