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

Proxied content from gemini://tilde.green/~pyroboynroses/gemlog/2026-03-01.gmi

Gemini request details:

Original URL
gemini://tilde.green/~pyroboynroses/gemlog/2026-03-01.gmi
Status code
Success
Meta
text/gemini
Proxied by
kineto

Be advised that no attempt was made to verify the remote SSL certificate.