github getgrav/grav 2.1.0

4 hours ago

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 <link rel="alternate" type="text/markdown"> 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 <template>.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
  • 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 #3735
  • The short and long date format pickers offer ISO 8601 presets, Y-m-d and Y-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 .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

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
  • 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: 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
  • 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
  • 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 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
  • 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 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 #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 a markdown="1" block now renders as a blockquote #3204
  • & in code spans and entities such as &commat; inside a markdown="1" block are no longer escaped twice #764 #2590
  • <https://...> and <name@example.com> links now work inside a markdown="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 a markdown="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_url set 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 ?lightbox link, now link through the page route when pages.media_route_urls is on, so enabling the user/pages deny rule that goes with it no longer turns them into 403s. Thanks to @complanar #4298
  • With pages.media_route_urls and images.cls.auto_sizes both on, a page showing an image at its original size no longer fails with a getimagesize() 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-XXXX was refused by the API plugin's install endpoint and by License Manager while a hand-written user/data/licenses.yaml worked. 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 install now prints the store's explanation and where to renew or buy, instead of only "Unauthorized Premium License Key"

Don't miss a new grav release

NewReleases is sending notifications on new releases.