jekyll-md 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -2
- data/README.md +53 -3
- data/lib/jekyll/md/configuration.rb +15 -1
- data/lib/jekyll/md/converter.rb +20 -0
- data/lib/jekyll/md/generator.rb +5 -0
- data/lib/jekyll/md/layout_renderer.rb +42 -0
- data/lib/jekyll/md/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f5b3a895e587a7a86ec969b72d2f2b86420396ebc9b09206a28b84ea42d3c93
|
|
4
|
+
data.tar.gz: ab118399e1349f5045c38558a1e36ecaea4fdec3d49547f3c08185f475ec7a6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b563c6418679c743987982a769f0a5878143189d46b868602413b45ee36a83364938ba6a01c697726de5626dab4eb2eb9889c05ad444c07c89e3206cb6e34161
|
|
7
|
+
data.tar.gz: 10fae325297d0caf1cf6ecf12086ec33f78ae71de2df545fd9fcc96a02cc3ab2acd55f4d55c49e5011515eabe8e16eb6dd6a6e892b173f5d75b5cc42040042e6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
### 0.2.0 (
|
|
1
|
+
### 0.2.0 (2026/09/20)
|
|
2
2
|
|
|
3
|
-
*
|
|
3
|
+
* Replaces non-breaking spaces (` `) with regular spaces in converted Markdown - [@dblock](https://github.com/dblock).
|
|
4
|
+
* [#7](https://github.com/dblock/jekyll-md/pull/7): Adds `md: layout:` (and per-page `md_layout`) to wrap converted Markdown in a custom Jekyll layout, with access to `content`, `page`, and `site` - [@dblock](https://github.com/dblock).
|
|
4
5
|
|
|
5
6
|
### 0.1.0 (2026/09/19)
|
|
6
7
|
|
data/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
A Jekyll plugin that serves a clean Markdown version of every page, for AI agents and other machine readers.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
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](https://code.dblock.org/2026/09/19/serving-markdown-with-a-jekyll-plugin.html).
|
|
9
11
|
|
|
10
12
|
## Installation
|
|
11
13
|
|
|
@@ -67,6 +69,39 @@ md:
|
|
|
67
69
|
- /assets/**
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
### Custom Layouts
|
|
73
|
+
|
|
74
|
+
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:
|
|
75
|
+
|
|
76
|
+
```liquid
|
|
77
|
+
<!-- _layouts/md_page.liquid -->
|
|
78
|
+
---
|
|
79
|
+
title: {{ page.title }}
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
# {{ page.title }}
|
|
83
|
+
|
|
84
|
+
{{ content }}
|
|
85
|
+
|
|
86
|
+
Source: {{ site.url }}{{ page.url }}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
md:
|
|
91
|
+
layout: md_page
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
You can override, or opt out of, the site-wide layout for an individual page via front matter:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
---
|
|
100
|
+
md_layout: md_alt # use a different layout for this page only
|
|
101
|
+
md_layout: false # skip the layout for this page even though one is configured site-wide
|
|
102
|
+
---
|
|
103
|
+
```
|
|
104
|
+
|
|
70
105
|
### Per-Page Front Matter
|
|
71
106
|
|
|
72
107
|
```yaml
|
|
@@ -74,6 +109,7 @@ md:
|
|
|
74
109
|
md: false # opt this page out of Markdown generation entirely
|
|
75
110
|
md_link: false # generate the .md file, but don't add the <link> tag to this page
|
|
76
111
|
md_selector: "#x" # override the selector for this page only
|
|
112
|
+
md_layout: "..." # override the site-wide layout for this page only, or `false` to skip it
|
|
77
113
|
---
|
|
78
114
|
```
|
|
79
115
|
|
|
@@ -88,11 +124,25 @@ If a page at the derived destination path already exists after Jekyll writes the
|
|
|
88
124
|
1. `:pages`/`:documents`, `:post_render` — after a page's layout and Liquid have fully rendered, inject the `<link rel="alternate">` tag into its `<head>`.
|
|
89
125
|
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.
|
|
90
126
|
|
|
127
|
+
## llms.txt
|
|
128
|
+
|
|
129
|
+
`jekyll-md` intentionally does not generate an [`llms.txt`](https://llmstxt.org). 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.
|
|
130
|
+
|
|
131
|
+
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](https://code.dblock.org/llms.txt) 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](https://github.com/dblock/code.dblock.org/blob/gh-pages/llms.txt).
|
|
132
|
+
|
|
133
|
+
The [v2 spec](https://llmstxt.org/changes.html) 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.
|
|
134
|
+
|
|
91
135
|
## Similar Projects
|
|
92
136
|
|
|
93
|
-
|
|
137
|
+
| | Markdown source | Discovery `<link>` | `llms.txt` | Notes |
|
|
138
|
+
|---|---|---|---|---|
|
|
139
|
+
| **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](#custom-layouts) (`md: layout:`) can add a title heading, front matter block, source link, or any combination. |
|
|
140
|
+
| [jekyll-llms](https://github.com/skatkov/jekyll-llms) | Source file | Automatic | Yes, exhaustive by default | Inline HTML leaks through verbatim; only pages with Markdown/HTML source get a sidecar. |
|
|
141
|
+
| [jekyll-markdown-output](https://github.com/abhinavs/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. |
|
|
142
|
+
| [jekyll-agent-markdown](https://github.com/lucianghinda/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`. |
|
|
143
|
+
| [jekyll-third-audience](https://github.com/dbreunig/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. |
|
|
94
144
|
|
|
95
|
-
|
|
145
|
+
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.
|
|
96
146
|
|
|
97
147
|
## Contributing
|
|
98
148
|
|
|
@@ -9,7 +9,8 @@ module Jekyll
|
|
|
9
9
|
'selector' => nil,
|
|
10
10
|
'strip' => %w[script style],
|
|
11
11
|
'link' => true,
|
|
12
|
-
'exclude' => []
|
|
12
|
+
'exclude' => [],
|
|
13
|
+
'layout' => nil
|
|
13
14
|
}.freeze
|
|
14
15
|
|
|
15
16
|
def initialize(site_config)
|
|
@@ -55,6 +56,19 @@ module Jekyll
|
|
|
55
56
|
def selector_for(item)
|
|
56
57
|
item.data['md_selector'] || selector
|
|
57
58
|
end
|
|
59
|
+
|
|
60
|
+
def layout
|
|
61
|
+
@config['layout']
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A per-page `md_layout` front matter value overrides the
|
|
65
|
+
# site-wide layout; either may be `false` to opt a page back out
|
|
66
|
+
# of the layout even when one is configured site-wide.
|
|
67
|
+
def layout_for(item)
|
|
68
|
+
return false if item.data['md_layout'] == false
|
|
69
|
+
|
|
70
|
+
item.data['md_layout'] || layout
|
|
71
|
+
end
|
|
58
72
|
end
|
|
59
73
|
end
|
|
60
74
|
end
|
data/lib/jekyll/md/converter.rb
CHANGED
|
@@ -36,6 +36,7 @@ module Jekyll
|
|
|
36
36
|
return nil unless node
|
|
37
37
|
|
|
38
38
|
@strip_selectors.each { |s| node.css(s).remove }
|
|
39
|
+
normalize_non_breaking_spaces(node)
|
|
39
40
|
|
|
40
41
|
markdown = ReverseMarkdown.convert(
|
|
41
42
|
node.inner_html,
|
|
@@ -67,6 +68,25 @@ module Jekyll
|
|
|
67
68
|
|
|
68
69
|
private
|
|
69
70
|
|
|
71
|
+
# reverse_markdown renders non-breaking spaces (U+00A0) as the
|
|
72
|
+
# literal HTML entity " " instead of a plain space, leaking
|
|
73
|
+
# HTML into otherwise clean Markdown. Replace them with a regular
|
|
74
|
+
# space before conversion, but only outside <code>/<pre>, so we
|
|
75
|
+
# don't corrupt code that legitimately contains the literal text
|
|
76
|
+
# " " (e.g. documenting the entity itself) or real non-
|
|
77
|
+
# breaking spaces preserved verbatim in a code sample.
|
|
78
|
+
def normalize_non_breaking_spaces(node)
|
|
79
|
+
node.traverse do |child|
|
|
80
|
+
next unless child.text? && !within_code_or_pre?(child)
|
|
81
|
+
|
|
82
|
+
child.content = child.content.gsub("\u00A0", ' ')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def within_code_or_pre?(node)
|
|
87
|
+
node.ancestors.any? { |a| %w[code pre].include?(a.name) }
|
|
88
|
+
end
|
|
89
|
+
|
|
70
90
|
def default_node_for(doc)
|
|
71
91
|
DEFAULT_SELECTORS.each do |selector|
|
|
72
92
|
node = doc.at_css(selector)
|
data/lib/jekyll/md/generator.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'fileutils'
|
|
|
4
4
|
require 'jekyll'
|
|
5
5
|
require_relative 'configuration'
|
|
6
6
|
require_relative 'converter'
|
|
7
|
+
require_relative 'layout_renderer'
|
|
7
8
|
|
|
8
9
|
module Jekyll
|
|
9
10
|
module Md
|
|
@@ -14,6 +15,7 @@ module Jekyll
|
|
|
14
15
|
next unless config.enabled?
|
|
15
16
|
|
|
16
17
|
converter = Converter.new(strip_selectors: config.strip_selectors)
|
|
18
|
+
layout_renderer = LayoutRenderer.new
|
|
17
19
|
|
|
18
20
|
(site.pages + site.docs_to_write).each do |item|
|
|
19
21
|
next unless item.destination(site.dest).end_with?('.html')
|
|
@@ -30,6 +32,9 @@ module Jekyll
|
|
|
30
32
|
markdown = converter.convert(item.output, selector: config.selector_for(item))
|
|
31
33
|
next unless markdown
|
|
32
34
|
|
|
35
|
+
layout = config.layout_for(item)
|
|
36
|
+
markdown = layout_renderer.render(layout, markdown, item, site) if layout
|
|
37
|
+
|
|
33
38
|
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
34
39
|
File.write(dest_path, markdown)
|
|
35
40
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'liquid'
|
|
4
|
+
|
|
5
|
+
module Jekyll
|
|
6
|
+
module Md
|
|
7
|
+
# Wraps the Markdown converted from a page/document's rendered HTML
|
|
8
|
+
# in a Jekyll layout, so sites that want more than a flat body
|
|
9
|
+
# (e.g. a title heading, a front matter block, a "Source:" link, or
|
|
10
|
+
# any combination/order of these) can express it themselves
|
|
11
|
+
# instead of the plugin growing a boolean per idea.
|
|
12
|
+
#
|
|
13
|
+
# Reuses Jekyll's own layout lookup (+site.layouts+, keyed by
|
|
14
|
+
# basename without extension, read from `_layouts/`) rather than an
|
|
15
|
+
# arbitrary file path -- the same place/mechanism as HTML layouts,
|
|
16
|
+
# just rendered against the converted Markdown instead of HTML.
|
|
17
|
+
# Give it a distinct name from any HTML layout (e.g.
|
|
18
|
+
# `_layouts/md_page.liquid` instead of `_layouts/page.html`) so the
|
|
19
|
+
# two don't collide in that lookup.
|
|
20
|
+
class LayoutRenderer
|
|
21
|
+
def initialize
|
|
22
|
+
@cache = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# +layout_name+ is the name of a layout in `_layouts/` (without
|
|
26
|
+
# extension). Exposes `content` (the already-converted Markdown),
|
|
27
|
+
# `page` (the same front matter/data a Jekyll layout sees), and
|
|
28
|
+
# `site` (the site payload) as Liquid variables.
|
|
29
|
+
def render(layout_name, content, item, site)
|
|
30
|
+
layout = site.layouts[layout_name]
|
|
31
|
+
raise "jekyll-md: layout not found: #{layout_name}" unless layout
|
|
32
|
+
|
|
33
|
+
parsed = @cache[layout_name] ||= Liquid::Template.parse(layout.content)
|
|
34
|
+
parsed.render!(
|
|
35
|
+
'content' => content,
|
|
36
|
+
'page' => item.to_liquid,
|
|
37
|
+
'site' => site.site_payload['site']
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/jekyll/md/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-md
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Doubrovkine
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- lib/jekyll/md/configuration.rb
|
|
70
70
|
- lib/jekyll/md/converter.rb
|
|
71
71
|
- lib/jekyll/md/generator.rb
|
|
72
|
+
- lib/jekyll/md/layout_renderer.rb
|
|
72
73
|
- lib/jekyll/md/version.rb
|
|
73
74
|
homepage: http://github.com/dblock/jekyll-md
|
|
74
75
|
licenses:
|