Created
December 31, 2025 22:54
-
-
Save patio11/d43f1d499b3ca2fd3a922e787ead5408 to your computer and use it in GitHub Desktop.
Jekyll plugin to enable publishing Markdown for LLM accessibility.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Jekyll plugin to generate .md versions of posts for LLM consumption | |
| # | |
| # For any post not opting out via `no_markdown_export: true` in frontmatter, | |
| # generates a .md file at the same URL path with .md extension. | |
| # | |
| # Only includes allowlisted frontmatter fields to avoid exposing sensitive | |
| # internal fields like noindex, seo settings, etc. | |
| # | |
| # (Claude Code wrote most of this, FWIW.) I release this work into the public domain. -- patio11 | |
| module Jekyll | |
| class MarkdownExportGenerator < Generator | |
| safe true | |
| priority :low | |
| ALLOWED_FRONTMATTER = %w[ | |
| title | |
| date | |
| author | |
| permalink | |
| categories | |
| description | |
| blurb | |
| ].freeze | |
| # These fields are only included if explicitly set in source frontmatter | |
| EXPLICIT_ONLY_FRONTMATTER = %w[ | |
| excerpt | |
| ].freeze | |
| def generate(site) | |
| site.posts.docs.each do |post| | |
| next if post.data['no_markdown_export'] | |
| # Determine output path from post URL | |
| post_url = post.url.to_s.chomp('/') | |
| dir = File.dirname(post_url).sub(/^\//, '') # Remove leading slash | |
| filename = File.basename(post_url, '.*') + '.md' | |
| # Build the markdown content | |
| content = build_export_content(post) | |
| # Write to a temp location and add as static file | |
| export_path = File.join(site.source, '.markdown_exports', dir) | |
| FileUtils.mkdir_p(export_path) | |
| file_path = File.join(export_path, filename) | |
| File.write(file_path, content) | |
| site.static_files << MarkdownExportFile.new(site, export_path, '', filename, dir) | |
| end | |
| end | |
| def build_export_content(post) | |
| # Build filtered frontmatter | |
| filtered = {} | |
| ALLOWED_FRONTMATTER.each do |key| | |
| if post.data.key?(key) && post.data[key] && post.data[key].to_s.strip != '' | |
| filtered[key] = post.data[key] | |
| end | |
| end | |
| # Include explicit-only fields only if they appear in source frontmatter | |
| source_frontmatter = extract_source_frontmatter(post.path) | |
| EXPLICIT_ONLY_FRONTMATTER.each do |key| | |
| if source_frontmatter.include?(key) | |
| filtered[key] = post.data[key] if post.data[key] | |
| end | |
| end | |
| # Always use canonical author name | |
| # Commented this out for the gist to save copy/pasters some pain | |
| # filtered['author'] = 'Patrick McKenzie (patio11)' | |
| # Get raw markdown content from source file | |
| raw_content = extract_raw_content(post.path) | |
| # Build output | |
| output = "---\n" | |
| filtered.each do |key, value| | |
| output += yaml_line(key, value) | |
| end | |
| output += "---\n\n" | |
| output += raw_content | |
| output | |
| end | |
| def extract_raw_content(path) | |
| return '' unless File.exist?(path) | |
| content = File.read(path) | |
| # Remove YAML frontmatter (everything between --- and ---) | |
| if content =~ /\A---\s*\n(.*?\n?)^---\s*\n/m | |
| content = $' # Everything after the frontmatter | |
| end | |
| content | |
| end | |
| def extract_source_frontmatter(path) | |
| return [] unless File.exist?(path) | |
| content = File.read(path) | |
| if content =~ /\A---\s*\n(.*?\n?)^---\s*\n/m | |
| frontmatter_text = $1 | |
| # Extract keys from YAML (simple regex approach) | |
| frontmatter_text.scan(/^(\w+):/).flatten | |
| else | |
| [] | |
| end | |
| end | |
| def yaml_line(key, value) | |
| case value | |
| when Array | |
| if value.empty? | |
| "#{key}: []\n" | |
| else | |
| "#{key}:\n" + value.map { |v| " - #{yaml_escape(v)}" }.join("\n") + "\n" | |
| end | |
| when Time, DateTime | |
| "#{key}: #{value.iso8601}\n" | |
| when Date | |
| "#{key}: #{value.to_s}\n" | |
| else | |
| "#{key}: #{yaml_escape(value.to_s)}\n" | |
| end | |
| end | |
| def yaml_escape(str) | |
| # Quote strings that contain special characters | |
| if str =~ /[:\#\[\]\{\}\,\&\*\?\|\-\<\>\=\!\%\@\`]/ || str =~ /\A[\s]/ || str =~ /[\s]\z/ || str.include?("\n") | |
| "\"#{str.gsub('"', '\\"').gsub("\n", "\\n")}\"" | |
| else | |
| str | |
| end | |
| end | |
| end | |
| # Custom StaticFile that outputs to a different path than the source | |
| class MarkdownExportFile < StaticFile | |
| def initialize(site, base, dir, name, dest_dir) | |
| super(site, base, dir, name) | |
| @dest_dir = dest_dir | |
| end | |
| def destination(dest) | |
| File.join(dest, @dest_dir, @name) | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment