jekyll-agent-markdown 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 +5 -0
- data/README.md +15 -4
- data/lib/jekyll/agent_markdown/configuration.rb +18 -2
- data/lib/jekyll/agent_markdown/date_metadata.rb +60 -0
- data/lib/jekyll/agent_markdown/generator.rb +33 -22
- data/lib/jekyll/agent_markdown/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 747268dbb22f5d319ed8f760fa97ff9503eb12b632a44c8ef74bcef8694d821d
|
|
4
|
+
data.tar.gz: 57e5b25acc73b618d1d74f3e11f8e20f44393af1f976bfd17669e2f6d52b226a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02e6fa6d6a722bee4b94052f2a2d42ea0d5de18599f4380da778c1e42390a7b54e346d68e3830b7cb0bbb35c0634031ae4fac1c2d8374c074947191f38248cb6
|
|
7
|
+
data.tar.gz: 715edcbd124b53fca22dedfbf96e115fc4ed1373797ef27ded43868c551f5ec61aecabc1f8057612a842f237d9a544c20e228ecd9ec33a3899d0c308854fb11c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-08-20
|
|
4
|
+
|
|
5
|
+
- Add configurable `llms.txt` ordering by normalized published date, defaulting to newest first and placing missing or invalid dates last.
|
|
6
|
+
- Include available published and updated dates in `llms.txt` and Markdown exports by default, with an `include_dates` opt-out.
|
|
7
|
+
|
|
3
8
|
## [0.1.0] - 2026-08-18
|
|
4
9
|
|
|
5
10
|
- Export Jekyll posts as raw Markdown siblings.
|
data/README.md
CHANGED
|
@@ -27,9 +27,11 @@ Both exports are enabled by default:
|
|
|
27
27
|
agent_markdown:
|
|
28
28
|
posts: true
|
|
29
29
|
llms_txt: true
|
|
30
|
+
sort: desc
|
|
31
|
+
include_dates: true
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
`posts` and `
|
|
34
|
+
`posts`, `llms_txt`, and `include_dates` accept `true`, `false`, or one of the false-style strings `"false"`, `"no"`, and `"off"` (case-insensitively). `sort` accepts `asc` or `desc` and defaults to `desc`, ordering the `llms.txt` article list by normalized published date. Posts with a missing, incomplete, or invalid published date are listed last in their existing order. Unknown keys and other values stop the build with a configuration error instead of being silently ignored. An explicit per-post `agent_markdown` value follows the boolean-setting rules, so a mistyped opt-out cannot publish raw source by accident.
|
|
33
35
|
|
|
34
36
|
Set `url` to your site's absolute HTTP(S) URL; the `llms.txt` index uses it to generate absolute article links:
|
|
35
37
|
|
|
@@ -54,7 +56,16 @@ Markdown sibling paths are deterministic:
|
|
|
54
56
|
|
|
55
57
|
Extensions are matched case-insensitively, and percent-encoded aliases are compared by their final decoded destination. Destination ownership also treats case-only and Unicode-normalization aliases as equivalent on every platform, keeping builds portable across filesystems. File-versus-directory conflicts are reserved as well. Before adding an export, the plugin checks every page, static file, and writable collection document already known to Jekyll. The existing destination owner wins; later post exports are skipped with a warning and omitted from `llms.txt`. A committed `llms.txt` wins in the same way.
|
|
56
58
|
|
|
57
|
-
The generated file
|
|
59
|
+
The generated file preserves the post's original Markdown body as its initial content, with no front matter, HTML conversion, or Liquid rendering. Liquid tags and directives such as `{{ site.title }}` are published literally. By default, the plugin appends the post's published `date` and optional `last_modified_at` value as a small footer:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
---
|
|
63
|
+
Published at: 2026-01-01 | Updated at: 2026-02-03
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Each available date is formatted as `YYYY-MM-DD`. String values must contain an explicit year, month, and day; missing, incomplete, and invalid values and their labels are omitted. An empty post contains only the metadata line, without a leading `---` that could be mistaken for front matter. Set `include_dates: false` to omit date metadata from both Markdown exports and `llms.txt`, leaving the body-only export shape used before date metadata was introduced.
|
|
67
|
+
|
|
68
|
+
Exclude one post with front matter:
|
|
58
69
|
|
|
59
70
|
```yaml
|
|
60
71
|
agent_markdown: false
|
|
@@ -81,7 +92,7 @@ The generated `/llms.txt` is a compact index, for example:
|
|
|
81
92
|
|
|
82
93
|
> Posts only. Pages and collections are not included.
|
|
83
94
|
|
|
84
|
-
- [First article](https://example.com/articles/first.md)
|
|
95
|
+
- [First article](https://example.com/articles/first.md) | Published at: 2026-01-01
|
|
85
96
|
```
|
|
86
97
|
|
|
87
98
|
## Deployment notes
|
|
@@ -90,7 +101,7 @@ Configure your host to serve generated `.md` files as `Content-Type: text/markdo
|
|
|
90
101
|
|
|
91
102
|
## v0.1.0 limitations
|
|
92
103
|
|
|
93
|
-
Only posts are exported. Pages and custom collections, custom Markdown transformations or templates, front-matter allowlisting, automatic response headers, sitemaps, and
|
|
104
|
+
Only posts are exported. Pages and custom collections, custom Markdown transformations or templates, front-matter allowlisting, automatic response headers, sitemaps, and richer article metadata are intentionally deferred.
|
|
94
105
|
|
|
95
106
|
## Development
|
|
96
107
|
|
|
@@ -6,8 +6,9 @@ require "uri"
|
|
|
6
6
|
module Jekyll
|
|
7
7
|
module AgentMarkdown
|
|
8
8
|
class Configuration
|
|
9
|
-
ALLOWED_SETTINGS = %w[llms_txt posts].freeze
|
|
9
|
+
ALLOWED_SETTINGS = %w[include_dates llms_txt posts sort].freeze
|
|
10
10
|
FALSE_STRINGS = %w[false no off].freeze
|
|
11
|
+
SORT_ORDERS = %w[asc desc].freeze
|
|
11
12
|
|
|
12
13
|
class << self
|
|
13
14
|
def for(site)
|
|
@@ -26,6 +27,10 @@ module Jekyll
|
|
|
26
27
|
!disabled?(settings.fetch(key, true))
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
def sort_order(settings)
|
|
31
|
+
settings.fetch("sort", "desc")
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
def enabled_value?(value, name:)
|
|
30
35
|
validate_value!(name, value)
|
|
31
36
|
!disabled?(value)
|
|
@@ -66,10 +71,21 @@ module Jekyll
|
|
|
66
71
|
|
|
67
72
|
def validate_values!(settings)
|
|
68
73
|
settings.each do |key, value|
|
|
69
|
-
|
|
74
|
+
if key == "sort"
|
|
75
|
+
validate_sort_order!(value)
|
|
76
|
+
else
|
|
77
|
+
validate_value!("agent_markdown.#{key}", value)
|
|
78
|
+
end
|
|
70
79
|
end
|
|
71
80
|
end
|
|
72
81
|
|
|
82
|
+
def validate_sort_order!(value)
|
|
83
|
+
return if SORT_ORDERS.include?(value)
|
|
84
|
+
|
|
85
|
+
raise Jekyll::Errors::FatalException,
|
|
86
|
+
"agent_markdown.sort must be asc or desc; got #{value.inspect}"
|
|
87
|
+
end
|
|
88
|
+
|
|
73
89
|
def validate_value!(name, value)
|
|
74
90
|
return if valid_value?(value)
|
|
75
91
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Jekyll
|
|
6
|
+
module AgentMarkdown
|
|
7
|
+
class DateMetadata
|
|
8
|
+
DATE_FORMAT = "%Y-%m-%d"
|
|
9
|
+
FIELDS = {
|
|
10
|
+
"Published at" => "date",
|
|
11
|
+
"Updated at" => "last_modified_at"
|
|
12
|
+
}.freeze
|
|
13
|
+
REQUIRED_DATE_PARTS = %i[year mon mday].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(data)
|
|
16
|
+
@data = data
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s
|
|
20
|
+
@to_s ||= FIELDS.filter_map { |label, key| entry(label, @data[key]) }.join(" | ")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def append_to(content)
|
|
24
|
+
return content if to_s.empty?
|
|
25
|
+
return "#{self}\n" if content.empty?
|
|
26
|
+
|
|
27
|
+
"#{content}#{separator_for(content)}---\n#{self}\n"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def published_date = parsed_date(@data["date"])
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def entry(label, value)
|
|
35
|
+
date = formatted_date(value)
|
|
36
|
+
"#{label}: #{date}" if date
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def separator_for(content)
|
|
40
|
+
content.end_with?("\n") ? "\n" : "\n\n"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def formatted_date(value)
|
|
44
|
+
parsed_date(value)&.strftime(DATE_FORMAT)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parsed_date(value)
|
|
48
|
+
return Date.strptime(value.strftime(DATE_FORMAT), DATE_FORMAT) if value.respond_to?(:strftime)
|
|
49
|
+
return unless value.is_a?(String)
|
|
50
|
+
|
|
51
|
+
parts = Date._parse(value, false)
|
|
52
|
+
return unless REQUIRED_DATE_PARTS.all? { |part| parts.key?(part) }
|
|
53
|
+
|
|
54
|
+
Date.new(*parts.values_at(*REQUIRED_DATE_PARTS))
|
|
55
|
+
rescue Date::Error
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "jekyll"
|
|
4
4
|
require_relative "configuration"
|
|
5
|
+
require_relative "date_metadata"
|
|
5
6
|
require_relative "destination_claims"
|
|
6
7
|
require_relative "markdown_sibling_path"
|
|
7
8
|
require_relative "raw_markdown_file"
|
|
@@ -24,7 +25,7 @@ module Jekyll
|
|
|
24
25
|
|
|
25
26
|
@context = Liquid::Context.new({}, {}, { site: site })
|
|
26
27
|
destination_claims = destination_claims(site)
|
|
27
|
-
included_posts = export_posts(site, destination_claims) if Configuration.enabled?(settings, "posts")
|
|
28
|
+
included_posts = export_posts(site, settings, destination_claims) if Configuration.enabled?(settings, "posts")
|
|
28
29
|
write_llms_txt(site, settings, included_posts || [], destination_claims)
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -36,16 +37,16 @@ module Jekyll
|
|
|
36
37
|
end
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
def export_posts(site, destination_claims)
|
|
40
|
-
site.posts.docs.filter_map { |post| export_post(site, destination_claims, post) }
|
|
40
|
+
def export_posts(site, settings, destination_claims)
|
|
41
|
+
site.posts.docs.filter_map { |post| export_post(site, settings, destination_claims, post) }
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
def export_post(site, destination_claims, post)
|
|
44
|
+
def export_post(site, settings, destination_claims, post)
|
|
44
45
|
setting = post.data.fetch("agent_markdown", true)
|
|
45
46
|
setting_name = "agent_markdown in #{post.relative_path}"
|
|
46
47
|
return unless Configuration.enabled_value?(setting, name: setting_name)
|
|
47
48
|
|
|
48
|
-
file = RawMarkdownFile.new(site, MarkdownSiblingPath.for(post.url), post
|
|
49
|
+
file = RawMarkdownFile.new(site, MarkdownSiblingPath.for(post.url), post_content(post, settings))
|
|
49
50
|
url = file.url
|
|
50
51
|
return collision_warning(post, url) unless claim_destination?(destination_claims, site, file)
|
|
51
52
|
|
|
@@ -64,15 +65,13 @@ module Jekyll
|
|
|
64
65
|
return unless Configuration.enabled?(settings, "llms_txt")
|
|
65
66
|
return unless llms_txt_ready?(site, settings)
|
|
66
67
|
|
|
67
|
-
file = RawMarkdownFile.new(site, "/llms.txt", llms_txt(site, posts))
|
|
68
|
+
file = RawMarkdownFile.new(site, "/llms.txt", llms_txt(site, posts, settings))
|
|
68
69
|
return llms_txt_collision_warning unless claim_destination?(destination_claims, site, file)
|
|
69
70
|
|
|
70
71
|
site.static_files << file
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
def claim_destination?(destination_claims, site, file)
|
|
74
|
-
destination_claims.claim?(file.destination(site.dest))
|
|
75
|
-
end
|
|
74
|
+
def claim_destination?(destination_claims, site, file) = destination_claims.claim?(file.destination(site.dest))
|
|
76
75
|
|
|
77
76
|
def llms_txt_collision_warning
|
|
78
77
|
Jekyll.logger.warn "AgentMarkdown:",
|
|
@@ -91,8 +90,8 @@ module Jekyll
|
|
|
91
90
|
false
|
|
92
91
|
end
|
|
93
92
|
|
|
94
|
-
def llms_txt(site, posts)
|
|
95
|
-
sections = [llms_headings(site), article_links(site, posts)]
|
|
93
|
+
def llms_txt(site, posts, settings)
|
|
94
|
+
sections = [llms_headings(site), article_links(site, posts, settings)]
|
|
96
95
|
"#{sections.reject(&:empty?).join("\n\n")}\n"
|
|
97
96
|
end
|
|
98
97
|
|
|
@@ -106,22 +105,36 @@ module Jekyll
|
|
|
106
105
|
headings.join("\n\n")
|
|
107
106
|
end
|
|
108
107
|
|
|
109
|
-
def one_line(value)
|
|
110
|
-
return "" if value.nil? || value == false
|
|
108
|
+
def one_line(value) = value ? value.to_s.gsub(/\s+/, " ").strip : ""
|
|
111
109
|
|
|
112
|
-
|
|
110
|
+
def article_links(site, posts, settings)
|
|
111
|
+
site_url = site.config["url"].sub(%r{/+\z}, "")
|
|
112
|
+
sorted_posts(posts, settings).map { |post| article_link(site_url, post, settings) }.join("\n")
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
def sorted_posts(posts, settings)
|
|
116
|
+
dated, undated = posts.map { |post| [post, date_metadata(post).published_date] }.partition(&:last)
|
|
117
|
+
dated.sort_by!(&:last)
|
|
118
|
+
dated.reverse! if Configuration.sort_order(settings) == "desc"
|
|
119
|
+
(dated + undated).map(&:first)
|
|
118
120
|
end
|
|
119
121
|
|
|
120
|
-
def article_link(site_url, post)
|
|
122
|
+
def article_link(site_url, post, settings)
|
|
121
123
|
url = "#{site_url}#{relative_url(post.data.fetch("agent_markdown_url"))}"
|
|
122
|
-
"- [#{link_title(post)}](#{escaped_link_url(url)})"
|
|
124
|
+
link = "- [#{link_title(post)}](#{escaped_link_url(url)})"
|
|
125
|
+
return link unless Configuration.enabled?(settings, "include_dates")
|
|
126
|
+
|
|
127
|
+
[link, date_metadata(post).to_s].reject(&:empty?).join(" | ")
|
|
123
128
|
end
|
|
124
129
|
|
|
130
|
+
def post_content(post, settings)
|
|
131
|
+
return post.content unless Configuration.enabled?(settings, "include_dates")
|
|
132
|
+
|
|
133
|
+
date_metadata(post).append_to(post.content)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def date_metadata(post) = DateMetadata.new(post.data)
|
|
137
|
+
|
|
125
138
|
# Backslashes and square brackets would end the Markdown link text early;
|
|
126
139
|
# whitespace runs (including newlines) would break the one-entry-per-line
|
|
127
140
|
# format.
|
|
@@ -132,9 +145,7 @@ module Jekyll
|
|
|
132
145
|
end
|
|
133
146
|
|
|
134
147
|
# Unescaped parentheses would end the Markdown link destination early.
|
|
135
|
-
def escaped_link_url(url)
|
|
136
|
-
url.gsub("(", "%28").gsub(")", "%29")
|
|
137
|
-
end
|
|
148
|
+
def escaped_link_url(url) = url.gsub("(", "%28").gsub(")", "%29")
|
|
138
149
|
end
|
|
139
150
|
end
|
|
140
151
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-agent-markdown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucian Ghinda
|
|
@@ -42,6 +42,7 @@ files:
|
|
|
42
42
|
- README.md
|
|
43
43
|
- lib/jekyll-agent-markdown.rb
|
|
44
44
|
- lib/jekyll/agent_markdown/configuration.rb
|
|
45
|
+
- lib/jekyll/agent_markdown/date_metadata.rb
|
|
45
46
|
- lib/jekyll/agent_markdown/destination_claims.rb
|
|
46
47
|
- lib/jekyll/agent_markdown/generator.rb
|
|
47
48
|
- lib/jekyll/agent_markdown/markdown_sibling_path.rb
|