Howto — Publish a family tree online with Gramps + GitHub + Topola Viewer
This guide takes you from zero to a shareable, interactive family tree hosted at https://YOUR_USERNAME.github.io/YOUR_REPO/, built locally with Gramps and rendered online with Topola Viewer.
End result: a URL you can send to family that opens an interactive tree with photos, no login required, no server to maintain, zero recurring cost.
What this is NOT: a private solution. The repo and its contents are public. See the Privacy section at the end for alternatives.
Live example built with this exact pipeline: github.com/octarina8/LOArbre — browse the repo for a real working setup.
Overview of the pipeline
┌─────────┐ .ged ┌──────────┐ .gdz ┌─────────────┐
│ Gramps │ ─────────> │ Python │ ─────────> │ GitHub repo │
│ (local) │ + photos │ script │ (zipped) │ + Pages │
└─────────┘ └──────────┘ └─────────────┘
│
▼
https://user.github.io/repo/
→ Topola Viewer renders the tree
The pipeline has three artifacts:
.gedfile — standard GEDCOM export from Gramps (text, references photos by relative path)..gdzfile — a ZIP containing the.ged+ amedia/folder with all the photos. Topola Viewer natively understands this format.- The repo on GitHub Pages — hosts the pre-built Topola Viewer web app + the
.gdzfile, served over HTTPS atgithub.io.
Prerequisites
Install the following on your local machine:
- Gramps — free, cross-platform genealogy software. Any 5.x version works.
- Python 3.9+ — needed for the packaging script. On Windows, check “Add Python to PATH” during install.
- Git — optional but recommended for syncing the repo. GitHub web upload also works.
- A GitHub account at github.com.
No paid plans required. GitHub Pages is free on public repos.
Part 1 — Build the tree locally in Gramps
1.1 Create a Family Tree
Open Gramps → Family Trees → New… → give it a name → Load Family Tree.
1.2 Add people, events, and relationships
Use the People, Families, Events, Places tabs. For each person, fill at minimum:
- Name (given + surname)
- Sex
- Birth date and place
- Death date (if deceased)
For relationships, create Family records that link spouses and children.
1.3 Media — critical for the online version
This is the single most important part to get right. Topola online will show photos only if the photos travel inside the .gdz. For that to work smoothly, keep all photos in ONE folder on disk with simple filenames.
Recommended setup:
- Create a dedicated folder on disk, for example
C:\genealogy\my-tree\media\. - Put every photo you plan to use in that folder (or subfolders). Rename them if needed to avoid special characters.
- In Gramps: Edit → Preferences → Family Tree → Base path for relative media paths — point it to that folder.
- When you add a media object in Gramps (via the Media tab or Gallery of a person), browse to a file inside that folder. Gramps will store the path relative to the base path (e.g.,
photo-123.jpginstead ofC:\genealogy\my-tree\media\photo-123.jpg). - For a person’s primary photo, double-click the person → Gallery tab → Add → select the media object. The first image in the gallery is the one shown in the tree.
1.4 Fix absolute paths (if you already have them)
If you imported from another program and your Media tab shows paths like C:\Users\...\photos\foo.jpg, convert them to relative:
- Tools → Family Tree Processing → Media Manager…
- Choose “Convert paths from absolute to relative” → Forward → confirm.
- Verify: the Media tab Path column should now show short names, not full paths.
If paths point to multiple source folders, consolidate first:
- Copy all photos to your single media folder.
- Media Manager → “Replace substrings in the path” → replace the old prefix with the new one. Repeat for each old prefix.
Part 2 — Export the GEDCOM
Once your tree is clean and media paths are relative:
- Family Trees → Export…
- Pick format GEDCOM → Forward
- Save as
my-tree.gedin a working folder (e.g.,C:\genealogy\my-tree\). - In the options, if you have living people, enable “Restrict data on living people” or “Privatize living people” for an extra layer of privacy.
The resulting .ged is a plain text file. Open it with a text editor and verify that 1 FILE lines look like relative paths (1 FILE media/photo.jpg), not absolute (1 FILE C:\...). If they’re still absolute, the packaging script in Part 4 will normalize them, so it’s not a blocker — but relative is cleaner.
Part 3 — Set up the GitHub repo and GitHub Pages
3.1 Create a public repo
On github.com/new:
- Repository name: anything (lowercase recommended, no spaces). Example:
my-family-tree. - Visibility: Public (required for free GitHub Pages).
- Initialize with a README so the repo isn’t empty.
3.2 Download the pre-built Topola Viewer
Topola Viewer publishes a ready-to-deploy build on its gh-pages branch. Download it:
https://github.com/PeWu/topola-viewer/archive/refs/heads/gh-pages.zip
Extract the zip. You’ll see files like index.html, favicon.png, and an assets/ folder.
3.3 Upload Topola Viewer to your repo
Two options:
Via GitHub web: go to your repo → Add file → Upload files → drag the contents of the extracted folder (not the folder itself) → Commit changes. Make sure index.html ends up at the repo root.
Via Git CLI:
git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git
cd YOUR_REPO
# Copy the extracted gh-pages.zip contents here
git add .
git commit -m "Add Topola Viewer"
git push
3.4 Enable GitHub Pages
In your repo: Settings → Pages → under Source select Deploy from a branch → Branch main, folder / (root) → Save.
Wait 1-2 minutes. Pages will give you a URL: https://YOUR_USERNAME.github.io/YOUR_REPO/. Open it — you’ll see Topola Viewer’s welcome screen. The tree isn’t there yet; next step is adding your .gdz.
Part 4 — The packaging script make-gdz.py
Topola Viewer needs a .gdz file: a ZIP containing the .ged at the root plus a media/ folder with all photos. The paths inside the .ged must match the paths inside the ZIP.
This script does the whole job in one shot. Save it as make-gdz.py in your working folder (alongside the .ged and the media folder):
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Generates a .gdz ready for Topola Viewer from:
- A .ged exported from Gramps
- A folder with the photos (subfolders allowed)
Usage:
1) Edit the three paths in the CONFIGURATION section below.
2) Run: python make-gdz.py
3) Upload the resulting .gdz to your GitHub repo.
Uses only Python standard library — nothing to install.
"""
import os
import re
import sys
import zipfile
# =============== CONFIGURATION ===============
# Path to the .ged you exported from Gramps:
GED_PATH = r"C:\genealogy\my-tree\my-tree.ged"
# Folder with all the photos (same one as Gramps base media path):
MEDIA_DIR = r"C:\genealogy\my-tree\media"
# Where to save the output .gdz:
OUTPUT_GDZ = r"C:\genealogy\my-tree\my-tree.gdz"
# =============================================
def normalize_file_path(raw_path):
"""
Convert any FILE path (absolute Windows, relative with ..\\, .\\, etc.)
to the standard form 'media/<rest>' pointing inside the ZIP.
- If the path contains a segment called 'media', take what follows it.
- Otherwise, keep only the filename and prefix with 'media/'.
"""
parts = re.split(r'[/\\]', raw_path)
parts = [p for p in parts if p not in ('', '.', '..')]
media_idx = None
for i, seg in enumerate(parts):
if seg.lower() == 'media':
media_idx = i
if media_idx is not None and media_idx < len(parts) - 1:
return 'media/' + '/'.join(parts[media_idx + 1:])
return 'media/' + parts[-1]
def detect_encoding(raw_bytes):
"""Gramps usually exports UTF-8; older GEDCOMs may be Latin-1."""
try:
raw_bytes.decode('utf-8')
return 'utf-8'
except UnicodeDecodeError:
return 'latin-1'
def main():
if not os.path.isfile(GED_PATH):
sys.exit(f"ERROR: cannot find .ged: {GED_PATH}")
if not os.path.isdir(MEDIA_DIR):
sys.exit(f"ERROR: cannot find media folder: {MEDIA_DIR}")
print(f"[1/4] Reading .ged: {GED_PATH}")
with open(GED_PATH, 'rb') as f:
raw = f.read()
encoding = detect_encoding(raw)
text = raw.decode(encoding)
print(f" Detected encoding: {encoding}")
print("[2/4] Normalizing FILE paths...")
out_lines = []
refs_found = []
for line in text.splitlines(keepends=True):
bare = line.rstrip('\r\n')
eol = line[len(bare):]
if bare.startswith('1 FILE '):
old_path = bare[len('1 FILE '):]
# Skip the self-reference in the header (points to the .ged file itself)
if old_path.lower().endswith('.ged'):
out_lines.append(line)
continue
new_path = normalize_file_path(old_path)
refs_found.append(new_path)
out_lines.append(f'1 FILE {new_path}{eol}')
else:
out_lines.append(line)
new_ged_bytes = ''.join(out_lines).encode(encoding)
print(f" {len(refs_found)} references normalized")
print(f"[3/4] Verifying photos in {MEDIA_DIR}")
unique_refs = sorted(set(refs_found))
files_to_add = {}
missing = []
for ref in unique_refs:
rel = ref[len('media/'):]
disk = os.path.join(MEDIA_DIR, rel.replace('/', os.sep))
if os.path.isfile(disk):
files_to_add[ref] = disk
else:
missing.append(ref)
if missing:
print(f" WARNING: {len(missing)} photo(s) NOT found on disk:")
for m in missing[:10]:
print(f" - {m}")
if len(missing) > 10:
print(f" ... and {len(missing) - 10} more")
print(" (those refs stay in the .ged but will show no image)")
print(f" {len(files_to_add)} photo(s) ready to pack")
print(f"[4/4] Building {OUTPUT_GDZ}")
ged_arcname = os.path.basename(GED_PATH)
with zipfile.ZipFile(OUTPUT_GDZ, 'w', zipfile.ZIP_STORED) as gdz:
gdz.writestr(ged_arcname, new_ged_bytes)
for arcname, disk in sorted(files_to_add.items()):
gdz.write(disk, arcname)
size = os.path.getsize(OUTPUT_GDZ)
print()
print(f"OK -> {OUTPUT_GDZ}")
print(f" {size:,} bytes ({size/1024/1024:.1f} MB)")
if size > 100 * 1024 * 1024:
print(" WARNING: over 100 MB. GitHub will reject the upload.")
print(" Resize the photos before building the .gdz.")
elif size > 50 * 1024 * 1024:
print(" NOTE: over 50 MB. GitHub shows a warning but still accepts it.")
if __name__ == '__main__':
main()Edit the three paths at the top to match your setup. Then from PowerShell:
cd C:\genealogy\my-tree
python make-gdz.py
Expected output ends with something like:
OK -> C:\genealogy\my-tree\my-tree.gdz
42,315,120 bytes (40.3 MB)
If you see WARNING: X photo(s) NOT found on disk, those photos are referenced by the .ged but missing from MEDIA_DIR. Find them, copy them in, and re-run.
Size limits:
- Single file max in GitHub: 100 MB (hard reject).
- GitHub starts warning at 50 MB.
- If you exceed 100 MB, resize the photos. 1000 px on the long side is plenty for display. Tools: IrfanView, XnConvert, or ImageMagick’s
mogrify -resize 1000x1000.
Part 5 — Upload the .gdz and view the tree
Upload my-tree.gdz to the root of your GitHub repo (web upload or git push).
Your shareable URL is:
https://YOUR_USERNAME.github.io/YOUR_REPO/#/view?url=https://YOUR_USERNAME.github.io/YOUR_REPO/my-tree.gdz&handleCors=false
Replace YOUR_USERNAME, YOUR_REPO, and my-tree.gdz with your actual values. Case matters — GitHub Pages URLs are case-sensitive on the path.
Optionally add a starting person: append &indi=I0001 (replace with the GEDCOM id you want as the starting focus).
Open in a private/incognito window to test. The 40-80 MB .gdz takes a few seconds to download on first load; after that Topola caches it client-side.
Part 6 — Updating the tree
Routine update cycle:
- Edit your tree in Gramps as usual.
- Family Trees → Export → GEDCOM → overwrite the same
.ged. python make-gdz.py→ overwrites the same.gdz.- Push the new
.gdzto GitHub (replace the file in the web UI, orgit add . && git commit -m "update" && git push). - Wait 1-2 minutes for GitHub Pages to rebuild.
- In the browser, hard-refresh with Ctrl+Shift+R (bypasses cache). The new tree should load.
If viewers tell you “I still see the old version,” they also need a hard refresh.
Part 7 — Add a README with the shareable link
Create a README.md at the repo root so people landing on the repo page have a direct link:
# My Family Tree
An interactive family tree published with [Topola Genealogy Viewer](https://github.com/PeWu/topola-viewer).
## See the tree
https://YOUR_USERNAME.github.io/YOUR_REPO/#/view?url=https://YOUR_USERNAME.github.io/YOUR_REPO/my-tree.gdz&handleCors=falseTroubleshooting
Tree loads but no photos.
Open DevTools (F12) → Network tab → filter by “gdz”. If no request appears, the browser is caching an old version — hard refresh (Ctrl+Shift+R). If the request is a 404, the URL in the url= parameter is wrong (check case, path, filename).
Tree loads but some people show broken image placeholders.
The .gdz is missing those specific photos. Re-run make-gdz.py and check its output for WARNING: ... NOT found on disk.
net::ERR_FILE_NOT_FOUND for blob: URLs in DevTools.
The .ged inside the .gdz references filenames that don’t exist in the ZIP. Make sure you ran the latest make-gdz.py output, not a hand-built ZIP.
Google indexes my tree and I don’t want it to.
Add a robots.txt at the repo root with:
User-agent: *
Disallow: /
Major search engines respect it. Not a privacy control, but keeps the URL out of search.
The .gdz is over 100 MB.
Resize photos before packaging. The viewer only needs ~1000 px on the long side. A one-liner with ImageMagick:
mogrify -resize 1200x1200> -quality 85 *.jpg
Repo folder on disk and GitHub URL don’t match case. Rename the repo in GitHub (Settings → General → Repository name). Then update every URL you shared (including the one in your README).
Privacy considerations
GitHub Pages on free accounts means your tree is public. Anything in the .gdz is accessible to anyone with the URL. Mitigations, in increasing strength:
Lowest effort — security through obscurity:
- Use a non-descriptive repo name (not
prats-family-tree, something liketree-app). - Add
robots.txtso search engines don’t index it. - Only share the URL privately. 99% of people will never find it.
Moderate — minimize sensitive content:
- In Gramps, mark living people as private (right-click person → Edit → check “private”).
- At export, enable “Restrict data on living people”.
- Resize photos aggressively so they’re not high-value standalone material.
Strong — real access control:
- Move hosting from GitHub Pages to Cloudflare Pages + Cloudflare Access (free up to 50 users). You add a list of emails that can log in; everyone else gets blocked. Same
.gdz, different hosting. - Or GitHub Pro ($4/month) with a private repo + Pages, which keeps the source hidden but the published site is still public URL-wise.
Strongest — don’t publish:
- Build
make-gdz.pylocally, send the.gdzby email/WeTransfer, and open it with Topola Viewer at https://pewu.github.io/topola-viewer/ using “Load from file”. Nothing lives on the public internet.
Attribution
- Gramps — GPL-licensed genealogy software.
- Topola Viewer by PeWu — Apache 2.0.
- GitHub Pages — hosting.
This howto is written to be reused freely. No warranty.