Skip to content

Conversation

@willyogo
Copy link
Member

@willyogo willyogo commented Dec 30, 2025

Previously, the app used Mod Protocol and some custom logic to handle embedding content in casts. As a result, mini apps couldn't be embedded in casts and URLs weren't embedded properly in casts with opengraph metadata.

This PR leverages neynar's fetchEmbeddedUrlMetadata method to fetch URLs metadata (including mini app manifest) and properly structures the embed content according to @farcaster/core's publishMessageToFarcaster standard.

Last but not least, it makes the cast modal scrollable.

Summary

  • add Neynar-backed URL crawl and preview flow in CreateCast that normalizes embeds to Farcaster schema and lets users remove unwanted items
  • introduce shared embed normalization helpers and backend validation plus a new embedMetadata API endpoint to gate publish requests
  • extend shared embed cache and add regression tests covering embed sanitation and API validation

Testing

  • npm test -- --run tests/farcasterEmbeds.test.ts
  • npm run check-types
  • Manual: include websites and mini app URLs in casts to see the preview, and cast to confirm embeds render correctly in other clients (can also use neynar explorer to inspect all cast & embed metadata)

Mini app previews in casts:
image

URL preview in casts:
image

mini app embedded in cast on farcaster:
image


Codex Task

Summary by CodeRabbit

  • New Features

    • Live embed previews in cast creation with images, titles, descriptions, frames and video support; per-embed loading/error states, removal, and server-backed URL metadata endpoint.
    • Pre-publish embed validation that returns clear, structured errors when embeds are invalid or exceed limits.
  • Refactor

    • Centralized embed normalization, deduplication, URL resolution and metadata mapping; embed cache now stores per-URL metadata and enforces a size cap.
  • Bug Fixes

    • Modal content now has min/max height and vertical scrolling to avoid overflow.
  • Tests

    • Added tests for embed normalization, deduplication, limits and publish validation.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Dec 30, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
space-system Ready Ready Preview, Comment Dec 30, 2025 9:43pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 30, 2025

Warning

Rate limit exceeded

@willyogo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 9 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 0765d93 and 6e0e67a.

📒 Files selected for processing (9)
  • src/common/components/molecules/Modal.tsx
  • src/common/providers/SharedDataProvider.tsx
  • src/fidgets/farcaster/components/CreateCast.tsx
  • src/fidgets/farcaster/utils.ts
  • src/fidgets/farcaster/utils/embedNormalization.ts
  • src/fidgets/farcaster/utils/embedTypes.ts
  • src/pages/api/farcaster/neynar/embedMetadata.ts
  • src/pages/api/farcaster/neynar/publishMessage.ts
  • tests/farcasterEmbeds.test.ts
📝 Walkthrough

Walkthrough

Adds client-side embed previewing with per-URL metadata fetching/caching, Farcaster embed normalization and sanitization utilities, SharedDataProvider support for embed metadata, server endpoints for metadata proxying and publish-time embed validation, tests for embed logic, and a small modal layout height/scroll change.

Changes

Cohort / File(s) Summary
Shared Data Provider
src/common/providers/SharedDataProvider.tsx
Change recentEmbeds shape to Record<string, { embed: FarcasterEmbed; metadata?: EmbedUrlMetadata }>; update addRecentEmbed/getRecentEmbed signatures, add MAX_RECENT_EMBEDS eviction, and adjust internal state type.
CreateCast (embed previews)
src/fidgets/farcaster/components/CreateCast.tsx
Add editor embed preview state (previews, loading, errors, removed), per-URL metadata fetching with debouncing, caching via SharedDataProvider, normalization/deduplication into sanitized embeds, preview rendering (frame/video/link), per-embed removal, and use sanitized embeds when publishing.
Embed types & utils
src/fidgets/farcaster/utils.ts, src/fidgets/farcaster/utils/embedTypes.ts
Move Farcaster embed type definitions and isFarcasterUrlEmbed into embedTypes.ts; update imports to consume shared types and guards.
Embed normalization & metadata mapping
src/fidgets/farcaster/utils/embedNormalization.ts
New module: FARCASTER_EMBED_LIMIT, castId normalization, embed normalization/validation, deduplication/sanitization (sanitizeFarcasterEmbeds, validateFarcasterEmbeds), URL resolution from metadata, and mapping EmbedUrlMetadata to simplified URL metadata.
Server API: embed metadata proxy
src/pages/api/farcaster/neynar/embedMetadata.ts
New GET handler validating url param and calling neynar.fetchEmbeddedUrlMetadata, forwarding successful responses and Axios-aware errors.
Server API: publish validation
src/pages/api/farcaster/neynar/publishMessage.ts
Add pre-publish validation using validateFarcasterEmbeds; return 400 with details on invalids or over-limit; sanitize embeds before calling neynar.publishMessageToFarcaster.
Tests
tests/farcasterEmbeds.test.ts
New tests for sanitizeFarcasterEmbeds (dedupe/limit), validateFarcasterEmbeds (invalid detection), and publishMessage handler behavior for malformed embeds.
Modal layout
src/common/components/molecules/Modal.tsx
Adjust modal content container classes to relative min-h-[320px] max-h-[90vh] overflow-y-auto overflow-x-visible to enforce min/max height and enable vertical scrolling.

Sequence Diagram

sequenceDiagram
    participant User
    participant Editor as CreateCast (client)
    participant Shared as SharedDataProvider
    participant API as Next API /embedMetadata
    participant Neynar as Neynar Service
    participant PublishAPI as Next API /publishMessage

    User->>Editor: Type or paste text containing URL(s)
    Editor->>Editor: Debounce, extract URL(s)
    alt cache miss
        Editor->>API: GET /api/farcaster/neynar/embedMetadata?url=...
        API->>Neynar: fetchEmbeddedUrlMetadata(url)
        Neynar-->>API: metadata
        API-->>Editor: metadata
        Editor->>Shared: addRecentEmbed(url, embed, metadata)
    else cache hit
        Editor->>Shared: getRecentEmbed(url)
        Shared-->>Editor: {embed, metadata?}
    end
    Editor->>Editor: normalize/sanitize embeds, build previews, render embed cards

    User->>Editor: Click publish
    Editor->>PublishAPI: POST /api/farcaster/neynar/publishMessage (sanitized embeds)
    PublishAPI->>PublishAPI: validateFarcasterEmbeds -> sanitizeFarcasterEmbeds
    alt invalid or overLimit
        PublishAPI-->>Editor: 400 { message: "Invalid embeds", invalid: N, overLimit: true|false }
    else valid
        PublishAPI->>Neynar: publishMessageToFarcaster
        Neynar-->>PublishAPI: success
        PublishAPI-->>Editor: success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I sniffed some links and fetched their lore,
I cached their pictures and opened the door.
I hopped through duplicates, kept the best few,
Previews now sparkle — fresh metadata too.
Tiny rabbit cheers for tidy embeds! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Use Neynar metadata for cast embeds' accurately and concisely describes the main change: replacing custom embed logic with Neynar's metadata service for handling cast embeds.

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

❤️ Share

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

@willyogo willyogo marked this pull request as ready for review December 30, 2025 20:53
Copy link
Contributor

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
src/pages/api/farcaster/neynar/publishMessage.ts (1)

43-44: Incorrect status extraction from AxiosError.

AxiosError doesn't have a status property directly; it's on response. This could cause valid upstream error statuses to be lost.

Proposed fix
     if (isAxiosError(e)) {
-      res.status(e.status || 500).json(e.response?.data);
+      res.status(e.response?.status || 500).json(e.response?.data);
     } else {
🧹 Nitpick comments (4)
src/pages/api/farcaster/neynar/embedMetadata.ts (1)

6-9: Redundant method check.

The requestHandler wrapper already restricts this handler to GET requests (line 30-31) and returns a 405 for unsupported methods. The manual check on lines 7-9 is unreachable.

Proposed fix
 const embedMetadata = async (req: NextApiRequest, res: NextApiResponse) => {
-  if (req.method !== "GET") {
-    return res.status(405).json({ message: "Method not allowed" });
-  }
-
   const url = typeof req.query.url === "string" ? req.query.url : "";
src/pages/api/farcaster/neynar/publishMessage.ts (1)

18-34: Minor inefficiency: double validation.

sanitizeFarcasterEmbeds (line 33) internally calls validateFarcasterEmbeds again. Since validation already passed, you could use the normalized array directly:

Proposed optimization
     if (Array.isArray(message.embeds)) {
       const { normalized, invalid, overLimit } = validateFarcasterEmbeds(message.embeds);
       if (invalid.length || overLimit) {
         console.warn("Rejecting invalid Farcaster embeds", {
           invalidEmbeds: invalid,
           overLimit,
         });
         return res.status(400).json({
           message: "Invalid embeds",
           invalid: invalid.length,
           overLimit,
           limit: FARCASTER_EMBED_LIMIT,
         });
       }

-      message.embeds = sanitizeFarcasterEmbeds(message.embeds);
+      message.embeds = normalized.slice(0, FARCASTER_EMBED_LIMIT);
     }

Note: This loses deduplication. If deduplication is needed server-side, keep the current approach or extract a helper that accepts pre-normalized embeds.

src/common/providers/SharedDataProvider.tsx (1)

45-67: Consider adding cache size limits.

Unlike recentChannels which limits to 20 entries (line 54), recentEmbeds grows unbounded. For long-lived sessions with many URLs, this could cause memory growth.

Proposed fix
+const MAX_RECENT_EMBEDS = 50;
+
 const addRecentEmbed = useCallback(
   (url: string, embed: FarcasterEmbed, metadata?: EmbedUrlMetadata) => {
-    setRecentEmbeds((prev) => ({
-      ...prev,
-      [url]: { embed, metadata },
-    }));
+    setRecentEmbeds((prev) => {
+      const entries = Object.entries(prev);
+      const updated = { ...prev, [url]: { embed, metadata } };
+      if (entries.length >= MAX_RECENT_EMBEDS) {
+        // Remove oldest entry (first key)
+        const [oldestKey] = entries[0];
+        delete updated[oldestKey];
+      }
+      return updated;
+    });
   },
   [],
 );
src/fidgets/farcaster/components/CreateCast.tsx (1)

677-769: Potential stale closure issue with loadingEmbeds dependency.

The effect depends on loadingEmbeds, but inside the setTimeout callback you access the potentially stale loadingEmbeds value. While the current logic works because the check happens before the async operation starts, the dependency on loadingEmbeds can cause unnecessary effect re-runs.

Consider using a ref to track loading URLs to avoid effect re-triggering:

Alternative approach
+const loadingEmbedsRef = useRef<Set<string>>(new Set());
 const [loadingEmbeds, setLoadingEmbeds] = useState<Set<string>>(new Set());
+
+// Sync ref with state
+useEffect(() => {
+  loadingEmbedsRef.current = loadingEmbeds;
+}, [loadingEmbeds]);

 // In the effect, use loadingEmbedsRef.current for checks
-if (loadingEmbeds.has(normalizedUrl)) {
+if (loadingEmbedsRef.current.has(normalizedUrl)) {
   return;
 }

 // Remove loadingEmbeds from dependency array
-}, [text, embedPreviews, addRecentEmbed, getRecentEmbed, removedEmbeds, loadingEmbeds]);
+}, [text, embedPreviews, addRecentEmbed, getRecentEmbed, removedEmbeds]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0765d93 and f442e6c.

📒 Files selected for processing (8)
  • src/common/providers/SharedDataProvider.tsx
  • src/fidgets/farcaster/components/CreateCast.tsx
  • src/fidgets/farcaster/utils.ts
  • src/fidgets/farcaster/utils/embedNormalization.ts
  • src/fidgets/farcaster/utils/embedTypes.ts
  • src/pages/api/farcaster/neynar/embedMetadata.ts
  • src/pages/api/farcaster/neynar/publishMessage.ts
  • tests/farcasterEmbeds.test.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-29T18:25:25.957Z
Learnt from: j-paterson
Repo: blankdotspace/space-system PR: 1625
File: src/common/lib/utils/tokenGates.ts:12-20
Timestamp: 2025-12-29T18:25:25.957Z
Learning: Gate tokens in src/common/lib/utils/tokenGates.ts are currently fetched redundantly from 3 client components (CreateCast.tsx, BackgroundGenerator.tsx, NogsGateButton.tsx) with no caching, and should be refactored to use a cached API route with a shared client hook to reduce redundant server calls.

Applied to files:

  • src/fidgets/farcaster/components/CreateCast.tsx
🧬 Code graph analysis (5)
src/pages/api/farcaster/neynar/publishMessage.ts (1)
src/fidgets/farcaster/utils/embedNormalization.ts (3)
  • validateFarcasterEmbeds (87-105)
  • FARCASTER_EMBED_LIMIT (8-8)
  • sanitizeFarcasterEmbeds (107-132)
src/pages/api/farcaster/neynar/embedMetadata.ts (1)
src/common/data/api/requestHandler.ts (1)
  • requestHandler (31-51)
src/common/providers/SharedDataProvider.tsx (1)
src/fidgets/farcaster/utils/embedTypes.ts (1)
  • FarcasterEmbed (12-12)
tests/farcasterEmbeds.test.ts (1)
src/fidgets/farcaster/utils/embedNormalization.ts (3)
  • sanitizeFarcasterEmbeds (107-132)
  • FARCASTER_EMBED_LIMIT (8-8)
  • validateFarcasterEmbeds (87-105)
src/fidgets/farcaster/utils/embedNormalization.ts (2)
src/fidgets/farcaster/utils/embedTypes.ts (2)
  • FarcasterEmbed (12-12)
  • isFarcasterUrlEmbed (14-18)
