Skip to content

Instantly share code, notes, and snippets.

@yuuki7
Last active November 20, 2025 08:52
  • Select an option

Select an option

Convert a GitHub Wiki to static HTML files
#!/usr/bin/env ruby
require 'pathname'
require 'uri'
require 'commonmarker'
require 'gollum-lib'
require 'liquid'
require 'nokogiri'
# Home page URL of the source GitHub Wiki
WIKI_URL = URI('https://github.com/wikinder/wikinder/wiki')
# Title of the output site
SITE_TITLE = 'Wikinder'
# Home page URL of the output site
SITE_URL = URI('https://wikinder.org/')
# Root-relative path to the home page of the output site
SITE_HOME_PATH = Pathname(SITE_URL.path)
OG_IMAGE_URL = URI.join(SITE_URL, '/og-image.jpg')
OG_IMAGE_ALT = 'bear'
# Path to the directory of the locally cloned GitHub Wiki repository
WIKI_REPO_PATH = Pathname('./wikinder.wiki')
# Path to the output directory
OUTPUT_DIRECTORY_PATH = Pathname('./wikinder.github.io')
# Path to the HTML template
HTML_TEMPLATE_FILE_PATH = Pathname('./template.html.liquid')
# Configure Gollum to use Commonmarker as the Markdown renderer.
# https://github.com/gollum/gollum/wiki/Custom-rendering-gems
module Gollum
class Markup
GitHub::Markup::Markdown::MARKDOWN_GEMS.clear
GitHub::Markup::Markdown::MARKDOWN_GEMS['commonmarker'] = proc do |markdown|
# FIXME: Configure Commonmarker options for GitHub Wiki-compatible rendering.
Commonmarker.to_html(markdown, options: {
render: {
unsafe: true,
},
extension: {
strikethrough: true,
tagfilter: true,
table: true,
autolink: true,
tasklist: true,
footnotes: true,
},
})
end
end
end
# Tweak HTML converted from Markdown.
def postprocess_html(html)
dom = Nokogiri::HTML5.fragment(html)
# Handle internal links.
dom.css('a.internal').each do |a|
uri = URI(a['href'].gsub('%3F', ''))
path = Pathname(uri.path)
# Strip the extension.
if path.extname == '.md'
path = path.sub_ext('')
end
# Make the path relative.
path = path.relative_path_from(SITE_HOME_PATH)
uri.path = path.to_s
a['href'] = uri.to_s
# Remove rel="nofollow".
a.remove_attribute('rel')
end
# Handle anchors.
dom.css('a.anchor').each do |a|
a.remove_attribute('rel')
end
# Remove class="editable".
dom.css('.editable').each do |element|
element.remove_class('editable')
end
dom.to_html
end
# Load the HTML template.
html_template = Liquid::Template.parse(File.read(HTML_TEMPLATE_FILE_PATH))
# Create a wiki object.
# FIXME: Configure Gollum options for GitHub Wiki-compatible rendering.
wiki = Gollum::Wiki.new(WIKI_REPO_PATH.to_s, {
base_path: SITE_HOME_PATH.to_s,
hyphened_tag_lookup: true,
emoji: true,
# FIXME: Verify that this filter chain is necessary and sufficient.
filter_chain: [:Sanitize, :Code, :Emoji, :Tags, :Render],
})
# FIXME
all_page_links = wiki.pages
.filter { |wiki_page| wiki_page.format == :markdown }
.map { |wiki_page|
{
slug: URI.encode_uri_component(wiki_page.filename_stripped.tr('?', '')),
title: wiki_page.title.tr('-', ' '),
}
}
.reject { |link| link[:title] =~ /^(?:Home|LICENSE|README)$/ }
.sort_by { |link| link[:title].downcase }
.map { |link| '<a href="%{slug}">%{title}</a>' % link }
# Iterate through all wiki pages.
wiki.pages.each do |wiki_page|
# Skip if the page is not in Markdown.
# TODO: Support page formats other than Markdown.
next unless wiki_page.format == :markdown
# Wiki page slug
wiki_page_slug = wiki_page.filename_stripped
next if wiki_page_slug =~ /^(?:LICENSE|README)$/
escaped_wiki_page_slug = URI.encode_uri_component(wiki_page_slug)
# Site page slug
site_page_slug = wiki_page_slug.tr('?', '')
escaped_site_page_slug = URI.encode_uri_component(site_page_slug)
# Flag indicating whether the page is the home page
is_home = wiki_page_slug == 'Home'
if is_home
canonical_url = SITE_URL
article_title = SITE_TITLE
wiki_page_url = WIKI_URL
output_filename = 'index.html'
else
canonical_url = URI.join(SITE_URL, escaped_site_page_slug)
article_title = wiki_page.title.tr('-', ' ')
# FIXME
wiki_page_url = URI.join(WIKI_URL, 'wiki/', escaped_wiki_page_slug)
output_filename = "#{site_page_slug}.html"
end
# HTML fragment string converted from Markdown
article_body_html = wiki_page.formatted_data
# Tweak HTML.
article_body_html = postprocess_html(article_body_html)
# Render the HTML template to get the full HTML string.
full_html = html_template.render(
'site_title' => SITE_TITLE,
'site_url' => SITE_URL.to_s,
'og_image_url' => OG_IMAGE_URL.to_s,
'og_image_alt' => OG_IMAGE_ALT,
'canonical_url' => canonical_url.to_s,
'home_path' => SITE_HOME_PATH.to_s,
'article_title' => article_title,
'article_body' => article_body_html,
'page_footer' => wiki_page.footer.formatted_data,
'wiki_page_url' => wiki_page_url.to_s,
'is_home' => is_home,
'all_page_links' => all_page_links,
)
# Write HTML to a file.
output_file_path = OUTPUT_DIRECTORY_PATH.join(output_filename)
File.write(output_file_path, full_html)
end
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="light dark">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="{% if is_home %}{{ site_title }}{% else %}{{ article_title }}{% endif %}">
<meta property="og:url" content="{{ canonical_url }}">
<meta property="og:site_name" content="{{ site_title }}">
<meta property="og:image" content="{{ og_image_url }}">
<meta property="og:type" content="article">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="{{ og_image_url }}">
<meta name="twitter:image:alt" content="{{ og_image_alt }}">
<title>
{% if is_home %}
{{ site_title }}
{% else %}
{{ article_title }} - {{ site_title }}
{% endif %}
</title>
<link rel="canonical" href="{{ canonical_url }}">
<link rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">
<script>
window.MathJax = {
tex: {
inlineMath: {'[+]': [['$', '$']]},
packages: {'[+]': ['ams']},
},
loader: {
dependencies: {
'[mathjax-euler-extension]/chtml': ['output/chtml'],
},
paths: {
font: 'https://cdn.jsdelivr.net/npm/@mathjax',
'mathjax-euler-extension': '[font]/mathjax-euler-font-extension',
},
load: [
'input/tex-base',
'[tex]/ams',
'output/chtml',
'[mathjax-euler-extension]/chtml',
],
},
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@4/startup.js"></script>
<style>
body {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
}
pre {
white-space: pre-wrap;
}
</style>
</head>
<body>
<!-- Page header -->
<header>
<nav>
{% if is_home %}
{{ all_page_links | join: ' · ' }}
{% else %}
<a href="{{ home_path }}">{{ site_title }}</a>
{% endif %}
</nav>
</header>
<!-- Page body -->
<main>
<!-- Article -->
<article>
<!-- Article header -->
<header>
<h1>{{ article_title }}</h1>
</header>
<!-- Article body -->
<section>{{ article_body }}</section>
</article>
</main>
<!-- Page footer -->
<footer>
{{ page_footer }}
<p>
<a href="{{ wiki_page_url }}">Edit this page</a>
</p>
</footer>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment