A Go-based static site generator behaviorally compatible with Zola. Existing Zola sites can build with minimal changes while preserving Zola semantics.
go install github.com/abdusco/kopkop/cmd/kopkop@latestOr build from source:
git clone https://github.com/abdusco/kopkop
cd kopkop
go build ./cmd/kopkopRequires Go 1.22+.
# Create a new site
kopkop init mysite
cd mysite
# Build the site to ./public
kopkop build
# Start the dev server with live reload
kopkop serve
# Check external links
kopkop checkScaffolds a new site with a minimal directory structure:
mysite/
zola.toml # site config
content/
_index.md # root section
templates/
index.html
page.html
section.html
static/
style.css
Flags:
--force— overwrite existing files
Renders the full site to the output directory.
Flags:
| Flag | Default | Description |
|---|---|---|
--root |
. |
Root site directory |
--config |
auto-discovered | Config file name |
--base-url |
from config | Override base URL |
--output-dir |
from config | Output directory |
--drafts |
false |
Include draft pages |
--minify |
false |
Minify HTML output |
--force |
false |
Overwrite existing output directory |
Build order:
- Clean output directory (disk mode only)
- Render pages (markdown + shortcodes + templates)
- Render alias redirects
- Render sections
- Render taxonomies
- Render sitemap (if
generate_sitemap) - Render feeds (if
generate_feeds) - Render 404
- Render robots.txt (if
generate_robots_txt) - Build search index (if
build_search_indexorsearch.build_index) - Copy static directory
- Copy colocated assets
Starts a local dev server with filesystem watching and WebSocket live reload.
Flags:
| Flag | Default | Description |
|---|---|---|
--root |
. |
Root site directory |
--interface |
127.0.0.1 |
Bind interface |
--port |
1111 |
Bind port |
--drafts |
false |
Include drafts |
--open |
false |
Open browser on start |
--debounce |
200ms |
File watcher debounce duration |
--store-html |
false |
Also write HTML to disk (default: memory only) |
--fast |
false |
Fast rebuild mode |
By default, generated HTML is served from memory without writing to disk. The server watches content/, templates/, static/, and the config file for changes. Extra paths can be added via extra_watch_paths in the config.
Live reload is injected automatically via a WebSocket endpoint at /__livereload.
Builds the site and then verifies all external links found in the rendered content.
Flags:
| Flag | Default | Description |
|---|---|---|
--root |
. |
Root site directory |
--drafts |
false |
Include drafts |
Exit code is non-zero if broken links are found (unless link_checker.internal_level = "warn").
Config is loaded from zola.toml or config.toml, searched in the current directory and its ancestors.
base_url = "https://example.com"
title = "My Site"
description = "A site built with kopkop"
author = "Alice"
output_dir = "public"
# "absolute" or "relative"
link_strategy = "absolute"
# Theme to use (from themes/<name>/)
theme = ""
build_search_index = false
generate_sitemap = true
generate_feeds = false
feed_filenames = ["atom.xml"]
generate_robots_txt = true
minify_html = false
# Glob patterns for content files to ignore
ignored_content = []
# Additional paths to watch in serve mode
extra_watch_paths = []
# Whether to preserve file modification dates on output files
paths_keep_dates = false
# Include drafts in all builds (overridden by --drafts flag)
enable_drafts_in_build = false
[markdown]
insert_anchor_links = false
external_links_target_blank = false
# Any built-in Chroma style name:
# https://github.com/alecthomas/chroma/tree/master/styles
highlight_theme = "github"
[search]
build_index = false
index_path = "search_index.json"
[link_checker]
internal_level = "error" # "warn" or "error"
timeout_seconds = 10
use_cache = true
cache_file = ".kopkop-linkcheck-cache.json"
skip_anchor_prefixes = []
[[taxonomies]]
name = "tags"
feed = false
[extra]
# Arbitrary key-value pairs accessible in templates as config.extraIf theme is set, kopkop loads themes/<theme>/theme.toml and merges values that are not set in the main config (title, description, taxonomies, extra).
content/
_index.md # root section
blog/
_index.md # blog section
first-post.md # page at /blog/first-post/
second-post/
index.md # page with colocated assets
image.png # colocated asset
Pages and sections support TOML (+++) or YAML (---) front matter.
Page front matter:
+++
title = "My Post"
description = "A short description"
date = 2024-01-15
updated = 2024-02-01
slug = "custom-slug" # overrides URL slug
path = "/custom/path/" # full path override
weight = 10
draft = false
render = true
template = "custom.html" # override template
aliases = ["/old/url/"]
insert_anchor_links = "left"
[taxonomies]
tags = ["go", "web"]
[extra]
custom_field = "value"
+++
Page content goes here.Section front matter (_index.md):
+++
title = "Blog"
description = "All posts"
template = "blog.html"
page_template = "post.html"
sort_by = "date" # "date", "weight", or anything else (preserves loader order)
paginate_by = 10
paginate_path = "page"
paginate_reversed = false
generate_feed = false
transparent = false
draft = false
weight = 0
+++Dates can be embedded in filenames: 2024-01-15-my-post.md sets the page date automatically.
Insert <!-- more --> in content to define where the summary ends:
This is the summary shown in listings.
<!-- more -->
This content is only shown on the full page.Templates use MiniJinja syntax (compatible with Jinja2/Tera). Templates live in templates/ and theme templates in themes/<name>/templates/. Site templates take precedence over theme templates.
Resolution happens in two stages.
Stage 1 — choose a template name based on content type:
| Content type | Template name selection |
|---|---|
| Page | template front matter → parent section's page_template (walked up to root) → page.html |
| Section | template front matter → index.html (root section only, if the file exists) → section.html |
| Taxonomy list | <name>/list.html → <slug>/list.html → taxonomy_list.html |
| Taxonomy term | <name>/single.html → <slug>/single.html → taxonomy_single.html |
| 404 | 404.html |
| Sitemap | sitemap.xml |
| Feed | feed filename (e.g. atom.xml) |
| Robots | robots.txt |
→ means "fall back to the next if missing or not set".
Stage 2 — find the template file for the chosen name:
templates/<name>— site templates (highest priority)themes/<theme>/templates/<name>— theme templates- Built-in fallback (covers 404, sitemap, atom/RSS feeds, robots.txt, alias redirects)
Site templates always override theme templates. The built-ins only apply if neither the site nor the theme provides the template.
Page context:
{{ page.title }}
{{ page.description }}
{{ page.content }} {# rendered HTML #}
{{ page.summary }} {# HTML up to <!-- more --> #}
{{ page.permalink }}
{{ page.path }}
{{ page.slug }}
{{ page.date }}
{{ page.updated }}
{{ page.draft }}
{{ page.toc }} {# list of {id, level, title} #}
{{ page.assets }} {# colocated asset URLs #}
{{ page.taxonomies }} {# map of taxonomy name to list of terms #}
{{ page.extra }}
{{ config.title }}
{{ config.base_url }}
{{ config.extra }}Section context:
{{ section.title }}
{{ section.content }}
{{ section.permalink }}
{{ section.pages }} {# list of page objects #}
{{ section.subsections }} {# list of subsection relative paths (strings) #}
{{ paginator }} {# pagination object, present when paginate_by > 0 #}When paginate_by is set, paginator contains: current_index, number_pagers, per_page, total_pages, first, last, current, pages, and optionally previous / next.
| Function | Description |
|---|---|
get_url(path=...) |
Resolve a URL. Supports cachebust=true and absolute=true/false. |
get_page(path=...) |
Look up a page by its content path (e.g. "blog/post.md"). |
get_section(path=...) |
Look up a section by its content path (e.g. "blog/_index.md"). |
get_taxonomy(kind=...) |
Get a full taxonomy object by name. |
get_taxonomy_term(kind=..., term=...) |
Get a single taxonomy term with its pages. |
get_taxonomy_url(kind=..., name=...) |
Get the URL for a taxonomy term. |
now() |
Current UTC time as RFC3339 string. |
get_env(name=..., default=...) |
Read an environment variable. |
load_data(path, format=...) |
Load a file relative to the site root. JSON, TOML, YAML, and CSV files are parsed into structured data; everything else is returned as a plain string. The optional format kwarg ("json", "toml", "yaml", "csv", "plain") overrides extension-based detection. CSV files return {headers, records}. |
load_url(url=..., format=..., method="GET", headers=[...], body=...) |
Fetch a URL and return parsed data. Format is auto-detected from the URL extension or Content-Type response header, or overridden with format. headers is a list of "Key: Value" strings. body sends a request body (useful with method="POST"). Responses are cached in memory for the duration of the build. Timeout defaults to 30s; override with LOAD_URL_TIMEOUT env var (e.g. "10s"). |
get_hash(path, base64=false) |
SHA-384 hash of a file or string. |
get_image_metadata(path) |
Returns {width, height, format} for an image. |
resize_image(path, width, height) |
Resize an image and return its URL. |
| Filter | Description |
|---|---|
| markdown |
Render a string as Markdown to HTML. |
| base64_encode |
Base64-encode a string. |
| base64_decode |
Base64-decode a string. |
| regex_replace(pattern, replacement) |
Replace regex matches in a string. |
| num_format(precision) |
Format a number with fixed decimal places (default: 2). |
| default(value) |
Return value if the input is undefined or empty. |
| concat(with=...) |
Concatenate two arrays. |
| date(format=...) |
Format a date using strftime-style patterns (e.g. "%Y-%m-%d"). |
kopkop normalizes common Tera template syntax to MiniJinja on load:
- Named end tags:
{% endmacro name %}becomes{% endmacro %} - Macro call syntax:
macros::func(...)becomesmacros.func(...)
Shortcodes are templates placed in templates/shortcodes/. They can be .html (rendered after Markdown) or .md (rendered before, so Markdown inside is processed).
{{ youtube(id="dQw4w9WgXcQ") }}
Template at templates/shortcodes/youtube.html:
<iframe src="https://www.youtube.com/embed/{{ id }}"></iframe>{% quote(author="Jane Doe") %}
This is the body content passed to the shortcode.
{% end %}
Template at templates/shortcodes/quote.html:
<blockquote>
{{ body }}
<cite>— {{ author }}</cite>
</blockquote>To show a shortcode as literal text without rendering it, wrap with comment markers:
{{/* youtube(id="example") */}}
{%/* quote(author="x") */%} body {%/* end */%}
- Heading anchors — set
insert_anchor_links = truein[markdown]to add#anchor links to headings - Syntax highlighting — set
highlight_themein[markdown]to a Chroma style name; CSS classes are prefixed withz- - Highlight stylesheet output — when highlighted code is present on a page, kopkop auto-generates
code-<theme>.cssand injects a<link rel="stylesheet">into that page's<head> - External link behavior — set
external_links_target_blank = trueto addtarget="_blank" rel="noopener"to external links - Internal links — use
@/path/to/page.mdsyntax to link to content by path; broken links cause a build error - Colocated assets — relative links in content resolve against the page's permalink
When build_search_index = true, a JSON file is written to search.index_path (default: search_index.json) containing:
[
{
"title": "Page Title",
"permalink": "https://example.com/blog/post/",
"summary": "...",
"content": "plain text content stripped of HTML tags"
}
]When generate_feeds = true, feeds are generated for the site and any sections with generate_feed = true. Feed templates default to Atom (atom.xml) but can be configured via feed_filenames.
Files in static/ are copied verbatim to the output directory. Colocated assets (files next to a page in content/) are copied adjacent to the rendered page output.
Images can be resized via the resize_image template function. Processed images are written to the output directory and served normally.
cmd/kopkop/ CLI entry point
internal/
config/ Config loading, validation, theme merge
content/ Page/section parsing, front matter, taxonomy, content graph
markdown/ Markdown rendering, shortcode parsing, link resolution
templates/ MiniJinja engine, filters, global functions, template manager
site/ Build orchestrator, output writing
assets/ Static file copy, image processing
search/ Search index generation
server/ Dev server, filesystem watcher, WebSocket live reload
linkcheck/ External link verification