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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b38e647be1b229a45a26dc56ff22daa49501775f7be57ffeb5f109167342ae1
4
- data.tar.gz: 5938e251a981da7e8e8c6934515edb6050a0bc7df213ccf729e4bc1c3fc441ae
3
+ metadata.gz: 9f5b3a895e587a7a86ec969b72d2f2b86420396ebc9b09206a28b84ea42d3c93
4
+ data.tar.gz: ab118399e1349f5045c38558a1e36ecaea4fdec3d49547f3c08185f475ec7a6f
5
5
  SHA512:
6
- metadata.gz: 4de38715b596bdb012af865144ee385d83b2428e85f813322cdfac1f34f61f4c75c3fc9ef73e598839724b35c7ed6c5e747c3bc697cd7313e78d1d8819828e52
7
- data.tar.gz: 5a0876b411b7215de3b8a0ce006b8b9a0a45b11b7fadb7f758a2608fd784e3d5e4dd003e1f0575c35e43053731a455e6d63aa7a289be271549f876d672c6dadc
6
+ metadata.gz: b563c6418679c743987982a769f0a5878143189d46b868602413b45ee36a83364938ba6a01c697726de5626dab4eb2eb9889c05ad444c07c89e3206cb6e34161
7
+ data.tar.gz: 10fae325297d0caf1cf6ecf12086ec33f78ae71de2df545fd9fcc96a02cc3ab2acd55f4d55c49e5011515eabe8e16eb6dd6a6e892b173f5d75b5cc42040042e6
data/CHANGELOG.md CHANGED
@@ -1,6 +1,7 @@
1
- ### 0.2.0 (Next)
1
+ ### 0.2.0 (2026/09/20)
2
2
 
3
- * Your contribution here.
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
- 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.
8
+ ![jekyll-md](images/jekyll-markdown.gif)
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
- ### jekyll-llms
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
- Unlike `jekyll-md`, [jekyll-llms](https://github.com/skatkov/jekyll-llms) generates Markdown sidecars from each page's **source** your original Markdown/HTML file, with Liquid resolved but otherwise untouched plus an `llms.txt` index. Inline HTML (`<a>`, `<img>`, tables, embeds, etc.) leaks through verbatim, and only pages with Markdown/HTML source get a sidecar, not generated pages like tag or pagination pages.
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
@@ -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 "&nbsp;" 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
+ # "&nbsp;" (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)
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Md
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
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.1.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-19 00:00:00.000000000 Z
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: