Skip to content

Search results image summary#604

Merged
kentcdodds merged 5 commits intomainfrom
cursor/search-results-image-summary-56d5
Feb 19, 2026
Merged

Search results image summary#604
kentcdodds merged 5 commits intomainfrom
cursor/search-results-image-summary-56d5

Conversation

@kentcdodds
Copy link
Owner

@kentcdodds kentcdodds commented Feb 19, 2026

Add image thumbnails and content summaries to semantic search results to improve their presentation across all consumers.

The summary, imageUrl, and imageAlt fields are dynamically derived at query time from existing content metadata (MDX frontmatter, YAML) and URL patterns, avoiding the need for re-indexing. Fallbacks are provided for content without explicit metadata.


Open in Cursor Open in Web


Note

Medium Risk
Introduces new server-side enrichment that reads repo content/YAML at query time, which could affect performance and result consistency across environments, though changes are additive and include fallbacks.

Overview
Semantic search results are now enriched at query time with summary, imageUrl, and imageAlt, derived from local repo metadata (MDX frontmatter and YAML data) plus type-based fallbacks, and surfaced through semanticSearchKCD, the /resources/search suggestions endpoint, and the MCP find_content tool.

Search UIs (navbar autocomplete and /search page) are updated to render thumbnails, display the new summaries (with truncation for small payloads), and handle invalid suggestion URLs more safely when displaying the path. Test scripts are tweaked to use vitest --run by default and add a test:watch script.

Written by Cursor Bugbot for commit e33068c. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

Release Notes

  • New Features

    • Search results now display optional images and summaries alongside content for richer context.
    • Enhanced search suggestions with support for images and summary text.
  • Improvements

    • Redesigned search results layout with images and restructured content details for better readability.
    • Added inline summary or snippet display within search results for improved content discovery.

cursoragent and others added 5 commits February 19, 2026 03:37
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor
Copy link

cursor bot commented Feb 19, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

This PR extends semantic search results with enriched metadata (summary, imageUrl, imageAlt). A new presentation module generates type-specific metadata for various document types (blog, page, talk, etc.). Results are enriched via concurrent post-processing. UI components updated to display images and summaries. Test scripts configured for explicit run mode.

Changes

Cohort / File(s) Summary
Type & API Extensions
app/components/navbar.tsx, app/utils/semantic-search.server.ts
Added optional fields (summary, imageUrl, imageAlt) to SearchSuggestion and SemanticSearchResult types for metadata enrichment.
Result Enrichment Pipeline
app/routes/mcp+/mcp.server.ts, app/routes/resources+/search.ts, app/utils/semantic-search.server.ts
Added new fields (summary, imageUrl, imageAlt) to result mappings. Introduced normalizeSummary function for text sanitization. Enhanced semantic search post-processing with concurrent result enrichment via getSemanticSearchPresentation.
Presentation Logic
app/utils/semantic-search-presentation.server.ts
New server module generating enriched semantic search presentation data. Handles MDX frontmatter parsing, YAML data loading, Cloudinary image generation, and type-specific metadata enrichment for blog, page, talk, credit, testimonial, and resume documents. Includes caching, slug inference, and fallback handling.
UI Rendering
app/search.tsx
Restructured search result display from two-column layout to nested image + details layout. Conditionally renders images or placeholders, titles as links, type/URL details, and summary/snippet text inline.
Build Configuration
package.json
Updated test script to use vitest --run for explicit run mode. Added new test:watch script for watch-mode testing.

Sequence Diagram

sequenceDiagram
    actor Client
    participant NavSearch as NavSearch Component
    participant SemanticSearch as semanticSearchKCD
    participant PresentationModule as getSemanticSearchPresentation
    participant FileSystem as MDX/YAML Files
    participant Cloudinary as Cloudinary Service

    Client->>NavSearch: Trigger search
    NavSearch->>SemanticSearch: Execute semantic search
    SemanticSearch->>SemanticSearch: Rank & slice results
    SemanticSearch->>PresentationModule: Enrich each result (concurrent)
    
    par Enrichment for each result
        PresentationModule->>FileSystem: Read MDX/YAML (cached)
        FileSystem-->>PresentationModule: Metadata (title, description, banner)
        PresentationModule->>Cloudinary: Generate thumbnail URLs
        Cloudinary-->>PresentationModule: Image URLs
        PresentationModule-->>PresentationModule: Merge base + enriched data
    end
    
    PresentationModule-->>SemanticSearch: Enriched results
    SemanticSearch-->>NavSearch: Results with summary, imageUrl, imageAlt
    NavSearch->>Client: Display search suggestions with images & summaries
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hops through the search with gleaming pride,
Images and summaries side by side!
From MDX to YAML, cached with care,
Cloudinary thumbnails float in the air.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'Search results image summary' accurately reflects the main change: adding image thumbnails and summaries to search results across the application.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/search-results-image-summary-56d5

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.

