Update-Manager
Aktuelle Version
3.5.0-beta.11-b751dd2c
Update verfügbar
3.6.0
minorVeröffentlicht: 2026-09-24
Total CMS 3.6 is a publishing release. Every site gets search-engine and social metadata from one call in its layout, a video field, an undo history on every record, and public forms that no longer load the admin. Operators get podcasting, Markdown storage and inline editing in the collection table, and extension authors get Composer packages, agent skills and JavaScript field types.
Highlights
- Core SEO —
{{ cms.seo.head(page) }}emits the title, description, canonical, robots, Open Graph, Twitter and JSON-LD for any page, driven by an SEO card on each record and a Site SEO record for the defaults: templated titles and descriptions, favicons from one upload, free-form head tags for site verification, IndexNow submissions so Bing and other engines recrawl changes quickly, and a generator tag you can switch off - Video field — paste a YouTube, Vimeo, Bunny, Cloudflare Stream, Loom, Wistia, Livid, Publitio or Jet-Stream URL, or a direct file, and render it responsively with
cms.render.video(), inside cards and decks and in CSV too - Undo history — every save and delete keeps the version it replaced;
tcms backup:listandtcms backup:restoreput any of them back as an ordinary save - Public forms without the admin bundle —
cms.form.builder()on a public page now loads a runtime under 50 KB compressed instead of about 300 KB, with heavier fields loading their own code only when a form uses them - Podcasting — a bundled extension with show and episode schemas and a feed Apple and the directories accept, several shows per site
- Markdown storage — a collection can keep each object as a Markdown file with YAML frontmatter, and
tcms collection:convertmoves an existing one either way - Editing in place — edit a single value from the collection table, gated by a site switch and an access-group permission, plus HTMX fragment helpers and recipes for building the same on your own pages
- Typesetting — the
|typographyfilter sets quotes, dashes, ellipses and non-breaking spaces the way the site's locale expects, without touching markup or code - Extensions — install one with
composer require, ship an agent skill with it, and give a custom field type its own JavaScript - WebMCP (experimental) — make forms callable by an AI agent in the visitor's browser and hand it the MCP server's read tools, in Chrome 149+ during the origin trial; off by default
Upgrade notes
- Site Builder page descriptions and images moved to the SEO card. Existing pages are migrated automatically on the first request after updating, files included. Templates that read
page.descriptionorpage.imagemust switch topage.seo.descriptionandpage.seo.image; a layout that only callscms.seo.head(page)needs nothing. Update both ends before runningtcms pushortcms pullbetween sites. See the Changed section for detail - Dynamic OAuth client registration is now on by default, so AI clients such as Claude and ChatGPT can connect without a static client. The consent screen shows where each client sends the code. Turn it off under Settings → OAuth Server if you only use static clients
tcms collection:export --format=csvwrites the admin's CSV shape, with cards flattened into dot-notation columns. Scripts that parsed the old JSON-in-one-column shape need updating- RSS and WordPress imports slugify like the rest of Total CMS. Entries with non-ASCII titles get a new id once, so a feed polled after updating may import those entries again
- The
sortByfilter now sorts strings case-insensitively, matchingsortByKey. Lists that relied on capitalized names sorting first will reorder - A fieldset nested inside another fieldset now renders nested. Schemas that nested two fieldsets by accident will see a layout change
- The SEO head prints
<meta name="generator" content="Total CMS">. White-labeled sites can turn it off with Emit generator tag on the Site SEO record - Object backups are on by default, under
tcms-data/.system/backups/objects/, keeping the ten newest versions for up to 30 days. They are JSON only, never uploaded files. Tune or disable them under Settings → Backups composer updatenow runstcms deployon Composer installs: it clears the compiled container and every cache and runs pending migrations. Deploy scripts that already run it can drop the step- Extension authors: a custom field type that reached a service through its form now uses
$this->form->services(), and the HTTP client is Guzzle 8, which wraps exceptions thrown from progress callbacks
Security
-
One permission engine.
AccessControlServicecarried a second, session-flavoured copy of every rule inUserAuthority(fourteen near-identical methods) and the two had drifted. The session methods now resolve the caller'sUserAuthorityand ask it, so a rule exists once. Two rules that only the session side had now apply to OAuth and MCP callers as well: the super-admin-only utils (jumpstart,permission-matrix) cannot be granted by an access group on any path, and per-extension access follows the group'sextensionspermission -
The OAuth client and grant files are written like the API-key file. Both repositories hand-rolled their JSON store: no lock around a read-modify-write, an ignored write failure, and a file that no longer parsed was silently replaced on the next write. They now sit on
AtomicJsonStorethrough a sharedJsonListRepository: locked, atomic, private (0600), and a corrupt file is refused rather than overwritten. The access-groups file gets the same treatment -
The session lifecycle policy lives in
SessionActivityTracker. Activity tracking, id rotation every quarter of the lifetime, and idle expiry (with the remember-me exemption) were a private method duplicated in the two auth middlewares, untested; the tracker is shared and covered -
A revoked OAuth token stays revoked for its whole lifetime. The revocation list and the replay detector sized their caches at a fixed one hour and 30 days, so a token issued with a longer lifetime became valid again once its id fell out of the cache. Both now derive their retention from the configured access, refresh and auth-code lifetimes
Added
-
Image and file columns in a deck table. An
imageorfileproperty in a deck table's item schema now works: a saved row shows a small square thumbnail (or the file's type icon and name), an empty cell is a drop target, and hovering shows a 2×2 grid of edit, links, download and delete, with the field's usual edit dialog behind the pencil; replacing a file is a drop onto the cell. Uploads into a row that has not been saved yet wait for the save, as in a deck item; a row deleted from the table has its files cleaned up on save. Four things stood in the way: the upload path resolver only knew deck items with a dialog, a new row never deferred its uploads, a saved row's thumbnail rendered from the wrong path, and a cloned row's dialog kept the template's ids — which also broke validation on any cloned row whose dialog held a password field. Both decks now share one id-regeneration routine -
An extension can ship an agent skill. A
skill/directory next toextension.json—SKILL.mdplus optional references, the core skill's layout — is installed to.claude/skills/{vendor}-{name}/while the extension is enabled, by the sametcms skill:installthat installs the core skill: same fingerprint stamp, same zip-layout path rewrite, same--check, refreshed by the Composer plugin on everycomposer update. Enabling or disabling the extension installs or removes the folder at once; folders the command did not write are never touched. A skill is instructions to an agent, so the pre-enable review now shows its full text next to the source-code findings before the operator consents -
composer requireinstalls an extension. A Composer package of typetotalcms-extensionis discovered from its directory undervendor/and loaded like any other extension: Composer autoloads its classes,composer updatemoves it,composer removetakes it out, and nothing is copied intotcms-data/. It reports the version Composer installed, shows a Composer badge in the admin and acomposersource intcms extension:list, and can be disabled but not removed here —extension:removenames thecomposer removecommand instead. On an id collision the project copy wins over Composer, which wins overtcms-data/extensions/, which wins over bundled. The extension-starter repo already carries thecomposer.jsonshape; publishing to Packagist is all an author adds -
composer updaterunstcms deployfor you.totalcms/cmsis a Composer plugin, and it now runs the deploy cleanup — wipe the compiled DI container, clear every cache, run pending migrations — after everycomposer update, printing what it cleared. Until now the operator had to remember, and a forgotten deploy after a version bump meant a stale compiled container and aTypeErroron the first request. It does not run after a plaincomposer installof an unchanged lockfile, which needs neither; it never aborts the composer run; and the PHP-FPM reload remains the deploy script's job -
The SEO head names Total CMS as the generator.
cms.seo.head()andcms.seo.meta()print<meta name="generator" content="Total CMS">, the tag Wappalyzer, BuiltWith and the CMS market-share surveys read to know what a site runs on. The name only, never a version number: the name is what a directory needs, a version is what a vulnerability scanner reads. A new Emit generator tag toggle on the Site SEO record, on by default, turns it off for white-labeled sites -
A
|typographyTwig filter sets prose the way a typesetter would. Straight quotes become curly in the site locale's style (English, German, Swiss/Italian/Spanish guillemets, French with its narrow no-break space, Polish, Scandinavian), apostrophes and inch marks are told apart from quotes,--and---become dashes,...an ellipsis,1024x768gets a real ×,(c)a ©, numbers stay glued to their units and titles, and the last two words of every paragraph and heading are joined so no lone word hangs on the last line. It is HTML-aware — attributes,code,pre,scriptand comments are never touched — so it runs on styledtext,|markdownoutput and plain strings alike, and it is idempotent, so hand-typed proper glyphs survive. Fractions, ordinals and Typogrify-style CSS hooks are there but off, since they change markup. Nothing is stored: the change is render-time only -
The image meta dialog has Save and Discard Changes buttons. Save keeps your edits — the image field autosaves when the dialog closes — and until now that was the only way out. Discard Changes puts every field in the dialog back to what it held when the dialog opened, as already-saved values, so the close-time autosave has nothing to send and a later Save of the form has nothing of the abandoned edit to sweep up. That last part is why a simple "don't autosave" would not have done: the edit would still have been sitting in the fields. Escape discards too; clicking outside the dialog saves, like Save. Same buttons on the gallery's shared dialog. A featured-star click is not a dialog edit (it saves itself), so discarding leaves it alone. Requested as #111
-
Every object now has an undo history. Each save keeps the version it replaced and each delete keeps the final state, under
tcms-data/.system/backups/objects/— records only, never uploaded files, so the footprint stays the size of the JSON. It is on by default and needs no setup. Two commands read it back:tcms backup:list <collection> <id>shows the snapshots newest first, andtcms backup:restore <collection> <id> <snapshot>(or--latest) puts one back as an ordinary save — the index rebuilds, listeners fire, and the state being replaced is itself snapshotted, so a restore is never a one-way door. A deleted object's history survives the delete, and restoring recreates it. Retention is count and age (defaults: the ten newest, nothing older than 30 days) — count alone let one busy afternoon evict last week's version; age alone let a never-edited record hold a snapshot forever. Tune or disable under Settings → Backups, or underbackupsinconfig/tcms.php. Sync's pre-overwrite snapshots (which already existed) now land in the same tree, and the class behind them,SyncBackupService, has become the sharedBackupStore. Imports never write snapshots. Underneath it,object.deletednow carries the deleted record asprevious— until now every listener on that event was blind to what had been deleted. See Backups -
cms.render.picture()renders a responsive image. The same three arguments ascms.render.image(), returning a<picture>with one<source>per modern format — AVIF and WebP by default — each carrying asrcsetof ImageWorks candidates at 480, 768, 1024, 1440 and 1920 pixels, then an<img>fallback in the image's own format that carries the samesrcsetso a browser without<picture>support still picks a size. Everywdescriptor is the width ImageWorks will actually deliver, not the one asked for: ImageWorks never upscales, so a candidate wider than the source would deliver the source's own width and lie to the browser about it — those are dropped, and the source width joins as the largest candidate.widths,formatsandsizesride in the options, or site-wide underimageworks.picture; awin the transforms is a ceiling on the largest candidate. A GIF gets no<source>at all, since re-encoding would drop its animation. See Render → picture() -
IndexNow tells search engines what just changed. A toggle in the SEO Site Collection (Collections → Seo Site, beside Emit JSON-LD — not the admin Settings groups); with it on, every publish, edit and delete of a sitemap-listed URL is submitted to the IndexNow network, which one submission reaches in full — Bing, Yandex, Seznam, Naver. Google does not take part, so it complements the sitemap rather than replacing it. What goes out is decided by the sitemap's own rules, applied to the one record that changed — sitemap on, include/exclude filters, no No Index — so a crawler is never told two different things about one URL, and a post moved back to draft or deleted is submitted too, so it is recrawled and dropped quickly. Imports count too — a CSV that publishes a hundred posts submits them together when it completes. Submissions are queued for
tcms jobs:process, never sent during a save, and coalesced: everything that changed between two runs goes out as one request (up to the protocol's 10,000 URLs), and a record saved five times in that window is submitted once; a rate limit puts the unsent URLs back for the next run, a rejection is logged once, and clearing the job queue discards the pending submissions with it. The verification key is generated on the first save with the toggle on and served at/{key}.txt. See Core SEO → IndexNow -
Accordions in the form grid. A schema's
formgridcan now collapse sections of its admin form:>>opens a panel,<<closes the group, and consecutive panels before a<<form one accordion. The<<is what defines the group, and its size decides the resting state - one panel renders closed, which is how you tuck advanced fields out of the way, while two or more render with the first open and only one open at a time. Two<<-terminated runs are therefore two independent accordions, so both shapes come out of one construct with no flags. A panel's interior is its own mini-grid: dividers, headers and[[ ]]fieldsets all work inside one. Panels cannot nest, not by rule but by grammar ->>always ends the panel it appears in - and an unterminated group simply runs to the end of the formgrid. Fields that build a widget reading the DOM at construction (alistfield's chips, for one) rebuild themselves the first time their panel opens, once, so a collapsed panel does not leave them half-rendered; a custom field type opts into the same treatment by overridingreinit(). See Form Grid Layout -
A public form no longer needs the admin bundle.
cms.form.builder()on a public page used to needcms.adminAssetsHead()andcms.adminAssetsBody()in the layout, because the form script lived in the admin bundle with every field editor the dashboard can show — around 300 KB compressed for a four-field form. The form runtime is now theformscore frontend feature thatcms.assetsHead()andcms.assetsBody()already emit:forms.cssand a smallforms.jscarrying the light field classes, with a heavier field (styled text, uploads, code, lists, decks) loading its own module the first time a form renders it; the whole feature is under 50 KB compressed. Field classes reach the form runtime through a registry the entry point fills, so the dashboard keeps building every field up front, exactly as before. A site with no public forms drops the pair withformsinfrontendAssets.except;assetsBody()emits the translation catalog and config the script reads whenever the feature is on the page; the admin helpers keep working where a layout still calls them. See What a public form needs -
WebMCP extension (experimental). Makes forms callable by browser-resident AI agents through the WebMCP origin trial in Chrome 149+:
webmcp_form('contact', {name: 'send_message', description: '…'})renders a form annotated with the declarative WebMCP attributes and answers an agent's submit with the form's own save result; tool descriptions come only from schema metadata and the operator's words, autosubmit is opt-in per form, and registration forms are refused. Read tools are the MCP server's own, registered from a statelesstools/listcall — see the Changed entry below for the shipped design. Bundled, off by default. Core gained three generic seams for it: anattributesform option,settings.attributeson any field control, and per-property options on auto-built forms; andsave()in the form runtime now returns its promise. See WebMCP -
Extension field types have a JavaScript half.
addFieldType()made a field type first class on the PHP side, but the admin bundle built every unknowndata-typeas a plain text field, so an extension's field could render but not behave.window.TotalCMS.registerFieldType(type, class)registers a class extendingTotalField(also onwindow.TotalCMS) and the form factory builds it, so the field takes part in unsaved-state tracking, saving and the action chain like a core field. Core type names cannot be replaced
This installation is managed by Composer. Run composer update totalcms/cms to update.