src/common/lib/utils/urls.ts (1)
  • isWebUrl (22-28)
🔇 Additional comments (19)
src/fidgets/farcaster/utils/embedNormalization.ts (6)

10-42: LGTM!

The normalizeCastId function correctly validates the cast ID structure, handling both Uint8Array and hex string hash formats with proper length validation (20 bytes for Farcaster cast hashes).


44-67: LGTM!

The normalizeEmbed function handles multiple input variations (castId/cast_id, url/uri) and includes defensive URL normalization with a safe fallback.


69-85: LGTM!

The key generation functions create deterministic, unique keys for deduplication purposes.


87-105: LGTM!

The validation function properly categorizes embeds and provides structured feedback for error handling.


107-132: LGTM!

The sanitization function correctly combines validation, deduplication, removal filtering, and limit enforcement in a single pass.


134-187: LGTM!

The metadata resolution and mapping functions properly handle optional fields with appropriate fallbacks and type safety.

tests/farcasterEmbeds.test.ts (3)

12-25: LGTM!

Good test coverage for deduplication and limit enforcement. The test correctly verifies that the first occurrence is kept and subsequent duplicates are removed.


27-35: LGTM!

Good edge case test verifying that invalid hash lengths are correctly identified.


38-64: LGTM!

Good integration test for the validation rejection flow.

Consider adding tests for: embeds exceeding the limit, and valid embeds passing through successfully.

src/fidgets/farcaster/utils/embedTypes.ts (1)

1-18: LGTM!

Clean type definitions with a proper discriminated union and type guard. The hash as Uint8Array aligns with Farcaster core types.

src/fidgets/farcaster/utils.ts (1)

30-30: LGTM!

Clean refactor extracting embed types to a dedicated module for better reusability.

src/common/providers/SharedDataProvider.tsx (1)

23-25: LGTM!

The updated context types correctly reflect the new cache structure storing both embed and metadata.

src/fidgets/farcaster/components/CreateCast.tsx (7)

150-162: LGTM!

Simple and effective URL extraction utilities with appropriate defensive coding.


193-197: LGTM!

Well-structured state management for the embed preview lifecycle (loading, success, error, removal).


475-486: LGTM!

Clean integration with the editor's metadata fetching hook.


585-591: LGTM!

The embed prioritization order (previews → editor → initial) ensures newly added embeds take precedence, with proper deduplication via sanitizeFarcasterEmbeds.


663-675: LGTM!

Clean removal handler with proper immutable state updates.


1259-1325: LGTM!

Well-structured preview UI with appropriate loading states, error handling, and dismissal functionality.


635-644: The application already has Next.js image configuration that permits arbitrary external domains. The next.config.js file includes remotePatterns: [{ protocol: "https", hostname: "**" }], which allows any HTTPS image URL, including those from og:image metadata. No configuration changes are needed.

Copy link
Contributor

@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: 1

🧹 Nitpick comments (4)
src/fidgets/farcaster/components/CreateCast.tsx (4)

476-487: Consider enhancing error details for debugging.

The generic error message "Failed to fetch embed metadata" could include the HTTP status and URL to aid troubleshooting.

🔎 Enhanced error message
 const fetchMetadataForEditor = useCallback(async (url: string) => {
   const response = await fetch(
     `/api/farcaster/neynar/embedMetadata?url=${encodeURIComponent(url)}`,
   );

   if (!response.ok) {
-    throw new Error("Failed to fetch embed metadata");
+    throw new Error(`Failed to fetch embed metadata for ${url}: ${response.status} ${response.statusText}`);
   }

   const payload = await response.json();
   return mapEmbedMetadataToUrlMetadata(payload?.metadata);
 }, []);

682-774: Embed fetching logic is functional but has minor race condition risk.

The effect correctly extracts URLs, checks multiple conditions before fetching, and uses a 500ms debounce for UX. However, if the component unmounts or text changes during an in-flight fetch, the subsequent setEmbedPreviews or setEmbedErrors calls will attempt to update unmounted state.

Consider using an AbortController and cleanup to cancel in-flight fetches:

