Skip to content

“Creating an image sharing service” tutorial#731

Open
dahlia wants to merge 67 commits intofedify-dev:mainfrom
dahlia:docs/tutorial/content-sharing
Open

“Creating an image sharing service” tutorial#731
dahlia wants to merge 67 commits intofedify-dev:mainfrom
dahlia:docs/tutorial/content-sharing

Conversation

@dahlia
Copy link
Copy Markdown
Member

@dahlia dahlia commented Apr 27, 2026

This PR adds a second end-to-end tutorial for Fedify, parallel to the existing Creating your own federated microblog, that teaches Fedify through a Pixelfed-style federated image-sharing service built on Nuxt 4 and the new @fedify/nuxt integration shipping in 2.2.0. Resolves #693.

Read it at https://pr-731-0.fedify.pages.dev/tutorial/content-sharing.

For reviewers

A few things to be aware of before you try the tutorial yourself:

  • Fedify 2.2.0 is not released yet. The tutorial is written against the 2.2 line and several chapters rely on changes that have not made it to the latest stable. Install the latest prerelease before running anything—at the time this PR was opened the current build is 2.2.0-dev.978+cb3394b9, installable as npm install -g @fedify/cli@2.2.0-dev.978. The released @fedify/cli does not yet ship the -w nuxt template fedify init needs, and fedify tunnel only works correctly with the fix from Fix CLI config fallbacks for Optique 1.0 #728 in the prerelease line.
  • The companion example repository tracks this PR. The tutorial maps chapter-by-chapter to fedify-dev/content-sharing; if you want to compare paste-by-paste, clone that repo alongside this branch. Each chapter that touches code lands as its own commit at the bottom of the log, with a small number of rehearsal-driven follow-ups on top.
  • Federation interop tests need external partners. Chapters 9, 10, 11, 18, 19, 21, 22, and 23 verify activities round-trip against both ActivityPub.Academy and a Pixelfed instance reachable via a public URL (e.g. cloudflared or fedify tunnel). If you do not want to set those up, the Trying it out sections still work as fedify lookup smoke tests for outbound shape—the inbound steps are the parts that need a real partner.
  • Docs build is the canonical local check. pnpm build inside docs/ exercises every twoslash block in the tutorial against the in-tree @fedify/* source, so it catches any divergence between prose snippets and the runtime API. The build takes around 5 minutes on a warm cache.

Background

The microblog tutorial is the canonical “build a federated app from scratch” reference for Fedify, but it is also actor- and timeline-centric. Many readers come to the fediverse from a media-sharing angle (Pixelfed, PeerTube), and the microblog tutorial does not exercise the parts of Fedify that handle image attachments, multipart uploads, post-detail pages with Open Graph metadata, or the Pixelfed-side quirks: Document attachments demanded as an array, Accept(Follow) carrying a freshly-minted Follow id, fragment-URL Notes silently dropped, threaded replies that only attach when cc lists the parent author and the Note carries a matching Mention tag, the likes_count cache lag, and so on. Issue #693 asked for a second tutorial that exercises this surface and doubles as the canonical example for @fedify/nuxt.

What is in this PR

  • docs/tutorial/content-sharing.md — the new tutorial, in line with docs/tutorial/microblog.md in scale and structure. 24 chapters: 23 chapters that touch code plus a closing Where next chapter listing follow-on features.
  • docs/tutorial/content-sharing/ — 30+ screenshots covering the app we build alongside its federation partners (ActivityPub.Academy and a local Pixelfed instance).
  • Sidebar entry in docs/.vitepress/config.mts under the Tutorials section, after the microblog link.
  • Changelog entry under Fedify 2.2.0 in CHANGES.md.

The companion example repository lives at fedify-dev/content-sharing. Each chapter that touches code lands as its own commit at the bottom of that repo's log, with a small number of rehearsal-driven follow-ups on top.

What the tutorial covers

In order: scaffold via fedify init -w nuxt -p npm -k in-memory -m in-process; minimal Nuxt + UnoCSS shell; Drizzle ORM with better-sqlite3; single-user account creation enforced via CHECK (id = 1); HTML profile page; actor dispatcher under @fedify/nuxt's shared-route content negotiation; lazy RSA-PKCS1-v1_5 + Ed25519 key generation and WebFinger by side effect; first federation test through fedify tunnel against Mastodon and Pixelfed; Follow/Undo(Follow) and the followers OrderedCollection; image post schema and multipart upload to public/uploads/; setObjectDispatcher(Note, …) with attachments emitted as an array; profile feed and post-detail page with Open Graph metadata; outbound Create(Note) fan-out to followers; following remote accounts (lookup, Follow, and an Accept(Follow) matcher that survives Pixelfed's freshly-minted ids); following list and collection; home timeline cache fed by inbound Create(Note), with remote Note.content run through sanitize-html before storage; Like/Undo(Like) heart toggle that matches either by activity id or (actor, note); threaded comments through UUID-addressable Notes with inReplyTo, Mention tags, and parent-author cc so Pixelfed and Mastodon both thread them under the parent.

Each federation step is exercised in two directions (inbound + outbound) against both Mastodon (ActivityPub.Academy) and a local Pixelfed instance, with screenshots from both sides.

Testing

  • pnpm build inside docs/ succeeds cleanly.
  • An end-to-end rehearsal driven by Codex on a fresh checkout walked through every fedify init, dependency install, code paste, migration, and fedify lookup call in the tutorial. Three follow-up Codex review passes on main..HEAD exercised the prose for lockstep with the example repo, the security shape of the new sanitisation pipeline, and matcher correctness against Mastodon, GoToSocial, and Pixelfed; the final pass returned no findings.
  • Federation flows for follow, unfollow, image-bearing Create(Note), Like/Undo(Like), and Note-with-inReplyTo were verified live in both directions against ActivityPub.Academy and a local Pixelfed instance.

@dahlia dahlia added this to the Fedify 2.2 milestone Apr 27, 2026
@dahlia dahlia self-assigned this Apr 27, 2026
@dahlia dahlia added type/documentation Improvements or additions to documentation examples Example code related integration/nuxt Nuxt integration (@fedify/nuxt) labels Apr 27, 2026
@issues-auto-labeler issues-auto-labeler Bot added activitypub/pixelfed Pixelfed compatibility component/cli CLI tools related component/federation Federation object related component/integration Web framework integration labels Apr 27, 2026
dahlia added 20 commits April 27, 2026 12:52
Introduce docs/tutorial/content-sharing.md as a new end-to-end tutorial
that walks the reader through building a Pixelfed-style federated image
sharing service on top of Nuxt and the @fedify/nuxt integration shipping
with Fedify 2.2.0.  It parallels docs/tutorial/microblog.md so readers
who know that tutorial can quickly see what's different (Nuxt instead of
Hono + JSX, shared HTML/ActivityPub routes via @fedify/nuxt, image
attachments, likes, comments), and is meant to be released alongside
Fedify 2.2.0 when @fedify/nuxt ships.

This first slice covers chapters 1 through 3:

 -  An introduction, target audience, and feature goals for the final
    app.
 -  Setting up the development environment via

        fedify init -w nuxt -p npm -k in-memory -m in-process

    with a walkthrough of the scaffolded tree, dev-server verification,
    and a first `fedify lookup` against the default actor dispatcher.
 -  Prerequisites that cover just enough TypeScript, Vue + Nuxt, and
    ActivityPub vocabulary for the rest of the tutorial to land with
    readers who know only vanilla JavaScript.

It corresponds to the initial-scaffold commit in the companion example
repository (fedify-dev/content-sharing); each subsequent chapter will
map 1:1 to the next commit there, mirroring the
docs/tutorial/microblog.md and fedify-dev/microblog relationship.

A sidebar entry is added under the Tutorials menu, placed between the
microblog and astro-blog tutorials.

Assisted-by: Claude Code:claude-opus-4-7
Walk the reader through replacing Nuxt's placeholder welcome page with
a tiny two-row shell (brand bar, main area, footer) styled with UnoCSS.
The emphasis is on teaching just enough Vue and Nuxt (single-file
components, `<NuxtPage />`, `<NuxtLink />`, file-based routing) for
later federation chapters to paste markup without the reader being
surprised, and on keeping CSS out of the way for the rest of the
tutorial.

The chapter installs `@unocss/nuxt`, `@unocss/preset-wind3`, and
`@unocss/reset`; registers UnoCSS as a Nuxt module; adds a uno.config
with one custom brand colour; creates *app/assets/styles.css*, the new
*app/app.vue* root layout, and *app/pages/index.vue*; and verifies that
the same URL still serves both the HTML home page and the ActivityPub
actor JSON via `fedify lookup`, reinforcing the content-negotiation
trick that Chapter 6 will develop further.

Matches commit 2 of fedify-dev/content-sharing ("Add a minimal app
shell styled with UnoCSS").

Assisted-by: Claude Code:claude-opus-4-7
Walk the reader through adding [Drizzle ORM] and [better-sqlite3] to the
project so subsequent chapters can declare typed tables for the local
user, followers, posts, likes, and comments without stopping to explain
the persistence layer.

Matches commit 3 of fedify-dev/content-sharing ("Set up Drizzle ORM and
SQLite").  The chapter installs the packages, creates an empty
*server/db/schema.ts* (with an `export {}` marker so TypeScript treats
it as a module), a *server/db/client.ts* that opens
*content-sharing.sqlite3* and exports a Drizzle `db` with WAL mode and
foreign-key enforcement turned on, a *drizzle.config.ts* for drizzle-kit,
and adds `db:push` / `db:studio` npm scripts.  It also git-ignores the
SQLite file and its WAL sidecars, and explains the two SQLite pragmas
in a definition list with reference links to the relevant upstream
documentation.

SQL-averse readers are reassured that they never have to write raw SQL
to finish the tutorial; everything lands through Drizzle's typed query
builder.

[Drizzle ORM]: https://orm.drizzle.team/
[better-sqlite3]: https://www.npmjs.com/package/better-sqlite3

Assisted-by: Claude Code:claude-opus-4-7
Walk the reader through building a single-user signup flow: the
`users` table with a `CHECK (id = 1)` constraint, helper functions
shared between server routes, the signup and "who is the local user"
endpoints, the setup page, and a global route middleware that
redirects un-set-up instances to */setup*.

