docs(readme): document all patch scripts, production deployment, and env vars

Adds documentation for 14 previously undocumented patch scripts:
- AP threading patches (federation-bridge-base-url, compose-default-checked,
  mastodon-reply-threading)
- Conversations Bluesky patches (cursor-fix, self-filter)
- Micropub session token fix
- Microsub compose draft guard and AP dispatch (fixed // comment prefix)
- Indiekit endpoint URLs protocol fix
- Webmention sender hentry syntax fix
- Endpoint posts fetch diagnostic

Adds Production deployment section covering:
- start.example.sh structure and webmention poller architecture
- FreeBSD rc.d service script (indiekit.rcd.example)
- Prometheus metrics shim (metrics-shim.cjs, port 9209)

Extends Setup section with required and key optional env var tables.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sven
2026-03-30 09:05:16 +02:00
parent 1628b47cc8
commit 7ce1112c4e
+96 -3
View File
@@ -410,6 +410,15 @@ Adds a public `GET /activitypub/api/ap-url?url=` endpoint that resolves a blog p
**`patch-endpoint-activitypub-locales.mjs`** **`patch-endpoint-activitypub-locales.mjs`**
Injects German (`de`) locale overrides into `@rmdes/indiekit-endpoint-activitypub` (e.g. "Benachrichtigungen", "Mein Profil"). The package ships only an English locale; this copies and customises it. Injects German (`de`) locale overrides into `@rmdes/indiekit-endpoint-activitypub` (e.g. "Benachrichtigungen", "Mein Profil"). The package ships only an English locale; this copies and customises it.
**`patch-ap-federation-bridge-base-url.mjs`**
Adds an optional `publicationUrl` parameter to `createFedifyMiddleware()` and `fromExpressRequest()`. When provided, uses the configured canonical URL as the base for all Fedify request objects instead of parsing `req.protocol` + `req.get("host")`. Required because nginx can omit `X-Forwarded-Proto` in some configurations, causing Fedify to see `http://` URLs that don't match its configured `https://` base — it then calls `next()`, the request falls through to auth middleware, and AP inbox/WebFinger clients receive a 302 redirect instead of AP JSON.
**`patch-ap-compose-default-checked.mjs`**
In the AP reader compose form (`/activitypub/admin/reader/compose`), the upstream code hardcoded `target.defaultChecked` to `true` only when `target.name === "@rick@rmendes.net"` — the upstream developer's own handle, which never matches on this installation. Fix: replaces the hardcoded comparison with `target.defaultChecked = target.checked === true`, so the pre-check state comes from the configured syndication target (which has `checked: true` in `indiekit.config.mjs`). Without this, the AP syndication checkbox is always unchecked in the AP reader compose form and replies written there are never federated.
**`patch-ap-mastodon-reply-threading.mjs`**
After `POST /api/v1/statuses` (Phanpy/Elk creates a post), the handler intentionally did not insert the new post into `ap_timeline` — it relied on the Eleventy build webhook firing 30120 s later. If the user replies to their own new post during that window, `findTimelineItemById` returns null, `in_reply_to_id` is silently dropped, and the follow-up reply is classified as a "note" (wrong post type, wrong URL path, no `inReplyTo` in AP output, broken thread on Mastodon). Fix: immediately after `postContent.create()`, inserts a provisional timeline item via `addTimelineItem()` using `$setOnInsert` (idempotent — syndicator's later upsert is a no-op).
### Conversations ### Conversations
**`patch-conversations-collection-guards.mjs`** **`patch-conversations-collection-guards.mjs`**
@@ -418,6 +427,12 @@ Adds null-safety guards to `conversation-items.js` so the endpoint does not cras
**`patch-conversations-mastodon-disconnect.mjs`** **`patch-conversations-mastodon-disconnect.mjs`**
Patches the conversations endpoint to handle a missing or disconnected Mastodon account gracefully — prevents startup crashes when Mastodon credentials are not configured. Patches the conversations endpoint to handle a missing or disconnected Mastodon account gracefully — prevents startup crashes when Mastodon credentials are not configured.
**`patch-conversations-bluesky-cursor-fix.mjs`**
The Bluesky `listNotifications` API paginates backwards in time — using the cursor as a polling marker causes the cursor to drift into the past until the poller fetches no new interactions at all. Fix: removes the `cursor` parameter from `fetchBlueskyNotifications` so every poll fetches the latest page, and clears any stale `bluesky_cursor` from the DB state. Deduplication relies on `platform_id` (unchanged).
**`patch-conversations-bluesky-self-filter.mjs`**
Self-interactions from the blog's own Bluesky account (likes, reposts, replies) appeared as inbound interactions in the conversations endpoint. Two-pronged fix: (1) the scheduler skips storing any notification whose author handle matches `BLUESKY_IDENTIFIER`; (2) the conversations controller filters out items where the author URL matches the site owner's Bluesky profile URL.
### Files ### Files
**`patch-endpoint-files-upload-route.mjs`** **`patch-endpoint-files-upload-route.mjs`**
@@ -447,6 +462,9 @@ Fixes the `~module/path` resolver in `lightningcss.js` to use `require.resolve()
### Micropub ### Micropub
**`patch-micropub-session-token.mjs`**
The Micropub action controller destructures the session token as `session.token`, but the IndieAuth middleware stores it as `session.access_token`. Result: `token` is `undefined``Authorization: Bearer undefined` → file upload via OwnYourSwarm always fails with 401. Fix: changes the destructuring to `const { scope, access_token: token }`.
**`patch-endpoint-micropub-where-note-visibility.mjs`** **`patch-endpoint-micropub-where-note-visibility.mjs`**
Defaults OwnYourSwarm `/where` check-in notes to `visibility: unlisted` unless the post explicitly sets a visibility. Prevents accidental public syndication of location check-ins. Defaults OwnYourSwarm `/where` check-in notes to `visibility: unlisted` unless the post explicitly sets a visibility. Prevents accidental public syndication of location check-ins.
@@ -461,6 +479,9 @@ Adds AI disclosure field UI (text level, code level, etc.) to the post creation/
**`patch-endpoint-posts-ai-cleanup.mjs`** **`patch-endpoint-posts-ai-cleanup.mjs`**
Removes AI disclosure fields from the post form submission before saving, delegating persistence to the AI block sidecar system. Removes AI disclosure fields from the post form submission before saving, delegating persistence to the AI block sidecar system.
**`patch-endpoint-posts-fetch-diagnostic.mjs`**
In the two-jail setup the node jail cannot reach `https://blog.giersig.eu` directly; self-referential fetches in `@indiekit/endpoint-posts` fail with `ECONNREFUSED`. Fix: rewrites self-referential fetch URLs to `http://localhost:<PORT>` using `INTERNAL_FETCH_URL` (or an automatic fallback), and wraps the fetch in a try-catch that logs the URL and response status on failure to make networking problems easier to diagnose.
**`patch-endpoint-posts-uid-lookup.mjs`** **`patch-endpoint-posts-uid-lookup.mjs`**
Fixes post editing 404s by adding `uid`-based lookup to the micropub source query. Without this, posts older than the first 40 results could not be opened for editing. Fixes post editing 404s by adding `uid`-based lookup to the micropub source query. Without this, posts older than the first 40 results could not be opened for editing.
@@ -479,6 +500,9 @@ Prevents unlisted posts from being re-syndicated via `@indiekit/endpoint-syndica
### Indiekit core ### Indiekit core
**`patch-indiekit-endpoint-urls-protocol.mjs`**
Endpoint URL resolution in `@indiekit/indiekit/lib/endpoints.js` uses `getUrl(request)` which returns the HTTP protocol (Express sees HTTP from nginx). This causes mixed-content failures in Safari when the frontend tries to load endpoint assets from `http://` URLs on an `https://` page. Fix: prefers `application.url` (the configured HTTPS base URL) over `getUrl(request)` when resolving relative endpoint paths.
**`patch-indiekit-routes-rate-limits.mjs`** **`patch-indiekit-routes-rate-limits.mjs`**
Replaces the single blanket rate limiter with separate strict (session/auth) and relaxed (general) limiters so legitimate API traffic is not throttled during normal use. Replaces the single blanket rate limiter with separate strict (session/auth) and relaxed (general) limiters so legitimate API traffic is not throttled during normal use.
@@ -510,8 +534,11 @@ Adds OPML file upload support to the podroll endpoint.
### Microsub / Reader ### Microsub / Reader
// **`patch-microsub-reader-ap-dispatch.mjs`** **`patch-microsub-compose-draft-guard.mjs`**
Adds Fediverse/ActivityPub detection and dispatch to the Microsub reader so AP profile URLs are routed to the ActivityPub reader rather than the RSS reader. The Microsub reader compose form was syndicating draft posts. Fix: extracts `post-status` from the request body; when the post is a draft, clears all `mp-syndicate-to` targets before forwarding to Micropub, and forwards `post-status: draft` so the post is saved as a draft.
**`patch-microsub-reader-ap-dispatch.mjs`**
Three related issues in the Microsub reader's `detectProtocol()` and syndication dispatch: (1) the hardcoded fediverse domain list missed common instances (troet.cafe, hachyderm.io, etc.); (2) same-instance Mastodon URLs weren't detected because the naive hostname check didn't match against the configured target set; (3) likes and reposts of fediverse posts were not dispatched as native AP Like/Announce activities. Fix: extends the fediverse detection list; builds the Mastodon hostname set dynamically from configured syndication targets; dispatches native AP Like/Announce for likes/reposts after the Micropub POST succeeds. Serve-only (not postinstall).
**`patch-microsub-feed-discovery.mjs`** **`patch-microsub-feed-discovery.mjs`**
Improves feed discovery in `fetchAndParseFeed`: when a bookmarked URL is an HTML page, falls back to `<link rel="alternate">` discovery and a broader set of candidate paths rather than only the fixed short list. Improves feed discovery in `fetchAndParseFeed`: when a bookmarked URL is an HTML page, falls back to `<link rel="alternate">` discovery and a broader set of candidate paths rather than only the fixed short list.
@@ -523,6 +550,9 @@ Applies several guards to the listening endpoints: scopes Funkwhale history fetc
### Webmention sender ### Webmention sender
**`patch-webmention-sender-hentry-syntax.mjs`**
`@rmdes/indiekit-endpoint-webmention-sender` v1.0.8 shipped with a typo: `_html.includes("h-entry"")` — the extra closing quote is a `SyntaxError` that prevents the module from loading at all, so the webmention sender never starts. This patch runs first (alphabetically) and fixes the typo before any other webmention-sender patches apply.
**`patch-webmention-sender-livefetch.mjs`** (v6) **`patch-webmention-sender-livefetch.mjs`** (v6)
Replaces the upstream content-fetching block with a synthetic h-entry builder. Reads stored post properties directly from the MongoDB document (`in-reply-to`, `like-of`, `bookmark-of`, `repost-of`, `syndication`, `content.html`) and constructs a minimal `<div class="h-entry">` with the appropriate microformat anchor tags. No live page fetch, no nginx dependency, no networking failures. Logs which properties were found per post. Upgrades from any prior version (v1v5) in-place. Replaces the upstream content-fetching block with a synthetic h-entry builder. Reads stored post properties directly from the MongoDB document (`in-reply-to`, `like-of`, `bookmark-of`, `repost-of`, `syndication`, `content.html`) and constructs a minimal `<div class="h-entry">` with the appropriate microformat anchor tags. No live page fetch, no nginx dependency, no networking failures. Logs which properties were found per post. Upgrades from any prior version (v1v5) in-place.
@@ -744,7 +774,70 @@ npm install # installs dependencies and runs all postinstall patches
npm run serve # runs preflights + patches + starts the server npm run serve # runs preflights + patches + starts the server
``` ```
Environment variables are loaded from `.env` via `dotenv`. See `indiekit.config.mjs` for the full configuration. Environment variables are loaded from `.env` via `dotenv`. Copy `.env.example` to `.env` and fill in the required values. See `indiekit.config.mjs` for the full configuration.
### Required environment variables
| Variable | Purpose |
|---|---|
| `SECRET` | Session signing secret — at least 32 characters |
| `PASSWORD_SECRET` | bcrypt hash of the admin password (`$2a$…`) |
| `MONGO_URL` or `MONGO_USERNAME`+`MONGO_PASSWORD` | MongoDB connection |
| `GH_CONTENT_TOKEN` | GitHub token with write access to the `blog` repo |
### Key optional variables
| Variable | Default | Purpose |
|---|---|---|
| `PUBLICATION_URL` | `https://blog.giersig.eu` | Canonical blog URL |
| `INDIEKIT_URL` | same as `PUBLICATION_URL` | Application base URL |
| `REDIS_URL` | — | Redis for AP message queue + KV store (production-required for persistence) |
| `INTERNAL_FETCH_URL` | `http://localhost:PORT` | Direct Indiekit URL for self-fetches, bypassing nginx |
| `INDIEKIT_BIND_HOST` | `0.0.0.0` | Jail IP to bind on (FreeBSD jails don't have loopback) |
| `PORT` | `3000` | Listen port |
| `AP_HANDLE` | `svemagie` | ActivityPub username |
| `AP_ALSO_KNOWN_AS` | — | Old Mastodon account URL for migration alias |
| `AP_LOG_LEVEL` | `info` | Fedify log verbosity |
| `BLUESKY_HANDLE` | — | Bluesky handle for syndication |
| `BLUESKY_PASSWORD` | — | Bluesky app password |
| `WEBMENTION_IO_TOKEN` | — | Webmention.io API token |
| `YOUTUBE_OAUTH_CLIENT_ID` | — | YouTube OAuth client ID for likes sync |
| `YOUTUBE_OAUTH_CLIENT_SECRET` | — | YouTube OAuth client secret |
| `LASTFM_API_KEY` | — | Last.fm API key |
| `LASTFM_USERNAME` | — | Last.fm username |
| `FUNKWHALE_INSTANCE` | — | Funkwhale instance URL |
| `FUNKWHALE_USERNAME` | — | Funkwhale username |
| `FUNKWHALE_TOKEN` | — | Funkwhale API token |
---
## Production deployment
### Startup script (`start.example.sh`)
Copy `start.example.sh` to `start.sh` and make it executable. It:
1. Loads `.env` via `dotenv`
2. Validates required variables (`SECRET`, `PASSWORD_SECRET`, `MONGO_URL` or credentials, `GH_CONTENT_TOKEN`) and exits with a clear error if any are missing
3. Runs all preflight scripts (`scripts/preflight-*.mjs`) in order
4. Runs all patch scripts (`scripts/patch-*.mjs`) in order
5. Starts Indiekit via `node --require ./metrics-shim.cjs node_modules/.bin/indiekit serve`
6. Launches the webmention poller as a background subprocess (polls `WEBMENTION_SENDER_POLL_INTERVAL` seconds; default 300)
7. Traps `EXIT`/`INT`/`TERM` to cleanly shut down the poller when Indiekit exits
The webmention poller connects **directly to Indiekit** at `INDIEKIT_BIND_HOST:PORT` (not through nginx). nginx returns HTTP 444 for requests with an unrecognised `Host` header; the poller's `Host: <jail-ip>` would match nothing and silently fail.
### FreeBSD rc.d service (`indiekit.rcd.example`)
Copy `indiekit.rcd.example` to `/usr/local/etc/rc.d/indiekit` (in the node jail) and enable it with `sysrc indiekit_enable=YES`. The script:
- Uses `daemon(8)` with `-P <pidfile> -p <child_pidfile> -o <logfile>` so both the supervisor and the Node process are tracked
- Supports a configurable stop timeout (`indiekit_stop_timeout`, default 30 s) before sending SIGKILL
- Supports `service indiekit reload` (sends SIGHUP) for configuration reloads without a full restart
### Prometheus metrics (`metrics-shim.cjs`)
A CommonJS preload module that runs a Prometheus scrape endpoint on port 9209 (configurable via `METRICS_BIND_HOST` and `METRICS_PORT`). Loaded at startup via `node --require ./metrics-shim.cjs`. Exposes basic Node.js process metrics for scraping by a Prometheus instance.
--- ---