🔎 Add fetch cancellation
 useEffect(() => {
   const urlsInText = extractUrlsFromText(text || "");
   const timers: Array<ReturnType<typeof setTimeout>> = [];
+  const controllers: AbortController[] = [];

   urlsInText.forEach((rawUrl) => {
     // ... existing checks ...

     const timer = setTimeout(() => {
+      const controller = new AbortController();
+      controllers.push(controller);
+
       setLoadingEmbeds((prev) => new Set(prev).add(normalizedUrl));
       setEmbedErrors((prev) => {
         const next = { ...prev };
         delete next[normalizedUrl];
         return next;
       });

-      fetch(`/api/farcaster/neynar/embedMetadata?url=${encodeURIComponent(normalizedUrl)}`)
+      fetch(`/api/farcaster/neynar/embedMetadata?url=${encodeURIComponent(normalizedUrl)}`, {
+        signal: controller.signal
+      })
         .then(async (res) => {
           // ... existing logic ...
         })
         .catch((error) => {
+          if (error.name === 'AbortError') return;
           setEmbedErrors((prev) => ({ ...prev, [normalizedUrl]: error.message }));
         })
         .finally(() => {
           setLoadingEmbeds((prev) => {
             const next = new Set(prev);
             next.delete(normalizedUrl);
             return next;
           });
         });
     }, 500);

     timers.push(timer);
   });

   return () => {
     timers.forEach((timer) => clearTimeout(timer));
+    controllers.forEach((controller) => controller.abort());
   };
 }, [text, embedPreviews, addRecentEmbed, getRecentEmbed, removedEmbeds, loadingEmbeds]);

Additionally, Line 690-692: URL normalization silently keeps the raw value on failure. Consider whether invalid URLs should be skipped entirely rather than processed.


803-803: Cache check logic is semantically unclear.

The condition !mentionFidCache.has(username) || !mentionFidCache.get(username) checks both key existence and value truthiness. If a FID could legitimately be 0 (or any falsy value), it would be incorrectly treated as uncached. While Farcaster FIDs start from 1, making this safe in practice, the intent would be clearer with just !mentionFidCache.has(username).

🔎 Clearer cache check
 const uncachedUsernames = uniqueUsernames.filter(
-  (username) => !mentionFidCache.has(username) || !mentionFidCache.get(username),
+  (username) => !mentionFidCache.has(username),
 );

1264-1331: Embed preview UI is functional but missing accessibility labels.

The UI correctly shows loading states, dismissible errors, and embed previews with remove controls. However, the remove buttons (lines 1305-1311 and 1319-1325) should have aria-label attributes for screen reader users, as the "✕" symbol alone doesn't convey the action.

🔎 Add accessibility labels
 <button
   type="button"
   className="absolute right-3 top-3 z-10 rounded-full bg-white/90 px-2 py-1 text-xs font-semibold text-gray-700 shadow hover:bg-white"
   onClick={() => handleRemoveEmbed(item.embed)}
+  aria-label="Remove embed"
 >
   ✕
 </button>

Apply the same change to the button at lines 1319-1325.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57d9c9b and 3450d5e.

📒 Files selected for processing (1)
  • src/fidgets/farcaster/components/CreateCast.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-29T18:25:25.957Z
Learnt from: j-paterson
Repo: blankdotspace/space-system PR: 1625
File: src/common/lib/utils/tokenGates.ts:12-20
Timestamp: 2025-12-29T18:25:25.957Z
Learning: Gate tokens in src/common/lib/utils/tokenGates.ts are currently fetched redundantly from 3 client components (CreateCast.tsx, BackgroundGenerator.tsx, NogsGateButton.tsx) with no caching, and should be refactored to use a cached API route with a shared client hook to reduce redundant server calls.

Applied to files:

  • src/fidgets/farcaster/components/CreateCast.tsx
🧬 Code graph analysis (1)
src/fidgets/farcaster/components/CreateCast.tsx (4)
src/fidgets/farcaster/utils/embedTypes.ts (3)
  • FarcasterEmbed (12-12)
  • FarcasterCastIdEmbed (5-10)
  • isFarcasterUrlEmbed (14-18)
src/common/providers/SharedDataProvider.tsx (1)
  • useSharedData (115-123)
src/fidgets/farcaster/utils/embedNormalization.ts (3)
  • mapEmbedMetadataToUrlMetadata (174-187)
  • sanitizeFarcasterEmbeds (107-132)
  • createEmbedKey (74-85)
src/common/lib/utils/urls.ts (2)
  • isImageUrl (1-20)
  • isWebUrl (22-28)
🔇 Additional comments (6)
src/fidgets/farcaster/components/CreateCast.tsx (6)

194-198: Well-structured embed state management.

The state design effectively tracks embed lifecycle with appropriate data structures: Records for keyed access to previews/errors, and Sets for efficient membership checks on loading/removed states. The integration with SharedDataProvider enables cross-component caching.


558-606: Solid embed normalization and sanitization flow.

The multi-step process correctly:

  1. Normalizes Mod Protocol embeds to Farcaster schema
  2. Combines embeds from previews, editor, and initial state
  3. Applies deduplication, removal filtering, and limit enforcement via sanitizeFarcasterEmbeds
  4. Enriches with metadata for rendering

