jekyll-agent-markdown 0.3.0 → 0.4.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 +12 -1
- data/README.md +231 -42
- data/docs/deployment.md +138 -0
- data/examples/cloudflare/src/worker.js +254 -0
- data/examples/cloudflare/wrangler.toml +8 -0
- data/examples/netlify/netlify/edge-functions/markdown-negotiation.ts +281 -0
- data/examples/netlify/netlify.toml +6 -0
- data/examples/nginx/negotiation.js +267 -0
- data/examples/nginx/nginx.conf +40 -0
- data/lib/jekyll/agent_markdown/agent_markdown_link_tag.rb +20 -0
- data/lib/jekyll/agent_markdown/author_metadata.rb +28 -0
- data/lib/jekyll/agent_markdown/collection_validator.rb +73 -0
- data/lib/jekyll/agent_markdown/configuration.rb +67 -6
- data/lib/jekyll/agent_markdown/date_metadata.rb +2 -8
- data/lib/jekyll/agent_markdown/document_exporter.rb +113 -0
- data/lib/jekyll/agent_markdown/document_header.rb +59 -0
- data/lib/jekyll/agent_markdown/document_settings.rb +170 -0
- data/lib/jekyll/agent_markdown/exported_document.rb +17 -0
- data/lib/jekyll/agent_markdown/generator.rb +78 -70
- data/lib/jekyll/agent_markdown/llms_document_index.rb +125 -0
- data/lib/jekyll/agent_markdown/llms_document_ordering.rb +40 -0
- data/lib/jekyll/agent_markdown/llms_full_renderer.rb +72 -0
- data/lib/jekyll/agent_markdown/llms_headings.rb +10 -15
- data/lib/jekyll/agent_markdown/llms_index_renderer.rb +102 -0
- data/lib/jekyll/agent_markdown/llms_text.rb +30 -0
- data/lib/jekyll/agent_markdown/metadata_footer.rb +26 -0
- data/lib/jekyll/agent_markdown/source_documents.rb +66 -0
- data/lib/jekyll/agent_markdown/version.rb +1 -1
- data/lib/jekyll-agent-markdown.rb +3 -0
- metadata +25 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 239761c2150364ff0d2966b3b7999f03a65c578bffeb144f2f69c4ad5da2c6cc
|
|
4
|
+
data.tar.gz: 9b7d54ce53a9f8cf68c6278a6811bd1405c89d21440a99920f64ac509dfdfc4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e955bf1a3cc544b9b0e4c5b0fe76e0f95d2550ee57f2ccc73d31e0f2f817e460391833ff9a4cd8097e72bcdc94b901afdd017e7343c9613fb51f4b9b19e95328
|
|
7
|
+
data.tar.gz: ca1cb1d9b76c276a34d83a515ca0f1211014b43c27e05e6aee398884c483354b3825906ef7b527d739ec7a6b909abbbc81d2862d539e9d36fb1bca19194752e3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.4.0] - 2026-08-25
|
|
2
|
+
|
|
3
|
+
- Export Markdown-backed pages and selected output collections alongside posts.
|
|
4
|
+
- Add per-document export, index, section, optional, and document-header settings.
|
|
5
|
+
- Curate `llms.txt` with sections and descriptions, and add optional `llms-full.txt` output.
|
|
6
|
+
- Register `{% agent_markdown_link %}` and expose `agent_markdown_url` for custom layouts.
|
|
7
|
+
- Package Cloudflare Workers, Netlify Edge, and nginx+njs negotiation recipes.
|
|
8
|
+
- Preserve existing posts-only defaults, collision handling, raw Markdown, and metadata output.
|
|
9
|
+
|
|
10
|
+
## [0.3.1] - 2026-08-20
|
|
11
|
+
|
|
12
|
+
- Include the site author in each exported article's metadata footer on the same line as published and updated dates.
|
|
2
13
|
|
|
3
14
|
## [0.3.0] - 2026-08-20
|
|
4
15
|
|
data/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# jekyll-agent-markdown
|
|
2
2
|
|
|
3
|
-
Export Jekyll
|
|
3
|
+
Export Jekyll content as Markdown siblings and curated `llms.txt` and `llms-full.txt` indexes.
|
|
4
|
+
|
|
5
|
+
AI agents and LLM crawlers read Markdown better than rendered HTML.
|
|
6
|
+
This plugin publishes a Markdown copy of each exported post, page, and collection document next to its HTML.
|
|
7
|
+
It also writes the index files described by the [llms.txt convention](https://llmstxt.org).
|
|
4
8
|
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
7
|
-
Add the gem to your site's `Gemfile
|
|
11
|
+
Add the gem to your site's `Gemfile`.
|
|
8
12
|
|
|
9
13
|
```ruby
|
|
10
14
|
group :jekyll_plugins do
|
|
@@ -12,44 +16,68 @@ group :jekyll_plugins do
|
|
|
12
16
|
end
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
Install dependencies.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The `:jekyll_plugins` group already enables the plugin.
|
|
26
|
+
If you keep the gem outside that group, list it under `plugins:` instead.
|
|
16
27
|
|
|
17
28
|
```yaml
|
|
18
29
|
plugins:
|
|
19
30
|
- jekyll-agent-markdown
|
|
20
31
|
```
|
|
21
32
|
|
|
22
|
-
##
|
|
33
|
+
## Quick Start
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
Configure the sources you want and build the site.
|
|
25
36
|
|
|
26
37
|
```yaml
|
|
38
|
+
url: https://example.com
|
|
39
|
+
collections:
|
|
40
|
+
guides:
|
|
41
|
+
output: true
|
|
27
42
|
agent_markdown:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
include_author: true
|
|
32
|
-
include_dates: true
|
|
43
|
+
pages: true
|
|
44
|
+
collections:
|
|
45
|
+
- guides
|
|
33
46
|
```
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
```sh
|
|
49
|
+
bundle exec jekyll build
|
|
50
|
+
```
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
The build now writes:
|
|
38
53
|
|
|
39
|
-
|
|
54
|
+
- `/llms.txt`, an index of the exported documents
|
|
55
|
+
- a Markdown sibling for every post, Markdown page, and `guides` document, such as `/guides/start.md` next to `/guides/start/`
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
Enable the sources you want.
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
- Posts export by default.
|
|
62
|
+
- Markdown-backed pages need `pages: true`.
|
|
63
|
+
- Markdown collection documents need `collections` and `output: true`.
|
|
64
|
+
- Non-Markdown pages, non-Markdown collection documents, and generated pages are ignored.
|
|
65
|
+
- A missing or non-output collection fails the build.
|
|
66
|
+
- A selected collection with a Markdown document lacking a public document URL fails.
|
|
67
|
+
- The reserved `posts` collection cannot appear in `collections`.
|
|
46
68
|
|
|
47
|
-
|
|
69
|
+
Use site-level `agent_markdown: true` to enable the defaults.
|
|
70
|
+
Use site-level `agent_markdown: false` to disable every export.
|
|
71
|
+
Boolean settings accept `true` and `false` scalars.
|
|
72
|
+
They also accept `"false"`, `"no"`, and `"off"` as false.
|
|
73
|
+
False-style strings are case-insensitive and work in front matter.
|
|
74
|
+
Unknown keys, wrong types, and invalid combinations fail the build.
|
|
75
|
+
Duplicate collection names also fail the build.
|
|
48
76
|
|
|
49
|
-
Markdown
|
|
77
|
+
Markdown siblings follow the permalink.
|
|
50
78
|
|
|
51
79
|
| Post URL | Markdown URL |
|
|
52
|
-
|
|
80
|
+
| --- | --- |
|
|
53
81
|
| `/foo/` | `/foo.md` |
|
|
54
82
|
| `/foo.html`, `/foo.htm` | `/foo.md` |
|
|
55
83
|
| `/foo.html/` | `/foo.html.md` |
|
|
@@ -57,34 +85,71 @@ Markdown sibling paths are deterministic:
|
|
|
57
85
|
| `/foo` | `/foo.md` |
|
|
58
86
|
| `/` | `/index.md` |
|
|
59
87
|
|
|
60
|
-
|
|
61
|
-
|
|
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:
|
|
88
|
+
Each post export ends with a metadata footer built from the available dates and the site author.
|
|
89
|
+
Disable it with `include_dates: false` and `include_author: false`.
|
|
63
90
|
|
|
64
91
|
```text
|
|
92
|
+
Post body.
|
|
93
|
+
|
|
65
94
|
---
|
|
66
|
-
Published at: 2026-01-01 | Updated at: 2026-
|
|
95
|
+
Published at: 2026-01-01 | Updated at: 2026-01-05 | Author: Example Author
|
|
67
96
|
```
|
|
68
97
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
Exclude one post with front matter:
|
|
98
|
+
Set per-document settings in front matter.
|
|
72
99
|
|
|
73
100
|
```yaml
|
|
74
101
|
agent_markdown: false
|
|
75
102
|
```
|
|
76
103
|
|
|
77
|
-
|
|
104
|
+
```yaml
|
|
105
|
+
agent_markdown:
|
|
106
|
+
export: true
|
|
107
|
+
index: true
|
|
108
|
+
section: Documentation
|
|
109
|
+
optional: false
|
|
110
|
+
include_document_header: true
|
|
111
|
+
```
|
|
78
112
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
Use `optional: true` without `section` for the `Optional` section.
|
|
114
|
+
Front matter cannot enable a globally excluded source kind.
|
|
115
|
+
`export: false` also disables `index`.
|
|
116
|
+
Do not combine `export: false` with `index: true`.
|
|
117
|
+
Do not combine `optional: true` with `section`.
|
|
118
|
+
|
|
119
|
+
Add `include_document_header: true` to prepend a small header.
|
|
120
|
+
That setting needs a valid absolute `url`.
|
|
121
|
+
A valid `url` uses HTTP(S) without credentials, a query, or a fragment.
|
|
122
|
+
|
|
123
|
+
The header holds the title, the front matter description when present, and a `Source:` link to the HTML page.
|
|
124
|
+
The body below the divider stays untouched.
|
|
125
|
+
|
|
126
|
+
```markdown
|
|
127
|
+
# About this site
|
|
128
|
+
|
|
129
|
+
A one-line description from front matter
|
|
130
|
+
|
|
131
|
+
Source: https://example.com/about/
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## What we do
|
|
136
|
+
|
|
137
|
+
Raw page body.
|
|
83
138
|
```
|
|
84
139
|
|
|
85
|
-
|
|
140
|
+
Use `llms_txt: true` for the compact index.
|
|
141
|
+
`llms.txt` has two layouts.
|
|
142
|
+
With the posts-only defaults it keeps the original single-list layout, byte-for-byte identical with releases before 0.4.0.
|
|
143
|
+
Enabling pages, collections, or descriptions, or using `section` or `optional` in front matter, switches to the sectioned layout shown below.
|
|
86
144
|
|
|
87
|
-
|
|
145
|
+
Add `include_descriptions: true` for sanitized descriptions.
|
|
146
|
+
Descriptions fall back to the document excerpt.
|
|
147
|
+
Use `sort: asc` or `sort: desc` for each section.
|
|
148
|
+
Set `include_author: false` or `include_dates: false` to trim metadata.
|
|
149
|
+
Mark a document `optional: true` to move it under `Optional`.
|
|
150
|
+
Default sections appear before custom sections.
|
|
151
|
+
Custom sections keep their first-occurrence order.
|
|
152
|
+
The `Optional` section always appears last.
|
|
88
153
|
|
|
89
154
|
```text
|
|
90
155
|
# Example Site
|
|
@@ -95,24 +160,148 @@ Author: Example Author
|
|
|
95
160
|
|
|
96
161
|
## Articles
|
|
97
162
|
|
|
98
|
-
> Posts only. Pages and collections are not included.
|
|
99
|
-
|
|
100
163
|
- [First article](https://example.com/articles/first.md) | Published at: 2026-01-01
|
|
164
|
+
|
|
165
|
+
## Pages
|
|
166
|
+
|
|
167
|
+
- [About](https://example.com/about.md): About page
|
|
168
|
+
|
|
169
|
+
## Guides
|
|
170
|
+
|
|
171
|
+
- [Getting started](https://example.com/guides/start.md)
|
|
172
|
+
|
|
173
|
+
## Optional
|
|
174
|
+
|
|
175
|
+
- [Reference](https://example.com/reference.md)
|
|
101
176
|
```
|
|
102
177
|
|
|
103
|
-
|
|
178
|
+
Use `llms_full_txt: true` for the full index.
|
|
179
|
+
It uses the same document selection as `llms.txt`.
|
|
180
|
+
It still works when `llms_txt: false`.
|
|
181
|
+
Each entry expands into a full Markdown document block.
|
|
182
|
+
Set `url` to a valid absolute HTTP(S) URL first.
|
|
183
|
+
The generator warns once when the rendered file exceeds 1 MiB.
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
# Example Site
|
|
187
|
+
|
|
188
|
+
## Articles
|
|
104
189
|
|
|
105
|
-
|
|
190
|
+
### [First article](https://example.com/articles/first.md)
|
|
106
191
|
|
|
107
|
-
|
|
192
|
+
Source: https://example.com/articles/first/
|
|
108
193
|
|
|
109
|
-
|
|
194
|
+
Body of the first article.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Add the alternate link from a layout.
|
|
198
|
+
|
|
199
|
+
```liquid
|
|
200
|
+
{% agent_markdown_link %}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
It renders a discovery link for the current page.
|
|
204
|
+
|
|
205
|
+
```html
|
|
206
|
+
<link rel="alternate" type="text/markdown" href="/about.md">
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The tag is baseurl-aware.
|
|
210
|
+
It is empty for opted-out or collided documents.
|
|
211
|
+
Use `page.agent_markdown_url` when custom markup needs the generated path.
|
|
212
|
+
|
|
213
|
+
On collision, existing destinations win.
|
|
214
|
+
Generated claims run through posts, then pages, then configured collections.
|
|
215
|
+
Later exports skip with a warning and receive no `agent_markdown_url`.
|
|
216
|
+
They are also omitted from both indexes.
|
|
217
|
+
A committed `llms.txt` or `llms-full.txt` also wins on collision.
|
|
218
|
+
Collision detection only sees files Jekyll knows about when this plugin runs at `priority :low`.
|
|
219
|
+
Another plugin generating files at `priority :lowest` runs later and can still claim the same destination.
|
|
220
|
+
|
|
221
|
+
## Options
|
|
222
|
+
|
|
223
|
+
Boolean settings accept `true`, `false`, and false-style strings.
|
|
224
|
+
Unknown keys and invalid values raise `Jekyll::Errors::FatalException`.
|
|
225
|
+
|
|
226
|
+
These are the complete defaults.
|
|
227
|
+
|
|
228
|
+
```yaml
|
|
229
|
+
agent_markdown:
|
|
230
|
+
posts: true
|
|
231
|
+
pages: false
|
|
232
|
+
collections: []
|
|
233
|
+
llms_txt: true
|
|
234
|
+
llms_full_txt: false
|
|
235
|
+
include_descriptions: false
|
|
236
|
+
include_document_header: false
|
|
237
|
+
include_author: true
|
|
238
|
+
include_dates: true
|
|
239
|
+
sort: desc
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Option | Default | Notes |
|
|
243
|
+
| --- | --- | --- |
|
|
244
|
+
| `posts` | `true` | Export posts. |
|
|
245
|
+
| `pages` | `false` | Export pages. |
|
|
246
|
+
| `collections` | `[]` | Export output collections named here. |
|
|
247
|
+
| `llms_txt` | `true` | Write `llms.txt`. |
|
|
248
|
+
| `llms_full_txt` | `false` | Write `llms-full.txt`. |
|
|
249
|
+
| `include_descriptions` | `false` | Append sanitized descriptions to `llms.txt`. |
|
|
250
|
+
| `include_document_header` | `false` | Prepend document headers to Markdown exports. |
|
|
251
|
+
| `include_author` | `true` | Include author metadata. |
|
|
252
|
+
| `include_dates` | `true` | Include published and updated dates. |
|
|
253
|
+
| `sort` | `desc` | Order each section by normalized publish date. |
|
|
254
|
+
|
|
255
|
+
The default `llms_txt` warns and skips when `url` is missing or invalid.
|
|
256
|
+
Explicitly configuring `llms_txt` makes an invalid `url` fatal.
|
|
257
|
+
`llms_full_txt` needs a valid absolute `url` whenever enabled.
|
|
258
|
+
Document headers enforce the same URL rules only when enabled.
|
|
259
|
+
|
|
260
|
+
## Deployment
|
|
261
|
+
|
|
262
|
+
The plugin writes static files; any host can serve them as-is.
|
|
263
|
+
Content negotiation is optional and happens at the host, not in Ruby.
|
|
264
|
+
A negotiating host serves the Markdown sibling when a request prefers `text/markdown`.
|
|
265
|
+
It falls back to HTML when the sibling does not exist.
|
|
266
|
+
|
|
267
|
+
[examples/](examples/) ships dependency-free recipes that share one negotiation contract:
|
|
268
|
+
|
|
269
|
+
- [Cloudflare Workers](examples/cloudflare)
|
|
270
|
+
- [Netlify Edge Functions](examples/netlify)
|
|
271
|
+
- [nginx with njs](examples/nginx)
|
|
272
|
+
|
|
273
|
+
All three negotiate only `GET` and `HEAD` requests and skip static assets.
|
|
274
|
+
See the [deployment guide](docs/deployment.md) for the full negotiation contract and host setup.
|
|
275
|
+
|
|
276
|
+
## Compatibility
|
|
277
|
+
|
|
278
|
+
Ruby 3.2 or newer is required.
|
|
279
|
+
Jekyll 4.3 or newer is required, but Jekyll 5 is unsupported.
|
|
280
|
+
The plugin writes files only.
|
|
281
|
+
There is no automatic injection into rendered HTML.
|
|
282
|
+
Layouts must invoke `{% agent_markdown_link %}` explicitly.
|
|
283
|
+
It does not handle content negotiation.
|
|
284
|
+
It does not generate crawler policy, analytics, middleware, response headers, or crawler permissions.
|
|
285
|
+
It publishes raw Markdown without rendering Liquid.
|
|
286
|
+
Post exports append enabled date and author metadata.
|
|
287
|
+
Document headers prepend content only when enabled.
|
|
288
|
+
Destination ownership is normalized across case, Unicode normalization, encoded aliases, and file-versus-directory conflicts.
|
|
289
|
+
|
|
290
|
+
### Limitations
|
|
291
|
+
|
|
292
|
+
Page and collection exports require authored Markdown and public URLs.
|
|
293
|
+
Generated pages and non-Markdown page or collection sources are ignored.
|
|
294
|
+
Raw Liquid may expose source directives to Markdown readers.
|
|
295
|
+
GitHub Pages safe mode may require an external build pipeline.
|
|
110
296
|
|
|
111
297
|
## Development
|
|
112
298
|
|
|
113
|
-
Run
|
|
299
|
+
Run the tests and RuboCop.
|
|
114
300
|
|
|
115
|
-
|
|
301
|
+
```sh
|
|
302
|
+
bundle install
|
|
303
|
+
bundle exec rake
|
|
304
|
+
```
|
|
116
305
|
|
|
117
306
|
## License
|
|
118
307
|
|
data/docs/deployment.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Deploy HTML and Markdown variants
|
|
2
|
+
|
|
3
|
+
`jekyll-agent-markdown` writes files; the web host decides how those files are represented over HTTP. This guide defines one negotiation contract and provides dependency-free starting points for Cloudflare Workers, Netlify Edge Functions, and nginx with njs.
|
|
4
|
+
|
|
5
|
+
The packaged examples assume Jekyll's pretty permalinks: an HTML URL such as `/articles/example/` is backed by `_site/articles/example/index.html`, while this plugin writes `_site/articles/example.md`. They also map `/` to `/index.md`, remove `.html` or `.htm` before adding `.md`, and map an extensionless `/example` to `/example.md`. Adapt the path helpers if the site uses a different permalink scheme. Build the site first and point each example's `_site` setting at that output.
|
|
6
|
+
|
|
7
|
+
## Negotiation contract
|
|
8
|
+
|
|
9
|
+
Apply negotiation to page URLs, not to CSS, JavaScript, images, fonts, or other static assets.
|
|
10
|
+
|
|
11
|
+
| Request | Result |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Missing `Accept` or wildcard-only `Accept: */*` | HTML |
|
|
14
|
+
| A more specific media range | Its `q` value overrides a less-specific wildcard for that representation |
|
|
15
|
+
| Different effective HTML and Markdown quality | The supported representation with the larger quality wins |
|
|
16
|
+
| At equal quality | Markdown wins only when the request explicitly names `text/markdown`; otherwise HTML wins |
|
|
17
|
+
| A representation with effective `q=0` | Excluded, even if a less-specific wildcard has a positive quality |
|
|
18
|
+
| Neither `text/html` nor `text/markdown` acceptable | `406 Not Acceptable` |
|
|
19
|
+
| A negotiated Markdown variant that does not exist | HTML when HTML is acceptable; otherwise the Markdown `404` |
|
|
20
|
+
| A URL ending in `.md` | Markdown regardless of `Accept`; this URL is not negotiated |
|
|
21
|
+
| A method other than `GET` or `HEAD` | Not negotiated |
|
|
22
|
+
|
|
23
|
+
The examples calculate an effective quality independently for `text/html` and `text/markdown`. Media-range specificity is applied before explicit `q` values are compared: an exact type is more specific than `text/*`, which is more specific than `*/*`. Supported representation parameters, currently `charset=utf-8`, add specificity. Invalid quality values make that range unacceptable instead of silently broadening access.
|
|
24
|
+
|
|
25
|
+
This follows HTTP's [`Accept` precedence and quality rules](https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1). Returning 406 is an intentional server policy when none of the available representations is acceptable; HTTP also permits a server to disregard the preference, but these examples do not. See [406 Not Acceptable](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.7).
|
|
26
|
+
|
|
27
|
+
### Representation availability
|
|
28
|
+
|
|
29
|
+
Negotiation selects among representations that actually exist. This plugin exports posts by default, so on a site that has not enabled `pages` or `collections` most page URLs have no `.md` sibling. A request preferring Markdown for such a URL must not become a 404 when the host could have served HTML.
|
|
30
|
+
|
|
31
|
+
Each example therefore fetches the selected variant and, when a negotiated Markdown variant returns 404 and HTML is acceptable, retries the HTML variant and labels the response as HTML. Two cases deliberately do not fall back: an explicit `.md` URL, because that URL names one representation rather than negotiating, and a request that excluded HTML with `q=0`, because no acceptable representation exists.
|
|
32
|
+
|
|
33
|
+
### Request methods
|
|
34
|
+
|
|
35
|
+
Negotiation applies to `GET` and `HEAD`. The nginx example rejects other methods with `405 Method Not Allowed` and an `Allow` header, because an njs subrequest is always a `GET` and would otherwise turn a `POST` into a static read of the page body. The Cloudflare and Netlify examples hand non-page and non-matching requests to the platform's own asset handler, which applies its method rules.
|
|
36
|
+
|
|
37
|
+
## Response contract
|
|
38
|
+
|
|
39
|
+
Every successful variant has an explicit UTF-8 content type:
|
|
40
|
+
|
|
41
|
+
```http
|
|
42
|
+
Content-Type: text/html; charset=utf-8
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
or:
|
|
46
|
+
|
|
47
|
+
```http
|
|
48
|
+
Content-Type: text/markdown; charset=utf-8
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Every response selected from `Accept`, including a 406 response, carries:
|
|
52
|
+
|
|
53
|
+
```http
|
|
54
|
+
Vary: Accept
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`Vary` prevents a shared cache from reusing an HTML response for a Markdown request or the reverse. Preserve existing `Vary` fields when adding `Accept`; see the HTTP [`Vary` definition](https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.5).
|
|
58
|
+
|
|
59
|
+
HTML and Markdown advertise each other with a [`Link` response header](https://www.rfc-editor.org/rfc/rfc8288.html). For example:
|
|
60
|
+
|
|
61
|
+
```http
|
|
62
|
+
Link: </articles/example.md>; rel="alternate"; type="text/markdown"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
and the Markdown response advertises HTML:
|
|
66
|
+
|
|
67
|
+
```http
|
|
68
|
+
Link: </articles/example/>; rel="alternate"; type="text/html"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The explicit `.md` URL always returns `text/markdown; charset=utf-8`, even if a caller sends `Accept: text/html`. The `text/markdown` media type is registered by [RFC 7763](https://www.rfc-editor.org/rfc/rfc7763.html).
|
|
72
|
+
|
|
73
|
+
## Cloudflare Workers
|
|
74
|
+
|
|
75
|
+
Copy [`examples/cloudflare`](../examples/cloudflare) into the deployment project, or adapt its paths. `wrangler.toml` binds the Jekyll output as `ASSETS` and sets `run_worker_first = true`; without that setting, a matching static asset can bypass negotiation. The Worker passes non-page assets straight through and uses `env.ASSETS.fetch()` for selected variants.
|
|
76
|
+
|
|
77
|
+
The relevant primary documentation is Cloudflare's [static assets binding and `run_worker_first` reference](https://developers.cloudflare.com/workers/static-assets/binding/). Set `compatibility_date` to the deployment project's chosen current date and confirm the `_site` path before deploying.
|
|
78
|
+
|
|
79
|
+
## Netlify Edge Functions
|
|
80
|
+
|
|
81
|
+
Copy [`examples/netlify/netlify.toml`](../examples/netlify/netlify.toml) and [`markdown-negotiation.ts`](../examples/netlify/netlify/edge-functions/markdown-negotiation.ts) to the corresponding locations in the site repository. The catch-all declaration is minimal and easy to test; narrow the declared paths or add exclusions on a large site to avoid invoking the function for assets that it only passes through.
|
|
82
|
+
|
|
83
|
+
The Edge Function calls `context.next()` for the requested HTML or explicit Markdown asset. When a page URL negotiates Markdown, it fetches the `.md` sibling; that same-site request runs the Edge Function again but terminates on the explicit `.md` branch. Review Netlify's [Edge Functions API](https://docs.netlify.com/build/edge-functions/api/) and [declaration and request-chain rules](https://docs.netlify.com/build/edge-functions/declarations/) before combining this example with other rewrites or Edge Functions.
|
|
84
|
+
|
|
85
|
+
## nginx with njs
|
|
86
|
+
|
|
87
|
+
Install nginx's JavaScript module, place [`negotiation.js`](../examples/nginx/negotiation.js) at `/etc/nginx/njs/negotiation.js`, adapt the `_site` filesystem path in [`nginx.conf`](../examples/nginx/nginx.conf), then validate and reload nginx. Current njs recommends the QuickJS engine; the example selects it with `js_engine qjs`. The example also sets `include mime.types` and a binary `default_type`, without which its pass-through location would serve CSS, JavaScript, fonts, and images as `text/plain`.
|
|
88
|
+
|
|
89
|
+
The public location delegates page requests to `js_content`. The handler performs an internal subrequest to a private static location, sets the variant headers, and returns the buffer. `subrequest_output_buffer_size` must exceed the largest generated page, so change the example's `10m` ceiling to fit the site's real output and memory budget. Consult the official [`ngx_http_js_module` directives](https://nginx.org/en/docs/http/ngx_http_js_module.html) and [njs request/subrequest API](https://nginx.org/en/docs/njs/reference.html).
|
|
90
|
+
|
|
91
|
+
## Pre-release verification
|
|
92
|
+
|
|
93
|
+
Replace the sample origin and article paths, deploy to a staging hostname, and run this curl matrix. Use GET with discarded bodies so the check exercises the same response path as production clients.
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
origin=https://staging.example.com
|
|
97
|
+
article=/articles/example/
|
|
98
|
+
markdown=/articles/example.md
|
|
99
|
+
page=/about/
|
|
100
|
+
|
|
101
|
+
curl -sS -D - -o /dev/null -H 'Accept:' "$origin$article"
|
|
102
|
+
curl -sS -D - -o /dev/null -H 'Accept: */*' "$origin$article"
|
|
103
|
+
curl -sS -D - -o /dev/null -H 'Accept: text/markdown' "$origin$article"
|
|
104
|
+
curl -sS -D - -o /dev/null -H 'Accept: text/html, text/markdown' "$origin$article"
|
|
105
|
+
curl -sS -D - -o /dev/null -H 'Accept: text/*;q=0.8, text/html;q=0.4' "$origin$article"
|
|
106
|
+
curl -sS -D - -o /dev/null -H 'Accept: text/markdown;q=0, */*;q=0.8' "$origin$article"
|
|
107
|
+
curl -sS -D - -o /dev/null -H 'Accept: application/json' "$origin$article"
|
|
108
|
+
curl -sS -D - -o /dev/null -H 'Accept: text/markdown;q="1"' "$origin$article"
|
|
109
|
+
curl -sS -D - -o /dev/null -H 'Accept: text/html' "$origin$markdown"
|
|
110
|
+
curl -sS -D - -o /dev/null -H 'Accept: text/markdown' "$origin$page"
|
|
111
|
+
curl -sS -D - -o /dev/null -H 'Accept: text/markdown, text/html;q=0' "$origin$page"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Expected results, in order:
|
|
115
|
+
|
|
116
|
+
| Case | Status | Content-Type |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| Missing `Accept` | 200 | `text/html; charset=utf-8` |
|
|
119
|
+
| Wildcard-only | 200 | `text/html; charset=utf-8` |
|
|
120
|
+
| Explicit Markdown | 200 | `text/markdown; charset=utf-8` |
|
|
121
|
+
| Equal explicit HTML and Markdown | 200 | `text/markdown; charset=utf-8` |
|
|
122
|
+
| More-specific HTML is lower quality than `text/*` Markdown | 200 | `text/markdown; charset=utf-8` |
|
|
123
|
+
| Exact Markdown exclusion overrides wildcard | 200 | `text/html; charset=utf-8` |
|
|
124
|
+
| Neither supported | 406 | `text/plain; charset=utf-8` |
|
|
125
|
+
| Invalid quoted quality | 406 | `text/plain; charset=utf-8` |
|
|
126
|
+
| Explicit `.md` despite HTML preference | 200 | `text/markdown; charset=utf-8` |
|
|
127
|
+
| Page with no Markdown sibling | 200 | `text/html; charset=utf-8` |
|
|
128
|
+
| Same page with HTML excluded | 404 | the host's own error representation |
|
|
129
|
+
|
|
130
|
+
For every negotiated 200 response, also verify `Vary: Accept` and the appropriate alternate `Link`. Check the explicit Markdown URL's HTML `Link`. Finally, submit representative public staging URLs to the [acceptmarkdown.com public scanner](https://acceptmarkdown.com/public) and retain its results with the release evidence. The scanner is a useful external pre-release check, not a substitute for the matrix or host logs.
|
|
131
|
+
|
|
132
|
+
## Crawler policy is separate
|
|
133
|
+
|
|
134
|
+
`Content-Signal` is an evolving crawler-policy convention, not a transport requirement for HTML/Markdown negotiation. Whether a site permits search, AI input, or model training is a decision for the site owner, informed by the site's content rights and legal advice. The examples deliberately do not generate crawler permissions. Review the current [Content Signals project](https://contentsignals.org/) and the behavior of the crawlers that matter to the site, then write and audit an owner-approved policy; do not copy a generated crawler policy without that review.
|
|
135
|
+
|
|
136
|
+
## Runtime gaps to validate
|
|
137
|
+
|
|
138
|
+
These packaged examples statically lock the shared HTTP behavior, but they are not deployed by this gem. Before production, validate host-specific routing, redirects, custom error pages, caching, compression, conditional requests, range requests, base paths, non-pretty permalinks, large response bodies, and interaction with any existing middleware. Confirm that a missing Markdown sibling produces the site's intended error rather than mislabeled content, and monitor 406 rates after launch for clients with unexpected `Accept` headers.
|