nandi/gleanpublic⑂ Fork 0
⑂ 67b0e8d
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Add favicon support to feed data and UIUnverified

Julien Robert committed 2026-04-20T23:40:28+02:00 Browse files
67b0e8d parent: 2dcefd3
modified internal/db/article.go +21 -20
@@ -7,20 +7,21 @@ import (
77 )
88
99 type Article struct {
10- ID int64
11- FeedURL string
12- FeedTitle string
13- GUID string
14- Title string
15- URL sql.NullString
16- Author sql.NullString
17- Summary sql.NullString
18- Content sql.NullString
19- FullContent sql.NullString
20- Published sql.NullTime
21- Updated sql.NullTime
22- FetchedAt sql.NullTime
23- IsRead sql.NullBool
10+ ID int64
11+ FeedURL string
12+ FeedTitle string
13+ FeedFaviconURL sql.NullString
14+ GUID string
15+ Title string
16+ URL sql.NullString
17+ Author sql.NullString
18+ Summary sql.NullString
19+ Content sql.NullString
20+ FullContent sql.NullString
21+ Published sql.NullTime
22+ Updated sql.NullTime
23+ FetchedAt sql.NullTime
24+ IsRead sql.NullBool
2425 }
2526
2627 type ReadState struct {
@@ -62,7 +63,7 @@ func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) {
6263
6364 func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {
6465 query := `
65- SELECT a.id, a.feed_url, COALESCE(f.title, ''), a.guid, a.title, a.url, a.author, a.summary, a.content,
66+ SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
6667 a.published, a.updated, a.fetched_at,
6768 COALESCE(r.is_read, 0)
6869 FROM articles a
@@ -88,7 +89,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit,
8889 var articles []*Article
8990 for rows.Next() {
9091 a := &Article{}
91- if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.GUID, &a.Title, &a.URL, &a.Author,
92+ if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
9293 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
9394 &a.IsRead); err != nil {
9495 return nil, err
@@ -100,7 +101,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit,
100101
101102 func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {
102103 query := `
103- SELECT a.id, a.feed_url, COALESCE(f.title, ''), a.guid, a.title, a.url, a.author, a.summary, a.content,
104+ SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
104105 a.published, a.updated, a.fetched_at,
105106 COALESCE(r.is_read, 0)
106107 FROM articles a
@@ -127,7 +128,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l
127128 var articles []*Article
128129 for rows.Next() {
129130 a := &Article{}
130- if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.GUID, &a.Title, &a.URL, &a.Author,
131+ if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
131132 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
132133 &a.IsRead); err != nil {
133134 return nil, err
@@ -139,7 +140,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l
139140
140141 func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {
141142 query := `
142- SELECT a.id, a.feed_url, COALESCE(f.title, ''), a.guid, a.title, a.url, a.author, a.summary, a.content,
143+ SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
143144 a.published, a.updated, a.fetched_at,
144145 COALESCE(r.is_read, 0)
145146 FROM articles a
@@ -166,7 +167,7 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim
166167 var articles []*Article
167168 for rows.Next() {
168169 a := &Article{}
169- if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.GUID, &a.Title, &a.URL, &a.Author,
170+ if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
170171 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
171172 &a.IsRead); err != nil {
172173 return nil, err
@@ -7,20 +7,21 @@ import (
7 )7 )
8 8
9 type Article struct {9 type Article struct {
10- ID int6410+ ID int64
11- FeedURL string11+ FeedURL string
12- FeedTitle string12+ FeedTitle string
13- GUID string13+ FeedFaviconURL sql.NullString
14- Title string14+ GUID string
15- URL sql.NullString15+ Title string
16- Author sql.NullString16+ URL sql.NullString
17- Summary sql.NullString17+ Author sql.NullString
18- Content sql.NullString18+ Summary sql.NullString
19- FullContent sql.NullString19+ Content sql.NullString
20- Published sql.NullTime20+ FullContent sql.NullString
21- Updated sql.NullTime21+ Published sql.NullTime
22- FetchedAt sql.NullTime22+ Updated sql.NullTime
23- IsRead sql.NullBool23+ FetchedAt sql.NullTime
24+ IsRead sql.NullBool
24 }25 }
25 26
26 type ReadState struct {27 type ReadState struct {
@@ -62,7 +63,7 @@ func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) {
62 63
63 func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {64 func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {
64 query := `65 query := `
65- SELECT a.id, a.feed_url, COALESCE(f.title, ''), a.guid, a.title, a.url, a.author, a.summary, a.content,66+ SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
66 a.published, a.updated, a.fetched_at,67 a.published, a.updated, a.fetched_at,
67 COALESCE(r.is_read, 0)68 COALESCE(r.is_read, 0)
68 FROM articles a69 FROM articles a
@@ -88,7 +89,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit,
88 var articles []*Article89 var articles []*Article
89 for rows.Next() {90 for rows.Next() {
90 a := &Article{}91 a := &Article{}
91- if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.GUID, &a.Title, &a.URL, &a.Author,92+ if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
92 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,93 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
93 &a.IsRead); err != nil {94 &a.IsRead); err != nil {
94 return nil, err95 return nil, err
@@ -100,7 +101,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit,
100 101
101 func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {102 func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {
102 query := `103 query := `
103- SELECT a.id, a.feed_url, COALESCE(f.title, ''), a.guid, a.title, a.url, a.author, a.summary, a.content,104+ SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
104 a.published, a.updated, a.fetched_at,105 a.published, a.updated, a.fetched_at,
105 COALESCE(r.is_read, 0)106 COALESCE(r.is_read, 0)
106 FROM articles a107 FROM articles a
@@ -127,7 +128,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l
127 var articles []*Article128 var articles []*Article
128 for rows.Next() {129 for rows.Next() {
129 a := &Article{}130 a := &Article{}
130- if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.GUID, &a.Title, &a.URL, &a.Author,131+ if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
131 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,132 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
132 &a.IsRead); err != nil {133 &a.IsRead); err != nil {
133 return nil, err134 return nil, err
@@ -139,7 +140,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l
139 140
140 func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {141 func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) {
141 query := `142 query := `
142- SELECT a.id, a.feed_url, COALESCE(f.title, ''), a.guid, a.title, a.url, a.author, a.summary, a.content,143+ SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
143 a.published, a.updated, a.fetched_at,144 a.published, a.updated, a.fetched_at,
144 COALESCE(r.is_read, 0)145 COALESCE(r.is_read, 0)
145 FROM articles a146 FROM articles a
@@ -166,7 +167,7 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim
166 var articles []*Article167 var articles []*Article
167 for rows.Next() {168 for rows.Next() {
168 a := &Article{}169 a := &Article{}
169- if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.GUID, &a.Title, &a.URL, &a.Author,170+ if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
170 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,171 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
171 &a.IsRead); err != nil {172 &a.IsRead); err != nil {
172 return nil, err173 return nil, err
modified internal/db/cluster.go +4 -2
@@ -205,7 +205,7 @@ func (db *DB) ComputeFeedRecommendations(ctx context.Context, userDID string) er
205205
206206 func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit int) ([]map[string]any, error) {
207207 rows, err := db.QueryContext(ctx, `
208- SELECT r.feed_url, r.score, f.title, f.site_url, f.description, f.subscriber_count
208+ SELECT r.feed_url, r.score, f.title, f.site_url, f.description, f.subscriber_count, f.favicon_url
209209 FROM user_feed_recommendations r
210210 JOIN feeds f ON f.feed_url = r.feed_url
211211 WHERE r.user_did = ?
@@ -222,8 +222,9 @@ func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit
222222 var feedURL string
223223 var score float64
224224 var title, siteURL, description sql.NullString
225+ var faviconURL sql.NullString
225226 var subCount int
226- if err := rows.Scan(&feedURL, &score, &title, &siteURL, &description, &subCount); err != nil {
227+ if err := rows.Scan(&feedURL, &score, &title, &siteURL, &description, &subCount, &faviconURL); err != nil {
227228 return nil, err
228229 }
229230 results = append(results, map[string]any{
@@ -233,6 +234,7 @@ func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit
233234 "site_url": siteURL,
234235 "description": description,
235236 "subscriber_count": subCount,
237+ "favicon_url": faviconURL,
236238 })
237239 }
238240 return results, rows.Err()
@@ -205,7 +205,7 @@ func (db *DB) ComputeFeedRecommendations(ctx context.Context, userDID string) er
205 205
206 func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit int) ([]map[string]any, error) {206 func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit int) ([]map[string]any, error) {
207 rows, err := db.QueryContext(ctx, `207 rows, err := db.QueryContext(ctx, `
208- SELECT r.feed_url, r.score, f.title, f.site_url, f.description, f.subscriber_count208+ SELECT r.feed_url, r.score, f.title, f.site_url, f.description, f.subscriber_count, f.favicon_url
209 FROM user_feed_recommendations r209 FROM user_feed_recommendations r
210 JOIN feeds f ON f.feed_url = r.feed_url210 JOIN feeds f ON f.feed_url = r.feed_url
211 WHERE r.user_did = ?211 WHERE r.user_did = ?
@@ -222,8 +222,9 @@ func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit
222 var feedURL string222 var feedURL string
223 var score float64223 var score float64
224 var title, siteURL, description sql.NullString224 var title, siteURL, description sql.NullString
225+ var faviconURL sql.NullString
225 var subCount int226 var subCount int
226- if err := rows.Scan(&feedURL, &score, &title, &siteURL, &description, &subCount); err != nil {227+ if err := rows.Scan(&feedURL, &score, &title, &siteURL, &description, &subCount, &faviconURL); err != nil {
227 return nil, err228 return nil, err
228 }229 }
229 results = append(results, map[string]any{230 results = append(results, map[string]any{
@@ -233,6 +234,7 @@ func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit
233 "site_url": siteURL,234 "site_url": siteURL,
234 "description": description,235 "description": description,
235 "subscriber_count": subCount,236 "subscriber_count": subCount,
237+ "favicon_url": faviconURL,
236 })238 })
237 }239 }
238 return results, rows.Err()240 return results, rows.Err()
modified internal/db/feed.go +3 -2
@@ -35,6 +35,7 @@ type Subscription struct {
3535 UnreadCount int
3636 URI sql.NullString
3737 CID sql.NullString
38+ FaviconURL sql.NullString
3839 }
3940
4041 func (db *DB) UpsertFeed(ctx context.Context, feed *Feed) error {
@@ -228,7 +229,7 @@ func (db *DB) GetSubscription(ctx context.Context, userDID, feedURL string) (*Su
228229
229230 func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, limit, offset int) ([]*Subscription, error) {
230231 query := `SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at,
231- s.uri, s.cid
232+ s.uri, s.cid, f.favicon_url
232233 FROM subscriptions s
233234 LEFT JOIN feeds f ON s.feed_url = f.feed_url
234235 WHERE s.user_did = ?`
@@ -250,7 +251,7 @@ func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, l
250251 var subs []*Subscription
251252 for rows.Next() {
252253 s := &Subscription{}
253- if err := rows.Scan(&s.ID, &s.UserDID, &s.FeedURL, &s.FeedTitle, &s.Category, &s.AddedAt, &s.URI, &s.CID); err != nil {
254+ if err := rows.Scan(&s.ID, &s.UserDID, &s.FeedURL, &s.FeedTitle, &s.Category, &s.AddedAt, &s.URI, &s.CID, &s.FaviconURL); err != nil {
254255 return nil, err
255256 }
256257 subs = append(subs, s)
@@ -35,6 +35,7 @@ type Subscription struct {
35 UnreadCount int35 UnreadCount int
36 URI sql.NullString36 URI sql.NullString
37 CID sql.NullString37 CID sql.NullString
38+ FaviconURL sql.NullString
38 }39 }
39 40
40 func (db *DB) UpsertFeed(ctx context.Context, feed *Feed) error {41 func (db *DB) UpsertFeed(ctx context.Context, feed *Feed) error {
@@ -228,7 +229,7 @@ func (db *DB) GetSubscription(ctx context.Context, userDID, feedURL string) (*Su
228 229
229 func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, limit, offset int) ([]*Subscription, error) {230 func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, limit, offset int) ([]*Subscription, error) {
230 query := `SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at,231 query := `SELECT s.id, s.user_did, s.feed_url, COALESCE(s.title, f.title, ''), s.category, s.added_at,
231- s.uri, s.cid232+ s.uri, s.cid, f.favicon_url
232 FROM subscriptions s233 FROM subscriptions s
233 LEFT JOIN feeds f ON s.feed_url = f.feed_url234 LEFT JOIN feeds f ON s.feed_url = f.feed_url
234 WHERE s.user_did = ?`235 WHERE s.user_did = ?`
@@ -250,7 +251,7 @@ func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, l
250 var subs []*Subscription251 var subs []*Subscription
251 for rows.Next() {252 for rows.Next() {
252 s := &Subscription{}253 s := &Subscription{}
253- if err := rows.Scan(&s.ID, &s.UserDID, &s.FeedURL, &s.FeedTitle, &s.Category, &s.AddedAt, &s.URI, &s.CID); err != nil {254+ if err := rows.Scan(&s.ID, &s.UserDID, &s.FeedURL, &s.FeedTitle, &s.Category, &s.AddedAt, &s.URI, &s.CID, &s.FaviconURL); err != nil {
254 return nil, err255 return nil, err
255 }256 }
256 subs = append(subs, s)257 subs = append(subs, s)
modified internal/db/social.go +5 -2
@@ -201,6 +201,7 @@ type TrendingItem struct {
201201 Summary string
202202 FeedURL string
203203 FeedTitle string
204+ FaviconURL string
204205 LikeCount int
205206 AnnotationCount int
206207 }
@@ -209,6 +210,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
209210 rows, err := db.QueryContext(ctx, fmt.Sprintf(`
210211 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
211212 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
213+ COALESCE(f.favicon_url, ''),
212214 COUNT(DISTINCT l.id) AS like_count,
213215 COUNT(DISTINCT a.id) AS annotation_count
214216 FROM likes l
@@ -235,7 +237,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
235237 for rows.Next() {
236238 item := &TrendingItem{}
237239 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
238- &item.Summary, &item.FeedURL, &item.FeedTitle,
240+ &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,
239241 &item.LikeCount, &item.AnnotationCount); err != nil {
240242 return nil, err
241243 }
@@ -248,6 +250,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off
248250 rows, err := db.QueryContext(ctx, fmt.Sprintf(`
249251 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
250252 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
253+ COALESCE(f.favicon_url, ''),
251254 COUNT(DISTINCT l.id) AS like_count,
252255 COUNT(DISTINCT a.id) AS annotation_count
253256 FROM likes l
@@ -268,7 +271,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off
268271 for rows.Next() {
269272 item := &TrendingItem{}
270273 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
271- &item.Summary, &item.FeedURL, &item.FeedTitle,
274+ &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,
272275 &item.LikeCount, &item.AnnotationCount); err != nil {
273276 return nil, err
274277 }
@@ -201,6 +201,7 @@ type TrendingItem struct {
201 Summary string201 Summary string
202 FeedURL string202 FeedURL string
203 FeedTitle string203 FeedTitle string
204+ FaviconURL string
204 LikeCount int205 LikeCount int
205 AnnotationCount int206 AnnotationCount int
206 }207 }
@@ -209,6 +210,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
209 rows, err := db.QueryContext(ctx, fmt.Sprintf(`210 rows, err := db.QueryContext(ctx, fmt.Sprintf(`
210 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),211 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
211 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),212 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
213+ COALESCE(f.favicon_url, ''),
212 COUNT(DISTINCT l.id) AS like_count,214 COUNT(DISTINCT l.id) AS like_count,
213 COUNT(DISTINCT a.id) AS annotation_count215 COUNT(DISTINCT a.id) AS annotation_count
214 FROM likes l216 FROM likes l
@@ -235,7 +237,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
235 for rows.Next() {237 for rows.Next() {
236 item := &TrendingItem{}238 item := &TrendingItem{}
237 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,239 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
238- &item.Summary, &item.FeedURL, &item.FeedTitle,240+ &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,
239 &item.LikeCount, &item.AnnotationCount); err != nil {241 &item.LikeCount, &item.AnnotationCount); err != nil {
240 return nil, err242 return nil, err
241 }243 }
@@ -248,6 +250,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off
248 rows, err := db.QueryContext(ctx, fmt.Sprintf(`250 rows, err := db.QueryContext(ctx, fmt.Sprintf(`
249 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),251 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
250 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),252 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
253+ COALESCE(f.favicon_url, ''),
251 COUNT(DISTINCT l.id) AS like_count,254 COUNT(DISTINCT l.id) AS like_count,
252 COUNT(DISTINCT a.id) AS annotation_count255 COUNT(DISTINCT a.id) AS annotation_count
253 FROM likes l256 FROM likes l
@@ -268,7 +271,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off
268 for rows.Next() {271 for rows.Next() {
269 item := &TrendingItem{}272 item := &TrendingItem{}
270 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,273 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
271- &item.Summary, &item.FeedURL, &item.FeedTitle,274+ &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,
272 &item.LikeCount, &item.AnnotationCount); err != nil {275 &item.LikeCount, &item.AnnotationCount); err != nil {
273 return nil, err276 return nil, err
274 }277 }
modified internal/db/store.go +4 -0
@@ -62,3 +62,7 @@ func (a *FeedStoreAdapter) MarkFeedFetched(ctx context.Context, feedURL, etag, l
6262 func (a *FeedStoreAdapter) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error {
6363 return a.db.MarkFeedFetchError(ctx, feedURL, lastError)
6464 }
65+
66+func (a *FeedStoreAdapter) UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error {
67+ return a.db.UpdateFeedFavicon(ctx, feedURL, faviconURL)
68+}
@@ -62,3 +62,7 @@ func (a *FeedStoreAdapter) MarkFeedFetched(ctx context.Context, feedURL, etag, l
62 func (a *FeedStoreAdapter) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error {62 func (a *FeedStoreAdapter) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error {
63 return a.db.MarkFeedFetchError(ctx, feedURL, lastError)63 return a.db.MarkFeedFetchError(ctx, feedURL, lastError)
64 }64 }
65+
66+func (a *FeedStoreAdapter) UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error {
67+ return a.db.UpdateFeedFavicon(ctx, feedURL, faviconURL)
68+}
modified internal/feed/fetcher.go +10 -0
@@ -72,6 +72,7 @@ type FeedStore interface {
7272 UpsertArticle(ctx context.Context, article *Article) (int64, error)
7373 MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error
7474 MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error
75+ UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error
7576 }
7677
7778 type fetchCall struct {
@@ -168,4 +169,13 @@ func (s *Scheduler) FetchFeed(ctx context.Context, feed *Feed) {
168169 if err := s.store.MarkFeedFetched(ctx, feed.URL, newEtag, newLastModified); err != nil {
169170 s.logger.Error("failed to update feed fetch result", "error", err, "feed", feed.URL)
170171 }
172+
173+ if feed.SiteURL != "" {
174+ go func() {
175+ discResult, err := Discover(context.Background(), feed.SiteURL)
176+ if err == nil && discResult.Favicon != "" {
177+ _ = s.store.UpdateFeedFavicon(context.Background(), feed.URL, discResult.Favicon)
178+ }
179+ }()
180+ }
171181 }
@@ -72,6 +72,7 @@ type FeedStore interface {
72 UpsertArticle(ctx context.Context, article *Article) (int64, error)72 UpsertArticle(ctx context.Context, article *Article) (int64, error)
73 MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error73 MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error
74 MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error74 MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error
75+ UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error
75 }76 }
76 77
77 type fetchCall struct {78 type fetchCall struct {
@@ -168,4 +169,13 @@ func (s *Scheduler) FetchFeed(ctx context.Context, feed *Feed) {
168 if err := s.store.MarkFeedFetched(ctx, feed.URL, newEtag, newLastModified); err != nil {169 if err := s.store.MarkFeedFetched(ctx, feed.URL, newEtag, newLastModified); err != nil {
169 s.logger.Error("failed to update feed fetch result", "error", err, "feed", feed.URL)170 s.logger.Error("failed to update feed fetch result", "error", err, "feed", feed.URL)
170 }171 }
172+
173+ if feed.SiteURL != "" {
174+ go func() {
175+ discResult, err := Discover(context.Background(), feed.SiteURL)
176+ if err == nil && discResult.Favicon != "" {
177+ _ = s.store.UpdateFeedFavicon(context.Background(), feed.URL, discResult.Favicon)
178+ }
179+ }()
180+ }
171 }181 }
modified internal/tmpl/feeds.html +8 -5
@@ -54,12 +54,15 @@
5454 <div id="feed-list" class="bg-spot-surface rounded-xl divide-y divide-spot-divider">
5555 {{range .Subscriptions}}
5656 <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
57- <div class="min-w-0 flex-1">
58- <div class="flex items-center gap-2">
59- <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
60- {{if .Category.Valid}}<span class="text-xs bg-spot-hover text-spot-secondary px-2 py-0.5 rounded-full shrink-0">{{.Category.String}}</span>{{end}}
57+ <div class="min-w-0 flex-1 flex items-center gap-3">
58+ {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted">{{template "icon-globe"}}</span>{{end}}
59+ <div class="min-w-0 flex-1">
60+ <div class="flex items-center gap-2">
61+ <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
62+ {{if .Category.Valid}}<span class="text-xs bg-spot-hover text-spot-secondary px-2 py-0.5 rounded-full shrink-0">{{.Category.String}}</span>{{end}}
63+ </div>
64+ <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div>
6165 </div>
62- <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div>
6366 </div>
6467 <div class="flex items-center gap-3">
6568 {{if .UnreadCount}}<span class="text-xs bg-spot-green/20 text-spot-green px-2.5 py-0.5 rounded-full font-bold">{{.UnreadCount}}</span>{{end}}
@@ -54,12 +54,15 @@
54 <div id="feed-list" class="bg-spot-surface rounded-xl divide-y divide-spot-divider">54 <div id="feed-list" class="bg-spot-surface rounded-xl divide-y divide-spot-divider">
55 {{range .Subscriptions}}55 {{range .Subscriptions}}
56 <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">56 <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
57- <div class="min-w-0 flex-1">57+ <div class="min-w-0 flex-1 flex items-center gap-3">
58- <div class="flex items-center gap-2">58+ {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted">{{template "icon-globe"}}</span>{{end}}
59- <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>59+ <div class="min-w-0 flex-1">
60- {{if .Category.Valid}}<span class="text-xs bg-spot-hover text-spot-secondary px-2 py-0.5 rounded-full shrink-0">{{.Category.String}}</span>{{end}}60+ <div class="flex items-center gap-2">
61+ <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
62+ {{if .Category.Valid}}<span class="text-xs bg-spot-hover text-spot-secondary px-2 py-0.5 rounded-full shrink-0">{{.Category.String}}</span>{{end}}
63+ </div>
64+ <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div>
61 </div>65 </div>
62- <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div>
63 </div>66 </div>
64 <div class="flex items-center gap-3">67 <div class="flex items-center gap-3">
65 {{if .UnreadCount}}<span class="text-xs bg-spot-green/20 text-spot-green px-2.5 py-0.5 rounded-full font-bold">{{.UnreadCount}}</span>{{end}}68 {{if .UnreadCount}}<span class="text-xs bg-spot-green/20 text-spot-green px-2.5 py-0.5 rounded-full font-bold">{{.UnreadCount}}</span>{{end}}
modified internal/tmpl/partials/article-card.html +1 -0
@@ -7,6 +7,7 @@
77 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>
88 </div>
99 <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2">
10+ {{if .FeedFaviconURL.Valid}}<img src="{{.FeedFaviconURL.String}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}}
1011 {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}}
1112 {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}
1213 <span class="text-spot-muted">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
@@ -7,6 +7,7 @@
7 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>7 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>
8 </div>8 </div>
9 <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2">9 <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2">
10+ {{if .FeedFaviconURL.Valid}}<img src="{{.FeedFaviconURL.String}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}}
10 {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}}11 {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}}
11 {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}12 {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}
12 <span class="text-spot-muted">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>13 <span class="text-spot-muted">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
modified internal/tmpl/partials/article-list.html +1 -0
@@ -5,6 +5,7 @@
55 <div class="min-w-0 flex-1">
66 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>
77 <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2">
8+ {{if .FeedFaviconURL.Valid}}<img src="{{.FeedFaviconURL.String}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}}
89 {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}}
910 {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}
1011 <span class="text-spot-muted">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
@@ -5,6 +5,7 @@
5 <div class="min-w-0 flex-1">5 <div class="min-w-0 flex-1">
6 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>6 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>
7 <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2">7 <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2">
8+ {{if .FeedFaviconURL.Valid}}<img src="{{.FeedFaviconURL.String}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}}
8 {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}}9 {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}}
9 {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}10 {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}
10 <span class="text-spot-muted">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>11 <span class="text-spot-muted">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
modified internal/tmpl/partials/recommendation-card.html +8 -5
@@ -1,11 +1,14 @@
11 {{define "recommendation-card.html"}}
22 <div class="bg-spot-surface rounded-xl p-3 hover:bg-spot-hover-50 transition">
3- <div class="flex items-center justify-between">
4- <div class="min-w-0">
5- <div class="font-bold text-sm text-spot-text">{{if .title}}{{.title}}{{else}}{{.feed_url}}{{end}}</div>
6- {{if .description}}<p class="text-xs text-spot-secondary truncate mt-0.5">{{.description}}</p>{{end}}
3+ <div class="flex items-center justify-between gap-2">
4+ <div class="min-w-0 flex items-center gap-2">
5+ {{if .favicon_url}}{{if .favicon_url.Valid}}<img src="{{.favicon_url.String}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}}{{end}}
6+ <div class="min-w-0">
7+ <div class="font-bold text-sm text-spot-text truncate">{{if .title}}{{.title}}{{else}}{{.feed_url}}{{end}}</div>
8+ {{if .description}}<p class="text-xs text-spot-secondary truncate mt-0.5">{{.description}}</p>{{end}}
9+ </div>
710 </div>
8- <span class="text-xs text-spot-secondary shrink-0 ml-2">{{.subscriber_count}} subs</span>
11+ <span class="text-xs text-spot-secondary shrink-0">{{.subscriber_count}} subs</span>
912 </div>
1013 </div>
1114 {{end}}
@@ -1,11 +1,14 @@
1 {{define "recommendation-card.html"}}1 {{define "recommendation-card.html"}}
2 <div class="bg-spot-surface rounded-xl p-3 hover:bg-spot-hover-50 transition">2 <div class="bg-spot-surface rounded-xl p-3 hover:bg-spot-hover-50 transition">
3- <div class="flex items-center justify-between">3+ <div class="flex items-center justify-between gap-2">
4- <div class="min-w-0">4+ <div class="min-w-0 flex items-center gap-2">
5- <div class="font-bold text-sm text-spot-text">{{if .title}}{{.title}}{{else}}{{.feed_url}}{{end}}</div>5+ {{if .favicon_url}}{{if .favicon_url.Valid}}<img src="{{.favicon_url.String}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}}{{end}}
6- {{if .description}}<p class="text-xs text-spot-secondary truncate mt-0.5">{{.description}}</p>{{end}}6+ <div class="min-w-0">
7+ <div class="font-bold text-sm text-spot-text truncate">{{if .title}}{{.title}}{{else}}{{.feed_url}}{{end}}</div>
8+ {{if .description}}<p class="text-xs text-spot-secondary truncate mt-0.5">{{.description}}</p>{{end}}
9+ </div>
7 </div>10 </div>
8- <span class="text-xs text-spot-secondary shrink-0 ml-2">{{.subscriber_count}} subs</span>11+ <span class="text-xs text-spot-secondary shrink-0">{{.subscriber_count}} subs</span>
9 </div>12 </div>
10 </div>13 </div>
11 {{end}}14 {{end}}