Gitea Contents API DELETE commits fire on:push CI; POST/PUT do not.
delete was triggering both on:push and workflow_dispatch → 2 CI runs.
Now dispatch is skipped for delete; on:push handles the rebuild.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub API infers JSON without Content-Type; Gitea requires it explicitly.
Without the header, Gitea cannot parse the POST/PUT body and returns 422
Unprocessable Entity on all content write operations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gitea's Contents API differs from GitHub's:
POST /contents/{path} = create new file (no SHA)
PUT /contents/{path} = update existing file (SHA required)
store-github used PUT for createFile() because GitHub accepts PUT for
both — Gitea's PUT without SHA returns 422. Also updates the
update-fallback patch to bail to createFile() instead of falling through
to PUT-without-SHA when the file doesn't exist in the store.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Posts that exist in MongoDB but not in Gitea (e.g. due to a previous
failed write) caused HTTP 500 on re-publish: updateFile() tried to read
the file's SHA, got 404, and threw instead of creating. Now detects
Not Found and falls through to a create-style PUT (no sha field).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gitea Contents API commits don't trigger on:push CI workflows.
Patches action.js to fire a workflow_dispatch to giersig.eu/indiekit-blog
after every create/update/delete/undelete so the Eleventy build runs
immediately after a post is published.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
npm fetches git deps before node_modules exist, so git URL rewriting
must happen in preinstall. Detects jail env via INDIEKIT_BIND_HOST /
INTERNAL_FETCH_URL — no-ops on local dev.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded user: githubUsername (svemagie) and repo: "blog" were wrong
for Gitea where the org is giersig.eu and the repo is indiekit-blog.
Now reads from GITEA_CONTENT_USER / GITEA_CONTENT_REPO env vars with
sensible defaults.
Set in .env on server:
GITEA_CONTENT_USER=giersig.eu
GITEA_CONTENT_REPO=indiekit-blog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runner is on the internal network — connecting to the public domain
fails due to hairpin NAT, same as the syndication webhook.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- runs-on: freebsd (act_runner host label) instead of ubuntu-latest
- Drop appleboy/ssh-action; use plain ssh in a run step (same pattern as
indiekit-blog deploy.yml)
- Drop actions/setup-node; no build step on runner side
- On deploy: set git remote to internal Gitea URL, fetch, reset --hard
- npm ci --legacy-peer-deps (postinstall applies all patches automatically)
- .env and SECRET preflight checks; preflight-production-security and
preflight-mongo-connection before restart
- Async restart via nohup + poll loop (avoids SSH hanging on open stdout)
- add workflow_dispatch trigger
Required repo secrets: SSH_PRIVATE_KEY, SSH_USER, SSH_HOST
(copy values from giersig.eu/indiekit-blog repo secrets)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewrote from scratch based on a full read of README.md.
Focus: actionable rules and pitfalls for the AI agent, not human overview.
Covers: patch pattern + rules, two-jail architecture, internal URL
rewriting, nginx/Fedify requirements, MongoDB collections, post-type
discovery table, three compose paths and their checkbox mechanics,
ap_timeline insertion timing, status ID format, JF2→AS2 mapping,
visibility mapping, fork update commands, debugging starting points
(12 symptoms), and full env var reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused replies-to-replies to be posted as 'note' type without
ActivityPub federation:
1. patch-ap-compose-default-checked: The AP reader compose form had
defaultChecked hardcoded to '@rick@rmendes.net' (original dev's handle),
so the AP syndication checkbox was never pre-checked. Fixed to use
target.checked from the Micropub q=config response, which already
carries checked: true for the AP syndicator.
2. patch-ap-mastodon-reply-threading: POST /api/v1/statuses deferred
ap_timeline insertion until the Eleventy build webhook fired (30–120 s).
If the user replied to their own new post before the build finished,
findTimelineItemById returned null → inReplyTo = null → no in-reply-to
in JF2 → post-type-discovery returned 'note' → reply saved at /notes/
and sent without inReplyTo in the AP activity, breaking thread display
on remote servers. Fixed by eagerly inserting the provisional timeline
item immediately after postContent.create() ($setOnInsert — idempotent;
syndicator upsert later is a no-op).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
uploadMedia() had no content-type check, so an HTML login-redirect response
from an auth-protected internal endpoint was uploaded to Bluesky as a blob
with encoding "text/html". uploadBlob() accepts it, but record validation
rejects the post with 'Expected "image/*" (got "text/html")'.
The patch mirrors the guard already present in uploadImageFromUrl() and also
wraps per-photo uploads in try/catch so one bad photo doesn't abort the
entire syndication — other photos and the post text are still published.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without nginx forwarding Host/X-Forwarded-Proto headers, fromExpressRequest()
builds a wrong URL (e.g. http://127.0.0.1:3000/...) that Fedify doesn't
recognise as its own base URL — so it calls next() and requests fall through
to auth middleware, returning 302 to the login page. This breaks webfinger,
actor lookups, and AP inbox delivery.
The patch overrides the URL construction in createFedifyMiddleware() and
fromExpressRequest() to use the configured publicationUrl as the base,
bypassing the dependency on proxy headers entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/og/{slug}.png is only generated by Eleventy for replies, bookmarks, and
articles — not for photo post types. Photo posts now use properties.photo[0]
directly as the ActivityPub image field; all other post types keep the
/og/{slug}.png fallback. Patch handles all known file states (original
upstream, v1 patched, already v2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The OLD_SNIPPET had wrong indentation (8 spaces vs 6) and was missing the
method guard line added in the fork:
if (req.method !== "GET" && req.method !== "HEAD") return next();
Without this fix the patch silently skips patching and webfinger continues
to return 302 → 401 on fediverse delivery.
https://claude.ai/code/session_0124D41vdLYE3DkJxhPqYthX
The Gitea conflict resolution kept main's prom-client/metrics-shim/microsub
changes but dropped our two new AP patch registrations. Re-add them to both
postinstall and serve.
https://claude.ai/code/session_0124D41vdLYE3DkJxhPqYthX
- Add scripts/patch-ap-og-image.mjs to both postinstall and serve so OG
preview images are included in ActivityPub activities (flat URL slug
extraction instead of date-based regex).
- Add scripts/patch-ap-webfinger-before-auth.mjs (new file) to both
postinstall and serve. Extends contentNegotiationRoutes Fedify delegation
to also forward /.well-known/* paths, ensuring webfinger is served by
Fedify before indiekit auth middleware can issue a 302 redirect. Fixes
401 Unauthorized errors from remote fediverse instances.
https://claude.ai/code/session_0124D41vdLYE3DkJxhPqYthX
Preloads metrics-shim.cjs via `node --require` into the Indiekit process
so heap, GC, event loop lag, CPU and handle metrics are exposed at
:9209/metrics for Prometheus scraping. Uses prom-client collectDefaultMetrics.
- Add metrics-shim.cjs (prom-client HTTP server, port 9209)
- Add prom-client ^15.1.3 to dependencies
- Wire --require ./metrics-shim.cjs into start.example.sh and npm serve script
- Grafana: NodeJS Application Dashboard (11159) at console.giersig.eu
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nginx HTTP/80 returns 301 → HTTPS; pf has no hairpin NAT for jail traffic,
so following the redirect causes UND_ERR_SOCKET on every internal POST.
Correct value: http://10.100.0.20:3000 (direct to node jail).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`getEndpointUrls()` resolved relative endpoint paths (e.g. `/media`) using
`getUrl(request)`, which returns `http://` because Express sees HTTP from nginx
without trust proxy. This produced `http://blog.giersig.eu/media` as the
endpoint attribute in the file-input component, causing Safari to block the
fetch as mixed content ('Load failed').
Fix: prefer `application.url` (the configured HTTPS base URL) over
`getUrl(request)` when resolving relative endpoint paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nginx port 80 returns 444 (no response) for requests with an unrecognised
Host header. The poller's curl sends Host: 10.100.0.10 (the IP) which
doesn't match any server_name, causing the 180s readiness timeout and
"empty reply from server" on every poll.
Since livefetch v6 builds synthetic HTML from stored properties and no
longer fetches live pages, the poller no longer needs to go through nginx.
It now connects directly to Indiekit on INDIEKIT_BIND_HOST:PORT.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>