Skip to content

Add Nuxt example application#676

Open
2chanhaeng wants to merge 5 commits intofedify-dev:mainfrom
2chanhaeng:pr/example-nuxt
Open

Add Nuxt example application#676
2chanhaeng wants to merge 5 commits intofedify-dev:mainfrom
2chanhaeng:pr/example-nuxt

Conversation

@2chanhaeng
Copy link
Copy Markdown
Contributor

Add Nuxt example application

Depends on #675.

Changes

New example: examples/nuxt/

A comprehensive Nuxt example app demonstrating @fedify/nuxt
integration with ActivityPub federation, following the standard
Fedify example architecture.

Features

  • Federation: Actor dispatcher, inbox listeners (Follow, Undo),
    object dispatcher for Notes, followers collection, NodeInfo, and
    key pair management via server/federation.ts.
  • Vue pages: Home page with post creation, follower/following
    lists, user search, and SSE-powered live updates (pages/index.vue);
    actor profile page (pages/users/[identifier]/index.vue); post
    detail page (pages/users/[identifier]/posts/[id].vue).
  • Server API routes: RESTful endpoints under server/api/ for
    home data, posting, follow/unfollow, search, profile lookup, post
    detail, and SSE events.
  • Static assets: Fedify logo, demo profile image, CSS stylesheet,
    and dark/light theme toggle script in public/.
  • Nuxt config: SSR enabled, @fedify/nuxt module wired with
    federation module path, open host/vite config for tunnel
    compatibility.

@fedify/nuxt bugfix

  • Replaced addTemplate() with addServerTemplate() in
    packages/nuxt/src/mod.ts to ensure the generated federation
    middleware module is available in the Nitro server bundle rather
    than only in the client build output.

Test integration

  • Added Nuxt example to examples/test-examples/mod.ts with
    pnpm build + pnpm start workflow and 30-second ready timeout.

Co-Authored-By: Claude Opus 4.6

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

Adds a complete Nuxt example app demonstrating Fedify ActivityPub federation: project config, frontend pages, public assets, server-side federation module, in-memory stores, SSE broadcasting, API endpoints for search/follow/unfollow/post, and a Nitro logging plugin.

Changes

Cohort / File(s) Summary
Project config
examples/nuxt/package.json, examples/nuxt/tsconfig.json, examples/nuxt/nuxt.config.ts, examples/nuxt/.gitignore, pnpm-workspace.yaml
Adds example project manifest, TypeScript config, Nuxt config enabling @fedify/nuxt, gitignore, and registers examples/nuxt in workspace.
Frontend pages & app
examples/nuxt/app.vue, examples/nuxt/pages/index.vue, examples/nuxt/pages/users/[identifier]/index.vue, examples/nuxt/pages/users/[identifier]/posts/[id].vue, examples/nuxt/README.md
Implements main app shell and pages: home feed (search, compose, follow/unfollow, SSE), profile view, and post detail; includes README for running and federation usage.
Public assets
examples/nuxt/public/style.css, examples/nuxt/public/theme.js
Adds global stylesheet with light/dark theming and client-side theme script.
Server federation & stores
examples/nuxt/server/federation.ts, examples/nuxt/server/store.ts
Creates in-process Fedify federation instance (hardcoded demo actor), keypair management, inbox listeners (Follow/Undo), Note object dispatch, followers collection, nodeinfo, and in-memory stores with PostStore class.
Server API endpoints
examples/nuxt/server/api/home.get.ts, .../profile/[identifier].get.ts, .../posts/[identifier]/[id].get.ts, .../search.get.ts, .../follow.post.ts, .../unfollow.post.ts, .../post.post.ts, .../events.get.ts
Adds REST/Nitro route handlers for home feed, profile, post retrieval, search, follow/unfollow, creating posts, and Server-Sent Events streaming.
SSE & plugins
examples/nuxt/server/sse.ts, examples/nuxt/server/plugins/logging.ts
Introduces SSE client registry and broadcast helper; adds Nitro logging plugin configured with logtape and AsyncLocalStorage.
Example registration & small tweaks
examples/test-examples/mod.ts, .agents/.../public/style.css, .agents/.../public/theme.js, packages/nuxt/package.json
Registers the Nuxt example in test runner; minor formatting/theme script adjustments in agent examples; updates Nuxt peer deps and moves @nuxt/schema to peerDependencies.

Sequence Diagram

sequenceDiagram
    actor User
    participant Client as Browser
    participant API as Nuxt API
    participant Federation as Fedify Context
    participant Store as In-memory Stores
    participant SSE as SSE Broadcaster

    User->>Client: search input / follow / submit post
    Client->>API: GET /api/search?q=...
    API->>Federation: ctx.lookupObject(q)
    Federation-->>API: Person/NotFound
    API->>Store: check followingStore
    API-->>Client: search result

    Client->>API: POST /api/follow (uri)
    API->>Federation: ctx.lookupObject(uri)
    API->>Federation: send Follow activity
    API->>Store: add to followingStore
    API->>SSE: broadcastEvent()
    SSE-->>Client: SSE update

    Client->>API: POST /api/post (content)
    API->>Store: postStore.append(Note)
    API->>Federation: fetch persisted Note & send Create to followers
    API->>SSE: broadcastEvent()
    SSE-->>Client: SSE update
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested labels

type/feature

Suggested reviewers

  • dahlia
  • sij411
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and directly describes the main change: adding a Nuxt example application, which is the primary focus of the entire PR.
Description check ✅ Passed The description is comprehensive and well-organized, covering all major aspects of the changeset including new example features, bugfixes, and test integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@issues-auto-labeler issues-auto-labeler bot added component/build Build system and packaging component/federation Federation object related component/integration Web framework integration component/testing Testing utilities (@fedify/testing) labels Apr 13, 2026
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces 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.

Comment thread packages/init/src/templates/nuxt/server/middleware/federation.ts.tpl Outdated
Comment thread packages/init/src/webframeworks/nuxt.ts
Comment thread packages/init/src/templates/nuxt/nuxt.config.ts.tpl Outdated
Comment thread packages/init/src/webframeworks/nuxt.ts Outdated
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

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

@2chanhaeng
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between fe50936 and 4cc14ed.

⛔ Files ignored due to path filters (4)
  • deno.lock is excluded by !**/*.lock
  • examples/nuxt/public/demo-profile.png is excluded by !**/*.png
  • examples/nuxt/public/fedify-logo.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is 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.toml
  • AGENTS.md
  • CHANGES.md
  • CONTRIBUTING.md
  • cspell.json
  • deno.json
  • docs/manual/integration.md
  • examples/nuxt/.gitignore
  • examples/nuxt/README.md
  • examples/nuxt/app.vue
  • examples/nuxt/nuxt.config.ts
  • examples/nuxt/package.json
  • examples/nuxt/pages/index.vue
  • examples/nuxt/pages/users/[identifier]/index.vue
  • examples/nuxt/pages/users/[identifier]/posts/[id].vue
  • examples/nuxt/public/style.css
  • examples/nuxt/public/theme.js
  • examples/nuxt/server/api/events.get.ts
  • examples/nuxt/server/api/follow.post.ts
  • examples/nuxt/server/api/home.get.ts
  • examples/nuxt/server/api/post.post.ts
  • examples/nuxt/server/api/posts/[identifier]/[id].get.ts
  • examples/nuxt/server/api/profile/[identifier].get.ts
  • examples/nuxt/server/api/search.get.ts
  • examples/nuxt/server/api/unfollow.post.ts
  • examples/nuxt/server/federation.ts
  • examples/nuxt/server/plugins/logging.ts
  • examples/nuxt/server/sse.ts
  • examples/nuxt/server/store.ts
  • examples/nuxt/tsconfig.json
  • examples/test-examples/mod.ts
  • mise.toml
  • packages/fedify/README.md
  • packages/init/src/const.ts
  • packages/init/src/json/deps.json
  • packages/init/src/templates/nuxt/nuxt.config.ts.tpl
  • packages/init/src/test/lookup.ts
  • packages/init/src/test/port.ts
  • packages/init/src/webframeworks/mod.ts
  • packages/init/src/webframeworks/nuxt.ts
  • packages/nuxt/README.md
  • packages/nuxt/deno.json
  • packages/nuxt/package.json
  • packages/nuxt/src/mod.test.ts
  • packages/nuxt/src/mod.ts
  • packages/nuxt/src/module.ts
  • packages/nuxt/src/runtime/server/lib.ts
  • packages/nuxt/src/runtime/server/logic.ts
  • packages/nuxt/src/runtime/server/middleware.ts
  • packages/nuxt/src/runtime/server/plugin.ts
  • packages/nuxt/tsdown.config.ts
  • pnpm-workspace.yaml