The memoization dependencies are accurate and the prioritization (previews + editor embeds before initialEmbeds) is logical.


608-666: Comprehensive embed rendering with proper fallbacks.

The function correctly handles cast embeds, images, videos, and link previews with appropriate metadata extraction. The Next.js Image component is properly configured with fill and sizes attributes.

Consider adding error boundaries or fallback handling if image loading fails or metadata is malformed, but this is not critical for the current implementation.


668-680: LGTM: Removal logic is correct.

The function properly tracks removed embeds and clears preview state. Note that once removed, the same URL won't auto-preview again unless removedEmbeds is cleared—this appears to be intentional UX to prevent annoying re-previews.


868-868: Correct usage of sanitizedEmbeds.

The mentions effect now uses sanitizedEmbeds which properly combines and deduplicates embeds from all sources (previews, editor, initial state) with user removals applied. This aligns with the PR's goal of normalizing embeds to the Farcaster schema.


900-900: LGTM: Correct embed prioritization for publish.

The logic sanitizedEmbeds.length ? sanitizedEmbeds : draft.embeds || [] appropriately prioritizes the normalized/sanitized embeds from the new preview system while falling back to draft embeds for compatibility.

Comment on lines +151 to +154
const extractUrlsFromText = (text: string): string[] => {
const urlRegex = /(https?:\/\/[^\s]+)/gi;
return Array.from(new Set(text.match(urlRegex) || []));
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

URL regex captures trailing punctuation.

The pattern /(https?:\/\/[^\s]+)/gi will include trailing punctuation like ., ,, ), ! that are often not part of the URL. For example, "Check out https://example.com." will capture "https://example.com." with the period.

🔎 Improved URL extraction that handles common punctuation
 const extractUrlsFromText = (text: string): string[] => {
-  const urlRegex = /(https?:\/\/[^\s]+)/gi;
+  const urlRegex = /(https?:\/\/[^\s]+?)(?=[.,;:!?)}\]]*(?:\s|$))/gi;
   return Array.from(new Set(text.match(urlRegex) || []));
 };

This pattern uses a positive lookahead to stop before trailing punctuation that's followed by whitespace or end of string.

📝 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 extractUrlsFromText = (text: string): string[] => {
const urlRegex = /(https?:\/\/[^\s]+)/gi;
return Array.from(new Set(text.match(urlRegex) || []));
};
const extractUrlsFromText = (text: string): string[] => {
const urlRegex = /(https?:\/\/[^\s]+?)(?=[.,;:!?)}\]]*(?:\s|$))/gi;
return Array.from(new Set(text.match(urlRegex) || []));
};
🤖 Prompt for AI Agents
In src/fidgets/farcaster/components/CreateCast.tsx around lines 151 to 154, the
current URL extractor regex captures trailing punctuation (e.g. periods, commas,
closing parens) so URLs like "https://example.com." include the dot; replace the
regex with one that excludes common trailing punctuation in the character class
and uses a positive lookahead to assert the URL ends before whitespace or
end-of-string (i.e. stop before .,;:!?)]}<> or quotes), then return unique
matches as before.

@willyogo
Copy link
Member Author

@CodeRabbit full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 30, 2025

✅ Actions performed

Full review triggered.

@willyogo willyogo added the LGFTP Looks Good From Testing Perspective label Dec 30, 2025
@willyogo willyogo requested a review from j-paterson December 30, 2025 21:41
Copy link
Contributor

@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: 0

♻️ Duplicate comments (1)
src/fidgets/farcaster/components/CreateCast.tsx (1)

151-154: URL regex captures trailing punctuation.

This is a known issue from a previous review. The pattern /(https?:\/\/[^\s]+)/gi will include trailing punctuation like ., ,, ), ! that are often not part of the URL. For example, "Check out https://example.com." will capture "https://example.com." with the period.

🧹 Nitpick comments (4)
src/fidgets/farcaster/components/CreateCast.tsx (3)

687-779: Consider cancellation for in-flight requests when component unmounts.

The effect clears pending timers on cleanup, but doesn't abort fetch requests that have already started. If the component unmounts while a fetch is in progress, state updates will be attempted on an unmounted component.

