“Creating an image sharing service” tutorial#731
“Creating an image sharing service” tutorial#731dahlia wants to merge 67 commits intofedify-dev:mainfrom
Conversation
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
|
@codex review |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (30)
docs/tutorial/content-sharing/academy-after-follow.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/academy-alice-profile.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/academy-search-alice.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/app-shell-home.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/compose-form.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/follow-page-empty.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/follow-success.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/followers-list.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/following-list.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/home-after-signup.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/home-with-pixelfed-post.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/nuxt-welcome-page.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/pixelfed-after-follow.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/pixelfed-after-unfollow.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/pixelfed-alice-profile.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/pixelfed-follow-notification.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/pixelfed-home-with-alice-post.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/pixelfed-post-with-alice-comment.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/pixelfed-post-with-like.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/post-detail-with-comment.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/post-detail-with-like.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/post-detail-with-pixelfed-comment.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/post-detail.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/profile-feed.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/profile-page-empty.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/profile-with-follower-count.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/profile-with-following-counter.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/signup-form-empty.pngis excluded by!**/*.pngdocs/tutorial/content-sharing/signup-form-filled.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
.agents/skills/add-vocab/SKILL.md.agents/skills/create-example-app-with-integration/SKILL.mdAGENTS.mdCHANGES.mdCONTRIBUTING.mddocs/.vitepress/config.mtsdocs/manual/collections.mddocs/manual/deploy.mddocs/manual/mq.mddocs/manual/relay.mddocs/package.jsondocs/tutorial/astro-blog.mddocs/tutorial/content-sharing.mddocs/tutorial/threadiverse.mddocs/why.mdmise.tomlpackages/init/README.md
There was a problem hiding this comment.
💡 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".
Codecov Report✅ All modified and coverable lines are covered by tests. 🚀 New features to boost your workflow:
|
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
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
|
@codex review |
There was a problem hiding this comment.
💡 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".
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
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
Pre-release has been published for this pull request: Packages
DocumentationThe docs for this pull request have been published: |
2chanhaeng
left a comment
There was a problem hiding this comment.
Some modifications are needed.
| [installing Visual Studio Code]: https://code.visualstudio.com/docs/setup/setup-overview | ||
|
|
||
|
|
||
| Prerequisites |
There was a problem hiding this comment.
Other tutorials also have similar content. Is there any way to separate it into a different file and then import it here?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Add npm install unocss.
| `followers` table, and chain a listener onto | ||
| `~Federatable.setInboxListeners()`: | ||
|
|
||
| ~~~~ typescript twoslash [server/federation.ts] |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
The code is a bit different from lines 2557-2576.
| schema imports, then add the lookup just before the response: | ||
|
|
||
| ~~~~ typescript [server/api/users/[username]/posts/[id].get.ts] | ||
| import { count, eq } from "drizzle-orm"; |
| import { Create, Document, Note } from "@fedify/vocab"; | ||
| import { and, eq } from "drizzle-orm"; | ||
| import { db } from "./db/client"; | ||
| import { following, timelinePosts } from "./db/schema"; |
There was a problem hiding this comment.
Show timelinePosts import to viewers.
| InProcessMessageQueue, | ||
| MemoryKvStore, | ||
| } from "@fedify/fedify"; | ||
| import { Mention, Note, PUBLIC_COLLECTION } from "@fedify/vocab"; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Show comments import to viewers.
| method, | ||
| body: { noteUri: post.noteUri }, | ||
| }); | ||
| await refresh(); |
There was a problem hiding this comment.
refresh is never defined. I think const { data, error, refresh } = await useFetch( is missed?
| const commentDraft = ref(""); | ||
| const submitting = ref(false); | ||
|
|
||
| async function submitComment() { |
There was a problem hiding this comment.
submitComment is only defined, and no used. I think the usage is missed?
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/nuxtintegration 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:
npm install -g @fedify/cli@2.2.0-dev.978. The released@fedify/clidoes not yet ship the-w nuxttemplatefedify initneeds, andfedify tunnelonly works correctly with the fix from Fix CLI config fallbacks for Optique 1.0 #728 in the prerelease line.cloudflaredorfedify tunnel). If you do not want to set those up, the Trying it out sections still work asfedify lookupsmoke tests for outbound shape—the inbound steps are the parts that need a real partner.pnpm buildinside 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:
Documentattachments demanded as an array,Accept(Follow)carrying a freshly-mintedFollowid, fragment-URLNotes silently dropped, threaded replies that only attach whencclists the parent author and theNotecarries a matchingMentiontag, thelikes_countcache 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
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 withbetter-sqlite3; single-user account creation enforced viaCHECK (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 throughfedify tunnelagainst Mastodon and Pixelfed;Follow/Undo(Follow)and the followersOrderedCollection; 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; outboundCreate(Note)fan-out to followers; following remote accounts (lookup,Follow, and anAccept(Follow)matcher that survives Pixelfed's freshly-minted ids); following list and collection; home timeline cache fed by inboundCreate(Note), with remoteNote.contentrun through sanitize-html before storage;Like/Undo(Like)heart toggle that matches either by activity id or(actor, note); threaded comments through UUID-addressableNotes withinReplyTo,Mentiontags, and parent-authorccso 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 buildinside docs/ succeeds cleanly.fedify init, dependency install, code paste, migration, andfedify lookupcall in the tutorial. Three follow-up Codex review passes onmain..HEADexercised 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.Create(Note),Like/Undo(Like), andNote-with-inReplyTowere verified live in both directions against ActivityPub.Academy and a local Pixelfed instance.