gem2web premade source archive (.zip)
A simple tool-chain for converting gemtext to HTML. Written with python and bash/shell. Made by Thomas Conway.
released under GPL3-or-later, all rights reserved.
chmod +x gem2web.py chmod +x publish-gem2web.sh
3. Test gem2web.py first to ensure it works. Create a sample .gmi file to convert and point the script at it using the command documented in usage.
4. After your sure it works, test publish-gem2web.sh with the usage command documented below.
A simple python program that formats gemtext .gmi capsule pages into HTML webpages.
python3 gem2web.py < input.gmi > output.html
Adjust headers like <title> to reflect your domain and feel free to add/adjust style.
if you choose to rename this python script make sure that convert-gemini.sh points to the scripts new name which is set around ln 19 in the shell script.
#!/usr/bin/env python3 # Copyright (c) 2025 Thomas Conway. All rights reserved. # SPDX-License-Identifier: GPL-3.0-or-later # Description: A simple python program to format gemtext .gmi text files into HTML webpages. # Usage: python3 gemini_to_html.py < input.gmi > output.html import sys def escape_html(text): """Escape special HTML characters in text""" return text.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') def convert_gmi_links(url): """Convert .gmi extension to .html in relative URLs""" if '://' not in url: # Relative URL # Split URL into path and query/fragment components path_part = url query_fragment = '' # Find first occurrence of ? or # for sep in ('?', '#'): idx = url.find(sep) if idx != -1: path_part = url[:idx] query_fragment = url[idx:] break # Replace .gmi extension if found at end of path if path_part.endswith('.gmi'): return path_part[:-4] + '.html' + query_fragment return url def gemini_to_html(): """Convert Gemtext from stdin to HTML""" in_pre = False current_block = None # 'p', 'ul', 'blockquote' output_lines = [] print('<!DOCTYPE html>') print('<html>') print('<head>') print('<meta charset="UTF-8">') print('<meta name="viewport" content="width=device-width, initial-scale=1.0">') print('<title>yourdomain.com</title>') print('<style>') print(' blockquote {') print(' border-left: 4px solid #ccc;') print(' padding-left: 10px;') print(' margin-left: 20px;') print(' color: green;') print(' font-style: italic;') print(' }') print('</style>') print('</head>') print('<body>') for line in sys.stdin: line = line.rstrip() # Handle preformatted blocks if in_pre: if line == '```': print('</pre>') in_pre = False else: print(escape_html(line)) continue # Handle special blocks if line.startswith('```'): print('<pre>') in_pre = True elif line.startswith('=>'): parts = line[2:].strip().split(maxsplit=1) url = parts[0] text = url if len(parts) < 2 else parts[1].strip() # Convert .gmi links to .html url = convert_gmi_links(url) print(f'<p><a href="{escape_html(url)}">{escape_html(text)}</a></p>') elif line.startswith('#'): level = min(3, line.count('#')) heading = line.lstrip('#').strip() print(f'<h{level}>{escape_html(heading)}</h{level}>') elif line.startswith('>'): # Start new blockquote if needed if current_block != 'blockquote': if current_block: print(f'</{current_block}>') print('<blockquote>') current_block = 'blockquote' quote = line[1:].strip() print(f'<p>{escape_html(quote)}</p>') elif line.startswith('* '): # Start new list if needed if current_block != 'ul': if current_block: print(f'</{current_block}>') print('<ul>') current_block = 'ul' item = line[2:].strip() print(f'<li>{escape_html(item)}</li>') elif line == '': # Blank line ends current block if current_block: print(f'</{current_block}>') current_block = None else: # Start new paragraph if needed if current_block != 'p': if current_block: print(f'</{current_block}>') print('<p>', end='') current_block = 'p' else: # Continue existing paragraph print('<br>', end='') print(escape_html(line), end='') # Close any open blocks at end if current_block: print(f'</{current_block}>') if in_pre: print('</pre>') print('</body>') print('</html>') if __name__ == '__main__': gemini_to_html()
bash/shell script to batch apply the python conversion program to every gemtext file in the input path and copy the output to a specified web server html file path.
./publish-gem2web.sh /your/gemtext/path /your/html/path
sudo ./publish-gem2web.sh /home/caddy/gemini/gemtext /var/www/html
alias gempub='sudo /home/user/gemini/scripts/gem2web/publish-gem2web.sh /home/user/gemini/gemtext /var/www/html'
sudo will probably be required for write permissions.
Setting file permissions beginning at line number 34 may be needed depending on your setup. I commented it out in the source code. Theres also some funkyness that may go on if you mess with chmod and chown that could be worked on.
#!/bin/bash # Copyright (c) 2025 Thomas Conway. All rights reserved. # SPDX-License-Identifier: GPL-3.0-or-later # Description: Batch convert shell script to convert a whole directory of gemtext .gmi files to HTML. # Usage: ./publish-gem2web.sh /your/gemtext/path /your/html/path # real example: sudo ./publish-gem2web.sh /home/caddy/gemini/gemtext /var/www/html set -e # Validate arguments if [ "$#" -ne 2 ]; then echo "Usage: $0 <source-dir> <output-dir>" echo "Example: $0 ./gemini ./html" exit 1 fi SOURCE_DIR="$1" OUTPUT_DIR="$2" SCRIPT_PATH="$(dirname "$0")/gem2web.py" # Verify directories exist [ -d "$SOURCE_DIR" ] || { echo "Error: Source directory not found"; exit 1; } [ -d "$OUTPUT_DIR" ] || { echo "Error: Output directory not found"; exit 1; } [ -f "$SCRIPT_PATH" ] || { echo "Error: gem2web.py not found in script directory"; exit 1; } # Process files echo "Starting conversion from $SOURCE_DIR to $OUTPUT_DIR" for gmi_file in "$SOURCE_DIR"/*.gmi; do [ -f "$gmi_file" ] || continue # Skip if no files found base_name=$(basename "$gmi_file" .gmi) html_file="$OUTPUT_DIR/${base_name}.html" # Set proper permissions chown username "$html_file" 2>/dev/null || true # chmod 644 "$html_file" # Convert Gemini to HTML "$SCRIPT_PATH" < "$gmi_file" > "$html_file" echo "Converted: $gmi_file to $html_file" done echo "Success! Gemtext files from $SOURCE_DIR have been converted to HTML and put into $OUTPUT_DIR"