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.
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 buildOr create an alias:
alias genny='docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/workspace" -w /workspace ghcr.io/benbristow/genny:latest'
genny buildNote: 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- 🚀 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
Pull the Docker image from GitHub Container Registry:
docker pull ghcr.io/benbristow/genny:latestOr use a specific version:
docker pull ghcr.io/benbristow/genny:mainBuilding locally:
Alternatively, build the Docker image locally from the repository:
docker build -t genny:latest https://github.com/benbristow/genny.gitOr 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 buildThis runs the container with your current user ID and group ID, ensuring all generated files have the correct ownership.
Download the latest release binary for your platform from the GitHub Actions artifacts or build from source.
Use the Genny GitHub Action in your CI/CD workflows. See the Using GitHub Actions section below for detailed instructions.
Prerequisites:
- .NET 10.0 SDK or later
git clone <repository-url>
cd Genny
dotnet buildGenny 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/
Create a genny.toml file in your project root:
name = "My Awesome Site"
description = "A static site built with Genny"Create pages/index.html:
<body>
<h1>Welcome to My Site</h1>
<p>This is my homepage.</p>
</body>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 buildOr 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 buildImportant: 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 buildUsing the CLI Binary:
If you've downloaded the binary:
./genny buildOr if installed globally:
genny buildUsing .NET directly:
dotnet run --project Genny/Genny.csproj -- buildVerbose 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 -vYour site will be generated in the build/ directory.
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@v1With 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.
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 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 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).
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
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 /.
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 buildUsing CLI:
genny buildOptions:
-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 -vNote: 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.
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)
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>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>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>cd Genny.Tests
dotnet testdotnet builddotnet run --project Genny/Genny.csproj -- build- 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
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:
generate_sitemap = false- Blog/articles support
- RSS feed support
- Some sort of support for 'objects' (e.g. portfolio items)
This project is licensed under the GNU General Public License v3.0 or later. See the LICENSE file for details.
Contributions are welcome! Please feel free to submit a pull request.