Last active
November 24, 2025 12:42
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
| ## | |
| # 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 |
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
| 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) |
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"> | |
| {% 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> |
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
| ## | |
| # 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