# Molodetz documentation

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

# Welcome to Molodetz

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.

## What is here

- **Roll**: short notes, newest first.
- **Standard**: what Molodetz stands for.
- **Flyers** and **Memes**: two galleries.
- **People**: who writes here.
- **Join**: the form to write along.

## Where to start

Read [Writing on the roll](/docs/writing) for the content, and [API and authentication](/docs/api) for integrations.

# Writing on the roll

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.

## Markdown

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.

## Placeholders

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.

## Slugs

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.

# Joining

The form at [/join](/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.

# Terms and privacy

- [Terms](/terms)
- [Privacy](/privacy)

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

# API and authentication

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

## Authentication

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.

## Example

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

## Errors

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

## Invites

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.

## Gallery

`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](/docs/content) for how publishing works.

## Old paths

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

# Galleries and content

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

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

## Memes

Memes live at [/memes](/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.

## How publishing works

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.

## Freshness

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](/docs/api).

# API reference

## API: Content

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

### `GET /`

- **Title:** Home
- **Auth:** public (guest)
- **Negotiation:** negotiable

Latest posts and the featured flyer.

#### Request sample

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

#### Response sample

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

### `GET /roll`

- **Title:** The roll
- **Auth:** public (guest)
- **Negotiation:** negotiable

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

#### Fields

- `before` (string, query, optional) - Cursor from next_cursor

#### Request sample

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

#### Response sample

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

### `GET /standard`

- **Title:** The standard
- **Auth:** public (guest)
- **Negotiation:** negotiable

Published posts in the standard topic.

#### Request sample

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

#### Response sample

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

### `GET /posts/{slug}`

- **Title:** Post
- **Auth:** public (guest)
- **Negotiation:** negotiable

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

#### Fields

- `slug` (string, path, required) example='a-pass-is-not-an-ambition'

#### Request sample

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

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

#### Response sample

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

### `GET /flyers`

- **Title:** Flyers
- **Auth:** public (guest)
- **Negotiation:** negotiable

Flyer gallery.

#### Request sample

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

#### Response sample

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

### `GET /memes`

- **Title:** Memes
- **Auth:** public (guest)
- **Negotiation:** negotiable

Meme gallery.

#### Request sample

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

#### Response sample

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

### `GET /people`

- **Title:** People
- **Auth:** public (guest)
- **Negotiation:** negotiable

People on the roll.

#### Request sample

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

#### Response sample

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

### `GET /people/{username}`

- **Title:** Person
- **Auth:** public (guest)
- **Negotiation:** negotiable

Profile with posts.

#### Fields

- `username` (string, path, required) example='retoor'

#### Request sample

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

```json
{
  "path": {
    "username": "retoor"
  }
}
```

#### Response sample

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

### `GET /robots.txt`

- **Title:** robots.txt
- **Auth:** public (guest)
- **Negotiation:** none

#### Request sample

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

#### Response sample

```json
null
```

### `GET /sitemap.xml`

- **Title:** Sitemap
- **Auth:** public (guest)
- **Negotiation:** none

#### Request sample

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

#### Response sample

```json
null
```

### `GET /avatar/svg/{seed}`

- **Title:** Avatar
- **Auth:** public (guest)
- **Negotiation:** none

Deterministic SVG avatar.

#### Request sample

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

#### Response sample

```json
null
```

### `GET /health`

- **Title:** Health
- **Auth:** public (guest)
- **Negotiation:** json

Liveness and version.

#### Request sample

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

#### Response sample

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

### `GET /presence/roster`

- **Title:** Presence
- **Auth:** public (guest)
- **Negotiation:** json

Online set (uids).

#### Request sample

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

#### Response sample

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


## API: Join

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

### `GET /join`

- **Title:** Join form
- **Auth:** public (guest)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

```json
{
  "submitted": false
}
```

### `POST /join`

- **Title:** I'm in
- **Auth:** public (guest)
- **Negotiation:** negotiable

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

#### Fields

- `name` (string, body, required) example='Ada'
- `contact` (string, body, required) example='ada@example.com'
- `repo_url` (string, body, optional) example='https://git.example.com/ada'
- `message` (string, body, optional) example='I write about finishing things.'

#### Request sample

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

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

#### Response sample

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

### `GET /invite/{token}`

- **Title:** Invite claim form
- **Auth:** public (guest)
- **Negotiation:** negotiable

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

#### Fields

- `token` (string, path, required) example='unknown-token'

#### Request sample

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

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

#### Response sample

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

### `POST /invite/{token}`

- **Title:** Claim invite
- **Auth:** public (guest)
- **Negotiation:** negotiable

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

#### Fields

- `token` (string, path, required) example='unknown-token'
- `username` (string, body, required) example='ada'
- `email` (string, body, required) example='ada@example.com'
- `password` (string, body, required) example='correct horse battery staple'
- `password_confirm` (string, body, required) example='correct horse battery staple'
- `terms` (boolean, body, required) example=True

#### Request sample

```bash
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'
```

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

#### Response sample

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


## API: Account

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

### `GET /auth/login`

- **Title:** Login page
- **Auth:** public (guest)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

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

### `POST /auth/login`

- **Title:** Log in
- **Auth:** public (guest)
- **Negotiation:** negotiable

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

#### Fields

- `username` (string, body, required) example='{{username}}'
- `password` (password, body, required)
- `remember` (boolean, body, optional)

#### Request sample

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

```json
{
  "body": {
    "username": "retoor"
  }
}
```

#### Response sample

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

### `POST /auth/logout`

- **Title:** Log out
- **Auth:** public (guest)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

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

### `GET /profile/api-key`

- **Title:** View API key
- **Auth:** member (Member)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

```json
{
  "api_key": "YOUR_API_KEY"
}
```

### `POST /profile/api-key/regenerate`

- **Title:** Renew API key
- **Auth:** member (Member)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

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

### `POST /profile/avatar/regenerate`

- **Title:** Renew avatar
- **Auth:** member (Member)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

```json
{
  "ok": true
}
```

### `GET /notifications`

- **Title:** Notifications
- **Auth:** member (Member)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

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

### `POST /notifications/read`

- **Title:** Mark all read
- **Auth:** member (Member)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

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

### `GET /terms`

- **Title:** Terms
- **Auth:** public (guest)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

```json
{
  "title": "Terms"
}
```

### `POST /terms/accept`

- **Title:** Accept terms
- **Auth:** member (Member)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

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

### `GET /privacy`

- **Title:** Privacy
- **Auth:** public (guest)
- **Negotiation:** negotiable

#### Request sample

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

#### Response sample

```json
{
  "title": "Privacy"
}
```
