-
Notifications
You must be signed in to change notification settings - Fork 127
feat: add endpoint-driven Alby Blog widget #2173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c0e2411
70bdaf6
405af53
d88c40e
fb24c64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import ( | |
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/getAlby/hub/config" | ||
|
|
@@ -208,3 +209,63 @@ func (svc *albyService) GetInfo(ctx context.Context) (*AlbyInfo, error) { | |
| Incidents: incidents, | ||
| }, nil | ||
| } | ||
|
|
||
| func (svc *albyService) GetLatestBlogPost(ctx context.Context) (*BlogPost, error) { | ||
| client := &http.Client{Timeout: 10 * time.Second} | ||
|
|
||
| url := fmt.Sprintf("%s/hub/blog/latest", albyInternalAPIURL) | ||
|
|
||
| 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 | ||
| } | ||
| 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") | ||
| } | ||
|
|
||
| if res.StatusCode >= 300 { | ||
| logger.Logger.WithFields(logrus.Fields{ | ||
| "body": string(body), | ||
| "status_code": res.StatusCode, | ||
| }).Error("Blog endpoint returned non-success code") | ||
| return nil, fmt.Errorf("blog endpoint returned non-success code: %s", string(body)) | ||
| } | ||
|
|
||
| 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") | ||
| } | ||
|
Comment on lines
+246
to
+262
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle fallback keys and enforce required fields before returning. Line 246 only decodes 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 |
||
|
|
||
| return &BlogPost{ | ||
| ID: post.ID, | ||
| Title: post.Title, | ||
| Description: post.Lead, | ||
| URL: post.URL, | ||
| ImageURL: strings.ReplaceAll(post.ImageURL, "&", "&"), | ||
| }, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| #VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need of env variables once we shift api fetching to backend. we have alby internal api endpoints already defined in backend |
||
|
|
||
| # optional blog endpoint used by Home Alby Blog widget | ||
| #VITE_ALBY_BLOG_ENDPOINT=https://getalby.com/api/hub/blog/latest | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { SquareArrowOutUpRightIcon } from "lucide-react"; | ||
| import { | ||
| Card, | ||
| CardContent, | ||
| CardDescription, | ||
| CardFooter, | ||
| CardHeader, | ||
| CardTitle, | ||
| } from "src/components/ui/card"; | ||
| import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; | ||
| import { useAlbyBlog } from "src/hooks/useAlbyBlog"; | ||
| import { cn } from "src/lib/utils"; | ||
|
|
||
| const fallbackThemes = [ | ||
| "from-emerald-200 via-cyan-200 to-yellow-200", | ||
| "from-orange-200 via-amber-100 to-pink-100", | ||
| "from-slate-200 via-zinc-100 to-lime-100", | ||
| "from-sky-200 via-indigo-100 to-violet-100", | ||
| ]; | ||
|
|
||
| function hashString(str: string): number { | ||
| return Math.abs([...str].reduce((sum, ch) => sum + ch.charCodeAt(0), 0)); | ||
| } | ||
|
|
||
| export function AlbyBlogWidget() { | ||
| const { data: post } = useAlbyBlog(); | ||
|
|
||
| if (!post) { | ||
| return null; | ||
| } | ||
|
|
||
| const theme = fallbackThemes[hashString(post.id) % fallbackThemes.length]; | ||
|
|
||
| return ( | ||
| <Card> | ||
| <CardHeader> | ||
| <CardTitle>Alby Blog</CardTitle> | ||
| </CardHeader> | ||
| <CardContent> | ||
| <div className="relative h-[247px] overflow-hidden rounded-xl border"> | ||
| {post.imageUrl ? ( | ||
| <img | ||
| src={post.imageUrl} | ||
| alt={post.title} | ||
| className="absolute inset-0 size-full object-cover" | ||
| /> | ||
| ) : ( | ||
| <div className={cn("absolute inset-0 bg-gradient-to-br", theme)}> | ||
| <div className="absolute -left-10 top-6 size-36 rounded-full bg-white/35 blur-3xl" /> | ||
| <div className="absolute -right-8 bottom-2 size-40 rounded-full bg-white/20 blur-3xl" /> | ||
| <div className="absolute inset-0 bg-white/10" /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </CardContent> | ||
| <CardFooter className="flex flex-col items-start gap-4"> | ||
| <div className="space-y-1"> | ||
| <CardTitle className="text-xl leading-7">{post.title}</CardTitle> | ||
| <CardDescription className="text-base leading-6"> | ||
| {post.description} | ||
| </CardDescription> | ||
| </div> | ||
| <div className="flex w-full justify-end"> | ||
| <ExternalLinkButton to={post.url} variant="outline"> | ||
| Read on Alby Blog | ||
| <SquareArrowOutUpRightIcon /> | ||
| </ExternalLinkButton> | ||
| </div> | ||
| </CardFooter> | ||
| </Card> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import useSWR from "swr"; | ||
|
|
||
| import { swrFetcher } from "src/utils/swr"; | ||
|
|
||
| type BlogPost = { | ||
| id: string; | ||
| title: string; | ||
| description: string; | ||
| url: string; | ||
| imageUrl?: string; | ||
| }; | ||
|
|
||
| export function useAlbyBlog() { | ||
| return useSWR<BlogPost>("/api/alby/blog/latest", swrFetcher, { | ||
| dedupingInterval: 5 * 60 * 1000, // 5 minutes | ||
| }); | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
Repository: getAlby/hub
Length of output: 696
🏁 Script executed:
Repository: 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:
Repository: getAlby/hub
Length of output: 1984
Wrap errors with context in
GetLatestBlogPostto 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