Skip to content

Instantly share code, notes, and snippets.

@patio11
Created December 31, 2025 22:54
Show Gist options
  • Select an option

  • Save patio11/d43f1d499b3ca2fd3a922e787ead5408 to your computer and use it in GitHub Desktop.

Select an option

Save patio11/d43f1d499b3ca2fd3a922e787ead5408 to your computer and use it in GitHub Desktop.
Jekyll plugin to enable publishing Markdown for LLM accessibility.
# 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