Genny
A static site generator built with .NET that transforms your HTML pages and assets into a production-ready website.
For people that want to generate websites and don't want a headache of configuring a complex static site generator. Plays nicely with JavaScript asset management tools like Vite/Parcel/Webpack if that's what you want.
Quick Start
Using Docker (easiest):
# Pull the image docker pull ghcr.io/benbristow/genny:latest # Build your site (run from your project root) # Use --user flag to run as current user (prevents permission issues with build directory) docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest build
Or create an alias:
alias genny='docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest' genny build
Note: The --user "$(id -u):$(id -g)" flag ensures the container runs as your current user, so the build/ directory will have the correct permissions and you won't need sudo to modify files.
Building locally:
If you prefer to build the image locally:
# Build the image locally (one-time setup) docker build -t genny:latest https://github.com/benbristow/genny.git # Build your site (run from your project root) # Use --user flag to run as current user (prevents permission issues) docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace genny:latest build
Features
- ๐ Simple and Fast - Build static sites quickly with minimal configuration
- ๐ Layout System - Reusable layouts with content placeholders
- ๐งฉ Partials Support - Include reusable HTML snippets in layouts and pages
- ๐ Organized Structure - Clean separation of pages, layouts, and assets
- ๐จ Asset Management - Automatic copying of public assets
- ๐งน Smart Filtering - Automatically ignores common development files
- โ๏ธ TOML Configuration - Simple configuration file format
Installation
Using Docker (Recommended)
Pull the Docker image from GitHub Container Registry:
docker pull ghcr.io/benbristow/genny:latest
Or use a specific version:
docker pull ghcr.io/benbristow/genny:main
Building locally:
Alternatively, build the Docker image locally from the repository:
docker build -t genny:latest https://github.com/benbristow/genny.git
Or clone the repository and build from your local copy:
git clone https://github.com/benbristow/genny.git cd genny docker build -t genny:latest .
Running as Current User:
By default, Docker containers run as root, which means files created in the build/ directory will be owned by root. To avoid permission issues, always use the --user flag:
docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest build
This runs the container with your current user ID and group ID, ensuring all generated files have the correct ownership.
Using the CLI Binary
Download the latest release binary for your platform from the GitHub Actions artifacts or build from source.
Using GitHub Actions
Use the Genny GitHub Action in your CI/CD workflows. See the Using GitHub Actions section below for detailed instructions.
Build from Source
Prerequisites:
- .NET 10.0 SDK or later
git clone <repository-url> cd Genny dotnet build
Getting Started
1. Create a Project Structure
Genny expects the following directory structure:
your-site/
โโโ genny.toml # Site configuration
โโโ pages/ # Your HTML pages
โ โโโ index.html
โ โโโ about.html
โโโ layouts/ # Layout templates (optional)
โ โโโ default.html
โโโ partials/ # Reusable HTML snippets (optional)
โ โโโ header.html
โ โโโ footer.html
โโโ public/ # Static assets (optional)
โโโ style.css
โโโ images/
2. Create Configuration File
Create a genny.toml file in your project root:
name = "My Awesome Site" description = "A static site built with Genny"
3. Create Your First Page
Create pages/index.html:
<body> <h1>Welcome to My Site</h1> <p>This is my homepage.</p> </body>
4. Build Your Site
Using Docker:
# From your project root directory # Use --user flag to run as current user (prevents permission issues) docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest build
Or create an alias for convenience:
alias genny='docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest' genny build
Important: The --user "$(id -u):$(id -g)" flag runs the container as your current user instead of root. This ensures the build/ directory and all generated files have the correct ownership, so you can modify them without sudo.
Using locally built image:
# Use --user flag to run as current user docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace genny:latest build
Using the CLI Binary:
If you've downloaded the binary:
Or if installed globally:
Using .NET directly:
dotnet run --project Genny/Genny.csproj -- build
Verbose output:
Add the -v or --verbose flag for detailed build information:
# Docker (ghcr.io) docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest build -v # Docker (locally built) docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace genny:latest build -v # CLI genny build -v
Your site will be generated in the build/ directory.
Using GitHub Actions
You can use the Genny GitHub Action to automatically build your site in CI/CD pipelines.
Basic Usage:
Create a .github/workflows/build.yml file in your repository:
name: Build Site on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Build site with Genny uses: benbristow/genny-action@v1
With Custom Working Directory:
If your Genny site files are in a subdirectory:
- name: Build site with Genny uses: benbristow/genny-action@v1 with: working-directory: './site'
With Deployment:
You can use the build directory output for deployment:
- name: Build site with Genny id: genny uses: benbristow/genny-action@v1 - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: publish_dir: ${{ steps.genny.outputs.build-directory }}
Available Inputs:
| Input | Description | Required | Default |
|---|---|---|---|
repository |
Repository URL to clone | No | https://github.com/benbristow/genny |
working-directory |
Working directory for the action | No | . |
Available Outputs:
| Output | Description |
|---|---|
build-directory |
Directory where the site was built |
For more information, see the Genny Action repository.
Project Structure
Pages Directory (pages/)
Place all your HTML pages in the pages/ directory. Pages can be organized in subdirectories:
pages/
โโโ index.html # Becomes build/index.html
โโโ about.html # Becomes build/about.html
โโโ blog/
โโโ post.html # Becomes build/blog/post.html
Note: Root-level pages are flattened to the build root, while subdirectory pages preserve their structure.
Layouts Directory (layouts/)
Layouts are HTML templates that wrap your page content. They use the {{content}} placeholder to inject page content.
Default Layout (layouts/default.html):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="{{ site.description }}"> <title>{{ site.name }} - {{ title }}</title> <link rel="stylesheet" href="/style.css"> </head> <body> <header> <h1>{{ site.name }}</h1> <nav> <a href="/">Home</a> <a href="/about.html">About</a> </nav> </header> <main> {{ content }} </main> <footer> <p>© {{ year }} {{ site.name }}</p> </footer> </body> </html>
Available Placeholders:
{{content}}or{{ content }}- The page body content{{title}}or{{ title }}- The page title (extracted from page){{site.name}}or{{ site.name }}- Site name fromgenny.toml{{site.description}}or{{ site.description }}- Site description fromgenny.toml{{year}}or{{ year }}- Current year (e.g., 2025){{epoch}}or{{ epoch }}- Current Unix epoch timestamp in seconds (e.g., 1734201600){{permalink}}or{{ permalink }}- The full URL of the current page
Note: Spaces around placeholder names are optional. Both {{title}} and {{ title }} work the same way.
The {{epoch}} placeholder is useful for cache busting:
<link rel="stylesheet" href="/style.css?v={{ epoch }}"> <script src="/app.js?t={{ epoch }}"></script>
Using a Custom Layout:
Add a comment at the top of your page to specify a custom layout:
<!-- layout: custom.html --> <body> <h1>Custom Page</h1> <p>This page uses a custom layout.</p> </body>
Note: Layout comments are automatically removed from the final output.
Specifying Page Title:
You can specify a page title in two ways:
- Using a comment (recommended):
<!-- title: My Page Title --> <body> <h1>My Page</h1> </body>
- Using a
<title>tag:
<html> <head> <title>My Page Title</title> </head> <body> <h1>My Page</h1> </body> </html>
The title will be extracted and available as {{title}} in your layout. If no title is specified, {{title}} will be replaced with an empty string.
Note: Title comments (<!-- title: ... -->) are automatically removed from the final output. The <title> tag method will also extract the title, but the tag itself will remain in the output if no layout is used.
Layout Behavior:
- If no layout is specified, Genny looks for
default.html - If no layout exists, the page content is used as-is
- Layouts are not copied to the build directory (they're templates only)
- Optional .html Extension: The
.htmlextension is optional when referencing layouts. You can use<!-- layout: custom -->or<!-- layout: custom.html -->- both will findcustom.html. Layout files must be named with the.htmlextension.
Partials Directory (partials/)
Partials are reusable HTML snippets that can be included in layouts, pages, and other partials. They're perfect for components like headers, footers, navigation menus, or any repeated content.
Syntax:
{{ partial: filename.html }}Spaces around the colon are optional: {{ partial : filename.html }} works the same way.
Optional .html Extension: The .html extension is optional when referencing partials. You can use {{ partial: header }} or {{ partial: header.html }} - both will find header.html. Partial files must be named with the .html extension.
Example Partial (partials/header.html):
<header> <h1>{{ site.name }}</h1> <nav> <a href="/">Home</a> <a href="/about.html">About</a> </nav> </header>
Using Partials in a Layout:
<!DOCTYPE html> <html> <head> <title>{{ site.name }} - {{ title }}</title> </head> <body> {{ partial: header.html }} <main> {{ content }} </main> {{ partial: footer.html }} </body> </html>
Using Partials in a Page:
<body> <h1>Welcome</h1> <p>Check out our latest news:</p> {{ partial: news-section.html }} </body>
Nested Partials:
Partials can include other partials. For example, header.html can include nav.html:
partials/header.html:
<header> <h1>{{ site.name }}</h1> {{ partial: nav.html }} </header>
partials/nav.html:
<nav> <a href="/">Home</a> <a href="/about.html">About</a> </nav>
Circular Reference Prevention: Genny automatically prevents circular references (e.g., partial A includes partial B which includes partial A). If a circular reference is detected, the placeholder is removed to prevent infinite loops.
Missing Partials: If a partial file doesn't exist, the placeholder is automatically removed from the output.
Note: Partials are not copied to the build directory (they're templates only).
Public Directory (public/)
Static assets like CSS, JavaScript, images, and other files go in the public/ directory. Everything in public/ is copied to the build root:
public/
โโโ style.css # Copied to build/style.css
โโโ script.js # Copied to build/script.js
โโโ images/
โโโ logo.png # Copied to build/images/logo.png
Configuration
genny.toml
The configuration file supports the following options:
| Option | Description | Required | Default |
|---|---|---|---|
name |
Site name | No | "" |
description |
Site description | No | "" |
base_url |
Base URL for the site (used in sitemap and permalinks) | No | null |
generate_sitemap |
Whether to generate sitemap.xml | No | true |
minify_output |
Whether to minify HTML output by removing unnecessary whitespace | No | false |
Note: Minification uses WebMarkupMin to remove unnecessary whitespace, newlines, and collapse multiple spaces. It also optimizes HTML structure (e.g., removes optional closing tags per HTML5 spec). Enable it (minify_output = true) for production builds to reduce file sizes. By default, minification is disabled to preserve formatting for readability during development.
Example:
name = "My Blog" description = "A personal blog about technology and life" base_url = "https://example.com"
Note: If base_url is not specified, sitemap URLs will use relative paths starting with /.
Commands
Build
Build your static site:
Using Docker:
# Use --user flag to run as current user (prevents permission issues) docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest build
Using CLI:
Options:
-v, --verbose- Enable verbose output (shows detailed build information including file paths and directory operations)
Examples:
# Build with verbose output (Docker) docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest build -v # Build with verbose output (CLI) genny build -v
Note: When using Docker, make sure you're running the command from your project root directory (where genny.toml is located). The Docker container mounts your current directory and runs the build command inside it.
Permission Issues: If you encounter permission errors when trying to modify files in the build/ directory, make sure you're using the --user "$(id -u):$(id -g)" flag. Without it, Docker runs as root and creates files owned by root, requiring sudo to modify them.
Ignored Files and Directories
Genny automatically ignores common development files and directories:
Ignored Files:
.gitignore,.env,.env.local,.env.productionpackage.json,package-lock.json,yarn.lock,pnpm-lock.yaml.git,.gitattributes,.gitkeep.DS_Store,Thumbs.db
Ignored Directories:
node_modules,.git,.vscode,.idea,.vs.next,.nuxt,dist,build,.cachelayouts(templates, not copied to build)partials(templates, not copied to build)
Examples
Example 1: Simple Blog Post
pages/blog/my-first-post.html:
<!-- layout: post.html --> <!-- title: My First Post --> <body> <article> <h1>My First Post</h1> <p>Published on January 1, 2025</p> <p>This is my first blog post!</p> </article> </body>
layouts/post.html:
<!DOCTYPE html> <html> <head> <title>{{site.name}} - Blog - {{title}}</title> <meta name="description" content="{{site.description}}"> <link rel="stylesheet" href="/style.css"> </head> <body> <header> <h1>{{site.name}}</h1> <nav> <a href="/">Home</a> <a href="/blog">Blog</a> </nav> </header> <main> {{content}} </main> <footer> <p>© {{year}} {{site.name}}</p> </footer> </body> </html>
Example 2: Page Without Layout
pages/standalone.html:
<!DOCTYPE html> <html> <head> <title>Standalone Page</title> </head> <body> <h1>This page doesn't use a layout</h1> <p>It's a complete HTML document.</p> </body> </html>
Example 3: Using Partials
partials/header.html:
<header> <h1>{{ site.name }}</h1> <nav>{{ partial: nav.html }}</nav> </header>
partials/nav.html:
<a href="/">Home</a> <a href="/about.html">About</a> <a href="/blog.html">Blog</a>
partials/footer.html:
<footer> <p>© {{ year }} {{ site.name }}</p> <p><a href="{{ permalink }}">Permalink</a></p> </footer>
layouts/default.html:
<!DOCTYPE html> <html> <head> <title>{{ site.name }} - {{ title }}</title> <link rel="canonical" href="{{ permalink }}"> </head> <body> {{ partial: header.html }} <main> {{ content }} </main> {{ partial: footer.html }} </body> </html>
pages/index.html:
<!-- title: Home --> <body> <h1>Welcome</h1> <p>This is the homepage.</p> </body>
Development
Running Tests
cd Genny.Tests dotnet test
Building
Running
dotnet run --project Genny/Genny.csproj -- build
How It Works
- Configuration Parsing: Genny looks for
genny.tomlin the current directory or parent directories - Page Discovery: Recursively finds all
.htmlfiles in thepages/directory - Layout Application: Applies layouts from the
layouts/directory if available - Asset Copying: Copies all files from
public/to the build directory - Output Generation: Writes processed pages to the
build/directory - Sitemap Generation: Automatically generates
sitemap.xmlwith all pages
Sitemap
Genny automatically generates a sitemap.xml file in the build directory containing all pages from your site. The sitemap includes:
- URLs: All pages with proper paths (index.html maps to root URL)
- Last Modified: File modification dates
- Change Frequency: Set to "monthly" by default
- Priority: Root page (index.html) gets priority 1.0, other pages get 0.8
Example sitemap.xml:
<?xml version="1.0" encoding="utf-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://example.com</loc> <lastmod>2025-01-15T10:30:00Z</lastmod> <changefreq>monthly</changefreq> <priority>1.0</priority> </url> <url> <loc>https://example.com/about.html</loc> <lastmod>2025-01-14T09:20:00Z</lastmod> <changefreq>monthly</changefreq> <priority>0.8</priority> </url> </urlset>
To use a custom base URL in the sitemap, add base_url to your genny.toml:
base_url = "https://example.com"
To disable sitemap generation, set generate_sitemap = false:
TODO?
- Blog/articles support
- RSS feed support
- Some sort of support for 'objects' (e.g. portfolio items)
License
This project is licensed under the GNU General Public License v3.0 or later. See the LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit a pull request.