Project

jekyll-md

0.0
The project is in a healthy, maintained state
A Jekyll plugin that converts each page's fully rendered HTML output into a Markdown sibling file (e.g. /about/index.html -> /about.md) and adds a discovery <link rel="alternate" type="text/markdown"> tag to every page's <head>.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

jekyll-md

Test Coverage Status

A Jekyll plugin that serves a clean Markdown version of every page, for AI agents and other machine readers.

jekyll-md

For every rendered HTML page, jekyll-md writes a sibling .md file (e.g. /about/index.html -> /about.md) and adds a <link rel="alternate" type="text/markdown"> tag to the page's <head> so agents can discover it. Read more in Serving Markdown for AI Agents, Now as a Jekyll Plugin.

Installation

Add this line to your Jekyll site's Gemfile:

group :jekyll_plugins do
  gem 'jekyll-md'
end

And then run bundle install.

Note

This plugin requires a custom Ruby gem and therefore cannot run in GitHub Pages' default build (which only allows a fixed whitelist of plugins). Deploy via a GitHub Actions workflow that runs bundle exec jekyll build instead (GitHub Pages' "GitHub Actions" build type), and it will work.

Usage

No configuration is required to get started; every rendered HTML page gets a Markdown counterpart.

Configuring the CSS Selector

By default (no selector configured), jekyll-md looks for a <main> element or [role="main"] in the rendered page (the closest thing HTML has to a convention for "this is the content, not the header/nav/footer chrome"), and converts that. If your layouts don't use either of these, it falls back to converting the entire <body>, including navigation, headers, footers, and anything else on the page — this is simple but rarely what you want for a real site, since it dumps your header/nav/footer HTML into every single .md file.

Many themes (including Jekyll's default minima) already wrap page content in <main>, so this default may work with no configuration at all. Otherwise, set selector to a CSS selector that scopes the conversion to just your content, e.g. the wrapper div around {{ content }} in your layout:

md:
  selector: "#markdown-content"
<!-- _layouts/post.html -->
<article>
  <div id="markdown-content">
    {{ content }}
  </div>
</article>

You can override the selector for an individual page via front matter:

---
md_selector: "#post-body"
---

Other Configuration

md:
  enabled: true                 # master on/off switch, default true
  selector: "#markdown-content" # CSS selector to convert; default nil (try <main>/[role=main], then the whole page)
  strip: [script, style]        # elements always removed from the selected content before conversion
  link: true                    # inject <link rel="alternate" type="text/markdown"> into <head>, default true
  renderer: reverse_markdown    # HTML-to-Markdown engine, default reverse_markdown
  exclude:                      # array of URL glob patterns to skip entirely
    - /404.html
    - /assets/**

Renderers

jekyll-md supports three HTML-to-Markdown renderers, selected via md: renderer::

  • reverse_markdown (default) - a hard dependency of this gem, no extra setup required.
  • html-to-markdown - a fast, Rust-backed native gem that produces output nearly identical to reverse_markdown. It is not a dependency of this gem, so add it to your own Gemfile (gem "html-to-markdown") before selecting it.
  • kramdown - already ships with Jekyll, so it requires no extra dependency, but its HTML-to-Markdown conversion has some limitations compared to reverse_markdown: links are emitted in reference style ([text][1] with a footnote) rather than inline, code blocks are indented with four spaces instead of fenced with triple backticks, and tables and generic containers (<div>, <span>) are left as raw HTML rather than being converted.
md:
  renderer: html-to-markdown

Custom Layouts

By default, the generated .md file is just the converted content, with nothing added. If you want a title heading, a front matter block, a "Source:" link back to the HTML page, or any combination of these in any order, wrap the content in a layout. This reuses Jekyll's own layout mechanism (_layouts/), rendered against the converted Markdown instead of HTML — give it a name distinct from any HTML layout (e.g. _layouts/md_page.liquid, not _layouts/page.html) so the two don't collide:

<!-- _layouts/md_page.liquid -->
---
title: {{ page.title }}
---

# {{ page.title }}

{{ content }}

Source: {{ site.url }}{{ page.url }}
md:
  layout: md_page

The layout has access to content (the already-converted Markdown), page (the same front matter/data a Jekyll layout sees), and site (the site payload) — the same variables available in a normal Jekyll layout.

You can override, or opt out of, the site-wide layout for an individual page via front matter:

---
md_layout: md_alt   # use a different layout for this page only
md_layout: false    # skip the layout for this page even though one is configured site-wide
---

Per-Page Front Matter

---
md: false          # opt this page out of Markdown generation entirely
md_link: false     # generate the .md file, but don't add the <link> tag to this page
md_selector: "#x"  # override the selector for this page only
md_layout: "..."   # override the site-wide layout for this page only, or `false` to skip it
---

Avoiding Clobbering Hand-Authored Markdown Pages

If a page at the derived destination path already exists after Jekyll writes the site (for example, you hand-author /tags.md yourself from a data-driven Liquid template), jekyll-md will not overwrite it.

How It Works

jekyll-md hooks into two points in the Jekyll build:

  1. :pages/:documents, :post_render — after a page's layout and Liquid have fully rendered, inject the <link rel="alternate"> tag into its <head>.
  2. :site, :post_write — after Jekyll has written the whole site to disk, walk every page and document, extract the configured selector (or the whole <body>) from its rendered HTML, convert it to Markdown, and write it next to the HTML output.

llms.txt

jekyll-md intentionally does not generate an llms.txt. The spec asks for a curated index that "stays small enough to fit in context," explicitly contrasting itself with sitemap.xml, which it criticizes for being too large and unfiltered. A plugin can't know which of your pages are worth surfacing, and dumping every post/page (as some plugins do) just recreates the sitemap problem in Markdown.

Instead, author llms.txt yourself as a plain Jekyll page with Liquid front matter, opting in specific content (e.g. via a per-page pinned: true/llms: true flag) rather than listing everything. See code.dblock.org for a working example that lists pinned highlights, the 10 most recent posts, and key pages out of a blog with almost 600 posts: the template.

The v2 spec also allows nested/scoped llms.txt files, e.g. /docs/llms.txt covering only pages under /docs/, with the most specific file (closest to a given URL) taking precedence over the site-wide one at /llms.txt. Same idea applies: each is just another hand-curated Jekyll page, not a plugin feature.

Similar Projects

Markdown source Discovery <link> llms.txt Notes
jekyll-md Rendered HTML Automatic Not generated (author your own, see above) Covers generated pages (tags, pagination); no source-to-source fidelity issues, but HTML round-trip is lossy for complex markup; optional custom Jekyll layout (md: layout:) can add a title heading, front matter block, source link, or any combination.
jekyll-llms Source file Automatic Yes, exhaustive by default Inline HTML leaks through verbatim; only pages with Markdown/HTML source get a sidecar.
jekyll-markdown-output Source file None (manual URL guessing) No Adds a synthetic YAML front matter block (title, date, tags, etc.) and optional # Title heading to each sidecar.
jekyll-agent-markdown Source file Manual ({% agent_markdown_link %} in layout) Yes, curated (opt-in pages/collections, per-doc section/optional) Can append a metadata footer/header (dates, author, description, source link) to each sidecar; also supports llms-full.txt.
jekyll-third-audience Source file Manual (Liquid tag in layout) No Posts only (configurable layouts); adds a synthetic front matter block (title, date, author, description, tags, url); can strip or rewrite {% include %} tags from the source before writing.

All four alternatives convert from each document's source rather than its rendered HTML: they re-read the original Markdown/HTML file from disk (Liquid resolved, but otherwise untouched), so generated pages without a Markdown/HTML source (tag pages, pagination) don't get a sidecar, and inline HTML in the source leaks through verbatim rather than being converted.

Contributing

See CONTRIBUTING.

Copyright and License

MIT License, see LICENSE for details.