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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b29b2ab68b394cfc7b37489422cbb516b6f4c3dce9af32aeb459438fce2b41ee
4
- data.tar.gz: ce818d0127fcaf5cb89788144a32795264c962406219db55360fedf3bfb253b0
3
+ metadata.gz: 747268dbb22f5d319ed8f760fa97ff9503eb12b632a44c8ef74bcef8694d821d
4
+ data.tar.gz: 57e5b25acc73b618d1d74f3e11f8e20f44393af1f976bfd17669e2f6d52b226a
5
5
  SHA512:
6
- metadata.gz: ee88b5e526b873e40eaa405c99810de61431aabbbf43bff1c8705e6e17ed828df408d94581735dcfed3180b10cf3e04116c8a6c216297fbb89c1cffab6521f45
7
- data.tar.gz: 29c1aaaa685a8c8a44c9ffb7041a0f9c05e9cf1043ebd70aab445a3b5cfc634b0539d2941f56839ed07cb5ce57209657fe708df1f2ecfc0cb170d284f8c0046a
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 `llms_txt` are the only accepted settings. Each accepts `true`, `false`, or one of the false-style strings `"false"`, `"no"`, and `"off"` (case-insensitively). 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 same rules, so a mistyped opt-out cannot publish raw source by accident.
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 contains the post's original Markdown body with no front matter, HTML conversion, or Liquid rendering. Liquid tags and directives such as `{{ site.title }}` are published literally. Exclude one post with front matter:
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 AI-specific metadata are intentionally deferred.
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
- validate_value!("agent_markdown.#{key}", value)
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.content)
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
- value.to_s.gsub(/\s+/, " ").strip
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 article_links(site, posts)
116
- site_url = site.config["url"].sub(%r{/+\z}, "")
117
- posts.map { |post| article_link(site_url, post) }.join("\n")
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module AgentMarkdown
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  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.1.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