diff --git a/messages/de.json b/messages/de.json
index 9f76ae5..0f1098c 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -203,7 +203,8 @@
"FeedList": {
"forYou": "Für dich",
"recent": "Aktuell",
- "allCaughtUp": "Du bist auf dem neuesten Stand. Folge mehr Tieren für weitere Beiträge."
+ "allCaughtUp": "Du bist auf dem neuesten Stand. Folge mehr Tieren für weitere Beiträge.",
+ "discoveryBanner": "Du folgst noch niemandem — hier siehst du, was andere Tiere wie deins gerade teilen."
},
"ProfileTabs": {
"posts": "Beiträge",
diff --git a/messages/en.json b/messages/en.json
index 6c0b5d9..0ea052f 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -203,7 +203,8 @@
"FeedList": {
"forYou": "For you",
"recent": "Recent",
- "allCaughtUp": "You're all caught up. Follow more pets to see more posts."
+ "allCaughtUp": "You're all caught up. Follow more pets to see more posts.",
+ "discoveryBanner": "You're not following anyone yet — here's what other pets like yours are sharing."
},
"ProfileTabs": {
"posts": "Posts",
diff --git a/src/__tests__/feed.test.ts b/src/__tests__/feed.test.ts
index 67df55d..6325a11 100644
--- a/src/__tests__/feed.test.ts
+++ b/src/__tests__/feed.test.ts
@@ -200,6 +200,63 @@ describe("feed.getFeed", () => {
);
});
+ it("cold-start: empty follow feed falls back to species-matched discovery posts", async () => {
+ const { caller, mockPrisma } = createCaller();
+
+ mockPrisma.pet.findUnique.mockResolvedValue({
+ id: "pet-1",
+ ownerId: "owner-1",
+ speciesId: "species-cat",
+ });
+
+ mockGetFeedPage.mockResolvedValue({ postIds: [], nextCursor: null });
+ mockPrisma.block.findMany.mockResolvedValue([]);
+
+ const discoveryPost = {
+ id: "post-discovery-1",
+ petId: "pet-stranger",
+ caption: "Hello from another cat",
+ createdAt: new Date(),
+ reactionCount: 2,
+ commentCount: 1,
+ repostCount: 0,
+ pet: { id: "pet-stranger", name: "Whiskers", avatarKey: null },
+ images: [],
+ milestone: null,
+ };
+ mockPrisma.post.findMany.mockResolvedValue([discoveryPost]);
+
+ const result = await caller.getFeed({ petId: "pet-1", cursor: null, limit: 20 });
+
+ expect(result.posts).toEqual([discoveryPost]);
+ expect(result.source).toBe("discovery");
+ expect(mockPrisma.post.findMany).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: expect.objectContaining({
+ pet: expect.objectContaining({ speciesId: "species-cat" }),
+ }),
+ })
+ );
+ });
+
+ it("cold-start: discovery fallback does not trigger when paginating (cursor present)", async () => {
+ const { caller, mockPrisma } = createCaller();
+
+ mockPrisma.pet.findUnique.mockResolvedValue({
+ id: "pet-1",
+ ownerId: "owner-1",
+ speciesId: "species-cat",
+ });
+
+ mockGetFeedPage.mockResolvedValue({ postIds: [], nextCursor: null });
+
+ const result = await caller.getFeed({ petId: "pet-1", cursor: 1700000000000, limit: 20 });
+
+ expect(result.posts).toEqual([]);
+ expect(result.source).toBe("following");
+ expect(mockPrisma.post.findMany).not.toHaveBeenCalled();
+ });
+
it("FEED-01 — throws FORBIDDEN when petId does not belong to requesting user", async () => {
const { caller, mockPrisma } = createCaller({ userId: "owner-1" });
diff --git a/src/components/feed/FeedList.tsx b/src/components/feed/FeedList.tsx
index 06c8627..1c15786 100644
--- a/src/components/feed/FeedList.tsx
+++ b/src/components/feed/FeedList.tsx
@@ -74,6 +74,10 @@ export function FeedList() {
const ads = useMemo(() => activeAds ?? [], [activeAds]);
const allPosts = useMemo(() => data?.pages.flatMap((page) => page.posts) ?? [], [data]);
+ // Cold-start: brand-new pet with no follows yet gets species-matched
+ // discovery posts instead of a dead feed (see feed.ts's getDiscoveryPosts).
+ // Only the first page can carry this flag.
+ const isDiscoveryFeed = data?.pages[0]?.source === "discovery";
// Build interleaved feed: inject one ad every AD_INTERVAL posts (cycle through ads)
type Post = (typeof allPosts)[number];
@@ -138,6 +142,15 @@ export function FeedList() {
{/* New-user welcome card */}
{activePetId &&
+ + {t("discoveryBanner")} +
+ )} + {/* Feed posts + injected ads */}