Image Generation
The img-gen component lets you describe an image inline in your Markdown and
have Neko generate it for you on demand using an OpenAI image model. The
matching neko gen-images command walks every .md file under the input
directory, asks an LLM for a filename and an alt-text description for each
directive, calls the OpenAI image API, saves the PNG into the page's
assets/img-gen/ folder, and rewrites the directive into a regular Markdown
image — preserving the original directive as an HTML comment so you can
re-generate the image later if needed.
By default Neko generates each image at landscape 1536x1024, adds an
instruction to make it look good on a light theme, then automatically
generates a paired dark-mode variant by calling the image-edit endpoint with
the freshly generated light image. Both files are saved side-by-side
(name.png and name-dark.png) and the rewritten Markdown uses a
src-dark="…" attribute so the active theme picks the right one at runtime.
All defaults can be overridden globally in neko.yml or per-directive.
# Generate images for every [!img-gen ...] directive under the current
# directory. Defaults to the gpt-image-1 image model and gpt-4o-mini for the
# filename/alt-text generation step.
neko gen-images --api-key sk-... --input .
If --api-key is omitted, Neko reads OPENAI_API_KEY from the environment.
Only OpenAI is supported at the moment.
Basic syntax
Place the directive on its own block. The body is the image prompt.
[!img-gen
A cozy cat sitting by a window during a rainstorm, watercolor style
]
Running neko build (or neko watch) before generating the image renders
nothing in place of the directive. After running neko gen-images, the
directive in the source file is rewritten to:
<!--
[!img-gen
A cozy cat sitting by a window during a rainstorm, watercolor style
]
-->
{src-dark="assets/img-gen/cozy-cat-rainstorm-dark.png"}
Subsequent neko build runs then render the image as any other Markdown
image. The src-dark="…" attribute makes Neko emit two <img> tags — the
light one hidden in dark mode, the dark one hidden in light mode — so the
right variant shows for the active theme. Re-enable regeneration by
uncommenting the directive and removing the image line.
If you set darkMode: false in neko.yml (or pass dark=false on a
single directive) the rewrite is just the plain  with no dark
variant.
Options
Attributes can be passed on the first line of the directive, before the prompt:
[!img-gen size=1024x1024 quality=high
A diagram of how Neko builds a static site, isometric pixel art
]
| Attribute | Values | Notes |
|---|---|---|
size |
auto, 1024x1024 (square), 1536x1024 (landscape, default), 1024x1536 (portrait), 2048x2048 (2K square), 2048x1152 (2K landscape), 3840x2160 (4K landscape), 2160x3840 (4K portrait), plus any <width>x<height> |
Defaults to the imageGen.size value in neko.yml (which itself defaults to 1536x1024). Sizes from the TornadoImageSizes enum are sent directly; the larger 2K/4K resolutions go through Custom with explicit width/height. |
quality |
auto, low, medium, high, standard, hd |
Forwarded to the API. Costs and latency scale with quality. |
background |
auto, opaque, transparent |
Only meaningful for models that support transparent backgrounds. |
style |
natural, vivid |
DALL·E-3 style preset. Ignored by other models. |
light |
true, false |
Per-directive override of imageGen.lightMode. Defaults to the value in neko.yml (true). |
dark |
true, false |
Per-directive override of imageGen.darkMode. Defaults to the value in neko.yml (true). |
neko.yml configuration
Global defaults live under the imageGen section. Every key is optional.
imageGen:
systemPrompt: "Use a flat illustration style with thin strokes and the Curiosity blue accent."
size: 1536x1024 # default landscape
lightMode: true # append a 'render for light theme' instruction
darkMode: true # also generate a *-dark.png variant after the light one
lightModePrompt: "Render this in light mode: bright background, colors tuned for a white theme."
darkModePrompt: "Recreate this image in dark mode: dark background, colors tuned for a dark theme."
| Key | Default | Purpose |
|---|---|---|
systemPrompt |
(empty) | Appended to every [!img-gen] prompt before the image API is called. Use it for a global house style. |
size |
1536x1024 |
Default image size when a directive doesn't specify one. |
lightMode |
true |
When true, lightModePrompt is appended to every prompt so the generated image renders well on a light theme. |
darkMode |
true |
When true, Neko follows up the light generation with an image-edit call using darkModePrompt to make a dark variant. |
lightModePrompt |
(see source) | Override the boilerplate appended for light mode. |
darkModePrompt |
(see source) | Override the boilerplate sent to the image-edit endpoint for the dark variant. |
Command-line options
| Flag | Default | Description |
|---|---|---|
--input, -i |
. |
Directory to scan recursively for .md files. |
--api-key |
OPENAI_API_KEY |
OpenAI API key. If omitted, the env var is used. |
--image-model |
gpt-image-1 |
OpenAI image model to call. Pass dall-e-3 if you prefer DALL·E-3. |
--llm-model |
gpt-4o-mini |
OpenAI chat model used to pick a filename and alt-text from the prompt. |
Backfilling dark variants for existing images
If your repository already contains generated images that don't yet have a
paired dark version — for instance, pages rewritten by an older version of
neko gen-images, or images authored manually but living under
assets/img-gen/ — run neko gen-dark-images to fill in the gaps without
re-generating the light originals.
neko gen-dark-images --api-key sk-... --input .
The command walks every .md file under --input, finds image references
of the form  that don't already carry a
{src-dark="…"} attribute, reads the existing PNG, asks the OpenAI
image-edit endpoint to recreate it in dark mode (using imageGen.darkModePrompt
from neko.yml), saves the result as <name>-dark.png next to the
original, and rewrites the Markdown to add the src-dark="…" attribute —
preserving any other attributes already on the image. Images whose dimensions
can be read from the PNG header are regenerated at the same size as the
original; everything else falls back to imageGen.size.
| Flag | Default | Description |
|---|---|---|
--input, -i |
. |
Directory to scan recursively for .md files. |
--api-key |
OPENAI_API_KEY |
OpenAI API key. If omitted, the env var is used. |
--image-model |
gpt-image-1 |
OpenAI image model used for the edit call. |
The command is idempotent: images that already carry src-dark="…", or that
are themselves <name>-dark.png files, are skipped. Running it twice never
re-spends tokens. If a <name>-dark.png already exists on disk but the
Markdown doesn't reference it, the command relinks the Markdown without
making a second API call.
How filenames and alt text are chosen
For every directive, Neko sends the prompt and the page name to the configured chat model with a short system prompt asking for a JSON object of the form:
{
"filename": "lowercase-ascii-slug",
"alt": "Short factual description of the generated image."
}
The filename is sanitised (lowercased, ASCII-only, --separated, max 60
chars) and de-duplicated against files that already exist in the target
assets/img-gen/ folder. If the chat call fails or returns invalid JSON,
Neko falls back to a slug derived from the prompt.
Tips
- Commit the generated PNGs (and the rewritten Markdown) so visitors don't need an API key just to view the site.
- Use
quality=lowfor drafts to keep API costs down, then re-generate withquality=highonce you're happy with the prompt. - The directive renders nothing in
neko builduntil you generate the image — pages with un-generated directives will simply show no image yet. - A dark variant doubles the per-image cost. Turn it off site-wide with
imageGen.darkMode: false, or selectively withdark=falseon individual directives (e.g. for screenshots that already work in both themes).