Molodetz documentation

Auto-generated from the live docs registry. Give this file to an agent to learn the API.

Molodetz is a quiet blog roll. Notes about the work, finishing it, and judgement. This documentation describes how the site works, how to use the API and how admin keeps the whole thing running.

Read Writing on the roll for the content, and API and authentication for integrations.

Posts are markdown and live in SQLite. Only administrators write. A post has a title, a topic (roll or standard), a status (draft or published) and an optional placeholder marker.

The server renders GFM: tables, strikethrough, hard line breaks. HTML in the text is escaped. Links may only be http, https, mailto and tel. Write emoji as :shortcode:, for example :fire:.

A bare image URL becomes an image, @name becomes a link to the profile.

The first notes were carried over from the old Molodetz page and carry the Placeholder label. retoor replaces them with his own stories. Untick the box while editing once a post is real.

A slug is the title plus the last twelve characters of the uid. Editing does not change the slug. If a title is ever rewritten in the database, the old slug still resolves through its uid tail and redirects with 301 to the canonical one.

The form at /join stores a join request: name, contact, an optional link to your work and a message. Administrators get a notification and see the request under Admin, Join requests.

There is no automatic access. retoor reads every request and gets in touch himself.

Molodetz has no ads, no cross-app tracking, no payments and no social login.

Every route has four faces: HTML, JSON, documentation and (where selected) an assistant tool. Ask for JSON with Accept: application/json.

Resolution order:

  1. session cookie (64 hex characters) after logging in at /auth/login.
  2. X-API-KEY header with your API key.
  3. Authorization: Bearer <key>.
  4. Authorization: Basic with name or email and password.

There is no JWT and no OAuth.

curl -H 'Accept: application/json' https://molodetz.nl/roll

Errors have the shape {"error": {"status": 404, "message": "..."}}. Validation errors return 422 with {"error": "validation", "fields": [...], "messages": [...]}.

Membership starts from a join request plus an admin-issued invite. Admins call POST /admin/joins/{uid}/invite (returns claim_url and expires_at in data) and POST /admin/joins/{uid}/invite/revoke. The public claim is GET and POST /invite/{token} with username, email, password, password_confirm and terms. Claim links are single use and every dead link answers 404 with the same message.

GET /admin/gallery reports live flyer and meme counts plus catalogued source files missing from disk. POST /admin/gallery/resync re-reads the sources and retires removed entries. See Galleries and content for how publishing works.

The Dutch paths from before (/rol, /standaard, /mensen, /binnen, /voorwaarden) answer with a 301 to their English replacement. Query strings are kept.

The flyers and the memes are curated galleries. Every work is a source file plus a catalogue entry with its caption. Nothing is uploaded through the site: new works arrive with a deploy, and the database reconciles at boot.

Flyers live at /flyers. New flyers are 1080 by 1350 (portrait). The first flyer in the catalogue is the featured one on the home page.

Memes live at /memes. New memes are 1080 by 1080 (square). Some older memes are 1280 by 720 (landscape) from the previous generation; they stay as they are.

  1. The source file lands in the media directory and the catalogue entry (filename, kind, caption) lands in the code, both through deploy.
  2. At boot the sync reads every catalogued source file and upserts the database row: position, dimensions, perceptual hash and a generated webp thumbnail.
  3. Near-duplicates are skipped: a work whose image hash is within a small distance of an already synced work is logged and left out, so an accidental double never shows twice.
  4. Removed works are retired, never hard-deleted: their rows are soft-deleted and disappear from the galleries.

An administrator can also press Resync under Admin, Gallery after a deploy. Resync re-reads the sources, refreshes thumbnails whose content changed, reports missing source files and retires removed entries. It never invents catalogue entries.

Public pages are cached for seconds to minutes (landing, settings, sitemap each have their own short TTL), so a fresh deploy can take a moment to show everywhere. The exact TTLs are operator detail; see the operator runbook under Admin.

Old Dutch paths (/rol, /mensen, and friends) redirect with 301 to their English replacements; see API and authentication.

API reference

API: Content

Public read routes: the roll, the standard, posts, galleries and people.

GET /

Home · auth public (guest) · negotiable

Latest posts and the featured flyer.

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/'

Response sample

{
  "flyer_count": 4,
  "meme_count": 22,
  "posts": []
}

GET /roll

The roll · auth public (guest) · negotiable

Published notes in the roll topic, newest first, cursor paginated.

Fields

NameTypeLocationRequiredExample
beforestringquerynoNone

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/roll'

Response sample

{
  "next_cursor": null,
  "posts": [],
  "title": "Roll",
  "topic": "roll"
}

GET /standard

The standard · auth public (guest) · negotiable

Published posts in the standard topic.

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/standard'

Response sample

{
  "posts": [],
  "title": "Standard",
  "topic": "standard"
}

GET /posts/{slug}

Post · auth public (guest) · negotiable

A published post by slug or uid. Old slugs redirect with 301 to the canonical slug.

Fields

NameTypeLocationRequiredExample
slugstringpathyesa-pass-is-not-an-ambition

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/posts/a-pass-is-not-an-ambition'

Request JSON

{
  "path": {
    "slug": "a-pass-is-not-an-ambition"
  }
}

Response sample

{
  "author": {
    "username": "retoor"
  },
  "post": {
    "title": "A pass is not an ambition.",
    "topic": "roll"
  }
}

GET /flyers

Flyers · auth public (guest) · negotiable

Flyer gallery.

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/flyers'

Response sample

{
  "items": [],
  "kind": "flyer",
  "title": "Flyers"
}

GET /memes

Memes · auth public (guest) · negotiable

Meme gallery.

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/memes'

Response sample

{
  "items": [],
  "kind": "meme",
  "title": "Memes"
}

GET /people

People · auth public (guest) · negotiable

People on the roll.

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/people'

Response sample

{
  "people": [
    {
      "post_count": 5,
      "username": "retoor"
    }
  ]
}

GET /people/{username}

Person · auth public (guest) · negotiable

Profile with posts.

Fields

NameTypeLocationRequiredExample
usernamestringpathyesretoor

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/people/retoor'

Request JSON

{
  "path": {
    "username": "retoor"
  }
}

Response sample

{
  "person": {
    "username": "retoor"
  },
  "posts": []
}

GET /robots.txt

robots.txt · auth public (guest) · none

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/robots.txt'

Response sample

null

GET /sitemap.xml

Sitemap · auth public (guest) · none

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/sitemap.xml'

Response sample

null

GET /avatar/svg/{seed}

Avatar · auth public (guest) · none

Deterministic SVG avatar.

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/avatar/svg/{seed}'

Response sample

null

GET /health

Health · auth public (guest) · json

Liveness and version.

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/health'

Response sample

{
  "status": "ok",
  "version": "1.0.0"
}

GET /presence/roster

Presence · auth public (guest) · json

Online set (uids).

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/presence/roster'

Response sample

{
  "online": [],
  "tracked": 0
}

API: Join

Sign up to write along. Stored for administrators, never public.

GET /join

Join form · auth public (guest) · negotiable

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/join'

Response sample

{
  "submitted": false
}

POST /join

I'm in · auth public (guest) · negotiable

Stores a join request: name, contact and an optional repo link.

Fields

NameTypeLocationRequiredExample
namestringbodyyesAda
contactstringbodyyesada@example.com
repo_urlstringbodynohttps://git.example.com/ada
messagestringbodynoI write about finishing things.

Request sample

curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"name": "Probe", "contact": "probe@example.com"}' 'https://example.com/join'

Request JSON

{
  "contact": "probe@example.com",
  "name": "Probe"
}

Response sample

{
  "data": {
    "uid": "0190..."
  },
  "ok": true,
  "redirect": "/join?ok=1"
}

GET /invite/{token}

Invite claim form · auth public (guest) · negotiable

Shows the claim form for a live invite; unknown, used and expired links answer 404 with the same message.

Fields

NameTypeLocationRequiredExample
tokenstringpathyesunknown-token

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/invite/unknown-token'

Request JSON

{
  "path": {
    "token": "unknown-token"
  }
}

Response sample

{
  "email": "ada@example.com",
  "email_locked": true,
  "username": "ada",
  "valid": true
}

POST /invite/{token}

Claim invite · auth public (guest) · negotiable

Creates the Member account, logs in and marks the join request accepted. Single use.

Fields

NameTypeLocationRequiredExample
tokenstringpathyesunknown-token
usernamestringbodyyesada
emailstringbodyyesada@example.com
passwordstringbodyyescorrect horse battery staple
password_confirmstringbodyyescorrect horse battery staple
termsbooleanbodyyesTrue

Request sample

curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"username": "Probe", "email": "probe@example.com", "password": "probe-pass-1", "password_confirm": "probe-pass-1", "terms": true}' 'https://example.com/invite/unknown-token'

Request JSON

{
  "email": "probe@example.com",
  "password": "probe-pass-1",
  "password_confirm": "probe-pass-1",
  "terms": true,
  "username": "Probe"
}

Response sample

{
  "data": {
    "username": "ada"
  },
  "ok": true,
  "redirect": "/"
}

API: Account

Session login, API key, notifications. No JWT, no OAuth.

GET /auth/login

Login page · auth public (guest) · negotiable

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/auth/login'

Response sample

{
  "errors": [],
  "next": "/admin"
}

POST /auth/login

Log in · auth public (guest) · negotiable

Sets an httponly session cookie (7 days, 30 with remember me).

Fields

NameTypeLocationRequiredExample
usernamestringbodyyes{{username}}
passwordpasswordbodyyesNone
rememberbooleanbodynoNone

Request sample

curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"username": "retoor"}' 'https://example.com/auth/login'

Request JSON

{
  "body": {
    "username": "retoor"
  }
}

Response sample

{
  "data": null,
  "ok": true,
  "redirect": "/admin"
}

POST /auth/logout

Log out · auth public (guest) · negotiable

Request sample

curl -X POST -H 'Accept: application/json' 'https://example.com/auth/logout'

Response sample

{
  "data": null,
  "ok": true,
  "redirect": "/"
}

GET /profile/api-key

View API key · auth member (Member) · negotiable

Request sample

curl -X GET -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/profile/api-key'

Response sample

{
  "api_key": "YOUR_API_KEY"
}

POST /profile/api-key/regenerate

Renew API key · auth member (Member) · negotiable

Request sample

curl -X POST -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/profile/api-key/regenerate'

Response sample

{
  "data": {
    "api_key": "..."
  },
  "ok": true,
  "redirect": "/profile/api-key"
}

POST /profile/avatar/regenerate

Renew avatar · auth member (Member) · negotiable

Request sample

curl -X POST -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/profile/avatar/regenerate'

Response sample

{
  "ok": true
}

GET /notifications

Notifications · auth member (Member) · negotiable

Request sample

curl -X GET -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/notifications'

Response sample

{
  "notifications": [],
  "unread": 0
}

POST /notifications/read

Mark all read · auth member (Member) · negotiable

Request sample

curl -X POST -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/notifications/read'

Response sample

{
  "ok": true,
  "redirect": "/notifications"
}

GET /terms

Terms · auth public (guest) · negotiable

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/terms'

Response sample

{
  "title": "Terms"
}

POST /terms/accept

Accept terms · auth member (Member) · negotiable

Request sample

curl -X POST -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/terms/accept'

Response sample

{
  "ok": true,
  "redirect": "/"
}

GET /privacy

Privacy · auth public (guest) · negotiable

Request sample

curl -X GET -H 'Accept: application/json' 'https://example.com/privacy'

Response sample

{
  "title": "Privacy"
}