Skip to content

Add Nuxt example application#676

Draft
2chanhaeng wants to merge 31 commits intofedify-dev:mainfrom
2chanhaeng:pr/example-nuxt
Draft

Add Nuxt example application#676
2chanhaeng wants to merge 31 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 new @fedify/nuxt Nuxt 4 integration (module + runtime), a complete Nuxt example app, fedify init Nuxt scaffolding, docs, tests, workspace/package manifests, and auxiliary example runtime stores/API and SSE support.

Changes

Cohort / File(s) Summary
Nuxt package core
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
New @fedify/nuxt module and runtime: module options/types, server template generation, middleware to delegate requests to Fedify, deferred 406 handling, and server plugin to apply deferred responses.
Nuxt package manifests & build
packages/nuxt/package.json, packages/nuxt/deno.json, packages/nuxt/tsdown.config.ts, packages/nuxt/README.md
New package manifests, build config, and README for @fedify/nuxt.
Nuxt package tests
packages/nuxt/src/mod.test.ts, packages/nuxt/src/module.test.ts, packages/nuxt/src/runtime/server/logic.test.ts, packages/nuxt/src/runtime/server/plugin.test.ts
New tests covering fetch delegation, deferred 406 resolution, context factory resolver behavior, and plugin beforeResponse behavior.
Nuxt example app (runtime + UI)
examples/nuxt/server/federation.ts, examples/nuxt/server/store.ts, examples/nuxt/server/sse.ts, examples/nuxt/server/plugins/logging.ts, examples/nuxt/server/api/*, examples/nuxt/pages/**, examples/nuxt/app.vue, examples/nuxt/public/*
Full example Nuxt app demonstrating federation instance, in-memory stores, SSE broadcast, ActivityPub endpoints (home, profile, posts, follow/unfollow), UI pages, styling, and theme script.
Example packaging & config
examples/nuxt/package.json, examples/nuxt/nuxt.config.ts, examples/nuxt/tsconfig.json, examples/nuxt/README.md, examples/nuxt/.gitignore
Example project config, npm scripts, Nuxt config enabling the module, TypeScript config, README, and gitignore.
Init CLI & templates
packages/init/src/webframeworks/nuxt.ts, packages/init/src/templates/nuxt/nuxt.config.ts.tpl, packages/init/src/const.ts, packages/init/src/json/deps.json, packages/init/src/test/*, packages/init/src/webframeworks/mod.ts
Register Nuxt in fedify init: scaffolding template, deps, test/port handling adjustments, and inclusion in the web frameworks registry.
Monorepo & tooling config
deno.json, pnpm-workspace.yaml, mise.toml, cspell.json, .hongdown.toml, AGENTS.md, CONTRIBUTING.md, CHANGES.md, packages/fedify/README.md
Workspace and tooling updates to include packages/nuxt, add catalog version mappings, spellcheck entries, changelog/docs updates, and a tunnel task.
Agent skills & docs
.agents/skills/*, docs/manual/integration.md, packages/nuxt/README.md
Updated skill guidance and integration docs to reference Nuxt and example templates; new manual section documenting Nuxt integration.
Test harness
examples/test-examples/mod.ts
Added Nuxt example configuration to the examples test registry.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Nuxt as Nuxt Server (Nitro/H3)
    participant Fedify as `@fedify/nuxt` Middleware
    participant Federation as Federation Instance
    participant Framework as Nuxt Routes/API

    Client->>Nuxt: HTTP Request
    Nuxt->>Fedify: middleware converts to Web Request and invokes fetcher
    alt Fedify handles federation route
        Fedify->>Federation: federation.fetch(request)
        Federation-->>Fedify: ActivityPub Response (200)
        Fedify-->>Nuxt: return Response (handled)
        Nuxt-->>Client: Response
    else Fedify defers to framework (not acceptable / not found)
        Fedify->>Framework: onNotFound / onNotAcceptable callbacks
        Framework-->>Fedify: Not found (404) or framework response
        alt Framework returned 404 and should be deferred
            Fedify-->>Nuxt: signal deferred-not-acceptable
            Nuxt->>Nuxt: beforeResponse hook computes 406 and rewrites response
            Nuxt-->>Client: 406 Not Acceptable
        else Framework handles route (200)
            Nuxt-->>Client: Framework response (200 or other)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

type/test, activitypub/interop

Suggested reviewers

  • dahlia
🚥 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 'Add Nuxt example application' accurately summarizes the main change: introducing a comprehensive new Nuxt example app demonstrating @fedify/nuxt integration.
Description check ✅ Passed The description is well-structured and directly related to the changeset, detailing the new example features, server integration, test setup, and a related bugfix to the @fedify/nuxt module.

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

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

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 on lines +1 to +24
import { defineEventHandler } from "h3";
import federation from "../federation";

export default defineEventHandler(async (event) => {
// Construct the full URL from headers
const proto = event.headers.get("x-forwarded-proto") || "http";
const host = event.headers.get("host") || "localhost";
const url = new URL(event.node.req.url || "", `${proto}://${host}`);

const request = new Request(url, {
method: event.node.req.method,
headers: event.node.req.headers as Record<string, string>,
body: ["GET", "HEAD", "DELETE"].includes(event.node.req.method)
? undefined
: undefined,
});

const response = await federation.fetch(request, {
contextData: undefined,
});

if (response.status === 404) return; // Let Nuxt handle 404
return response;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The manual construction of the Request object has a bug where the body is always undefined (lines 13-15), which will break incoming ActivityPub activities (POST requests). Additionally, manual URL reconstruction is error-prone. It is highly recommended to use h3's toWebRequest(event) utility, which correctly handles headers, methods, and bodies across different runtimes and ensures the host/port are preserved correctly.

import { defineEventHandler, toWebRequest } from "h3";
import federation from "../federation";

export default defineEventHandler(async (event) => {
  const request = toWebRequest(event);
  const response = await federation.fetch(request, {
    contextData: undefined,
  });

  if (response.status === 404) return; // Let Nuxt handle 404
  return response;
});
References
  1. When reconstructing a URL from a request object, prefer using methods that preserve the host and port (like the Host header) to ensure functionality like federation signature verification remains intact.

devDependencies: {
...defaultDevDependencies,
"typescript": deps["npm:typescript"],
"@types/node": deps["npm:@types/node@25"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The key npm:@types/node@25 appears to be a typo and is likely missing from packages/init/src/json/deps.json. Node.js version 25 is not yet released. This should probably refer to a current version like @types/node@22 or simply @types/node as defined in the dependencies catalog.

      "@types/node": deps["npm:@types/node"],

@@ -0,0 +1,5 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
ssr: false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Setting ssr: false (Single Page Application mode) is generally inappropriate for federated applications. ActivityPub requires the server to respond with JSON-LD to actors, often on the same routes that serve HTML to browsers. Disabling SSR can complicate content negotiation and discovery. It is better to default to ssr: true, as seen in the example application.

  ssr: true,

Comment on lines +24 to +29
"nuxt.config.ts": await readTemplate("nuxt/nuxt.config.ts"),
"server/federation.ts": await readTemplate("nuxt/server/federation.ts"),
"server/logging.ts": await readTemplate("nuxt/server/logging.ts"),
"server/middleware/federation.ts": await readTemplate(
"nuxt/server/middleware/federation.ts",
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The fedify init setup for Nuxt is currently inconsistent. It adds @fedify/nuxt to the dependencies but then manually sets up a Nitro middleware in server/middleware/federation.ts. It would be much cleaner and more idiomatic to enable the @fedify/nuxt module in the nuxt.config.ts template and remove the manual middleware file. This avoids redundancy and leverages the module's built-in features like deferred 406 handling.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

❌ Patch coverage is 55.83333% with 106 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/init/src/webframeworks/nuxt.ts 16.17% 57 Missing ⚠️
packages/nuxt/src/module.ts 47.25% 48 Missing ⚠️
packages/init/src/test/port.ts 85.71% 1 Missing ⚠️
Files with missing lines Coverage Δ
packages/init/src/const.ts 100.00% <100.00%> (ø)
packages/init/src/test/lookup.ts 23.48% <100.00%> (+0.58%) ⬆️
packages/init/src/webframeworks/mod.ts 100.00% <100.00%> (ø)
packages/nuxt/src/runtime/server/lib.ts 100.00% <100.00%> (ø)
packages/nuxt/src/runtime/server/logic.ts 100.00% <100.00%> (ø)
packages/nuxt/src/runtime/server/plugin.ts 100.00% <100.00%> (ø)
packages/init/src/test/port.ts 14.52% <85.71%> (+4.09%) ⬆️
packages/nuxt/src/module.ts 47.25% <47.25%> (ø)
packages/init/src/webframeworks/nuxt.ts 16.17% <16.17%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Use @fedify/fixture instead of node:test in mod.test.ts
- Type event parameter as H3Event instead of unknown in middleware.ts

Co-Authored-By: Claude (claude-opus-4-20250514)
Move NOT_ACCEPTABLE_BODY and DEFERRED_NOT_ACCEPTABLE_CONTEXT_KEY into
a shared lib.ts to avoid string duplication across logic.ts and
plugin.ts.

Co-Authored-By: Claude (claude-opus-4-20250514)
- Close unclosed bash code fence in SKILL.md
- Fix "can not" to "cannot" in SKILL.md
- Use asterisk-wrapped file paths in README.md and integration.md

Co-Authored-By: Claude (claude-opus-4-20250514)
@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

- *packages/mysql/*: MySQL/MariaDB drivers (@fedify/mysql) for Fedify.
- *packages/nestjs/*: NestJS integration (@fedify/nestjs) for Fedify.
- *packages/next/*: Next.js integration (@fedify/next) for Fedify.
- *packages/nuxt/*: Nuxt integration (@fedify/nuxt) for Fedify.
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

Optional wording polish for repeated phrasing.

Line 395 is correct, but a small rephrase can reduce repetitive wording in adjacent bullets.

Suggested edit
- -  *packages/nuxt/*: Nuxt integration (`@fedify/nuxt`) for Fedify.
+ -  *packages/nuxt/*: Nuxt integration package (`@fedify/nuxt`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- *packages/nuxt/*: Nuxt integration (@fedify/nuxt) for Fedify.
- *packages/nuxt/*: Nuxt integration package (`@fedify/nuxt`).
🧰 Tools
🪛 LanguageTool

[style] ~395-~395: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...gration (@fedify/next) for Fedify. - packages/nuxt/: Nuxt integration (@fedify/nuxt)...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

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

In `@CONTRIBUTING.md` at line 395, The bullet "*packages/nuxt/*: Nuxt integration
(`@fedify/nuxt`) for Fedify." repeats wording used nearby; reword it to avoid
repetition by shortening or changing phrasing—e.g., replace with
"packages/nuxt/*: Nuxt integration (`@fedify/nuxt`)" or "packages/nuxt/*: Nuxt
adapter (`@fedify/nuxt`) for Fedify" so the list reads more concise and varied
while preserving the package path and npm scope mention.

{ rel: "stylesheet", href: "/style.css" },
{ rel: "icon", type: "image/svg+xml", href: "/fedify-logo.svg" },
],
script: [{ src: "/theme.js" }],
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 on lines +97 to +107
.on(Undo, async (context, undo) => {
const activity = await undo.getObject(context);
if (activity instanceof Follow) {
if (activity.id == null) {
return;
}
if (undo.actorId == null) {
return;
}
relationStore.delete(undo.actorId.href);
broadcastEvent();
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

Validate the undone Follow before removing a follower.

This branch deletes relationStore for any undone Follow as long as undo.actorId is present. On the shared inbox, an unrelated Undo(Follow(...)) can remove a real follower because the handler never checks that activity.objectId resolves back to /users/demo.

🐛 Proposed fix
   .on(Undo, async (context, undo) => {
     const activity = await undo.getObject(context);
     if (activity instanceof Follow) {
-      if (activity.id == null) {
-        return;
-      }
-      if (undo.actorId == null) {
+      if (activity.objectId == null || undo.actorId == null) {
         return;
       }
+      const result = context.parseUri(activity.objectId);
+      if (result?.type !== "actor" || result.identifier !== IDENTIFIER) {
+        return;
+      }
       relationStore.delete(undo.actorId.href);
       broadcastEvent();
     } else {
Based on learnings, keep ActivityPub compliance in mind for interoperability when working with federation code.
📝 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
.on(Undo, async (context, undo) => {
const activity = await undo.getObject(context);
if (activity instanceof Follow) {
if (activity.id == null) {
return;
}
if (undo.actorId == null) {
return;
}
relationStore.delete(undo.actorId.href);
broadcastEvent();
.on(Undo, async (context, undo) => {
const activity = await undo.getObject(context);
if (activity instanceof Follow) {
if (activity.objectId == null || undo.actorId == null) {
return;
}
const result = context.parseUri(activity.objectId);
if (result?.type !== "actor" || result.identifier !== IDENTIFIER) {
return;
}
relationStore.delete(undo.actorId.href);
broadcastEvent();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/nuxt/server/federation.ts` around lines 97 - 107, In the Undo
handler (.on(Undo, async (context, undo) => { ... })) you currently delete
relationStore for any undone Follow when undo.actorId exists; instead, first
resolve and validate that the undone Follow's target (activity.object /
activity.objectId / activity.id) actually refers to our local user (e.g.,
compare to the local actor id like "/users/demo" or the localActor.id) before
calling relationStore.delete and broadcastEvent; keep the existing instanceof
Follow check, ensure you use the resolved object id (not just undo.actorId) to
confirm the Follow was aimed at our user, and only then remove the follower
entry via relationStore.delete(undo.actorId.href) and call broadcastEvent.

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 on lines +38 to +54
function* getInitCommand(pm: PackageManager) {
yield* getNuxtInitCommand(pm);
yield* [
"init",
".",
"--template",
"minimal",
"--no-install",
"--force",
"--packageManager",
pm,
"--no-gitInit",
"--no-modules",
"&&",
"rm",
"nuxt.config.ts",
];
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

Keep cleanup out of the command argv.

Lines 51-54 append shell tokens to a string[] command. If the init runner executes argv directly, nuxi receives && rm nuxt.config.ts as arguments and the cleanup never runs; if it goes through a shell, rm is still POSIX-only. Move the nuxt.config.ts deletion into the init pipeline itself, or rely on the generated files entry to overwrite it.

🛠️ Minimal fix in this segment
 function* getInitCommand(pm: PackageManager) {
   yield* getNuxtInitCommand(pm);
   yield* [
     "init",
     ".",
     "--template",
     "minimal",
     "--no-install",
     "--force",
     "--packageManager",
     pm,
     "--no-gitInit",
     "--no-modules",
-    "&&",
-    "rm",
-    "nuxt.config.ts",
   ];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/init/src/webframeworks/nuxt.ts` around lines 38 - 54, The command
array returned by getInitCommand currently appends shell tokens ("&& rm
nuxt.config.ts") which will be passed as argv to nuxi or break on non-POSIX
shells; remove the cleanup tokens from the yielded array in
getInitCommand/getNuxtInitCommand and instead perform the nuxt.config.ts removal
as a separate init pipeline step (e.g., add a post-init action that deletes
"nuxt.config.ts") or ensure the generated files entry will overwrite that file;
update any pipeline/init runner code to call that deletion action rather than
embedding shell commands in the command argv.

 - Use en dash instead of hyphen in example README template
   title ("Fedify–프레임워크")
 - Wrap HTTP status codes in backticks and use official reason
   phrases in SKILL.md headings ("404 Not Found",
   "406 Not Acceptable")
 - Capitalize "Acceptable" in CHANGES.md to match the official
   HTTP status phrase

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6
The test file imports from @fedify/fixture but it was missing
from devDependencies, which would cause ERR_MODULE_NOT_FOUND
in a clean install when running Node.js tests.

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6
Add failing tests that reproduce four bugs identified in PR
review comments:

 - logic.test.ts: handler-returned 406 is misclassified as
   "not-acceptable" instead of "handled" (review fedify-dev#5)
 - logic.test.ts: framework's intentional 404 on shared routes
   is rewritten to 406 by resolveDeferredNotAcceptable (review fedify-dev#6)
 - module.test.ts: resolveAlias does not resolve plain relative
   paths to absolute, breaking .nuxt/ imports (review fedify-dev#7)
 - module.test.ts: missing default/contextDataFactory exports
   silently return undefined instead of throwing (review fedify-dev#9)

All four tests fail against the current implementation and will
pass once the corresponding bugs are fixed.

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6

Add regression tests for PR review issues

Add regression tests for PR review issues
Replace the status-code-based check (response.status === 406) with
an identity comparison against the synthetic response created by
the onNotAcceptable callback.  This prevents genuine handler-returned
406 responses from being misclassified as "not-acceptable".

The fix mirrors the existing pattern used for not-found detection,
where DUMMY_NOT_FOUND_RESPONSE is compared by reference.

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6
Add a routeHandled parameter to resolveDeferredNotAcceptable so
that it can distinguish a framework's intentional 404 (e.g., a
matched page returning "user not found") from a genuine route
miss.  When routeHandled is true, the framework's 404 is
preserved instead of being rewritten to 406.

The plugin now passes event.context.matchedRoute to detect
whether a Nitro route handler processed the request.

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6
resolveAlias() only expands aliases like ~ and @, leaving plain
relative paths (./server/federation) unchanged.  These paths
then resolve against .nuxt/ at runtime instead of the project
root, causing ERR_MODULE_NOT_FOUND.

Add resolveModulePath() that wraps resolveAlias and falls back
to path.resolve() against nuxt.options.rootDir for non-absolute
results.

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6
When contextDataFactoryModule is configured but exports neither
a default function nor a named contextDataFactory, the resolver
previously set contextDataFactory to undefined silently.  This
made misconfiguration hard to detect.

Now it throws a descriptive TypeError when both exports are
missing, matching the existing behavior for non-function exports.

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6
Add plugin-level tests for the Nuxt beforeResponse hook. The
new coverage verifies deferred 404 responses are rewritten to
406, ordinary 404 responses are left untouched when no
deferred flag is present, and shared-route 404 responses
remain unchanged when Nuxt matched the route.

fedify-dev#674

Assisted-by: GitHub Copilot:GPT-5.4
When resolveAlias() passes through a bare package specifier like
"@acme/federation" unchanged, the previous code resolved it
against rootDir, producing an invalid filesystem path such as
"/app/@acme/federation".

Now only explicit relative paths (./ or ../) are resolved against
rootDir.  Bare specifiers are returned as-is so the bundler can
resolve them from node_modules.

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6
Add a TContextData type parameter (defaulting to unknown) so that
typed context data produced by ContextDataFactory flows through
the fetcher signature end-to-end instead of being erased to
unknown at the helper boundary.

fedify-dev#674

Assisted-by: GitHub Copilot:claude-opus-4.6
@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 on lines +63 to +69
Copy the template files from the [package/](./package/) directory into
the directory you created. Use these commands in the root path:

~~~~ sh
mkdir -p packages/framework
cp -r .agents/skills/create-integration-package/package/* packages/framework/
~~~~
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

Copy the template directory contents, not just non-dotfiles.

package/* skips hidden files, so template dotfiles like .npmignore or .gitignore would be silently omitted by agents following this skill. Use package/. instead.

💡 Proposed fix
 mkdir -p packages/framework
-cp -r .agents/skills/create-integration-package/package/* packages/framework/
+cp -r .agents/skills/create-integration-package/package/. packages/framework/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/create-integration-package/SKILL.md around lines 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).

Comment on lines 187 to +189
You can test the integration using `mise test:init`, which will be explained
later, but write unit tests as well if possible. Import the `test` function
from `@fedify/fixture` to write runtime-agnostic tests that work across
Deno, Node.js, and Bun. Name test files with the `*.test.ts` convention
(e.g., `src/mod.test.ts`).
later, but write unit tests as well if possible. Name test files with the
`*.test.ts` convention (e.g., `src/mod.test.ts`).
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

Don't make tests optional in this skill.

The new coverage checklist is strong, but "write unit tests as well if possible" still gives generators permission to skip tests on new integrations. Tighten this to require tests unless the framework has a documented technical blocker.

💡 Proposed fix
-You can test the integration using `mise test:init`, which will be explained
-later, but write unit tests as well if possible.  Name test files with the
-`*.test.ts` convention (e.g., `src/mod.test.ts`).
+You can test the integration using `mise test:init`, which will be explained
+later, but write unit tests for the integration unless the framework has a
+documented technical blocker.  Name test files with the `*.test.ts`
+convention (e.g., `src/mod.test.ts`).

Based on learnings: Add unit tests for new features before implementing them in the feature implementation process.

Also applies to: 207-220

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

In @.agents/skills/create-integration-package/SKILL.md around lines 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.

Comment on lines +143 to +154
.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;
font-weight: 500;
text-decoration: none;
}
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

Boost the badge contrast.

white on #7dd3fc is low-contrast already, and the wrapper's opacity: 0.7 fades it further. That makes the "Fedify" link hard to read at the badge's small text size.

💡 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;
 }
@@
 .fedify-badge {
   text-align: center;
   margin-top: 1rem;
   font-size: 0.875rem;
-  opacity: 0.7;
+  color: color-mix(in srgb, var(--foreground) 70%, transparent);
 }

Also applies to: 281-286

🤖 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 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).

Comment on lines +12 to +16
[Fedify]: https://fedify.dev
[Nuxt]: https://nuxt.com/
[Mastodon]: https://mastodon.social/
[Misskey]: https://misskey.io/

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

Move reference-style links to the section end.

These link definitions should be placed at the section/document end to match repository markdown conventions.

As per coding guidelines, “maintain reference-style links at section end.”

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

Comment on lines +18 to +41
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();
return sendRedirect(event, "/", 303);
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 remote lookup/delivery failures and keep the 303 fallback.

examples/nuxt/server/api/search.get.ts already treats lookup failures as a soft miss. Here, a rejection from the remote lookup or delivery path turns the form submission into a 500 instead of redirecting back to /.

♻️ 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 {
+    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, 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).

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 on lines +19 to +24
const post = new Note({
id: url,
attribution,
content: content.trim(),
url,
});
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

Persist published when creating the note.

examples/nuxt/server/federation.ts, examples/nuxt/server/api/home.get.ts, and examples/nuxt/server/api/posts/[identifier]/[id].get.ts all read the stored note's published value, but this constructor never sets it. Every locally created post therefore comes back with published: null, so timestamps disappear from the timeline/detail page and the federated object has no publish time.

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

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.

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