How-to: a blog
Write posts in a form, publish a static site — the whole loop from one poops.json. Drafts stay a database row; only published posts become pages.
1. Scaffold
mkdir blog && cd blog && npm init -y
npm i -D poops laxative
2. One config
{
"markup": {
"in": "src/markup",
"out": "dist",
"options": { "includePaths": ["_layouts", "_partials"] }
},
"septic": {
"db": "data/blog.db",
"auth": { "seed": { "email": "you@example.com", "password": "changeme", "role": "admin" } },
"resources": {
"posts": {
"methods": ["GET", "POST", "PUT", "DELETE"],
"access": { "read": "public", "write": "admin" },
"fields": {
"title": "string required",
"slug": "slug required unique",
"body": "text required",
"excerpt": "text",
"status": "enum(draft,published) = draft",
"published_at": "datetime",
"created": "datetime = now",
"updated": "datetime = now!"
}
}
},
"build": {
"resources": {
"posts": { "into": "src/markup/posts", "slug": "slug", "body": "body", "layout": "post", "where": { "status": "published" } }
},
"forms": {
"posts": { "into": "src/markup/_partials", "submitLabel": "Publish" }
}
}
},
"watch": true,
"serve": { "port": 4040 },
"livereload": true
}
Three blocks, one file: the schema + API (resources), the static build (build.resources, published only), and the author form (build.forms). watch + serve + livereload are what let laxative dev hand the dev loop to poops; without a serve block it falls back to watching markup only, and says so.
Two lines in there are the ones to get right:
includePaths: ["_layouts", "_partials"]is the template engine's search path. poops skips_-prefixed directories when rendering pages, but will not search them for includes or layouts unless they are on the engine's paths — leave it out and neither the layout nor the generated form resolves."layout": "post", with no extension. poops appends its own: front matterlayout: postbecomes{% extends 'post.html' %}. Writingpost.htmlthere asks forpost.html.html.
3. A post layout
src/markup/_layouts/post.html renders each generated post. This is poops templating:
<!-- _layouts/post.html -->
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>{{ page.title }}</title></head>
<body>
<article>
<h1>{{ page.title }}</h1>
<time datetime="{{ page.published_at | date('YYYY-MM-DDTHH:mm:ss') }}">{{ page.published_at | date('D MMMM YYYY') }}</time>
{% block content %}{% endblock %}
</article>
<p><a href="/">← all posts</a></p>
</body></html>
{% block content %}{% endblock %} is where the post body lands — poops wraps each page's own source in that block and extends the layout, so {{ content }} is not a variable and renders nothing. The date filter takes dayjs tokens: septic stores 2026-08-05 12:00:00, and a <time datetime> wants ISO 8601, so the machine-readable value and the human one are two different formats of one field.
The layout owns the <h1> — write post bodies starting at ##, or every post ships two top-level headings and no outline.
An src/markup/index.html lists the posts collection (see poops collections). The generated _partials/posts-form.html is your write/edit form — include it on an admin page.
4. Write, then publish
npx laxative dev # http://localhost:3000
Post to the API (or use the generated form) as the seeded admin:
curl -c jar -X POST localhost:3000/api/_auth/login \
-H 'content-type: application/json' -d '{"email":"you@example.com","password":"changeme"}'
curl -b jar -X POST localhost:3000/api/posts -H 'content-type: application/json' \
-d '{"title":"Hello","slug":"hello","body":"## Hi","status":"published","published_at":"2026-08-05 12:00:00"}'
A draft is just "status":"draft" — it lives in the DB and never reaches the site until you flip it. GET /api/posts/:id as admin returns a prefilled edit form to do exactly that.
5. Publish the static site
npx laxative build # published posts → markup → poops → dist/
npx laxative serve # serve dist/ + the /api (to keep writing) on one origin
build writes one markdown file per published row into src/markup/posts/ — every field as front matter, the body field as the document — then poops renders them. The draft never appears, in the markup tree or in dist/.
Deploy dist/ anywhere — it's plain HTML. Keep laxative serve running where you author; rebuild to publish.
The loop
Write in a form → it's a draft row → flip to published → laxative build → a static page. Content in a database, a static site out, an author form in between — one config.
The backend half alone — schema, the where filter, the generated form, no app around it — is septic's version of this how-to.