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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/lib/jekyll-third-audience/config.rb +22 -0
- data/lib/jekyll-third-audience/generator.rb +76 -0
- data/lib/jekyll-third-audience/meta_tag.rb +25 -0
- data/lib/jekyll-third-audience/version.rb +5 -0
- data/lib/jekyll-third-audience.rb +13 -0
- metadata +111 -0
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('"', """)
|
|
22
|
+
%(<link rel="alternate" type="text/markdown" href="#{md_url}" title="#{title} (Markdown)" />)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
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: []
|