feat: add endpoint-driven Alby Blog widget#2173
feat: add endpoint-driven Alby Blog widget#2173stackingsaunter wants to merge 5 commits intomasterfrom
Conversation
Render Home blog content from a getalby.com endpoint. Place the widget under Recently Used Apps in the right column. Made-with: Cursor
Place the Alby Blog widget directly above Stats for nerds in the Home right column. Made-with: Cursor
📝 WalkthroughWalkthroughAdds a new Alby Blog widget to Home that fetches the latest blog post via a new API route, adds backend service logic to retrieve and normalize the post from Alby, updates CSP img-src to include framerusercontent, and exposes an optional frontend env var for the Alby blog endpoint. Changes
Sequence DiagramsequenceDiagram
participant Widget as AlbyBlogWidget
participant FrontendAPI as Frontend (/api/alby/blog/latest)
participant AlbySvc as AlbyService
participant AlbyExternal as Alby Internal API
Widget->>FrontendAPI: GET /api/alby/blog/latest
FrontendAPI->>AlbySvc: GetLatestBlogPost(ctx)
AlbySvc->>AlbyExternal: GET /hub/blog/latest (10s timeout)
AlbyExternal-->>AlbySvc: JSON response (post)
AlbySvc->>FrontendAPI: *BlogPost* (normalized)
FrontendAPI-->>Widget: 200 OK with post JSON
alt post present
Widget->>Widget: derive theme from post.id, render image or gradient, title, description, external link
else no post / error
Widget-->>Widget: render nothing (null)
end
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
Allow framerusercontent images in CSP. Normalize HTML-escaped image query params so thumbnails render. Made-with: Cursor
There was a problem hiding this comment.
🧹 Nitpick comments (3)
frontend/src/components/home/widgets/AlbyBlogWidget.tsx (2)
140-144: Consider addingloading="lazy"to the image.Since this widget may be below the fold, lazy loading the image can improve initial page load performance.
🔧 Proposed fix
<img src={post.imageUrl} alt={post.title} + loading="lazy" className="absolute inset-0 size-full object-cover" />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/home/widgets/AlbyBlogWidget.tsx` around lines 140 - 144, The <img> in the AlbyBlogWidget component is missing lazy loading; update the JSX inside AlbyBlogWidget (the <img> using post.imageUrl and alt={post.title}) to include loading="lazy" so the image is deferred until needed (optionally also consider decoding="async" or adding fetchpriority when appropriate).
84-100: Consider using SWR for fetching blog posts.The component uses raw
fetch()withuseEffectfor data fetching. Per coding guidelines, SWR should be used for server state management. SWR would provide automatic caching, deduplication, and revalidation.That said, since this fetches from an external endpoint (not the Hub API), the
request()helper may not apply directly. The current implementation is functional and handles errors gracefully.♻️ Example SWR refactor
+import useSWR from "swr"; + +const fetcher = async (url: string): Promise<BlogPost | null> => { + const response = await fetch(url); + if (!response.ok) return null; + const payload = (await response.json()) as unknown; + const candidates = Array.isArray(payload) + ? payload + : Array.isArray((payload as { posts?: unknown[] })?.posts) + ? (payload as { posts: unknown[] }).posts + : [payload]; + const posts = candidates + .map(normalizePost) + .filter((post): post is BlogPost => !!post); + return posts.length > 0 ? pickLatestPost(posts) : null; +}; + export function AlbyBlogWidget() { - const [post, setPost] = React.useState<BlogPost | null>(null); - const [themeClassName, setThemeClassName] = React.useState(fallbackThemes[0]); - - React.useEffect(() => { - const loadPost = async () => { - try { - const posts = await fetchBlogPosts(); - if (!posts.length) { - setPost(null); - return; - } - const latest = pickLatestPost(posts); - setPost(latest); - const themeIndex = Math.abs( - [...latest.id].reduce((sum, ch) => sum + ch.charCodeAt(0), 0) - ); - setThemeClassName(fallbackThemes[themeIndex % fallbackThemes.length]); - } catch { - setPost(null); - } - }; - - void loadPost(); - }, []); + const { data: post } = useSWR(ALBY_BLOG_ENDPOINT, fetcher); + + const themeClassName = React.useMemo(() => { + if (!post) return fallbackThemes[0]; + const themeIndex = Math.abs( + [...post.id].reduce((sum, ch) => sum + ch.charCodeAt(0), 0) + ); + return fallbackThemes[themeIndex % fallbackThemes.length]; + }, [post]);As per coding guidelines:
Use SWR for server state management.Also applies to: 102-126
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/home/widgets/AlbyBlogWidget.tsx` around lines 84 - 100, The fetchBlogPosts function uses raw fetch() and should be refactored to use SWR for server-state management; replace direct calls to ALBY_BLOG_ENDPOINT inside fetchBlogPosts with an SWR data fetcher hooked into the component (useSWR key = ALBY_BLOG_ENDPOINT) and move the existing response/error handling and payload normalization (normalizePost, BlogPost) into the SWR fetcher function so the component consumes data from useSWR (with loading/error states) enabling caching, deduplication, and revalidation.frontend/.env.local.example (1)
5-5: Add a trailing newline.The file is missing a trailing newline at the end. Some tools and editors expect files to end with a newline.
🔧 Proposed fix
# optional blog endpoint used by Home Alby Blog widget `#VITE_ALBY_BLOG_ENDPOINT`=https://getalby.com/api/hub/blog/latest +🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/.env.local.example` at line 5, Add a trailing newline to the end of the file so the final line ("#VITE_ALBY_BLOG_ENDPOINT=https://getalby.com/api/hub/blog/latest") ends with a newline character; simply ensure the file terminates with a newline (LF) so editors and tools recognize the last line correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@frontend/.env.local.example`:
- Line 5: Add a trailing newline to the end of the file so the final line
("#VITE_ALBY_BLOG_ENDPOINT=https://getalby.com/api/hub/blog/latest") ends with a
newline character; simply ensure the file terminates with a newline (LF) so
editors and tools recognize the last line correctly.
In `@frontend/src/components/home/widgets/AlbyBlogWidget.tsx`:
- Around line 140-144: The <img> in the AlbyBlogWidget component is missing lazy
loading; update the JSX inside AlbyBlogWidget (the <img> using post.imageUrl and
alt={post.title}) to include loading="lazy" so the image is deferred until
needed (optionally also consider decoding="async" or adding fetchpriority when
appropriate).
- Around line 84-100: The fetchBlogPosts function uses raw fetch() and should be
refactored to use SWR for server-state management; replace direct calls to
ALBY_BLOG_ENDPOINT inside fetchBlogPosts with an SWR data fetcher hooked into
the component (useSWR key = ALBY_BLOG_ENDPOINT) and move the existing
response/error handling and payload normalization (normalizePost, BlogPost) into
the SWR fetcher function so the component consumes data from useSWR (with
loading/error states) enabling caching, deduplication, and revalidation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9fa94d79-505c-4a15-a35a-da92e8edd5d5
📒 Files selected for processing (5)
frontend/.env.local.examplefrontend/src/components/home/widgets/AlbyBlogWidget.tsxfrontend/src/screens/Home.tsxfrontend/vite.config.tshttp/http_service.go
pavanjoshi914
left a comment
There was a problem hiding this comment.
there are few issues with how this feature is implemented added comments here
| publishedAt?: string; | ||
| }; | ||
|
|
||
| const ALBY_BLOG_ENDPOINT = |
There was a problem hiding this comment.
we shouldn't do fetching in the view itself. its not consistent. we use swr for api requests and api requests shall be done on go backend and repsonse should be passed on frontend
|
|
||
| const ALBY_BLOG_ENDPOINT = | ||
| import.meta.env.VITE_ALBY_BLOG_ENDPOINT || | ||
| "https://getalby.com/api/hub/blog/latest"; |
There was a problem hiding this comment.
it uses this single api endpoint. then where this api/hub/blog/feed is used. i see it added in other pr
| @@ -1,2 +1,5 @@ | |||
| # set a custom messageboard wallet (should be a sub-wallet with only make invoice and list transactions permissions) | |||
| #VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894" No newline at end of file | |||
| #VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894" | |||
There was a problem hiding this comment.
no need of env variables once we shift api fetching to backend. we have alby internal api endpoints already defined in backend
…nd wails handlers Add BlogPost type and GetLatestBlogPost to alby service interface Replace direct frontend fetch with useAlbyBlog SWR hook for consistent data fetching Simplify AlbyBlogWidget to match codebase widget patterns
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@alby/alby_service.go`:
- Around line 246-262: The decoded temporary struct "post" may miss upstream
variants (slug, link, description, excerpt, image) and the code only validates
Title and URL; update the normalization and validation after json.Unmarshal in
alby_service.go so you map fallback keys into the canonical fields (e.g., use
slug -> ID, link -> URL, description/excerpt -> Lead/description, alternate
image keys -> ImageURL) and then enforce required fields (ID, Title, URL) before
returning; if any required canonical field is still empty return an error ("no
blog post found" or similar) and log the detailed decode/normalization failure
via logger.Logger.WithError.
- Around line 219-236: In GetLatestBlogPost wrap returned errors with fmt.Errorf
to preserve error chains and add context: wherever the code currently logs and
returns err (the request creation error after
logger.Logger.WithError(err).Error("Error creating request to blog endpoint"),
the client.Do error logged as "Failed to fetch blog endpoint", and the
io.ReadAll error logged as "Failed to read response body"), return
fmt.Errorf("creating blog request: %w", err), fmt.Errorf("fetching blog
endpoint: %w", err), and fmt.Errorf("reading blog response body: %w", err)
respectively (keep logging calls but change the returned errors), ensuring
imports include fmt and references to GetLatestBlogPost,
setDefaultRequestHeaders, client.Do, and io.ReadAll to locate the spots.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: e236b837-894c-41c8-bd6b-39cb9a0b2f63
📒 Files selected for processing (7)
alby/alby_service.goalby/models.gofrontend/src/components/home/widgets/AlbyBlogWidget.tsxfrontend/src/hooks/useAlbyBlog.tshttp/alby_http_service.gotests/mocks/AlbyService.gowails/wails_handlers.go
✅ Files skipped from review due to trivial changes (1)
- tests/mocks/AlbyService.go
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/components/home/widgets/AlbyBlogWidget.tsx
| if err != nil { | ||
| logger.Logger.WithError(err).Error("Error creating request to blog endpoint") | ||
| return nil, err | ||
| } | ||
| setDefaultRequestHeaders(req) | ||
|
|
||
| res, err := client.Do(req) | ||
| if err != nil { | ||
| logger.Logger.WithError(err).Error("Failed to fetch blog endpoint") | ||
| return nil, err | ||
| } | ||
| defer res.Body.Close() | ||
|
|
||
| body, err := io.ReadAll(res.Body) | ||
| if err != nil { | ||
| logger.Logger.WithError(err).Error("Failed to read response body") | ||
| return nil, errors.New("failed to read response body") | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify unwrapped errors in the new method.
rg -n 'GetLatestBlogPost|return nil, err|errors.New\("failed to read response body"\)' alby/alby_service.goRepository: getAlby/hub
Length of output: 696
🏁 Script executed:
# Read the GetLatestBlogPost method to see context
sed -n '213,270p' alby/alby_service.goRepository: getAlby/hub
Length of output: 1763
🏁 Script executed:
# Check imports to verify fmt and errors are available
head -30 alby/alby_service.goRepository: getAlby/hub
Length of output: 475
🏁 Script executed:
# Confirm the exact line numbers and error returns in the method
sed -n '213,265p' alby/alby_service.go | cat -nRepository: getAlby/hub
Length of output: 1984
Wrap errors with context in GetLatestBlogPost to aid debugging.
Lines 221, 228, 235, and 257 return raw or opaque errors, losing caller context. Use fmt.Errorf("context: %w", err) to preserve error chains for diagnosis across stack layers.
Suggested error-wrapping patch
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request to blog endpoint")
- return nil, err
+ return nil, fmt.Errorf("create blog request: %w", err)
}
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch blog endpoint")
- return nil, err
+ return nil, fmt.Errorf("fetch blog endpoint: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).Error("Failed to read response body")
- return nil, errors.New("failed to read response body")
+ return nil, fmt.Errorf("read blog response body: %w", err)
}
err = json.Unmarshal(body, &post)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode blog API response")
- return nil, err
+ return nil, fmt.Errorf("decode blog API response: %w", err)
}Per coding guidelines for Go: "Use error wrapping with fmt.Errorf("context: %w", err) for debugging".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@alby/alby_service.go` around lines 219 - 236, In GetLatestBlogPost wrap
returned errors with fmt.Errorf to preserve error chains and add context:
wherever the code currently logs and returns err (the request creation error
after logger.Logger.WithError(err).Error("Error creating request to blog
endpoint"), the client.Do error logged as "Failed to fetch blog endpoint", and
the io.ReadAll error logged as "Failed to read response body"), return
fmt.Errorf("creating blog request: %w", err), fmt.Errorf("fetching blog
endpoint: %w", err), and fmt.Errorf("reading blog response body: %w", err)
respectively (keep logging calls but change the returned errors), ensuring
imports include fmt and references to GetLatestBlogPost,
setDefaultRequestHeaders, client.Do, and io.ReadAll to locate the spots.
| var post struct { | ||
| ID string `json:"id"` | ||
| Title string `json:"title"` | ||
| Lead string `json:"lead"` | ||
| URL string `json:"url"` | ||
| ImageURL string `json:"imageUrl"` | ||
| PublishedAt string `json:"publishedAt"` | ||
| } | ||
| err = json.Unmarshal(body, &post) | ||
| if err != nil { | ||
| logger.Logger.WithError(err).Error("Failed to decode blog API response") | ||
| return nil, err | ||
| } | ||
|
|
||
| if post.Title == "" || post.URL == "" { | ||
| return nil, errors.New("no blog post found") | ||
| } |
There was a problem hiding this comment.
Handle fallback keys and enforce required fields before returning.
Line 246 only decodes lead/url/imageUrl, and Line 260 validates only title/url. If upstream sends slug, description/excerpt, or link, this can return an incomplete BlogPost (empty id/description) instead of treating it as invalid.
Suggested normalization + validation patch
func (svc *albyService) GetLatestBlogPost(ctx context.Context) (*BlogPost, error) {
@@
var post struct {
- ID string `json:"id"`
- Title string `json:"title"`
- Lead string `json:"lead"`
- URL string `json:"url"`
- ImageURL string `json:"imageUrl"`
- PublishedAt string `json:"publishedAt"`
+ ID string `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Lead string `json:"lead"`
+ Description string `json:"description"`
+ Excerpt string `json:"excerpt"`
+ URL string `json:"url"`
+ Link string `json:"link"`
+ ImageURL string `json:"imageUrl"`
+ ImageURLAlt string `json:"image_url"`
+ CoverImage string `json:"coverImage"`
+ CoverImage2 string `json:"cover_image"`
}
@@
- if post.Title == "" || post.URL == "" {
+ firstNonEmpty := func(values ...string) string {
+ for _, v := range values {
+ if v != "" {
+ return v
+ }
+ }
+ return ""
+ }
+
+ id := firstNonEmpty(post.ID, post.Slug)
+ description := firstNonEmpty(post.Lead, post.Description, post.Excerpt)
+ url := firstNonEmpty(post.URL, post.Link)
+ imageURL := firstNonEmpty(post.ImageURL, post.ImageURLAlt, post.CoverImage, post.CoverImage2)
+
+ if id == "" || post.Title == "" || description == "" || url == "" {
return nil, errors.New("no blog post found")
}
return &BlogPost{
- ID: post.ID,
+ ID: id,
Title: post.Title,
- Description: post.Lead,
- URL: post.URL,
- ImageURL: strings.ReplaceAll(post.ImageURL, "&", "&"),
+ Description: description,
+ URL: url,
+ ImageURL: strings.ReplaceAll(imageURL, "&", "&"),
}, nil
}Also applies to: 264-270
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@alby/alby_service.go` around lines 246 - 262, The decoded temporary struct
"post" may miss upstream variants (slug, link, description, excerpt, image) and
the code only validates Title and URL; update the normalization and validation
after json.Unmarshal in alby_service.go so you map fallback keys into the
canonical fields (e.g., use slug -> ID, link -> URL, description/excerpt ->
Lead/description, alternate image keys -> ImageURL) and then enforce required
fields (ID, Title, URL) before returning; if any required canonical field is
still empty return an error ("no blog post found" or similar) and log the
detailed decode/normalization failure via logger.Logger.WithError.
Summary
AlbyBlogWidgetdriven by a future getalby.com blog endpointarray,{ posts: [] }, or single object) and render the latest valid postStats for nerdsin the right columnfrontend/.env.local.exampleTest plan
cd frontend && yarn tsc:compilegetalby.com endpoint requirements
Endpoint:
GET https://getalby.com/api/hub/blog/latestThe widget accepts any of:
BlogPost[]{ posts: BlogPost[] }BlogPostRequired per post:
idorslugtitlelead/description/excerpturlorlinkOptional per post:
imageUrl(orimage_url/coverImage/cover_image)publishedAt(orpublished_at) for latest-post sortingCORS: allow Hub frontend origins (dev and production) for
GET.No auth or secrets in the URL; response should be public-safe metadata only.
Made with Cursor
Summary by CodeRabbit
New Features
Chores