Matches commit 5 of fedify-dev/content-sharing ("Add account
creation").

Notable teaching moments:

 -  How to express a `CHECK` constraint with Drizzle's `check()`
    helper and why single-user enforcement lives in both the database
    schema and the API handler.
 -  The `.post.ts` / `.get.ts` Nuxt naming convention for
    method-specific server routes.
 -  Route middleware with the `.global.ts` suffix running before every
    navigation, including the "skip ourselves or loop forever" guard
    for the target route.
 -  Verifying state both visually (screenshots of the form before and
    after submission) and at the database layer via a `sqlite3 ...`
    one-liner, matching the pattern the microblog tutorial uses.

Assisted-by: Claude Code:claude-opus-4-7
Walk the reader through serving an HTML profile at */users/:username*,
backed by a small JSON endpoint and Nuxt's `useFetch`.  The home page
also gets a redirect to the local user's profile, so single-user
instances do not strand visitors on a generic landing page.

The profile URL deliberately matches the path the scaffolded actor
dispatcher already uses; later chapters will lean on @fedify/nuxt's
shared-route content negotiation so the same URL serves both the HTML
profile and the ActivityPub `Person` JSON.  A `curl` aside at the end
of the chapter demonstrates the two responses the same URL can already
return.

Matches commit 6 of fedify-dev/content-sharing ("Add the HTML profile
page").

Assisted-by: Claude Code:claude-opus-4-7
Walk through the scaffolded `setActorDispatcher`, then rewrite it to
read from the `users` table and return `null` for unknown identifiers.
The new `Person` exposes `inbox` and a shared inbox via `Endpoints`,
plus the trio Pixelfed needs to surface a remote profile cleanly:
`manuallyApprovesFollowers: false`, `discoverable: true`, and
`indexable: true`.

The chapter also explains why a stub `setInboxListeners()` call has to
appear in this chapter even though we have not handled any activity
yet.  Without it `ctx.getInboxUri()` throws.

Verification: `fedify lookup` returns the expected Person for alice
and a 404 hint for an unknown identifier; the HTML profile page from
chapter 6 still renders for `Accept: text/html`, demonstrating the
content-negotiation half of the shared-route pattern.

Assisted-by: Claude Code:claude-opus-4-7
Rewrite four sentences to use commas, colons, parentheses, or simple
sentence breaks instead of spaced em dashes.  This brings the chapter
in line with the project house style of avoiding em dashes (and
keeping any unavoidable ones unspaced).

Assisted-by: Claude Code:claude-opus-4-7
Walk through why the actor needs key pairs, why two algorithms are
worth the trouble, and how to add the `actor_keys` table and a
`setKeyPairsDispatcher` that lazily generates and caches pairs in
JWK form.  The actor dispatcher gains `publicKey` and
`assertionMethods`, so RSA-only and Ed25519-aware peers each find a
key they can verify.

A short coda points at the WebFinger response Fedify wires up
automatically off the back of the actor dispatcher; we did not
write a WebFinger handler, but `curl` shows one at
*/.well-known/webfinger*.

Verification: `fedify lookup` shows the new `publicKey` and
`assertionMethods`; the example repo's `actor_keys` table holds
exactly two rows after the first lookup, and re-lookups are
idempotent.

Assisted-by: Claude Code:claude-opus-4-7
Walk readers through running `fedify tunnel`, allowing the rotating
subdomain in `vite.server.allowedHosts`, and pointing two real
fediverse servers at the result: ActivityPub.Academy as the
Mastodon proof point, and pxlmo.com as the Pixelfed proof point.
Both render alice's actor with her display name, federated handle,
and zero-state counters, which closes the loop opened by the actor
dispatcher and key-pair chapters.

The chapter also documents the visible quirks:

 -  Vite's blocked-host page when `allowedHosts` is missing.
 -  Pixelfed's quick-search dropdown filling the input with
    `[object Object]` after Enter; URL navigation works around it.
 -  The Follow buttons that appear on both servers but stay inert
    until chapter 10 wires up the inbox handler.

Three new screenshots are checked in:

 -  *academy-search-alice.png*
 -  *academy-alice-profile.png*
 -  *pxlmo-alice-profile.png*

Assisted-by: Claude Code:claude-opus-4-7
Three small follow-ups to chapter 9:

 -  Use `allowedHosts: true` in *nuxt.config.ts* and the matching
    code block in the tutorial.  We do not know which tunneling
    service the reader will use, so listing specific subdomains is
    too narrow.
 -  Stop naming a specific Pixelfed instance.  Send the reader to
    <https://pixelfed.org/servers> to pick an instance, and refer to
    it generically (`<your-instance>`) for the rest of the chapter.
    The Pixelfed screenshot is renamed to *pixelfed-alice-profile*
    to match.
 -  Mark keyboard input with `<kbd>` (`<kbd>Ctrl</kbd>+<kbd>C</kbd>`,
    `<kbd>Enter</kbd>`) instead of inline backticks.

Assisted-by: Claude Code:claude-opus-4-7
Walk readers through receiving a remote `Follow` activity and
sending the matching `Accept` back so the remote button flips from
*Follow* to *Following* (or, on Pixelfed, the follower count
ticks up).

Schema additions:

 -  `followers` table on `(following_id, actor_uri)` storing the
    handle, name, inbox URL, optional shared inbox URL, and
    profile URL of every remote actor that follows the local user.

Listener additions:

 -  `setInboxListeners().on(Follow, ...)` that validates the
    target via `ctx.parseUri()`, fetches the follower with
    `follow.getActor()`, upserts the cached profile fields, and
    sends `Accept(Follow)` back via `ctx.sendActivity()`.

The chapter is verified twice over: once on Mastodon
(ActivityPub.Academy) and once on Pixelfed.  Pixelfed coverage is
the more important of the two for this tutorial because the whole
point is to build a Pixelfed-style image-sharing service that
federates with Pixelfed instances correctly.

Two new screenshots:

 -  *academy-after-follow.png*
 -  *pixelfed-after-follow.png*

Assisted-by: Claude Code:claude-opus-4-7
Two follow-ups for the upstream init template change in
fedify-dev#726:

 -  Chapter 2 now lists *server/plugins/logging.ts* alongside
    *server/logging.ts*, with a one-line explanation of how the
    Nitro plugin awaits the configuration promise.
 -  Chapter 10's *Follow* listener now sets an explicit `id` on
    the outbound `Accept`, matching the example repository.  A new
    bullet in the walkthrough calls out why the explicit id matters
    for Pixelfed compatibility, even though Fedify can auto-generate
    one.

Assisted-by: Claude Code:claude-opus-4-7
End-to-end testing showed that even with the explicit Accept(Follow)
id, Pixelfed's public profile view of a remote actor that has no
`followers` collection does not visibly flip the *Follow* button or
update the followers counter.  The Accept does reach Pixelfed (the
local user's *Following* counter ticks up), so the relationship is
recorded; the visible UI lag is on Pixelfed's side and resolves once
the remote actor exposes a followers collection.

Replace the misleading "Followers counter ticks up" prose and its
screenshot with:

 -  A dev-log excerpt that shows the inbound Follow plus the
    outbound Accept, which is the wire-level proof.
 -  A NOTE that the public Pixelfed profile of alice will only
    fully reflect the relationship after chapter 12 wires up the
    `followers` collection, with a forward link to that chapter.

Drop the *pixelfed-after-follow.png* screenshot since the tutorial
no longer references it; the chapter-9 *pixelfed-alice-profile.png*
remains as the introduction to Pixelfed.

Assisted-by: Claude Code:claude-opus-4-7
Add the `firstKnock: "draft-cavage-http-signatures-12"` option to
the *server/federation.ts* code block in chapter 10, matching the
example repository.  A short explainer bridges the two HTTP
Signatures specs and explains why we override the first knock
specifically for Pixelfed.

Replace the wishful "the relationship is recorded inside Pixelfed"
note with an honest one: Pixelfed's queue worker has multiple
silent early-return points in `handleAcceptActivity`, so even when
our `Accept` lands on the wire the relationship sometimes gets
stuck at `requested: true`.  The dev-log "Successfully sent
activity" line is the wire-level proof of success; UI propagation
on the Pixelfed side is treated as out of scope and tracked as a
draft Pixelfed bug report.

Assisted-by: Claude Code:claude-opus-4-7
After standing up a local Pixelfed and instrumenting its inbox
pipeline, we found that Pixelfed's `AcceptValidator` rejects
Accept payloads whose `object.actor` is a nested Person object.
The validator (*app/Util/ActivityPub/Validator/Accept.php*)
requires `object.actor` and `object.object` to be URL strings.

Fedify's `Follow.getActor()` hydrates `actor` into a full Person,
and re-serializing the same `Follow` instance inline as the
Accept's `object` carries that Person along, which trips the
validator.  Reconstructing a minimal `Follow({ id, actor, object })`
from the original URL fields keeps every other peer happy and
unblocks Pixelfed.

Update the chapter accordingly:

 -  The code block now builds the minimal Follow.
 -  A new walkthrough bullet explains why we don't reuse the
    inbound `follow` directly.
 -  Replace the old "Pixelfed records the follow internally but
    UI may lag" note with a positive proof-it-works image
    (*pixelfed-after-follow.png* now shows the *Unfollow* button).
 -  The redirect to chapter 12 for "0 Followers on alice" still
    applies and is preserved in the new caption.

Verified end-to-end against a local Dockerised Pixelfed and
against the public test instance.

Assisted-by: Claude Code:claude-opus-4-7
Two small fixes flagged on review:

 -  The troubleshooting tip suggested `LOG_LEVEL=debug npm run dev`,
    but our example app does not actually consume a `LOG_LEVEL`
    env var — *server/logging.ts* hardcodes the per-category levels
    via LogTape's `lowestLevel`.  Replace the bogus instruction
    with the real procedure: edit *server/logging.ts* and lower
    the `fedify` logger from `"info"` to `"debug"`, then restart
    the dev server.
 -  The text linked to GitHub issue fedify-dev#693 alongside the RFC 9421
    explainer.  The issue is the umbrella tracking ticket for the
    whole tutorial, unrelated to Pixelfed's signature spec gap, so
    the link was misleading.  Drop both the inline reference and
    the dangling link definition.

Assisted-by: Claude Code:claude-opus-4-7
Walk readers through the inbound `Undo(Follow)` activity and the
`on(Undo, ...)` listener that deletes the matching `followers` row.

Coverage:

 -  A short narrative on the JSON shape of `Undo(Follow)` activities
    and why our chapter-10 inbox quietly accepts them today.
 -  The full listener with checks for the inner activity type, the
    target identifier, and the deletion key on `(following_id,
    actor_uri)`.
 -  A walkthrough that explains why we delete on `undo.actorId`
    (HTTP signature verification has already authenticated the
    sender) and why the composite predicate matters for chapters
    that add multi-account support later.
 -  A NOTE about idempotence — `Undo(Follow)` for an already-deleted
    row is a no-op, no error.

Verified end-to-end:

 -  ActivityPub.Academy: re-follow alice, click *Unfollow*; the
    Mastodon UI flips back to *Follow* and the `followers` row
    disappears.
 -  Local Pixelfed: same; new screenshot
    *pixelfed-after-unfollow.png* shows the *Follow* button restored.

Assisted-by: Claude Code:claude-opus-4-7
Walk readers through wiring up alice's followers in two parallel
shapes:

 -  ActivityPub: `setFollowersDispatcher(...)` plus `setCounter`
    serve `OrderedCollection` and total count to remote servers.
    The actor dispatcher gains `followers: ctx.getFollowersUri(...)`
    so peers discover the collection.
 -  HTML: */users/&fedify-dev#91;username&fedify-dev#93;/followers* renders the same
    rows for the local user, with each entry linking out to the
    follower's remote profile.  The profile API now also returns
    `followerCount`, displayed as a link to the new page.

Two new screenshots checked in:

 -  *profile-with-follower-count.png*
 -  *followers-list.png*

A short note at the end explains why Mastodon and Pixelfed cached
counts can lag for a few minutes after the dispatcher goes live,
and why re-following from the remote side forces a refresh.

Assisted-by: Claude Code:claude-opus-4-7
VitePress' code-fence file-path attribute does not handle nested
square brackets: `~~~~ typescript [path/to/[username].ts]` gets
parsed as two separate fields and the displayed path is truncated.
Replace the inner `[…]` with the equivalent HTML entities
(`&fedify-dev#91;…&fedify-dev#93;`) on every affected fence so the rendered file
label reads correctly.

Two tutorials are touched:

- docs/tutorial/content-sharing.md: chapter 6 (profile API and
  Vue page) and chapter 12 (followers API/page plus the
  rewired profile API and index page).
- docs/tutorial/threadiverse.md: profile, new-thread form and
  its server action, and the thread-detail page.

Assisted-by: Claude Code:claude-opus-4-7
Define the `posts` table that the next several chapters all build
on (compose, Note dispatcher, profile feed, post detail,
distribution, home timeline, likes, comments).  Walk through each
column and why it lives where it does:

 -  `id` (autoincrement) is the public post identifier woven into
    ActivityPub IRIs in chapter 15.
 -  `userId` is forward-compatible with chapter 19's remote-actor
    cache.
 -  `caption` is nullable to mirror Pixelfed's captionless posts.
 -  `mediaPath` is a relative path under *public/uploads/* so
    Nuxt's static-asset pipeline serves images for free.
 -  `mediaType` rides directly into `Document.mediaType` later.
 -  `createdAt` defaults to SQL `CURRENT_TIMESTAMP`.

Cover the matching uploads directory: `mkdir public/uploads`, a
*.gitkeep* placeholder, and a *.gitignore* rule that lets only the
*.gitkeep* through so uploaded images do not leak into commits.

A short note at the end flags object storage (S3, R2, MinIO) as
the production-grade alternative without making the tutorial
swap the implementation just yet.

No federation or HTTP code runs in this chapter; the next chapter
will add the compose form and upload endpoint that finally insert
rows.

Assisted-by: Claude Code:claude-opus-4-7
@dahlia
Copy link
Copy Markdown
Member Author

dahlia commented Apr 27, 2026

@codex review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new tutorial titled "Creating an image sharing service" and updates the documentation sidebar and changelog accordingly. It also adds better-sqlite3 and drizzle-orm dependencies to the documentation package and upgrades the hongdown tool to version 0.3.10. The remaining changes consist of minor formatting adjustments and indentation fixes across several Markdown files. I have no feedback to provide as no review comments were submitted.

@dahlia dahlia removed component/federation Federation object related component/integration Web framework integration labels Apr 27, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/package.json`:
- Around line 48-51: The docs/package.json currently pins third-party deps
"better-sqlite3" and "drizzle-orm" with version ranges instead of routing them
through the workspace pnpm catalog like other deps (e.g. "h3", "express");
update the project to add catalog entries for these packages in
pnpm-workspace.yaml and replace the explicit ranges in docs/package.json with
the corresponding "catalog:" references so examples and docs share the same
controlled versions (modify the entries for better-sqlite3 and drizzle-orm in
docs/package.json and add matching keys/versions in pnpm-workspace.yaml).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75cf0442-0ac8-40f0-aac3-3dd9813533fd

📥 Commits

Reviewing files that changed from the base of the PR and between cb3394b and ab76882.

⛔ Files ignored due to path filters (30)
  • docs/tutorial/content-sharing/academy-after-follow.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/academy-alice-profile.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/academy-search-alice.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/app-shell-home.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/compose-form.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/follow-page-empty.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/follow-success.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/followers-list.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/following-list.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/home-after-signup.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/home-with-pixelfed-post.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/nuxt-welcome-page.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/pixelfed-after-follow.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/pixelfed-after-unfollow.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/pixelfed-alice-profile.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/pixelfed-follow-notification.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/pixelfed-home-with-alice-post.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/pixelfed-post-with-alice-comment.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/pixelfed-post-with-like.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/post-detail-with-comment.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/post-detail-with-like.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/post-detail-with-pixelfed-comment.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/post-detail.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/profile-feed.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/profile-page-empty.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/profile-with-follower-count.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/profile-with-following-counter.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/signup-form-empty.png is excluded by !**/*.png
  • docs/tutorial/content-sharing/signup-form-filled.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • .agents/skills/add-vocab/SKILL.md
  • .agents/skills/create-example-app-with-integration/SKILL.md
  • AGENTS.md
  • CHANGES.md
  • CONTRIBUTING.md
  • docs/.vitepress/config.mts
  • docs/manual/collections.md
  • docs/manual/deploy.md
  • docs/manual/mq.md
  • docs/manual/relay.md
  • docs/package.json
  • docs/tutorial/astro-blog.md
  • docs/tutorial/content-sharing.md
  • docs/tutorial/threadiverse.md
  • docs/why.md
  • mise.toml
  • packages/init/README.md

Comment thread docs/package.json Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab768823a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/tutorial/content-sharing.md Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.
see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

dahlia added 2 commits April 27, 2026 13:07
Mirror the example repo's returning()-and-short-circuit pattern in
the chapter 22 likes.post.ts twoslash block, and document why with
a new design note above the deflist for the file.

Addresses fedify-dev#731 (comment)

Assisted-by: Codex:gpt-5.5
Assisted-by: Claude Code:claude-opus-4-7
The microblog, threadiverse, and content-sharing tutorials all
paste the same better-sqlite3 + drizzle-orm versions into their
twoslash blocks. Promote those packages (plus
@types/better-sqlite3) into pnpm-workspace.yaml's catalog and
swap the explicit ranges in docs/package.json for catalog
references so the three tutorials stay in lockstep.

Addresses fedify-dev#731 (comment)

Assisted-by: Claude Code:claude-opus-4-7
dahlia added a commit to fedify-dev/content-sharing that referenced this pull request Apr 27, 2026
A duplicate Like POST (a second click, or a retry after a network
blip) used to fall through to ctx.sendActivity even though
onConflictDoNothing kept the existing row's likeActivityId. The
recipient saw two Likes with different ids; later when alice
unliked, the Undo only retracted the first delivery and the
duplicate was orphaned.

Use returning() after the upsert: an empty result means the row
was already present, so we short-circuit the request instead of
emitting another Like.

Addresses fedify-dev/fedify#731 (comment)

Assisted-by: Codex:gpt-5.5
Assisted-by: Claude Code:claude-opus-4-7
@dahlia
Copy link
Copy Markdown
Member Author

dahlia commented Apr 27, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31c0fc6d47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/tutorial/content-sharing.md
Comment thread docs/tutorial/content-sharing.md Outdated
VitePress strips the backticks from the heading text and emits the
slug #likes-and-undo-like. The hand-written link used
#like-s-and-undo-like- instead, which does not match anything on
the page, so clicking it scrolled to the top of the chapter rather
than the target.

Addresses fedify-dev#731 (comment)

Assisted-by: Codex:gpt-5.5
Assisted-by: Claude Code:claude-opus-4-7
@dahlia
Copy link
Copy Markdown
Member Author

dahlia commented Apr 27, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dahlia dahlia requested review from 2chanhaeng and sij411 April 27, 2026 05:04
@github-actions
Copy link
Copy Markdown
Contributor

Pre-release has been published for this pull request:

Packages

Package Version JSR npm
@fedify/fedify 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/cli 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/amqp 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/astro 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/cfworkers 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/create 2.2.0-pr.731.0.33+a54d18e7 npm
@fedify/debugger 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/denokv 2.2.0-pr.731.0.33+a54d18e7 JSR
@fedify/elysia 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/express 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/fastify 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/fixture 2.2.0-pr.731.0.33+a54d18e7 JSR
@fedify/fresh 2.2.0-pr.731.0.33+a54d18e7 JSR
@fedify/h3 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/hono 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/init 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/koa 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/lint 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/mysql 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/nestjs 2.2.0-pr.731.0.33+a54d18e7 npm
@fedify/next 2.2.0-pr.731.0.33+a54d18e7 npm
@fedify/nuxt 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/postgres 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/redis 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/relay 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/solidstart 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/sqlite 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/sveltekit 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/testing 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/vocab 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/vocab-runtime 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/vocab-tools 2.2.0-pr.731.0.33+a54d18e7 JSR npm
@fedify/webfinger 2.2.0-pr.731.0.33+a54d18e7 JSR npm

Documentation

The docs for this pull request have been published:

https://3d012cd4.fedify.pages.dev

@issues-auto-labeler issues-auto-labeler Bot added component/federation Federation object related component/integration Web framework integration labels Apr 27, 2026
@dahlia dahlia removed component/federation Federation object related component/cli CLI tools related component/integration Web framework integration labels Apr 27, 2026
@issues-auto-labeler issues-auto-labeler Bot added component/cli CLI tools related component/federation Federation object related component/integration Web framework integration labels Apr 27, 2026
@dahlia dahlia removed component/federation Federation object related component/cli CLI tools related component/integration Web framework integration labels Apr 27, 2026
Copy link
Copy Markdown
Contributor

@2chanhaeng 2chanhaeng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some modifications are needed.

[installing Visual Studio Code]: https://code.visualstudio.com/docs/setup/setup-overview


Prerequisites
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other tutorials also have similar content. Is there any way to separate it into a different file and then import it here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vitepress has the feature! This can help us.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's much benefit in extracting just this part into a shared fragment since it's not very long. However, it might be a good idea to cover things like setup instructions for major IDEs like Visual Studio Code and Zed, along with how to install and use the Agentic Skill coming in Fedify 2.2.0, in the docs/install.md file.

CSS reset:

~~~~ sh
npm install -D @unocss/nuxt @unocss/preset-wind3 @unocss/reset
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add npm install unocss.

`followers` table, and chain a listener onto
`~Federatable.setInboxListeners()`:

~~~~ typescript twoslash [server/federation.ts]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure exactly what the problem is, but whenever I try to drag just a part of this code block, the entire text of the page gets dragged.

Comment on lines +5858 to +5875
if (object instanceof Follow) {
if (undo.actorId == null || object.objectId == null) return;
const target = ctx.parseUri(object.objectId);
if (target?.type !== "actor") return;
const localUser = (
await db.select().from(users).where(eq(users.username, target.identifier)).limit(1)
)[0];
if (localUser === undefined) return;
await db
.delete(followers)
.where(
and(
eq(followers.followingId, localUser.id),
eq(followers.actorUri, undo.actorId.href),
),
);
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is a bit different from lines 2557-2576.

schema imports, then add the lookup just before the response:

~~~~ typescript [server/api/users/&#91;username&#93;/posts/&#91;id&#93;.get.ts]
import { count, eq } from "drizzle-orm";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and is missed.

import { Create, Document, Note } from "@fedify/vocab";
import { and, eq } from "drizzle-orm";
import { db } from "./db/client";
import { following, timelinePosts } from "./db/schema";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show timelinePosts import to viewers.

InProcessMessageQueue,
MemoryKvStore,
} from "@fedify/fedify";
import { Mention, Note, PUBLIC_COLLECTION } from "@fedify/vocab";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show Mention import to viewers.

[FEP] index is where the fediverse codifies how things *should*
work. Reading the FEPs that cover the activities you are
sending is the fastest way to figure out which side has the
bug.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show comments import to viewers.

method,
body: { noteUri: post.noteUri },
});
await refresh();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refresh is never defined. I think const { data, error, refresh } = await useFetch( is missed?

const commentDraft = ref("");
const submitting = ref(false);

async function submitComment() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

submitComment is only defined, and no used. I think the usage is missed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

activitypub/pixelfed Pixelfed compatibility examples Example code related integration/nuxt Nuxt integration (@fedify/nuxt) type/documentation Improvements or additions to documentation

Development

Successfully merging this pull request may close these issues.

Build federated content sharing example and tutorial (Nuxt + Node.js)

2 participants