Comment thread CONTRIBUTING.md
Comment thread examples/nuxt/app.vue
{ rel: "stylesheet", href: "/style.css" },
{ rel: "icon", type: "image/svg+xml", href: "/fedify-logo.svg" },
],
script: [{ src: "/theme.js" }],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.js

Repository: 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.

Suggested change
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.

Comment on lines +183 to +194
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
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="/">&larr; Back to home</a>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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="/">&larr; Back to home</a>
+    <NuxtLink class="back-link" to="/">&larr; 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.

Suggested change
<a class="back-link" href="/">&larr; Back to home</a>
<NuxtLink class="back-link" to="/">&larr; 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.

Comment on lines +32 to +34
} catch {
// lookup failed
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
} 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.

Comment on lines +23 to +41
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,
}),
}),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
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.

Comment thread examples/nuxt/server/federation.ts Outdated
Comment on lines +16 to +20
export function broadcastEvent(): void {
const data = JSON.stringify({ type: "update" });
for (const client of clients) {
client.send(data);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment thread packages/init/src/webframeworks/nuxt.ts
@2chanhaeng
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

♻️ Duplicate comments (6)
examples/nuxt/app.vue (1)

11-11: ⚠️ Potential issue | 🔴 Critical

Defer /theme.js to prevent early document.body access.

Line 11 injects a non-deferred head script, while theme.js touches document.body immediately. 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 | 🟡 Minor

Fix 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 | 🟠 Major

Use a per-post Create activity id.

new URL("#activity", attribution) gives every Create the 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 | 🟠 Major

Ignore stale search responses.

The debounce reduces request count, but it does not serialize responses. If an older /api/search call resolves after a newer one, searchResult is 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 | 🟠 Major

Keep cleanup out of the command argv.

command is a raw argument vector, so && rm nuxt.config.ts will be passed to nuxi literally. The cleanup never runs, and rm is POSIX-only anyway; delete the file in a separate init step or just let files["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 | 🟠 Major

Validate the undone Follow target before deleting a follower.

Any Undo(Follow(...)) with an undo.actorId currently removes the stored follower, even when the original Follow targeted 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4cc14ed and 8ef4fc9.

⛔ Files ignored due to path filters (3)
  • examples/nuxt/public/demo-profile.png is excluded by !**/*.png
  • examples/nuxt/public/fedify-logo.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is 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.md
  • CHANGES.md
  • docs/manual/integration.md
  • examples/nuxt/.gitignore
  • examples/nuxt/README.md
  • examples/nuxt/app.vue
  • examples/nuxt/nuxt.config.ts
  • examples/nuxt/package.json
  • examples/nuxt/pages/index.vue
  • examples/nuxt/pages/users/[identifier]/index.vue
  • examples/nuxt/pages/users/[identifier]/posts/[id].vue
  • examples/nuxt/public/style.css
  • examples/nuxt/public/theme.js
  • examples/nuxt/server/api/events.get.ts
  • examples/nuxt/server/api/follow.post.ts
  • examples/nuxt/server/api/home.get.ts
  • examples/nuxt/server/api/post.post.ts
  • examples/nuxt/server/api/posts/[identifier]/[id].get.ts
  • examples/nuxt/server/api/profile/[identifier].get.ts
  • examples/nuxt/server/api/search.get.ts
  • examples/nuxt/server/api/unfollow.post.ts
  • examples/nuxt/server/federation.ts
  • examples/nuxt/server/plugins/logging.ts
  • examples/nuxt/server/sse.ts
  • examples/nuxt/server/store.ts
  • examples/nuxt/tsconfig.json
  • examples/test-examples/mod.ts
  • packages/init/src/const.ts
  • packages/init/src/json/deps.json
  • packages/init/src/templates/nuxt/nuxt.config.ts.tpl
  • packages/init/src/test/lookup.ts
  • packages/init/src/test/port.ts
  • packages/init/src/webframeworks/mod.ts
  • packages/init/src/webframeworks/nuxt.ts
  • packages/nuxt/README.md
  • packages/nuxt/package.json
  • packages/nuxt/src/module.test.ts
  • packages/nuxt/src/module.ts
  • packages/nuxt/src/runtime/server/logic.test.ts
  • packages/nuxt/src/runtime/server/logic.ts
  • packages/nuxt/src/runtime/server/plugin.test.ts
  • packages/nuxt/src/runtime/server/plugin.ts
  • pnpm-workspace.yaml

Comment thread .agents/skills/create-integration-package/SKILL.md
Comment thread .agents/skills/create-integration-package/SKILL.md
Comment thread examples/nuxt/public/style.css
Comment thread examples/nuxt/README.md
Comment thread examples/nuxt/server/api/follow.post.ts
Comment thread examples/nuxt/server/api/post.post.ts Outdated
Comment on lines +3 to +4
import federation from "../federation";
import { postStore } from "../store";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment thread examples/nuxt/server/api/post.post.ts
Comment on lines +7 to +10
const query = getQuery(event);
const q = query.q as string | undefined;
if (!q || !q.trim()) {
return { result: null };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 Script executed:

cat -n examples/nuxt/server/api/search.get.ts

Repository: 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.

Suggested change
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.

Comment on lines +6 to +31
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");
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
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.

@2chanhaeng 2chanhaeng force-pushed the pr/example-nuxt branch 2 times, most recently from b9a066b to 1c2ef9e Compare April 16, 2026 17:00
@2chanhaeng 2chanhaeng marked this pull request as ready for review April 16, 2026 17:01
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (18)
examples/nuxt/README.md (2)

10-10: ⚠️ Potential issue | 🟡 Minor

Tighten 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 | 🟡 Minor

Move 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 | 🔴 Critical

Defer /theme.js to prevent early document.body access failure.

Loading this script from <head> without deferral can execute before <body> exists, causing a runtime crash in theme.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 | 🟡 Minor

Use a single header strategy for SSE responses.

setResponseHeader(...) and new 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 | 🟡 Minor

Harden stream lifecycle on disconnect/send races.

controller.enqueue(...) can throw after disconnect, and the close listener 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 | 🟠 Major

Isolate per-client SSE send failures in fanout.

One throwing client.send(...) currently aborts broadcastEvent() 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 | 🔵 Trivial

Use <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="/">&larr; Back to home</a>
+    <NuxtLink class="back-link" to="/">&larr; Back to home</NuxtLink>
...
-    <a class="back-link" href="/">&larr; Back to home</a>
+    <NuxtLink class="back-link" to="/">&larr; 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 | 🟠 Major

Guard sendActivity to 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 | 🟡 Minor

Handle useFetch error state distinctly from "not found".

Right now null data 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 | 🟡 Minor

Validate identifier type instead of asserting with as string.

event.context.params?.identifier should 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 | 🟠 Major

Make the Create activity 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 | 🟠 Major

Persist published on the created Note.

/api/home and /api/posts/[identifier]/[id] both read published, 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 | 🟠 Major

Broadcast an SSE update after a successful post.

This handler mutates postStore, but unlike the follow/unfollow routes it never notifies /api/events subscribers. 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 | 🟠 Major

Soft-fail remote lookup/delivery and keep the 303 fallback.

If ctx.lookupObject() or ctx.sendActivity() rejects, this form submission becomes a 500. The handler should keep the existing redirect behavior and only update followingStore/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 | 🟠 Major

Narrow query.q before calling .trim().

getQuery() can return string[] for repeated query params. The cast on Line 8 hides that, and q.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 | 🟡 Minor

Increase the badge contrast.

White text on #7dd3fc is 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 | 🟠 Major

Validate the undone Follow targets 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 | 🟠 Major

Prevent stale debounced search responses from overwriting newer results.

Line 193 can apply out-of-order /api/search responses, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8ef4fc9 and 1c2ef9e.

⛔ Files ignored due to path filters (3)
  • examples/nuxt/public/demo-profile.png is excluded by !**/*.png
  • examples/nuxt/public/fedify-logo.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • examples/nuxt/.gitignore
  • examples/nuxt/README.md
  • examples/nuxt/app.vue
  • examples/nuxt/nuxt.config.ts
  • examples/nuxt/package.json
  • examples/nuxt/pages/index.vue
  • examples/nuxt/pages/users/[identifier]/index.vue
  • examples/nuxt/pages/users/[identifier]/posts/[id].vue
  • examples/nuxt/public/style.css
  • examples/nuxt/public/theme.js
  • examples/nuxt/server/api/events.get.ts
  • examples/nuxt/server/api/follow.post.ts
  • examples/nuxt/server/api/home.get.ts
  • examples/nuxt/server/api/post.post.ts
  • examples/nuxt/server/api/posts/[identifier]/[id].get.ts
  • examples/nuxt/server/api/profile/[identifier].get.ts
  • examples/nuxt/server/api/search.get.ts
  • examples/nuxt/server/api/unfollow.post.ts
  • examples/nuxt/server/federation.ts
  • examples/nuxt/server/plugins/logging.ts
  • examples/nuxt/server/sse.ts
  • examples/nuxt/server/store.ts
  • examples/nuxt/tsconfig.json
  • examples/test-examples/mod.ts
  • pnpm-workspace.yaml


<div class="fedify-badge">
Powered by
<a href="https://fedify.dev" class="fedify-anchor" target="_blank">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

Comment on lines +207 to +215
onMounted(() => {
const eventSource = new EventSource("/api/events");
eventSource.onmessage = () => {
refresh();
};
onBeforeUnmount(() => {
eventSource.close();
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 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.

Suggested change
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.

Comment on lines +37 to +40
href="https://fedify.dev"
class="fedify-anchor"
target="_blank"
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n examples/nuxt/pages/users/[identifier]/posts/[id].vue

Repository: 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.

Comment on lines +133 to +145
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 };
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how setFollowersDispatcher passes route params and inspect current usages.
rg -n -C3 'setFollowersDispatcher\(' packages examples

Repository: fedify-dev/fedify

Length of output: 10080


🏁 Script executed:

# Check the CollectionDispatcher type definition
rg -n "type CollectionDispatcher" packages

Repository: 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 -50

Repository: 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 5

Repository: fedify-dev/fedify

Length of output: 43


🏁 Script executed:

# Get full CollectionDispatcher type definition
sed -n '122,160p' packages/fedify/src/federation/callback.ts

Repository: 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 10

Repository: 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 8

Repository: 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 12

Repository: 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 -50

Repository: 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 15

Repository: 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.ts

Repository: 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 -5

Repository: 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 8

Repository: 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.json

Repository: 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 5

Repository: 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 -60

Repository: 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 3

Repository: 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 -20

Repository: 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.tsx

Repository: 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pnpm-workspace.yaml (1)

34-45: 🧹 Nitpick | 🔵 Trivial

Nit: examples/nuxt placement breaks alphabetical order.

The examples/* block is roughly alphabetical (astro, cloudflare-workers, elysia, express, koa, next-integration, then nuxt, fastify, next14-…, …). The existing list is already slightly out of order, but inserting nuxt between next-integration and fastify makes it worse. Consider moving - examples/nuxt below - examples/next15-app-router to keep entries sorted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pnpm-workspace.yaml` around lines 34 - 45, The list entry "- examples/nuxt"
in pnpm-workspace.yaml is out of alphabetical order; move the "- examples/nuxt"
line so it appears after "- examples/next15-app-router" (i.e., place the nuxt
entry below next15-app-router and above "- examples/solidstart") to restore
proper alphabetical ordering within the examples/* block.
♻️ Duplicate comments (17)
examples/nuxt/server/api/events.get.ts (1)

1-7: 🧹 Nitpick | 🔵 Trivial

Use one path for SSE headers.

The h3 setResponseHeader() calls duplicate the headers already supplied to the returned raw Response; keep the Response headers and remove the redundant import/calls.

♻️ Proposed cleanup
-import { setResponseHeader } from "h3";
 import { addClient, removeClient } from "../sse";
 
 export default defineEventHandler((event) => {
-  setResponseHeader(event, "Content-Type", "text/event-stream");
-  setResponseHeader(event, "Cache-Control", "no-cache");
-  setResponseHeader(event, "Connection", "keep-alive");
-
   const stream = new ReadableStream({

Also applies to: 29-34

🤖 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 1 - 7, The SSE handler
currently calls h3.setResponseHeader multiple times which duplicates headers
already provided on the returned raw Response; remove the import and all calls
to setResponseHeader in the handler (references: setResponseHeader,
defineEventHandler, addClient, removeClient) and rely solely on the Response
object’s headers when constructing/returning the SSE Response so no duplicate
header setting remains.
examples/nuxt/app.vue (1)

11-11: ⚠️ Potential issue | 🔴 Critical

Defer /theme.js so document.body exists.

Line 11 injects a head script, while /theme.js touches document.body at top level. Add defer to avoid a startup crash during head parsing.

🐛 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 injected head script for "/theme.js"
runs too early and accesses document.body during head parsing; update the script
entry (the script: [...] object that currently has src: "/theme.js") to include
the defer attribute so the browser defers execution until after parsing (e.g.,
add defer: true on the object with src "/theme.js") to prevent startup crashes.
examples/nuxt/README.md (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Fix the awkward final clause.

Line 10 reads unnaturally as “the Fedify and [Nuxt]”; keep both product names consistently linked.

✏️ Proposed fix
-implementations using the Fedify and [Nuxt].
+implementations 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, The sentence "implementations using the
Fedify and [Nuxt]." reads awkwardly; edit it to consistently reference both
products by name and link style—e.g., change to "implementations using [Fedify]
and [Nuxt]." or "implementations using Fedify and Nuxt." ensuring both names are
linked the same way and remove the extra "the" before Fedify.
examples/nuxt/server/sse.ts (1)

16-20: ⚠️ Potential issue | 🟠 Major

Isolate SSE fanout failures per client.

Line 19 can throw for a disconnected stream; one bad client should not abort the whole broadcast or bubble into federation/API handlers.

🛡️ Proposed 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, The broadcastEvent
function currently iterates over clients and calls client.send(data) which can
throw for a disconnected stream; change it to isolate failures by wrapping each
client.send(data) in a try/catch so one bad client doesn't abort the loop, and
on error log (or handle) the failure and remove/close that specific client from
the clients collection to avoid repeated failures; reference broadcastEvent,
clients, and client.send when making this change.
examples/nuxt/server/api/search.get.ts (1)

7-10: ⚠️ Potential issue | 🟠 Major

Narrow query.q before calling .trim().

The cast hides non-string query values; repeated ?q=a&q=b can make q an array and throw at runtime on Line 9.

h3 getQuery repeated query parameter string array Nuxt documentation
🐛 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 };
   }

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 narrows
query.q unsafely by casting to string|undefined and then calling .trim(), which
will throw if query.q is an array from repeated params; in the handler using
getQuery(event) and variable q, first inspect the raw value (e.g., query.q) and
explicitly narrow it: if Array.isArray(query.q) take the first element (or treat
as invalid), else ensure typeof query.q === 'string' before assigning to q and
calling q.trim(); update the early-return logic to handle non-string/empty cases
accordingly so .trim() is only ever called on a guaranteed string.
examples/nuxt/server/api/follow.post.ts (1)

18-41: ⚠️ Potential issue | 🟠 Major

Guard remote lookup and delivery failures before mutating local state.

ctx.lookupObject() or ctx.sendActivity() can reject; currently that bubbles into a 500 instead of the existing 303 fallback.

🛡️ 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);
+    }
+  } catch (error) {
+    console.debug("Failed to follow target:", error);
+    return sendRedirect(event, "/", 303);
   }
   broadcastEvent();
🤖 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, The code
currently calls ctx.lookupObject() and ctx.sendActivity() without guarding for
rejections, which can cause a 500 and still mutate local state; wrap the remote
calls in a try/catch and only perform followingStore.set(target.id.href) and
broadcastEvent() after sendActivity resolves successfully. Specifically, call
ctx.lookupObject(identifier) and if it returns null keep the existing
sendRedirect(event, "/", 303) path; then call ctx.sendActivity(...) inside a try
block and on success do the Person check, followingStore.set and broadcastEvent;
on any error from lookup or sendActivity catch it and return sendRedirect(event,
"/", 303) so local state is never mutated on remote failures.
examples/nuxt/pages/users/[identifier]/posts/[id].vue (2)

44-49: ⚠️ Potential issue | 🟡 Minor

Handle useFetch failures separately from missing posts.

data being empty can mean either “not found” or an API/network failure; keep error separate so the UI/title do not misreport failures as missing posts.

Nuxt useFetch return data error properties and error handling documentation
🛠️ Proposed change
-const { data } = await useFetch(`/api/posts/${identifier}/${id}`);
+const { data, error } = await useFetch(`/api/posts/${identifier}/${id}`);
 
 useHead({
-  title: data.value
+  title: error.value
+    ? "Error - Fedify Nuxt Example"
+    : data.value
     ? `Post - ${data.value.author.name} - Fedify Nuxt Example`
     : "Not Found - Fedify Nuxt Example",
 });
🤖 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 around lines 44 - 49,
The title logic incorrectly treats any empty data as "Not Found" even when
useFetch failed; update the fetch handling around the useFetch call (const {
data, error } = await useFetch(`/api/posts/${identifier}/${id}`)) and change the
useHead logic to distinguish error vs missing post: if error is present set a
failure title (e.g., "Error - Fedify Nuxt Example"), otherwise if no error and
data.value is null/undefined set "Not Found - Fedify Nuxt Example", otherwise
use `Post - ${data.value.author.name} - Fedify Nuxt Example`; ensure you
reference useFetch, data, error, useHead, identifier and id when making the
change.

3-3: 🧹 Nitpick | 🔵 Trivial

Prefer Nuxt client-side navigation for internal links.

These internal home links still use plain anchors, causing full page reloads instead of Nuxt SPA navigation.

♻️ Proposed change
-    <a class="back-link" href="/">&larr; Back to home</a>
+    <NuxtLink class="back-link" to="/">&larr; Back to home</NuxtLink>
...
-    <a class="back-link" href="/">&larr; Back to home</a>
+    <NuxtLink class="back-link" to="/">&larr; 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 the
plain anchor used for internal navigation with Nuxt's client-side link component
to enable SPA navigation: find the HTML anchor element with class "back-link"
(and the other identical occurrence noted) and swap it to use NuxtLink (or
<router-link> if your project uses Vue Router directly) targeting the root path,
preserving the class and link text so clicks use Nuxt's client-side navigation
instead of causing a full page reload.
examples/nuxt/pages/users/[identifier]/index.vue (2)

64-69: ⚠️ Potential issue | 🟡 Minor

Handle profile fetch errors separately from “not found.”

A failed request and a missing profile both currently fall through to the same v-else path.

Nuxt useFetch return data error properties and error handling documentation
🛠️ Proposed change
-const { data } = await useFetch(`/api/profile/${identifier}`);
+const { data, error } = await useFetch(`/api/profile/${identifier}`);
 
 useHead({
-  title: data.value
+  title: error.value
+    ? "Error - Fedify Nuxt Example"
+    : data.value
     ? `${data.value.name} - Fedify Nuxt Example`
     : "Not Found - Fedify Nuxt Example",
 });
🤖 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 64 - 69, The
current useFetch result lumps network/errors and missing profiles together;
update the logic around the useFetch call (useFetch, data, error, identifier)
and use the returned error (error.value) and/or response status to distinguish a
failed request from a 404/no-profile: if error.value exists show an "Error -
Fedify Nuxt Example" title (or surface the error message), else if data.value is
null/undefined show "Not Found - Fedify Nuxt Example", otherwise use the profile
name; apply the same distinction to any v-if/v-else UI branches so errors and
"not found" render different states.

36-40: ⚠️ Potential issue | 🟠 Major

Add opener protection to the external _blank link.

The Fedify link opens a new tab without explicit rel protection.

🔒 Proposed change
               <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/users/`[identifier]/index.vue around lines 36 - 40, The
external anchor with class "fedify-anchor" opens in a new tab (target="_blank")
without opener protection; update the anchor element (the <a> that has
href="https://fedify.dev" and class="fedify-anchor") to include rel="noopener
noreferrer" so the new window cannot access window.opener and to prevent
potential performance/security issues.
examples/nuxt/server/api/unfollow.post.ts (1)

18-45: ⚠️ Potential issue | 🟠 Major

Guard remote lookup/delivery failures and keep the local unfollow flow resilient.

A rejection from lookupObject or sendActivity currently prevents the 303 redirect; for delivery failures after a valid target, local unfollow state should still be updated.

🛡️ Proposed resilient handling
-  const target = await ctx.lookupObject(targetUri) as APObject | null;
+  let target: APObject | null;
+  try {
+    target = await ctx.lookupObject(targetUri) as APObject | null;
+  } catch (error) {
+    console.debug("Failed to look up unfollow target:", error);
+    return sendRedirect(event, "/", 303);
+  }
   if (target?.id == null) {
     return sendRedirect(event, "/", 303);
   }
 
-  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.debug("Failed to send Undo activity:", error);
+  }
 
   followingStore.delete(target.id.href);
🤖 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 18 - 45, lookupObject
and sendActivity can reject and currently stop the 303 redirect and local state
update; wrap the remote calls in try/catch blocks: call ctx.lookupObject(...)
inside a try and treat failures as a null target (so you still return
sendRedirect if no target), and when target exists call ctx.sendActivity(...)
inside its own try/catch so any delivery errors are caught but do not prevent
executing followingStore.delete(target.id.href), broadcastEvent(), and the final
sendRedirect(event, "/", 303). Reference the functions ctx.lookupObject,
ctx.sendActivity, followingStore.delete, broadcastEvent, and sendRedirect to
locate where to add the error handling.
examples/nuxt/pages/index.vue (3)

207-215: ⚠️ Potential issue | 🟠 Major

Register onBeforeUnmount in setup scope, not inside onMounted.

Vue lifecycle hooks must be registered synchronously during setup; otherwise the EventSource cleanup may never attach.

In Vue 3 Composition API, must onBeforeUnmount be called synchronously during setup, or is calling it inside onMounted supported?
Move cleanup registration to setup scope
+let eventSource: EventSource | null = null;
+
 onMounted(() => {
-  const eventSource = new EventSource("/api/events");
+  eventSource = new EventSource("/api/events");
   eventSource.onmessage = () => {
     refresh();
   };
-  onBeforeUnmount(() => {
-    eventSource.close();
-  });
+});
+
+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 cleanup hook
onBeforeUnmount is being registered inside onMounted which violates Vue 3's
requirement that lifecycle hooks be registered synchronously during setup; move
the onBeforeUnmount call into the setup scope, declare a module-scoped or
setup-scoped variable (e.g., let eventSource) and assign it inside onMounted,
keep eventSource.onmessage = () => refresh() in onMounted, and close the
EventSource in the synchronously-registered onBeforeUnmount (with a null-check)
so the cleanup always attaches; reference the existing onMounted,
onBeforeUnmount, EventSource, and refresh identifiers when making the change.

160-160: ⚠️ Potential issue | 🟡 Minor

Harden the external link opened in a new tab.

Add rel="noopener noreferrer" to prevent opener access.

Harden the external anchor
-      <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
opening a new tab (the <a> element with class "fedify-anchor" and href
"https://fedify.dev" that has target="_blank") should be hardened by adding
rel="noopener noreferrer" to prevent the new page from accessing window.opener;
update that anchor to include the rel attribute alongside target="_blank".

181-194: ⚠️ Potential issue | 🟠 Major

Ignore stale search responses.

A slower older /api/search response can overwrite a newer result and make the follow/unfollow form target the wrong actor.

Track the latest search request
 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;
     }
@@
-    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 onSearchInput
debounce can let slower earlier /api/search responses overwrite newer results;
modify onSearchInput to track the latest request (e.g., increment a requestId or
store a local token like latestSearchId on the outer scope) before calling
$fetch, capture that id/token inside the async callback, and only assign
searchResult.value if the captured id matches the current latestSearchId;
alternatively, use an AbortController per request (store it alongside
searchTimeout) and abort the previous controller before making a new fetch so
only the latest response updates searchResult.value.
examples/nuxt/server/api/post.post.ts (2)

24-29: ⚠️ Potential issue | 🟠 Major

Persist published when creating the local Note.

This still creates stored posts without a publish timestamp, so timeline/detail/federated Note rendering can lose the created time. Add a published value using the timestamp type expected by @fedify/vocab. 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 24 - 29, When
constructing the local Note (the Note constructor call creating variable post),
include a published field set to the current timestamp in the
ActivityPub-compatible string format (e.g. new Date().toISOString()) so the
stored Note has a publish time; update the object passed to new Note({ id: url,
attribution, content: content.trim(), url, published: /* ISO timestamp */ }) to
ensure `@fedify/vocab` receives the expected timestamp type.

8-9: ⚠️ Potential issue | 🟠 Major

Broadcast after a successful post so SSE clients refresh.

This handler mutates postStore, but unlike follow/unfollow changes it never notifies /api/events subscribers. Other open home pages will not see the post until refresh.

Emit the SSE update after the Create succeeds
 import federation from "../federation.ts";
+import { broadcastEvent } from "../sse.ts";
 import { postStore } from "../store.ts";
@@
     await ctx.sendActivity(
       { identifier },
       "followers",
       new Create({
@@
         ccs: note?.ccIds,
       }),
     );
+    broadcastEvent();
   } catch {
     postStore.delete(url);
   }

Also applies to: 30-46

🤖 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 8 - 9, The POST handler
currently mutates postStore but never notifies SSE clients; after the create
succeeds you must emit the same SSE update used by follow/unfollow so
/api/events subscribers refresh. Locate the POST route in
examples/nuxt/server/api/post.post.ts where postStore is updated and,
immediately after the successful mutation (and only on success), call the
module/method used elsewhere for broadcasts (the federation/event emitter or the
postStore broadcast helper used by follow/unfollow) to push an update for the
new post to /api/events subscribers, including the created post payload so open
home pages refresh correctly.
examples/nuxt/server/federation.ts (1)

130-142: ⚠️ Potential issue | 🟠 Major

Validate the requested followers collection before returning demo followers.

As written, /users/<anything>/followers can return the demo account’s followers. Check that the request targets IDENTIFIER before exposing the collection.

#!/bin/bash
# Verify the current followers dispatcher callback/context API before applying the guard.
rg -n -C4 'type FollowersDispatcher|setFollowersDispatcher|parseUri' packages/fedify/src/federation --type ts
Guard the followers dispatcher
   .setFollowersDispatcher(
     "/users/{identifier}/followers",
-    () => {
+    (context) => {
+      const requested = context.parseUri(context.url);
+      if (requested?.type !== "followers" || requested.identifier !== IDENTIFIER) {
+        return { items: [] };
+      }
       const followers = Array.from(relationStore.values());
       const items: Recipient[] = followers.map((f) => ({
         id: f.id,
🤖 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 130 - 142, The followers
dispatcher currently returns demo followers for any
/users/{identifier}/followers request; update the setFollowersDispatcher
callback (the function passed to setFollowersDispatcher for
"/users/{identifier}/followers") to accept the request/context parameter,
extract the path parameter (identifier) and verify it equals IDENTIFIER before
using relationStore; if it does not match, return an empty collection or a
not-found response instead of exposing relationStore. Ensure you reference the
same parameter name used by the dispatcher API (e.g., params or context.params)
and only map relationStore.values() when identifier === IDENTIFIER.
🤖 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/nuxt.config.ts`:
- Around line 5-6: Add a short inline comment above the devServer and
vite.server settings (devServer.host and vite.server.allowedHosts) explaining
these loosened options are intentional for this demo to support tunneling/local
federation testing (e.g., pinggy/localtunnel) and are not recommended for
general Nuxt projects; keep the existing settings unchanged for the demo but
clearly warn consumers copying the file to remove or tighten them for production
or normal development.

In `@examples/nuxt/server/api/post.post.ts`:
- Around line 12-13: The code reads the request body loosely causing content to
be typed as unknown; explicitly type the request shape and pass it to readBody
so TypeScript enforces the expected fields (e.g., declare an interface/type for
the body with content) and then destructure or access content from that typed
body. Update the readBody(event) call in post.post.ts to
readBody<YourBodyType>(event), replace the loose body variable with the typed
one (body or destructured { content }), and ensure any downstream use of content
matches the declared type or is validated before use.

In `@examples/nuxt/server/federation.ts`:
- Around line 85-89: The Accept activity ID must be unique per Follow, so change
the Accept constructor to derive its id from the Follow activity's id rather
than the follower actor; replace the current use of follower.id.href with the
Follow activity identifier (e.g., follow.id.href or followActivity.id.href) when
building the new URL in the new Accept(...) call, and if the Follow may lack an
id ensure you generate/validate a stable unique id from the Follow before
constructing the Accept.
- Around line 122-123: The federated object is advertising unescaped
user-composed text as HTML (see content: post.content and mediaType: "text/html"
in federation.ts); either escape post.content before exposing it as HTML (use a
central escapeHtml utility and apply it where the federated payload is
constructed) or change mediaType to "text/plain" to avoid treating raw text as
HTML—update the payload construction to use the escaped value (e.g.,
escapeHtml(post.content)) or set mediaType: "text/plain" accordingly and ensure
any consumers expect plain text.
- Around line 78-81: The code currently force-casts the result of
follow.getActor(context) to Person which bypasses validation; replace the "as
Person" cast by checking the returned value with the isActor() type guard (e.g.,
const follower = await follow.getActor(context); if (!follower ||
!isActor(follower) || follower.type !== 'Person') { throw new Error("invalid or
missing follower actor"); } ) and only treat it as a Person after that guard so
federated input is validated before storing.

In `@examples/nuxt/server/store.ts`:
- Around line 14-20: The append method can add duplicate ids when the input
posts array contains multiple items with the same id; change the logic in append
(function append and its use of this.#map and this.#timeline) to perform
deduplication inside the iteration: iterate over posts and for each p compute
the key (p.id!.toString()), then check this.#map.has(key) (or a local Set
seenKeys for intra-batch duplicates) before calling this.#map.set(key, p) and
this.#timeline.push(key); this ensures duplicates within the same batch are
skipped even if they all passed the initial filter.
- Around line 3-8: The global var declarations for keyPairsStore, relationStore,
postStore, and followingStore pollute the global type namespace; instead create
a single namespaced holder (e.g., const g = globalThis as unknown as {
__nuxtStores?: { keyPairsStore?: Map<string, CryptoKeyPair[]>, relationStore?:
Map<string,Person>, postStore?: PostStore, followingStore?: Map<string,Person> }
}) and initialize/access stores on that namespace (use
g.__nuxtStores.keyPairsStore ??= new Map() etc.), then export the initialized
instances from this module so only the exported symbols are used publicly;
update references in the file to read/write via the namespaced holder rather
than declaring global vars.

---

Outside diff comments:
In `@pnpm-workspace.yaml`:
- Around line 34-45: The list entry "- examples/nuxt" in pnpm-workspace.yaml is
out of alphabetical order; move the "- examples/nuxt" line so it appears after
"- examples/next15-app-router" (i.e., place the nuxt entry below
next15-app-router and above "- examples/solidstart") to restore proper
alphabetical ordering within the examples/* block.

---

Duplicate comments:
In `@examples/nuxt/app.vue`:
- Line 11: The injected head script for "/theme.js" runs too early and accesses
document.body during head parsing; update the script entry (the script: [...]
object that currently has src: "/theme.js") to include the defer attribute so
the browser defers execution until after parsing (e.g., add defer: true on the
object with src "/theme.js") to prevent startup crashes.

In `@examples/nuxt/pages/index.vue`:
- Around line 207-215: The cleanup hook onBeforeUnmount is being registered
inside onMounted which violates Vue 3's requirement that lifecycle hooks be
registered synchronously during setup; move the onBeforeUnmount call into the
setup scope, declare a module-scoped or setup-scoped variable (e.g., let
eventSource) and assign it inside onMounted, keep eventSource.onmessage = () =>
refresh() in onMounted, and close the EventSource in the
synchronously-registered onBeforeUnmount (with a null-check) so the cleanup
always attaches; reference the existing onMounted, onBeforeUnmount, EventSource,
and refresh identifiers when making the change.
- Line 160: The external anchor element opening a new tab (the <a> element with
class "fedify-anchor" and href "https://fedify.dev" that has target="_blank")
should be hardened by adding rel="noopener noreferrer" to prevent the new page
from accessing window.opener; update that anchor to include the rel attribute
alongside target="_blank".
- Around line 181-194: The onSearchInput debounce can let slower earlier
/api/search responses overwrite newer results; modify onSearchInput to track the
latest request (e.g., increment a requestId or store a local token like
latestSearchId on the outer scope) before calling $fetch, capture that id/token
inside the async callback, and only assign searchResult.value if the captured id
matches the current latestSearchId; alternatively, use an AbortController per
request (store it alongside searchTimeout) and abort the previous controller
before making a new fetch so only the latest response updates
searchResult.value.

In `@examples/nuxt/pages/users/`[identifier]/index.vue:
- Around line 64-69: The current useFetch result lumps network/errors and
missing profiles together; update the logic around the useFetch call (useFetch,
data, error, identifier) and use the returned error (error.value) and/or
response status to distinguish a failed request from a 404/no-profile: if
error.value exists show an "Error - Fedify Nuxt Example" title (or surface the
error message), else if data.value is null/undefined show "Not Found - Fedify
Nuxt Example", otherwise use the profile name; apply the same distinction to any
v-if/v-else UI branches so errors and "not found" render different states.
- Around line 36-40: The external anchor with class "fedify-anchor" opens in a
new tab (target="_blank") without opener protection; update the anchor element
(the <a> that has href="https://fedify.dev" and class="fedify-anchor") to
include rel="noopener noreferrer" so the new window cannot access window.opener
and to prevent potential performance/security issues.

In `@examples/nuxt/pages/users/`[identifier]/posts/[id].vue:
- Around line 44-49: The title logic incorrectly treats any empty data as "Not
Found" even when useFetch failed; update the fetch handling around the useFetch
call (const { data, error } = await useFetch(`/api/posts/${identifier}/${id}`))
and change the useHead logic to distinguish error vs missing post: if error is
present set a failure title (e.g., "Error - Fedify Nuxt Example"), otherwise if
no error and data.value is null/undefined set "Not Found - Fedify Nuxt Example",
otherwise use `Post - ${data.value.author.name} - Fedify Nuxt Example`; ensure
you reference useFetch, data, error, useHead, identifier and id when making the
change.
- Line 3: Replace the plain anchor used for internal navigation with Nuxt's
client-side link component to enable SPA navigation: find the HTML anchor
element with class "back-link" (and the other identical occurrence noted) and
swap it to use NuxtLink (or <router-link> if your project uses Vue Router
directly) targeting the root path, preserving the class and link text so clicks
use Nuxt's client-side navigation instead of causing a full page reload.

In `@examples/nuxt/README.md`:
- Line 10: The sentence "implementations using the Fedify and [Nuxt]." reads
awkwardly; edit it to consistently reference both products by name and link
style—e.g., change to "implementations using [Fedify] and [Nuxt]." or
"implementations using Fedify and Nuxt." ensuring both names are linked the same
way and remove the extra "the" before Fedify.

In `@examples/nuxt/server/api/events.get.ts`:
- Around line 1-7: The SSE handler currently calls h3.setResponseHeader multiple
times which duplicates headers already provided on the returned raw Response;
remove the import and all calls to setResponseHeader in the handler (references:
setResponseHeader, defineEventHandler, addClient, removeClient) and rely solely
on the Response object’s headers when constructing/returning the SSE Response so
no duplicate header setting remains.

In `@examples/nuxt/server/api/follow.post.ts`:
- Around line 18-41: The code currently calls ctx.lookupObject() and
ctx.sendActivity() without guarding for rejections, which can cause a 500 and
still mutate local state; wrap the remote calls in a try/catch and only perform
followingStore.set(target.id.href) and broadcastEvent() after sendActivity
resolves successfully. Specifically, call ctx.lookupObject(identifier) and if it
returns null keep the existing sendRedirect(event, "/", 303) path; then call
ctx.sendActivity(...) inside a try block and on success do the Person check,
followingStore.set and broadcastEvent; on any error from lookup or sendActivity
catch it and return sendRedirect(event, "/", 303) so local state is never
mutated on remote failures.

In `@examples/nuxt/server/api/post.post.ts`:
- Around line 24-29: When constructing the local Note (the Note constructor call
creating variable post), include a published field set to the current timestamp
in the ActivityPub-compatible string format (e.g. new Date().toISOString()) so
the stored Note has a publish time; update the object passed to new Note({ id:
url, attribution, content: content.trim(), url, published: /* ISO timestamp */
}) to ensure `@fedify/vocab` receives the expected timestamp type.
- Around line 8-9: The POST handler currently mutates postStore but never
notifies SSE clients; after the create succeeds you must emit the same SSE
update used by follow/unfollow so /api/events subscribers refresh. Locate the
POST route in examples/nuxt/server/api/post.post.ts where postStore is updated
and, immediately after the successful mutation (and only on success), call the
module/method used elsewhere for broadcasts (the federation/event emitter or the
postStore broadcast helper used by follow/unfollow) to push an update for the
new post to /api/events subscribers, including the created post payload so open
home pages refresh correctly.

In `@examples/nuxt/server/api/search.get.ts`:
- Around line 7-10: The code narrows query.q unsafely by casting to
string|undefined and then calling .trim(), which will throw if query.q is an
array from repeated params; in the handler using getQuery(event) and variable q,
first inspect the raw value (e.g., query.q) and explicitly narrow it: if
Array.isArray(query.q) take the first element (or treat as invalid), else ensure
typeof query.q === 'string' before assigning to q and calling q.trim(); update
the early-return logic to handle non-string/empty cases accordingly so .trim()
is only ever called on a guaranteed string.

In `@examples/nuxt/server/api/unfollow.post.ts`:
- Around line 18-45: lookupObject and sendActivity can reject and currently stop
the 303 redirect and local state update; wrap the remote calls in try/catch
blocks: call ctx.lookupObject(...) inside a try and treat failures as a null
target (so you still return sendRedirect if no target), and when target exists
call ctx.sendActivity(...) inside its own try/catch so any delivery errors are
caught but do not prevent executing followingStore.delete(target.id.href),
broadcastEvent(), and the final sendRedirect(event, "/", 303). Reference the
functions ctx.lookupObject, ctx.sendActivity, followingStore.delete,
broadcastEvent, and sendRedirect to locate where to add the error handling.

In `@examples/nuxt/server/federation.ts`:
- Around line 130-142: The followers dispatcher currently returns demo followers
for any /users/{identifier}/followers request; update the setFollowersDispatcher
callback (the function passed to setFollowersDispatcher for
"/users/{identifier}/followers") to accept the request/context parameter,
extract the path parameter (identifier) and verify it equals IDENTIFIER before
using relationStore; if it does not match, return an empty collection or a
not-found response instead of exposing relationStore. Ensure you reference the
same parameter name used by the dispatcher API (e.g., params or context.params)
and only map relationStore.values() when identifier === IDENTIFIER.

In `@examples/nuxt/server/sse.ts`:
- Around line 16-20: The broadcastEvent function currently iterates over clients
and calls client.send(data) which can throw for a disconnected stream; change it
to isolate failures by wrapping each client.send(data) in a try/catch so one bad
client doesn't abort the loop, and on error log (or handle) the failure and
remove/close that specific client from the clients collection to avoid repeated
failures; reference broadcastEvent, clients, and client.send when making this
change.
🪄 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: a06c199a-8199-4a0c-b165-04a4415234ff

📥 Commits

Reviewing files that changed from the base of the PR and between 1c2ef9e and 4876c0b.

⛔ Files ignored due to path filters (4)
  • deno.lock is excluded by !**/*.lock
  • examples/nuxt/public/demo-profile.png is excluded by !**/*.png
  • examples/nuxt/public/fedify-logo.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • .agents/skills/create-example-app-with-integration/example/public/style.css
  • .agents/skills/create-example-app-with-integration/example/public/theme.js
  • examples/nuxt/.gitignore
  • examples/nuxt/README.md
  • examples/nuxt/app.vue
  • examples/nuxt/nuxt.config.ts
  • examples/nuxt/package.json
  • examples/nuxt/pages/index.vue
  • examples/nuxt/pages/users/[identifier]/index.vue
  • examples/nuxt/pages/users/[identifier]/posts/[id].vue
  • examples/nuxt/public/style.css
  • examples/nuxt/public/theme.js
  • examples/nuxt/server/api/events.get.ts
  • examples/nuxt/server/api/follow.post.ts
  • examples/nuxt/server/api/home.get.ts
  • examples/nuxt/server/api/post.post.ts
  • examples/nuxt/server/api/posts/[identifier]/[id].get.ts
  • examples/nuxt/server/api/profile/[identifier].get.ts
  • examples/nuxt/server/api/search.get.ts
  • examples/nuxt/server/api/unfollow.post.ts
  • examples/nuxt/server/federation.ts
  • examples/nuxt/server/plugins/logging.ts
  • examples/nuxt/server/sse.ts
  • examples/nuxt/server/store.ts
  • examples/nuxt/tsconfig.json
  • examples/test-examples/mod.ts
  • packages/nuxt/package.json
  • pnpm-workspace.yaml

Comment on lines +5 to +6
devServer: { host: "0.0.0.0" },
vite: { server: { allowedHosts: true } },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Confirm devServer.host: "0.0.0.0" and vite.server.allowedHosts: true are only intended for this demo.

Binding the dev server to all interfaces and disabling Vite's host-header allow-list is useful for tunneling (e.g., pinggy/localtunnel) during local federation testing, but it turns off a defence against DNS-rebinding-style attacks on the dev server. Since the test runner uses pnpm start against the built Nitro output (not the Vite dev server), this only affects developers running nuxt dev. Consider a brief comment in the config noting these options are intentional for tunnel-based federation demos and not recommended defaults for general Nuxt projects, so consumers who copy this file don't propagate the looser settings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/nuxt/nuxt.config.ts` around lines 5 - 6, Add a short inline comment
above the devServer and vite.server settings (devServer.host and
vite.server.allowedHosts) explaining these loosened options are intentional for
this demo to support tunneling/local federation testing (e.g.,
pinggy/localtunnel) and are not recommended for general Nuxt projects; keep the
existing settings unchanged for the demo but clearly warn consumers copying the
file to remove or tighten them for production or normal development.

Comment on lines +12 to +13
const body = await readBody(event);
const content = body?.content;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Type the request body before reading content.

readBody is currently loose here; give it an explicit shape so content stays unknown until validated. As per coding guidelines: Maintain strict TypeScript typing throughout.

Type the body shape
-  const body = await readBody(event);
+  const body = await readBody<{ content?: unknown }>(event);
   const content = body?.content;
📝 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.

Suggested change
const body = await readBody(event);
const content = body?.content;
const body = await readBody<{ content?: unknown }>(event);
const content = body?.content;
🤖 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 12 - 13, The code reads
the request body loosely causing content to be typed as unknown; explicitly type
the request shape and pass it to readBody so TypeScript enforces the expected
fields (e.g., declare an interface/type for the body with content) and then
destructure or access content from that typed body. Update the readBody(event)
call in post.post.ts to readBody<YourBodyType>(event), replace the loose body
variable with the typed one (body or destructured { content }), and ensure any
downstream use of content matches the declared type or is validated before use.

Comment on lines +78 to +81
const follower = await follow.getActor(context) as Person;
if (!follower?.id) {
throw new Error("follower is null");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify available actor type guards/classes before choosing isActor vs instanceof Person.
rg -n -C3 '\bisActor\b|class Person|export .*Person' packages examples --type ts

Repository: fedify-dev/fedify

Length of output: 11815


Validate the fetched follower actor instead of forcing Person.

follow.getActor() can return null or a non-Person actor; the as Person assertion bypasses strict typing and federated input validation before storing it. Replace with isActor() guard to validate it matches an allowed actor type.

Use an explicit actor validation
   Image,
+  isActor,
   Note,
   Person,
@@
-    const follower = await follow.getActor(context) as Person;
-    if (!follower?.id) {
+    const follower = await follow.getActor(context);
+    if (!isActor(follower) || follower.id == null) {
       throw new Error("follower is null");
     }
🤖 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 78 - 81, The code currently
force-casts the result of follow.getActor(context) to Person which bypasses
validation; replace the "as Person" cast by checking the returned value with the
isActor() type guard (e.g., const follower = await follow.getActor(context); if
(!follower || !isActor(follower) || follower.type !== 'Person') { throw new
Error("invalid or missing follower actor"); } ) and only treat it as a Person
after that guard so federated input is validated before storing.

Comment on lines +85 to +89
new Accept({
id: new URL(
`#accepts/${follower.id.href}`,
context.getActorUri(IDENTIFIER),
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the Accept activity ID unique per Follow.

The current ID is derived from the follower actor, so repeated follow requests from the same actor reuse the same Accept ID and can be deduplicated remotely. Use the Follow activity ID instead. Based on learnings: keep ActivityPub compliance in mind for interoperability when working with federation code.

Derive the Accept ID from the Follow activity
         id: new URL(
-          `#accepts/${follower.id.href}`,
+          `#accepts/${encodeURIComponent(follow.id.href)}`,
           context.getActorUri(IDENTIFIER),
         ),
🤖 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 85 - 89, The Accept activity
ID must be unique per Follow, so change the Accept constructor to derive its id
from the Follow activity's id rather than the follower actor; replace the
current use of follower.id.href with the Follow activity identifier (e.g.,
follow.id.href or followActivity.id.href) when building the new URL in the new
Accept(...) call, and if the Follow may lack an id ensure you generate/validate
a stable unique id from the Follow before constructing the Accept.

Comment on lines +122 to +123
content: post.content,
mediaType: "text/html",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not expose raw composed text as text/html.

post.content originates from the compose form, but the federated object advertises it as HTML. Escape it before serving as HTML, or change the media type to plaintext.

Escape content before exposing it as HTML
+function escapeHtml(value: string): string {
+  return value.replace(/[&<>"']/g, (char) => ({
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': "&quot;",
+    "'": "&#39;",
+  })[char]!);
+}
+
 federation.setObjectDispatcher(
@@
-      content: post.content,
+      content: escapeHtml(String(post.content ?? "")),
       mediaType: "text/html",
🤖 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 122 - 123, The federated
object is advertising unescaped user-composed text as HTML (see content:
post.content and mediaType: "text/html" in federation.ts); either escape
post.content before exposing it as HTML (use a central escapeHtml utility and
apply it where the federated payload is constructed) or change mediaType to
"text/plain" to avoid treating raw text as HTML—update the payload construction
to use the escaped value (e.g., escapeHtml(post.content)) or set mediaType:
"text/plain" accordingly and ensure any consumers expect plain text.

Comment on lines +3 to +8
declare global {
var keyPairsStore: Map<string, Array<CryptoKeyPair>>;
var relationStore: Map<string, Person>;
var postStore: PostStore;
var followingStore: Map<string, Person>;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid polluting the global type namespace with var declarations.

Declaring keyPairsStore, relationStore, postStore, and followingStore as global vars makes these names accessible unqualified across any file in the project, which is an easy source of accidental shadowing/mis-use. Since the intent is only to persist instances across Nuxt/Nitro HMR reloads, prefer a single namespaced symbol (or a typed globalThis cast at the assignment site) and keep the exports as the public API:

Proposed fix
-declare global {
-  var keyPairsStore: Map<string, Array<CryptoKeyPair>>;
-  var relationStore: Map<string, Person>;
-  var postStore: PostStore;
-  var followingStore: Map<string, Person>;
-}
+interface FedifyDemoGlobals {
+  keyPairsStore?: Map<string, Array<CryptoKeyPair>>;
+  relationStore?: Map<string, Person>;
+  postStore?: PostStore;
+  followingStore?: Map<string, Person>;
+}
+const g = globalThis as typeof globalThis & FedifyDemoGlobals;

and then use g.keyPairsStore ?? new Map() etc.

Not a correctness issue for the demo — feel free to ignore if you'd rather keep it simple.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/nuxt/server/store.ts` around lines 3 - 8, The global var
declarations for keyPairsStore, relationStore, postStore, and followingStore
pollute the global type namespace; instead create a single namespaced holder
(e.g., const g = globalThis as unknown as { __nuxtStores?: { keyPairsStore?:
Map<string, CryptoKeyPair[]>, relationStore?: Map<string,Person>, postStore?:
PostStore, followingStore?: Map<string,Person> } }) and initialize/access stores
on that namespace (use g.__nuxtStores.keyPairsStore ??= new Map() etc.), then
export the initialized instances from this module so only the exported symbols
are used publicly; update references in the file to read/write via the
namespaced holder rather than declaring global vars.

Comment on lines +14 to +20
append(posts: Note[]) {
posts.filter((p) => p.id && !this.#map.has(p.id.toString()))
.forEach((p) => {
this.#map.set(p.id!.toString(), p);
this.#timeline.push(p.id!);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

append can insert duplicates from the same batch.

The filter runs before any set, so if the input posts array contains two notes with the same id, both pass the predicate and both get pushed onto #timeline (the map then just overwrites itself). Consider deduplicating inside the loop instead:

Proposed fix
-  append(posts: Note[]) {
-    posts.filter((p) => p.id && !this.#map.has(p.id.toString()))
-      .forEach((p) => {
-        this.#map.set(p.id!.toString(), p);
-        this.#timeline.push(p.id!);
-      });
-  }
+  append(posts: Note[]) {
+    for (const p of posts) {
+      if (!p.id) continue;
+      const key = p.id.toString();
+      if (this.#map.has(key)) continue;
+      this.#map.set(key, p);
+      this.#timeline.push(p.id);
+    }
+  }

In practice callers only append one note at a time, so this is a latent issue rather than an active bug.

📝 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.

Suggested change
append(posts: Note[]) {
posts.filter((p) => p.id && !this.#map.has(p.id.toString()))
.forEach((p) => {
this.#map.set(p.id!.toString(), p);
this.#timeline.push(p.id!);
});
}
append(posts: Note[]) {
for (const p of posts) {
if (!p.id) continue;
const key = p.id.toString();
if (this.#map.has(key)) continue;
this.#map.set(key, p);
this.#timeline.push(p.id);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/nuxt/server/store.ts` around lines 14 - 20, The append method can
add duplicate ids when the input posts array contains multiple items with the
same id; change the logic in append (function append and its use of this.#map
and this.#timeline) to perform deduplication inside the iteration: iterate over
posts and for each p compute the key (p.id!.toString()), then check
this.#map.has(key) (or a local Set seenKeys for intra-batch duplicates) before
calling this.#map.set(key, p) and this.#timeline.push(key); this ensures
duplicates within the same batch are skipped even if they all passed the initial
filter.

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

Labels

component/build Build system and packaging component/federation Federation object related component/integration Web framework integration component/testing Testing utilities (@fedify/testing)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant