Configuration reference
Every poops.json key, with a short explanation and example. The pipeline keys
(scripts, styles, postcss, markup, reactor, images) each link to a full guide for the
deep dive; everything else is documented in full on this page.
Every key
| Key | Purpose | Documented in |
|---|---|---|
$schema |
Editor completion and validation for this file | โ |
scripts |
Bundle / transpile JS & TS (esbuild) | โ |
styles |
Compile Sass / CSS | โ |
postcss |
PostCSS / Tailwind pass over compiled CSS | โ |
reactor |
Render React components to static HTML | โ |
images |
Responsive image processing | โ |
markup |
Templates โ static site | โ |
markup.searchIndex |
JSON search index of every page | โ |
markup.sitemap |
sitemap.xml generation |
โ |
markup.llms |
llms.txt index for LLMs / GEO |
โ |
markup.robots |
robots.txt generation |
โ |
markup.nav |
Navigation-tree data | โ |
markup.feed |
RSS / Atom feed from a collection | โ |
copy |
Copy static assets into the output | โ |
banner |
Comment stamped on every output file | โ |
serve |
Local dev server | โ |
livereload |
Reload the browser on changes | โ |
watch |
Paths to watch (or true to auto-derive) |
โ |
includePaths |
Import-resolution roots (Sass @use, JS imports) |
โ |
The remaining markup sub-keys โ in, out, engine, site, data, includePaths,
dateFormat, collections, baseURL, autoescape โ are covered in
Templating HTML.
$schema
A mistyped key is not a build error. A top-level "stlyes", an "inn" in a styles entry, an
"engnie" in markup.options โ each is read by nothing, and the build stays green with the file
it should have written simply missing. You find out when you look.
Poops names every one of them at startup, reading the same schema your editor does:
[info][warn] Unknown key "inn" in styles[0] โ ignored. Valid: in, out, options
Key names only, and only in the blocks Poops owns. images belongs to
poops-images and site is yours to name, so an
unrecognised key in either passes without comment. Types are checked by nobody here:
"minify": "yes" reaches the compiler and fails there, loudly.
The same JSON Schema drives editor completion and inline docs for
every key on this page. Point $schema at the copy in your node_modules:
{
"$schema": "./node_modules/poops/schema/poops.schema.json",
"scripts": [{ "in": "src/js/main.ts", "out": "dist/js/app.js" }]
}
Or at the hosted copy, which needs nothing installed:
{
"$schema": "https://stamat.info/poops/poops.schema.json"
}
VS Code, JetBrains and anything else speaking the language server protocol read it from the file
itself. To attach it without touching your config, map it in VS Code's settings.json instead โ
the same file, matched by name:
{
"json.schemas": [
{
"fileMatch": ["poops.json", "๐ฉ.json"],
"url": "https://stamat.info/poops/poops.schema.json"
}
]
}
The $schema key itself is inert โ Poops reads it, recognises it, and does nothing with it. The
URL is your editor's business: the startup check reads the copy inside node_modules/poops, so
pointing $schema at the hosted file, at a stale one, or leaving it out changes nothing about
what the CLI says. Nothing is added to what Poops installs into your project either way.
Blocks belonging to another package
poops.json is shared. septic reads a septic block out of
the same file, and Poops has no business calling that a mistake. So an unknown top-level key is
accepted in silence when a package by that name is in your dependencies, devDependencies,
peerDependencies or optionalDependencies โ declaring it is enough, and Poops never loads it:
{
"styles": [{ "in": "src/scss/index.scss", "out": "dist/css/app.css" }],
"septic": { "db": "data/app.db" }
}
With nothing by that name declared, the key is warned about as before โ which is what catches the typo.
Warning
Your editor cannot see your node_modules, so the schema cannot make that distinction. It
allows an object under any name it does not know, and rejects everything else:
"stlyes": [ โฆ ] is still flagged, "srve": { โฆ } is not. That is the price of one shared
config file, and the CLI still catches what the editor lets through.
A companion that owns a block describes it in its own schema โ septic
does โ and $schema takes one URL, so having both checked means composing them in a local file.
Each package's README carries its schema URL and that two-line allOf; this page deliberately does
not repeat them, since a URL copied into two repos is a URL that goes stale in one.
Info
The schema is hand-written, so it can drift from the code. Poops' test suite validates it
against the draft-07 meta-schema, then validates its own poops.json and every complete example
on this site against it. Its top-level keys are asserted to be exactly the set the CLI accepts,
its exec stages exactly the ones that fire, and its markup.options a superset of what the
markup engine reads. A per-entry options object โ mostly esbuild's and PostCSS's, not Poops' โ
has no such list, so if the editor does not offer an option this page documents, the schema is
behind, and that is worth reporting.
scripts
Bundles and transpiles JavaScript / TypeScript with esbuild. A single
{ in, out } object or an array of them; in accepts a path, an array of paths, or globs โ a
glob-matched index.* is named after its directory, relative to the glob's static prefix, so
src/elements/*/index.ts builds one bundle per component. out also accepts a template โ
{{dir}} (the match's directory relative to that static prefix) and
{{name}} (its basename without extension) โ naming one output per matched
entry, extension included. Per-entry options cover sourcemap, minify, justMinified, format
and target.
{
"scripts": {
"in": "src/js/main.ts",
"out": "dist/js/app.js",
"options": {
"sourcemap": true,
"minify": true,
"format": "iife",
"target": "es2019"
}
}
}
Full guide: Transpiling JS.
styles
Compiles Sass/SCSS (and plain CSS) to CSS. Same { in, out, options } shape as scripts, including
the index.* glob rule and the out templates; options adds tokenPaths for design-token inputs.
Pair it with postcss for Autoprefixer or Tailwind.
{
"styles": {
"in": "src/scss/index.scss",
"out": "dist/css/app.css",
"options": { "sourcemap": true, "minify": true }
}
}
Full guide: Transpiling CSS.
postcss
Runs a PostCSS pipeline โ separate from the Sass styles step โ for
Tailwind, Autoprefixer or any PostCSS plugin. options.plugins lists the
plugins to load. Accepts one entry or an array. Needs postcss installed (npm i -D postcss).
{
"postcss": {
"in": "src/css/main.css",
"out": "dist/css/main.css",
"options": { "plugins": ["@tailwindcss/postcss"], "minify": true }
}
}
Full guide: PostCSS & Tailwind.
markup
Turns a directory of templates (Nunjucks or Liquid, plus Markdown) into a static site. Same shape as
a scripts or styles entry: in and out, everything else under options โ engine, site,
data, includePaths, dateFormat, collections, baseURL, autoescape, plus
searchIndex, sitemap and nav below.
Warning
Deprecated placement. Poops 1.x also read these keys directly on markup
("markup": { "site": โฆ }). That still works in 2.x and logs a warning
naming the key; it stops working in 3.0. Move them into options.
{
"markup": {
"in": "src/markup",
"out": "dist",
"options": {
"engine": "nunjucks",
"site": { "title": "My Site", "description": "Built with Poops." }
}
}
}
The site object holds global data every template reads. The SEO filters pick up title,
description, url, logo, author and lang โ lang feeds both the <html lang> attribute
(<html lang="{{ page.lang or site.lang or 'en' }}">) and the JSON-LD
inLanguage; a page's front-matter lang overrides it. A site.jsonld object sets site-wide
JSON-LD defaults โ "jsonld": { "@type": "TechArticle" } for a docs site โ
merged over the generated ones and still overridable per page. Add anything else you want globally
available โ e.g. repo and branch to drive "Edit on GitHub" links (see
Building a documentation site).
Full guide: Templating HTML.
reactor
Renders React components to static HTML at build time and emits a hydration bundle. component is
the component rendered to markup, inject names the global the HTML is exposed as, and in/out
are the client hydration entry/bundle.
{
"reactor": {
"component": "src/js/App.jsx",
"inject": "app_html",
"in": "src/js/app-hydrate.jsx",
"out": "dist/js/app-hydrate.js"
}
}
Full guide: React.
images
Responsive image processing โ resize, convert (WebP/AVIF), crop and read EXIF โ via
poops-images. sizes is the responsive ladder plus any
named crops; format lists output formats.
Poops' schema leaves this block open, because poops-images owns the keys inside it. poops-images
publishes a schema of its own, and its README shows how to point images at it so the block is
completed and checked inside your poops.json too.
{
"images": {
"in": "src/images",
"out": "dist/images",
"sizes": [
{ "width": 640 },
{ "width": 1280 },
{ "name": "thumb", "width": 200, "height": 200, "crop": true }
],
"format": ["webp"]
}
}
Full guide: Images & galleries.
copy
Copies files or directories into the output โ static assets like fonts, favicons, OG images.
Accepts a single { in, out } object or an array of them; in can be a path or an array of
paths:
{
"copy": [
{
"in": ["src/static/ogimage.jpg", "src/static/favicon.ico", "src/fonts"],
"out": "dist"
},
{ "in": "images", "out": "dist/static" }
]
}
Input paths accept glob and extglob patterns (everything except POSIX character classes
like [[:alpha:]]):
{
"copy": {
"in": [
"images/**/awesome.{jpeg,jpg,png}",
"notes/info[0-9].txt",
"assets/!(vendor)/*.js",
"fonts/@(woff|woff2)/*.+(woff|woff2)"
],
"out": "dist"
}
}
exec
Shell commands to run after a pipeline stage compiles โ a post-processor that needs the built output, like stripping comments from the unminified CSS or regenerating a reference page. Keyed by stage, each value a command string or an array run in order:
{
"exec": {
"styles": [
"node script/strip-css-comments.mjs dist/styles.css",
"node script/gen-reference.mjs"
],
"build": "node script/deploy.mjs"
}
}
Unlike chaining poops -b && cmd in an npm script, the hook runs on every rebuild โ in
watch/dev too โ so the post-processed output never drifts while you work. Commands run from the
project root; a failing command fails a -b build's exit code but is logged and swallowed in
watch so the watcher survives.
Stages:
| Stage | Runs after |
|---|---|
styles |
CSS is final (after PostCSS) โ use this for anything reading the built CSS |
scripts |
scripts compile |
reactor |
reactor components render (build only) |
images |
images process |
markup |
markup renders |
copy |
files copy |
build |
once, after the full initial pipeline (not per watch rebuild) |
banner
A comment stamped on top of every output file. Templatable via mustache from your project's
package.json โ available variables: name, version, homepage, license, author,
description:
{
"banner": "/* {{ name }} v{{ version }} | {{ homepage }} | {{ license }} License */"
}
A plain string works too โ templating is optional.
serve
A local dev server:
| Option | Meaning |
|---|---|
port |
Port to serve on (CLI --port/-p overrides). |
base |
Base path of the server โ where your built HTML lives, e.g. "/dist". Defaults to the markup out directory. |
livereload
Reloads the browser when a build finishes. A switch, not an object โ there is nothing to configure:
{
"serve": { "base": "dist" },
"livereload": true
}
It rides the serve port, so it needs serve to be on. Poops answers
/__poops_reload as a server-sent events stream and appends the client script
to every HTML page it serves โ your templates need no snippet, and nothing
is written into your build output.
One save means one reload, after the build it triggered has settled. When everything a build wrote is CSS, stylesheets are swapped in place instead: no page reload, so scroll position and form state survive a style edit. The browser reconnects on its own after a Poops restart.
watch
An array of paths to watch; changes rebuild the affected pipeline:
{
"watch": ["src"]
}
Set it to true to derive the list automatically from every task's in path
(file entries like a script/style bundle collapse to their parent dir so
sibling imports still trigger a rebuild):
{
"watch": true
}
This covers sources that live under a task's own directory. Imports that reach
outside it โ a shared folder above the entry, node_modules โ aren't watched;
use an explicit array for those.
includePaths
Paths to resolve imports from (Sass @use, script imports). node_modules is the default โ
if you set this key, include node_modules yourself, since the value replaces the default:
{
"includePaths": ["node_modules", "lib"]
}
markup.searchIndex
Writes a JSON search index of every page. A string sets the output filename with defaults; the object form takes options:
| Option | Meaning |
|---|---|
out |
Output filename, written to the markup output directory. |
minWordLength |
Minimum word length considered a keyword. Default 3. |
maxKeywords |
Maximum keywords per page. Default 20. |
globalFrequencyCeiling |
Drop words appearing in more than this fraction of pages. Default 0.8. |
stopWords |
undefined = bundled English list, false = disable, an inline array, or a path to a JSON array file. |
All front matter fields pass through to the index; internal fields (content, isIndex,
layout, published) are stripped. A page's own keywords front matter overrides the
auto-extracted ones. Pages with published: false are excluded.
[
{
"title": "My Post",
"description": "A great post about things.",
"url": "blog/my-post.html",
"keywords": ["javascript", "bundler", "esbuild"]
}
]
markup.sitemap
Writes a standard sitemap.xml with <loc> and <lastmod> (from front matter date). If
site.url is set, it is prepended to all URLs. Collection index/pagination pages are included
here but excluded from the search index. A string sets the filename; the object form takes
out.
A page's front matter robots: noindex (or none) drops it from the sitemap and llms.txt
โ for drafts, thin or utility pages. Emit {% if page.robots %}<meta name="robots" content="{{ page.robots }}">{% endif %} in your layout <head> so the page carries
the directive itself.
markup.llms
Writes an llms.txt โ a Markdown index of your pages that LLMs and
generative engines (GEO) read to understand the site. An # H1 title, a > blockquote summary,
then - [title](url): description links grouped by URL path: the first folder is a ## section, a
second folder nests as a ### subsection (so docs/quick-start/x.html โ ### Quick Start under
## Docs), and root-level pages fall under the lead section. Collection sections are ordered
newest-first by date; other sections keep file order. site.url makes the links absolute;
collection index/pagination pages are skipped. A string sets the filename; the object form takes options:
| Option | Meaning |
|---|---|
out |
Output filename, written to the markup output directory. |
title |
H1 title. Defaults to site.title. |
description |
Blockquote summary. Defaults to site.description. |
intro |
Path (from project root) to a Markdown file inserted as free-form body context. |
sectionTitle |
Heading for the lead (uncollected) section. Default "Pages". |
full |
Also write the full-content file (below). true derives its name from out (llms.txt โ llms-full.txt); a string sets it explicitly. |
fullIntro |
Path (from project root) to a Markdown preamble inserted into the full-content file after its header. The full counterpart to intro. |
Point intro at a file authored for LLMs (e.g. llms-intro.md) โ not a raw README, whose
badges, install noise and ## headings collide with the generated sections.
full writes the companion full-content file โ every page's full content concatenated into one
file (the index is the link map; this is the whole corpus). true names it after out with a
-full suffix (llms.txt โ llms-full.txt, ai.txt โ ai-full.txt); pass a string to set the
path yourself. Content is each page's Markdown
source, so only .md/.markdown pages are included (an .njk/.liquid source is template
code, not prose); noindex and collection index pages are dropped. The file opens with a
# Full Documentation Archive for {title} header, a one-line intro naming the site and a > blockquote of the description, then each page
becomes an # title + URL: line + body, joined by ---. Set fullIntro to a Markdown file path
(from the project root) to insert your own preamble after that header โ the full counterpart to
intro; inserted verbatim (a missing file warns and is skipped). Unrendered
{% โฆ %} tags or shortcodes in a Markdown body pass through verbatim.
markup.robots
Writes a robots.txt. A string writes an allow-all file (User-agent: *, empty Disallow:) with
a Sitemap: line pointing at your generated sitemap โ absolute when site.url is set. The object
form takes options:
| Option | Meaning |
|---|---|
out |
Output filename, written to the markup output directory. |
userAgent |
The User-agent line. Default "*". |
disallow |
A path or array of paths to disallow. |
allow |
A path or array of paths to explicitly allow. |
sitemap |
An explicit Sitemap: URL, or false to omit the line. Auto-derived by default. |
markup.nav
Builds the page hierarchy as sidebar-ready data โ the nav template global plus a nested JSON
file. See Building a documentation site for the walkthrough; the options:
| Option | Meaning |
|---|---|
out |
Output filename, written to the markup output directory. |
collections |
true = include every collection page nested under its collection (default); false = exclude all; ["docs"] = allowlist; "index" = only each collection's landing page as a leaf. |
home |
false drops the site's root index page from the tree. Default true. |
root |
Scope the tree to a subdirectory (e.g. "docs"); its children are emitted at the top level with the section index pinned first. |
Each node has title, url (omitted on synthesized section nodes), order when set, and
children when it has subpages:
[
{
"title": "Guide",
"url": "guide",
"order": 1,
"children": [
{ "title": "Getting Started", "url": "guide/getting-started", "order": 1 }
]
}
]
Front matter shaping the tree: order (sort among siblings), navTitle (sidebar label),
nav: false (hide from sidebar). If nothing survives filtering, an empty array is written.
markup.feed
Generates an RSS or Atom subscription feed from a collection โ
no hand-authored feed template. Items are the collection's posts newest-first by date (capped at
limit), with channel metadata pulled from your site data. robots: noindex posts are excluded,
and links / guids are made absolute with site.url. The object form:
| Option | Meaning |
|---|---|
collection |
Collection to feed from. Omit to emit a feed for every collection. |
out |
File to write. A bare filename (default feed.xml) goes in the collection's folder; a slashed path is used as-is. |
type |
"rss" (default) or "atom". |
limit |
Max items, newest first. Default 20. |
title |
Channel title. Default "<Collection> | <site.title>". |
description |
Channel description. Default site.description. |
author |
Feed author. Default site.author. |
lang |
Feed language. Default site.lang. |
content |
true adds each post's full article HTML (RSS <content:encoded>, Atom <content type="html">). Default off. |
Shorthand: true (or a filename string) emits an RSS feed for every collection; an array of these
objects generates several feeds at once (e.g. an RSS and an Atom for one collection). Item
<description>/<summary> uses each post's description, falling back to its auto-excerpt. Link
readers to it from your layout <head>:
<link rel="alternate" type="application/rss+xml" href="{{ site.url }}/changelog/feed.rss">
content: true renders each post's Markdown source to article-body HTML (not the whole page โ
no layout/nav chrome), so only .md/.markdown posts get a <content:encoded>; others fall back
to <description> alone. Unrendered {% โฆ %} tags or shortcodes in a body
pass through verbatim.