diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15f7938..273f77c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
+## 0.50.2 — 2026-07-19 10:15
+
+### Fixed
+- Recipes without a cover photo now get a varied gradient + food/drink icon instead of the same flat emoji everywhere — deterministic per recipe, so it stays stable across reloads. Applied to the Recipes grid/list, Explore/collections cards, and public profile pages.
+
## 0.50.1 — 2026-07-19 09:30
### Fixed
diff --git a/apps/web/app/(app)/u/[username]/page.tsx b/apps/web/app/(app)/u/[username]/page.tsx
index 6090efe..2c4771a 100644
--- a/apps/web/app/(app)/u/[username]/page.tsx
+++ b/apps/web/app/(app)/u/[username]/page.tsx
@@ -24,6 +24,7 @@ import { FollowButton } from "@/components/social/follow-button";
import { BlockButton } from "@/components/social/block-button";
import { MessageButton } from "@/components/social/message-button";
import { getPublicUrl } from "@/lib/storage";
+import { RecipeCoverPlaceholder } from "@/components/recipe/recipe-cover-placeholder";
import { ProfileTabs, type HistoryData, type PhotosData } from "@/components/profile/profile-tabs";
const PAGE_SIZE = 24;
@@ -247,9 +248,7 @@ export default async function UserProfilePage({ params, searchParams }: Params)
className="object-cover group-hover:scale-105 transition-transform duration-200"
/>
) : (
-
- 🍽️
-
+
)}
diff --git a/apps/web/components/recipe/recipe-card.tsx b/apps/web/components/recipe/recipe-card.tsx
index ff1f542..e3c1ec4 100644
--- a/apps/web/components/recipe/recipe-card.tsx
+++ b/apps/web/components/recipe/recipe-card.tsx
@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { Clock, Users, Lock, Globe, Link2, ExternalLink, UserCheck } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
+import { RecipeCoverPlaceholder } from "@/components/recipe/recipe-cover-placeholder";
import { getPublicUrl } from "@/lib/storage";
type Recipe = {
@@ -57,9 +58,7 @@ export function RecipeCard({ recipe }: { recipe: Recipe }) {
/>
) : (
-
- {recipe.recipeType === "drink" ? "🍹" : "🍽️"}
-
+
)}
diff --git a/apps/web/components/recipe/recipe-cover-placeholder.tsx b/apps/web/components/recipe/recipe-cover-placeholder.tsx
new file mode 100644
index 0000000..5658432
--- /dev/null
+++ b/apps/web/components/recipe/recipe-cover-placeholder.tsx
@@ -0,0 +1,20 @@
+import { getRecipePlaceholder } from "@/lib/recipe-placeholder";
+import { cn } from "@/lib/utils";
+
+/** Gradient + food/drink icon shown in place of a cover photo — deterministic per recipe id, so it stays stable across renders instead of flickering between icons/colors. */
+export function RecipeCoverPlaceholder({
+ recipe,
+ className,
+ iconClassName,
+}: {
+ recipe: { id: string; recipeType?: "dish" | "drink" };
+ className?: string;
+ iconClassName?: string;
+}) {
+ const { gradient, Icon } = getRecipePlaceholder(recipe);
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/components/recipe/recipe-grid-card.tsx b/apps/web/components/recipe/recipe-grid-card.tsx
index e3a28ee..7188e5e 100644
--- a/apps/web/components/recipe/recipe-grid-card.tsx
+++ b/apps/web/components/recipe/recipe-grid-card.tsx
@@ -6,6 +6,7 @@ import { Globe, Lock, Link2, ExternalLink, ChefHat, Clock, Users, Utensils, User
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { FavoriteButton } from "@/components/social/favorite-button";
+import { RecipeCoverPlaceholder } from "@/components/recipe/recipe-cover-placeholder";
import { getPublicUrl } from "@/lib/storage";
import { cn, stripMarkdown } from "@/lib/utils";
@@ -54,9 +55,7 @@ export function RecipeThumb({ recipe, className }: { recipe: GridCardRecipe; cla
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
) : (
-
- {recipe.recipeType === "drink" ? "🍹" : "🍽️"}
-
+
)}
);
diff --git a/apps/web/components/recipe/recipes-grid.tsx b/apps/web/components/recipe/recipes-grid.tsx
index 22fc540..f22de65 100644
--- a/apps/web/components/recipe/recipes-grid.tsx
+++ b/apps/web/components/recipe/recipes-grid.tsx
@@ -25,6 +25,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { FavoriteButton } from "@/components/social/favorite-button";
+import { RecipeCoverPlaceholder } from "@/components/recipe/recipe-cover-placeholder";
import { toast } from "sonner";
import { getPublicUrl } from "@/lib/storage";
import { useLocale } from "@/lib/i18n/provider";
@@ -92,9 +93,10 @@ function RecipeThumb({ recipe, selectMode, selected, className }: { recipe: Reci
)}
/>
) : (
-
- {recipe.recipeType === "drink" ? "🍹" : "🍽️"}
-
+
)}
{selectMode && selected && }
{selectMode && (
diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts
index c55e0f3..4634374 100644
--- a/apps/web/lib/changelog.ts
+++ b/apps/web/lib/changelog.ts
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together.
-export const APP_VERSION = "0.50.1";
+export const APP_VERSION = "0.50.2";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ version: "0.50.2",
+ date: "2026-07-19 10:15",
+ fixed: [
+ "Recipes without a cover photo now get a varied gradient + food/drink icon instead of the same flat emoji everywhere — deterministic per recipe, so it stays stable across reloads. Applied to the Recipes grid/list, Explore/collections cards, and public profile pages.",
+ ],
+ },
{
version: "0.50.1",
date: "2026-07-19 09:30",
diff --git a/apps/web/lib/recipe-placeholder.ts b/apps/web/lib/recipe-placeholder.ts
new file mode 100644
index 0000000..1297edf
--- /dev/null
+++ b/apps/web/lib/recipe-placeholder.ts
@@ -0,0 +1,47 @@
+import {
+ Soup, Salad, Pizza, Sandwich, IceCream, Cookie, Beef, Fish, Cherry,
+ Carrot, Egg, Drumstick, Croissant, Donut, Cake, ChefHat,
+ Coffee, Wine, Beer, GlassWater, Martini, CupSoda,
+ type LucideIcon,
+} from "lucide-react";
+
+const GRADIENTS = [
+ "from-orange-200 to-rose-200 dark:from-orange-950 dark:to-rose-950",
+ "from-amber-200 to-lime-200 dark:from-amber-950 dark:to-lime-950",
+ "from-sky-200 to-indigo-200 dark:from-sky-950 dark:to-indigo-950",
+ "from-emerald-200 to-teal-200 dark:from-emerald-950 dark:to-teal-950",
+ "from-fuchsia-200 to-purple-200 dark:from-fuchsia-950 dark:to-purple-950",
+ "from-yellow-200 to-orange-200 dark:from-yellow-950 dark:to-orange-950",
+ "from-rose-200 to-pink-200 dark:from-rose-950 dark:to-pink-950",
+ "from-teal-200 to-cyan-200 dark:from-teal-950 dark:to-cyan-950",
+];
+
+const DISH_ICONS: LucideIcon[] = [
+ ChefHat, Soup, Salad, Pizza, Sandwich, IceCream, Cookie, Beef, Fish,
+ Cherry, Carrot, Egg, Drumstick, Croissant, Donut, Cake,
+];
+
+const DRINK_ICONS: LucideIcon[] = [Coffee, Wine, Beer, GlassWater, Martini, CupSoda];
+
+/** Small, fast, deterministic string hash (djb2) — same recipe always gets the same placeholder. */
+function hashString(s: string): number {
+ let hash = 5381;
+ for (let i = 0; i < s.length; i++) {
+ hash = (hash * 33) ^ s.charCodeAt(i);
+ }
+ return hash >>> 0;
+}
+
+export function getRecipePlaceholder(recipe: { id: string; recipeType?: "dish" | "drink" }): {
+ gradient: string;
+ Icon: LucideIcon;
+} {
+ const hash = hashString(recipe.id);
+ const icons = recipe.recipeType === "drink" ? DRINK_ICONS : DISH_ICONS;
+ return {
+ gradient: GRADIENTS[hash % GRADIENTS.length]!,
+ // Different modulus base than gradient's so icon/color pairing doesn't
+ // repeat in lockstep across ids that happen to share a gradient.
+ Icon: icons[Math.floor(hash / GRADIENTS.length) % icons.length]!,
+ };
+}
diff --git a/apps/web/package.json b/apps/web/package.json
index deb7edc..9c3af3d 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@epicure/web",
- "version": "0.50.1",
+ "version": "0.50.2",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/package.json b/package.json
index e511343..333bbff 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "epicure",
- "version": "0.50.1",
+ "version": "0.50.2",
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",