Skip to main content

rynz

rynz turns a folder of Markdown files into a plain, fast website. No JavaScript, no tracking, no build chain: one command and you have HTML you can host anywhere.

It powers nih.ar, a personal site that has been online since 2011.

  • Write in Markdown. Tables, footnotes, task lists and highlighted code work out of the box.
  • Plain HTML out. Zero JavaScript, and no CSS classes unless you add them.
  • Yours to change. Every template and stylesheet is a normal file you can override.

Contents


Quick start

You need Python 3.10 or newer.

pip install rynz
rynz new my-site
cd my-site
rynz serve

Open http://127.0.0.1:5555. You'll see a working site with a sample post. Every time you save a file, rynz rebuilds the site; refresh the browser to see the change.

When you're ready to publish:

rynz build

The finished site is in public/. Upload that folder to any web host.


Writing a post

rynz add "My first post"

This creates content/note/my-first-post.md:

---
title: "My first post"
description: ""
date: 2026-10-05
tags: [note]
draft: true
---

Start writing here.

The part between the --- lines is the frontmatter: settings for this page. Everything below it is your post, written in Markdown.

A new post starts as a draft: rynz serve shows it, rynz build leaves it out. Delete the draft: true line when you want to publish it.

Posts and pages. A file with the note tag is a post: it appears in the list on your home page and in the RSS feed. A file without it is a page, like an About page: it gets built, but isn't listed.

Frontmatter fields

Only title is needed. Everything else is optional.

Field What it does Example
title Page title title: "Hello"
description One line shown in search results, social previews and the feed description: "Why I self-host"
date When it was first published date: 2026-10-05
updated When you last changed it meaningfully updated: 2026-11-01
tags Tags; note makes it a post tags: [note, linux]
draft Leave it out of rynz build draft: true
slug Change the file name in the URL slug: hello
template Use a different template for this page template: wide.html
image Image for social previews image: /img/cover.png
aliases Old URLs that should redirect here aliases: [old-name]
noindex Ask search engines to skip it noindex: true
nofeed Keep a post out of the feed nofeed: true
toc Show a table of contents toc: true
head Extra HTML for this page's <head> head: '<meta name="x" content="y">'

Any other field you add is kept and available in templates as page.extra.<name>.


Project layout

my-site/
├── config.yml        site settings
├── content/          your writing
│   ├── home.md       text at the top of the home page
│   ├── about.md      a page  → about.html
│   └── note/         posts (any folder depth works)
│       └── hello-world.md
├── data/             YAML files available to templates as site.data
├── static/           files copied as-is: images, CSS, favicon
├── template/         your own templates (override the theme's)
└── public/           the built site — created by `rynz build`

Configuration

config.yml needs just two lines:

title: My site
url: https://example.com

Everything else has a sensible default. Here is a fuller example with the common options:

title: My site
url: https://example.com
description: Notes on Linux, homelabs and banking software.
author: Your Name
language: en
image: /img/card.png        # default social preview image
theme: default

menu:                        # links in the header
  - name: about
    url: /about.html
footer_menu:                 # links in the footer
  - name: feed
    url: /rss.xml

urls: flat                   # flat: /about.html · clean: /about · pretty: /about/
tags: true                   # a page per tag at /tag/<name>.html
archive: true                # /archive.html with posts grouped by year
gemini: false                # also build a Gemini capsule

markdown:
  highlight:
    style: monokai           # any Pygments style name

All options

Option Default What it does
title, url — Required
description, author, email, language, image empty, en Used in meta tags and the feed
theme none default, or a path to your own theme folder
menu, footer_menu [] Lists of {name, url} links
urls flat How pages are linked: flat, clean or pretty
paths.content / templates / static / output / data content, template, static, public, data Folder names
partials.home / header / footer content/home.md … Markdown snippets used by templates
templates.home / note / list home.html, note.html, list.html Which templates to use
posts.tag note The tag that makes a page a post
markdown.extras tables, footnotes, fenced code… markdown2 extras to enable
markdown.highlight on, monokai Code highlighting; highlight: false turns it off
markdown.strip_classes true Remove the CSS classes Markdown adds
markdown.lazy_images true Add loading="lazy" and image sizes
feed on, rss.xml output, limit, full_content, tag_feeds
sitemap, robots on sitemap.xml and robots.txt
tags, archive, opml, gemini off Extra pages; set to true to turn on
redirects on Redirect pages for aliases:
reading_speed 200 Words per minute for reading time
plugins [] Your plugins (see Plugins)

Run rynz config to see every value your site is actually using.


Commands

Command What it does
rynz new my-site Start a new site in my-site/
rynz add "Title" Create a new post (as a draft)
rynz serve Preview at http://127.0.0.1:5555; rebuilds when you save. -p 8080 for another port
rynz build Build the site into public/. --drafts, --incremental, --check
rynz check Find broken links, missing titles and images without alt text
rynz config Show your full configuration and check it for mistakes
rynz migrate Upgrade a rynz 1.x config.yml to the new key names
rynz init-ci gitlab Write a deploy file: gitlab, cloudflare or rsync

Add -q for quiet output or --verbose for every step. rynz -C path/to/site build runs in another folder.


Templates and themes

rynz looks for each template in this order:

  1. your template/ folder
  2. the theme named in config.yml
  3. rynz's built-in templates

So to change one thing, copy just that file into template/ and edit it. To change the look, put your own nss.css in static/.

The default theme's files are base.html, home.html, note.html, list.html, partials/head.html, partials/nav.html, partials/footer.html, feed.xml, sitemap.xml and robots.txt.

What templates can use

Variable Contains
page.title, page.description, page.date, page.updated, page.tags From frontmatter
page.html The page's content as HTML
page.url, page.permalink Link (/about.html) and full address
page.toc, page.reading_time, page.words Table of contents, minutes, word count
page.prev, page.next Older and newer post
site.posts, site.pages Every post / every page, newest first
site.tags, site.years Posts grouped by tag / by year
site.data.<file> Contents of data/<file>.yml
site.home, site.header, site.footer The Markdown snippets, as HTML
config Everything in config.yml

Filters: date("%d %b %Y"), iso, rfc822, xml, absolute_url.


Plugins

A plugin is a Python file with a register function. List it in config.yml:

title: My site
url: https://example.com
plugins:
  - plugins/humans.py

plugins/humans.py:

def register(rynz):
    @rynz.generator
    def humans(site, render):
        yield "humans.txt", f"Written by {site.config['author']}\n"

    @rynz.hook("html")
    def highlight_todos(html, page, site):
        return html.replace("TODO", "<mark>TODO</mark>")
Hook Runs Arguments
config after config.yml is read config
page after a page's frontmatter is read page, site
html after Markdown becomes HTML; return the new HTML html, page, site
site after posts and tags are collected site
context before a page's template renders context, page, site
done after every file is written site, written

Also available: @rynz.generator to add files (yield path, content) and @rynz.filter("name") to add a template filter. rynz's own feed, sitemap, tags, archive, redirects, OPML and Gemini output are built with this same API; see rynz/features/ for examples.


Deploying

rynz init-ci writes a ready-to-use deploy file:

Where Command Notes
GitLab Pages rynz init-ci gitlab Builds and publishes on every push to your main branch
Cloudflare Workers rynz init-ci cloudflare Add CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as CI variables
Your own server rynz init-ci rsync Then DEST=user@host:/srv/www ./deploy.sh

Every option runs rynz build --check first, so a broken link stops the deploy.


Upgrading from rynz 1.x

Your 1.x site builds with 2.0 as it is. rynz prints a warning listing the old config keys, and pages come out the same except for intended fixes:

  • Markdown tables, fenced code and lists directly under a bold line now render properly.
  • Headings get ids, so you can link to them.
  • The feed's <description> is filled from your config.
  • sitemap.xml and robots.txt are added.

When you're ready, run:

rynz migrate

It renames the old keys in config.yml (keeping your comments) and saves a backup as config.yml.bak. Renamed commands: create → new, deploy → build, test → check. The old names still work in 2.0. rynz save was removed; use git directly.


FAQ

Does the output really have no JavaScript? Yes. Not on your pages, not in rynz serve. That's why you refresh the browser yourself after a rebuild.

Why does code highlighting use style="" instead of classes? So highlighted code works with class-free stylesheets. Turn it off with markdown: {highlight: false}.

Can I use my own CSS framework? Yes. Put your stylesheet in static/ and your templates in template/.

Where do I report a bug? At gitlab.com/niharokz/rynz/-/issues.


Changelog · Contributing · MIT License · made by Nihar

Metadata

Release files for rynz 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rynz 2.0.0
File Size Uploaded Provenance Actions
rynz-2.0.0.tar.gz 53.5 kB Without provenance Details for rynz-2.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for rynz 2.0.0
File Interpreter ABI Platform Actions
rynz-2.0.0-py3-none-any.whl Python 3 none any Details for rynz-2.0.0-py3-none-any.whl

Total release size: 107.5 kB

Release files / rynz-2.0.0.tar.gz

Download URL rynz-2.0.0.tar.gz
Size 53.5 kB
Tags Source
SHA-256 checksum
How to use checksums
088fbdc5c6f81f46a7c244a16f653ec4a99c97ff882d6f271ebd5699507edf04
BLAKE2b-256 checksum
How to use checksums
0d9d00f0a511970a41cd07bedf6af0e5a28491950d04a0f57a3ff2aef50fe90b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / rynz-2.0.0-py3-none-any.whl

Download URL rynz-2.0.0-py3-none-any.whl
Size 54.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a1d6a25ee480b01efc297347b07c3b327e9d49d80c3612e96fe79899f22d8d15
BLAKE2b-256 checksum
How to use checksums
ab6140ade1050a0fffb531bae60bfe2984054785c93cc2e096ccb01905d911d2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for rynz 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page