🔎 Add AbortController for fetch cancellation
   useEffect(() => {
     const urlsInText = extractUrlsFromText(text || "");
     const timers: Array<ReturnType<typeof setTimeout>> = [];
+    const abortController = new AbortController();

     urlsInText.forEach((rawUrl) => {
       // ... existing validation logic ...

       const timer = setTimeout(() => {
         setLoadingEmbeds((prev) => new Set(prev).add(normalizedUrl));
         setEmbedErrors((prev) => {
           const next = { ...prev };
           delete next[normalizedUrl];
           return next;
         });

-        fetch(`/api/farcaster/neynar/embedMetadata?url=${encodeURIComponent(normalizedUrl)}`)
+        fetch(`/api/farcaster/neynar/embedMetadata?url=${encodeURIComponent(normalizedUrl)}`, {
+          signal: abortController.signal,
+        })
           .then(async (res) => {
             // ... existing success logic ...
           })
           .catch((error) => {
+            if (error.name === 'AbortError') return;
             setEmbedErrors((prev) => ({ ...prev, [normalizedUrl]: error.message }));
           })
           .finally(() => {
+            if (abortController.signal.aborted) return;
             setLoadingEmbeds((prev) => {
               const next = new Set(prev);
               next.delete(normalizedUrl);
               return next;
             });
           });
       }, 500);

       timers.push(timer);
     });

     return () => {
       timers.forEach((timer) => clearTimeout(timer));
+      abortController.abort();
     };
   }, [text, embedPreviews, addRecentEmbed, getRecentEmbed, removedEmbeds]);

733-771: 500ms fixed delay per URL may cause user-perceived lag for fast typing.

Each URL gets a 500ms delay before fetching. While this provides debouncing, it adds noticeable latency. Consider if a shorter delay (e.g., 300ms) or a single debounced effect might improve UX without overwhelming the API.


375-395: Ref .current in dependency array won't trigger effect re-runs.

editorContentRef.current in the dependency array won't cause the effect to re-run when the ref changes because mutating .current doesn't trigger a re-render. This effect only runs once on mount, which may be the intended behavior, but the dependency is misleading.

🔎 Fix the dependency array
     return () => {
       el.removeEventListener("paste", handler as any);
     };
-  }, [editorContentRef.current]);
+  }, []); // Effect should only run once on mount
src/common/providers/SharedDataProvider.tsx (1)

61-76: Cache eviction assumes insertion order is preserved in object keys.

The eviction logic uses Object.keys(updated)[0] to find the "oldest" entry. While modern JavaScript engines preserve insertion order for string keys, this is a subtle assumption. Also, the spread { ...prev, [url]: ... } moves an existing key to the end only in some cases.

Consider using a Map instead of a plain object if strict LRU behavior is desired, as Map guarantees insertion order and makes eviction semantics clearer. For the current use case where approximate eviction is acceptable, the implementation works fine.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3450d5e and 6e0e67a.

📒 Files selected for processing (5)
  • src/common/providers/SharedDataProvider.tsx
  • src/fidgets/farcaster/components/CreateCast.tsx
  • src/fidgets/farcaster/utils/embedNormalization.ts
  • src/pages/api/farcaster/neynar/embedMetadata.ts
  • src/pages/api/farcaster/neynar/publishMessage.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/pages/api/farcaster/neynar/publishMessage.ts
  • src/pages/api/farcaster/neynar/embedMetadata.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-29T18:25:25.957Z
Learnt from: j-paterson
Repo: blankdotspace/space-system PR: 1625
File: src/common/lib/utils/tokenGates.ts:12-20
Timestamp: 2025-12-29T18:25:25.957Z
Learning: Gate tokens in src/common/lib/utils/tokenGates.ts are currently fetched redundantly from 3 client components (CreateCast.tsx, BackgroundGenerator.tsx, NogsGateButton.tsx) with no caching, and should be refactored to use a cached API route with a shared client hook to reduce redundant server calls.

Applied to files:

  • src/fidgets/farcaster/components/CreateCast.tsx
🧬 Code graph analysis (3)
src/fidgets/farcaster/components/CreateCast.tsx (5)
src/fidgets/farcaster/utils/embedTypes.ts (3)
  • FarcasterEmbed (12-12)
  • FarcasterCastIdEmbed (5-10)
  • isFarcasterUrlEmbed (14-18)
src/common/providers/SharedDataProvider.tsx (1)
  • useSharedData (124-132)
src/fidgets/farcaster/utils/embedNormalization.ts (3)
  • mapEmbedMetadataToUrlMetadata (180-193)
  • sanitizeFarcasterEmbeds (113-138)
  • createEmbedKey (74-85)
src/common/lib/utils/urls.ts (2)
  • isImageUrl (1-20)
  • isWebUrl (22-28)
src/common/components/atoms/spinner.tsx (1)
  • Spinner (12-27)
src/common/providers/SharedDataProvider.tsx (1)
src/fidgets/farcaster/utils/embedTypes.ts (1)
  • FarcasterEmbed (12-12)
