Last active
November 20, 2025 08:52
Convert a GitHub Wiki to static HTML files
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
| #!/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 |
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
| <!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