Skip to content

Repository files navigation

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.

About

Jekyll plugin that converts each page's fully rendered HTML into a Markdown sibling, for AI agents and other machine readers.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages