Skip to main content

KilnCMS 1.2.0

Upgrade notes

  • Building from source now needs Elixir 1.20.4 / OTP 29.1.1 (and Node 22.23.3 for the assets); move your project's .tool-versions to match before mix kiln.update. mix.exs requires elixir: "~> 1.20", so an older Elixir refuses to compile the project, and only after mix deps.compile has run. A project that overlays Kiln and pins its own toolchain (mix kiln.toolchain.check) has to move in the same change. The release image already carries its own runtime, so a deploy that pulls ghcr.io/the-verscienta/kiln_cms or builds the Dockerfile needs nothing. See the toolchain entry.

Added

  • docs/system-requirements.md lists what a machine needs to run the release image, build it, and develop on Kiln. One page for the question that was spread across the README, getting-started.md, deploy.md, the platform guides and the Dockerfile: CPU, RAM and disk for running (512 MB minimum, 1 GB recommended) and for building (2 GB marginal, 4 GB comfortable), Postgres 17 with the vector extension as the one required service, the native libraries the runner image carries (libvips, qpdf, postgresql-client-17) and the optional one it does not (ffmpeg), the optional services behind compose profiles, and the developer toolchain pinned by .tool-versions. Linked from the getting-started prerequisites and registered under Operations & deployment. Three deployment presets under deploy/presets/ (minimal, publishing, everything) turn the runtime features on or off as a set, each a layer on .env.prod.
  • mix kiln.import.ghost imports a Ghost JSON export: posts, pages, tags, images, SEO fields and bylines, members-only posts kept gated, and a redirect from every old URL. KilnCMS.Portability.Ghost reads the export into the same source-neutral shape as the WordPress parser, so it shares that importer's dry run, re-run rules, media sideloading, author mapping and report. --site-url replaces Ghost's __GHOST_URL__ placeholder, and the task refuses to start without it when the export contains one. Posts restricted to members, paid members or tiers import into the member audience, or as drafts if no gated audience is configured. Scheduled and email-only posts import as drafts, and the task lists each one. The shared HTML converter now keeps every image in a figure holding several, so a Ghost gallery card no longer imports as its first image only. (#1876)
  • The importers run from a release: bin/kiln_cms rpc 'KilnCMS.Release.import_wordpress(path, dry_run: true)', and likewise import_ghost/2 and import_content/2. A Docker or Coolify deploy has no Mix, so a migration used to need a source checkout with the production environment loaded. The functions take the tasks' flags as keyword options, refuse an unknown one (so a misspelt dry_run: cannot import for real), and print the same report into the rpc terminal. They refuse under eval, which has no job queue, media fetching or caches. The mix tasks and the release now run the same code, KilnCMS.Portability.Commands, and KilnCMS.Portability.CLI no longer calls Mix; the tasks' output and errors are unchanged. See content portability.
  • Public "Kiln vs" pages for WordPress, Ghost, Strapi, Payload and Directus, plus WordPress and Ghost migration guides, every competitor claim dated and sourced. They live in docs/compare/ and publish to kilncms.dev with the other guides. Each comparison table links a source in every row (a test enforces it), states the date its facts were checked, and names a review owner. The docs publisher now sends a guide's <!-- seo-description: … --> comment as the entry's seo_description. (#1876)
  • A Newsletter sign-up block puts an email sign-up on any page; it adds people to the newsletter list with double opt-in. Until now the only way onto a page was a theme edit: the rich-text sanitizer strips a raw <form>, and the Form block's submissions go to the forms inbox, not the subscriber list. The block has an optional heading and intro, a button label (default "Subscribe") and a switch to also ask for a name. It posts to the existing POST /newsletter/subscribe, so sign-up behaves exactly as before: the shared honeypot, one "check your inbox" page for every outcome, and nothing sent until the reader confirms. The fired :web artifact is the real form, and the :json surface carries action and honeypot_field for a headless frontend. See the newsletter guide. (#1870)

Changed

  • The toolchain moves to Elixir 1.20.4 / OTP 29.1.1 and Node 22.23.3 in CI and the release image; building from source now needs Elixir 1.20+. It was 1.19.5 / OTP 27.3.4.15, held as a floor while development ran ahead on 1.20 / OTP 29. The gap had two costs: OTP 29's stricter dialyzer opacity checks never ran in CI (#599), and 1.20's mix format breaks some lines differently from 1.19's, so downstream overlay PRs failed "Check formatting" on code that was clean locally. .tool-versions, the Dockerfile's builder image (hexpm/elixir:1.20.4-erlang-29.1.1, Debian bookworm-20261005-slim) and mix.exs's elixir: "~> 1.20" move together. .tool-versions now pins Node too (nodejs 22.23.3), which CI's setup-node reads and mix kiln.toolchain.check holds the Dockerfile's new NODE_VERSION to. The image used to build assets with Debian bookworm's Node 18, past end of life, while CI used 22. The runner moves to Debian trixie-20261005-slim for three months of security updates. A Docker deploy needs nothing: the release carries its own runtime. A downstream project that builds Kiln from source should move its own .tool-versions to the same versions.
  • docs/path-forward.md sets Kiln's direction from 1.1: live content delivery plus provable, governed publishing, with the 2.0, 3.0 and 4.0 milestones and the minors leading to each. The page says what Kiln is for from here on, why that changed, and the order of releases that get there: 2.0 Provable publishing (minors 1.2 to 1.7), 3.0 Live delivery (2.1 to 2.6) and 4.0 Governed at scale (3.1 to 3.6), each gated by a passing line of one demo rather than a date. Majors keep their contract meaning: each also lands the breaking changes already queued in its milestone. The issue tracker gained two labels, wedge for direction work and frozen for areas kept and maintained but not extended, and seven off-direction issues were closed as not planned. The README links the page beside the project plan.
  • Custom fields sit in the editor's main column, under the blocks, instead of behind the inspector's Settings tab. The inspector opens on Preview, so a type's own fields (a recipe's chef, an event's venue) were invisible until a writer thought to open Settings, where they were mixed in with URL, SEO and scheduling. They are now a Custom fields panel below the block canvas, the way WordPress puts meta boxes under the post body. The panel flags its own validation errors, so the Settings tab's alert dot no longer counts them. The panel stays visible in the Markdown view. Inputs, names and the save path are unchanged. See KilnCMSWeb.ContentEditor.CustomFieldsPanel.
  • The newsletter's sign-up and unsubscribe pages render in the site's own layout instead of as bare unstyled pages. "Almost there", "Check that address", "Something went wrong" and both unsubscribe pages were hand-built HTML with inline styles, so a reader who signed up from a styled page landed somewhere that looked like a different site. They now use Layouts.public and the confirmation page's card, like the confirmation page already did. Routes, status codes, wording and the unsubscribe one-click are unchanged. (#1870)

Fixed

  • The plugin docs no longer promise Hex or git-dependency plugins: a plugin compiles only as a projects/<name>/ directory, and is shared as a git repository a site vendors there. (#1909)

Security

  • decimal 3.2.0 for EEF-CVE-2026-97853: Decimal.round/3 no longer allocates without bound on a caller-chosen places. The advisory (MEDIUM) is a denial of service when the places argument comes from outside. Kiln does not call Decimal.round/3 itself; decimal is in the tree through Ecto, Jason and the JSON-schema libraries, and the bump keeps the Dependency audit job green. The lock moves from 3.1.1 to 3.2.0, the newest release the tree's constraints allow; the fix landed in 3.1.2.
  • Ash 3.34.6: an MCP read tool's count/exists/aggregate result no longer skips related resources' read policies in its filter. EEF-CVE-2026-101028 (MEDIUM): Ash.count, Ash.exists and Ash.aggregate applied only the root resource's read policy, so a filter crossing a relationship could test conditions against related rows the caller can't read. Kiln's MCP read tools keep AshAi's default parameters, which let an API-key holder choose those result types and send a filter. Ash.read and page counts were not affected.