# v2.1.9
## 09/21/2026
1. [](#bugfix)
* The bundled `robots.txt` no longer blocks pipelined CSS and JS. `Disallow: /assets/` was removed, because it beat the shorter `Allow: *.css$` rules and stopped Google from rendering pages on sites with the asset pipeline turned on. The CSS and JS rules now start with `/`, also match URLs with a query string, and explicitly allow assets under `system/` and `user/plugins/`. Upgrades never replace `robots.txt`, so existing sites need to apply this change by hand.
# v2.1.8
## 09/18/2026
1. [](#bugfix)
* Upgrading now adds the `tmp/` block from 2.1.7 to an existing site's `.htaccess`, so upgraded Apache sites stop serving temporary files and the dashboard storage warning clears. [#4316](https://github.com/getgrav/grav/issues/4316)
# v2.1.7
## 09/18/2026
1. [](#improved)
* Updated vendor libraries to latest versions
* Defer OPcache compilation of newly generated YAML and Markdown cache files until they are first included, reducing cold-cache rebuild work on large sites while still invalidating stale bytecode immediately.
* Repeated deprecation notices now share one debug trace with an occurrence count, preventing large page-tree rebuilds from filling memory and the debug toolbar with thousands of identical traces. Notices from different YAML documents and Twig source locations remain separate.
1. [](#bugfix)
* [security] Editor-authored Twig can no longer read the Clockwork debugger token from system configuration. Thanks @manus-pi
* [security] Bundled nginx, Caddy, lighttpd and IIS rules now block hidden files and directories at any depth, including nested Git repositories, while still allowing `.well-known` for ACME challenges. Operators who copied one of these configurations must update their active server configuration. Thanks @onetrev
* [security] Image transforms now refuse source rasters above the configured pixel limit before GD or Imagick decodes them. Thanks @manus-pi
* Modular page content that uses request-aware Twig is now rendered for each visitor instead of being shared from the page cache. Thanks @Lxcardoza993
* [security] Clockwork profiler data now requires a configured token, including for requests from the local machine. Thanks @Zagn
* GPM now explains skipped symlinked updates and reports unwritable package directories before downloading or changing packages. Preflight annotates the same destination issues, and development builds no longer receive a misleading prompt to upgrade to an older release. [#4319](https://github.com/getgrav/grav/issues/4319)
* Block direct access to `tmp/` in the bundled Apache, nginx, Caddy, lighttpd and IIS rules, including temporary package downloads. Servers that serve static files before Apache must apply the equivalent rule at that layer, and operators who copied one of these configurations must update their active server configuration. [#4316](https://github.com/getgrav/grav/issues/4316)
* Image `format()` and `quality()` settings now apply to all srcset alternatives, including derivatives created before or after those settings. This also fixes format conversion through `images.defaults`. Thanks @onetrev [#4318](https://github.com/getgrav/grav/pull/4318)
* **A theme's Flex, user or config blueprints are now found, instead of being ignored unless the theme also happened to ship page blueprints.** A theme supplying, say, a Flex type of its own had it quietly never register: nothing errored and nothing was logged, the type simply never appeared. Each kind of blueprint a theme ships is now registered on its own, which also keeps a theme from shadowing the blueprints Grav itself provides. Thanks @wakqasahmed [#4303](https://github.com/getgrav/grav/issues/4303)
# v2.1.6
## 09/15/2026
1. [](#improved)
* **A package that ships its own `.htaccess` can no longer opt out of the protections around `user/`.** Those rules are pushed down into every folder beneath `user/` and run first, which is what stops a plugin or theme from replacing them. A folder can still ask Apache to run them last and then stop before they are reached. That takes a deliberate line in the package's own file rather than the accidental case this guards against, but the protection Grav shipped before 2.1.4 held against it, so this restores that. A second set of rules now backs up the first using a different Apache module, one that a folder underneath cannot switch off. [#4236](https://github.com/getgrav/grav/issues/4236)
1. [](#bugfix)
* **The compiled cache is written in one piece, so a busy site no longer logs `Corrupt compiled cache` warnings after an install or a cache clear.** Every request includes the compiled copy of a YAML or markdown file without taking a lock, while the process rebuilding it truncated the file first and filled it afterwards. A request landing in between saw a syntax error, and since 2.0.20 each one wrote a warning to `grav.log`, which on a cold cache with the admin's parallel requests meant a burst of them for a problem that had already healed itself. The compiled file is now written next to its target and renamed over it, so a reader only ever sees a complete file. The warning stays for a file that really is broken, but no longer fires when another process is in the middle of writing it, and a corrupt file is reported once per request rather than once per read path. A compiled file that cannot be written is now a cache miss instead of a server error.
* **`user/data` answers correctly on a restricted host too, on sites an earlier update had put beyond the reach of 2.1.5's repair.** An update in June widened that folder's file in place rather than replacing it, which produced two versions that exist only on disk — in no release and in no checkout — so the sweep behind 2.1.5 could not find them to list. A site carrying either one kept the directive that returns a server error for the whole folder on a host with a restricted `AllowOverride`, which is every image and file uploaded to `user/data`. Both are now recognised and replaced. [#4311](https://github.com/getgrav/grav/issues/4311)
* **A number field accepts every value that sits on its step, instead of refusing some of them.** Checking a value against a step was done in binary floating point, where a decimal like `0.0000001` has no exact representation, so a perfectly valid entry could be rejected with no way for the person filling the form to tell why — a latitude of `81.96` on a field stepping by `0.0000001` was refused. The check is now done on the digits as typed, which has an exact answer. Thanks @TheoAcker12 [#3585](https://github.com/getgrav/grav/issues/3585)
* **A number, range or select field no longer takes the site down when its step is `any`, zero, or not a number.** `any` is the standard way to say a field has no step at all, and it was being read as zero and then divided by, which is a fatal error rather than a failed validation — the same for a step left empty or set to something that is not a number. Multi-value select and checkbox fields had the same fault a few lines away. All of them now treat a step that is not a positive number as no step, which is what browsers do. Thanks @sridharkalaibala [#4308](https://github.com/getgrav/grav/pull/4308)
* **The last folder under `user/` that could answer with a server error on a host with a restricted `AllowOverride` now answers correctly.** 2.1.5 fixed the four files Grav ships, but a site with a `user/env` folder also has a file there that an earlier update wrote, and Grav has never shipped that one — so it kept the directive the rest were moved off. Nothing is served from that folder, so no site was broken by it; it is a stray error page where a "forbidden" belongs. An upgrade replaces the file if it is the one Grav wrote, and leaves a file you edited alone. [#4311](https://github.com/getgrav/grav/issues/4311)
# v2.1.5
## 09/14/2026
1. [](#bugfix)
* **Avatars and files uploaded to `user/data` are served again, and the rest of the `user/.htaccess` fix from 2.1.4 now reaches the folders it missed.** 2.1.4 restated the site root's folder blocks without the two exceptions the root makes, so profile avatars and Flex Object image uploads came back as "forbidden" on Apache. The separate files in `user/accounts`, `user/config` and `user/data` also still used the directive that takes a site offline on a host with a restricted `AllowOverride`, so those three folders kept failing where 2.1.4 had fixed the rest. All four files now work the same way, and an upgrade replaces any of them a previous Grav wrote — a file you edited yourself is left alone. Thanks @onetrev [#4311](https://github.com/getgrav/grav/issues/4311)
* **A premium package covered by a licence you already hold now installs, instead of being refused as unlicensed.** A store can sell one licence that carries several packages — a shop plugin whose payment providers come with it, say — and the repository entry says so with `premium.license_product`. The download proxy has always honoured that, but GPM only ever looked for a key filed under the package's own name, so a customer holding one key had to paste it once per package, and `bin/gpm install` failed on every package they had not pasted it against. The key filed under the product a package belongs to now counts for that package, and a key filed under the package's own name still wins wherever there is one.
# v2.1.4
## 09/14/2026
1. [](#bugfix)
* **Sites on hosts with a restricted `AllowOverride` are no longer taken offline by the `user/.htaccess` file added in 2.0.19.** That file used an Apache directive many shared hosts do not permit in `.htaccess`, and where it was not permitted Apache returned an error for everything inside `user/` — so the admin went blank, the theme's styles and scripts stopped loading, and the front end broke too. It kept happening after a rollback, because rolling back Grav never replaces `user/`. The file now does the same job with directives every host running Grav already allows. Thanks @elanorpam [#4309](https://github.com/getgrav/grav/issues/4309)
# v2.1.3
## 09/13/2026
1. [](#bugfix)
* **Asking a template for an image caption or credit that was never set no longer changes the image address.** Reading a `.meta.yaml` field that is missing, such as `{{ image.copyright }}`, added the field name to the end of the image URL for every visitor instead of simply returning nothing. Thanks @phmg701 [#4301](https://github.com/getgrav/grav/issues/4301)
* **The debug bar is back on pages that swap in a different page while the request runs.** A form that failed validation, or an error page served by the Error plugin, rendered without the bar because Grav no longer recognised the replacement page as one it had already loaded. Thanks @hughbris [#4300](https://github.com/getgrav/grav/issues/4300)
* The Clockwork debug badge now appears when the JavaScript pipeline is enabled. Its script was being merged into the combined file, which dropped the attributes it needs to find itself. Thanks @wakqasahmed [#3871](https://github.com/getgrav/grav/issues/3871)
* AVIF images now honour the quality setting when the Imagick adapter is in use. The value was being written to a field the AVIF encoder never reads, so every AVIF came out at the encoder's own default regardless of the setting. Thanks @sridharkalaibala and @Rotzbua [#4059](https://github.com/getgrav/grav/issues/4059)
* **Saving a page or a Flex object no longer fails with `Invalid input in "Date"` on a date the author never touched.** A `date:` written the ordinary unquoted way comes back out of YAML as a number, and the date validator only accepted text, so editing any other field and saving was refused outright in Admin Next and through the API. Classic admin was never affected because it reformatted the date before submitting it [#4304](https://github.com/getgrav/grav/issues/4304)
# v2.1.2
## 09/11/2026
1. [](#bugfix)
* **Updating a 2.0 site to 2.1 from Admin2 no longer fails.** The check that runs before an update still treated a minor release as a major one, so it refused the update unless every enabled plugin and theme listed Grav 2.1 as compatible, which almost none do yet. From 2.0 on those checks only run for a new major version [#4299](https://github.com/getgrav/grav/issues/4299)
* `bin/gpm update` no longer calls a 2.0 to 2.1 update a new major version
# v2.1.1
## 09/11/2026
1. [](#bugfix)
* **Grav 2.0 sites can update to 2.1.** `bin/gpm selfupgrade` treated each minor release as a separate line, the way 1.7 and 1.8 were, so a 2.0 site was told it was up to date while 2.1.0 was out. Only a new major version now needs a manual move [#4299](https://github.com/getgrav/grav/issues/4299)
* When a new major version of Grav is out, `bin/gpm selfupgrade` says so and links to the migration guide instead of only reporting that the site is up to date
# v2.1.0
## 09/11/2026
1. [](#new)
* Every page can now be read as Markdown, built for AI agents and other text clients. Add `.md` to any page URL, or send an `Accept: text/markdown` request header, and Grav answers with the rendered page converted back to Markdown instead of the theme's HTML
* The Markdown is the page as the theme renders it, reduced to its main content region, so blog listings, shops, product pages and anything else a template builds read the way they display. Shortcodes, content Twig, modular pages and resolved image and link paths all come through, and site navigation, sidebars and footers are left out
* Each Markdown document opens with a YAML block (title, URL, date, description, taxonomy) and closes with links to the parent, neighbouring and child pages by their own `.md` URLs, so an agent can walk a whole site without leaving Markdown
* The feature and each of its parts can be switched off under the new **Markdown Output** settings in **Configuration → System → Content**
* HTML responses now carry a `Link` header and a `` tag pointing at their Markdown version, and Markdown responses carry an `X-Markdown-Tokens` header with an estimated token count, matching Cloudflare's Markdown for Agents
* Themes can override the Markdown layout with a `default.md.twig` or `.md.twig` template, using the new `markdown_output()`, `markdown_frontmatter()`, `markdown_body()`, `markdown_links()` and `markdown_url()` Twig functions and the `html_to_markdown` filter
* Grav's own templates are now also reachable under the `@grav` Twig namespace, so a theme can include or extend `@grav/partials/metadata.html.twig` to add a line instead of keeping a copy of the whole file
* The home page can be requested in any output format as `/index.md`, `/index.rss`, `/index.json` and so on, the way static site generators do it, instead of `/.md`, which every web server treats as a hidden file. A root page actually named `index` still takes precedence
* `page.url()` takes a fifth argument naming an output format, so `page.url(true, false, true, false, 'rss')` gives the right link for any page, home included, without a theme having to check for the home page and append `index` itself
* A media file's `url()` now takes a second argument that prepends the scheme and host, so `page.media['photo.jpg'].url(true, true)` gives a full URL for one image where Open Graph, Pinterest or a feed needs it, without turning on `absolute_urls` for the whole site. It matches `page.url(true)` in subfolder installs, with `custom_base_url`, and for media linked through the page route with `pages.media_route_urls` [#894](https://github.com/getgrav/grav/issues/894)
* The content type served for an output format can be changed per site: set `media.types.rss.mime` in `user/config/media.yaml` and the RSS feed is sent as that type, so a feed can be styled with XSLT without a plugin. The same works for `atom`, `xml`, `json` and `md`. Thanks to @wakqasahmed for the matching PR [#4293](https://github.com/getgrav/grav/pull/4293) [#3735](https://github.com/getgrav/grav/issues/3735)
* The short and long date format pickers offer ISO 8601 presets, `Y-m-d` and `Y-m-d H:i` [#2283](https://github.com/getgrav/grav/issues/2283)
* A new **Flex Render Hints** debugger setting wraps every rendered Flex object and collection in an HTML comment naming it, so the source of a block can be found in the page markup. Off by default
1. [](#improved)
* A redirect answered to a `.md` request now points at the `.md` version of its target, so a section URL that forwards to its first page keeps an agent in Markdown
* A URL with no extension sends `Vary: Accept` while Markdown output is on, so a shared cache never hands an agent the HTML or a browser the Markdown
* The Apache and lighttpd configs now forbid `.md` URLs only when they point at a real file, so page routes ending in `.md` reach Grav while source files under `user/pages` stay blocked
* Upgrading patches the same rule into an existing site's `.htaccess`, which upgrades never replace, as long as the stock line is still there untouched. nginx, Caddy and IIS configs never blocked page routes and need no change
* Parsedown Extra updated to 1.0.1, which removes two PHP 8.2+ deprecation notices
1. [](#bugfix)
* **The Clockwork browser extension can now sign in with the debugger token.** The extension posts the password as a multipart form, and the `/__clockwork/auth` endpoint only read raw JSON or query-string bodies, so every password entered in the extension was refused while `curl` with the same token worked. The parsed form body is read first now
* **Updating Grav no longer deletes the processed-image cache.** The update ran a full cache clear that ignored `cache.clear_images_by_default`, so every gallery thumbnail was regenerated on the next visit. Resized images now survive every cache clear and update unless that setting is on; `bin/grav cache --images-only` still removes them on demand [#3416](https://github.com/getgrav/grav/issues/3416)
* With the debugger on, Flex wrote a comment marker around every rendered object and collection into RSS, Atom, XML and Markdown output, breaking feeds and sitemaps. The marker also used dashes that are not a valid HTML comment. It is now opt-in through the new Flex Render Hints setting, only ever appears in HTML pages, and is a real comment [#3538](https://github.com/getgrav/grav/issues/3538)
* Pages with `twig_first: true` broke in 2.0.26 with a Twig syntax error such as `Unexpected character "&"`, because the fix for GHSA-pp89-h475-7gj6 sent every content-Twig page down a path that always ran Markdown before Twig. Twig-first pages run Twig on the raw source again and their output is never put in the page cache, and Markdown-first pages no longer have their Twig tags altered by Markdown
* **Large responses on hosts with `zlib.output_compression` turned on no longer end in a PHP error.** On the way out Grav tried to close PHP's own compression buffer, which PHP refuses once compressed output has started, so every response over about 16 KB got an HTML error block appended (breaking Admin2's plugin picker and other large API responses), a CRITICAL line was logged, and `onShutdown` work never ran. Grav now only closes the buffers PHP allows it to. Thanks to @sandymac [#4294](https://github.com/getgrav/grav/issues/4294)
* **Scheduler folders and processed-image cache folders are created group-writable like the rest of Grav**, so on hosts where the web server and the command line run as different users, `bin/grav clearcache` can empty them again. The `system.images.cache_perms` default is now `0775`, and the umask still applies. Thanks to @sandymac [#4295](https://github.com/getgrav/grav/issues/4295)
* A user group saved without a display name is listed under its own name in the Groups field, instead of as a blank entry [getgrav/grav-plugin-admin2#172](https://github.com/getgrav/grav-plugin-admin2/issues/172)
* Saving a page whose code samples contain heredocs or long runs of `key='value'` lines no longer fails with `PREG_BACKTRACK_LIMIT_ERROR`. The XSS check's event-handler rule gave up on that content, and a check that can't finish counts as a hit, so the save was refused. The rule now runs in linear time and still catches everything it did before. Thanks to @amadeusp [#4291](https://github.com/getgrav/grav/issues/4291)
* Markdown Extra no longer deletes page content that follows the first element of an HTML block. Every raw HTML block went through PHP's DOM parser, which kept only its first element [#4291](https://github.com/getgrav/grav/issues/4291) [#3452](https://github.com/getgrav/grav/issues/3452) [#1198](https://github.com/getgrav/grav/issues/1198)
* Markdown Extra leaves raw HTML exactly as written, as it does with Extra off, so Twig in `href` and `src` attributes works again and SVG attributes, entities and self-closing tags are no longer rewritten. Only blocks marked `markdown="1"` are still processed [#1495](https://github.com/getgrav/grav/issues/1495) [#1449](https://github.com/getgrav/grav/issues/1449) [#1352](https://github.com/getgrav/grav/issues/1352)
* Pages with an HTML block that starts with `` no longer crash with Markdown Extra turned on
* Markdown inside a `markdown="1"` block is now rendered from the text as written, so a fenced code block keeps its capital letters and tags [#1840](https://github.com/getgrav/grav/issues/1840)
* A `>` blockquote inside a `markdown="1"` block now renders as a blockquote [#3204](https://github.com/getgrav/grav/issues/3204)
* `&` in code spans and entities such as `@` inside a `markdown="1"` block are no longer escaped twice [#764](https://github.com/getgrav/grav/issues/764) [#2590](https://github.com/getgrav/grav/issues/2590)
* `` and `` links now work inside a `markdown="1"` block [#287](https://github.com/getgrav/grav/issues/287)
* A `` inside a `` no longer swallows the image after it [#1168](https://github.com/getgrav/grav/issues/1168)
* Twig in attributes, SVG attribute names and text after a `<` are kept as written inside a `markdown="1"` block
* `markdown="1"` on a `