jekyll-third-audience 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eab3d2ed521dc91879c340970539d128cf4cc4af571c690cc52d72786ed34d8b
4
+ data.tar.gz: e015c7017899c8b64935118fd05b1368ca57da324ab67e18c4881faf804bc13b
5
+ SHA512:
6
+ metadata.gz: 8fddeea087660b6eab36c3f7e77db20f9f93949f70064bb7d4a9b3076ad6c387b92612932f1d7d0140239bfcce1edca9d78fc619e058d715545bef7811383bbe
7
+ data.tar.gz: 8dd29c297f61057d732e40d0649c3663e6a0c1b05a252b3f2c98dd92af78fdf21aaaf469317183b7c31f556e4a5a6df581b65891bc5683908cb6ac6bf80f550d
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Drew Breunig
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # jekyll-third-audience
2
+
3
+ A Jekyll plugin that generates clean Markdown copies of blog posts alongside their HTML output, making your content accessible to AI agents — the "third audience" of the web.
4
+
5
+ In this case, "clean" means, "Removing includes and other things that match regex patterns you provide."
6
+
7
+ Will this reduce AI traffic to your site? No, in fact it will likely increase it, as bots will have to read the HTML page to learn the Markdown version exists. But I like Markdown and I wish more sites provided Markdown versions, for human readers as well as robots.
8
+
9
+ Inspired by [Dries Buytaert's article on the 'third audience'](https://dri.es/the-third-satisfying-audience-for-your-blog).
10
+
11
+ ## What it does
12
+
13
+ 1. **Generates `.md` files** — After Jekyll builds your site, the plugin writes a clean Markdown version of each post next to its HTML file. The Markdown version has structured front matter and the raw body content with no HTML, no includes, no Liquid tags.
14
+
15
+ 2. **Adds `<link>` tags** — The `{% third_audience_meta %}` Liquid tag outputs a `<link rel="alternate" type="text/markdown">` tag in your HTML `<head>`, telling AI agents where to find the Markdown version.
16
+
17
+ ## Installation
18
+
19
+ Add to your `Gemfile`:
20
+
21
+ ```ruby
22
+ gem "jekyll-third-audience"
23
+ ```
24
+
25
+ Add to your `_config.yml`:
26
+
27
+ ```yaml
28
+ plugins:
29
+ - jekyll-third-audience
30
+ ```
31
+
32
+ Add to your `_includes/head.html` (or equivalent):
33
+
34
+ ```html
35
+ {% third_audience_meta %}
36
+ ```
37
+
38
+ Run `bundle install`.
39
+
40
+ ## Configuration
41
+
42
+ Add a `third_audience` block to `_config.yml`. All settings are optional — these are the defaults:
43
+
44
+ ```yaml
45
+ third_audience:
46
+ layouts:
47
+ - post
48
+ front_matter:
49
+ - title
50
+ - date
51
+ - author
52
+ - description
53
+ - tags
54
+ - url
55
+ strip_includes: []
56
+ replace_includes: []
57
+ ```
58
+
59
+ ### `layouts`
60
+
61
+ Which layouts get `.md` versions and `<link>` tags. Default: `["post"]`.
62
+
63
+ ### `front_matter`
64
+
65
+ Which fields to include in the Markdown file's YAML front matter. Default: `["title", "date", "author", "description", "tags", "url"]`.
66
+
67
+ ### `strip_includes`
68
+
69
+ List of include filenames to remove entirely from the Markdown output:
70
+
71
+ ```yaml
72
+ third_audience:
73
+ strip_includes:
74
+ - email_subscribe.html
75
+ - contact_form.html
76
+ ```
77
+
78
+ This removes any `{% include email_subscribe.html %}` or `{% include contact_form.html %}` tags from the Markdown body.
79
+
80
+ ### `replace_includes`
81
+
82
+ List of regex pattern/replacement rules for transforming includes:
83
+
84
+ ```yaml
85
+ third_audience:
86
+ replace_includes:
87
+ - pattern: "\\{%\\s*include\\s+emoji_break\\.html.*?%\\}"
88
+ replacement: "\n---\n"
89
+ ```
90
+
91
+ ## Example
92
+
93
+ Given a post at `_posts/2024-01-15-my-post.md`, after `jekyll build` you'll find:
94
+
95
+ - `_site/2024/01/15/my-post.html` — the normal HTML output
96
+ - `_site/2024/01/15/my-post.md` — clean Markdown with structured front matter
97
+
98
+ The Markdown file looks like:
99
+
100
+ ```markdown
101
+ ---
102
+ title: "My Post Title"
103
+ date: 2024-01-15
104
+ author: Your Name
105
+ description: "A description of the post"
106
+ tags: ["jekyll", "markdown"]
107
+ url: https://example.com/2024/01/15/my-post.html
108
+ ---
109
+ The raw markdown body of your post, with all includes
110
+ stripped or replaced per your configuration.
111
+ ```
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllThirdAudience
4
+ class Config
5
+ DEFAULTS = {
6
+ "strip_includes" => [],
7
+ "replace_includes" => [],
8
+ "layouts" => ["post"],
9
+ "front_matter" => %w[title date author description tags url],
10
+ }.freeze
11
+
12
+ attr_reader :strip_includes, :replace_includes, :layouts, :front_matter_fields
13
+
14
+ def initialize(site_config)
15
+ cfg = DEFAULTS.merge(site_config.fetch("third_audience", {}))
16
+ @strip_includes = Array(cfg["strip_includes"])
17
+ @replace_includes = Array(cfg["replace_includes"])
18
+ @layouts = Array(cfg["layouts"])
19
+ @front_matter_fields = Array(cfg["front_matter"])
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllThirdAudience
4
+ class Generator
5
+ def self.register!
6
+ Jekyll::Hooks.register :site, :post_write do |site|
7
+ config = Config.new(site.config)
8
+
9
+ site.posts.docs.each do |post|
10
+ next unless config.layouts.include?(post.data["layout"])
11
+
12
+ md_path = File.join(
13
+ site.dest,
14
+ File.dirname(post.url),
15
+ File.basename(post.url, File.extname(post.url)) + ".md"
16
+ )
17
+
18
+ front = build_front_matter(post, site, config)
19
+ body = extract_body(post, config)
20
+
21
+ FileUtils.mkdir_p(File.dirname(md_path))
22
+ File.write(md_path, front + body + "\n")
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.build_front_matter(post, site, config)
28
+ lines = ["---"]
29
+
30
+ config.front_matter_fields.each do |field|
31
+ case field
32
+ when "title"
33
+ lines << "title: \"#{post.data['title'].to_s.gsub('"', '\\"')}\""
34
+ when "date"
35
+ date = post.data["date"]
36
+ lines << "date: #{date.respond_to?(:strftime) ? date.strftime('%Y-%m-%d') : date}"
37
+ when "author"
38
+ author = post.data["author"] || site.config["author"]
39
+ lines << "author: #{author}" if author
40
+ when "description"
41
+ desc = post.data["description"]
42
+ lines << "description: \"#{desc.to_s.gsub('"', '\\"')}\"" if desc
43
+ when "tags"
44
+ tags = post.data["tags"]
45
+ lines << "tags: #{tags.inspect}" if tags&.any?
46
+ when "url"
47
+ lines << "url: #{site.config['url']}#{post.url}"
48
+ else
49
+ value = post.data[field]
50
+ lines << "#{field}: #{value}" if value
51
+ end
52
+ end
53
+
54
+ lines << "---"
55
+ lines << ""
56
+ lines.join("\n") + "\n"
57
+ end
58
+
59
+ def self.extract_body(post, config)
60
+ raw = File.read(post.path, encoding: "utf-8")
61
+ body = raw.sub(/\A---.*?---\s*/m, "")
62
+
63
+ config.strip_includes.each do |inc|
64
+ pattern = Regexp.new("\\{%\\s*include\\s+#{Regexp.escape(inc)}\\s*%\\}")
65
+ body = body.gsub(pattern, "")
66
+ end
67
+
68
+ config.replace_includes.each do |rule|
69
+ pattern = Regexp.new(rule["pattern"])
70
+ body = body.gsub(pattern, rule["replacement"])
71
+ end
72
+
73
+ body.strip
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllThirdAudience
4
+ class MetaTag < Liquid::Tag
5
+ def render(context)
6
+ site = context.registers[:site]
7
+ page = context.registers[:page]
8
+ config = Config.new(site.config)
9
+
10
+ return "" unless config.layouts.include?(page["layout"])
11
+
12
+ url = page["url"].to_s
13
+ md_url = if url.end_with?("/")
14
+ url + "index.md"
15
+ elsif url.end_with?(".html")
16
+ url.sub(/\.html\z/, ".md")
17
+ else
18
+ url + ".md"
19
+ end
20
+
21
+ title = page["title"].to_s.gsub('"', "&quot;")
22
+ %(<link rel="alternate" type="text/markdown" href="#{md_url}" title="#{title} (Markdown)" />)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllThirdAudience
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jekyll"
4
+ require "jekyll-third-audience/version"
5
+ require "jekyll-third-audience/config"
6
+ require "jekyll-third-audience/generator"
7
+ require "jekyll-third-audience/meta_tag"
8
+
9
+ module JekyllThirdAudience
10
+ end
11
+
12
+ JekyllThirdAudience::Generator.register!
13
+ Liquid::Template.register_tag("third_audience_meta", JekyllThirdAudience::MetaTag)
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-third-audience
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Drew Breunig
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-03-07 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jekyll
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.7'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '5.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.7'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '5.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: bundler
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '2.0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '13.0'
53
+ type: :development
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '13.0'
60
+ - !ruby/object:Gem::Dependency
61
+ name: rspec
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3.0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '3.0'
74
+ description: A Jekyll plugin that generates clean Markdown copies of blog posts alongside
75
+ their HTML output, making your content accessible to AI agents — the 'third audience'
76
+ of the web. (People who like Markdown can use it too!)
77
+ email:
78
+ - dbreunig@gmail.com
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - LICENSE.txt
84
+ - README.md
85
+ - lib/jekyll-third-audience.rb
86
+ - lib/jekyll-third-audience/config.rb
87
+ - lib/jekyll-third-audience/generator.rb
88
+ - lib/jekyll-third-audience/meta_tag.rb
89
+ - lib/jekyll-third-audience/version.rb
90
+ homepage: https://github.com/dbreunig/jekyll-third-audience
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 2.5.0
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.6.5
109
+ specification_version: 4
110
+ summary: Generate clean Markdown versions of Jekyll posts for AI agents.
111
+ test_files: []