fix(blog): resolve blog author avatars at build time#1879
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds a ResolvedAuthor schema and a RawBlogPost frontmatter schema, updates components to accept Possibly related PRs
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
modules/blog.ts (1)
88-91:⚠️ Potential issue | 🔴 CriticalGuard date normalisation to prevent build-breaking
RangeError.
new Date(...).toISOString()throwsRangeError: Invalid time valueon invalid dates, bypassing schema validation entirely and breaking the build. Invalid dates should fail validation gracefully instead of crashing the build process.Suggested fix
- if (frontmatter.date) { - const raw = frontmatter.date - frontmatter.date = new Date(raw instanceof Date ? raw : String(raw)).toISOString() - } + if (frontmatter.date) { + const parsedDate = frontmatter.date instanceof Date + ? frontmatter.date + : new Date(String(frontmatter.date)) + if (!Number.isNaN(parsedDate.getTime())) { + frontmatter.date = parsedDate.toISOString() + } + }
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
app/components/AuthorList.vueapp/components/BlogPostListCard.vueapp/components/OgImage/BlogPost.vueapp/components/global/BlogPostWrapper.vuemodules/blog.tsshared/schemas/blog.ts
| <div v-if="post?.authors" class="mb-12 max-w-prose mx-auto"> | ||
| <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> | ||
| <AuthorList :authors="frontmatter.authors" variant="expanded" /> | ||
| <AuthorList :authors="post.authors" variant="expanded" /> |
There was a problem hiding this comment.
Guard on author count, not just array existence.
post?.authors is truthy even when empty, so this section can render with no visible content.
Suggested fix
- <div v-if="post?.authors" class="mb-12 max-w-prose mx-auto">
+ <div v-if="post?.authors?.length" class="mb-12 max-w-prose mx-auto">📝 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.
| <div v-if="post?.authors" class="mb-12 max-w-prose mx-auto"> | |
| <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> | |
| <AuthorList :authors="frontmatter.authors" variant="expanded" /> | |
| <AuthorList :authors="post.authors" variant="expanded" /> | |
| <div v-if="post?.authors?.length" class="mb-12 max-w-prose mx-auto"> | |
| <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> | |
| <AuthorList :authors="post.authors" variant="expanded" /> |
| const response = await fetch( | ||
| `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`, | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify fetch call currently lacks timeout/abort signal wiring
rg -nP "getProfiles\?" modules/blog.ts -C5
rg -nP "AbortController|signal:" modules/blog.ts -C3Repository: npmx-dev/npmx.dev
Length of output: 421
🏁 Script executed:
#!/bin/bash
# Get broader context around the fetch call to understand the function scope
sed -n '1,50p' modules/blog.tsRepository: npmx-dev/npmx.dev
Length of output: 1658
🏁 Script executed:
#!/bin/bash
# Get the rest of the function and where it's called
sed -n '45,100p' modules/blog.tsRepository: npmx-dev/npmx.dev
Length of output: 2038
🏁 Script executed:
#!/bin/bash
# Find where loadBlogPosts is called and if it's in module setup
sed -n '100,200p' modules/blog.tsRepository: npmx-dev/npmx.dev
Length of output: 2657
Add an abort timeout for the Bluesky fetch call.
A hanging Bluesky request can stall the full build because this runs during module setup (line 141 in the defineNuxtModule.setup() function).
Suggested fix
- const response = await fetch(
- `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`,
- )
+ const controller = new AbortController()
+ const timeout = setTimeout(() => controller.abort(), 8000)
+ let response: Response
+ try {
+ response = await fetch(
+ `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`,
+ { signal: controller.signal },
+ )
+ } finally {
+ clearTimeout(timeout)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const response = await fetch( | |
| `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`, | |
| ) | |
| const controller = new AbortController() | |
| const timeout = setTimeout(() => controller.abort(), 8000) | |
| let response: Response | |
| try { | |
| response = await fetch( | |
| `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`, | |
| { signal: controller.signal }, | |
| ) | |
| } finally { | |
| clearTimeout(timeout) | |
| } |
|
looks like CI is sad:
but this image (of you) certainly does exist 😄 |
I wonder if it's because the file doesn't end with .jpeg - will take a look 👀 |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9c7bfc7e-0bad-4f14-8eaf-48a79e4a849b
📒 Files selected for processing (2)
app/components/BlogPostListCard.vueapp/components/global/BlogPostWrapper.vue
🚧 Files skipped from review as they are similar to previous changes (1)
- app/components/BlogPostListCard.vue
| defineOgImageComponent('BlogPost', { | ||
| title: props.frontmatter.title, | ||
| authors: props.frontmatter.authors, | ||
| authors: post.value?.authors ?? [], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether OG BlogPost rendering currently consumes avatar URLs without a guard.
fd 'BlogPost.vue' app/components --exec sh -c '
for f in "$@"; do
echo "== $f ==";
rg -n -C2 "avatar|NuxtImg|<img|src=" "$f";
done
' sh {}
echo "== wrapper callsite =="
rg -n -C3 "defineOgImageComponent|authors:\\s*post\\.value\\?\\.authors" app/components/global/BlogPostWrapper.vueRepository: npmx-dev/npmx.dev
Length of output: 721
🏁 Script executed:
#!/bin/bash
# Get full context of BlogPostWrapper.vue to understand component structure
echo "=== BlogPostWrapper.vue (full context) ==="
cat -n app/components/global/BlogPostWrapper.vue | head -50
echo ""
echo "=== Check for type definitions ==="
rg -n "interface.*[Aa]uthor|type.*[Aa]uthor" --type ts --type tsx --type vue app/components app/types 2>/dev/null | head -20
echo ""
echo "=== Check OgImage BlogPost component more broadly ==="
cat -n app/components/OgImage/BlogPost.vue | head -120Repository: npmx-dev/npmx.dev
Length of output: 6500
🏁 Script executed:
#!/bin/bash
# Verify the ResolvedAuthor type definition
echo "=== ResolvedAuthor schema ==="
fd -e ts -e tsx -e vue . app/types -o -path "*/shared/*" | head -20
rg -n "ResolvedAuthor|interface.*Author|type.*Author" --type ts --type tsx -B2 -A5 2>/dev/null | head -40
# Check if avatar URLs are used elsewhere
echo ""
echo "=== Other avatar URL usages ==="
rg -n "author\.avatar|\.avatar\s*:" --type vue --type ts --type tsx 2>/dev/null | grep -v node_modules | head -20Repository: npmx-dev/npmx.dev
Length of output: 257
🏁 Script executed:
#!/bin/bash
# Search for ResolvedAuthor type definition
echo "=== ResolvedAuthor schema ==="
rg "ResolvedAuthor|interface.*Author|type.*Author" -B2 -A8 --type ts --type tsx 2>/dev/null | head -60
echo ""
echo "=== Avatar field usages ==="
rg "avatar" --type vue --type ts --type tsx -B1 -A1 2>/dev/null | grep -v node_modules | head -40Repository: npmx-dev/npmx.dev
Length of output: 119
🏁 Script executed:
#!/bin/bash
# Find schema files
echo "=== Locating blog schema files ==="
fd -e ts -e tsx "schema|blog" . --type f | grep -i schema | grep -i blog | head -10
echo ""
echo "=== Search in shared directory ==="
find . -type d -name shared 2>/dev/null | head -5
echo ""
echo "=== Files in potential schema locations ==="
find . -path "*/shared/schemas/*" -type f 2>/dev/null | head -20Repository: npmx-dev/npmx.dev
Length of output: 427
🏁 Script executed:
#!/bin/bash
# Read the blog schema file to find ResolvedAuthor definition
echo "=== Checking blog schema file size ==="
wc -l ./shared/schemas/blog.ts
echo ""
echo "=== Blog schema content ==="
cat -n ./shared/schemas/blog.tsRepository: npmx-dev/npmx.dev
Length of output: 2532
Sanitise unsupported avatar URLs before OG rendering to prevent build failures.
The authors prop at line 32 currently passes avatar URLs directly to the OG component without validating format support. The OG component renders these in <img :src="author.avatar"> with only an existence check, causing failures when encountering unsupported image types (e.g., "Unsupported image type: unknown").
Add type validation to filter out unsupported avatar URLs:
Suggested fix
const post = computed(() => posts.find(p => p.slug === props.frontmatter.slug))
+
+function isSupportedOgAvatarUrl(url: string | null): boolean {
+ if (!url)
+ return false
+ // Accept common image extensions and Bluesky's `@jpeg` suffix format
+ return /(\.(png|jpe?g|webp|gif|svg)(\?.*)?$)|(`@jpe`?g(\?.*)?$)/i.test(url)
+}
+
+const ogAuthors = computed(() =>
+ (post.value?.authors ?? []).map(author => ({
+ ...author,
+ avatar: isSupportedOgAvatarUrl(author.avatar) ? author.avatar : null,
+ })),
+)
defineOgImageComponent('BlogPost', {
title: props.frontmatter.title,
- authors: post.value?.authors ?? [],
+ authors: ogAuthors.value,
date: props.frontmatter.date,
})|
yes, I think it is. satori is likely choking on it this may also be fixed when we remake the nuxt-og-image v6 upgrade (cc: @harlan-zw) |
|
@danielroe what do you think about 32cedb2? Feel free to revert if you don't like it, but I think this is a nice side effect of the satori issue because it removes an origin connection for users (but the build still needs some fixes to work xD) |
e60895b to
32cedb2
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
modules/blog.ts (1)
102-105:⚠️ Potential issue | 🟠 MajorGuard date normalisation before calling
toISOString().Line 104 can throw on invalid date input, which aborts blog loading before
safeParsecan reject the post cleanly.As per coding guidelines: "Use error handling patterns consistently".Suggested fix
- if (frontmatter.date) { - const raw = frontmatter.date - frontmatter.date = new Date(raw instanceof Date ? raw : String(raw)).toISOString() - } + if (frontmatter.date) { + const parsedDate = new Date( + frontmatter.date instanceof Date ? frontmatter.date : String(frontmatter.date), + ) + if (Number.isNaN(parsedDate.getTime())) { + console.warn(`[blog] Skipping post with invalid date: ${file}`) + continue + } + frontmatter.date = parsedDate.toISOString() + }
♻️ Duplicate comments (2)
modules/blog.ts (2)
40-42:⚠️ Potential issue | 🟠 MajorAdd an abort timeout for the Bluesky profiles request.
Line 40 performs build-time network I/O without an upper bound; if the upstream hangs, module setup can hang CI/builds.
Suggested fix
- const response = await fetch( - `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`, - ) + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 8000) + let response: Response + try { + response = await fetch( + `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`, + { signal: controller.signal }, + ) + } finally { + clearTimeout(timeout) + }
56-61:⚠️ Potential issue | 🔴 CriticalValidate avatar payloads and keep the correct file type when caching.
Lines 56-61 cache responses as
.jpgwithout checking status/type first. Failed/non-image payloads can be persisted and later break OG image rendering.Suggested fix
- const res = await fetch(profile.avatar) - await writeFile(join(imagesDir, `${hash}.jpg`), res.body!) - } - - avatarMap.set(profile.handle, `/blog/avatar/${hash}.jpg`) + const res = await fetch(profile.avatar) + const contentType = res.headers.get('content-type') ?? '' + const ext = contentType.includes('png') + ? 'png' + : contentType.includes('webp') + ? 'webp' + : contentType.includes('jpeg') || contentType.includes('jpg') + ? 'jpg' + : null + + if (!res.ok || !ext) { + console.warn(`[blog] Skipping avatar for ${profile.handle}: status=${res.status}, type=${contentType}`) + continue + } + + const fileName = `${hash}.${ext}` + await writeFile(join(imagesDir, fileName), Buffer.from(await res.arrayBuffer())) + avatarMap.set(profile.handle, `/blog/avatar/${fileName}`) + continue + } + + avatarMap.set(profile.handle, `/blog/avatar/${hash}.jpg`)
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f56d02a3-5a39-4c0d-b1e2-9672e6f4488b
📒 Files selected for processing (2)
.gitignoremodules/blog.ts
✅ Files skipped from review due to trivial changes (1)
- .gitignore
|
nice 👌 |
🔗 Linked issue
🧭 Context
📚 Description
this is a slight perf improvement, hopefully!