diff --git a/internal/repository/collection_repo.go b/internal/repository/collection_repo.go index 4a4f018..1f1a3de 100644 --- a/internal/repository/collection_repo.go +++ b/internal/repository/collection_repo.go @@ -200,7 +200,10 @@ func (r *CollectionRepo) CollectionsContaining(ctx context.Context, postID, user return ids, rows.Err() } -// PostsIn returns the posts (with images + author) inside a collection. +// PostsIn returns the posts inside a collection, ordered by collection +// position then added_at. Images are NOT hydrated — call +// PostRepo.ImagesFor on the returned ids if your caller renders the +// post visually. The CollectionService.Get path already does this. func (r *CollectionRepo) PostsIn(ctx context.Context, collectionID string) ([]model.Post, error) { rows, err := r.pg.Pool.Query(ctx, ` SELECT p.id, p.user_id, p.caption, p.source_url, COALESCE(p.source_title, ''), @@ -234,8 +237,6 @@ func (r *CollectionRepo) PostsIn(ctx context.Context, collectionID string) ([]mo if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate collection posts: %w", err) } - // Defer image hydration to caller via PostRepo if needed; for v1 we just - // return without images here (the management page can fetch separately). return posts, nil } diff --git a/internal/repository/post_repo.go b/internal/repository/post_repo.go index 52bcd2c..e30fafe 100644 --- a/internal/repository/post_repo.go +++ b/internal/repository/post_repo.go @@ -118,7 +118,7 @@ func (r *PostRepo) FindByID(ctx context.Context, id string) (*model.Post, error) } return nil, fmt.Errorf("find post: %w", err) } - imgs, err := r.imagesFor(ctx, []string{p.ID}) + imgs, err := r.ImagesFor(ctx, []string{p.ID}) if err != nil { return nil, err } @@ -217,7 +217,7 @@ func (r *PostRepo) collectPosts(ctx context.Context, total int, query string, ar } if len(ids) > 0 { - imgs, err := r.imagesFor(ctx, ids) + imgs, err := r.ImagesFor(ctx, ids) if err != nil { return nil, 0, err } @@ -228,9 +228,12 @@ func (r *PostRepo) collectPosts(ctx context.Context, total int, query string, ar return posts, total, nil } -// imagesFor returns a map post_id → [images...] for a batch of post IDs in -// a single query so we don't N+1 the feed endpoint. -func (r *PostRepo) imagesFor(ctx context.Context, postIDs []string) (map[string][]model.PostImage, error) { +// ImagesFor returns a map post_id → [images...] for a batch of post IDs in +// a single query so we don't N+1 the feed endpoint. Exported because the +// CollectionService needs to hydrate images onto posts that come back +// from CollectionRepo.PostsIn (which only joins posts/users — not +// post_images — to keep that query focused). +func (r *PostRepo) ImagesFor(ctx context.Context, postIDs []string) (map[string][]model.PostImage, error) { out := make(map[string][]model.PostImage, len(postIDs)) if len(postIDs) == 0 { return out, nil diff --git a/internal/service/social_service.go b/internal/service/social_service.go index 13507cd..fde2175 100644 --- a/internal/service/social_service.go +++ b/internal/service/social_service.go @@ -320,6 +320,23 @@ func (s *CollectionService) Get(ctx context.Context, id, viewerID string) (*mode if err != nil { return nil, nil, err } + // PostsIn returns posts without images (single-table boundary). Hydrate + // them in one batched query so callers — and the post cards rendered + // inside collection carousels — can show the post's first image as + // background instead of falling back to the empty quote panel. + if len(posts) > 0 { + ids := make([]string, len(posts)) + for i := range posts { + ids[i] = posts[i].ID + } + imgs, err := s.posts.ImagesFor(ctx, ids) + if err != nil { + return nil, nil, err + } + for i := range posts { + posts[i].Images = imgs[posts[i].ID] + } + } return c, posts, nil }