Skip to content

Instantly share code, notes, and snippets.

@yuuki7
Last active November 28, 2025 04:19
  • Select an option

Select an option

Revisions

  1. yuuki7 revised this gist Nov 28, 2025. 7 changed files with 284 additions and 163 deletions.
    12 changes: 12 additions & 0 deletions .jsbeautifyrc
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,12 @@
    {
    "indent_size": 2,
    "end_with_newline": true,
    "html": {
    "indent_inner_html": true,
    "preserve_newlines": false,
    "extra_liners": [],
    "js": {
    "end_with_newline": false
    }
    }
    }
    53 changes: 10 additions & 43 deletions config.rb
    Original file line number Diff line number Diff line change
    @@ -13,7 +13,7 @@
    WIKI_URL = URI('https://github.com/wikinder/wikinder/wiki/')

    ##
    # Constants for the converted site
    # Constants for the generated site
    #

    # Path to the output directory
    @@ -23,55 +23,22 @@
    HTML_TEMPLATE_FILE = Pathname('./template.html.liquid')

    # Title of the site
    SITE_TITLE = 'Wikinder'
    SITE_NAME = '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
    #
    # URL of the site logo
    LOGO_URL = URI.join(SITE_URL, '/assets/images/icon.jpg')

    # 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],
    }
    # Path to the stylesheet file
    STYLESHEET_FILE = Pathname('/assets/css/style.css')

    # 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,
    # Path to the MathJax configuration script
    MATHJAX_CONFIG_SCRIPT = Pathname('/assets/js/mathjax-config.js')

    # Enable footnote syntax (GFM)
    footnotes: true,
    },
    })
    end
    # Article date format for display
    DATE_FORMAT = '%B %-d, %-Y'
    178 changes: 102 additions & 76 deletions github-wiki-to-html.rb
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    require 'cgi'
    require 'pathname'
    require 'uri'

    @@ -9,105 +10,130 @@
    # Load constants
    require_relative './config'

    # Load Gollum configuration
    require_relative './gollum-config'

    # Load methods
    require_relative './utils'

    # Load the HTML template
    html_template = Liquid::Template.parse(HTML_TEMPLATE_FILE.read)
    html_template = Liquid::Template.parse(HTML_TEMPLATE_FILE.read, error_mode: :strict)

    # Create a wiki object
    # Load the wiki
    wiki = Gollum::Wiki.new(WIKI_REPO.to_s, GOLLUM_OPTIONS)
    home_page = wiki.page('Home')
    page_footer_html = home_page.footer.formatted_data

    # Converted HTML string of the wiki footer
    page_footer = wiki.pages[0].footer.formatted_data

    # Pages to display on the home page
    # Pages to list on the home page and sitemap
    all_pages = []

    # Generate the individual article pages
    wiki.pages.each do |wiki_entry|
    # Skip non-Markdown entries
    next unless wiki_entry.format == :markdown
    # Generate individual article pages and add them to the list
    wiki.pages.each do |page|
    # Skip non-Markdown pages
    next unless page.format == :markdown

    wiki_entry_slug = wiki_entry.filename_stripped
    slug = page.filename_stripped

    # Skip home and special entries
    next if wiki_entry_slug =~ /^(?:Home|LICENSE|README)$/
    # Skip Home and special pages
    next if 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)
    article_title = slug.tr('-', ' ')
    encoded_slug = URI.encode_uri_component(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('-', ' ')
    # URL of the page on the generated site
    canonical_url = URI.join(SITE_URL, encoded_slug)

    # Converted HTML string of the article body
    article_body = wiki_entry.formatted_data
    # URL of the page on the wiki
    wiki_page_url = URI.join(WIKI_URL, encoded_slug)

    # Tweak HTML
    article_body = postprocess_html(article_body)
    # Get the first commit of the page (following renames)
    first_commit = page.versions({
    follow: true,
    per_page: 10000,
    }).last

    # 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,
    last_commit = page.last_version
    is_modified = last_commit.id != first_commit.id

    'page_url' => page_url.to_s,
    # Published date in UTC
    published_date = first_commit.authored_date.getutc
    published_date_iso = published_date.iso8601
    published_date_display = published_date.strftime(DATE_FORMAT)

    'article_title' => article_title,
    'article_body' => article_body,
    # Last modified date in UTC
    modified_date = last_commit.authored_date.getutc
    modified_date_iso = modified_date.iso8601
    modified_date_display = modified_date.strftime(DATE_FORMAT)

    'page_footer' => page_footer,
    'wiki_entry_url' => wiki_entry_url.to_s,
    author_name = first_commit.author.name

    # Generate the HTML file
    generate_html_file("#{slug}.html", page.formatted_data, html_template, {
    '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/
    'canonical_url' => canonical_url.to_s,
    'wiki_page_url' => wiki_page_url.to_s,
    'article_title' => article_title,
    'page_footer' => page_footer_html,

    'is_modified' => is_modified,
    'published_date_display' => published_date_display,
    'published_date_iso' => published_date_iso,
    'modified_date_display' => modified_date_display,
    'modified_date_iso' => modified_date_iso,
    'author_name' => author_name,
    })

    all_pages.push({
    url: escaped_page_slug,
    # Add the page to the list
    all_pages << {
    encoded_slug: encoded_slug,
    canonical_url: canonical_url,
    title: article_title,
    date: wiki_entry.last_version.authored_date,
    })
    escaped_title: CGI.escapeHTML(article_title),
    published_date: published_date,
    modified_date: modified_date,
    modified_date_iso: modified_date_iso,
    }
    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('/'),

    # Generate the home page
    generate_html_file('index.html', home_page.formatted_data, html_template, {
    'is_home' => true,
    )

    home_page_file = OUTPUT_DIRECTORY.join('index.html')
    home_page_file.write(full_html)
    'canonical_url' => SITE_URL.to_s,
    'wiki_page_url' => WIKI_URL.to_s.delete_suffix('/'), # Remove the trailing slash
    'article_title' => SITE_NAME,
    'page_footer' => page_footer_html,

    # Sort pages by published date (newest first)
    'all_pages' => all_pages
    .reject { |page| page[:title].start_with?(SITE_NAME) } # Exclude About pages
    .sort_by { |page| page[:published_date] }
    .reverse
    .map { |page| page.transform_keys(&:to_s) }, # Stringify keys because Liquid doesn't support symbols
    })

    # Generate the sitemap sorted by modified date (newest first)
    sitemap_urls_xml = all_pages
    .sort_by { |page| page[:modified_date] }
    .reverse
    .map { |page|
    <<~XML % page
    <url>
    <loc>%{canonical_url}</loc>
    <lastmod>%{modified_date_iso}</lastmod>
    </url>
    XML
    }
    .join('')

    sitemap_xml = <<~XML
    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
    <loc>#{SITE_URL}</loc>
    </url>
    #{sitemap_urls_xml}
    </urlset>
    XML

    sitemap_file = OUTPUT_DIRECTORY.join('sitemap.xml')
    sitemap_file.write(sitemap_xml)
    7 changes: 7 additions & 0 deletions github-wiki-to-html.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    #!/bin/bash
    # Exit on error
    set -e

    ruby ./github-wiki-to-html.rb
    find . -name '*.html' -type f -print0 | xargs -0 html-beautify --replace --quiet
    find . -name '*.xml' -type f -print0 | xargs -0 html-beautify --replace --quiet
    38 changes: 38 additions & 0 deletions gollum-config.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,38 @@
    ##
    # 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
    131 changes: 89 additions & 42 deletions template.html.liquid
    Original file line number Diff line number Diff line change
    @@ -1,98 +1,145 @@
    <!DOCTYPE html>
    <html lang="">
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <meta name="format-detection" content="telephone=no">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- OG meta tags -->
    {% 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 property="og:title" content="{{ article_title }}">
    <meta property="og:url" content="{{ canonical_url }}">
    <meta property="og:image" content="{{ logo_url }}">
    <!-- /OG meta tags -->

    <!-- Twitter Card meta tags -->
    <meta name="twitter:card" content="summary">
    <!-- /Twitter Card meta tags -->

    <title>
    {% if is_home %}
    {{ site_title }}
    {% else %}
    {{ article_title }} - {{ site_title }}
    {% endif %}
    </title>
    <title>{{ article_title }}</title>

    <link rel="canonical" href="{{ page_url }}">
    <link rel="canonical" href="{{ canonical_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'],
    <!-- MathJax -->
    <script defer src="{{ mathjax_config_script }}"></script>
    <script
    defer
    src="https://cdn.jsdelivr.net/npm/mathjax@4.0.0/startup.js"
    integrity="sha384-V8Uc+jzQMe7n4tFx1oAuCOiBj0WFbXurxmgghjXXzYHRKbpk8D/aCD16BkdE/MDh"
    crossorigin="anonymous"
    ></script>
    <!-- /MathJax -->

    <!-- Structured data -->
    {% if is_home %}
    <script type="application/ld+json">
    {
    "@context": "https://schema.org/",
    "@type": "WebSite",
    "name": "{{ site_name }}",
    "url": "{{ canonical_url }}",
    "publisher": {
    "@type": "Organization",
    "name": "{{ site_name }}",
    "url": "{{ site_url }}",
    "logo": "{{ logo_url }}"
    }
    }
    </script>
    {% else %}
    <script type="application/ld+json">
    {
    "@context": "https://schema.org/",
    "@type": "Article",
    "headline": "{{ article_title }}",
    "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "{{ canonical_url }}"
    },
    paths: {
    font: 'https://cdn.jsdelivr.net/npm/@mathjax',
    'mathjax-euler-extension': '[font]/mathjax-euler-font-extension',
    "image": "{{ logo_url }}",
    "datePublished": "{{ published_date_iso }}",
    "dateModified": "{{ modified_date_iso }}",
    "author": {
    "@type": "Person",
    "name": "{{ author_name }}"
    },
    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>
    "publisher": {
    "@type": "Organization",
    "name": "{{ site_name }}",
    "url": "{{ site_url }}",
    "logo": "{{ logo_url }}"
    }
    }
    </script>
    {% endif %}
    <!-- /Structured data -->
    </head>
    <body>
    <!-- Page header -->
    <header>
    <nav>
    <p>
    {% if is_home %}
    {{ all_page_links }}
    {% for page in all_pages %}
    <a href="{{ page.encoded_slug }}">{{ page.escaped_title }}</a>

    {% unless forloop.last %}
    ·
    {% endunless %}
    {% endfor %}
    {% else %}
    <a href="{{ base_path }}">{{ site_title }}</a>
    <a href="{{ base_path }}">{{ site_name }}</a>
    {% endif %}
    </p>
    </nav>
    </header>
    <!-- /Page header -->

    <!-- Page body -->
    <main>
    <article>
    <!-- Article header -->
    <header>
    <h1>{{ article_title }}</h1>

    {% unless is_home %}
    <p>
    <time datetime="{{ published_date_iso }}">{{ published_date_display }}</time>

    {% if is_modified %}
    · 🕒
    <time datetime="{{ modified_date_iso }}">{{ modified_date_display }}</time>
    {% endif %}
    </p>
    {% endunless %}
    </header>
    <!-- /Article header -->

    <!-- Article body -->
    <section>
    <div>
    {{ article_body }}
    </section>
    </div>
    <!-- /Article body -->
    </article>
    </main>
    <!-- /Page body -->

    <!-- Page footer -->
    <footer>
    {{ page_footer }}

    <p>
    <a href="{{ wiki_entry_url }}">Edit this page</a>
    <a href="{{ wiki_page_url }}">Edit this page</a>
    </p>
    </footer>
    <!-- /Page footer -->
    </body>
    </html>
    28 changes: 26 additions & 2 deletions utils.rb
    Original file line number Diff line number Diff line change
    @@ -6,7 +6,7 @@
    def postprocess_html(html)
    dom = Nokogiri::HTML5.fragment(html)

    # Handle internal links
    # Handle links with class="internal"
    dom.css('a.internal').each do |a|
    uri = URI(a['href'])
    path = Pathname(uri.path)
    @@ -17,9 +17,33 @@ def postprocess_html(html)
    # Make the path relative
    path = path.relative_path_from(BASE_PATH)

    uri.path = path.to_s.gsub('%3F', '')
    uri.path = path.to_s
    a['href'] = uri.to_s
    end

    dom.to_html
    end

    # Generate an HTML file
    def generate_html_file(output_filename, article_body_html, html_template, options)
    article_body_html = postprocess_html(article_body_html)

    # Render full HTML
    full_html = html_template.render!({
    'site_url' => SITE_URL.to_s,
    'site_name' => SITE_NAME,
    'base_path' => BASE_PATH.to_s,
    'logo_url' => LOGO_URL.to_s,
    'stylesheet_file' => STYLESHEET_FILE.to_s,
    'mathjax_config_script' => MATHJAX_CONFIG_SCRIPT.to_s,
    'article_body' => article_body_html,
    **options
    }, {
    strict_variables: true,
    strict_filters: true,
    })

    # Write HTML to a file
    output_file = OUTPUT_DIRECTORY.join(output_filename)
    output_file.write(full_html)
    end
  2. yuuki7 revised this gist Nov 24, 2025. 4 changed files with 213 additions and 187 deletions.
    77 changes: 77 additions & 0 deletions config.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,77 @@
    ##
    # 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
    244 changes: 87 additions & 157 deletions github-wiki-to-html.rb
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@
    #!/usr/bin/env ruby
    require 'pathname'
    require 'uri'

    @@ -7,177 +6,108 @@
    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
    # Load constants
    require_relative './config'

    # Tweak HTML converted from Markdown.
    def postprocess_html(html)
    dom = Nokogiri::HTML5.fragment(html)
    # Load methods
    require_relative './utils'

    # Handle internal links.
    dom.css('a.internal').each do |a|
    uri = URI(a['href'].gsub('%3F', ''))
    path = Pathname(uri.path)
    # Load the HTML template
    html_template = Liquid::Template.parse(HTML_TEMPLATE_FILE.read)

    # Strip the extension.
    if path.extname == '.md'
    path = path.sub_ext('')
    end
    # Create a wiki object
    wiki = Gollum::Wiki.new(WIKI_REPO.to_s, GOLLUM_OPTIONS)

    # Make the path relative.
    path = path.relative_path_from(SITE_HOME_PATH)
    # Converted HTML string of the wiki footer
    page_footer = wiki.pages[0].footer.formatted_data

    uri.path = path.to_s
    a['href'] = uri.to_s
    # Pages to display on the home page
    all_pages = []

    # Remove rel="nofollow".
    a.remove_attribute('rel')
    end
    # Generate the individual article pages
    wiki.pages.each do |wiki_entry|
    # Skip non-Markdown entries
    next unless wiki_entry.format == :markdown

    # Handle anchors.
    dom.css('a.anchor').each do |a|
    a.remove_attribute('rel')
    end
    wiki_entry_slug = wiki_entry.filename_stripped

    # Remove class="editable".
    dom.css('.editable').each do |element|
    element.remove_class('editable')
    end
    # Skip home and special entries
    next if wiki_entry_slug =~ /^(?:Home|LICENSE|README)$/

    dom.to_html
    end
    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)

    # 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.
    # Render full HTML
    full_html = html_template.render(
    'site_title' => SITE_TITLE,
    'site_url' => SITE_URL.to_s,
    'base_path' => BASE_PATH.to_s,
    'stylesheet_file' => STYLESHEET_FILE.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,

    'page_url' => page_url.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,
    'article_body' => article_body,

    'page_footer' => page_footer,
    'wiki_entry_url' => wiki_entry_url.to_s,

    'is_home' => is_home,
    'all_page_links' => all_page_links,
    'is_home' => false,
    )

    # Write HTML to a file.
    output_file_path = OUTPUT_DIRECTORY_PATH.join(output_filename)
    File.write(output_file_path, full_html)
    # 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)
    54 changes: 24 additions & 30 deletions template.html.liquid
    Original file line number Diff line number Diff line change
    @@ -6,15 +6,19 @@
    <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 }}">
    {% 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:type" content="article">
    <meta property="og:url" content="{{ page_url }}">
    <meta property="og:site_name" content="{{ site_title }}">

    <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 %}
    @@ -24,8 +28,9 @@
    {% endif %}
    </title>

    <link rel="canonical" href="{{ canonical_url }}">
    <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 = {
    @@ -51,44 +56,33 @@
    };
    </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>
    <p>
    {% if is_home %}
    {{ all_page_links }}
    {% else %}
    <a href="{{ base_path }}">{{ site_title }}</a>
    {% endif %}
    </p>
    </nav>
    </header>

    <!-- Page body -->
    <main>
    <!-- Article -->
    <article>
    <!-- Article header -->
    <header>
    <h1>{{ article_title }}</h1>
    </header>

    <!-- Article body -->
    <section>{{ article_body }}</section>
    <section>
    {{ article_body }}
    </section>
    </article>
    </main>

    @@ -97,7 +91,7 @@
    {{ page_footer }}

    <p>
    <a href="{{ wiki_page_url }}">Edit this page</a>
    <a href="{{ wiki_entry_url }}">Edit this page</a>
    </p>
    </footer>
    </body>
    25 changes: 25 additions & 0 deletions utils.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,25 @@
    ##
    # 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
  3. yuuki7 revised this gist Nov 20, 2025. 2 changed files with 256 additions and 184 deletions.
    336 changes: 152 additions & 184 deletions github-wiki-to-html.rb
    Original file line number Diff line number Diff line change
    @@ -1,215 +1,183 @@
    require 'fileutils'
    #!/usr/bin/env ruby
    require 'pathname'
    require 'uri'

    require 'commonmarker'
    require 'github/markup'
    require 'gollum-lib'
    require 'liquid'
    require 'nokogiri'

    def postprocess_html(content_html, toc_html = nil)
    dom = Nokogiri::HTML5.fragment(content_html)
    # Home page URL of the source GitHub Wiki
    WIKI_URL = URI('https://github.com/wikinder/wikinder/wiki')

    dom.css('a.internal[href]').each do |a|
    href = URI.parse(a['href'])
    path = Pathname.new(href.path)
    # Title of the output site
    SITE_TITLE = 'Wikinder'

    # Strip extensions from internal links
    # 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'
    href.path = path.sub_ext('').to_s
    a['href'] = href.to_s
    path = path.sub_ext('')
    end

    # Remove rel="nofollow"
    # 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

    # Insert TOC before the first h2 heading
    # if toc_html
    # first_heading = dom.at_css('h2')
    # if first_heading
    # first_heading.before(toc_html)
    # end
    # end
    # Remove class="editable".
    dom.css('.editable').each do |element|
    element.remove_class('editable')
    end

    dom.to_html
    end

    wiki_repo_dir = Pathname.new('./wikinder.wiki')
    output_dir = Pathname.new('./wikinder.github.io')
    FileUtils.mkdir_p(output_dir)
    HTML_TEMPLATE = DATA.read
    # Load the HTML template.
    html_template = Liquid::Template.parse(File.read(HTML_TEMPLATE_FILE_PATH))

    # https://github.com/gollum/gollum/wiki/Custom-rendering-gems
    GitHub::Markup::Markdown::MARKDOWN_GEMS['commonmarker'] = proc do |content|
    Commonmarker.to_html(content, options: {
    render: { unsafe: true },
    extension: {
    strikethrough: true,
    tagfilter: true,
    table: true,
    autolink: true,
    tasklist: true,
    footnotes: true,
    },
    })
    end
    GitHub::Markup::Markdown::MARKDOWN_GEMS.delete('kramdown')

    wiki = Gollum::Wiki.new(wiki_repo_dir, {
    # 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],
    })
    wiki.pages.each do |page|
    path = Pathname.new(page.path)
    is_home = path.to_s == 'Home.md'
    is_about = path.to_s == 'Wikinder.md'

    # 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
    title = 'Wikinder'
    article_title = 'Welcome to Wikinder'
    canonical_path = ''
    canonical_url = SITE_URL
    article_title = SITE_TITLE
    wiki_page_url = WIKI_URL
    output_filename = 'index.html'

    page_nav = wiki.pages
    .map { |pg|
    {
    path: "/#{URI.encode_uri_component(Pathname.new(pg.path).sub_ext('').to_s)}",
    title: pg.title.tr('-', ' '),
    }
    }
    .reject { |pg| pg[:title] =~ /^(?:Home|LICENSE|README)$|^Wikinder/ }
    .sort_by { |pg| pg[:title].downcase }
    .map { |pg| "<a href=\"%{path}\">%{title}</a>" % pg }
    .join(' · ')
    else
    if is_about
    article_title = 'About'
    else
    article_title = page.title.tr('-', ' ')
    end
    canonical_url = URI.join(SITE_URL, escaped_site_page_slug)
    article_title = wiki_page.title.tr('-', ' ')

    title = "#{article_title} - Wikinder"
    canonical_path = URI.encode_uri_component(path.sub_ext('').to_s)
    output_filename = path.sub_ext('.html')
    # FIXME
    wiki_page_url = URI.join(WIKI_URL, 'wiki/', escaped_wiki_page_slug)

    page_nav = '<a href="/">Wikinder</a>'
    output_filename = "#{site_page_slug}.html"
    end

    content = postprocess_html(page.formatted_data, page.toc_data)
    html = HTML_TEMPLATE % {
    title: title,
    canonical_path: canonical_path,
    page_nav: page_nav,
    article_title: article_title,
    content: content,
    footer: page.footer.formatted_data,
    wiki_page_path: is_home ? '' : "/#{canonical_path}",
    }

    output_path = output_dir.join(output_filename)
    File.write(output_path, html)
    # 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

    __END__
    <!DOCTYPE html>
    <html lang="mul">
    <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="%{title}">
    <meta property="og:url" content="https://wikinder.org/%{canonical_path}">
    <meta property="og:site_name" content="Wikinder">
    <meta property="og:image" content="https://wikinder.org/og-image.jpg">
    <meta property="og:type" content="article">

    <meta name="twitter:card" content="summary">
    <meta name="twitter:image" content="https://wikinder.org/og-image.jpg">
    <meta name="twitter:image:alt" content="bear">

    <title>%{title}</title>
    <link rel="canonical" href="https://wikinder.org/%{canonical_path}">
    <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>
    #article-header {
    align-items: baseline;
    display: flex;
    gap: 1rem;
    }

    #article-title {
    margin: 0;
    }

    .toc {
    margin-top: 1rem;
    }

    .toc-title {
    font-weight: bold;
    }

    img {
    max-width: 100%%;
    }

    pre {
    overflow-wrap: anywhere;
    white-space: pre-wrap;
    }
    </style>
    </head>
    <body>
    <header id="page-header">
    <nav id="page-nav">
    <p>
    %{page_nav}
    </p>
    </nav>
    </header>
    <main id="page-main">
    <article id="article">
    <header id="article-header">
    <h1 id="article-title">%{article_title}</h1>
    </header>
    <section id="article-main">
    %{content}
    </section>
    </article>
    </main>
    <footer id="page-footer">
    %{footer}
    <p>
    <a href="https://github.com/wikinder/wikinder/wiki%{wiki_page_path}">Edit this page</a>
    </p>
    </footer>
    </body>
    </html>
    104 changes: 104 additions & 0 deletions template.html.liquid
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,104 @@
    <!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>
  4. yuuki7 revised this gist Nov 14, 2025. 1 changed file with 195 additions and 10 deletions.
    205 changes: 195 additions & 10 deletions github-wiki-to-html.rb
    Original file line number Diff line number Diff line change
    @@ -1,30 +1,215 @@
    require 'commonmarker'
    require 'fileutils'
    require 'pathname'
    require 'uri'

    require 'commonmarker'
    require 'github/markup'
    require 'gollum-lib'
    require 'nokogiri'

    def postprocess_html(content_html, toc_html = nil)
    dom = Nokogiri::HTML5.fragment(content_html)

    repo_dir = '.'
    output_dir = './out'
    dom.css('a.internal[href]').each do |a|
    href = URI.parse(a['href'])
    path = Pathname.new(href.path)

    # Strip extensions from internal links
    if path.extname == '.md'
    href.path = path.sub_ext('').to_s
    a['href'] = href.to_s
    end

    # Remove rel="nofollow"
    a.remove_attribute('rel')
    end

    # Insert TOC before the first h2 heading
    # if toc_html
    # first_heading = dom.at_css('h2')
    # if first_heading
    # first_heading.before(toc_html)
    # end
    # end

    dom.to_html
    end

    wiki_repo_dir = Pathname.new('./wikinder.wiki')
    output_dir = Pathname.new('./wikinder.github.io')
    FileUtils.mkdir_p(output_dir)
    HTML_TEMPLATE = DATA.read

    # https://github.com/gollum/gollum/wiki/Custom-rendering-gems
    GitHub::Markup::Markdown::MARKDOWN_GEMS['commonmarker'] = proc do |content|
    Commonmarker.to_html(content, options: {
    render: { unsafe: true },
    extensions: {
    extension: {
    strikethrough: true,
    tagfilter: true,
    table: true,
    autolink: true,
    tasklist: true,
    footnotes: true,
    },
    })
    end
    GitHub::Markup::Markdown::MARKDOWN_GEMS.delete('kramdown')

    wiki = Gollum::Wiki.new(repo_dir, base_path: '/')
    FileUtils.mkdir_p(output_dir)

    wiki = Gollum::Wiki.new(wiki_repo_dir, {
    hyphened_tag_lookup: true,
    })
    wiki.pages.each do |page|
    output = page.formatted_data
    output_file = File.join(output_dir, page.path.sub(/\.[^.]+$/, '.html'))
    File.write(output_file, output)
    path = Pathname.new(page.path)
    is_home = path.to_s == 'Home.md'
    is_about = path.to_s == 'Wikinder.md'

    if is_home
    title = 'Wikinder'
    article_title = 'Welcome to Wikinder'
    canonical_path = ''
    output_filename = 'index.html'

    page_nav = wiki.pages
    .map { |pg|
    {
    path: "/#{URI.encode_uri_component(Pathname.new(pg.path).sub_ext('').to_s)}",
    title: pg.title.tr('-', ' '),
    }
    }
    .reject { |pg| pg[:title] =~ /^(?:Home|LICENSE|README)$|^Wikinder/ }
    .sort_by { |pg| pg[:title].downcase }
    .map { |pg| "<a href=\"%{path}\">%{title}</a>" % pg }
    .join(' · ')
    else
    if is_about
    article_title = 'About'
    else
    article_title = page.title.tr('-', ' ')
    end

    title = "#{article_title} - Wikinder"
    canonical_path = URI.encode_uri_component(path.sub_ext('').to_s)
    output_filename = path.sub_ext('.html')

    page_nav = '<a href="/">Wikinder</a>'
    end

    content = postprocess_html(page.formatted_data, page.toc_data)
    html = HTML_TEMPLATE % {
    title: title,
    canonical_path: canonical_path,
    page_nav: page_nav,
    article_title: article_title,
    content: content,
    footer: page.footer.formatted_data,
    wiki_page_path: is_home ? '' : "/#{canonical_path}",
    }

    output_path = output_dir.join(output_filename)
    File.write(output_path, html)
    end

    __END__
    <!DOCTYPE html>
    <html lang="mul">
    <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="%{title}">
    <meta property="og:url" content="https://wikinder.org/%{canonical_path}">
    <meta property="og:site_name" content="Wikinder">
    <meta property="og:image" content="https://wikinder.org/og-image.jpg">
    <meta property="og:type" content="article">

    <meta name="twitter:card" content="summary">
    <meta name="twitter:image" content="https://wikinder.org/og-image.jpg">
    <meta name="twitter:image:alt" content="bear">

    <title>%{title}</title>
    <link rel="canonical" href="https://wikinder.org/%{canonical_path}">
    <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>
    #article-header {
    align-items: baseline;
    display: flex;
    gap: 1rem;
    }

    #article-title {
    margin: 0;
    }

    .toc {
    margin-top: 1rem;
    }

    .toc-title {
    font-weight: bold;
    }

    img {
    max-width: 100%%;
    }

    pre {
    overflow-wrap: anywhere;
    white-space: pre-wrap;
    }
    </style>
    </head>
    <body>
    <header id="page-header">
    <nav id="page-nav">
    <p>
    %{page_nav}
    </p>
    </nav>
    </header>
    <main id="page-main">
    <article id="article">
    <header id="article-header">
    <h1 id="article-title">%{article_title}</h1>
    </header>
    <section id="article-main">
    %{content}
    </section>
    </article>
    </main>
    <footer id="page-footer">
    %{footer}
    <p>
    <a href="https://github.com/wikinder/wikinder/wiki%{wiki_page_path}">Edit this page</a>
    </p>
    </footer>
    </body>
    </html>
  5. yuuki7 created this gist Nov 9, 2025.
    30 changes: 30 additions & 0 deletions github-wiki-to-html.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,30 @@
    require 'commonmarker'
    require 'fileutils'
    require 'github/markup'
    require 'gollum-lib'

    repo_dir = '.'
    output_dir = './out'

    GitHub::Markup::Markdown::MARKDOWN_GEMS['commonmarker'] = proc do |content|
    Commonmarker.to_html(content, options: {
    render: { unsafe: true },
    extensions: {
    strikethrough: true,
    tagfilter: true,
    table: true,
    autolink: true,
    tasklist: true,
    },
    })
    end
    GitHub::Markup::Markdown::MARKDOWN_GEMS.delete('kramdown')

    wiki = Gollum::Wiki.new(repo_dir, base_path: '/')
    FileUtils.mkdir_p(output_dir)

    wiki.pages.each do |page|
    output = page.formatted_data
    output_file = File.join(output_dir, page.path.sub(/\.[^.]+$/, '.html'))
    File.write(output_file, output)
    end