Conversation
📝 WalkthroughWalkthroughIntroduces a complete Nuxt example application demonstrating ActivityPub federation capabilities using Fedify. Includes project configuration, Vue page components for the frontend UI, server-side API endpoints for federation actions, federation module setup, in-memory data stores, SSE implementation for real-time updates, and styling. Changes
Sequence DiagramsequenceDiagram
actor User
participant Client as Browser/Client
participant API as API Handler
participant Federation as Federation Context
participant Store as Data Store
participant EventBroadcast as SSE Broadcast
User->>Client: Search for user
Client->>API: GET /api/search?q=...
API->>Federation: ctx.lookupObject(q)
Federation->>API: Return Person object
API->>Store: Check followingStore.has(personId)
API->>Client: Return search result with isFollowing
Client->>User: Display user card
User->>Client: Click follow
Client->>API: POST /api/follow (uri)
API->>Federation: ctx.lookupObject(targetUri)
API->>Federation: Send Follow activity
API->>Store: Add to followingStore
API->>EventBroadcast: broadcastEvent()
EventBroadcast->>Client: SSE message (update event)
Client->>User: Refresh home feed
User->>Client: Compose post
Client->>API: POST /api/post (content)
API->>Store: postStore.append(Note)
API->>Federation: Fetch persisted Note
API->>Federation: Send Create activity to followers
API->>EventBroadcast: broadcastEvent()
EventBroadcast->>Client: SSE message (update event)
Client->>User: Show new post in feed
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces the @fedify/nuxt package for Nuxt integration, including an example application and support in the fedify init command. Feedback points out a bug in manual request construction within the templates and suggests using h3's toWebRequest utility for better reliability. Additionally, the reviewer recommends correcting a version typo in Node.js types, enabling SSR by default to ensure ActivityPub compatibility, and refactoring the fedify init scaffolding to use the Nuxt module instead of manual middleware.
Codecov Report✅ All modified and coverable lines are covered by tests. 🚀 New features to boost your workflow:
|
7673033 to
4cc14ed
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 20
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CONTRIBUTING.md`:
- Line 395: The bullet "*packages/nuxt/*: Nuxt integration (`@fedify/nuxt`) for
Fedify." repeats wording used nearby; reword it to avoid repetition by
shortening or changing phrasing—e.g., replace with "packages/nuxt/*: Nuxt
integration (`@fedify/nuxt`)" or "packages/nuxt/*: Nuxt adapter (`@fedify/nuxt`) for
Fedify" so the list reads more concise and varied while preserving the package
path and npm scope mention.
In `@examples/nuxt/app.vue`:
- Line 11: The injected script object in the script array (script: [{ src:
"/theme.js" }]) runs too early and can access document.body before it exists;
modify that script entry to include the defer attribute so the browser defers
execution until after parsing (e.g., add a defer:true property on the object or
otherwise render the tag with defer) so /theme.js runs only after the body is
available.
In `@examples/nuxt/pages/index.vue`:
- Around line 183-194: The current onSearchInput handler can apply stale results
because out-of-order fetches overwrite searchResult; modify onSearchInput to
track and ignore stale responses by incrementing a request counter (e.g.,
localRequestId / lastHandledRequestId) or by using an AbortController to cancel
the previous $fetch before starting a new one; ensure you reference and update
the shared identifier (searchTimeout, searchQuery, searchResult) and only assign
searchResult.value when the response's request id matches the latest id (or when
the fetch wasn't aborted) so older responses are ignored.
In `@examples/nuxt/pages/users/`[identifier]/index.vue:
- Line 64: The useFetch call only destructures data (const { data } = await
useFetch(`/api/profile/${identifier}`)) so API/network failures aren't handled;
update the call to also extract error (and optionally pending) and then check
error before treating null data as "User not found": e.g., capture const { data,
error } = await useFetch(...), log or display error.message when error is
truthy, and only show the "not found" UI when error is null but data is empty;
ensure checks reference useFetch, data, error and the identifier route/component
so behavior changes apply to this page.
In `@examples/nuxt/pages/users/`[identifier]/posts/[id].vue:
- Line 3: The back navigation anchors use plain <a href="/"> which causes full
page reloads; replace those anchors (the occurrences with class "back-link" in
the users/[identifier]/posts/[id].vue page) with <NuxtLink to="/"> preserving
existing attributes/classes and inner text to enable client-side SPA navigation;
ensure both instances (the one near the top and the one near the bottom) are
updated to NuxtLink so navigation is smooth and consistent.
In `@examples/nuxt/public/theme.js`:
- Around line 3-6: theme.js toggles document.body.classList.add and
mq.addEventListener to set "dark"/"light" classes but
examples/nuxt/public/style.css only uses media-query variables, so the classes
are unused; fix by updating the CSS to consume those classes (e.g., add
body.dark and body.light selectors or [data-theme="dark"/"light"] equivalents
that override the same CSS variables or color rules) or alternatively change
theme.js to set the same mechanism used in style.css (e.g., set a matching
media-query-based state); locate the toggling code in theme.js
(document.body.classList.add/remove and mq.addEventListener) and the root
variable definitions in style.css and make them consistent so the JS-driven
classes actually affect styling.
In `@examples/nuxt/README.md`:
- Line 10: Fix the awkward intro sentence that currently reads "using the Fedify
and [Nuxt]" in the README by removing the stray "the" and markdown brackets so
it reads naturally (for example: "implementations using Fedify and Nuxt" or
"implementations using the Fedify and Nuxt frameworks"); update the line in the
README where that phrase appears to one of these clearer variants.
In `@examples/nuxt/server/api/events.get.ts`:
- Around line 5-7: The three setResponseHeader calls (setResponseHeader(event,
"Content-Type", "text/event-stream"); setResponseHeader(event, "Cache-Control",
"no-cache"); setResponseHeader(event, "Connection", "keep-alive");) are
redundant because you later return a raw Response with its own headers; remove
those setResponseHeader calls and rely on the headers supplied to the Response
constructor (or, if you prefer h3 header handling, remove the Response headers
and write to event.node.res instead) so only one header-setting approach
(Response constructor or h3 setResponseHeader) remains; update any related
comments to avoid confusion.
- Around line 23-25: The close handler currently only calls removeClient(client)
but must also close the client's stream to avoid races; update the
event.node.req.on("close", ...) callback to call client.close() and ensure the
client's underlying controller is closed (e.g., controller.close() from wherever
the client/stream is created) so any pending readers/writers are cleaned up and
subsequent broadcastEvent writes won't throw. Locate the close listener and the
client creation (where a controller is stored for each client) and add
client.close() (and controller.close() if applicable) before or after
removeClient(client) to guarantee cleanup.
- Around line 13-14: The send method in the event stream (function send) can
throw if controller.enqueue is called after the stream is closed; wrap the
controller.enqueue(encoder.encode(`data: ${data}\n\n`)) call in a try-catch
inside send (the method used by broadcastEvent when calling client.send) and
either ignore the error or log it (avoid rethrowing) so a race on disconnect
doesn't cause an unhandled exception; keep the encoder.encode call as-is and
only guard the controller.enqueue invocation.
In `@examples/nuxt/server/api/follow.post.ts`:
- Around line 36-39: Replace the dynamic import of Person with a static
top-level import alongside Follow: remove the runtime await
import("@fedify/vocab") inside follow.post.ts and add a static import for Person
at the top of the file, then use the existing instanceof check (target
instanceof Person) and followingStore.set(target.id.href, target) as-is; this
eliminates the per-request async import and keeps the same behavior for
followingStore and the Follow handling.
In `@examples/nuxt/server/api/home.get.ts`:
- Around line 12-32: Extract the duplicated person-mapping logic used to build
followers and following into a reusable async helper (e.g., mapPersonEntries)
that accepts entries (Iterable<[string, Person]>) and ctx, and returns Promise
of mapped objects; replace the two inline Promise.all/Array.from blocks that
reference relationStore.entries() and followingStore.entries() with calls to
this helper (use the same field names: uri, name, handle, icon and the same
person.getIcon(ctx) call) to remove duplication while preserving behavior.
In `@examples/nuxt/server/api/post.post.ts`:
- Around line 31-33: The Create activity currently sets id to new
URL("#activity", attribution) which produces a static, non-unique activity ID;
change the construction of the Create id to include a unique per-post component
(for example incorporate the post's unique identifier like note.id or a
generated UUID/timestamp) so the Create activity id becomes something like new
URL(`#activity-${note.id}` or `#activity-${uuid}`, attribution) ensuring each
Create activity has a distinct ID.
- Line 27: ctx.getObject(Note, { identifier, id }) can return null which makes
downstream activity construction ambiguous; add an explicit null-check after the
call to ctx.getObject (checking the variable note) and handle the missing object
by returning a clear error/HTTP 404 or throwing a descriptive error, and
optionally log the situation before exiting the handler so note?.attributionIds
is only accessed when note is non-null.
In `@examples/nuxt/server/api/profile/`[identifier].get.ts:
- Line 6: The code blindly asserts event.context.params?.identifier as string;
instead validate that event.context.params?.identifier exists and is a string
(not an array) before using it: check that identifier !== undefined and typeof
identifier === 'string' (or Array.isArray(identifier) === false), and if
validation fails return/throw a proper HTTP error (e.g., 400/404) from this
handler so downstream code in this route doesn't receive an invalid value;
update the variable usage around identifier to use the validated value.
In `@examples/nuxt/server/api/search.get.ts`:
- Around line 32-34: The empty catch after the lookupObject call swallows
errors; update the catch block in the search endpoint (the try/catch surrounding
lookupObject) to log the caught error for debugging—e.g., call console.debug or
use the existing logger with a short message and the error object so lookup
failures are visible during development without changing behavior for users.
In `@examples/nuxt/server/api/unfollow.post.ts`:
- Around line 23-41: Wrap the ctx.sendActivity(...) invocation in a try-catch to
prevent a thrown error from turning into a 500; call ctx.sendActivity with the
same Undo/Follow payload (using identifier, target, Undo, Follow,
ctx.getActorUri) inside the try, and in the catch log the error and continue
with the local un-follow flow (update any local state and perform the redirect)
so UX proceeds even if the network/remote activity fails.
In `@examples/nuxt/server/federation.ts`:
- Around line 97-107: In the Undo handler (.on(Undo, async (context, undo) => {
... })) you currently delete relationStore for any undone Follow when
undo.actorId exists; instead, first resolve and validate that the undone
Follow's target (activity.object / activity.objectId / activity.id) actually
refers to our local user (e.g., compare to the local actor id like "/users/demo"
or the localActor.id) before calling relationStore.delete and broadcastEvent;
keep the existing instanceof Follow check, ensure you use the resolved object id
(not just undo.actorId) to confirm the Follow was aimed at our user, and only
then remove the follower entry via relationStore.delete(undo.actorId.href) and
call broadcastEvent.
In `@examples/nuxt/server/sse.ts`:
- Around line 16-20: broadcastEvent currently iterates clients and calls
client.send which can throw and abort the whole fanout; wrap the per-client send
call in a try/catch inside broadcastEvent so one failing client doesn't stop
others — on error log the failure (or at minimum swallow it) and optionally
remove/close the bad client from the clients collection to avoid repeated
failures; reference the broadcastEvent function, the clients iterable, and
client.send when making the change.
In `@packages/init/src/webframeworks/nuxt.ts`:
- Around line 38-54: The command array returned by getInitCommand currently
appends shell tokens ("&& rm nuxt.config.ts") which will be passed as argv to
nuxi or break on non-POSIX shells; remove the cleanup tokens from the yielded
array in getInitCommand/getNuxtInitCommand and instead perform the
nuxt.config.ts removal as a separate init pipeline step (e.g., add a post-init
action that deletes "nuxt.config.ts") or ensure the generated files entry will
overwrite that file; update any pipeline/init runner code to call that deletion
action rather than embedding shell commands in the command argv.
🪄 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: 0a57880d-7345-4b8d-8d11-ef644bc34776
⛔ Files ignored due to path filters (4)
deno.lockis excluded by!**/*.lockexamples/nuxt/public/demo-profile.pngis excluded by!**/*.pngexamples/nuxt/public/fedify-logo.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (57)
.agents/skills/add-to-fedify-init/SKILL.md.agents/skills/create-example-app-with-integration/SKILL.md.agents/skills/create-example-app-with-integration/example/README.md.agents/skills/create-example-app-with-integration/example/src/logging.ts.agents/skills/create-integration-package/SKILL.md.hongdown.tomlAGENTS.mdCHANGES.mdCONTRIBUTING.mdcspell.jsondeno.jsondocs/manual/integration.mdexamples/nuxt/.gitignoreexamples/nuxt/README.mdexamples/nuxt/app.vueexamples/nuxt/nuxt.config.tsexamples/nuxt/package.jsonexamples/nuxt/pages/index.vueexamples/nuxt/pages/users/[identifier]/index.vueexamples/nuxt/pages/users/[identifier]/posts/[id].vueexamples/nuxt/public/style.cssexamples/nuxt/public/theme.jsexamples/nuxt/server/api/events.get.tsexamples/nuxt/server/api/follow.post.tsexamples/nuxt/server/api/home.get.tsexamples/nuxt/server/api/post.post.tsexamples/nuxt/server/api/posts/[identifier]/[id].get.tsexamples/nuxt/server/api/profile/[identifier].get.tsexamples/nuxt/server/api/search.get.tsexamples/nuxt/server/api/unfollow.post.tsexamples/nuxt/server/federation.tsexamples/nuxt/server/plugins/logging.tsexamples/nuxt/server/sse.tsexamples/nuxt/server/store.tsexamples/nuxt/tsconfig.jsonexamples/test-examples/mod.tsmise.tomlpackages/fedify/README.mdpackages/init/src/const.tspackages/init/src/json/deps.jsonpackages/init/src/templates/nuxt/nuxt.config.ts.tplpackages/init/src/test/lookup.tspackages/init/src/test/port.tspackages/init/src/webframeworks/mod.tspackages/init/src/webframeworks/nuxt.tspackages/nuxt/README.mdpackages/nuxt/deno.jsonpackages/nuxt/package.jsonpackages/nuxt/src/mod.test.tspackages/nuxt/src/mod.tspackages/nuxt/src/module.tspackages/nuxt/src/runtime/server/lib.tspackages/nuxt/src/runtime/server/logic.tspackages/nuxt/src/runtime/server/middleware.tspackages/nuxt/src/runtime/server/plugin.tspackages/nuxt/tsdown.config.tspnpm-workspace.yaml
| { rel: "stylesheet", href: "/style.css" }, | ||
| { rel: "icon", type: "image/svg+xml", href: "/fedify-logo.svg" }, | ||
| ], | ||
| script: [{ src: "/theme.js" }], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that app head script is currently non-deferred and theme.js uses document.body at top-level.
rg -n 'script:\s*\[\{ src: "/theme.js"' examples/nuxt/app.vue
rg -n 'document\.body\.classList' examples/nuxt/public/theme.jsRepository: fedify-dev/fedify
Length of output: 275
Add defer attribute to prevent script execution before document.body exists.
Line 11 injects /theme.js without deferred loading. The script immediately accesses document.body.classList at the top level, which will fail if executed during head parsing before the body element is available.
Suggested fix
- script: [{ src: "/theme.js" }],
+ script: [{ src: "/theme.js", defer: true }],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| script: [{ src: "/theme.js" }], | |
| script: [{ src: "/theme.js", defer: true }], |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/app.vue` at line 11, The injected script object in the script
array (script: [{ src: "/theme.js" }]) runs too early and can access
document.body before it exists; modify that script entry to include the defer
attribute so the browser defers execution until after parsing (e.g., add a
defer:true property on the object or otherwise render the tag with defer) so
/theme.js runs only after the body is available.
| function onSearchInput() { | ||
| if (searchTimeout) clearTimeout(searchTimeout); | ||
| searchTimeout = setTimeout(async () => { | ||
| if (!searchQuery.value.trim()) { | ||
| searchResult.value = null; | ||
| return; | ||
| } | ||
| const res = await $fetch<{ result: typeof searchResult.value }>( | ||
| `/api/search?q=${encodeURIComponent(searchQuery.value)}`, | ||
| ); | ||
| searchResult.value = res.result; | ||
| }, 300); |
There was a problem hiding this comment.
Ignore stale search responses.
The debounce reduces request count, but it does not serialize responses. If an older /api/search request resolves after a newer one, searchResult is overwritten with stale data and the follow/unfollow form can point at the wrong actor.
🛠️ Proposed fix
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
+let latestSearchRequest = 0;
function onSearchInput() {
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
+ const requestId = ++latestSearchRequest;
if (!searchQuery.value.trim()) {
searchResult.value = null;
return;
}
const res = await $fetch<{ result: typeof searchResult.value }>(
`/api/search?q=${encodeURIComponent(searchQuery.value)}`,
);
- searchResult.value = res.result;
+ if (requestId === latestSearchRequest) {
+ searchResult.value = res.result;
+ }
}, 300);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/pages/index.vue` around lines 183 - 194, The current
onSearchInput handler can apply stale results because out-of-order fetches
overwrite searchResult; modify onSearchInput to track and ignore stale responses
by incrementing a request counter (e.g., localRequestId / lastHandledRequestId)
or by using an AbortController to cancel the previous $fetch before starting a
new one; ensure you reference and update the shared identifier (searchTimeout,
searchQuery, searchResult) and only assign searchResult.value when the
response's request id matches the latest id (or when the fetch wasn't aborted)
so older responses are ignored.
| const route = useRoute(); | ||
| const identifier = route.params.identifier as string; | ||
|
|
||
| const { data } = await useFetch(`/api/profile/${identifier}`); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider handling the error state from useFetch.
useFetch returns { data, error, ... }. Currently only data is destructured. If the API returns a 500 or network error, data will be null and error will contain the error. The UI shows "User not found" for both cases, which may be misleading. For a demo this is acceptable, but consider logging or displaying errors distinctly.
♻️ Handle error state
-const { data } = await useFetch(`/api/profile/${identifier}`);
+const { data, error } = await useFetch(`/api/profile/${identifier}`);
+
+if (error.value) {
+ console.error("Failed to fetch profile:", error.value);
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { data } = await useFetch(`/api/profile/${identifier}`); | |
| const { data, error } = await useFetch(`/api/profile/${identifier}`); | |
| if (error.value) { | |
| console.error("Failed to fetch profile:", error.value); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/pages/users/`[identifier]/index.vue at line 64, The useFetch
call only destructures data (const { data } = await
useFetch(`/api/profile/${identifier}`)) so API/network failures aren't handled;
update the call to also extract error (and optionally pending) and then check
error before treating null data as "User not found": e.g., capture const { data,
error } = await useFetch(...), log or display error.message when error is
truthy, and only show the "not found" UI when error is null but data is empty;
ensure checks reference useFetch, data, error and the identifier route/component
so behavior changes apply to this page.
| @@ -0,0 +1,61 @@ | |||
| <template> | |||
| <div v-if="data" class="post-detail-container"> | |||
| <a class="back-link" href="/">← Back to home</a> | |||
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider using <NuxtLink> for SPA navigation.
The hardcoded <a href="/"> at lines 3 and 34 triggers full page reloads. For smoother navigation in a Nuxt app, prefer <NuxtLink to="/">.
♻️ Suggested change
- <a class="back-link" href="/">← Back to home</a>
+ <NuxtLink class="back-link" to="/">← Back to home</NuxtLink>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <a class="back-link" href="/">← Back to home</a> | |
| <NuxtLink class="back-link" to="/">← Back to home</NuxtLink> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/pages/users/`[identifier]/posts/[id].vue at line 3, The back
navigation anchors use plain <a href="/"> which causes full page reloads;
replace those anchors (the occurrences with class "back-link" in the
users/[identifier]/posts/[id].vue page) with <NuxtLink to="/"> preserving
existing attributes/classes and inner text to enable client-side SPA navigation;
ensure both instances (the one near the top and the one near the bottom) are
updated to NuxtLink so navigation is smooth and consistent.
| } catch { | ||
| // lookup failed | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider logging lookup failures for debugging.
The empty catch block silently swallows all errors from lookupObject. For a demo, adding a console.debug would help diagnose issues during development without affecting user experience.
♻️ Optional improvement
- } catch {
- // lookup failed
+ } catch (error) {
+ console.debug("Actor lookup failed:", q, error);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch { | |
| // lookup failed | |
| } | |
| } catch (error) { | |
| console.debug("Actor lookup failed:", q, error); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/server/api/search.get.ts` around lines 32 - 34, The empty catch
after the lookupObject call swallows errors; update the catch block in the
search endpoint (the try/catch surrounding lookupObject) to log the caught error
for debugging—e.g., call console.debug or use the existing logger with a short
message and the error object so lookup failures are visible during development
without changing behavior for users.
| await ctx.sendActivity( | ||
| { identifier }, | ||
| target, | ||
| new Undo({ | ||
| id: new URL( | ||
| `#undo-follows/${target.id.href}`, | ||
| ctx.getActorUri(identifier), | ||
| ), | ||
| actor: ctx.getActorUri(identifier), | ||
| object: new Follow({ | ||
| id: new URL( | ||
| `#follows/${target.id.href}`, | ||
| ctx.getActorUri(identifier), | ||
| ), | ||
| actor: ctx.getActorUri(identifier), | ||
| object: target.id, | ||
| }), | ||
| }), | ||
| ); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider wrapping sendActivity in try-catch for resilience.
If sendActivity fails (network error, remote server down), the unhandled exception will cause a 500 error. For a demo this may be acceptable, but wrapping in try-catch with a fallback (still update local state and redirect) would improve UX.
🛡️ Proposed resilient handling
- await ctx.sendActivity(
- { identifier },
- target,
- new Undo({
- id: new URL(
- `#undo-follows/${target.id.href}`,
- ctx.getActorUri(identifier),
- ),
- actor: ctx.getActorUri(identifier),
- object: new Follow({
- id: new URL(
- `#follows/${target.id.href}`,
- ctx.getActorUri(identifier),
- ),
- actor: ctx.getActorUri(identifier),
- object: target.id,
- }),
- }),
- );
+ try {
+ await ctx.sendActivity(
+ { identifier },
+ target,
+ new Undo({
+ id: new URL(
+ `#undo-follows/${target.id.href}`,
+ ctx.getActorUri(identifier),
+ ),
+ actor: ctx.getActorUri(identifier),
+ object: new Follow({
+ id: new URL(
+ `#follows/${target.id.href}`,
+ ctx.getActorUri(identifier),
+ ),
+ actor: ctx.getActorUri(identifier),
+ object: target.id,
+ }),
+ }),
+ );
+ } catch (error) {
+ console.error("Failed to send Undo activity:", error);
+ // Continue with local state update even if federation fails
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await ctx.sendActivity( | |
| { identifier }, | |
| target, | |
| new Undo({ | |
| id: new URL( | |
| `#undo-follows/${target.id.href}`, | |
| ctx.getActorUri(identifier), | |
| ), | |
| actor: ctx.getActorUri(identifier), | |
| object: new Follow({ | |
| id: new URL( | |
| `#follows/${target.id.href}`, | |
| ctx.getActorUri(identifier), | |
| ), | |
| actor: ctx.getActorUri(identifier), | |
| object: target.id, | |
| }), | |
| }), | |
| ); | |
| try { | |
| await ctx.sendActivity( | |
| { identifier }, | |
| target, | |
| new Undo({ | |
| id: new URL( | |
| `#undo-follows/${target.id.href}`, | |
| ctx.getActorUri(identifier), | |
| ), | |
| actor: ctx.getActorUri(identifier), | |
| object: new Follow({ | |
| id: new URL( | |
| `#follows/${target.id.href}`, | |
| ctx.getActorUri(identifier), | |
| ), | |
| actor: ctx.getActorUri(identifier), | |
| object: target.id, | |
| }), | |
| }), | |
| ); | |
| } catch (error) { | |
| console.error("Failed to send Undo activity:", error); | |
| // Continue with local state update even if federation fails | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/server/api/unfollow.post.ts` around lines 23 - 41, Wrap the
ctx.sendActivity(...) invocation in a try-catch to prevent a thrown error from
turning into a 500; call ctx.sendActivity with the same Undo/Follow payload
(using identifier, target, Undo, Follow, ctx.getActorUri) inside the try, and in
the catch log the error and continue with the local un-follow flow (update any
local state and perform the redirect) so UX proceeds even if the network/remote
activity fails.
| .on(Undo, async (context, undo) => { | ||
| const activity = await undo.getObject(context); | ||
| if (activity instanceof Follow) { | ||
| if (activity.id == null) { | ||
| return; | ||
| } | ||
| if (undo.actorId == null) { | ||
| return; | ||
| } | ||
| relationStore.delete(undo.actorId.href); | ||
| broadcastEvent(); |
There was a problem hiding this comment.
Validate the undone Follow before removing a follower.
This branch deletes relationStore for any undone Follow as long as undo.actorId is present. On the shared inbox, an unrelated Undo(Follow(...)) can remove a real follower because the handler never checks that activity.objectId resolves back to /users/demo.
🐛 Proposed fix
.on(Undo, async (context, undo) => {
const activity = await undo.getObject(context);
if (activity instanceof Follow) {
- if (activity.id == null) {
- return;
- }
- if (undo.actorId == null) {
+ if (activity.objectId == null || undo.actorId == null) {
return;
}
+ const result = context.parseUri(activity.objectId);
+ if (result?.type !== "actor" || result.identifier !== IDENTIFIER) {
+ return;
+ }
relationStore.delete(undo.actorId.href);
broadcastEvent();
} else {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .on(Undo, async (context, undo) => { | |
| const activity = await undo.getObject(context); | |
| if (activity instanceof Follow) { | |
| if (activity.id == null) { | |
| return; | |
| } | |
| if (undo.actorId == null) { | |
| return; | |
| } | |
| relationStore.delete(undo.actorId.href); | |
| broadcastEvent(); | |
| .on(Undo, async (context, undo) => { | |
| const activity = await undo.getObject(context); | |
| if (activity instanceof Follow) { | |
| if (activity.objectId == null || undo.actorId == null) { | |
| return; | |
| } | |
| const result = context.parseUri(activity.objectId); | |
| if (result?.type !== "actor" || result.identifier !== IDENTIFIER) { | |
| return; | |
| } | |
| relationStore.delete(undo.actorId.href); | |
| broadcastEvent(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/server/federation.ts` around lines 97 - 107, In the Undo
handler (.on(Undo, async (context, undo) => { ... })) you currently delete
relationStore for any undone Follow when undo.actorId exists; instead, first
resolve and validate that the undone Follow's target (activity.object /
activity.objectId / activity.id) actually refers to our local user (e.g.,
compare to the local actor id like "/users/demo" or the localActor.id) before
calling relationStore.delete and broadcastEvent; keep the existing instanceof
Follow check, ensure you use the resolved object id (not just undo.actorId) to
confirm the Follow was aimed at our user, and only then remove the follower
entry via relationStore.delete(undo.actorId.href) and call broadcastEvent.
| export function broadcastEvent(): void { | ||
| const data = JSON.stringify({ type: "update" }); | ||
| for (const client of clients) { | ||
| client.send(data); | ||
| } |
There was a problem hiding this comment.
Guard SSE fanout against per-client send failures.
A throw from client.send() on Line 19 can abort broadcastEvent() and propagate into federation handlers that call it, causing Follow/Undo processing to fail unexpectedly.
💡 Suggested fix
export function broadcastEvent(): void {
const data = JSON.stringify({ type: "update" });
for (const client of clients) {
- client.send(data);
+ try {
+ client.send(data);
+ } catch {
+ clients.delete(client);
+ try {
+ client.close();
+ } catch {
+ // ignore close errors from already-closed streams
+ }
+ }
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/server/sse.ts` around lines 16 - 20, broadcastEvent currently
iterates clients and calls client.send which can throw and abort the whole
fanout; wrap the per-client send call in a try/catch inside broadcastEvent so
one failing client doesn't stop others — on error log the failure (or at minimum
swallow it) and optionally remove/close the bad client from the clients
collection to avoid repeated failures; reference the broadcastEvent function,
the clients iterable, and client.send when making the change.
4cc14ed to
8ef4fc9
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 9
♻️ Duplicate comments (6)
examples/nuxt/app.vue (1)
11-11:⚠️ Potential issue | 🔴 CriticalDefer
/theme.jsto prevent earlydocument.bodyaccess.Line 11 injects a non-deferred head script, while
theme.jstouchesdocument.bodyimmediately. This can throw before body parsing completes.🔧 Proposed fix
- script: [{ src: "/theme.js" }], + script: [{ src: "/theme.js", defer: true }],#!/bin/bash # Verify script injection and immediate body access. rg -n 'script:\s*\[\{\s*src:\s*"/theme\.js"' examples/nuxt/app.vue rg -n 'document\.body\.classList' examples/nuxt/public/theme.js🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/app.vue` at line 11, The head script injection for theme.js is non-deferred and can access document.body before it exists; update the script entry (the object in the array currently written as script: [{ src: "/theme.js" }]) to include defer: true (e.g., script: [{ src: "/theme.js", defer: true }]) so the browser defers execution until after parsing the body, preventing early document.body access errors from public/theme.js which references document.body.classList.examples/nuxt/README.md (1)
10-10:⚠️ Potential issue | 🟡 MinorFix the intro wording on Line 10.
The sentence reads awkwardly: “using the Fedify and [Nuxt]”. Consider “using [Fedify] and [Nuxt]”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/README.md` at line 10, Update the awkward intro sentence that currently reads "using the Fedify and [Nuxt]" in the README examples/nuxt README: replace it with "using [Fedify] and [Nuxt]" so the wording is clear and parallel.examples/nuxt/server/api/post.post.ts (1)
31-33:⚠️ Potential issue | 🟠 MajorUse a per-post
Createactivity id.
new URL("#activity", attribution)gives everyCreatethe same id. Remote inboxes can deduplicate later posts as repeats because the activity IRI never changes. Include the post id, or another unique component, in the fragment.Based on learnings: Keep ActivityPub compliance in mind for interoperability when working with federation code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/post.post.ts` around lines 31 - 33, The Create activity currently uses a constant fragment new URL("#activity", attribution) which makes every Create share the same id; change the id generation in the Create constructor to include a per-post unique component (e.g., the post's id or another unique token) so each Create activity IRI is unique—update the Create instantiation (the id field alongside note and attribution) to build the URL fragment using note.id (or the post id variable) like "#activity-{postId}" or similar.examples/nuxt/pages/index.vue (1)
181-194:⚠️ Potential issue | 🟠 MajorIgnore stale search responses.
The debounce reduces request count, but it does not serialize responses. If an older
/api/searchcall resolves after a newer one,searchResultis overwritten with stale actor data and the follow/unfollow form can target the wrong account.💡 Proposed fix
let searchTimeout: ReturnType<typeof setTimeout> | null = null; +let latestSearchRequest = 0; function onSearchInput() { if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(async () => { + const requestId = ++latestSearchRequest; if (!searchQuery.value.trim()) { searchResult.value = null; return; } const res = await $fetch<{ result: typeof searchResult.value }>( `/api/search?q=${encodeURIComponent(searchQuery.value)}`, ); - searchResult.value = res.result; + if (requestId === latestSearchRequest) { + searchResult.value = res.result; + } }, 300); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/pages/index.vue` around lines 181 - 194, The debounce currently lets out-of-order fetch responses overwrite searchResult; modify onSearchInput to ignore stale responses by introducing a monotonic request identifier or an AbortController: increment a local requestId (e.g., searchRequestId) before calling $fetch (or create/replace an AbortController saved alongside searchTimeout), capture the id/controller in the async closure, and when the fetch resolves only update searchResult if the captured id matches the latest searchRequestId (or the controller was not aborted); reference searchTimeout, onSearchInput, searchQuery and searchResult when applying this change.packages/init/src/webframeworks/nuxt.ts (1)
38-54:⚠️ Potential issue | 🟠 MajorKeep cleanup out of the
commandargv.
commandis a raw argument vector, so&& rm nuxt.config.tswill be passed tonuxiliterally. The cleanup never runs, andrmis POSIX-only anyway; delete the file in a separate init step or just letfiles["nuxt.config.ts"]overwrite it.💡 Minimal fix
function* getInitCommand(pm: PackageManager) { yield* getNuxtInitCommand(pm); yield* [ "init", ".", "--template", "minimal", "--no-install", "--force", "--packageManager", pm, "--no-gitInit", "--no-modules", - "&&", - "rm", - "nuxt.config.ts", ]; }As per coding guidelines: Code should work across Deno, Node.js, and Bun environments.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/init/src/webframeworks/nuxt.ts` around lines 38 - 54, The init command currently yields "&& rm nuxt.config.ts" inside getInitCommand/getNuxtInitCommand which passes the string to nuxi (so cleanup never runs) and is POSIX-only; remove the "&& rm nuxt.config.ts" token from the yielded argv and perform the removal as a separate cross-platform step after the nuxi init completes (e.g., run a cleanup function that deletes nuxt.config.ts or rely on files["nuxt.config.ts"] to overwrite it), referencing getInitCommand and getNuxtInitCommand to locate where to remove the token and add the new cleanup step.examples/nuxt/server/federation.ts (1)
97-107:⚠️ Potential issue | 🟠 MajorValidate the undone
Followtarget before deleting a follower.Any
Undo(Follow(...))with anundo.actorIdcurrently removes the stored follower, even when the originalFollowtargeted some other actor/path. On the shared inbox, that can delete a real follower entry for an unrelated undo.💡 Proposed fix
.on(Undo, async (context, undo) => { const activity = await undo.getObject(context); if (activity instanceof Follow) { - if (activity.id == null) { - return; - } - if (undo.actorId == null) { + if (activity.objectId == null || undo.actorId == null) { return; } + const result = context.parseUri(activity.objectId); + if (result?.type !== "actor" || result.identifier !== IDENTIFIER) { + return; + } relationStore.delete(undo.actorId.href); broadcastEvent(); } else {Based on learnings: Keep ActivityPub compliance in mind for interoperability when working with federation code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/federation.ts` around lines 97 - 107, The Undo(Follow) handler currently deletes the follower unconditionally; change it to first validate that the undone Follow's target matches the stored relation before removing anything. In the .on(Undo, ...) handler where you call activity = await undo.getObject(context) and you check activity instanceof Follow and activity.id, also verify that activity.id (or activity.id.href) equals the stored follow target for undo.actorId (or that relationStore contains a matching entry linking undo.actorId to that specific activity.id) and only then call relationStore.delete(undo.actorId.href) and broadcastEvent(); otherwise ignore the Undo.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.agents/skills/create-integration-package/SKILL.md:
- Around line 187-189: Replace the permissive guidance "write unit tests as well
if possible" with a firm requirement that new integrations must include unit
tests (naming convention `*.test.ts`, e.g., `src/mod.test.ts`) except when a
documented technical blocker exists; update the related phrasing in the same
section (and the repeated guidance around lines 207-220) to state tests are
required unless there is an explicit, documented blocker and ensure the
checklist/process text enforces adding unit tests before feature implementation.
- Around line 63-69: The cp command in
.agents/skills/create-integration-package/SKILL.md uses the glob "package/*"
which skips hidden dotfiles; update the instructions so the template copy copies
all files including dotfiles by replacing the cp target from "package/*" to
"package/." in the section that shows the commands (the lines that mention mkdir
-p packages/framework and the cp -r ... package/* packages/framework/ command).
In `@examples/nuxt/public/style.css`:
- Around line 143-154: The badge .fedify-anchor has insufficient contrast (white
on `#7dd3fc` and further faded by the wrapper's opacity: 0.7); update the styles
to ensure WCAG contrast by either using a darker background (e.g., a stronger
blue) or switching the text color from white to a high-contrast dark color, and
remove or neutralize the parent wrapper's opacity so the badge color isn’t
faded; make the same change for the duplicate .fedify-anchor rule later in the
file (the second occurrence).
In `@examples/nuxt/README.md`:
- Around line 12-16: The reference-style link definitions for [Fedify], [Nuxt],
[Mastodon], and [Misskey] are currently mid-document; move those lines to the
end of the section (or end of the README) so all reference-style links sit
together as per repo markdown conventions, ensuring any headings or paragraphs
that use those labels keep their inline references unchanged while relocating
the four definitions to the document/section footer.
In `@examples/nuxt/server/api/follow.post.ts`:
- Around line 18-41: The remote lookup and delivery calls (ctx.lookupObject and
ctx.sendActivity) can reject and currently turn the form submission into a 500;
wrap the remote operations in a try/catch so any error (from await
ctx.lookupObject(...) or await ctx.sendActivity(...)) is handled as a soft-fail:
log or swallow the error, skip or abort delivery logic (so you don't call
followingStore.set or broadcastEvent on failure), and always fall back to return
sendRedirect(event, "/", 303). Keep the existing early-return when target?.id is
null, perform the delivery inside the try block (referencing ctx.lookupObject,
ctx.sendActivity, Follow, and ctx.getActorUri), and ensure the catch still
executes sendRedirect(event, "/", 303).
In `@examples/nuxt/server/api/post.post.ts`:
- Around line 19-24: The Note instance creation for const post omits the
published field, so locally created posts end up with published: null; update
the Note constructor call in the post creation (the const post = new Note({...})
block) to include a published property, e.g. published: published || new
Date().toISOString(), or published: published if a timestamp is already
provided, so the stored note has a valid publish time that downstream code
(federation handlers and home/posts getters) can read.
- Around line 3-4: The POST handler in post.post.ts updates the shared postStore
but never notifies SSE clients; after mutating postStore in the request handler
(the function that processes the incoming post and calls postStore.*), call
broadcastEvent(...) to emit an SSE update (use the same broadcastEvent signature
used by follow/unfollow handlers) so home.get.ts clients get real-time updates;
locate the POST handler (default export or function handling the route) and add
the broadcastEvent call immediately after the postStore mutation.
In `@examples/nuxt/server/api/search.get.ts`:
- Around line 7-10: The code casts query.q to string and calls .trim() which can
throw if getQuery() returned a string[] for repeated params; update the
narrowing around query.q (from getQuery(event)) by using Array.isArray(query.q)
to detect arrays and pick a safe string (e.g., first element) or reject arrays,
then perform the .trim() check on the narrowed string. Specifically, locate the
q assignment and the conditional around q.trim() in the handler that uses
getQuery(event)/query, replace the direct cast with a runtime-narrowing like: if
Array.isArray(query.q) handle or extract query.q[0] else use query.q, then
continue with the existing empty/trim validation.
In `@packages/nuxt/src/module.test.ts`:
- Around line 6-31: Add a test case that asserts the Nuxt4-specific server alias
'#server/...' resolves to an absolute path: call
resolveModulePath("#server/federation", aliasesWithServer, rootDir) (where
aliases includes "#server": "/app"), assert isAbsolute(result) and equal(result,
"/app/server/federation"); place this alongside the existing '~/...' and './...'
assertions in the same test ("relative module path must resolve to absolute
path") to cover the Nuxt 4 dedicated server alias handling by resolveModulePath.
---
Duplicate comments:
In `@examples/nuxt/app.vue`:
- Line 11: The head script injection for theme.js is non-deferred and can access
document.body before it exists; update the script entry (the object in the array
currently written as script: [{ src: "/theme.js" }]) to include defer: true
(e.g., script: [{ src: "/theme.js", defer: true }]) so the browser defers
execution until after parsing the body, preventing early document.body access
errors from public/theme.js which references document.body.classList.
In `@examples/nuxt/pages/index.vue`:
- Around line 181-194: The debounce currently lets out-of-order fetch responses
overwrite searchResult; modify onSearchInput to ignore stale responses by
introducing a monotonic request identifier or an AbortController: increment a
local requestId (e.g., searchRequestId) before calling $fetch (or create/replace
an AbortController saved alongside searchTimeout), capture the id/controller in
the async closure, and when the fetch resolves only update searchResult if the
captured id matches the latest searchRequestId (or the controller was not
aborted); reference searchTimeout, onSearchInput, searchQuery and searchResult
when applying this change.
In `@examples/nuxt/README.md`:
- Line 10: Update the awkward intro sentence that currently reads "using the
Fedify and [Nuxt]" in the README examples/nuxt README: replace it with "using
[Fedify] and [Nuxt]" so the wording is clear and parallel.
In `@examples/nuxt/server/api/post.post.ts`:
- Around line 31-33: The Create activity currently uses a constant fragment new
URL("#activity", attribution) which makes every Create share the same id; change
the id generation in the Create constructor to include a per-post unique
component (e.g., the post's id or another unique token) so each Create activity
IRI is unique—update the Create instantiation (the id field alongside note and
attribution) to build the URL fragment using note.id (or the post id variable)
like "#activity-{postId}" or similar.
In `@examples/nuxt/server/federation.ts`:
- Around line 97-107: The Undo(Follow) handler currently deletes the follower
unconditionally; change it to first validate that the undone Follow's target
matches the stored relation before removing anything. In the .on(Undo, ...)
handler where you call activity = await undo.getObject(context) and you check
activity instanceof Follow and activity.id, also verify that activity.id (or
activity.id.href) equals the stored follow target for undo.actorId (or that
relationStore contains a matching entry linking undo.actorId to that specific
activity.id) and only then call relationStore.delete(undo.actorId.href) and
broadcastEvent(); otherwise ignore the Undo.
In `@packages/init/src/webframeworks/nuxt.ts`:
- Around line 38-54: The init command currently yields "&& rm nuxt.config.ts"
inside getInitCommand/getNuxtInitCommand which passes the string to nuxi (so
cleanup never runs) and is POSIX-only; remove the "&& rm nuxt.config.ts" token
from the yielded argv and perform the removal as a separate cross-platform step
after the nuxi init completes (e.g., run a cleanup function that deletes
nuxt.config.ts or rely on files["nuxt.config.ts"] to overwrite it), referencing
getInitCommand and getNuxtInitCommand to locate where to remove the token and
add the new cleanup step.
🪄 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: 5c0989ec-9b87-4d6e-bfe4-34d4cc9d9fb1
⛔ Files ignored due to path filters (3)
examples/nuxt/public/demo-profile.pngis excluded by!**/*.pngexamples/nuxt/public/fedify-logo.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (45)
.agents/skills/commit/SKILL.md.agents/skills/create-example-app-with-integration/example/README.md.agents/skills/create-integration-package/SKILL.mdCHANGES.mddocs/manual/integration.mdexamples/nuxt/.gitignoreexamples/nuxt/README.mdexamples/nuxt/app.vueexamples/nuxt/nuxt.config.tsexamples/nuxt/package.jsonexamples/nuxt/pages/index.vueexamples/nuxt/pages/users/[identifier]/index.vueexamples/nuxt/pages/users/[identifier]/posts/[id].vueexamples/nuxt/public/style.cssexamples/nuxt/public/theme.jsexamples/nuxt/server/api/events.get.tsexamples/nuxt/server/api/follow.post.tsexamples/nuxt/server/api/home.get.tsexamples/nuxt/server/api/post.post.tsexamples/nuxt/server/api/posts/[identifier]/[id].get.tsexamples/nuxt/server/api/profile/[identifier].get.tsexamples/nuxt/server/api/search.get.tsexamples/nuxt/server/api/unfollow.post.tsexamples/nuxt/server/federation.tsexamples/nuxt/server/plugins/logging.tsexamples/nuxt/server/sse.tsexamples/nuxt/server/store.tsexamples/nuxt/tsconfig.jsonexamples/test-examples/mod.tspackages/init/src/const.tspackages/init/src/json/deps.jsonpackages/init/src/templates/nuxt/nuxt.config.ts.tplpackages/init/src/test/lookup.tspackages/init/src/test/port.tspackages/init/src/webframeworks/mod.tspackages/init/src/webframeworks/nuxt.tspackages/nuxt/README.mdpackages/nuxt/package.jsonpackages/nuxt/src/module.test.tspackages/nuxt/src/module.tspackages/nuxt/src/runtime/server/logic.test.tspackages/nuxt/src/runtime/server/logic.tspackages/nuxt/src/runtime/server/plugin.test.tspackages/nuxt/src/runtime/server/plugin.tspnpm-workspace.yaml
| import federation from "../federation"; | ||
| import { postStore } from "../store"; |
There was a problem hiding this comment.
Emit an SSE update after a successful post.
examples/nuxt/server/api/home.get.ts renders from postStore, and the follow/unfollow handlers already call broadcastEvent() after mutating shared state. This handler updates postStore but never notifies SSE clients, so other open home pages will not see new posts until a manual refresh.
♻️ Proposed fix
import { Create, Note } from "@fedify/vocab";
import { readBody, sendRedirect, toWebRequest } from "h3";
import federation from "../federation";
+import { broadcastEvent } from "../sse";
import { postStore } from "../store";
@@
await ctx.sendActivity(
{ identifier },
"followers",
new Create({
id: new URL("#activity", attribution),
object: note,
actors: note?.attributionIds,
tos: note?.toIds,
ccs: note?.ccIds,
}),
);
+ broadcastEvent();
} catch {
postStore.delete(url);
}Also applies to: 25-43
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/server/api/post.post.ts` around lines 3 - 4, The POST handler
in post.post.ts updates the shared postStore but never notifies SSE clients;
after mutating postStore in the request handler (the function that processes the
incoming post and calls postStore.*), call broadcastEvent(...) to emit an SSE
update (use the same broadcastEvent signature used by follow/unfollow handlers)
so home.get.ts clients get real-time updates; locate the POST handler (default
export or function handling the route) and add the broadcastEvent call
immediately after the postStore mutation.
| const query = getQuery(event); | ||
| const q = query.q as string | undefined; | ||
| if (!q || !q.trim()) { | ||
| return { result: null }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In h3/Nuxt, can getQuery(event).q be a string[] when the URL contains repeated query parameters, and is narrowing with Array.isArray(...) the recommended way to handle that?
💡 Result:
Yes, in h3/Nuxt, getQuery(event).q can be a string[] when the URL contains repeated query parameters like ?q=foo&q=bar. This behavior is demonstrated in official Nuxt GitHub discussions where multiple values for the same key (especially with array notation like ids[]) result in arrays, and typing supports string | string[]. Using Array.isArray(q) to narrow the type from string | string[] to string[] is the recommended and standard TypeScript type guard approach for handling this polymorphism.
Citations:
- 1: Parsing Array with `getQuery` nuxt/nuxt#27181
- 2: https://stackoverflow.com/questions/76896297/how-to-add-typing-to-getqueryevent-in-nuxt-3
- 3: https://v7-7-1.vueuse.org/router/useRouteQuery/
🏁 Script executed:
cat -n examples/nuxt/server/api/search.get.tsRepository: fedify-dev/fedify
Length of output: 1885
Narrow query.q before calling .trim().
getQuery() returns string | string[] for repeated query parameters like ?q=a&q=b. The cast on line 8 hides this type, and calling .trim() on an array throws a TypeError at runtime.
Proposed fix
const query = getQuery(event);
- const q = query.q as string | undefined;
- if (!q || !q.trim()) {
+ const q = Array.isArray(query.q) ? query.q[0] : query.q;
+ if (typeof q !== "string" || !q.trim()) {
return { result: null };
}This violates the strict TypeScript typing guideline: use Array.isArray() to properly narrow the polymorphic type before accessing string methods.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const query = getQuery(event); | |
| const q = query.q as string | undefined; | |
| if (!q || !q.trim()) { | |
| return { result: null }; | |
| const query = getQuery(event); | |
| const q = Array.isArray(query.q) ? query.q[0] : query.q; | |
| if (typeof q !== "string" || !q.trim()) { | |
| return { result: null }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/server/api/search.get.ts` around lines 7 - 10, The code casts
query.q to string and calls .trim() which can throw if getQuery() returned a
string[] for repeated params; update the narrowing around query.q (from
getQuery(event)) by using Array.isArray(query.q) to detect arrays and pick a
safe string (e.g., first element) or reject arrays, then perform the .trim()
check on the narrowed string. Specifically, locate the q assignment and the
conditional around q.trim() in the handler that uses getQuery(event)/query,
replace the direct cast with a runtime-narrowing like: if Array.isArray(query.q)
handle or extract query.q[0] else use query.q, then continue with the existing
empty/trim validation.
| test( | ||
| "relative module path must resolve to absolute path", | ||
| () => { | ||
| const aliases = { "~": "/app", "@": "/app" }; | ||
| const rootDir = "/app"; | ||
|
|
||
| // Aliased path works correctly | ||
| const aliased = resolveModulePath("~/server/federation", aliases, rootDir); | ||
| ok( | ||
| isAbsolute(aliased), | ||
| `aliased path should be absolute, got: ${aliased}`, | ||
| ); | ||
|
|
||
| // Plain relative path must now also resolve to absolute | ||
| const relative = resolveModulePath( | ||
| "./server/federation", | ||
| aliases, | ||
| rootDir, | ||
| ); | ||
| ok( | ||
| isAbsolute(relative), | ||
| `relative path should be resolved to absolute, got: ${relative}`, | ||
| ); | ||
| equal(relative, "/app/server/federation"); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add an explicit #server/... alias test case.
Current coverage checks ~/... and relative paths, but Nuxt 4 integration should also be protected with a direct #server/... assertion.
♻️ Proposed test update
test(
"relative module path must resolve to absolute path",
() => {
- const aliases = { "~": "/app", "@": "/app" };
+ const aliases = { "~": "/app", "@": "/app", "#server": "/app/server" };
const rootDir = "/app";
@@
equal(relative, "/app/server/federation");
+
+ const serverAlias = resolveModulePath("#server/federation", aliases, rootDir);
+ equal(serverAlias, "/app/server/federation");
},
);Based on learnings, for Nuxt 4 the dedicated alias for files under server/ is #server/....
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test( | |
| "relative module path must resolve to absolute path", | |
| () => { | |
| const aliases = { "~": "/app", "@": "/app" }; | |
| const rootDir = "/app"; | |
| // Aliased path works correctly | |
| const aliased = resolveModulePath("~/server/federation", aliases, rootDir); | |
| ok( | |
| isAbsolute(aliased), | |
| `aliased path should be absolute, got: ${aliased}`, | |
| ); | |
| // Plain relative path must now also resolve to absolute | |
| const relative = resolveModulePath( | |
| "./server/federation", | |
| aliases, | |
| rootDir, | |
| ); | |
| ok( | |
| isAbsolute(relative), | |
| `relative path should be resolved to absolute, got: ${relative}`, | |
| ); | |
| equal(relative, "/app/server/federation"); | |
| }, | |
| ); | |
| test( | |
| "relative module path must resolve to absolute path", | |
| () => { | |
| const aliases = { "~": "/app", "@": "/app", "#server": "/app/server" }; | |
| const rootDir = "/app"; | |
| // Aliased path works correctly | |
| const aliased = resolveModulePath("~/server/federation", aliases, rootDir); | |
| ok( | |
| isAbsolute(aliased), | |
| `aliased path should be absolute, got: ${aliased}`, | |
| ); | |
| // Plain relative path must now also resolve to absolute | |
| const relative = resolveModulePath( | |
| "./server/federation", | |
| aliases, | |
| rootDir, | |
| ); | |
| ok( | |
| isAbsolute(relative), | |
| `relative path should be resolved to absolute, got: ${relative}`, | |
| ); | |
| equal(relative, "/app/server/federation"); | |
| const serverAlias = resolveModulePath("#server/federation", aliases, rootDir); | |
| equal(serverAlias, "/app/server/federation"); | |
| }, | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/nuxt/src/module.test.ts` around lines 6 - 31, Add a test case that
asserts the Nuxt4-specific server alias '#server/...' resolves to an absolute
path: call resolveModulePath("#server/federation", aliasesWithServer, rootDir)
(where aliases includes "#server": "/app"), assert isAbsolute(result) and
equal(result, "/app/server/federation"); place this alongside the existing
'~/...' and './...' assertions in the same test ("relative module path must
resolve to absolute path") to cover the Nuxt 4 dedicated server alias handling
by resolveModulePath.
40800bb to
b9a066b
Compare
b9a066b to
1c2ef9e
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (18)
examples/nuxt/README.md (2)
10-10:⚠️ Potential issue | 🟡 MinorTighten the intro wording for readability.
The phrase reads awkwardly; use “using [Fedify] and [Nuxt]”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/README.md` at line 10, Replace the awkward phrase "implementations using the Fedify and [Nuxt]." with the tightened wording "implementations using [Fedify] and [Nuxt]." so the intro reads cleanly; locate the sentence in README.md (the line containing "implementations using the Fedify and [Nuxt].") and update it to "implementations using [Fedify] and [Nuxt]."
12-15:⚠️ Potential issue | 🟡 MinorMove reference-style link definitions to the section/document end.
These definitions should be relocated to the end to match repository markdown conventions.
As per coding guidelines, “include reference-style links at the end of each section.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/README.md` around lines 12 - 15, Move the reference-style link definitions ([Fedify], [Nuxt], [Mastodon], [Misskey]) out of the middle of the README section and place them at the end of the relevant section or the document end to follow repository markdown conventions; locate where these labels are defined and cut/paste their link definitions to the bottom of the section/document, ensuring the labels remain unchanged and their references in the body still resolve.examples/nuxt/app.vue (1)
11-11:⚠️ Potential issue | 🔴 CriticalDefer
/theme.jsto prevent earlydocument.bodyaccess failure.Loading this script from
<head>without deferral can execute before<body>exists, causing a runtime crash intheme.js.🔧 Proposed fix
- script: [{ src: "/theme.js" }], + script: [{ src: "/theme.js", defer: true }],🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/app.vue` at line 11, The script entry for "/theme.js" is loaded too early and may execute before document.body exists; update the script object in the script array (the script: [...] entry) to include a defer attribute for "/theme.js" (e.g., add defer: true to the object referencing "/theme.js") so the browser defers execution until after parsing the body.examples/nuxt/server/api/events.get.ts (2)
5-7:⚠️ Potential issue | 🟡 MinorUse a single header strategy for SSE responses.
setResponseHeader(...)andnew Response(..., { headers })are both setting the same SSE headers; keep one approach to avoid misleading behavior.Also applies to: 29-35
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/events.get.ts` around lines 5 - 7, The SSE handler is setting duplicate headers via setResponseHeader(event, ...) and again when constructing new Response(..., { headers }), which can be misleading; pick one approach and remove the other. Locate the calls to setResponseHeader and the Response constructor in events.get.ts (the setResponseHeader(event, "Content-Type", "text/event-stream"), "Cache-Control", "Connection" block and the new Response(..., { headers }) block around lines 29–35) and keep only a single source of truth: either remove the setResponseHeader calls and rely solely on the headers object passed to new Response, or remove the headers object from new Response and rely on setResponseHeader for SSE headers, ensuring the remaining approach sets Content-Type, Cache-Control, and Connection correctly.
13-14:⚠️ Potential issue | 🟡 MinorHarden stream lifecycle on disconnect/send races.
controller.enqueue(...)can throw after disconnect, and thecloselistener should explicitly close the client stream before/after deregistration.♻️ Suggested fix
const client = { send(data: string) { - controller.enqueue(encoder.encode(`data: ${data}\n\n`)); + try { + controller.enqueue(encoder.encode(`data: ${data}\n\n`)); + } catch { + removeClient(client); + } }, close() { - controller.close(); + try { + controller.close(); + } catch { + // already closed + } }, }; addClient(client); event.node.req.on("close", () => { + client.close(); removeClient(client); });Also applies to: 23-25
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/events.get.ts` around lines 13 - 14, Wrap the controller.enqueue(encoder.encode(...)) call inside a try/catch in the send(data: string) function so any exception (e.g. due to client disconnect) is caught; on catch, call controller.close() (guarded to avoid double-close), remove/deregister the client from your clients collection, and return to stop further work. Likewise, update the close listener to explicitly call controller.close() (guarded) before deregistering the client so the stream is always closed even if enqueue races with disconnect; ensure both send and the close handler check a shared "closed" or client presence flag to avoid duplicate cleanup.examples/nuxt/server/sse.ts (1)
16-20:⚠️ Potential issue | 🟠 MajorIsolate per-client SSE send failures in fanout.
One throwing
client.send(...)currently abortsbroadcastEvent()for all clients and can break caller routes.♻️ Suggested fix
export function broadcastEvent(): void { const data = JSON.stringify({ type: "update" }); for (const client of clients) { - client.send(data); + try { + client.send(data); + } catch { + clients.delete(client); + try { + client.close(); + } catch { + // ignore close errors + } + } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/sse.ts` around lines 16 - 20, In broadcastEvent(), a thrown error from one client.send currently aborts fanout; wrap each client.send(data) in a per-client try/catch inside the for (const client of clients) loop (reference: clients and client.send) so a failing send is caught and handled (log the error via your logger and optionally remove/close that client from clients) and continue sending to remaining clients; ensure broadcastEvent still returns void even if some sends fail.examples/nuxt/pages/users/[identifier]/posts/[id].vue (1)
3-3: 🧹 Nitpick | 🔵 TrivialUse
<NuxtLink>for internal navigation paths.These anchors trigger full page reloads; router links keep client-side navigation smooth.
♻️ Suggested fix
- <a class="back-link" href="/">← Back to home</a> + <NuxtLink class="back-link" to="/">← Back to home</NuxtLink> ... - <a class="back-link" href="/">← Back to home</a> + <NuxtLink class="back-link" to="/">← Back to home</NuxtLink>Also applies to: 34-34
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/pages/users/`[identifier]/posts/[id].vue at line 3, Replace plain anchor tags used for internal links with Nuxt's client-side router component: find the <a class="back-link" href="/">…</a> usages (e.g., in pages/users/[identifier]/posts/[id].vue at the back-link occurrences) and change them to use <NuxtLink> with the equivalent to="/" prop and keep the class and text, ensuring internal navigation is handled client-side; update all duplicate occurrences (the one at line 3 and the one at line 34).examples/nuxt/server/api/unfollow.post.ts (1)
23-41:⚠️ Potential issue | 🟠 MajorGuard
sendActivityto avoid turning remote failures into 500s.A remote delivery failure currently aborts the request path before local cleanup/redirect.
♻️ Suggested fix
- await ctx.sendActivity( - { identifier }, - target, - new Undo({ - id: new URL( - `#undo-follows/${target.id.href}`, - ctx.getActorUri(identifier), - ), - actor: ctx.getActorUri(identifier), - object: new Follow({ - id: new URL( - `#follows/${target.id.href}`, - ctx.getActorUri(identifier), - ), - actor: ctx.getActorUri(identifier), - object: target.id, - }), - }), - ); + try { + await ctx.sendActivity( + { identifier }, + target, + new Undo({ + id: new URL( + `#undo-follows/${target.id.href}`, + ctx.getActorUri(identifier), + ), + actor: ctx.getActorUri(identifier), + object: new Follow({ + id: new URL( + `#follows/${target.id.href}`, + ctx.getActorUri(identifier), + ), + actor: ctx.getActorUri(identifier), + object: target.id, + }), + }), + ); + } catch (error) { + console.error("Failed to send Undo activity:", error); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/unfollow.post.ts` around lines 23 - 41, The call to ctx.sendActivity in unfollow.post.ts can throw on remote delivery and currently aborts the request, so wrap the ctx.sendActivity(...) invocation (the block creating new Undo and nested new Follow using ctx.getActorUri and target.id) in a try/catch; on error catch and log the failure (include the error and target.id), but do not rethrow—ensure the rest of the handler (local cleanup/redirect) always runs; optionally set a boolean flag indicating remoteDeliveryFailed for conditional handling later.examples/nuxt/pages/users/[identifier]/index.vue (1)
64-64:⚠️ Potential issue | 🟡 MinorHandle
useFetcherror state distinctly from "not found".Right now
nulldata from transport/server failures falls into the same UI path as missing users.♻️ Suggested fix
-const { data } = await useFetch(`/api/profile/${identifier}`); +const { data, error } = await useFetch(`/api/profile/${identifier}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/pages/users/`[identifier]/index.vue at line 64, The current code treats null `data` from useFetch(`/api/profile/${identifier}`) the same as a missing user; instead destructure and check the fetch error/status from useFetch (e.g., const { data, error, status } = await useFetch(...)) and handle three cases distinctly: if `error` is set (transport/server failure) render an error UI; else if `status === 404 || data.value === null` render the "not found" UI; otherwise render the profile with `data`; update any code that references `data` to use these checks (look for useFetch call, `data`, `error`, and `status` in this component).examples/nuxt/server/api/profile/[identifier].get.ts (1)
6-7:⚠️ Potential issue | 🟡 MinorValidate
identifiertype instead of asserting withas string.
event.context.params?.identifiershould be checked as a string before use.♻️ Suggested fix
- const identifier = event.context.params?.identifier as string; - if (identifier !== "demo") { + const identifier = event.context.params?.identifier; + if (typeof identifier !== "string" || identifier !== "demo") { return null; }As per coding guidelines, "Maintain strict TypeScript typing throughout."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/profile/`[identifier].get.ts around lines 6 - 7, Replace the unsafe type assertion for event.context.params?.identifier with a runtime string check: read the raw value (e.g., const rawIdentifier = event.context.params?.identifier), verify typeof rawIdentifier === "string" and only then assign to identifier; for non-string or missing values return/throw a proper validation error (HTTP 400 or createError) instead of proceeding. Ensure this change is applied where identifier is used (the route handler that currently declares const identifier = event.context.params?.identifier as string) so all downstream logic operates on a validated string.examples/nuxt/server/api/post.post.ts (3)
31-33:⚠️ Potential issue | 🟠 MajorMake the
Createactivity ID unique per post.
new URL("#activity", attribution)produces the same activity ID for every submission. Remote servers can deduplicate or reject later deliveries because the activity is no longer unique.♻️ Proposed fix
new Create({ - id: new URL("#activity", attribution), + id: new URL(`#activity/${id}`, attribution), object: note, actors: note?.attributionIds, tos: note?.toIds, ccs: note?.ccIds, }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/post.post.ts` around lines 31 - 33, The Create activity currently uses a constant fragment new URL("#activity", attribution) which yields the same activity ID for every post; change the ID generation in the Create constructor to produce a unique fragment per submission (for example using the post's unique identifier or a generated UUID/timestamp) so the activity id is unique: update the Create call that currently references new URL("#activity", attribution) to use new URL(`#activity-${uniqueValue}`, attribution) where uniqueValue is derived from the created note (note.id) or a crypto random UUID/timestamp generated in the same request handler.
19-24:⚠️ Potential issue | 🟠 MajorPersist
publishedon the createdNote.
/api/homeand/api/posts/[identifier]/[id]both readpublished, but this constructor never sets it. Every locally created post therefore renders without a timestamp, and the federated object is missing its publish time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/post.post.ts` around lines 19 - 24, The Note instance created as `post` is missing the `published` property so created posts lack a timestamp; update the constructor call that creates `new Note({ id: url, attribution, content: content.trim(), url })` to include `published` (e.g., `published: published ?? new Date()` or whatever source of publish time is intended) so the `post` object persists a publish timestamp that downstream endpoints (`/api/home`, `/api/posts/[identifier]/[id]`) can read.
25-38:⚠️ Potential issue | 🟠 MajorBroadcast an SSE update after a successful post.
This handler mutates
postStore, but unlike the follow/unfollow routes it never notifies/api/eventssubscribers. Other open home pages will stay stale until a manual refresh.♻️ Proposed fix
import { Create, Note } from "@fedify/vocab"; import { readBody, sendRedirect, toWebRequest } from "h3"; import federation from "../federation"; +import { broadcastEvent } from "../sse"; import { postStore } from "../store"; @@ await ctx.sendActivity( { identifier }, "followers", new Create({ id: new URL("#activity", attribution), @@ ccs: note?.ccIds, }), ); + broadcastEvent(); } catch { postStore.delete(url); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/post.post.ts` around lines 25 - 38, The handler mutates postStore (postStore.append) and sends an Activity (ctx.sendActivity) but never notifies /api/events subscribers, so add an SSE broadcast immediately after the successful sendActivity: call the same SSE notifier used by follow/unfollow routes (the module/function that publishes to /api/events — e.g., events broadcaster or sse.broadcast used elsewhere) to publish an update containing the new post (or a Create activity wrapping it) so open home pages receive the change; place this broadcast right after ctx.sendActivity completes and before the try block returns so subscribers are updated consistently.examples/nuxt/server/api/follow.post.ts (1)
18-41:⚠️ Potential issue | 🟠 MajorSoft-fail remote lookup/delivery and keep the 303 fallback.
If
ctx.lookupObject()orctx.sendActivity()rejects, this form submission becomes a 500. The handler should keep the existing redirect behavior and only updatefollowingStore/SSE after a successful delivery.♻️ Proposed fix
- const target = await ctx.lookupObject(targetUri) as APObject | null; - if (target?.id == null) { - return sendRedirect(event, "/", 303); - } - - await ctx.sendActivity( - { identifier }, - target, - new Follow({ - id: new URL( - `#follows/${target.id.href}`, - ctx.getActorUri(identifier), - ), - actor: ctx.getActorUri(identifier), - object: target.id, - }), - ); - - const { Person } = await import("@fedify/vocab"); - if (target instanceof Person) { - followingStore.set(target.id.href, target); + try { + const target = await ctx.lookupObject(targetUri) as APObject | null; + if (target?.id == null) { + return sendRedirect(event, "/", 303); + } + + await ctx.sendActivity( + { identifier }, + target, + new Follow({ + id: new URL( + `#follows/${target.id.href}`, + ctx.getActorUri(identifier), + ), + actor: ctx.getActorUri(identifier), + object: target.id, + }), + ); + + const { Person } = await import("@fedify/vocab"); + if (target instanceof Person) { + followingStore.set(target.id.href, target); + } + broadcastEvent(); + } catch { + return sendRedirect(event, "/", 303); } - broadcastEvent(); return sendRedirect(event, "/", 303);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/follow.post.ts` around lines 18 - 41, Wrap the remote operations so failures don't turn the POST into a 500: call ctx.lookupObject(targetUri) inside a try/catch and if it throws or returns null keep returning sendRedirect(event, "/", 303); likewise call ctx.sendActivity(...) inside a try/catch and on error still return sendRedirect(event, "/", 303) without updating followingStore or calling broadcastEvent; only after a successful ctx.sendActivity resolution should you run followingStore.set(target.id.href, target) and broadcastEvent. Ensure you still return sendRedirect(event, "/", 303) in all failure paths and only mutate followingStore/broadcast on successful delivery.examples/nuxt/server/api/search.get.ts (1)
7-10:⚠️ Potential issue | 🟠 MajorNarrow
query.qbefore calling.trim().
getQuery()can returnstring[]for repeated query params. The cast on Line 8 hides that, andq.trim()will throw at runtime when the request is?q=a&q=b.♻️ Proposed fix
const query = getQuery(event); - const q = query.q as string | undefined; - if (!q || !q.trim()) { + const q = Array.isArray(query.q) ? query.q[0] : query.q; + if (typeof q !== "string" || !q.trim()) { return { result: null }; }In h3/Nuxt, can `getQuery(event).q` be a `string[]` for repeated query parameters, and is `Array.isArray(query.q)` the correct way to narrow it before calling string methods like `trim()`?As per coding guidelines: "Maintain strict TypeScript typing throughout."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/api/search.get.ts` around lines 7 - 10, The code casts getQuery(event).q to string and calls q.trim(), which will throw if q is a string[] for repeated params; update the narrowing logic around getQuery and q so you detect Array.isArray(query.q) and handle it (e.g., pick the first element or return an error) before calling trim; specifically change the extraction of q so it does not use a blind cast and instead uses Array.isArray(query.q) to select a single string (or handle the array case) and then call .trim() on that narrowed value.examples/nuxt/public/style.css (1)
143-154:⚠️ Potential issue | 🟡 MinorIncrease the badge contrast.
White text on
#7dd3fcis too faint at this badge size, so the “Fedify” link is still hard to read.♻️ Proposed fix
.fedify-anchor { display: inline-flex; align-items: center; gap: 0.25rem; height: 1.5rem; padding: 0.125rem 0.25rem; border-radius: 0.375rem; background: `#7dd3fc`; - color: white; + color: `#082f49`; font-weight: 500; text-decoration: none; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/public/style.css` around lines 143 - 154, The .fedify-anchor badge has insufficient contrast; update the .fedify-anchor CSS to use a darker background or higher-contrast text color (e.g., change background from `#7dd3fc` to a deeper blue such as `#0284c7` or set color to a near-black like `#0f172a`) to meet readability/contrast requirements; adjust font-weight if needed (keep class name .fedify-anchor) and test the badge at its displayed size to confirm the contrast is improved.examples/nuxt/server/federation.ts (1)
97-107:⚠️ Potential issue | 🟠 MajorValidate the undone
Followtargets this actor before deleting the follower.On the shared inbox, an unrelated
Undo(Follow(...))can remove a real follower here because this branch never checks that the undone follow actually targeted/users/demo.♻️ Proposed fix
.on(Undo, async (context, undo) => { const activity = await undo.getObject(context); if (activity instanceof Follow) { - if (activity.id == null) { - return; - } - if (undo.actorId == null) { + if (activity.objectId == null || undo.actorId == null) { return; } + const result = context.parseUri(activity.objectId); + if (result?.type !== "actor" || result.identifier !== IDENTIFIER) { + return; + } relationStore.delete(undo.actorId.href); broadcastEvent(); } else {Based on learnings: "keep ActivityPub compliance in mind" when working with federation code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/server/federation.ts` around lines 97 - 107, The Undo(Follow) handler deletes a follower without confirming the undone Follow actually targeted our local actor; update the branch handling Undo/Follow (the async callback using undo.getObject and activity instanceof Follow) to check that activity.object (or activity.target) equals our local actor's id/URL (compare against the local actor identifier used in this module) before calling relationStore.delete(undo.actorId.href) and broadcastEvent(); only delete when undo.actorId is present and the Follow's object matches the local actor.examples/nuxt/pages/index.vue (1)
183-194:⚠️ Potential issue | 🟠 MajorPrevent stale debounced search responses from overwriting newer results.
Line 193 can apply out-of-order
/api/searchresponses, so an older request can replace a newer query’s result.🛠️ Suggested fix
let searchTimeout: ReturnType<typeof setTimeout> | null = null; +let latestSearchRequest = 0; function onSearchInput() { if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(async () => { + const requestId = ++latestSearchRequest; if (!searchQuery.value.trim()) { searchResult.value = null; return; } const res = await $fetch<{ result: typeof searchResult.value }>( `/api/search?q=${encodeURIComponent(searchQuery.value)}`, ); - searchResult.value = res.result; + if (requestId === latestSearchRequest) { + searchResult.value = res.result; + } }, 300); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/nuxt/pages/index.vue` around lines 183 - 194, The debounced onSearchInput can apply out-of-order responses and let an older /api/search result overwrite a newer one; fix by adding a per-request identifier (e.g., latestRequestId counter or token) at top-level near searchTimeout/searchResult/searchQuery, incrementing/getting a local requestId inside onSearchInput before calling $fetch, and after the await only assign to searchResult.value if the local requestId matches the current latestRequestId; alternatively use an AbortController signal passed into $fetch and abort previous requests when scheduling a new one—update the onSearchInput function and the surrounding state (latestRequestId or controller) accordingly to prevent stale responses from overwriting newer results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/nuxt/pages/index.vue`:
- Line 160: The external anchor element using target="_blank" (the <a
class="fedify-anchor" href="https://fedify.dev" target="_blank"> in index.vue)
must be hardened by adding rel="noopener noreferrer"; update that anchor to
include rel="noopener noreferrer" so the opened tab cannot access window.opener
and to avoid potential security/privacy issues.
- Around line 207-215: The onBeforeUnmount hook is being registered inside
onMounted—move lifecycle registration into the setup scope so hooks run
synchronously; declare a let eventSource: EventSource | null (or var) in the
outer setup scope, create the EventSource inside onMounted via onMounted(() => {
eventSource = new EventSource("/api/events"); ... }), set eventSource.onmessage
to call refresh(), and register onBeforeUnmount in the same setup scope to close
eventSource if it exists (check for null) to ensure proper cleanup.
In `@examples/nuxt/pages/users/`[identifier]/index.vue:
- Around line 37-40: The anchor element that sets target="_blank" (the fedify
anchor in the users/[identifier] page template) is missing rel protection;
update that <a> element used for the external link (class "fedify-anchor") to
include rel="noopener noreferrer" so the new tab cannot access window.opener and
to prevent potential security/performance issues.
In `@examples/nuxt/pages/users/`[identifier]/posts/[id].vue:
- Line 44: Destructure the response from useFetch to capture both data and error
(e.g., const { data, error } = await useFetch(...)) in the page that currently
calls useFetch(`/api/posts/${identifier}/${id}`); then update the rendering
logic to check error first and show a "Failed to load post" message when error
is truthy, and only treat data === null/undefined as "Post not found." Ensure
you reference the existing identifier and id variables and adjust any loading
states accordingly so API/network failures and missing posts are handled
distinctly.
In `@examples/nuxt/server/federation.ts`:
- Around line 133-145: The followers dispatcher registered via
setFollowersDispatcher ignores the path variable and required params; update its
callback signature to accept (context, identifier, cursor) like the actor
dispatcher, validate that identifier === IDENTIFIER before proceeding, and
return an error or empty result when it does not match; when valid, map
relationStore values to Recipient objects (id, inboxId, endpoints) and return {
items }; reference setFollowersDispatcher, IDENTIFIER, relationStore, and
Recipient to locate the code to change.
---
Duplicate comments:
In `@examples/nuxt/app.vue`:
- Line 11: The script entry for "/theme.js" is loaded too early and may execute
before document.body exists; update the script object in the script array (the
script: [...] entry) to include a defer attribute for "/theme.js" (e.g., add
defer: true to the object referencing "/theme.js") so the browser defers
execution until after parsing the body.
In `@examples/nuxt/pages/index.vue`:
- Around line 183-194: The debounced onSearchInput can apply out-of-order
responses and let an older /api/search result overwrite a newer one; fix by
adding a per-request identifier (e.g., latestRequestId counter or token) at
top-level near searchTimeout/searchResult/searchQuery, incrementing/getting a
local requestId inside onSearchInput before calling $fetch, and after the await
only assign to searchResult.value if the local requestId matches the current
latestRequestId; alternatively use an AbortController signal passed into $fetch
and abort previous requests when scheduling a new one—update the onSearchInput
function and the surrounding state (latestRequestId or controller) accordingly
to prevent stale responses from overwriting newer results.
In `@examples/nuxt/pages/users/`[identifier]/index.vue:
- Line 64: The current code treats null `data` from
useFetch(`/api/profile/${identifier}`) the same as a missing user; instead
destructure and check the fetch error/status from useFetch (e.g., const { data,
error, status } = await useFetch(...)) and handle three cases distinctly: if
`error` is set (transport/server failure) render an error UI; else if `status
=== 404 || data.value === null` render the "not found" UI; otherwise render the
profile with `data`; update any code that references `data` to use these checks
(look for useFetch call, `data`, `error`, and `status` in this component).
In `@examples/nuxt/pages/users/`[identifier]/posts/[id].vue:
- Line 3: Replace plain anchor tags used for internal links with Nuxt's
client-side router component: find the <a class="back-link" href="/">…</a>
usages (e.g., in pages/users/[identifier]/posts/[id].vue at the back-link
occurrences) and change them to use <NuxtLink> with the equivalent to="/" prop
and keep the class and text, ensuring internal navigation is handled
client-side; update all duplicate occurrences (the one at line 3 and the one at
line 34).
In `@examples/nuxt/public/style.css`:
- Around line 143-154: The .fedify-anchor badge has insufficient contrast;
update the .fedify-anchor CSS to use a darker background or higher-contrast text
color (e.g., change background from `#7dd3fc` to a deeper blue such as `#0284c7` or
set color to a near-black like `#0f172a`) to meet readability/contrast
requirements; adjust font-weight if needed (keep class name .fedify-anchor) and
test the badge at its displayed size to confirm the contrast is improved.
In `@examples/nuxt/README.md`:
- Line 10: Replace the awkward phrase "implementations using the Fedify and
[Nuxt]." with the tightened wording "implementations using [Fedify] and [Nuxt]."
so the intro reads cleanly; locate the sentence in README.md (the line
containing "implementations using the Fedify and [Nuxt].") and update it to
"implementations using [Fedify] and [Nuxt]."
- Around line 12-15: Move the reference-style link definitions ([Fedify],
[Nuxt], [Mastodon], [Misskey]) out of the middle of the README section and place
them at the end of the relevant section or the document end to follow repository
markdown conventions; locate where these labels are defined and cut/paste their
link definitions to the bottom of the section/document, ensuring the labels
remain unchanged and their references in the body still resolve.
In `@examples/nuxt/server/api/events.get.ts`:
- Around line 5-7: The SSE handler is setting duplicate headers via
setResponseHeader(event, ...) and again when constructing new Response(..., {
headers }), which can be misleading; pick one approach and remove the other.
Locate the calls to setResponseHeader and the Response constructor in
events.get.ts (the setResponseHeader(event, "Content-Type",
"text/event-stream"), "Cache-Control", "Connection" block and the new
Response(..., { headers }) block around lines 29–35) and keep only a single
source of truth: either remove the setResponseHeader calls and rely solely on
the headers object passed to new Response, or remove the headers object from new
Response and rely on setResponseHeader for SSE headers, ensuring the remaining
approach sets Content-Type, Cache-Control, and Connection correctly.
- Around line 13-14: Wrap the controller.enqueue(encoder.encode(...)) call
inside a try/catch in the send(data: string) function so any exception (e.g. due
to client disconnect) is caught; on catch, call controller.close() (guarded to
avoid double-close), remove/deregister the client from your clients collection,
and return to stop further work. Likewise, update the close listener to
explicitly call controller.close() (guarded) before deregistering the client so
the stream is always closed even if enqueue races with disconnect; ensure both
send and the close handler check a shared "closed" or client presence flag to
avoid duplicate cleanup.
In `@examples/nuxt/server/api/follow.post.ts`:
- Around line 18-41: Wrap the remote operations so failures don't turn the POST
into a 500: call ctx.lookupObject(targetUri) inside a try/catch and if it throws
or returns null keep returning sendRedirect(event, "/", 303); likewise call
ctx.sendActivity(...) inside a try/catch and on error still return
sendRedirect(event, "/", 303) without updating followingStore or calling
broadcastEvent; only after a successful ctx.sendActivity resolution should you
run followingStore.set(target.id.href, target) and broadcastEvent. Ensure you
still return sendRedirect(event, "/", 303) in all failure paths and only mutate
followingStore/broadcast on successful delivery.
In `@examples/nuxt/server/api/post.post.ts`:
- Around line 31-33: The Create activity currently uses a constant fragment new
URL("#activity", attribution) which yields the same activity ID for every post;
change the ID generation in the Create constructor to produce a unique fragment
per submission (for example using the post's unique identifier or a generated
UUID/timestamp) so the activity id is unique: update the Create call that
currently references new URL("#activity", attribution) to use new
URL(`#activity-${uniqueValue}`, attribution) where uniqueValue is derived from
the created note (note.id) or a crypto random UUID/timestamp generated in the
same request handler.
- Around line 19-24: The Note instance created as `post` is missing the
`published` property so created posts lack a timestamp; update the constructor
call that creates `new Note({ id: url, attribution, content: content.trim(), url
})` to include `published` (e.g., `published: published ?? new Date()` or
whatever source of publish time is intended) so the `post` object persists a
publish timestamp that downstream endpoints (`/api/home`,
`/api/posts/[identifier]/[id]`) can read.
- Around line 25-38: The handler mutates postStore (postStore.append) and sends
an Activity (ctx.sendActivity) but never notifies /api/events subscribers, so
add an SSE broadcast immediately after the successful sendActivity: call the
same SSE notifier used by follow/unfollow routes (the module/function that
publishes to /api/events — e.g., events broadcaster or sse.broadcast used
elsewhere) to publish an update containing the new post (or a Create activity
wrapping it) so open home pages receive the change; place this broadcast right
after ctx.sendActivity completes and before the try block returns so subscribers
are updated consistently.
In `@examples/nuxt/server/api/profile/`[identifier].get.ts:
- Around line 6-7: Replace the unsafe type assertion for
event.context.params?.identifier with a runtime string check: read the raw value
(e.g., const rawIdentifier = event.context.params?.identifier), verify typeof
rawIdentifier === "string" and only then assign to identifier; for non-string or
missing values return/throw a proper validation error (HTTP 400 or createError)
instead of proceeding. Ensure this change is applied where identifier is used
(the route handler that currently declares const identifier =
event.context.params?.identifier as string) so all downstream logic operates on
a validated string.
In `@examples/nuxt/server/api/search.get.ts`:
- Around line 7-10: The code casts getQuery(event).q to string and calls
q.trim(), which will throw if q is a string[] for repeated params; update the
narrowing logic around getQuery and q so you detect Array.isArray(query.q) and
handle it (e.g., pick the first element or return an error) before calling trim;
specifically change the extraction of q so it does not use a blind cast and
instead uses Array.isArray(query.q) to select a single string (or handle the
array case) and then call .trim() on that narrowed value.
In `@examples/nuxt/server/api/unfollow.post.ts`:
- Around line 23-41: The call to ctx.sendActivity in unfollow.post.ts can throw
on remote delivery and currently aborts the request, so wrap the
ctx.sendActivity(...) invocation (the block creating new Undo and nested new
Follow using ctx.getActorUri and target.id) in a try/catch; on error catch and
log the failure (include the error and target.id), but do not rethrow—ensure the
rest of the handler (local cleanup/redirect) always runs; optionally set a
boolean flag indicating remoteDeliveryFailed for conditional handling later.
In `@examples/nuxt/server/federation.ts`:
- Around line 97-107: The Undo(Follow) handler deletes a follower without
confirming the undone Follow actually targeted our local actor; update the
branch handling Undo/Follow (the async callback using undo.getObject and
activity instanceof Follow) to check that activity.object (or activity.target)
equals our local actor's id/URL (compare against the local actor identifier used
in this module) before calling relationStore.delete(undo.actorId.href) and
broadcastEvent(); only delete when undo.actorId is present and the Follow's
object matches the local actor.
In `@examples/nuxt/server/sse.ts`:
- Around line 16-20: In broadcastEvent(), a thrown error from one client.send
currently aborts fanout; wrap each client.send(data) in a per-client try/catch
inside the for (const client of clients) loop (reference: clients and
client.send) so a failing send is caught and handled (log the error via your
logger and optionally remove/close that client from clients) and continue
sending to remaining clients; ensure broadcastEvent still returns void even if
some sends fail.
🪄 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: 197ba781-ce5b-43ea-83be-9584e2ce6c88
⛔ Files ignored due to path filters (3)
examples/nuxt/public/demo-profile.pngis excluded by!**/*.pngexamples/nuxt/public/fedify-logo.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (25)
examples/nuxt/.gitignoreexamples/nuxt/README.mdexamples/nuxt/app.vueexamples/nuxt/nuxt.config.tsexamples/nuxt/package.jsonexamples/nuxt/pages/index.vueexamples/nuxt/pages/users/[identifier]/index.vueexamples/nuxt/pages/users/[identifier]/posts/[id].vueexamples/nuxt/public/style.cssexamples/nuxt/public/theme.jsexamples/nuxt/server/api/events.get.tsexamples/nuxt/server/api/follow.post.tsexamples/nuxt/server/api/home.get.tsexamples/nuxt/server/api/post.post.tsexamples/nuxt/server/api/posts/[identifier]/[id].get.tsexamples/nuxt/server/api/profile/[identifier].get.tsexamples/nuxt/server/api/search.get.tsexamples/nuxt/server/api/unfollow.post.tsexamples/nuxt/server/federation.tsexamples/nuxt/server/plugins/logging.tsexamples/nuxt/server/sse.tsexamples/nuxt/server/store.tsexamples/nuxt/tsconfig.jsonexamples/test-examples/mod.tspnpm-workspace.yaml
|
|
||
| <div class="fedify-badge"> | ||
| Powered by | ||
| <a href="https://fedify.dev" class="fedify-anchor" target="_blank"> |
There was a problem hiding this comment.
Harden external link opened with target="_blank".
Line 160 should include rel="noopener noreferrer" to prevent opener access from the new tab.
🛠️ Suggested fix
- <a href="https://fedify.dev" class="fedify-anchor" target="_blank">
+ <a href="https://fedify.dev" class="fedify-anchor" target="_blank" rel="noopener noreferrer">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <a href="https://fedify.dev" class="fedify-anchor" target="_blank"> | |
| <a href="https://fedify.dev" class="fedify-anchor" target="_blank" rel="noopener noreferrer"> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/pages/index.vue` at line 160, The external anchor element using
target="_blank" (the <a class="fedify-anchor" href="https://fedify.dev"
target="_blank"> in index.vue) must be hardened by adding rel="noopener
noreferrer"; update that anchor to include rel="noopener noreferrer" so the
opened tab cannot access window.opener and to avoid potential security/privacy
issues.
| onMounted(() => { | ||
| const eventSource = new EventSource("/api/events"); | ||
| eventSource.onmessage = () => { | ||
| refresh(); | ||
| }; | ||
| onBeforeUnmount(() => { | ||
| eventSource.close(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Vue 3 (Composition API), must onBeforeUnmount()be called synchronously duringsetup(), or is calling it inside an onMounted() callback supported?
💡 Result:
In Vue 3 Composition API, onBeforeUnmount must be called synchronously during the setup function (or synchronously from within setup). Calling it inside an onMounted callback is not supported and will not work. The official Vue.js documentation explicitly states: "All APIs listed on this page must be called synchronously during the setup phase of a component." This applies to all Composition API lifecycle hooks, including onBeforeUnmount. The general lifecycle documentation reinforces this: "This requires these hooks to be registered synchronously during component setup," with an example showing setTimeout( => { onMounted( => {}) }, 100) does not work, because onMounted executes asynchronously after setup completes. Since onMounted callbacks run after mounting (post-setup), any onBeforeUnmount call inside them occurs too late—the component instance context for registering hooks is no longer available.
Citations:
- 1: https://vuejs.org/api/composition-api-lifecycle
- 2: https://v3.vuejs.org/guide/composition-api-lifecycle-hooks.html
- 3: https://vuejs.org/api/composition-api-lifecycle.html
- 4: https://vuejs.org/guide/essentials/lifecycle.html
🏁 Script executed:
cat -n examples/nuxt/pages/index.vue | sed -n '200,220p'Repository: fedify-dev/fedify
Length of output: 543
Move onBeforeUnmount to setup scope—cannot register lifecycle hooks inside onMounted.
Calling onBeforeUnmount inside an onMounted callback (line 212) violates Vue 3 Composition API requirements. Per the official Vue documentation, all lifecycle hooks must be registered synchronously during the setup phase; hooks cannot be registered inside async callbacks. This will fail silently, leaving the EventSource cleanup never attached.
Fix
-onMounted(() => {
- const eventSource = new EventSource("/api/events");
- eventSource.onmessage = () => {
- refresh();
- };
- onBeforeUnmount(() => {
- eventSource.close();
- });
-});
+let eventSource: EventSource | null = null;
+
+onMounted(() => {
+ eventSource = new EventSource("/api/events");
+ eventSource.onmessage = () => {
+ refresh();
+ };
+});
+
+onBeforeUnmount(() => {
+ eventSource?.close();
+});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onMounted(() => { | |
| const eventSource = new EventSource("/api/events"); | |
| eventSource.onmessage = () => { | |
| refresh(); | |
| }; | |
| onBeforeUnmount(() => { | |
| eventSource.close(); | |
| }); | |
| }); | |
| let eventSource: EventSource | null = null; | |
| onMounted(() => { | |
| eventSource = new EventSource("/api/events"); | |
| eventSource.onmessage = () => { | |
| refresh(); | |
| }; | |
| }); | |
| onBeforeUnmount(() => { | |
| eventSource?.close(); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/pages/index.vue` around lines 207 - 215, The onBeforeUnmount
hook is being registered inside onMounted—move lifecycle registration into the
setup scope so hooks run synchronously; declare a let eventSource: EventSource |
null (or var) in the outer setup scope, create the EventSource inside onMounted
via onMounted(() => { eventSource = new EventSource("/api/events"); ... }), set
eventSource.onmessage to call refresh(), and register onBeforeUnmount in the
same setup scope to close eventSource if it exists (check for null) to ensure
proper cleanup.
| href="https://fedify.dev" | ||
| class="fedify-anchor" | ||
| target="_blank" | ||
| > |
There was a problem hiding this comment.
Add rel="noopener noreferrer" on external _blank links.
The external link opens a new tab without explicit opener protection.
♻️ Suggested fix
<a
href="https://fedify.dev"
class="fedify-anchor"
target="_blank"
+ rel="noopener noreferrer"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| href="https://fedify.dev" | |
| class="fedify-anchor" | |
| target="_blank" | |
| > | |
| href="https://fedify.dev" | |
| class="fedify-anchor" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| > |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/pages/users/`[identifier]/index.vue around lines 37 - 40, The
anchor element that sets target="_blank" (the fedify anchor in the
users/[identifier] page template) is missing rel protection; update that <a>
element used for the external link (class "fedify-anchor") to include
rel="noopener noreferrer" so the new tab cannot access window.opener and to
prevent potential security/performance issues.
| const identifier = route.params.identifier as string; | ||
| const id = route.params.id as string; | ||
|
|
||
| const { data } = await useFetch(`/api/posts/${identifier}/${id}`); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n examples/nuxt/pages/users/[identifier]/posts/[id].vueRepository: fedify-dev/fedify
Length of output: 2164
Add error handling to useFetch to distinguish API failures from missing posts.
Currently, both network/API errors and genuinely missing posts result in null data, so the UI shows "Post not found." for both cases. Destructure the error property from useFetch and handle it separately to provide appropriate feedback (e.g., "Failed to load post" vs. "Post not found.").
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/pages/users/`[identifier]/posts/[id].vue at line 44,
Destructure the response from useFetch to capture both data and error (e.g.,
const { data, error } = await useFetch(...)) in the page that currently calls
useFetch(`/api/posts/${identifier}/${id}`); then update the rendering logic to
check error first and show a "Failed to load post" message when error is truthy,
and only treat data === null/undefined as "Post not found." Ensure you reference
the existing identifier and id variables and adjust any loading states
accordingly so API/network failures and missing posts are handled distinctly.
| federation | ||
| .setFollowersDispatcher( | ||
| "/users/{identifier}/followers", | ||
| () => { | ||
| const followers = Array.from(relationStore.values()); | ||
| const items: Recipient[] = followers.map((f) => ({ | ||
| id: f.id, | ||
| inboxId: f.inboxId, | ||
| endpoints: f.endpoints, | ||
| })); | ||
| return { items }; | ||
| }, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify how setFollowersDispatcher passes route params and inspect current usages.
rg -n -C3 'setFollowersDispatcher\(' packages examplesRepository: fedify-dev/fedify
Length of output: 10080
🏁 Script executed:
# Check the CollectionDispatcher type definition
rg -n "type CollectionDispatcher" packagesRepository: fedify-dev/fedify
Length of output: 141
🏁 Script executed:
# Look at the actor and key-pair dispatcher implementations in the Nuxt example
cat -n examples/nuxt/server/federation.ts | head -145 | tail -50Repository: fedify-dev/fedify
Length of output: 1617
🏁 Script executed:
# Check how the middleware passes parameters to dispatchers
rg -n "followersDispatcher\(" packages/fedify/src/federation -A 5Repository: fedify-dev/fedify
Length of output: 43
🏁 Script executed:
# Get full CollectionDispatcher type definition
sed -n '122,160p' packages/fedify/src/federation/callback.tsRepository: fedify-dev/fedify
Length of output: 1395
🏁 Script executed:
# Find actor and key-pair dispatcher setups in Nuxt example
rg -n "setActorDispatcher|setKeyPairDispatcher" examples/nuxt/server/federation.ts -A 10Repository: fedify-dev/fedify
Length of output: 465
🏁 Script executed:
# Also check how identifier should be passed to followers dispatcher by looking at other examples that use it properly
rg -n "setFollowersDispatcher" examples -A 8 | grep -A 8 "identifier)"Repository: fedify-dev/fedify
Length of output: 43
🏁 Script executed:
# Verify how middleware invokes the followers dispatcher to see if parameters are actually passed
rg -n "followersDispatcher\?\." packages/fedify/src/federation/middleware.ts -B 3 -A 8Repository: fedify-dev/fedify
Length of output: 43
🏁 Script executed:
# Check if TypeScript would catch this parameter mismatch
grep -n "setFollowersDispatcher" examples/nuxt/server/federation.ts -B 2 -A 12Repository: fedify-dev/fedify
Length of output: 451
🏁 Script executed:
# Search for how the followers dispatcher is actually invoked in middleware
rg -n "followersDispatcher" packages/fedify/src/federation/middleware.ts -B 2 -A 5 | head -50Repository: fedify-dev/fedify
Length of output: 43
🏁 Script executed:
# Look at the federation builder to see type signature enforcement
rg -n "setFollowersDispatcher" packages/fedify/src/federation/builder.ts -B 2 -A 15Repository: fedify-dev/fedify
Length of output: 658
🏁 Script executed:
# Check if the Nuxt example has TypeScript compilation errors or if it somehow works
# Let's look at the context of the entire federation.ts file structure
head -20 examples/nuxt/server/federation.tsRepository: fedify-dev/fedify
Length of output: 429
🏁 Script executed:
# Check the TypeScript config for the Nuxt example
find examples/nuxt -name "tsconfig.json" -o -name "deno.json" | head -5Repository: fedify-dev/fedify
Length of output: 88
🏁 Script executed:
# Let's also check similar examples (sveltekit, astro) to compare their dispatcher signatures
rg -n "setFollowersDispatcher" examples/sveltekit-sample/src/lib/federation.ts -A 8Repository: fedify-dev/fedify
Length of output: 378
🏁 Script executed:
# Check the Nuxt tsconfig to see if strict type checking is enabled
cat examples/nuxt/tsconfig.jsonRepository: fedify-dev/fedify
Length of output: 101
🏁 Script executed:
# Look at the federation builder type more carefully - check if TContextData allows for loose typing
rg -n "class.*FederationBuilder" packages/fedify/src/federation/builder.ts -A 5Repository: fedify-dev/fedify
Length of output: 367
🏁 Script executed:
# Check if there's maybe function overloading or optional parameters
rg -n "setFollowersDispatcher\(" packages/fedify/src/federation/federation.ts -B 2 -A 20 | head -60Repository: fedify-dev/fedify
Length of output: 1057
🏁 Script executed:
# Search more carefully in middleware for how followers dispatcher is invoked
find packages/fedify/src -name "*.ts" -exec grep -l "followersDispatcher" {} \;Repository: fedify-dev/fedify
Length of output: 43
🏁 Script executed:
# Let's look at the federation.ts file which should have the interface
rg -n "interface FederationBuilder" packages/fedify/src/federation/federation.ts -A 500 | grep -A 25 "setFollowersDispatcher"Repository: fedify-dev/fedify
Length of output: 43
🏁 Script executed:
# Check if maybe there's a different signature or if the examples have type errors
# Let's check if the examples directory has a build or lint step
find examples/nuxt -name "package.json" -exec cat {} \;Repository: fedify-dev/fedify
Length of output: 708
🏁 Script executed:
# Let's verify the actual type error by checking if the examples can be typed-checked
# First, let's look for any type declarations or loose typing in the federation interface
rg -n "CollectionDispatcher<" packages/fedify/src/federation -B 2 -A 3Repository: fedify-dev/fedify
Length of output: 24054
🏁 Script executed:
# Check if there are any alternative overloads or if parameters are truly required
rg -n "dispatcher\?" packages/fedify/src/federation/builder.ts | head -20Repository: fedify-dev/fedify
Length of output: 111
🏁 Script executed:
# Let's examine the CLI example which does use parameters correctly
sed -n '207,215p' packages/cli/src/inbox.tsxRepository: fedify-dev/fedify
Length of output: 388
Fix followers dispatcher to validate the identifier parameter.
The dispatcher is missing all required parameters and ignores the {identifier} path variable, allowing /users/<anything>/followers to return demo followers for all identifiers. Add the context, identifier, and cursor parameters like the actor dispatcher, then validate that the identifier matches IDENTIFIER before returning followers.
Current code:
federation
.setFollowersDispatcher(
"/users/{identifier}/followers",
() => {
const followers = Array.from(relationStore.values());
const items: Recipient[] = followers.map((f) => ({
id: f.id,
inboxId: f.inboxId,
endpoints: f.endpoints,
}));
return { items };
},
);Compare with the actor dispatcher pattern in the same file at lines 29–39.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/nuxt/server/federation.ts` around lines 133 - 145, The followers
dispatcher registered via setFollowersDispatcher ignores the path variable and
required params; update its callback signature to accept (context, identifier,
cursor) like the actor dispatcher, validate that identifier === IDENTIFIER
before proceeding, and return an error or empty result when it does not match;
when valid, map relationStore values to Recipient objects (id, inboxId,
endpoints) and return { items }; reference setFollowersDispatcher, IDENTIFIER,
relationStore, and Recipient to locate the code to change.
Add Nuxt example application
Depends on #675.
Changes
New example:
examples/nuxt/A comprehensive Nuxt example app demonstrating
@fedify/nuxtintegration with ActivityPub federation, following the standard
Fedify example architecture.
Features
object dispatcher for Notes, followers collection, NodeInfo, and
key pair management via
server/federation.ts.lists, user search, and SSE-powered live updates (
pages/index.vue);actor profile page (
pages/users/[identifier]/index.vue); postdetail page (
pages/users/[identifier]/posts/[id].vue).server/api/forhome data, posting, follow/unfollow, search, profile lookup, post
detail, and SSE events.
and dark/light theme toggle script in
public/.@fedify/nuxtmodule wired withfederation module path, open host/vite config for tunnel
compatibility.
@fedify/nuxtbugfixaddTemplate()withaddServerTemplate()inpackages/nuxt/src/mod.tsto ensure the generated federationmiddleware module is available in the Nitro server bundle rather
than only in the client build output.
Test integration
examples/test-examples/mod.tswithpnpm build+pnpm startworkflow and 30-second ready timeout.Co-Authored-By: Claude Opus 4.6