Add favicon support to feed data and UIUnverified
67b0e8d parent: 2dcefd3 modified
internal/db/article.go +21 -20 | @@ -7,20 +7,21 @@ import ( | ||
| 7 | 7 | ) |
| 8 | 8 | |
| 9 | 9 | 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 | |
| 24 | 25 | } |
| 25 | 26 | |
| 26 | 27 | type ReadState struct { |
| @@ -62,7 +63,7 @@ func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) { | ||
| 62 | 63 | |
| 63 | 64 | func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { |
| 64 | 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 | 67 | a.published, a.updated, a.fetched_at, |
| 67 | 68 | COALESCE(r.is_read, 0) |
| 68 | 69 | FROM articles a |
| @@ -88,7 +89,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | ||
| 88 | 89 | var articles []*Article |
| 89 | 90 | for rows.Next() { |
| 90 | 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 | 93 | &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt, |
| 93 | 94 | &a.IsRead); err != nil { |
| 94 | 95 | return nil, err |
| @@ -100,7 +101,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | ||
| 100 | 101 | |
| 101 | 102 | func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { |
| 102 | 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 | 105 | a.published, a.updated, a.fetched_at, |
| 105 | 106 | COALESCE(r.is_read, 0) |
| 106 | 107 | FROM articles a |
| @@ -127,7 +128,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | ||
| 127 | 128 | var articles []*Article |
| 128 | 129 | for rows.Next() { |
| 129 | 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 | 132 | &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt, |
| 132 | 133 | &a.IsRead); err != nil { |
| 133 | 134 | return nil, err |
| @@ -139,7 +140,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | ||
| 139 | 140 | |
| 140 | 141 | func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, limit, offset int) ([]*Article, error) { |
| 141 | 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 | 144 | a.published, a.updated, a.fetched_at, |
| 144 | 145 | COALESCE(r.is_read, 0) |
| 145 | 146 | FROM articles a |
| @@ -166,7 +167,7 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | ||
| 166 | 167 | var articles []*Article |
| 167 | 168 | for rows.Next() { |
| 168 | 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 | 171 | &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt, |
| 171 | 172 | &a.IsRead); err != nil { |
| 172 | 173 | return nil, err |
| @@ -7,20 +7,21 @@ import ( | |||
| 7 | ) | 7 | ) |
| 8 | 8 | ||
| 9 | type Article struct { | 9 | type Article struct { |
| 10 | - ID int64 | 10 | + ID int64 |
| 11 | - FeedURL string | 11 | + FeedURL string |
| 12 | - FeedTitle string | 12 | + FeedTitle string |
| 13 | - GUID string | 13 | + FeedFaviconURL sql.NullString |
| 14 | - Title string | 14 | + GUID string |
| 15 | - URL sql.NullString | 15 | + Title string |
| 16 | - Author sql.NullString | 16 | + URL sql.NullString |
| 17 | - Summary sql.NullString | 17 | + Author sql.NullString |
| 18 | - Content sql.NullString | 18 | + Summary sql.NullString |
| 19 | - FullContent sql.NullString | 19 | + Content sql.NullString |
| 20 | - Published sql.NullTime | 20 | + FullContent sql.NullString |
| 21 | - Updated sql.NullTime | 21 | + Published sql.NullTime |
| 22 | - FetchedAt sql.NullTime | 22 | + Updated sql.NullTime |
| 23 | - IsRead sql.NullBool | 23 | + 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 a | 69 | FROM articles a |
| @@ -88,7 +89,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | |||
| 88 | var articles []*Article | 89 | 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, err | 95 | 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 a | 107 | FROM articles a |
| @@ -127,7 +128,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | |||
| 127 | var articles []*Article | 128 | 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, err | 134 | 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 a | 146 | FROM articles a |
| @@ -166,7 +167,7 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | |||
| 166 | var articles []*Article | 167 | 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, err | 173 | return nil, err |
modified
internal/db/cluster.go +4 -2 | @@ -205,7 +205,7 @@ func (db *DB) ComputeFeedRecommendations(ctx context.Context, userDID string) er | ||
| 205 | 205 | |
| 206 | 206 | func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit int) ([]map[string]any, error) { |
| 207 | 207 | 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 | |
| 209 | 209 | FROM user_feed_recommendations r |
| 210 | 210 | JOIN feeds f ON f.feed_url = r.feed_url |
| 211 | 211 | WHERE r.user_did = ? |
| @@ -222,8 +222,9 @@ func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit | ||
| 222 | 222 | var feedURL string |
| 223 | 223 | var score float64 |
| 224 | 224 | var title, siteURL, description sql.NullString |
| 225 | + var faviconURL sql.NullString | |
| 225 | 226 | 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 | 228 | return nil, err |
| 228 | 229 | } |
| 229 | 230 | results = append(results, map[string]any{ |
| @@ -233,6 +234,7 @@ func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit | ||
| 233 | 234 | "site_url": siteURL, |
| 234 | 235 | "description": description, |
| 235 | 236 | "subscriber_count": subCount, |
| 237 | + "favicon_url": faviconURL, | |
| 236 | 238 | }) |
| 237 | 239 | } |
| 238 | 240 | 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_count | 208 | + SELECT r.feed_url, r.score, f.title, f.site_url, f.description, f.subscriber_count, f.favicon_url |
| 209 | FROM user_feed_recommendations r | 209 | FROM user_feed_recommendations r |
| 210 | JOIN feeds f ON f.feed_url = r.feed_url | 210 | 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 string | 222 | var feedURL string |
| 223 | var score float64 | 223 | var score float64 |
| 224 | var title, siteURL, description sql.NullString | 224 | var title, siteURL, description sql.NullString |
| 225 | + var faviconURL sql.NullString | ||
| 225 | var subCount int | 226 | 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, err | 228 | 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 { | ||
| 35 | 35 | UnreadCount int |
| 36 | 36 | URI sql.NullString |
| 37 | 37 | CID sql.NullString |
| 38 | + FaviconURL sql.NullString | |
| 38 | 39 | } |
| 39 | 40 | |
| 40 | 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 | 230 | func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, limit, offset int) ([]*Subscription, error) { |
| 230 | 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.cid | |
| 232 | + s.uri, s.cid, f.favicon_url | |
| 232 | 233 | FROM subscriptions s |
| 233 | 234 | LEFT JOIN feeds f ON s.feed_url = f.feed_url |
| 234 | 235 | WHERE s.user_did = ?` |
| @@ -250,7 +251,7 @@ func (db *DB) ListSubscriptions(ctx context.Context, userDID, category string, l | ||
| 250 | 251 | var subs []*Subscription |
| 251 | 252 | for rows.Next() { |
| 252 | 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 | 255 | return nil, err |
| 255 | 256 | } |
| 256 | 257 | subs = append(subs, s) |
| @@ -35,6 +35,7 @@ type Subscription struct { | |||
| 35 | UnreadCount int | 35 | UnreadCount int |
| 36 | URI sql.NullString | 36 | URI sql.NullString |
| 37 | CID sql.NullString | 37 | 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.cid | 232 | + s.uri, s.cid, f.favicon_url |
| 232 | FROM subscriptions s | 233 | FROM subscriptions s |
| 233 | LEFT JOIN feeds f ON s.feed_url = f.feed_url | 234 | 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 []*Subscription | 251 | 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, err | 255 | 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 { | ||
| 201 | 201 | Summary string |
| 202 | 202 | FeedURL string |
| 203 | 203 | FeedTitle string |
| 204 | + FaviconURL string | |
| 204 | 205 | LikeCount int |
| 205 | 206 | AnnotationCount int |
| 206 | 207 | } |
| @@ -209,6 +210,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st | ||
| 209 | 210 | rows, err := db.QueryContext(ctx, fmt.Sprintf(` |
| 210 | 211 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 211 | 212 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| 213 | + COALESCE(f.favicon_url, ''), | |
| 212 | 214 | COUNT(DISTINCT l.id) AS like_count, |
| 213 | 215 | COUNT(DISTINCT a.id) AS annotation_count |
| 214 | 216 | FROM likes l |
| @@ -235,7 +237,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st | ||
| 235 | 237 | for rows.Next() { |
| 236 | 238 | item := &TrendingItem{} |
| 237 | 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 | 241 | &item.LikeCount, &item.AnnotationCount); err != nil { |
| 240 | 242 | return nil, err |
| 241 | 243 | } |
| @@ -248,6 +250,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off | ||
| 248 | 250 | rows, err := db.QueryContext(ctx, fmt.Sprintf(` |
| 249 | 251 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 250 | 252 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| 253 | + COALESCE(f.favicon_url, ''), | |
| 251 | 254 | COUNT(DISTINCT l.id) AS like_count, |
| 252 | 255 | COUNT(DISTINCT a.id) AS annotation_count |
| 253 | 256 | FROM likes l |
| @@ -268,7 +271,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off | ||
| 268 | 271 | for rows.Next() { |
| 269 | 272 | item := &TrendingItem{} |
| 270 | 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 | 275 | &item.LikeCount, &item.AnnotationCount); err != nil { |
| 273 | 276 | return nil, err |
| 274 | 277 | } |
| @@ -201,6 +201,7 @@ type TrendingItem struct { | |||
| 201 | Summary string | 201 | Summary string |
| 202 | FeedURL string | 202 | FeedURL string |
| 203 | FeedTitle string | 203 | FeedTitle string |
| 204 | + FaviconURL string | ||
| 204 | LikeCount int | 205 | LikeCount int |
| 205 | AnnotationCount int | 206 | 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_count | 215 | COUNT(DISTINCT a.id) AS annotation_count |
| 214 | FROM likes l | 216 | 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, err | 242 | 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_count | 255 | COUNT(DISTINCT a.id) AS annotation_count |
| 253 | FROM likes l | 256 | 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, err | 276 | 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 | ||
| 62 | 62 | func (a *FeedStoreAdapter) MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error { |
| 63 | 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 | +} | |
| @@ -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 { | ||
| 72 | 72 | UpsertArticle(ctx context.Context, article *Article) (int64, error) |
| 73 | 73 | MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error |
| 74 | 74 | MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error |
| 75 | + UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error | |
| 75 | 76 | } |
| 76 | 77 | |
| 77 | 78 | type fetchCall struct { |
| @@ -168,4 +169,13 @@ func (s *Scheduler) FetchFeed(ctx context.Context, feed *Feed) { | ||
| 168 | 169 | if err := s.store.MarkFeedFetched(ctx, feed.URL, newEtag, newLastModified); err != nil { |
| 169 | 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 | } |
| @@ -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) error | 73 | MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error |
| 74 | MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error | 74 | 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 @@ | ||
| 54 | 54 | <div id="feed-list" class="bg-spot-surface rounded-xl divide-y divide-spot-divider"> |
| 55 | 55 | {{range .Subscriptions}} |
| 56 | 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"> | |
| 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> | |
| 61 | 65 | </div> |
| 62 | - <div class="text-xs text-spot-muted truncate mt-0.5">{{.FeedURL}}</div> | |
| 63 | 66 | </div> |
| 64 | 67 | <div class="flex items-center gap-3"> |
| 65 | 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}} |
| @@ -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 @@ | ||
| 7 | 7 | <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> |
| 8 | 8 | </div> |
| 9 | 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 | 11 | {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}} |
| 11 | 12 | {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}} |
| 12 | 13 | <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 @@ | ||
| 5 | 5 | <div class="min-w-0 flex-1"> |
| 6 | 6 | <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> |
| 7 | 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 | 9 | {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}} |
| 9 | 10 | {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}} |
| 10 | 11 | <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 @@ | ||
| 1 | 1 | {{define "recommendation-card.html"}} |
| 2 | 2 | <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> | |
| 7 | 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 | 12 | </div> |
| 10 | 13 | </div> |
| 11 | 14 | {{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}} |
modified
internal/tmpl/trending.html +1 -0 | @@ -19,6 +19,7 @@ | ||
| 19 | 19 | <div class="min-w-0 flex-1"> |
| 20 | 20 | <a href="/articles/{{$t.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{$t.Title}}</a> |
| 21 | 21 | <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> |
| 22 | + {{if $t.FaviconURL}}<img src="{{$t.FaviconURL}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}} | |
| 22 | 23 | {{if $t.Author}}<span>{{$t.Author}}</span>{{end}} |
| 23 | 24 | <span class="text-spot-muted">{{if $t.FeedTitle}}{{$t.FeedTitle}}{{else}}{{$t.FeedURL}}{{end}}</span> |
| 24 | 25 | </div> |
| @@ -19,6 +19,7 @@ | |||
| 19 | <div class="min-w-0 flex-1"> | 19 | <div class="min-w-0 flex-1"> |
| 20 | <a href="/articles/{{$t.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{$t.Title}}</a> | 20 | <a href="/articles/{{$t.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{$t.Title}}</a> |
| 21 | <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> | 21 | <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> |
| 22 | + {{if $t.FaviconURL}}<img src="{{$t.FaviconURL}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}} | ||
| 22 | {{if $t.Author}}<span>{{$t.Author}}</span>{{end}} | 23 | {{if $t.Author}}<span>{{$t.Author}}</span>{{end}} |
| 23 | <span class="text-spot-muted">{{if $t.FeedTitle}}{{$t.FeedTitle}}{{else}}{{$t.FeedURL}}{{end}}</span> | 24 | <span class="text-spot-muted">{{if $t.FeedTitle}}{{$t.FeedTitle}}{{else}}{{$t.FeedURL}}{{end}}</span> |
| 24 | </div> | 25 | </div> |