No results

    Search failed

    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 matter layout: post becomes {% extends 'post.html' %}. Writing post.html there asks for post.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.