@kentcdodds kentcdodds marked this pull request as ready for review February 19, 2026 04:16
Copy link

@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 (9)
app/components/navbar.tsx (2)

543-543: Placeholder uses hardcoded gray colors instead of semantic theme tokens.

The rest of the app uses semantic classes like bg-primary, bg-secondary, etc. This hardcoded bg-gray-200 dark:bg-gray-700 will not adapt if the theme palette changes. Consider using a semantic token or at least a shared utility class. The same pattern appears in app/routes/search.tsx line 195.

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

In `@app/components/navbar.tsx` at line 543, The placeholder div in the Navbar
component uses hardcoded colors ("bg-gray-200 dark:bg-gray-700"); replace that
with a semantic theme token or shared utility class (for example use the app's
semantic class like "bg-secondary" or a shared "placeholder-bg" utility) so it
follows the app-wide palette and adapts to theme changes; apply the same
replacement in the similar placeholder in app/routes/search.tsx (the two JSX
placeholder divs) to ensure consistency across components.

553-559: Consider extracting the IIFE to a small helper or computing it before the JSX.

An inline IIFE in JSX is functional but slightly harder to scan. A small helper like getDisplayPath(url: string) or a const before the return would improve readability.

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

In `@app/components/navbar.tsx` around lines 553 - 559, Extract the inline IIFE
that computes the display path from s.url into a small helper and use it in the
JSX: create a function like getDisplayPath(url: string) (or compute const
displayPath = getDisplayPath(s.url) just before the component's return) which
returns new URL(url).pathname or falls back to the original url on error, then
replace the IIFE in the JSX with that helper/const; reference the existing s.url
and the new getDisplayPath/displayPath to locate and replace the logic in the
Navbar component.
app/routes/resources+/search.ts (1)

8-14: normalizeSummary duplicates the normalize + truncate logic from semantic-search-presentation.server.ts.

The presentation layer already truncates summary to 220 chars. This function is still useful as a safety net for the r.snippet fallback path, but the shared constants (220 / 217) and logic could be extracted to avoid drift.

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

In `@app/routes/resources`+/search.ts around lines 8 - 14, normalizeSummary
duplicates truncation constants and logic used in
semantic-search-presentation.server.ts; extract the shared truncate/normalize
logic and constants (e.g., MAX_SUMMARY_LENGTH = 220 and TRUNCATE_SLICE = 217 or
a single truncate function) into a common helper used by both normalizeSummary
in app/routes/resources+/search.ts and the presentation function in
semantic-search-presentation.server.ts so both code paths use the same
normalization + truncation implementation (keep normalizeSummary as a thin
wrapper that calls the shared helper to preserve the r.snippet safety net).
app/utils/semantic-search-presentation.server.ts (5)

221-257: mdxMetaCache is an unbounded, never-evicted module-level Map.

Every unique (type, slug) pair that goes through getMdxDocMeta is cached forever for the lifetime of the process. For a site with hundreds of blog posts, this will accumulate (though each entry is small). More importantly, cached data is never refreshed — if MDX frontmatter is updated, the stale value persists until restart.

Consider using an LRU cache (you already depend on lru-cache) with a reasonable max size and/or TTL.

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

In `@app/utils/semantic-search-presentation.server.ts` around lines 221 - 257, The
module-level mdxMetaCache Map used by getMdxDocMeta is unbounded and never
refreshed; replace it with an lru-cache instance (imported from 'lru-cache')
configured with a sensible max entries and optional ttl so entries expire, then
use that cache’s get/set methods instead of mdxMetaCache.get and
mdxMetaCache.set; ensure the cache key remains `${type}:${slug}` and keep the
try/catch flow the same so updates to MDX frontmatter are picked up after TTL or
eviction.

24-31: ResultLike is used in the exported function signature but not exported itself.

Callers of getSemanticSearchPresentation cannot reference this type by name. If any consumer needs to construct a ResultLike explicitly, they'll need to duplicate the shape. Consider exporting it alongside SemanticSearchPresentation.

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

In `@app/utils/semantic-search-presentation.server.ts` around lines 24 - 31, The
type alias ResultLike is referenced in the exported function
getSemanticSearchPresentation but is not exported, preventing callers from using
the type; export ResultLike (e.g., add export to the ResultLike declaration) so
consumers can import it alongside SemanticSearchPresentation and avoid
duplicating the shape — update the declaration of ResultLike and keep the
existing names used by getSemanticSearchPresentation and
SemanticSearchPresentation.

260-284: YAML-backed caches (talksBySlug, creditsBySlug, testimonialsBySlug, resumeMeta) are loaded once and never invalidated.

These module-level singletons will serve stale data if the underlying YAML files change without a server restart. This may be acceptable if the deployment model always restarts on content changes, but it's worth calling out. The same LRU/TTL approach or a simple timestamp-based revalidation would add resilience.

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

In `@app/utils/semantic-search-presentation.server.ts` around lines 260 - 284, The
module-level caches (talksBySlug and similar) are never invalidated, causing
stale data; update loadTalksBySlug to detect changes and reload: keep metadata
alongside the cached Map (e.g., lastLoaded timestamp or the source file mtime)
and in loadTalksBySlug call fs.stat on the YAML file to compare mtime or check
TTL before returning the cached Map; if the file changed or TTL expired, re-read
and reparse the file and update the cache. Apply the same pattern to
creditsBySlug, testimonialsBySlug, and resumeMeta (or centralize into a small
cache helper) so all YAML-backed singletons support revalidation.

73-135: Repetitive fallback image construction — consider a lookup table.

Each branch in getFallbackPresentation follows the same pattern: pick an image preset, call it with identical options, and return { imageUrl, imageAlt }. A data-driven lookup would reduce duplication and make adding new types trivial.

Example data-driven approach
+const FALLBACK_IMAGE_MAP: Record<string, { image: typeof images.microphone; resize: 'pad' | 'fill' }> = {
+  ck: { image: images.microphone, resize: 'pad' },
+  cwk: { image: images.kayak, resize: 'pad' },
+  podcast: { image: images.microphone, resize: 'pad' },
+  talk: { image: images.kentSpeakingAllThingsOpen, resize: 'fill' },
+  resume: { image: images.kentProfile, resize: 'fill' },
+}
+
 function getFallbackPresentation(type: string | undefined) {
-  if (type === 'ck') {
-    ...
-  }
-  ...
+  const entry = type ? FALLBACK_IMAGE_MAP[type] : undefined
+  const img = entry?.image ?? images.kodyProfileGray
+  const resize = entry?.resize ?? 'pad'
+  return {
+    imageUrl: img({ quality: 'auto', format: 'auto', resize: { type: resize, width: 96, height: 96 } }),
+    imageAlt: img.alt,
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/semantic-search-presentation.server.ts` around lines 73 - 135,
Refactor getFallbackPresentation to remove repetitive branches by creating a
lookup map from the type keys ('ck', 'cwk', 'podcast', 'talk', 'resume',
default) to the corresponding image factory (e.g., images.microphone,
images.kayak, images.kentSpeakingAllThingsOpen, images.kentProfile,
images.kodyProfileGray) and alt values, then call the selected image factory
once with the shared options ({ quality: 'auto', format: 'auto', resize: { ... }
}) and return { imageUrl, imageAlt }; keep the existing resize types (pad vs
fill) per entry in the map so behavior stays identical and fall back to
images.kodyProfileGray for unknown types.

43-47: asNonEmptyString is duplicated verbatim in app/utils/semantic-search.server.ts (line 20).

Extract to a shared utility to avoid drift between the two copies.

Shared utility approach

Create a small shared helper (e.g., in app/utils/misc.tsx or a new app/utils/string-helpers.ts) and import it from both files:

export function asNonEmptyString(value: unknown): string | undefined {
  if (typeof value !== 'string') return undefined
  const trimmed = value.trim()
  return trimmed || undefined
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/semantic-search-presentation.server.ts` around lines 43 - 47,
Duplicate function asNonEmptyString exists in two modules; extract it into a
single shared utility (e.g., export function asNonEmptyString(...) in a new
string-helpers module), remove the local copies from both consumers (the modules
that currently define asNonEmptyString), and import the shared asNonEmptyString
from that utility wherever it was used (update the import statements and ensure
types remain string | undefined).
app/utils/semantic-search.server.ts (1)

355-374: Enrichment via Promise.all is clean, but consider error resilience for individual failures.

getSemanticSearchPresentation internally catches per-branch errors, but if an unexpected error escapes (e.g., a filesystem permission issue), the entire Promise.all will reject and the search request fails with no results. Wrapping each enrichment in a per-item catch would be more defensive:

Defensive per-item fallback
 const enriched = await Promise.all(
   baseResults.map(async (r) => {
-    const presentation = await getSemanticSearchPresentation(r)
-    return { ...r, ...presentation }
+    try {
+      const presentation = await getSemanticSearchPresentation(r)
+      return { ...r, ...presentation }
+    } catch {
+      return r
+    }
   }),
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/semantic-search.server.ts` around lines 355 - 374, The enrichment
step uses Promise.all over baseResults.map calling
getSemanticSearchPresentation, but an unexpected thrown error will reject the
whole Promise.all and drop results; wrap each item enrichment in a per-item
try/catch inside the map so failures return a safe fallback (e.g., the original
r or r with minimal presentation) and log the error instead of letting it
propagate, ensuring enriched remains an array even if
getSemanticSearchPresentation throws for some items.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/routes/search.tsx`:
- Around line 199-205: The conditional rendering in the H4 always takes the Link
branch because href is always a non-empty string; update the condition to only
render the Link when href is a meaningful destination (e.g., href && href !==
'#') so the else branch (rendering r.id) can execute for placeholder
destinations—modify the JSX around H4/Link that uses the href variable (and
references r.title, r.url, r.id) to check for href !== '#' (or check r.url
directly) before rendering <Link>.

---

Nitpick comments:
In `@app/components/navbar.tsx`:
- Line 543: The placeholder div in the Navbar component uses hardcoded colors
("bg-gray-200 dark:bg-gray-700"); replace that with a semantic theme token or
shared utility class (for example use the app's semantic class like
"bg-secondary" or a shared "placeholder-bg" utility) so it follows the app-wide
palette and adapts to theme changes; apply the same replacement in the similar
placeholder in app/routes/search.tsx (the two JSX placeholder divs) to ensure
consistency across components.
- Around line 553-559: Extract the inline IIFE that computes the display path
from s.url into a small helper and use it in the JSX: create a function like
getDisplayPath(url: string) (or compute const displayPath =
getDisplayPath(s.url) just before the component's return) which returns new
URL(url).pathname or falls back to the original url on error, then replace the
IIFE in the JSX with that helper/const; reference the existing s.url and the new
getDisplayPath/displayPath to locate and replace the logic in the Navbar
component.

In `@app/routes/resources`+/search.ts:
- Around line 8-14: normalizeSummary duplicates truncation constants and logic
used in semantic-search-presentation.server.ts; extract the shared
truncate/normalize logic and constants (e.g., MAX_SUMMARY_LENGTH = 220 and
TRUNCATE_SLICE = 217 or a single truncate function) into a common helper used by
both normalizeSummary in app/routes/resources+/search.ts and the presentation
function in semantic-search-presentation.server.ts so both code paths use the
same normalization + truncation implementation (keep normalizeSummary as a thin
wrapper that calls the shared helper to preserve the r.snippet safety net).

In `@app/utils/semantic-search-presentation.server.ts`:
- Around line 221-257: The module-level mdxMetaCache Map used by getMdxDocMeta
is unbounded and never refreshed; replace it with an lru-cache instance
(imported from 'lru-cache') configured with a sensible max entries and optional
ttl so entries expire, then use that cache’s get/set methods instead of
mdxMetaCache.get and mdxMetaCache.set; ensure the cache key remains
`${type}:${slug}` and keep the try/catch flow the same so updates to MDX
frontmatter are picked up after TTL or eviction.
- Around line 24-31: The type alias ResultLike is referenced in the exported
function getSemanticSearchPresentation but is not exported, preventing callers
from using the type; export ResultLike (e.g., add export to the ResultLike
declaration) so consumers can import it alongside SemanticSearchPresentation and
avoid duplicating the shape — update the declaration of ResultLike and keep the
existing names used by getSemanticSearchPresentation and
SemanticSearchPresentation.
- Around line 260-284: The module-level caches (talksBySlug and similar) are
never invalidated, causing stale data; update loadTalksBySlug to detect changes
and reload: keep metadata alongside the cached Map (e.g., lastLoaded timestamp
or the source file mtime) and in loadTalksBySlug call fs.stat on the YAML file
to compare mtime or check TTL before returning the cached Map; if the file
changed or TTL expired, re-read and reparse the file and update the cache. Apply
the same pattern to creditsBySlug, testimonialsBySlug, and resumeMeta (or
centralize into a small cache helper) so all YAML-backed singletons support
revalidation.
- Around line 73-135: Refactor getFallbackPresentation to remove repetitive
branches by creating a lookup map from the type keys ('ck', 'cwk', 'podcast',
'talk', 'resume', default) to the corresponding image factory (e.g.,
images.microphone, images.kayak, images.kentSpeakingAllThingsOpen,
images.kentProfile, images.kodyProfileGray) and alt values, then call the
selected image factory once with the shared options ({ quality: 'auto', format:
'auto', resize: { ... } }) and return { imageUrl, imageAlt }; keep the existing
resize types (pad vs fill) per entry in the map so behavior stays identical and
fall back to images.kodyProfileGray for unknown types.
- Around line 43-47: Duplicate function asNonEmptyString exists in two modules;
extract it into a single shared utility (e.g., export function
asNonEmptyString(...) in a new string-helpers module), remove the local copies
from both consumers (the modules that currently define asNonEmptyString), and
import the shared asNonEmptyString from that utility wherever it was used
(update the import statements and ensure types remain string | undefined).

In `@app/utils/semantic-search.server.ts`:
- Around line 355-374: The enrichment step uses Promise.all over baseResults.map
calling getSemanticSearchPresentation, but an unexpected thrown error will
reject the whole Promise.all and drop results; wrap each item enrichment in a
per-item try/catch inside the map so failures return a safe fallback (e.g., the
original r or r with minimal presentation) and log the error instead of letting
it propagate, ensuring enriched remains an array even if
getSemanticSearchPresentation throws for some items.

Comment on lines +199 to +205
<H4 className="truncate">
{href ? (
<Link to={href}>{r.title ?? r.url ?? r.id}</Link>
) : (
r.id
)}
</H4>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The else branch (r.id) on line 203 is unreachable.

href on line 178 always resolves to a non-empty string (r.url, '/', or '#'), so the condition href ? is always truthy. The else branch can never execute.

If the intent is to avoid rendering a <Link> when there's no meaningful destination, the condition should exclude '#':

Suggested fix
 <H4 className="truncate">
-  {href ? (
+  {href && href !== '#' ? (
     <Link to={href}>{r.title ?? r.url ?? r.id}</Link>
   ) : (
     r.id
   )}
 </H4>
📝 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
<H4 className="truncate">
{href ? (
<Link to={href}>{r.title ?? r.url ?? r.id}</Link>
) : (
r.id
)}
</H4>
<H4 className="truncate">
{href && href !== '#' ? (
<Link to={href}>{r.title ?? r.url ?? r.id}</Link>
) : (
r.id
)}
</H4>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/search.tsx` around lines 199 - 205, The conditional rendering in
the H4 always takes the Link branch because href is always a non-empty string;
update the condition to only render the Link when href is a meaningful
destination (e.g., href && href !== '#') so the else branch (rendering r.id) can
execute for placeholder destinations—modify the JSX around H4/Link that uses the
href variable (and references r.title, r.url, r.id) to check for href !== '#'
(or check r.url directly) before rendering <Link>.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issues.

})
: base.imageUrl
return { summary, imageUrl, imageAlt: alt }
}
Copy link

Choose a reason for hiding this comment

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

Blog/page enrichment path missing try-catch unlike other paths

Low Severity

The blog/page enrichment path is the only type-specific branch in getSemanticSearchPresentation that lacks a try-catch block. All other enrichment paths (talk, credit, testimonial, resume) wrap their logic in try-catch and gracefully return base on error. Since this function runs inside Promise.all in semanticSearchKCD, an unexpected throw here (e.g. from buildThumbFromCloudinaryId) would reject the entire promise and break the search for all results, rather than gracefully degrading for just that one item.

Additional Locations (1)

Fix in Cursor Fix in Web

if (!text) return undefined
// Keep payloads small for consumers like Discord embeds/autocomplete UIs.
return text.length > 220 ? `${text.slice(0, 217)}...` : text
}
Copy link

Choose a reason for hiding this comment

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

Redundant normalization duplicates existing truncation logic

Low Severity

The new normalizeSummary function duplicates the whitespace-normalization and truncation-to-220-chars logic already performed by normalizeText + truncate in semantic-search-presentation.server.ts. Since semanticSearchKCD now enriches results with already-normalized summaries via getSemanticSearchPresentation, applying normalizeSummary on r.summary is redundant. Having the same magic number 220 in two places risks divergence during future changes.

Fix in Cursor Fix in Web

@kentcdodds kentcdodds merged commit ff8750f into main Feb 19, 2026
7 checks passed
@cursor
Copy link

cursor bot commented Feb 19, 2026

Bugbot Autofix prepared fixes for 2 of the 2 bugs found in the latest run.

  • ✅ Fixed: Blog/page enrichment path missing try-catch unlike other paths
    • Wrapped the blog/page enrichment logic in a try-catch to return the base presentation on errors.
  • ✅ Fixed: Redundant normalization duplicates existing truncation logic
    • Removed the redundant normalization helper and now return the already-truncated summary from enrichment.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments