Skip to content
Grav 2.0 is officially stable. Read the announcement →
journal 9 mins

Grav 2.1 Released: Every Page Speaks Markdown

Native Markdown output for AI agents, llms.txt, and a ten-year-old feature request finally closed

I've spent the last few months pointing AI agents at my own Grav sites, and the thing that kept getting in the way was HTML. An agent asking for a page gets the whole theme back: navigation, sidebars, cookie banner, footer, and somewhere in the middle, the content it actually wanted. Cloudflare launched a paid feature earlier this year that converts pages to Markdown at the edge, and it works, but it has to guess where the content is. Grav doesn't have to guess. It wrote the page.

So Grav 2.1 adds Markdown output to core. Add .md to any page URL and you get the page as Markdown. That's the headline. There's a good bit more in this release, and a couple of fixes that have been a long time coming.

Markdown for Agents

Take any page on a Grav 2.1 site and put .md on the end of the URL. /blog/my-post becomes /blog/my-post.md. Or leave the URL alone and send an Accept: text/markdown header, which is what most agent frameworks do anyway. Either way you get something like this:

MARKDOWN
---
title: Typography
url: 'https://example.com/typography'
markdown: 'https://example.com/typography.md'
date: '2026-04-29'
description: "Quark 2 is Grav 2.0's default theme. It uses Pico CSS as a classless foundation…"
---

Quark 2 is Grav 2.0's default theme. It uses [Pico CSS](https://picocss.com) as a classless
foundation, layered with a Cal.com-inspired design system.

### Headings

Quark 2 uses **Cal Sans** for display headings...

The Markdown is the page as your theme renders it, cut down to the main content region. That distinction matters. A blog listing, a shop's product grid, a modular landing page: anything a template builds comes through the way it displays, not the way the source file happens to be written. Shortcodes run. Content Twig runs. Image and link paths are resolved to full URLs. And the navigation, sidebars and footer are left out.

Each document opens with a YAML block carrying the title, URL, date, description and taxonomy, and closes with links to the parent, the neighbouring pages and the children, all pointing at their own .md versions. An agent can walk an entire site without ever seeing HTML.

A few smaller pieces make the whole thing behave properly on the web:

  • Every HTML response carries a Link header and a <link rel="alternate" type="text/markdown"> tag pointing at its Markdown version, so a client that knows to look finds it without guessing.
  • Markdown responses carry an X-Markdown-Tokens header with an estimated token count, matching what Cloudflare sends, so an agent can decide whether a page fits its budget before reading it.
  • The home page is /index.md, the way static site generators do it. /.md would be a hidden file on every web server ever made.
  • A redirect answered to a .md request lands on the .md version of its target, and extension-less URLs send Vary: Accept so a shared cache never hands an agent the HTML.

It's on by default. If you don't want it, or only want part of it, everything lives under Markdown Output in Configuration → System → Content, and each piece (the frontmatter, the navigation links, the token header, absolute URLs) has its own switch.

llms.txt

Sitemap 5.3.0 now serves /llms.txt, the index file AI agents have started looking for. It lists every page in your sitemap as a Markdown link with its description, each one pointing at the page's .md URL, and it follows the same ignore rules as your XML sitemap. If you've already told the sitemap to skip a page, llms.txt skips it too.

There's an optional /llms-full.txt that joins every page's full Markdown into a single document. It's off by default because on a big site it's a big file, and it's cached once built.

For Theme Developers

The Markdown layout is a Twig template like everything else. Drop a default.md.twig or <template>.md.twig into your theme to change it, and use the new markdown_frontmatter(), markdown_body(), markdown_links() and markdown_url() functions, or the html_to_markdown filter, to build whatever you want.

Grav's own templates are now reachable under the @grav Twig namespace. If you've ever copied a whole core partial into your theme just to add one line, you can now extend @grav/partials/metadata.html.twig instead and keep the copy out of your theme entirely.

page.url() takes a fifth argument naming an output format. page.url(true, false, true, false, 'rss') gives the right feed link for any page including home, which used to need a special case in every theme that offered feeds.

And a request from 2016 is closed. A media file's url() now takes a second argument that adds the scheme and host, so page.media['photo.jpg'].url(true, true) gives a full URL for one image where an Open Graph tag or a Pinterest button needs it. Until now the only answer was turning on absolute_urls for the whole site.

Two more small ones. The content type served for an output format can be changed per site, so media.types.rss.mime: application/xml in your media.yaml lets you style an RSS feed with XSLT without writing a plugin. And the date format pickers finally offer ISO 8601 presets.

Plugins that check the signed-in user during an Admin 2 request now get the admin instead of a guest. If you filter page types in an onAdminPageTypes listener, or read $grav['user'] anywhere else in the admin, it behaves the way it did in the classic admin.

Fixes Worth Knowing About

Markdown Extra stops rewriting your HTML. If you've ever switched on Markdown Extra and watched part of a page vanish, this is why. Extra ran every block of raw HTML in your content through PHP's DOM parser and wrote back its own version of it. It kept the first element of a block and quietly threw the rest away, it URL-encoded any Twig in an href or src so the Twig never ran, and a block starting with <html> crashed the page outright. Now raw HTML comes out exactly as you wrote it, the same as with Extra off. Blocks marked markdown="1" still get their Markdown rendered, but now from the text you actually wrote, so a code block inside one keeps its capital letters, and blockquotes and <https://...> links work the way they do everywhere else. Nothing after the block gets lost either. Before shipping it we rendered 3,040 documents with the old and new code (every page on the Learn site, a dozen real sites, and the CommonMark and GFM specs). 36 came out better. None came out worse.

Updating Grav no longer wipes your image cache. This one has bugged people since 2021. The update ran a full cache clear that ignored the clear_images_by_default setting, so every resized image on your site was regenerated on the next visit. On a site with a few big galleries that meant minutes of waiting and a very confused visitor. Resized images now survive every cache clear and every update unless you turn that setting on. bin/grav cache --images-only still clears them when you actually want to.

The Clockwork browser extension can sign in with the debugger token. The token gate arrived in 2.0.22 to stop anyone reading profiler data off a live site, and several of you reported that the extension's password prompt never accepted the token. You were right. The extension posts its password as a multipart form, and Grav was only reading JSON or query-string bodies, so curl worked and the extension didn't. Fixed.

Flex debug markers stay out of your feeds. With the debugger on, Flex wrapped every rendered object in a comment marker, including inside RSS, Atom and XML output, which broke feeds and sitemaps. Those markers are now opt-in through a new Flex Render Hints debugger setting and only ever appear in HTML pages.

Big pages work again on hosts with PHP's gzip compression switched on. Some shared hosts turn on PHP's own zlib.output_compression. On those hosts, any response bigger than about 16 KB ended with a PHP error stuck to the end of it, which broke Admin 2's plugin picker and filled the log with CRITICAL entries. Worse, the work Grav does at the end of each request, the scheduler included, never ran. Grav was trying to close PHP's compression buffer on the way out, and PHP refuses once it has started sending. Grav leaves it alone now.

The command line can clear the cache again on split-user hosts. On hosts where the web server and your SSH account are different users, a few folders (the scheduler's queue and the resized-image cache among them) were created writable only by the web server, so bin/grav clearcache died with Permission denied. They follow your server's umask like every other Grav folder now. Folders that already exist keep their old permissions, so delete them once and let Grav recreate them.

And the twig_first regression from 2.0.26 is fixed. Pages with that header broke with a Twig syntax error after the security fix in that release. They run Twig on the raw source again. Also, a long page full of code samples (shell heredocs in particular) can be saved again. The XSS check used to give up on content like that with a PREG_BACKTRACK_LIMIT_ERROR, and a check that can't finish counts as a failure, so the save was refused.

Upgrading

Warning

Always backup your site, or better yet, create a copy of your site and test upgrades there first.

Grav 2.1 upgrades in place from any 2.0 release.

BASH
bin/gpm selfupgrade
bin/gpm update

Or use the Updates screen in the admin. Update Admin 2 and the API plugin at the same time, since the new Markdown Output settings and the Flex Render Hints toggle need their labels.

Admin 2 2.1.12 also fixes a nasty one. Saving a page field that lives inside a list in the page's frontmatter, a blueprint field like header.panels.0.title, replaced the whole list with that single entry and deleted the rest. If your blueprints use numbered list fields like that, update Admin 2 before you edit those pages.

One thing to know if you run Apache. The stock .htaccess used to block every URL ending in .md, which would have blocked the new page routes too. The updater patches that rule in your existing .htaccess as long as the original line is still there untouched. If you've customised that line, check that /index.md on your site answers with Markdown after the upgrade. nginx, Caddy and IIS never blocked page routes and need no change.

Getting Help

If you hit anything, the Discord chat is the fastest place to get an answer. The Learn site has the full documentation for Markdown output and the new Twig functions.

I'd like to hear what you do with the Markdown side of this. I built it for agents, but I suspect there are uses for it I haven't thought of yet.

— Andy

Related posts

Keep reading