Users can favorite instances (with local or federated scope) as persistent
columns in a multi-column deck view. Each column streams its own public
timeline with independent infinite scroll. Includes two-tab explore UI
(Search + Decks), deck CRUD API with CSRF/SSRF protection, 8-deck limit,
responsive CSS Grid layout, and scope badges.
Register resolveActorAvatar() on Indiekit.config.application during
init(). Uses Fedify's authenticated document loader to fetch actor
profiles from servers with Authorized Fetch enabled (e.g., hachyderm.io,
indieweb.social). Called by the conversations plugin's avatar backfill.
- Add FediDB API client (lib/fedidb.js) with MongoDB caching (24h TTL)
for instance search, timeline support checks, and popular accounts
- Explore page: instance input now shows autocomplete suggestions from
FediDB with software type, MAU count, and timeline support indicator
(checkmark/cross) via background pre-check
- Reader page: @handle lookup input now shows popular fediverse accounts
from FediDB with avatar, name, handle, and follower count
- Three new API endpoints: /api/instances, /api/instance-check,
/api/popular-accounts
- Alpine.js components for both autocomplete UIs with keyboard navigation
- Fix mentions/hashtags bug: separate Fedify Mention and Hashtag types into
distinct mentions[] and category[] arrays with proper @ and # rendering
- Add hashtag timeline filtering at /admin/reader/tag with regex-safe queries
- Replace prev/next pagination with AlpineJS infinite scroll (IntersectionObserver)
with no-JS fallback pagination preserved
- Add public instance timeline explorer at /admin/reader/explore with SSRF
prevention and XSS sanitization via Mastodon-compatible API
- Add hashtag following with ap_followed_tags collection, inbox listener
integration for non-followed accounts, and followed tags sidebar display
- Include one-time migration script for legacy timeline data
Moved extractActorInfo() before logActivity() in Like, Announce, and
Reply handlers so the actor's avatar URL is persisted in ap_activities.
Previously only stored in ap_notifications, leaving conversation_items
without photos for non-followers.
Pass ctx.getDocumentLoader({ identifier: handle }) to every .getActor(),
.getObject(), and .getTarget() call in inbox handlers. This signs outbound
fetches with our actor's key, fixing silent failures against Authorized
Fetch (Secure Mode) servers like hachyderm.io.
The authenticated loader is also threaded through extractObjectData() and
extractActorInfo() in timeline-store.js so internal calls to
.getAttributedTo(), .getIcon(), .getTags(), and .getAttachments() also
use signed requests.
Also removes the endpoints.type workaround in federation-bridge.js since
Fedify 2.0 fixed issue #576 upstream. The attachment array workaround
for Mastodon compatibility remains.
Bumps version to 2.0.26.
Add OStatus subscribe template to WebFinger responses so remote servers
(WordPress AP, Misskey, etc.) can discover and redirect users to complete
follow interactions. Unauthenticated users are sent to login first, then
redirected to the existing reader profile page with follow/unfollow UI.
Mastodon 4.5's extract_url_from_html requires element['href'] ==
element.text for a field to be verifiable. Using @handle@domain as
display text caused value_for_verification to return nil, making
the field permanently unverifiable.
Change to using the full actor URL as display text so the href and
text content match.
Always include a "Fediverse" PropertyValue attachment in the actor
with the canonical @handle@domain address. This ensures 2+ attachments
when combined with user-defined fields, preventing Fedify's JSON-LD
compaction from collapsing single-element arrays to plain objects
(which Mastodon's update_account_fields silently rejects).
Also fixes the root cause of profile fields not appearing on Mastodon
for existing followers: Update(Person) activities were being sent with
compacted attachment objects that Mastodon ignored.
Fedify's JSON-LD compaction collapses single-element arrays to plain
objects. Mastodon checks `attachment.is_a?(Array)` and silently skips
non-array values, causing profile links to never display.
Also adds profile links section to the my-profile admin page and
fixes rel=me on the public profile page for bidirectional verification.
The | min filter is Jinja2 syntax, not available in Nunjucks. This caused
"filter not found: min" crashes when posts had photos (never triggered
before the async iteration fix because photo arrays were always empty).
Post-detail view now re-fetches from the origin server when the stored
timeline item has empty photo/video/audio arrays (from before the async
iteration fix). On successful extraction, updates MongoDB so future
views don't need to re-fetch.
MongoDB collections may not be available yet when init() runs if the
database connection hasn't completed. Wrap all createIndex calls in
try-catch so the plugin doesn't crash on startup. Indexes already exist
from previous runs; this is non-fatal.
Fedify 2.0's getAttachments() and getTags() return async iterables, but the
code used synchronous for...of which silently yielded zero results. Changed
to for await...of so media URLs (photo/video/audio) and hashtags are now
properly extracted from incoming posts.
Also replaced the gallery's target=_blank links with an Alpine.js lightbox
modal for full-size image viewing with prev/next navigation and keyboard
support.
dropIndex() was called with await inside the non-async init() method,
causing "Unexpected reserved word" and preventing Indiekit from starting.
Use promise .catch() instead since the result isn't needed.
Moderation page rewritten as single Alpine.js component with inline DOM
updates instead of location.reload(). Added hide/warn filter mode toggle
— warn mode shows muted items behind content warning instead of hiding.
Expanded keyword matching to check content, titles, and summaries.
Fixed MongoDB E11000 duplicate key error by dropping non-sparse indexes
on startup and recreating with sparse:true. Storage layer no longer
stores null url/keyword fields.
The replies tab was empty because it queried ap_activities for outbound
Create activities with a non-null targetUrl, but targetUrl was always null
(remote actor resolution often fails). Now queries posts collection for
post-type "reply" which reliably has in-reply-to URLs.
Also fixes activity log to store in-reply-to URL as targetUrl instead of
the resolved actor URL.
- Notification view: tab navigation (Replies, Likes, Boosts, Follows, All)
with count badges; defaults to Replies tab; type filter in storage layer
with compound index for efficient queries
- My Profile admin page: profile header with avatar/stats/bio, tabbed
activity view (Posts, Replies, Likes, Boosts) pulling from posts,
ap_activities, and ap_interactions collections
- Reader: default tab changed from All to Notes
- Timeline cards: timestamps now link to post detail view
- Notification cards: Reply and View Thread buttons on reply/mention types
Override upstream .mention { display: grid } that broke Mastodon's
hashtag/mention HTML in profile bios. Fixes both admin reader
(.ap-profile__bio) and public profile (.ap-pub__bio) views.
Remote servers (Mastodon, Bonfire) dereference Note IDs to verify
Create activities. Quick reply Notes had no public route — servers
got 302 to login and rejected the activity.
- Store quick reply Note data in ap_notes collection
- Add public GET /quick-replies/:id serving JSON-LD
- Use shared resolveAuthor() in compose.js for quick replies
Remote server timeouts during Announce.getObject() produced 20-line
stack traces. Now logs a single warning line with the cause code.
No behavior change — unreachable boosts were already skipped.
When lookupObject fails (Authorized Fetch, network issues) and the post
isn't in ap_timeline, likes returned 404 "Could not resolve post author".
Adds shared resolveAuthor() with 3 strategies:
1. lookupObject on post URL → getAttributedTo
2. Timeline + notifications DB lookup
3. Extract author from URL pattern (/users/NAME/, /@NAME/)
Refactors like, unlike, boost controllers to use the shared helper.
Quick replies only sent to followers, never directly to the
replied-to author's server. The author was also missing from
the Note's cc field, so Mastodon couldn't thread or notify.
Now resolves the author before constructing the Note, includes
them in ccs, sends directly to their inbox, and logs failures
instead of silently swallowing them.
Boost (Announce) was missing to/cc addressing so Mastodon silently
discarded it. Both boost and like used urn:uuid: IDs which are not
dereferenceable. Changed to HTTPS URLs and added Public/followers
addressing on Announce.
Fedify 2.0 renamed the Note/Article constructor parameter from
inReplyTo to replyTarget. The old name was silently ignored,
causing replies to appear as standalone posts on the fediverse.
Mastodon silently discards activities with urn:uuid: IDs since they
can't be dereferenced. Use an HTTPS URL under our domain instead.
Also add to/cc (Public + followers) on the Create wrapper activity,
not just the Note object — Mastodon requires addressing on both.
Remote servers (Mastodon, etc.) require explicit audience addressing
to display a post. Without to/cc, the Note was silently discarded.
- to: as:Public (visible to everyone)
- cc: followers collection
- Upgrade @fedify/fedify, @fedify/redis to ^2.0.0
- Add @fedify/debugger ^2.0.0 for live federation traffic dashboard
- Move all vocab type imports to @fedify/fedify/vocab (13 files)
- Move crypto imports (exportJwk, importJwk, generateCryptoKeyPair) to @fedify/fedify/sig
- Replace removed importSpki() with local Web Crypto API helper
- Add KvStore.list() async generator required by Fedify 2.0
- Add setOutboxPermanentFailureHandler for delivery failure logging
- Add debugDashboard/debugPassword config options
- Skip manual LogTape configure when debugger auto-configures it
- Fix Express-Fedify bridge to reconstruct body from req.body when
Express body parser has already consumed the stream (fixes debug
dashboard login TypeError)
- Add response.bodyUsed safety check in sendFedifyResponse
- Remove @fedify/express dependency (custom bridge handles sub-path mounting)
Replace the browser redirect on /activitypub/users/:handle with a
standalone HTML profile page showing avatar, bio, profile fields,
stats (posts/following/followers/joined), follow-me prompt with
copy button, pinned posts, and recent posts. Supports light/dark
mode via prefers-color-scheme. ActivityPub clients still get JSON-LD
from Fedify before this route is reached.
broadcastActorUpdate() now fetches followers from MongoDB, deduplicates
by shared inbox, and delivers in batches of 25 with 5s delays to prevent
thundering herd (hundreds of 499s from simultaneous re-fetches).
Browser GET on /users/:handle now redirects to homepage instead of 404.
ctx.getActor() only exists on RequestContext (inside HTTP handlers), not
on the base Context returned by createContext(). Extracted actor-building
logic into shared buildPersonActor() helper used by both the dispatcher
and broadcastActorUpdate(). Profile link attachments now propagate to
remote instances via Update(Person) activity.
Adds a search box at the top of the reader page where users can paste
any fediverse URL or @user@domain handle. Uses Fedify's lookupObject()
which natively resolves URLs, handles, and acct: URIs, then redirects
to the internal post detail or remote profile view.
document.njk already renders title as h1 via the heading macro.
All 14 AP templates were also calling heading() with level 1 inside
their content block, producing two h1 elements per page. Removed
the redundant calls and moved dynamic count prefixes into the title
variable in followers/following controllers.
- Fix cursor pagination: use string comparison (not Date objects) for
published field queries in both timeline and notifications
- Fix "Older" cursor to use oldest item's date, not newest
- Remove redundant parent breadcrumb from all AP page headings
- Reorder tabs: Notes first, All last
- Fix avatar loading: non-destructive hide/show with lazy loading
- Add actor avatars with type badge overlay to notification cards
- Add Fediverse navigation group in sidebar
Reader now resolves ActivityPub links internally instead of navigating
to external instances. Actor links open the profile view, post links
open a new post detail view with thread context (parent chain + replies).
External links in post content get rich preview cards (title, description,
image, favicon) fetched via unfurl.js at ingest time with fire-and-forget
async processing and concurrency limiting.
New files: post-detail controller, og-unfurl module, lookup-cache,
link preview template/CSS, client-side link interception JS.
Includes SSRF protection for OG fetching and GoToSocial URL support.
- Fix Unknown authors by adding multi-strategy fallback chain in
extractObjectData (getAttributedTo → actorFallback → attributionIds)
- Fix empty boosts from Lemmy/PieFed by checking content before storing
- Fix @mention/hashtag styling to stay inline instead of breaking layout
- Fix compose reply to show sanitized HTML blockquote instead of raw text
- Add default-checked syndication targets for AP and Bluesky
- Use authenticated document loader for all lookupObject calls
(fixes 401 errors on servers requiring Authorized Fetch)
- Fix like handler 404 by using canonical AP uid for interactions
instead of display URLs; add data-item-uid to card template
- Fix profile bio showing Nunjucks macro source code by renaming
summary→bio to avoid collision with Indiekit's summary macro
- Fix Fedify API misuse in timeline-store.js: use instanceof Article
(not string comparison), replyTargetId (not inReplyTo), getTags()
and getAttachments() async methods (not sync property access)
- Fix inbox-listeners.js: use replyTargetId instead of non-existent
getInReplyTo(), use instanceof Article for Update handler
- Add error logging to interaction catch blocks
express.urlencoded({ extended: true }) uses qs which strips [] from
field names, so link_name[] arrives as request.body.link_name — not
request.body["link_name[]"]. The old lookup always got undefined,
producing an empty attachments array that overwrote existing links.