jekyll-agent-markdown 0.1.0 → 0.3.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: aefdb0b717baa8666d581d48746d9b0afad4f730bbf25af6e534dc4a13800ec8
4
+ data.tar.gz: c702b617f5b8b022f7f3ddc5fe05ca69eeeadc94ed8ecbb87d20b84406c33291
5
5
  SHA512:
6
- metadata.gz: ee88b5e526b873e40eaa405c99810de61431aabbbf43bff1c8705e6e17ed828df408d94581735dcfed3180b10cf3e04116c8a6c216297fbb89c1cffab6521f45
7
- data.tar.gz: 29c1aaaa685a8c8a44c9ffb7041a0f9c05e9cf1043ebd70aab445a3b5cfc634b0539d2941f56839ed07cb5ce57209657fe708df1f2ecfc0cb170d284f8c0046a
6
+ metadata.gz: aef9d1e51132fd68ea7f6e0936addd32c5d69d1da0ef0a9c721df1076d6e9ee99680f7bd2b6c1005af3080b1fd0cf8091da49a68d423acfb8d587afa2ad64007
7
+ data.tar.gz: db2b8abb8fdb9c4d622483bffc2fadf7c32d65b20b1f338d6c2e09bcdefe6a51c351d32cdd203befdb4a97ec111689833432f794592bb2ac81a78799e56b19c9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-08-20
4
+
5
+ - Include the site author's name in `llms.txt` by default, with an `include_author` opt-out.
6
+
7
+ ## [0.2.0] - 2026-08-20
8
+
9
+ - Add configurable `llms.txt` ordering by normalized published date, defaulting to newest first and placing missing or invalid dates last.
10
+ - Include available published and updated dates in `llms.txt` and Markdown exports by default, with an `include_dates` opt-out.
11
+
3
12
  ## [0.1.0] - 2026-08-18
4
13
 
5
14
  - Export Jekyll posts as raw Markdown siblings.
data/README.md CHANGED
@@ -27,9 +27,14 @@ Both exports are enabled by default:
27
27
  agent_markdown:
28
28
  posts: true
29
29
  llms_txt: true
30
+ sort: desc
31
+ include_author: true
32
+ include_dates: true
30
33
  ```
31
34
 
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.
35
+ `posts`, `llms_txt`, `include_author`, 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.
36
+
37
+ By default, `llms.txt` includes the blog author's name from either a scalar Jekyll setting (`author: Jane Doe`) or a mapping (`author: { name: Jane Doe }`). If no author name is configured, the line is omitted. Set `include_author: false` under `agent_markdown` to omit it explicitly.
33
38
 
34
39
  Set `url` to your site's absolute HTTP(S) URL; the `llms.txt` index uses it to generate absolute article links:
35
40
 
@@ -54,7 +59,16 @@ Markdown sibling paths are deterministic:
54
59
 
55
60
  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
61
 
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:
62
+ 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:
63
+
64
+ ```text
65
+ ---
66
+ Published at: 2026-01-01 | Updated at: 2026-02-03
67
+ ```
68
+
69
+ 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.
70
+
71
+ Exclude one post with front matter:
58
72
 
59
73
  ```yaml
60
74
  agent_markdown: false
@@ -77,11 +91,13 @@ The generated `/llms.txt` is a compact index, for example:
77
91
 
78
92
  > A short description
79
93
 
94
+ Author: Example Author
95
+
80
96
  ## Articles
81
97
 
82
98
  > Posts only. Pages and collections are not included.
83
99
 
