Conversation
📝 WalkthroughWalkthroughThis PR adds Changes
Sequence DiagramsequenceDiagram
actor Client
participant Nuxt as Nuxt Server<br/>(H3/Nitro)
participant Fedify as Fedify Middleware<br/>(`@fedify/nuxt`)
participant Federation as Federation Instance<br/>(ActivityPub)
participant Framework as App Routes<br/>(Pages/API)
Client->>Nuxt: HTTP Request
Nuxt->>Fedify: Convert to Web Request<br/>Pass through middleware
alt Federation Route
Fedify->>Federation: Delegate to federation.fetch()
Federation->>Fedify: Response (200 ActivityPub)
Fedify->>Nuxt: "handled" with Response
Nuxt->>Client: Return Response
else Non-Federation Route (404 from Nuxt)
Fedify->>Framework: onNotFound callback
Framework-->>Fedify: Not found
Fedify->>Nuxt: "not-found" result
Nuxt->>Framework: Continue routing
Framework->>Client: 404 or matched route
else Non-Federation Route<br/>(Accept Header Incompatible)
Fedify->>Federation: Check federation route
Federation-->>Fedify: Fedify route exists
Fedify->>Framework: onNotAcceptable callback
Framework->>Nuxt: Framework also returns 404
Fedify->>Nuxt: "not-acceptable"<br/>(deferred)
Nuxt->>Nuxt: beforeResponse hook
Nuxt->>Client: 406 Not Acceptable
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
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.
| import { defineEventHandler } from "h3"; | ||
| import federation from "../federation"; | ||
|
|
||
| export default defineEventHandler(async (event) => { | ||
| // Construct the full URL from headers | ||
| const proto = event.headers.get("x-forwarded-proto") || "http"; | ||
| const host = event.headers.get("host") || "localhost"; | ||
| const url = new URL(event.node.req.url || "", `${proto}://${host}`); | ||
|
|
||
| const request = new Request(url, { | ||
| method: event.node.req.method, | ||
| headers: event.node.req.headers as Record<string, string>, | ||
| body: ["GET", "HEAD", "DELETE"].includes(event.node.req.method) | ||
| ? undefined | ||
| : undefined, | ||
| }); | ||
|
|
||
| const response = await federation.fetch(request, { | ||
| contextData: undefined, | ||
| }); | ||
|
|
||
| if (response.status === 404) return; // Let Nuxt handle 404 | ||
| return response; | ||
| }); |
There was a problem hiding this comment.
The manual construction of the Request object has a bug where the body is always undefined (lines 13-15), which will break incoming ActivityPub activities (POST requests). Additionally, manual URL reconstruction is error-prone. It is highly recommended to use h3's toWebRequest(event) utility, which correctly handles headers, methods, and bodies across different runtimes and ensures the host/port are preserved correctly.
import { defineEventHandler, toWebRequest } from "h3";
import federation from "../federation";
export default defineEventHandler(async (event) => {
const request = toWebRequest(event);
const response = await federation.fetch(request, {
contextData: undefined,
});
if (response.status === 404) return; // Let Nuxt handle 404
return response;
});
References
- When reconstructing a URL from a request object, prefer using methods that preserve the host and port (like the Host header) to ensure functionality like federation signature verification remains intact.
| devDependencies: { | ||
| ...defaultDevDependencies, | ||
| "typescript": deps["npm:typescript"], | ||
| "@types/node": deps["npm:@types/node@25"], |
There was a problem hiding this comment.
The key npm:@types/node@25 appears to be a typo and is likely missing from packages/init/src/json/deps.json. Node.js version 25 is not yet released. This should probably refer to a current version like @types/node@22 or simply @types/node as defined in the dependencies catalog.
"@types/node": deps["npm:@types/node"],| @@ -0,0 +1,5 @@ | |||
| // https://nuxt.com/docs/api/configuration/nuxt-config | |||
| export default defineNuxtConfig({ | |||
| ssr: false, | |||
There was a problem hiding this comment.
Setting ssr: false (Single Page Application mode) is generally inappropriate for federated applications. ActivityPub requires the server to respond with JSON-LD to actors, often on the same routes that serve HTML to browsers. Disabling SSR can complicate content negotiation and discovery. It is better to default to ssr: true, as seen in the example application.
ssr: true,
| "nuxt.config.ts": await readTemplate("nuxt/nuxt.config.ts"), | ||
| "server/federation.ts": await readTemplate("nuxt/server/federation.ts"), | ||
| "server/logging.ts": await readTemplate("nuxt/server/logging.ts"), | ||
| "server/middleware/federation.ts": await readTemplate( | ||
| "nuxt/server/middleware/federation.ts", | ||
| ), |
There was a problem hiding this comment.
The fedify init setup for Nuxt is currently inconsistent. It adds @fedify/nuxt to the dependencies but then manually sets up a Nitro middleware in server/middleware/federation.ts. It would be much cleaner and more idiomatic to enable the @fedify/nuxt module in the nuxt.config.ts template and remove the manual middleware file. This avoids redundancy and leverages the module's built-in features like deferred 406 handling.
Codecov Report❌ Patch coverage is
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
- Use @fedify/fixture instead of node:test in mod.test.ts - Type event parameter as H3Event instead of unknown in middleware.ts Co-Authored-By: Claude (claude-opus-4-20250514)
Move NOT_ACCEPTABLE_BODY and DEFERRED_NOT_ACCEPTABLE_CONTEXT_KEY into a shared lib.ts to avoid string duplication across logic.ts and plugin.ts. Co-Authored-By: Claude (claude-opus-4-20250514)
…chema` to `@fedify/nuxt`
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="https://github.com/"> 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
| - *packages/mysql/*: MySQL/MariaDB drivers (@fedify/mysql) for Fedify. | ||
| - *packages/nestjs/*: NestJS integration (@fedify/nestjs) for Fedify. | ||
| - *packages/next/*: Next.js integration (@fedify/next) for Fedify. | ||
| - *packages/nuxt/*: Nuxt integration (@fedify/nuxt) for Fedify. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Optional wording polish for repeated phrasing.
Line 395 is correct, but a small rephrase can reduce repetitive wording in adjacent bullets.
Suggested edit
- - *packages/nuxt/*: Nuxt integration (`@fedify/nuxt`) for Fedify.
+ - *packages/nuxt/*: Nuxt integration package (`@fedify/nuxt`).📝 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.
| - *packages/nuxt/*: Nuxt integration (@fedify/nuxt) for Fedify. | |
| - *packages/nuxt/*: Nuxt integration package (`@fedify/nuxt`). |
🧰 Tools
🪛 LanguageTool
[style] ~395-~395: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...gration (@fedify/next) for Fedify. - packages/nuxt/: Nuxt integration (@fedify/nuxt)...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CONTRIBUTING.md` at 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.
| { 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="https://github.com/"> 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="https://github.com/">← 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="https://github.com/"> 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.
| function* getInitCommand(pm: PackageManager) { | ||
| yield* getNuxtInitCommand(pm); | ||
| yield* [ | ||
| "init", | ||
| ".", | ||
| "--template", | ||
| "minimal", | ||
| "--no-install", | ||
| "--force", | ||
| "--packageManager", | ||
| pm, | ||
| "--no-gitInit", | ||
| "--no-modules", | ||
| "&&", | ||
| "rm", | ||
| "nuxt.config.ts", | ||
| ]; |
There was a problem hiding this comment.
Keep cleanup out of the command argv.
Lines 51-54 append shell tokens to a string[] command. If the init runner executes argv directly, nuxi receives && rm nuxt.config.ts as arguments and the cleanup never runs; if it goes through a shell, rm is still POSIX-only. Move the nuxt.config.ts deletion into the init pipeline itself, or rely on the generated files entry to overwrite it.
🛠️ Minimal fix in this segment
function* getInitCommand(pm: PackageManager) {
yield* getNuxtInitCommand(pm);
yield* [
"init",
".",
"--template",
"minimal",
"--no-install",
"--force",
"--packageManager",
pm,
"--no-gitInit",
"--no-modules",
- "&&",
- "rm",
- "nuxt.config.ts",
];
}🤖 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 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.
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