genster

March 16, 2026 · View on GitHub

go.dev reference Check Status Test Status

genster generates a static family history website from a GEDCOM or Gramps database. It uses a two-step pipeline: gen writes markdown content files, then build renders them into a complete HTML site ready to serve or rsync to a server.

I created it for myself so it makes many assumptions about my particular style of using GEDCOM, especially around Ancestry exports.

Installation

As of Go 1.19, install the latest genster executable using:

go install github.com/iand/genster@latest

This will download and build a binary in $GOBIN.

Workflow

genster gen   --gedcom family.ged --config mytree.kdl --output content/
genster build --content content/ --pub pub/
npx serve pub/           # or: rsync pub/ user@host:/var/www/html/

Step 1 — gen reads the genealogy data and writes markdown files with YAML front matter into the content directory. These files represent every person, place, source, family, citation, list page, and chart in the tree. Manual content (the research diary, stories, a home page) lives alongside generated files in the same content directory.

Step 2 — build walks the content directory, parses each markdown file, renders the body through goldmark, applies an HTML template based on the layout front-matter field, and writes complete HTML pages to the pub directory. Non-markdown files (images, media) are copied verbatim.

The pub directory is self-contained and can be deployed directly with rsync, scp, or any static hosting service.


Commands

genster gen — generate content from genealogy data

Reads a GEDCOM or Gramps file and writes markdown content files to a directory. Exactly one of --gedcom or --gramps must be supplied.

FlagShortDescription
--gedcom <file>-gGEDCOM file to read
--gramps <file>Gramps XML file to read
--gramps-dbname <name>Name of the Gramps database, used to keep IDs stable across exports
--config <file>-cPath to the KDL tree configuration file (required; see Tree configuration file)
--output <dir>-oDirectory to write generated content files into
--basepath <path>-bURL path prefix for all links (default /)
--key <id>-kID of the key individual; sets the anchor person for relation filtering and ancestor charts
--relation <mode>Filter which people get pages: any (default), common (must share a common ancestor with key person), or direct (must be a direct ancestor)
--include-privateInclude living people and those who died within the last 20 years (normally redacted)
--wikitreeGenerate WikiTree markup on person pages for copy-and-paste
--inspect <type/id>Print the internal data structure for one object (e.g. person/I123) and exit
--debugEmbed debug information as inline HTML comments
--verbose / --veryverboseIncrease log verbosity

Place maps

When the MAPTILER_API_KEY environment variable is set and a place has coordinates, gen downloads a static map image and embeds it inline on the place page. Maps are sourced from MapTiler Cloud, which hosts the National Library of Scotland historic map layers as well as OpenStreetMap raster tiles.

The map layer and zoom level are chosen automatically based on place type:

CoverageLayerUsed for
UK — smaller placesOrdnance Survey six-inch to the mile, 1888–1913Street, building, address, hamlet, village, parish, burial ground
UK — larger placesOrdnance Survey one-inch Hills edition, 1885–1903Town, city, county, country
Greater LondonOrdnance Survey five-foot to the mile, 1893–1896Any place in the London hierarchy
IrelandBartholomew quarter-inch to the mile, 1940Republic of Ireland and Northern Ireland
ElsewhereOpenStreetMapPlaces outside UK and Ireland coverage

Each place map includes a downward-pointing arrow marking the place's coordinates and a figcaption with a link to the interactive map viewer (NLS Geo/Explore for historic layers, OpenStreetMap for the OSM fallback).

Tile cache — individual map tiles are cached in $XDG_CACHE_HOME/genster/maptiles/ (defaulting to ~/.cache/genster/maptiles/ when XDG_CACHE_HOME is not set), organised as {layer}/{z}/{x}/{y}.jpg. Cached tiles are reused on subsequent runs.

Image cache — stitched 800×600 JPEG map images are cached in $XDG_CACHE_HOME/genster/maps/ as place-map-{id}.jpg. If the cached image exists it is copied directly to the output media directory without re-downloading any tiles. Delete a cached image to force it to be regenerated.

genster build — render content to HTML

Walks a content directory and renders every markdown file into a complete HTML page.

FlagShortDescription
--content <dir>-cContent directory to read (required)
--pub <dir>-pOutput directory for rendered HTML (required)
--assets <dir>-aDirectory of static assets (CSS, JS) to copy into pub; embedded defaults used when not set
--base-url <url>Scheme and host for absolute URLs in sitemap.xml (e.g. https://example.com); sitemap is omitted when not set
--include-draftsPublish pages marked draft: true
--verbose / --veryverboseIncrease log verbosity

genster chart — generate a standalone family tree chart

Produces an SVG family tree chart directly from a GEDCOM or Gramps file without generating a full site. Chart types: descendant, ancestor, butterfly, fan, focus.

genster report — produce a text report

Outputs a plain-text descendant or familyline report to stdout.

genster annotate — annotate diary markdown files

Walks a directory of hand-authored markdown files and replaces bare footnote references ([^label]) with fully-rendered citation links drawn from the genealogy database. Pass --undo to strip the generated citations and restore original syntax.


Tree configuration file

All commands that load genealogy data accept --config/-c pointing to a KDL 1.0 file. The file has three top-level nodes: tree, surname-groups, and annotations.

tree — tree identity and description

tree id="weston" {
    name "The Weston and Pryor Family Tree"
    description r#"
        The Westons were a farming family from rural Shropshire, England, working
        the same land from the mid-17th century until the agricultural depression
        of the 1870s drove the younger sons into the Midlands coalfields.

        The Pryors came from South Wales and moved to Birmingham in the 1890s,
        where Thomas Pryor met and married Alice Weston in 1903.
        "#
}
Child nodeDescription
idShort identifier used in URL paths (e.g. trees/weston/)
nameDisplay name shown on the tree overview page
descriptionMulti-paragraph description rendered on the tree overview page. Use a raw string (r#"..."#) for multi-line text. Blank lines produce paragraph breaks.

id may also be written as a property on the tree node: tree id="weston" { ... }.

surname-groups — variant surname groupings

Groups spelling variants under a single canonical surname for surname list pages and relationship display.

surname-groups {
    Weston "Wheston" "Westen"
    Pryor "Prior" "Pryer" "Priar"
    Griffiths "Griffith" "Gryffith"
    Hughes "Hughs" "Hewes"
}

Each child node names the canonical surname; its arguments are the variants that should be merged into that group.

annotations — corrections and additions

Overrides and supplements data from the source GEDCOM or Gramps file. Organised into people, places, and sources sections.

annotations {
    people {
        person id="A3KMNP2XWQR8T" {
            // Thomas Weston (1847-1921)
            nickname "Tom"
            olb "Agricultural labourer turned coal miner from Shropshire."
            wikitreeid "Weston-4521"
        }
        person id="B7YQZR4VNDX2L" {
            // Alice Pryor (1882-1954)
            preferredgivenname "Alice"
            preferredfamilyname "Weston"
        }
        person id="C9WLFT6HJMS3K" {
            // Living relative
            redacted true
        }
        person id="D2PXNK8GRVC5M" {
            // William Griffiths (1801-1879)
            tags "Direct Ancestor" "Shropshire"
        }
    }

    places {
        place id="E5QRTB9YNWJ7H" {
            // Shifnal, Shropshire — missing coordinates
            latlong "52.669, -2.368"
        }
        place id="F1MVZK3DCXP4G" {
            // Historical variant name
            name "Brewood, Staffordshire, England"
        }
    }

    sources {
        source id="G8HNWQ2YBXR6T" { iscivilregistration }
        source id="H4KCJP7FMVZ3L" { iscivilregistration }
        source id="J6TRWB5SDXN9Q" { iscensus }
        source id="K2LQMF8PVYC4H" { iscensus }
        source id="M9ZXDN3JWKR7B" {
            title "Shropshire Parish Records 1600–1900"
            repositoryname "Shropshire Archives"
        }
    }
}

Person annotation fields

All fields use replace semantics (overwrite the loaded value) except tags, which appends.

FieldTypeDescription
nicknamestringPreferred nickname
olbstringOne-line biography
epithetstringDistinguishing epithet (e.g. "the Elder")
preferredfullnamestringOverride full display name
preferredgivennamestringOverride given name
preferredfamiliarnamestringOverride informal given name
preferredfamiliarfullnamestringOverride informal full name
preferredfamilynamestringOverride family name
preferredsortnamestringOverride sort key
preferreduniquenamestringOverride unique display name
wikitreeidstringWikiTree profile ID
possiblyaliveboolMark as possibly still living
unmarriedboolMark as known to have never married
childlessboolMark as known to have had no children
illegitimateboolMark as born outside marriage
redactedboolSuppress this person from the published site
featuredboolHighlight this person
tagsstring or listAppend one or more tags

Place annotation fields

FieldTypeDescription
namestringOverride place name
latlongstringSet coordinates as "lat, long" (e.g. "52.669, -2.368")
tagsstring or listAppend one or more tags

Source annotation fields

FieldTypeDescription
titlestringOverride source title
searchlinkstringURL for searching this source online
repositorynamestringOverride repository name
repositorylinkstringURL for the repository
iscivilregistrationboolMark as a civil registration record
iscensusboolMark as a census record
isunreliableboolMark as an unreliable source
tagsstring or listAppend one or more tags

Content directory layout

The content directory holds both generated and hand-authored files. gen writes into trees/<tree-id>/; manual content sits at the top level alongside it.

content/
├── index.md                          # site home page  (layout: home)
├── images/                           # generic feature images — see below
├── diary/                            # hand-authored research diary
│   ├── index.md                      # diary home      (layout: diary)
│   └── 2024/
│       ├── index.md                  # year index      (layout: diary)
│       └── 2024-03-15/
│           └── index.md              # daily entry     (layout: single)
├── stories/                          # hand-authored narrative articles
│   ├── index.md
│   └── my-story/
│       └── index.md                  #                 (layout: single)
└── trees/
    └── <tree-id>/                    # one subtree per --id value
        ├── index.md                  # tree overview   (layout: treeoverview)
        ├── person/<id>/index.md      #                 (layout: person)
        ├── place/<id>/index.md       #                 (layout: place)
        ├── source/<id>/index.md      #                 (layout: source)
        ├── citation/<id>/index.md    #                 (layout: citation)
        ├── family/<id>/index.md      #                 (layout: family)
        ├── chart/<id>/index.md       #                 (layout: chartancestors)
        └── list/
            ├── people/               #                 (layout: listpeople)
            ├── surnames/             #                 (layout: listsurnames)
            ├── places/               #                 (layout: listplaces)
            ├── sources/              #                 (layout: listsources)
            ├── todo/                 #                 (layout: listtodo)
            ├── changes/              #                 (layout: listchanges)
            ├── anomalies/            #                 (layout: listanomalies)
            ├── inferences/           #                 (layout: listinferences)
            ├── families/             #                 (layout: listfamilies)
            └── familylines/          #                 (layout: listfamilylines)

The build command also generates /tags/ pages automatically from front-matter tags — do not create these manually.

Generic feature images

build looks for generic images in content/images/. These are never embedded in the binary, so you can supply your own. The expected filename conventions are:

Filename patternUsed for
person-{gender}-{era}-{trade}-{maturity}.webpMost specific person silhouette
person-{gender}-{era}-{trade}.webpPerson by gender, era, and trade
person-{gender}-{era}-{maturity}.webpPerson by gender, era, and maturity
person-{gender}-{era}.webpPerson by gender and era
person-{gender}.webpPerson by gender only
place-{placetype}.webpPlace by type (e.g. place-village.webp, place-parish.webp)
place-building-{kind}.webpBuilding by kind (e.g. place-building-church.webp)
place-building.webpBuilding fallback
place.webpPlace final fallback
section-diary.webpDiary section
section-stories.webpStories section
section-trees.webpTrees list page
section-search.webpSearch page
category-people.webpPeople list
category-surnames.webpSurnames list
category-place.webpPlaces list
category-sources.webpSources and citations
category-todo.webpTo-do list
default-oak.webpFinal fallback for all other pages

For person pages, build tries the five person patterns from most to least specific and uses the first file it finds. If nothing matches, no image is rendered.

Person-specific photos (actual photographs from Gramps) are set via the image: front-matter field by gen and take priority over all generic images.


Front-matter fields

Every content file begins with a YAML front-matter block delimited by ---. gen sets these automatically on generated pages; hand-authored files may set any field explicitly.

Core fields

FieldTypeDescription
titlestringPage title, shown in <h1> and <title>
layoutstringTemplate to use (see Layouts below)
idstringStable identifier for the entity this page represents
summarystringShort description used in meta tags and section listings
draftboolWhen true, excluded from build output unless --include-drafts is set
lastmodstringLast-modified date YYYY-MM-DD, shown in the page footer
aliaseslistAdditional URL paths that redirect to this page
tagslistTag names; build generates a /tags/<slug>/ page for each
imagestringURL of a person-specific photo; takes priority over all generic silhouettes

Tree navigation

FieldTypeDescription
basepathstringBase URL of the tree this page belongs to (e.g. /trees/mytree/); drives the per-tree nav bar

Pagination (list pages only)

FieldTypeDescription
firststringStem of the first page in the sequence
laststringStem of the last page
nextstringStem of the next page
prevstringStem of the previous page

Person-specific fields

FieldTypeValuesDescription
categorystringpersonMarks this as a person page
genderstringmale, female, unknownUsed to select silhouette
erastring1600s 1700s 1800s 1900s modernDerived from birth/death year
maturitystringchild young mature oldDerived from age at death
tradestringlabourer miner nautical crafts clerical commercial military serviceDerived from occupation group
ancestorbooltrue if this person is a direct ancestor of the key person
grampsidstringGramps handle
slugstringShort alias for diary links (e.g. john-smith/r/john-smith)
diarylinkslist of {title, link}Research diary entries mentioning this person
linkslist of {title, link}External links (Ancestry, FindMyPast, etc.)
descendantslist of {name, link, detail}Ancestor path entries in the sidebar
wikitreeformatstringWikiTree markup (set when --wikitree is used)

Place-specific fields

FieldTypeDescription
categorystringplace
placetypestringType of place: city, town, village, hamlet, parish, county, country, building, street, address, etc.
buildingkindstringKind of building when placetype is building: church, workhouse, farm, hospital, etc.

Source/citation fields

FieldTypeDescription
categorystringsource or citation

Story/diary fields (hand-authored)

FieldTypeDescription
authorstringAuthor name
startedstringDate started
updatedstringDate last updated
statusstringCompletion status (e.g. draft, complete)

Sitemap control

FieldTypeDescription
sitemapmapSet {disable: "1"} to suppress this page from sitemap.xml

Templating system

The build command uses Go's html/template package. All templates live in genster/build/templates/ and are embedded into the binary at compile time.

Template data

Every template receives a PageData value as its dot (.):

type PageData struct {
    FrontMatter        // all front-matter fields promoted onto dot
    Body  template.HTML // rendered HTML from the markdown body
    Tree  TreeData      // tree-level metadata
}

type TreeData struct {
    Title    string // title of the tree's index page
    BasePath string // base URL path (e.g. /trees/mytree/)
}

Because FrontMatter is embedded, all front-matter fields are accessible directly — for example {{.Title}}, {{.Category}}, {{.Gender}}. The embedded struct itself is also accessible as {{.FrontMatter}} when you need to pass it to a template function.

Layouts

The layout front-matter field selects the template. gen sets this on all generated pages; hand-authored files must set it explicitly (or leave it blank for a plain content page with no sidebar).

LayoutTemplateUsed for
personperson.htmlPerson pages
placeplace.htmlPlace pages
sourcesource.htmlSource pages
citationcitation.htmlCitation pages
familyfamily.htmlFamily pages
treeoverviewtreeoverview.htmlTree overview/index
chartancestorschartancestors.htmlAncestor SVG chart
calendarcalendar.htmlMonthly event calendar
listpeoplelistpeople.htmlAlphabetical people list
listsurnameslistsurnames.htmlSurnames list
listplaceslistplaces.htmlPlaces list
listsourceslistsources.htmlSources list
listtodolisttodo.htmlResearch to-do list
listchangeslistchanges.htmlRecently updated pages
listanomalieslistanomalies.htmlData anomalies
listinferenceslistinferences.htmlInferences
listfamilieslistfamilies.htmlFamilies list
listfamilylineslistfamilylines.htmlFamily lines list
listtreeslisttrees.htmlAll trees
homehome.htmlSite home page
diarydiary.htmlDiary section index and year index pages
singlesingle.htmlSimple content pages (stories, diary entries, tag pages)
listlist.htmlGeneric paginated list
searchsearch.htmlSearch page
(empty)plain.htmlPlain content, no sidebar

Shared partials (base.html)

Layout templates compose these named blocks via {{template "name" .}}:

PartialDescription
head<head> element: charset, title, meta description, viewport, CSS links
site-headerTop nav: home, trees, diary, stories, tags
tree-headerLike site-header plus a per-tree nav row (people, surnames, places, to-do, recent updates) when .Tree.BasePath is set
footerPage footer with last-modified date when .LastMod is set
scriptsJavaScript includes
featureimageSidebar image: real photo when .Image is set; otherwise best available generic image from content/images/ via cascade; nothing when no match; shows "representative image" caption on person pages with a generic silhouette
tagsSidebar tag list linking to /tags/<slug>/
paginationPrevious/next/first/last nav for paginated list pages

Template functions

FunctionDescription
urlize sLowercase and replace spaces with hyphens — used to build tag URL slugs
ukdate sFormat YYYY-MM-DD as 2 January 2006; returns input unchanged if unparseable
featureImageSrc fmGiven a FrontMatter value, return the best available feature image URL by checking content/images/; returns "" when nothing matches

Customising templates

Override the embedded templates by passing --assets <dir> to build, where <dir> contains a templates/ subdirectory with replacement .html files. Only the files you provide are replaced; all others use the built-in versions.

To add new template functions, add them to buildSiteTemplates in genster/build/template.go and reference them from a template file.


GEDCOM conventions

Genster understands several Ancestry-specific GEDCOM extensions:

  • _APID — Ancestry source citation identifier, translated to an Ancestry URL
  • _TREE — Ancestry tree reference in the GEDCOM header

Custom EVEN fact labels with specific handling:

  • Nickname — preferred nickname for the person
  • OLB — one-line biography: a short sentence summarising the person's life

Events whose values begin with certain phrases are included verbatim in the generated narrative:

  • He was recorded as
  • She was recorded as
  • It was recorded that

License

This is free and unencumbered software released into the public domain. For more information, see http://unlicense.org/ or the accompanying UNLICENSE file.