84
- - [First article](https://example.com/articles/first.md)
100
+ - [First article](https://example.com/articles/first.md) | Published at: 2026-01-01
85
101
  ```
86
102
 
87
103
  ## Deployment notes
@@ -90,7 +106,7 @@ Configure your host to serve generated `.md` files as `Content-Type: text/markdo
90
106
 
91
107
  ## v0.1.0 limitations
92
108
 
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.
109
+ 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
110
 
95
111
  ## Development
96
112
 
@@ -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_author 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,7 +2,9 @@
2
2
 
3
3
  require "jekyll"
4
4
  require_relative "configuration"
5
+ require_relative "date_metadata"
5
6
  require_relative "destination_claims"
7
+ require_relative "llms_headings"
6
8
  require_relative "markdown_sibling_path"
7
9
  require_relative "raw_markdown_file"
8
10
 
@@ -24,7 +26,7 @@ module Jekyll
24
26
 
25
27
  @context = Liquid::Context.new({}, {}, { site: site })
26
28
  destination_claims = destination_claims(site)
27
- included_posts = export_posts(site, destination_claims) if Configuration.enabled?(settings, "posts")
29
+ included_posts = export_posts(site, settings, destination_claims) if Configuration.enabled?(settings, "posts")
28
30
  write_llms_txt(site, settings, included_posts || [], destination_claims)
29
31
  end
30
32
 
@@ -36,16 +38,16 @@ module Jekyll
36
38
  end
37
39
  end
38
40
 
39
- def export_posts(site, destination_claims)
40
- site.posts.docs.filter_map { |post| export_post(site, destination_claims, post) }
41
+ def export_posts(site, settings, destination_claims)
42
+ site.posts.docs.filter_map { |post| export_post(site, settings, destination_claims, post) }
41
43
  end
42
44
 
43
- def export_post(site, destination_claims, post)
45
+ def export_post(site, settings, destination_claims, post)
44
46
  setting = post.data.fetch("agent_markdown", true)
45
47
  setting_name = "agent_markdown in #{post.relative_path}"
46
48
  return unless Configuration.enabled_value?(setting, name: setting_name)
47
49
 
48
- file = RawMarkdownFile.new(site, MarkdownSiblingPath.for(post.url), post.content)
50
+ file = RawMarkdownFile.new(site, MarkdownSiblingPath.for(post.url), post_content(post, settings))
49
51
  url = file.url
50
52
  return collision_warning(post, url) unless claim_destination?(destination_claims, site, file)
51
53
 
@@ -64,15 +66,13 @@ module Jekyll
64
66
  return unless Configuration.enabled?(settings, "llms_txt")
65
67
  return unless llms_txt_ready?(site, settings)
66
68
 
67
- file = RawMarkdownFile.new(site, "/llms.txt", llms_txt(site, posts))
69
+ file = RawMarkdownFile.new(site, "/llms.txt", llms_txt(site, posts, settings))
68
70
  return llms_txt_collision_warning unless claim_destination?(destination_claims, site, file)
69
71
 
70
72
  site.static_files << file
71
73
  end
72
74
 
73
- def claim_destination?(destination_claims, site, file)
74
- destination_claims.claim?(file.destination(site.dest))
75
- end
75
+ def claim_destination?(destination_claims, site, file) = destination_claims.claim?(file.destination(site.dest))
76
76
 
77
77
  def llms_txt_collision_warning
78
78
  Jekyll.logger.warn "AgentMarkdown:",
@@ -91,37 +91,39 @@ module Jekyll
91
91
  false
92
92
  end
93
93
 
94
- def llms_txt(site, posts)
95
- sections = [llms_headings(site), article_links(site, posts)]
94
+ def llms_txt(site, posts, settings)
95
+ sections = [LlmsHeadings.new(site, settings).to_s, article_links(site, posts, settings)]
96
96
  "#{sections.reject(&:empty?).join("\n\n")}\n"
97
97
  end
98
98
 
99
- def llms_headings(site)
100
- title = one_line(site.config.fetch("title", ""))
101
- headings = ["# #{title}".rstrip]
102
- description = one_line(site.config["description"])
103
- headings << "> #{description}" unless description.empty?
104
- headings << "## Articles"
105
- headings << "> Posts only. Pages and collections are not included."
106
- headings.join("\n\n")
99
+ def article_links(site, posts, settings)
100
+ site_url = site.config["url"].sub(%r{/+\z}, "")
101
+ sorted_posts(posts, settings).map { |post| article_link(site_url, post, settings) }.join("\n")
107
102
  end
108
103
 
109
- def one_line(value)
110
- return "" if value.nil? || value == false
111
-
112
- value.to_s.gsub(/\s+/, " ").strip
104
+ def sorted_posts(posts, settings)
105
+ dated, undated = posts.map { |post| [post, date_metadata(post).published_date] }.partition(&:last)
106
+ dated.sort_by!(&:last)
107
+ dated.reverse! if Configuration.sort_order(settings) == "desc"
108
+ (dated + undated).map(&:first)
113
109
  end
114
110
 
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")
111
+ def article_link(site_url, post, settings)
112
+ url = "#{site_url}#{relative_url(post.data.fetch("agent_markdown_url"))}"
113
+ link = "- [#{link_title(post)}](#{escaped_link_url(url)})"
114
+ return link unless Configuration.enabled?(settings, "include_dates")
115
+
116
+ [link, date_metadata(post).to_s].reject(&:empty?).join(" | ")
118
117
  end
119
118
 
120
- def article_link(site_url, post)
121
- url = "#{site_url}#{relative_url(post.data.fetch("agent_markdown_url"))}"
122
- "- [#{link_title(post)}](#{escaped_link_url(url)})"
119
+ def post_content(post, settings)
120
+ return post.content unless Configuration.enabled?(settings, "include_dates")
121
+
122
+ date_metadata(post).append_to(post.content)
123
123
  end
124
124
 
125
+ def date_metadata(post) = DateMetadata.new(post.data)
126
+
125
127
  # Backslashes and square brackets would end the Markdown link text early;
126
128
  # whitespace runs (including newlines) would break the one-entry-per-line
127
129
  # format.
@@ -132,9 +134,7 @@ module Jekyll
132
134
  end
133
135
 
134
136
  # Unescaped parentheses would end the Markdown link destination early.
135
- def escaped_link_url(url)
136
- url.gsub("(", "%28").gsub(")", "%29")
137
- end
137
+ def escaped_link_url(url) = url.gsub("(", "%28").gsub(")", "%29")
138
138
  end
139
139
  end
140
140
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "configuration"
4
+
5
+ module Jekyll
6
+ module AgentMarkdown
7
+ class LlmsHeadings
8
+ def initialize(site, settings)
9
+ @site = site
10
+ @settings = settings
11
+ end
12
+
13
+ def to_s
14
+ [title_heading, description_heading, author_heading, "## Articles", articles_description]
15
+ .compact.join("\n\n")
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :site, :settings
21
+
22
+ def title_heading = "# #{one_line(site.config.fetch("title", ""))}".rstrip
23
+
24
+ def description_heading
25
+ description = one_line(site.config["description"])
26
+ "> #{description}" unless description.empty?
27
+ end
28
+
29
+ def author_heading
30
+ return unless Configuration.enabled?(settings, "include_author")
31
+
32
+ name = author_name
33
+ "Author: #{name}" unless name.empty?
34
+ end
35
+
36
+ def author_name
37
+ author = site.config["author"]
38
+ author = author["name"] || author[:name] if author.is_a?(Hash)
39
+ one_line(author)
40
+ end
41
+
42
+ def articles_description = "> Posts only. Pages and collections are not included."
43
+
44
+ def one_line(value) = value ? value.to_s.gsub(/\s+/, " ").strip : ""
45
+ end
46
+ end
47
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module AgentMarkdown
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucian Ghinda
@@ -42,8 +42,10 @@ 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
48
+ - lib/jekyll/agent_markdown/llms_headings.rb
47
49
  - lib/jekyll/agent_markdown/markdown_sibling_path.rb
48
50
  - lib/jekyll/agent_markdown/raw_markdown_file.rb
49
51
  - lib/jekyll/agent_markdown/version.rb