2026-03-01 : Convert gemini capsule to http to host on neocities
convert_gemini.py
(don't forget to add # at first line)
!/usr/bin/env python3
"""
Convertit un site Gemini (.gmi) en site HTML en conservant l'arborescence.
Pipeline : Gemini (.gmi) -> Markdown (.md) -> HTML (.html)
POLITIQUE DE SORTIE (version allégée) :
- Fichiers AUTORISÉS en sortie : .html (et .md temporaires)
- Fichiers EXCLUS : images, archives (zip, tar, gz…), binaires, etc.
Dépendances :
- pandoc (installé sur le système)
- Python 3.9+
Usage :
python convert_gemini_to_html.py /chemin/site_gemini /chemin/sortie_html
"""
import subprocess
import sys
from pathlib import Path
import shutil
ALLOWED_COPY_EXTENSIONS = set()
BLOCKED_EXTENSIONS = {
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg",
".zip", ".tar", ".gz", ".bz2", ".xz", ".7z",
".pdf", ".mp3", ".ogg", ".wav", ".mp4",
}
def gemini_to_markdown(text: str) -> str:
"""
Convertit du Gemtext en Markdown.
- Titres (#, ##, ###)
- Liens Gemini => url [label]
- Conversion des liens internes .gmi -> .html
- Listes simples
"""
md_lines = []
for line in text.splitlines():
line = line.rstrip()
# Titres
if line.startswith("###"):
md_lines.append("### " + line[3:].strip())
elif line.startswith("##"):
md_lines.append("## " + line[2:].strip())
elif line.startswith("#"):
md_lines.append("# " + line[1:].strip())
# Liens Gemini
elif line.startswith("=>"):
parts = line[2:].strip().split(maxsplit=1)
if not parts:
md_lines.append("")
continue
url = parts[0]
label = parts[1] if len(parts) == 2 else url
if not url.startswith(("http://", "https://", "gemini://")):
if url.endswith(".gmi"):
url = url[:-4] + ".html"
md_lines.append("")
md_lines.append(f"[{label}]({url})")
md_lines.append("")
elif line.startswith("* "):
md_lines.append("- " + line[2:])
else:
md_lines.append(line)
return "\n".join(md_lines)
def run_pandoc(md_file: Path, html_out: Path):
html_out.parent.mkdir(parents=True, exist_ok=True)
cmd = [
"pandoc",
str(md_file),
"-f", "markdown",
"-t", "html",
"-s",
"-M", "charset=utf-8",
"-o", str(html_out)
]
subprocess.run(cmd, check=True)
def build_index(folder: Path):
html_files = sorted(
p for p in folder.iterdir()
if p.suffix == ".html" and p.name != "index.html"
)
if not html_files:
return
lines = [
"<html><head><meta charset='utf-8'><title>Index</title></head><body>",
f"<h1>{folder.name}</h1>",
"<ul>"
]
for f in html_files:
lines.append(f"<li><a href='{f.name}'>{f.stem}</a></li>")
lines.append("</ul></body></html>")
index_path = folder / "index.html"
index_path.write_text("\n".join(lines), encoding="utf-8")
def main(src_root: Path, dst_root: Path):
tmp_md_root = dst_root / "._md"
tmp_md_root.mkdir(parents=True, exist_ok=True)
for path in src_root.rglob("*"):
rel = path.relative_to(src_root)
out = dst_root / rel
if path.is_dir():
out.mkdir(parents=True, exist_ok=True)
continue
suffix = path.suffix.lower()
if suffix == ".gmi":
md_tmp = tmp_md_root / rel.with_suffix(".md")
md_tmp.parent.mkdir(parents=True, exist_ok=True)
text = path.read_text(encoding="utf-8")
md_text = gemini_to_markdown(text)
md_tmp.write_text(md_text, encoding="utf-8")
html_out = out.with_suffix(".html")
run_pandoc(md_tmp, html_out)
else:
continue
for folder in dst_root.rglob("*"):
if folder.is_dir() and folder.name != "._md":
build_index(folder)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: convert_gemini_to_html.py SRC_DIR DST_DIR")
sys.exit(1)
src = Path(sys.argv[1]).expanduser().resolve()
dst = Path(sys.argv[2]).expanduser().resolve()
if not src.exists():
print(f"Source inexistante : {src}")
sys.exit(1)
main(src, dst)
print("Conversion terminée ✔ (HTML only, archive allégée)")
publish_gemini.py
(don't forget to add # at first line)
!/bin/bash
export NEOCITIES_API_KEY=YOUR_API_KEY
neocitizen upload --dir=$HOME/gemini_html