An extensible, programmable, static site generator for Gemini first.
Go to file
pine c8d6509c71
fixed problem with specified template and error
just found a small edge case that broke metadata working on files with manually specified templates, and corrected a spelling mistake that resulted in an error
2021-07-24 19:11:09 +00:00
.gitignore Bug fix / changed the way messages output 2021-04-30 11:12:39 -06:00
LICENSE first commit / upload 2021-04-29 12:48:21 -06:00 first commit / upload 2021-04-29 12:48:21 -06:00 first commit / upload 2021-04-29 12:48:21 -06:00 fixed problem with specified template and error 2021-07-24 19:11:09 +00:00

Vanity Press

An extensible, programmable, static site generator for Gemini first.


  • Extensible - Users can create their own functions and variables to use on their site
  • Templates
  • Inline bash commands / python commands
  • Metadata, which can be used in templates or user-created functions
  • Tags
  • Atom feeds

Quick Start

  • clone this repository
  • edit with information like your site name, url, etc (this will be used to create atom feeds)
  • see Documentation for basic usage
  • run the script (do chmod +x on Unix systems to mark it as executable, then run it with ./ Alternatively, you could also run it with python



  • you modify with configuration options (" variables)
  • you apply basic metadata to desired files for use in functions and templates ("Metadata")
  • you can create templates for files ("Creating Templates") variables

  • SOURCE_DIR: the folder where you write your content in
  • DEST_DIR: the folder where vanity creates its output files (the location of the generated site)
  • TEMPLATE_DIR: where you store template files
  • TEMPLATE_EXTENSIONS: an array of file extensions. Vanity will only look at files with these extensions when doing things like templating or creating dynamic content. Files with a different extension will be hard-linked instead.
  • IGNORE_PREFIX: folders starting with this string and all of their contents will not be added to the site. Files, however, WILL be added but won't show up in functions like {{ atom }}, {{ index }}, or {{ recent }}
  • replace_table: a dictionary where you can define your own variables and functions to use on your site. Furthermore, some optional values are pulled from here to generate atom feeds.


Metadata is data appended to the start of files (metadata listed after page content begins will not be used) which provides information about the page. This info can be used by templates and functions. For example, this could be the date published, title, or tags associated with the page. Metadata names are case-insensitive and are preceded by the & character. A : delimiter is then used to separate it from the value. Metadata is not shown on the destination files.

For example, metadata for a simple page may look like this:

&TITLE:Vanity Press: A new static site generator for Gemini
&TAGS:Programming, Gemini
Lorem ipsum dolor asset. This is the page content

Metadata is used in a few functions:

  • &TITLE is used by {{ atom }} for page titles and {{ index }} for link titles (defaults to the file name if not provided).
  • &PUB_DATE is used by {{ index }} for specifying the page's publish date in link titles.
  • &TAGS is used by {{ tags }} to organize content based on tag

Users can also use metadata in templates and define their own metadata. For example, consider the file poem.gmi:

&AUTHOR:Percy Shelly
Lorem ipsum. Poem.

and the template text.template:

# {{ &title }}
## by {{ &author }}
{{ python:if {{ &poetry }}:out = '\n```' }}
{{ python:if {{ &poetry }}:out = '\n```' }}

which automatically wraps poems in pre-formatted text blocks and specifies their title and author.

Current functions / variables

Functions and variables are written in-line in the source directory files with the syntax {{ foo }} or {{ foo:args }} (foo is the name of a function and args are a comma separated list of arguments). The spacing between brackets does not matter, so {{ foo}}, {{foo}}, and {{foo }} all work the same.

Below is a list of currently defined functions / variables that you can use. See "Extending Features" for how to create your own.

CURRENT_DIR: outputs the source directory where it's being called from

CURRENT_FILE: outputs the source file path where it's being called from

atom: outputs an atom feed for n most-recent files in the specified directory

  • calling {{ atom }} returns an atom feed for every file in the current directory ending with an extension listed in TEMPLATE_EXTENSIONS
  • {{ atom:n }} changes it to the n most-recent files
  • {{ atom:n,dir }} changes the directory to dir (absolute path), no matter where the function is called from

date: outputs the current date in the specified format

  • calling {{ date }} returns the current date in the format "YYYY-mm-dd" (ISO 8601)
  • {{ date:format }} changes the format to format. This is a string like %Y-%m-%d, that can be used in Python's strftime function

index: outputs a list Gemini links for n most-recent files in the specified directory

  • calling {{ index }} returns a list of Gemini links (formatted => url pub_date title) for every file in the current directory. pub_date and title will be fetched from the file's metadata if provided. If title isn't provided, it'll be replaced with the file name.
  • {{ index:n }} changes it to the n most-recent files
  • {{ index:-n }} changes it to the n oldest files. {{ index:-0 }} will list all files without limit, oldest to newest
  • {{ index:n,dir }} changes the directory to dir (absolute path) no matter where the function is called from

recent: outputs a list of Gemini links for n most-recent files in the specified directory

  • {{ recent }} behaves exactly like {{ index }} does, with the exceptions that the default, unspecified count is 10 (whereas {{ index }} is 0, no limit), and that links are formatted as => url date_modified file name

last_updated: returns the modification date of the specified file in the format "YYYY-mm-dd" (ISO 8601)

  • {{ last_updated }} returns the modification date of the current file

python: computes the specified python command. Outputs whatever the variable out is defined as.

  • example: {{ python:import random; out="Hello" if random.random() < 0.5 else "World" }} would either return "Hello" or "World" with 50/50 odds.
  • You can use other functions, variables, and metadata in {{ python }}. For example, {{ python:out="{{ &title }}".upper() }} would output the title defined in metadata in all upper case.

shell: captures the output of the specified shell command

  • IMPORTANT: Shell commands are executed in your shell, that's how they work. They are therefore inherently dangerous; calling something like {{ shell:rm -rf {{ CURRENT_DIR }} }} would execute and delete all of the files in the current working directory. Use with caution!
  • example: {{ shell:ls | sed 's/^.*/=> & &/' }} could be used to create a quick-and-dirty index file

tags: outputs a list of tags used (tags found via metadata) in the specified directory. In the destination directory, creates the folder tag in the relative path

  • example: calling {{ tags }} in ./gemlog/index.gmi would output something like:
=> tag/cooking.gmi cooking (3)
=> tag/essays.gmi essays (2)
=> tag/programming.gmi programming (5)

and create the tag index files cooking.gmi, essays.gmi, programming.gmi in DEST_DIR/gemlog/tag/. These files by default look like:

# cooking

=> /gemlog/pasta.gmi pasta.gmi
=> /gemlog/banana_bread.gmi Cooking Banana Bread at Home
=> /gemlog/vegan_chili.txt My Vegan Chili Recipe

where the titles are pulled from the metadata if available, and then default to the file name.

  • You can modify the output/look of tag index files by creating the tag.template file in your TEMPLATE_DIR. See the Creating Templates section further on for more information.
  • Calling {{ tags }} and creating the template file is usually enough to handle tags on your gemlog without any further configuration

words: outputs the word count for the current file. Takes no arguments

Creating Templates

Templates are ways for you to reuse the same content/format on multiple pages without having to manually add it every time.

Templates are files with the extension .template stored in TEMPLATE_DIRS. Templates can be assigned automatically or manually. For example, the template foobar.template will automatically be applied to all files in any directories named foobar.

Manually specifying a template must be done in the first line of a file. Writing !TEMPLATE:foobar in the first line (followed immediately by a newline) will apply the foobar.template template (if it exists). You can also use !TEMPLATE:none, meaning that no template will be applied even if one would normally be used automatically.

Templates can use file metadata and defined functions / variables. Templates allow you to append prefixes / suffixes to files. For example, consider the following template:

# {{ &title }}


Last updated {{ last_updated }}
Published on {{ &pub_date }}

=> / home

The title is appended before the article, sourced from the original file's metadata (&title). At the end, a footer is applied using metadata (&pub_date), a function (last_updated), and just regular text (the home link). !CONTENT is then replaced by the content of the original file.

Tag Templates

The file tag.template, if created, modifies how tag index pages look. If not created, this default value is used:

# {{ tag }}


where {{ tag }} is replaced by the name of tag (i.e, "cooking", or "programming", etc)

Extending Features

Users can create their own variables and functions to use on their site by altering the dictionary replace_table in An example is provided there to guide you further. Custom functions are written in Python in the file. (although you could use something like the {{ shell }} function to execute your own files).


If you have a feature you'd like to request (or even better, to implement) I would be ecstatic to hear your input. Either make a pull request or let me know at my email, ponderosapinetree [ at ] Thanks!

If you're interested in taking a peek (I encourage you to do so), the file is heavily marked up with comments for both your and my understanding. However, followers of any code style guide will weep at the one-liners and inconsistencies that I've used. There is no 80-character line limit, there is no God here, let ye who pass through these parts be warned.


All files in this repository are liscenced under CC0. Feel free (and please) change files / extend this for your use. If you do change something / add a feature that you think others may use as well, why not make a pull request, too?