src/fidgets/farcaster/utils/embedNormalization.ts (2)
src/fidgets/farcaster/utils/embedTypes.ts (2)
  • FarcasterEmbed (12-12)
  • isFarcasterUrlEmbed (14-18)
src/common/lib/utils/urls.ts (1)
  • isWebUrl (22-28)
🔇 Additional comments (16)
src/fidgets/farcaster/utils/embedNormalization.ts (7)

10-42: LGTM! Robust cast ID normalization with proper validation.

The function handles multiple input formats (Uint8Array and hex strings with/without 0x prefix), validates fid is a finite non-negative number, and correctly validates the hash length to 20 bytes as required by Farcaster.


44-67: LGTM! Flexible embed normalization supporting multiple property names.

The function correctly handles both castId/cast_id and url/uri property variants, uses isWebUrl for validation, and safely normalizes URLs through the URL constructor with fallback.


69-85: LGTM! Clean implementation of embed key generation.

The toHexString helper correctly converts bytes to hex, and createEmbedKey produces unique, deterministic keys for deduplication. URL normalization via URL constructor ensures consistent keys.


87-105: LGTM! Validation function with good separation of valid and invalid embeds.

Returns both normalized embeds and invalid entries with their indices, which is useful for error reporting. The overLimit flag enables callers to detect when the embed count exceeds the limit.


113-138: LGTM! Sanitization correctly handles deduplication and limit enforcement.

The function efficiently deduplicates embeds using Set, respects the removed keys, and enforces the limit. The early break optimization prevents unnecessary iterations.


140-164: LGTM! Sensible priority order for URL resolution.

Prioritizes frame URL, then Open Graph URL, then oEmbed URL, and finally falls back to the source URL. Each candidate is validated with isWebUrl before acceptance.


166-193: LGTM! Clean metadata mapping with safe number coercion.

The pickOgImage helper safely handles missing values and uses Number.isFinite to validate dimensions. The mapEmbedMetadataToUrlMetadata function correctly maps Neynar metadata to the expected schema.

src/fidgets/farcaster/components/CreateCast.tsx (7)

194-203: LGTM! Well-structured state management for embed previews.

The use of loadingEmbedsRef alongside loadingEmbeds state is a good pattern to avoid stale closures in async callbacks. The separation of concerns (previews, removed, loading, errors) is clean.


481-492: LGTM! Clean metadata fetching for the editor.

The function properly encodes the URL and handles errors appropriately. The mapping through mapEmbedMetadataToUrlMetadata ensures consistent metadata structure.


563-597: LGTM! Correct embed normalization and sanitization flow.

The normalizeEditorEmbeds correctly converts ModEmbed to FarcasterEmbed. The sanitizedEmbeds memo properly prioritizes preview embeds over editor embeds over initial embeds, and applies the removal filter.


613-671: LGTM! Comprehensive preview rendering with proper type handling.

The renderPreviewForEmbed function correctly handles cast embeds, images, videos, and link previews. The fallback to URL for title when metadata is missing is sensible.


873-873: Verify sanitizedEmbeds is correctly used in draft state.

The draft is updated with sanitizedEmbeds which is derived from previewEmbeds, normalizedEditorEmbeds, and initialEmbeds. This should work correctly, but ensure that the order of precedence (preview → editor → initial) matches the intended behavior.


905-910: LGTM! Correct fallback for embeds in publish flow.

The logic correctly uses sanitizedEmbeds when available, falling back to draft.embeds otherwise. This ensures embeds are always present for submission.


1269-1335: LGTM! Good UX with loading states, error handling, and remove functionality.

The embed preview section properly shows loading indicators per URL, dismissible error messages, and remove buttons for each embed. The conditional rendering based on frame metadata is correct.

src/common/providers/SharedDataProvider.tsx (2)

10-16: LGTM! Clean type-only imports and updated type sources.

The imports correctly use type keyword for type-only imports, and the FarcasterEmbed type is now sourced from the new embedTypes module.


47-49: LGTM! Metadata-aware caching with bounded size.

The updated recentEmbeds structure correctly stores both embed and optional metadata. The MAX_RECENT_EMBEDS limit prevents unbounded memory growth.

Also applies to: 61-76

@sktbrd
Copy link
Collaborator

sktbrd commented Dec 31, 2025

image

its trying to convert urls without https:// to http websites

@willyogo willyogo marked this pull request as draft December 31, 2025 20:45
@sktbrd
Copy link
Collaborator

sktbrd commented Dec 31, 2025

image

taggin failed with multiple embeds

I also published the same caste twice because I double clicked the publish button

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

Labels

codex LGFTP Looks Good From Testing Perspective

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants