384 Commits

Author SHA1 Message Date
Sven
02919b6e37 fix: patch resolvePost to match post URLs with or without trailing slash
The Fedify object dispatcher constructs the post lookup URL from the
{+id} path variable (e.g. "replies/bd78a"), which has no trailing slash.
Posts in MongoDB store their URL with a trailing slash, so the exact
findOne() match was silently returning null → Fedify serving 404 →
mountains.social showing "Could not connect to the given address".

Fix uses $in to try both variants so the dispatcher works regardless
of whether the request URL has a trailing slash or not.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 15:17:27 +01:00
Sven
a42a6b6d45 fix: add AP inbox digest patch and AP URL lookup API for fediverse interaction
- patch-ap-inbox-raw-body-digest: preserve raw request bytes through the
  AP inbox buffer guard so Fedify's HTTP Signature Digest verification
  passes (JSON.stringify re-encoding broke SHA-256 digest check, causing
  Mastodon likes/replies/boosts to be silently rejected)
- patch-ap-url-lookup-api: add GET /activitypub/api/ap-url endpoint that
  maps a blog post URL to its Fedify-served AP object URL, enabling
  reliable content negotiation for authorize_interaction redirects
- wire both patches into postinstall and serve scripts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 15:00:13 +01:00
Sven
d2573146a7 feat: default ActivityPub syndication when force-syndicating from backend
When the backend Syndicate button is pressed on a post with no
mp-syndicate-to and no prior syndication URLs, fall back to targets
with checked:true (e.g. ActivityPub) instead of no-oping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:02:52 +01:00
Sven
4af7c49c6a feat: inline card template in /posts to show tags inside cards
Non-garden tags shown as small chips in the card body (above footer).
The 'garden' tag shown in the card footer on the right side of the
publish-state badge, using margin-left:auto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:14:32 +01:00
Sven
65f94da71c fix: remove broken regex-escape snippet from micropub source-filter patch
The template-literal double-backslash escaping produced malformed JS in
the injected code, causing a SyntaxError at startup. Replace the
regex-escape helper with a direct String(searchParam) pass-through —
safe for an admin-only search interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:03:53 +01:00
Sven
a52d93392f feat: add search and tag filtering to /posts list
- patch-endpoint-micropub-source-filter: support ?category= and ?search=
  query params in the Micropub ?q=source endpoint, filtering MongoDB
  documents by properties.category and a case-insensitive regex across
  name/content fields
- patch-endpoint-posts-search-tags: forward category/search params from
  the posts controller to Micropub, expose tagLinks on each item, and
  replace the posts.njk cardGrid with a custom loop that renders clickable
  tag chips and a search form above the grid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 11:58:36 +01:00
Sven
ee1313fb26 ci: upgrade to actions/checkout@v5 and setup-node@v5, node 18→22 2026-03-15 08:52:30 +01:00
Sven
b6bf84dcd4 fix: patch webmention-sender scope.find crash when post has no h-entry
Replace `contentRoot ?? $` with `contentRoot ?? $.root()` in webmention.js.
When a post has no .h-entry, <article>, or <main>, contentRoot is null and
`$` is a Cheerio constructor function — not a Cheerio object — so .find()
throws "scope.find is not a function". $.root() returns the document root
as a proper Cheerio object that supports .find().

Rewrites patch-webmention-sender-content-scope.mjs for the new 1.0.7 package
shape (content-scope logic already baked in, only the $.root() fix needed)
and registers it in postinstall + serve.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 23:23:46 +01:00
Sven
b24d38ba74 docs: add changelog section to README for last 20 commits
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 23:19:45 +01:00
Sven
d3fb055df4 chore: upgrade checkout and setup-node actions to v4
Addresses Node.js 20 deprecation warning in GitHub Actions runners.
actions/checkout and actions/setup-node v4 use Node.js 24-compatible
runtimes, ahead of the June 2026 forced migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 23:17:18 +01:00
Sven
53b40a5edc chore: update comments-locales patch for 1.0.10 template, drop livefetch patch
patch-endpoint-comments-locales: remove viewReplacements and sourceOverrides
since comments.njk was rewritten with Nunjucks macros and badge() — the old
HTML snippets no longer exist. Also drop obsolete locale keys (hiddenBadge,
targetPrefix, paginationLabel, page, of) that the new template doesn't use.

patch-webmention-sender-livefetch: delete — was orphaned (not in postinstall)
and the behavior is intentionally accepted as-is.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 23:12:57 +01:00
Sven
53bb7d363b chore: update @indiekit/* to beta.27, bump endpoint-comments and webmention-sender
- @indiekit/indiekit, @indiekit/store-github: ^1.0.0-beta.25 → ^1.0.0-beta.27
- @rmdes/indiekit-endpoint-comments: 1.0.0 → 1.0.10
- @rmdes/indiekit-endpoint-webmention-sender: 1.0.6 → 1.0.7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 23:12:50 +01:00
Sven
314a08542b fix: buffer ActivityPub body before checking for PeerTube View activities
The previous patch checked req.body?.type === "View" but Express's JSON
body parser ignores application/activity+json, so req.body was always
undefined and the guard never fired.

Fix in two parts:
1. In createFedifyMiddleware: manually buffer and JSON-parse the raw
   request stream for activity+json/ld+json POSTs, storing the result on
   req.body before the type check.
2. In fromExpressRequest: extend the content-type check to include
   activity+json/ld+json so non-View activities are correctly forwarded
   to Fedify with the buffered body.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 22:40:54 +01:00
Sven
3708dd92c3 chore: remove Mastodon syndicator and related patches
Blog is now a native ActivityPub actor; Mastodon syndication via
troet.cafe is no longer needed. Removes the syndicator package,
config vars, patch script, and env example entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 22:31:37 +01:00
Sven
296745fcf8 fix: skip PeerTube View activities before Fedify JSON-LD parse
The .on(View) inbox handler from the previous commit is never reached
because Fedify crashes parsing PeerTube's View activity before dispatch —
its JSON-LD deserialiser doesn't recognise Schema.org InteractionCounter,
throwing "Failed to parse activity: TypeError: Invalid type".

Add a guard at the top of createFedifyMiddleware that short-circuits any
POST with body.type === "View" and returns 200 immediately, bypassing
federation.fetch() entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 22:21:25 +01:00
Sven
f004ecdbcc fix: silently ignore PeerTube View activities in ActivityPub inbox
PeerTube broadcasts non-standard View (WatchAction) activities to all
followers on every video watch. Fedify has no handler for this type,
causing noisy "Unsupported activity type" errors in the federation log.

Adds a no-op .on(View, ...) handler at the end of the inbox listener
chain via the existing patch script mechanism.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 22:12:38 +01:00
Sven
37ac8c0ce9 Revert "chore: remove all occurrences of https://troet.cafe and https://troet.cafe/svemagie"
This reverts commit 5c137f01f3.
2026-03-14 22:00:59 +01:00
Sven
5c137f01f3 chore: remove all occurrences of https://troet.cafe and https://troet.cafe/svemagie 2026-03-14 20:58:42 +01:00
Sven
304c75f64a feat: add gardenStage and ai fields to all post type presets
- gardenStage: added to all post types (article, note, bookmark, like,
  repost, photo, reply, page) so Indiekit preserves it in frontmatter
- aiTextLevel/aiCodeLevel/aiTools/aiDescription: extended from article+note
  to all content post types (bookmark, repost, photo, reply, page)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 18:49:35 +01:00
svemagie
3781503abc fix: register bluesky cursor-fix patch in postinstall and serve scripts 2026-03-14 14:46:51 +01:00
svemagie
655bc736f2 fix: clear stale Bluesky polling cursor to restore interaction ingestion 2026-03-14 14:46:43 +01:00
svemagie
4f1440a634 fix: filter out self-interactions from own Bluesky account 2026-03-14 14:01:44 +01:00
svemagie
f8f595fe8d fix: filter out self-interactions from own Bluesky account 2026-03-14 14:01:38 +01:00
Sven
af7c89b2c5 update: fix npm 2026-03-14 13:56:54 +01:00
Sven
d3c094f91f update: activitypub 2026-03-14 13:53:40 +01:00
Sven
b632af9564 fix(webmention): scope link extraction to .h-entry not .e-content
u-in-reply-to, u-like-of, u-repost-of etc. are rendered in an aside
before the .e-content div, not inside it. Scoping to .h-entry .e-content
caused these microformat links to be missed entirely.

Also bump reset-stale migration to v3 so posts already marked sent with
zero results (like /replies/88feb/) are retried with the corrected scope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 09:40:48 +01:00
Sven
3ca920089b fix: improve microsub feed discovery via <link rel="alternate"> tags
When a bookmarked URL is an HTML page whose feed is not at a common
path (/feed, /rss.xml etc.), fetchAndParseFeed would throw and store
no items in microsub_items.  Sites like econsoc.mpifg.de or signal.org
post pages advertise their feed via a standard
  <link rel="alternate" type="application/rss+xml" href="...">
element, which discoverFeeds() already parses but was never called
from the fetch/parse pipeline.

Now, before probing common paths, fetchAndParseFeed calls discoverFeeds()
on the fetched HTML and uses any typed RSS/Atom/JSONFeed link it finds.
Common-path probing remains as the final fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 09:27:22 +01:00
Sven
0dc71d1922 fix: pre-fill reference URL when creating post from /news entry
share-post.js opens /posts/create?type=like&url=<link>&name=<title>
but postData.create only reads request.body, ignoring the query params.

Patch postData.create: when properties is empty and ?url= is present,
seed properties with the correct field name per post type:
  like     → like-of
  bookmark → bookmark-of  (also seeds name from ?name=)
  reply    → in-reply-to
  repost   → repost-of

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 07:23:36 +01:00
Sven
1d28df8d04 fix: post edit 404 — query micropub source by _id not paginated scan
getPostProperties queries ?q=source (no filter, default limit=40) then
scans items for a uid match. Posts outside the 40 most recent return
undefined → IndiekitError.notFound (404).

Fix:
- Patch micropub query controller: when ?q=source&uid=<objectId> is
  present, findOne({ _id: getObjectId(uid) }) directly and return
  { items: [mf2] } so it is compatible with getPostProperties.
- Patch getPostProperties to append uid= to the micropub source URL,
  so any post can be fetched regardless of recency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 20:45:12 +01:00
Sven
3979e12e8b fix: use express.Router() not require() in ESM patch
The blogroll index.js is an ES module so require() is not defined.
Replace `const { Router } = require("express")` with `express.Router()`
which is already in scope from the module's own top-level import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 19:20:53 +01:00
Sven
4cde4a28ba fix: transform /rssapi response shape to match /news static page
The /news page template uses item.link, item.feedId, item.sourceUrl,
and feedsRes.feeds — but the blogroll API returns item.url, item.blog.id,
item.blog.siteUrl, and {items:[...]} respectively.

Add a response-transformer middleware to the /rssapi alias router that:
- maps url -> link on each item
- maps blog.id -> feedId, blog.siteUrl -> sourceUrl on each item
- renames items -> feeds for the /api/feeds endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 19:16:56 +01:00
Sven
c4299b73b1 fix: dual-mount blogroll at /blogrollapi and /rssapi
/blogroll static page hardcodes /blogrollapi/api/* calls.
/news static page hardcodes /rssapi/api/* calls.
Both must work simultaneously.

- Revert mountPath to /blogrollapi (restores /blogroll page)
- Patch init() to also register publicRouter at /rssapi via a
  thin Indiekit.addEndpoint() alias (fixes /news page)
- /api/feeds alias retained for /news page (maps to listBlogs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 19:09:48 +01:00
Sven
d411a87161 chore: update lock file for activitypub 2.8.2 2026-03-13 18:59:18 +01:00
Sven
974d76b988 chore: switch activitypub to npm registry ^2.8.2
The github: source was pinned to 2.8.0 in the lock file but the repo
HEAD is now at 2.8.2, causing npm ci to fail. Switch to the versioned
npm package to keep the lock file stable.

Lock file needs regeneration: run npm install locally and commit
the updated package-lock.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 18:58:48 +01:00
Sven
64e5f99526 fix: mount blogroll at /rssapi and add /api/feeds alias
The /news/ page fetches /rssapi/api/items, /rssapi/api/feeds and
/rssapi/api/status. The blogroll endpoint was mounted at /blogrollapi,
so all three requests returned a 404 HTML page — causing the
"Unexpected token '<'" JSON parse error.

- Change blogroll mountPath from /blogrollapi to /rssapi
- Add patch-endpoint-blogroll-feeds-alias.mjs: injects a /api/feeds
  route alias pointing to listBlogs (page expects /feeds, endpoint
  only had /blogs)
- Wire new patch into postinstall and serve scripts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 18:54:24 +01:00
svemagie
bf1991d82c fix: scope webmention link extraction to post content only
Adds patch-webmention-sender-content-scope.mjs to restrict extractLinks()
to .h-entry .e-content / .e-content / article / main — preventing sidebar,
nav, and footer links from the live-fetched full page from being included.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 08:34:05 +01:00
svemagie
5df7314f8d chore: integrate AP patches into fork — remove 3 scripts, trim federation-unlisted-guards
Deleted scripts (logic now built into the fork):
- patch-endpoint-activitypub-docloader-loglevel.mjs
- patch-endpoint-activitypub-migrate-alias-clear.mjs
- patch-endpoint-activitypub-like-boost-methods.mjs

Trimmed patch-federation-unlisted-guards.mjs to only cover
endpoint-syndicate (separate package). AP unlisted guards are
now in the fork's federation-setup.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 08:07:36 +01:00
svemagie
630edf2b77 fix: drop compose.js docloader patch — now in fork
createPublicationAwareDocumentLoader and rawDocumentLoader wrapping
are built into the fork's compose.js; the patch was re-injecting the
function and causing a duplicate-declaration SyntaxError at startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 07:56:41 +01:00
svemagie
d9566919fc chore: remove like/repost content-negotiation patch (now in fork)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 07:51:45 +01:00
svemagie
593665bcbd feat: switch activitypub endpoint to svemagie fork with DM support
Use svemagie/indiekit-endpoint-activitypub instead of the upstream npm
package to get direct message receive and native AP reply support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 06:33:35 +01:00
svemagie
d35e7b7b28 fix: disable Mastodon external like/repost status posts
Serve like/repost posts as Note objects for AP content negotiation.
Returning a bare Like/Announce activity broke Mastodon's
authorize_interaction because it expects a content object (Note/Article).
Now like posts are served as ❤️ Note and reposts as 🔁 Note.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 14:59:04 +01:00
svemagie
71ee8672f4 fix: never auto-check Mastodon syndicator for like/repost compose actions
The site is its own fediverse actor (@svemagie@blog.giersig.eu). Likes and
reposts send native AP Like/Announce activities directly — troet.cafe has
no role in site-level interaction actions. Mastodon is now only auto-selected
in the compose form for fediverse *replies*, not likes or reposts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 14:03:37 +01:00
svemagie
77dd85b507 fix: disable Mastodon external like/repost status posts
AP endpoint already sends native Like/Announce activities to fediverse
authors, making the "❤️ URL" / "🔁 URL" Mastodon status posts redundant.
Disabling syndicateExternalLikes + syndicateExternalReposts prevents the
"did not return a URL" error from the scope-limited Mastodon token.

Native same-instance favourites/reblogs still route through Mastodon
(requires write:favourites + write:statuses scope on the token).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 13:58:34 +01:00
svemagie
ad0492ef64 feat: dispatch native AP Like/Announce from fediverse identity on like/repost
- Add likePost() and boostPost() programmatic methods to the ActivityPub
  plugin, sending native Like and Announce activities as @svemagie@blog.giersig.eu
- Wire up AP dispatch in submitCompose (microsub reader) after a successful
  Micropub post creation — fire-and-forget, non-blocking
- Fix detectProtocol to recognise troet.cafe, hachyderm.io, infosec.exchange,
  chaos.social and other fediverse domains not in the original pattern list
- Fix Mastodon syndication target auto-selection to match by service.name
  ("mastodon") and by configured instance hostname, not just uid string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 13:44:09 +01:00
svemagie
270249bf83 feat: update blogroll + microsub forks with unified bookmark flow
Both plugins now implement the intended bookmark→feed pipeline:

Microsub:
- tag → find/create channel (no more fallback-only to "Bookmarks")
- stores micropubPostUrl on feed for update/delete tracking
- calls notifyBlogroll() after bookmark-import (blogroll via microsub, not independently)
- moves feed when tag changes (delete from old channel, create in new)
- handles micropub update action: detects tag change or bookmark-of removal

Blogroll:
- skips direct bookmark import when microsub is installed (avoids duplicates)
- acts as fallback importer when microsub is absent
- updates category in-place when existing entry's tag changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 09:35:46 +01:00
svemagie
3f367610f5 fix: force one-time ai: block resync for posts with stale files
The v3 patch bug allowed Micropub to update MongoDB with aiTextLevel/
aiCodeLevel values but write post files without the ai: frontmatter
block (supportsAiDisclosure was always false). Re-saving with the same
values correctly returned "no properties changed" — but the file on disk
remained stale.

New patch (patch-micropub-ai-block-resync.mjs) adds _aiBlockVersion to
each post document in MongoDB. On update, if a post has AI fields but
_aiBlockVersion != "v4", the no-change check is bypassed and the file
is force-rewritten exactly once. Subsequent no-change saves behave
normally.

Also adds AI transparency section to README documenting the full
implementation, patch chain, v4 root cause, and re-save instructions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 09:03:22 +01:00
svemagie
cb1418bae5 feat: replace @rmdes/indiekit-endpoint-microsub with svemagie fork
Switches to github:svemagie/indiekit-endpoint-microsub#bookmarks-import
which adds a contentNegotiationRoutes hook to auto-follow bookmarked
URLs as Microsub feed subscriptions when a bookmark-of post is created.

Mirrors the same pattern already in place for the blogroll fork.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 08:36:55 +01:00
svemagie
5f4d8ca5e8 fix: detect article/note post type via permalink for AI frontmatter
Indiekit's getPostTemplateProperties() explicitly removes the post-type
property before passing JF2 to postTemplate(). The v3 patch relied on
post-type to set supportsAiDisclosure, which was therefore always false —
causing the ai: frontmatter block to never be written regardless of what
was selected in the backend form.

v4 patch falls back to permalink URL pattern (/articles/, /notes/) to
correctly detect the post type when post-type is absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 08:11:28 +01:00
svemagie
0862a266e6 fix: always fetch live page for webmention link extraction
The core bug: stored post content (post.properties.content.html) is only
the post body text. Template-rendered microformat links (u-in-reply-to,
u-like-of, u-bookmark-of, u-repost-of) live in the 11ty HTML output, not
in MongoDB. So replies, likes, bookmarks and reposts never had their target
URLs extracted — webmentions were silently skipped.

- patch-webmention-sender-livefetch: always fetch the live page; fall back
  to stored content only if the page is unavailable; skip (don't mark sent)
  when no content is available so the next poll retries it. Handles both
  original upstream code and the older retry-patch variant.
- patch-webmention-sender-reset-stale: bump to v2 so posts incorrectly
  marked as sent today (empty results due to the content bug) get reset
  and retried on next deploy.
- Remove patch-webmention-sender-retry: superseded by livefetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:41:30 +01:00
svemagie
6a2c38d798 fix: replace startup sleep with readiness check, clean up stale env/config
- start.example.sh: replace fixed sleep 30 with /status poll loop (up to 2min)
  so the webmention poller waits exactly until indiekit is ready, not longer
- indiekit.config.mjs: remove redundant webmentionIoMountPath variable and
  mountPath from webmention-io config (package default /webmentions is correct)
- .env.example: remove all stale proxy and unused WEBMENTION_SENDER_* vars
  (HOST, PORT, ENDPOINT, READY_TIMEOUT, STOP_TIMEOUT, AUTO_POLL) that were
  never read by start.example.sh; keep only WEBMENTION_SENDER_POLL_INTERVAL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:30:55 +01:00