Skip to main content
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.