New
- Every page can now be read as Markdown, built for AI agents and other text clients. Add
.mdto any page URL, or send anAccept: text/markdownrequest 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
.mdURLs, 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
Linkheader and a<link rel="alternate" type="text/markdown">tag pointing at their Markdown version, and Markdown responses carry anX-Markdown-Tokensheader with an estimated token count, matching Cloudflare's Markdown for Agents - Themes can override the Markdown layout with a
default.md.twigor<template>.md.twigtemplate, using the newmarkdown_output(),markdown_frontmatter(),markdown_body(),markdown_links()andmarkdown_url()Twig functions and thehtml_to_markdownfilter - Grav's own templates are now also reachable under the
@gravTwig namespace, so a theme can include or extend@grav/partials/metadata.html.twigto 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.jsonand 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 namedindexstill takes precedence page.url()takes a fifth argument naming an output format, sopage.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 appendindexitself- A media file's
url()now takes a second argument that prepends the scheme and host, sopage.media['photo.jpg'].url(true, true)gives a full URL for one image where Open Graph, Pinterest or a feed needs it, without turning onabsolute_urlsfor the whole site. It matchespage.url(true)in subfolder installs, withcustom_base_url, and for media linked through the page route withpages.media_route_urls#894 - The content type served for an output format can be changed per site: set
media.types.rss.mimeinuser/config/media.yamland the RSS feed is sent as that type, so a feed can be styled with XSLT without a plugin. The same works foratom,xml,jsonandmd. Thanks to @wakqasahmed for the matching PR #4293 #3735 - The short and long date format pickers offer ISO 8601 presets,
Y-m-dandY-m-d H:i#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
Improved
- A redirect answered to a
.mdrequest now points at the.mdversion 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: Acceptwhile 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
.mdURLs only when they point at a real file, so page routes ending in.mdreach Grav while source files underuser/pagesstay 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
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/authendpoint only read raw JSON or query-string bodies, so every password entered in the extension was refused whilecurlwith 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-onlystill removes them on demand #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
- Pages with
twig_first: truebroke in 2.0.26 with a Twig syntax error such asUnexpected 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_compressionturned 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, andonShutdownwork never ran. Grav now only closes the buffers PHP allows it to. Thanks to @sandymac #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 clearcachecan empty them again. Thesystem.images.cache_permsdefault is now0775, and the umask still applies. Thanks to @sandymac #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
- Saving a page whose code samples contain heredocs or long runs of
key='value'lines no longer fails withPREG_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 - 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 #3452 #1198
- Markdown Extra leaves raw HTML exactly as written, as it does with Extra off, so Twig in
hrefandsrcattributes works again and SVG attributes, entities and self-closing tags are no longer rewritten. Only blocks markedmarkdown="1"are still processed #1495 #1449 #1352 - Pages with an HTML block that starts with
<html>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 - A
>blockquote inside amarkdown="1"block now renders as a blockquote #3204 &in code spans and entities such as@inside amarkdown="1"block are no longer escaped twice #764 #2590<https://...>and<name@example.com>links now work inside amarkdown="1"block #287- A
<source>inside a<picture markdown="1">no longer swallows the image after it #1168 - Twig in attributes, SVG attribute names and text after a
<are kept as written inside amarkdown="1"block markdown="1"on a<script>or<style>tag no longer turns its code into paragraphs- Reference links, footnotes and abbreviations now work on both sides of a
markdown="1"block, instead of the block wiping out every definition written above it - Footnotes inside a
markdown="1"block now join the page's single footnote list and are numbered in reading order - A definition list item with more than one paragraph no longer breaks every reference link after it on the page
- With
system.custom_base_urlset to something like/act, pages whose names start with the same letters, such as/action-bar, no longer open the wrong page. Thanks to @wakqasahmed #4296 #3057 - Images placed in page content with Markdown, including images from other pages and the image inside a
?lightboxlink, now link through the page route whenpages.media_route_urlsis on, so enabling theuser/pagesdeny rule that goes with it no longer turns them into 403s. Thanks to @complanar #4298 - With
pages.media_route_urlsandimages.cls.auto_sizesboth on, a page showing an image at its original size no longer fails with agetimagesize()error #4298 - Licence keys from other stores are accepted. GPM serves packages licensed by stores other than Grav Premium, but the key format check only knew the Grav Premium shape, so a KahunaCart key such as
KC-XXXX-XXXX-XXXX-XXXXwas refused by the API plugin's install endpoint and by License Manager while a hand-writtenuser/data/licenses.yamlworked. The check now only turns away what no store could have issued and leaves the store that issued the key to say whether it is real - When getgrav.org refuses a premium download for a reason the person can act on, such as an updates window that has ended or a key that does not cover the add-on being installed,
bin/gpm installnow prints the store's explanation and where to renew or buy, instead of only "Unauthorized Premium License Key"