Skip to content

Instantly share code, notes, and snippets.

@yuuki7
Last active November 24, 2025 12:42
  • Select an option

Select an option

Convert a GitHub Wiki to static HTML files
##
# Constants for the main script
#
##
# Constants for the GitHub Wiki
#
# Path to the locally cloned wiki repository
WIKI_REPO = Pathname('../wikinder.wiki')
# URL of the wiki
WIKI_URL = URI('https://github.com/wikinder/wikinder/wiki/')
##
# Constants for the converted site
#
# Path to the output directory
OUTPUT_DIRECTORY = Pathname('./out')
# Path to the HTML template file
HTML_TEMPLATE_FILE = Pathname('./template.html.liquid')
# Title of the site
SITE_TITLE = 'Wikinder'
# URL of the site
SITE_URL = URI('https://wikinder.org/')
# Base path for internal links
BASE_PATH = Pathname(SITE_URL.path)
# Path to the stylesheet file
STYLESHEET_FILE = Pathname('/assets/style.css')
# URL of the OG image
OG_IMAGE_URL = URI.join(SITE_URL, '/og-image.jpg')
##
# Configuration for Gollum
#
# Options for the Gollum::Wiki constructor
GOLLUM_OPTIONS = {
# Base path for internal links
base_path: BASE_PATH.to_s,
# Convert spaces to hyphens in internal links
hyphened_tag_lookup: true,
# Do not add class="editable" to section headings
allow_editing: false,
# Keep Gollum's filter chain minimal
# :Tags - Convert internal links to standard Markdown links
# :Render - Render Markdown to HTML
filter_chain: [:Tags, :Render],
}
# Use Commonmarker as the Markdown renderer
GitHub::Markup::Markdown::MARKDOWN_GEMS.clear
GitHub::Markup::Markdown::MARKDOWN_GEMS['commonmarker'] = proc do |markdown|
::Commonmarker.to_html(markdown, options: {
render: {
# Allow raw HTML tags to support <details> tags etc.
unsafe: true,
},
extension: {
# Remove blacklisted HTML tags (GFM)
tagfilter: true,
# Enable footnote syntax (GFM)
footnotes: true,
},
})
end
require 'pathname'
require 'uri'
require 'commonmarker'
require 'gollum-lib'
require 'liquid'
require 'nokogiri'
# Load constants
require_relative './config'
# Load methods
require_relative './utils'
# Load the HTML template
html_template = Liquid::Template.parse(HTML_TEMPLATE_FILE.read)
# Create a wiki object
wiki = Gollum::Wiki.new(WIKI_REPO.to_s, GOLLUM_OPTIONS)
# Converted HTML string of the wiki footer
page_footer = wiki.pages[0].footer.formatted_data
# Pages to display on the home page
all_pages = []
# Generate the individual article pages
wiki.pages.each do |wiki_entry|
# Skip non-Markdown entries
next unless wiki_entry.format == :markdown
wiki_entry_slug = wiki_entry.filename_stripped
# Skip home and special entries
next if wiki_entry_slug =~ /^(?:Home|LICENSE|README)$/
escaped_wiki_entry_slug = URI.encode_uri_component(wiki_entry_slug)
wiki_entry_url = URI.join(WIKI_URL, escaped_wiki_entry_slug)
page_slug = wiki_entry_slug.tr('?', '')
escaped_page_slug = URI.encode_uri_component(page_slug)
page_url = URI.join(SITE_URL, escaped_page_slug)
article_title = wiki_entry_slug.tr('-', ' ')
# Converted HTML string of the article body
article_body = wiki_entry.formatted_data
# Tweak HTML
article_body = postprocess_html(article_body)
# Render full HTML
full_html = html_template.render(
'site_title' => SITE_TITLE,
'base_path' => BASE_PATH.to_s,
'stylesheet_file' => STYLESHEET_FILE.to_s,
'og_image_url' => OG_IMAGE_URL.to_s,
'page_url' => page_url.to_s,
'article_title' => article_title,
'article_body' => article_body,
'page_footer' => page_footer,
'wiki_entry_url' => wiki_entry_url.to_s,
'is_home' => false,
)
# Write HTML to a file
output_filename = "#{page_slug}.html"
output_file = OUTPUT_DIRECTORY.join(output_filename)
output_file.write(full_html)
# Skip project-related entries
next if wiki_entry_slug =~ /^Wikinder/
all_pages.push({
url: escaped_page_slug,
title: article_title,
date: wiki_entry.last_version.authored_date,
})
end
# Render the home page
all_page_links = all_pages
.sort { |a, b| b[:date] <=> a[:date] }
.map { |page| '<a href="%{url}">%{title}</a>' % page }
.join(' · ')
home_page_body = wiki.page('Home').formatted_data
home_page_body = postprocess_html(home_page_body)
full_html = html_template.render(
'site_title' => SITE_TITLE,
'base_path' => BASE_PATH.to_s,
'stylesheet_file' => STYLESHEET_FILE.to_s,
'og_image_url' => OG_IMAGE_URL.to_s,
'page_url' => SITE_URL.to_s,
'all_page_links' => all_page_links,
'article_title' => SITE_TITLE,
'article_body' => home_page_body,
'page_footer' => page_footer,
'wiki_entry_url' => WIKI_URL.to_s.delete_suffix('/'),
'is_home' => true,
)
home_page_file = OUTPUT_DIRECTORY.join('index.html')
home_page_file.write(full_html)
<!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">
{% if is_home %}
<meta property="og:title" content="{{ site_title }}">
<meta property="og:type" content="website">
{% else %}
<meta property="og:title" content="{{ article_title }}">
<meta property="og:type" content="article">
{% endif %}
<meta property="og:image" content="{{ og_image_url }}">
<meta property="og:url" content="{{ page_url }}">
<meta property="og:site_name" content="{{ site_title }}">
<meta name="twitter:card" content="summary">
<title>
{% if is_home %}
{{ site_title }}
{% else %}
{{ article_title }} - {{ site_title }}
{% endif %}
</title>
<link rel="canonical" href="{{ page_url }}">
<link rel="license" href="https://creativecommons.org/licenses/by-sa/4.0/">
<link rel="stylesheet" href="{{ stylesheet_file }}">
<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>
</head>
<body>
<!-- Page header -->
<header>
<nav>
<p>
{% if is_home %}
{{ all_page_links }}
{% else %}
<a href="{{ base_path }}">{{ site_title }}</a>
{% endif %}
</p>
</nav>
</header>
<!-- Page body -->
<main>
<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_entry_url }}">Edit this page</a>
</p>
</footer>
</body>
</html>
##
# Methods for the main script
#
# 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'])
path = Pathname(uri.path)
# Strip the extension
path = path.sub_ext('')
# Make the path relative
path = path.relative_path_from(BASE_PATH)
uri.path = path.to_s.gsub('%3F', '')
a['href'] = uri.to_s
end
dom.to_html
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment