Posts were marked as webmention-sent with 0/0/0 results during the
SyntaxError period. The v7 migration already ran before those posts
were processed, so bump to v8 to reset them for retry now that the
livefetch patch is fixed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The nested template literal in the patch output had over-escaped regex
and backticks (\\\\/ instead of \\/, \\\` instead of \`), producing
invalid JS that caused a SyntaxError at runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The livefetch patch was fetching postUrl (the public HTTPS URL) which
fails behind the nginx jail. Now rewrites to INTERNAL_FETCH_URL so the
live page fetch goes through nginx on the internal network. Without
this, likes/reposts/bookmarks fall back to stored content which has no
microformat links, resulting in 0 webmentions sent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The retry and livefetch patches both target the same upstream code block.
If retry runs first (current postinstall order), it transforms the code
into a variant that livefetch couldn't match — silently losing the
"always fetch live page" behavior. Now livefetch detects both the
original upstream code and the retry-patched variant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Retry behavior is already covered by the livefetch patch, but keeping
this patch ensures the skip-on-failure guard applies even if livefetch
is removed or the upstream code changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The public URL (blog.giersig.eu) is reachable from inside the jail, so
the INTERNAL_FETCH_URL rewriting approach was unnecessary and caused 400/502
errors because 10.100.0.10 does not serve the static blog pages.
Simplify the livefetch patch to fetch postUrl directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When rewriting the public URL to INTERNAL_FETCH_URL for jailed setups,
the Host header becomes the internal IP (e.g. 10.100.0.10) instead of
the public hostname (blog.giersig.eu). nginx uses the Host header for
virtual host routing and returns 400 without the correct value.
Fix: extract the host from the original public postUrl and set it as the
Host header on the internal fetch, so nginx routes to the correct vhost.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The livefetch patch used `process.env.PUBLICATION_URL || process.env.SITE_URL`
to determine the public base URL for rewriting to INTERNAL_FETCH_URL. Neither
env var is set — the publication URL comes from indiekit.config.mjs which has
a hardcoded fallback. Without a publicBase, URL rewriting never fired, the
public HTTPS URL was fetched directly, failed inside the FreeBSD jail, and fell
back to stored content.
For interaction posts (repost/bookmark/reply/like), stored content is just the
body text — the target URL (repostOf, bookmarkOf, inReplyTo, likeOf) is only
rendered in the live page via reply-context.njk. So 0 webmentions were sent.
Fix: add `|| siteUrl` as fallback. siteUrl is already in scope (derived from
`publication.me`) and is the correct value when env vars are absent.
Also bump reset-stale migration to v7 so interaction posts incorrectly marked
with 0 results (e.g. 342a5 repost) are reset and retried on next startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Posts processed before the livefetch patch was applied were re-processed
without live page fetching, leaving them with empty results again.
Bumping the migration ID triggers another reset on next startup so the
poller retries them with livefetch active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a post was processed but had no discoverable external links, the
expanded detail row rendered completely blank — result.details was
truthy ({}) so the 'noDetails' fallback never fired, but all three
arrays were empty so no tables rendered either.
Adds a patch script for the template that shows "No external links
discovered in this post." in that case, and wires it into both
postinstall and serve scripts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The patch script was corrupted (nested snippets embedded inside template
literals). Reconstructed it cleanly with only the two original specs
(scheduler + conversations controller). The webmention-sender backend
patch is removed — self-Bluesky filtering belongs in the blog sidebar
widget, not the backend dashboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
uploadMedia, uploadImageFromUrl, and fetchOpenGraphData all fetch from
the blog's public URL which is unreachable behind the nginx jail. Rewrite
own-domain URLs to http://localhost:PORT, same as micropub-fetch-internal-url.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The webmention sender livefetch patch was fetching the public HTTPS URL
which hangs in the jailed setup (port 443 is on the nginx jail). Rewrite
to localhost like all other internal-fetch patches, and add a 15s
AbortController timeout. Bump reset-stale migration to v5 so posts
incorrectly marked as sent with 0/0/0 get retried.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The webmention sender was using stored post content (just the body text)
instead of the live page, missing template-rendered links like
u-in-reply-to, u-like-of, u-bookmark-of. This caused reply/like/bookmark
posts to be marked as sent with 0 webmentions. Bump reset-stale migration
to v4 so affected posts are retried.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /rssapi transformation was missing feedTitle, sourceTitle and author
fields that the /news page expects. Map them from the nested blog object.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add localhost rewrite for three more self-referential fetches:
- indieauth.js: token exchange during login
- token.js: token introspection on every authenticated request
- media.js: file uploads via media endpoint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The localhost rewrite only covered endpoint-syndicate and endpoint-share.
Add coverage for 4 more files that also self-fetch via the public URL:
- @rmdes/indiekit-endpoint-microsub reader.js (2 fetch calls)
- @rmdes/indiekit-endpoint-activitypub compose.js (2 fetch calls)
- @rmdes/indiekit-endpoint-posts utils.js and endpoint.js
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- patch-ap-allow-private-address: detect allowPrivateAddress in source
- patch-endpoint-posts-ai-cleanup: detect any AI field key variant
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Node can't reach its own public HTTPS URL (ECONNREFUSED 127.0.0.1:443)
because port 443 only exists on the nginx jail. Rewrite self-referential
fetch URLs to http://localhost:3000 in endpoint-posts, endpoint-syndicate,
and endpoint-share.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-add PeerTube View activity patches that were prematurely removed in
e52e98c5c — the upstream fork doesn't reliably include these fixes on
all server deployments, causing noisy "Unsupported activity type" errors.
Also add fetch diagnostic patch to surface the real cause of
"TypeError: fetch failed" when posting articles via the form controller.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tags like `on/art/music` are reduced to their last segment (`#music`)
in both buildPlainTags and buildFedifyTags so ActivityPub hashtags are
valid on Mastodon and other platforms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hyphenated keys (ai-text-level, ai-code-level, etc.) from the upstream
beta.41 endpoint are now unconditionally deleted on form submission,
preventing them from coexisting with the camelCase equivalents in MongoDB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four patch scripts were warning when they couldn't find their target snippets
in @indiekit/endpoint-posts beta.41, because beta.41 already ships those
features natively:
- patch-endpoint-posts-ai-cleanup: beta.41 form.js has native AI field
cleanup — detect via "ai-text-level" presence and skip silently
- patch-endpoint-posts-search-tags: beta.41 posts.js/posts.njk have native
filter/sort/search — detect via buildFilterQuery / posts-filter-row
- patch-endpoint-posts-uid-lookup: beta.41 utils.js uses direct MongoDB
queries (getPostProperties) — skip silently
- patch-preset-eleventy-ai-frontmatter: add v5 block matching the new
upstream structure (mpUrl + URL pathname normalization)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
blog.giersig.eu resolves to 10.100.0.10 (private LAN) from the server,
causing Fedify's SSRF guard to block lookupObject() and WebFinger calls
for own posts when processing incoming ActivityPub activities.
Adds patch-ap-allow-private-address.mjs which sets allowPrivateAddress: true
on createFederation(), wired into both postinstall and serve scripts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before the raw-body digest fix, every Mastodon inbox delivery was
rejected with a Digest mismatch. Mastodon queued those activities for
retry. After the digest fix, the retried deliveries arrive with their
original HTTP Signatures which are now > 1 hour old. Fedify's default
signatureTimeWindow: { hours: 1 } rejects them with "Date is too far
in the past", logged as "Failed to verify the request's HTTP Signatures."
Extending to 12 hours allows those retried deliveries to be accepted.
The signature still must be cryptographically valid — only the replay
window is relaxed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>