+
+
+
{m.publicCollection.publicCollectionBy}
+
+ {col.user.name}
+
+ {session && (
+
+
+ {m.publicCollection.viewInLibrary}
+
+
+ )}
+ {!session && (
+
+ {m.publicRecipe.signUpFree}
+ {m.publicRecipe.logIn}
+
+ )}
+
+
+
+
{col.name}
+ {col.description &&
{col.description}
}
+
+ {visibleRecipes.length} recipe{visibleRecipes.length !== 1 ? "s" : ""}
+
+
+
+ {visibleRecipes.length > 0 && (
+
+ {visibleRecipes.map((recipe) => (
+
+
+
+ ))}
+
+ )}
+
+ {!session && (
+
+
{m.publicCollection.wantToSave}
+
{m.publicCollection.wantToSaveDescription}
+
{m.publicRecipe.getStartedFree}
+
+ )}
+
+ );
+}
diff --git a/apps/web/app/print/collection/[id]/page.tsx b/apps/web/app/print/collection/[id]/page.tsx
index e6647f1..5193a44 100644
--- a/apps/web/app/print/collection/[id]/page.tsx
+++ b/apps/web/app/print/collection/[id]/page.tsx
@@ -41,7 +41,7 @@ export default async function CollectionPrintPage({ params }: Params) {
// Same rationale as the recipe print page's QR: only link a URL an
// anonymous scanner could actually resolve.
const shareUrl = col.visibility === "public" || col.visibility === "unlisted"
- ? `${process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"}/collections/${id}`
+ ? `${process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"}/c/${id}`
: null;
const qrDataUrl = shareUrl ? await QRCode.toDataURL(shareUrl, { margin: 1, width: 120 }) : null;
diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts
index ef9ec0f..cc00e4a 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.54.0";
+export const APP_VERSION = "0.54.1";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ version: "0.54.1",
+ date: "2026-07-19 17:25",
+ added: [
+ "Public collections are now viewable by logged-out visitors at /c/{id} (no account needed), same as public recipes at /r/{id} — the collection's QR code/print link and the new \"view publicly\" icon on the collection page both point here. Followers-only collections check the visitor's follow status the same way followers-only recipes do.",
+ ],
+ },
{
version: "0.54.0",
date: "2026-07-19 17:05",
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 6c8cbf5..ba27591 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -531,6 +531,12 @@
"wantToSaveDescription": "Create a free account to save recipes, scale servings, and build your own library.",
"getStartedFree": "Get started free"
},
+ "publicCollection": {
+ "publicCollectionBy": "Public collection by",
+ "viewInLibrary": "View in your library",
+ "wantToSave": "Want to save this collection?",
+ "wantToSaveDescription": "Create a free account to fork collections, save recipes, and build your own library."
+ },
"substitute": {
"fetchError": "Couldn't fetch substitutes.",
"titleFor": "Substitute for {ingredient}",
diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json
index aec3c07..193dc78 100644
--- a/apps/web/messages/fr.json
+++ b/apps/web/messages/fr.json
@@ -531,6 +531,12 @@
"wantToSaveDescription": "Créez un compte gratuit pour sauvegarder des recettes, ajuster les portions et construire votre propre bibliothèque.",
"getStartedFree": "Commencer gratuitement"
},
+ "publicCollection": {
+ "publicCollectionBy": "Collection publique par",
+ "viewInLibrary": "Voir dans votre bibliothèque",
+ "wantToSave": "Envie de sauvegarder cette collection ?",
+ "wantToSaveDescription": "Créez un compte gratuit pour forker des collections, sauvegarder des recettes et construire votre propre bibliothèque."
+ },
"substitute": {
"fetchError": "Impossible de récupérer les substituts.",
"titleFor": "Substitut pour {ingredient}",
diff --git a/apps/web/package.json b/apps/web/package.json
index 54cb95a..b033b7c 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@epicure/web",
- "version": "0.54.0",
+ "version": "0.54.1",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts
index 879b43c..edb7a67 100644
--- a/apps/web/proxy.ts
+++ b/apps/web/proxy.ts
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
import { applyRateLimit } from "@/lib/rate-limit";
-const PUBLIC_PATHS = ["/login", "/signup", "/verify-email", "/verify-2fa", "/forgot-password", "/reset-password", "/api/auth", "/r/", "/u/", "/s/", "/docs", "/api/v1/openapi.json", "/api/webhooks", "/api/v1/invites/", "/api/internal/", "/features", "/pricing", "/about", "/privacy", "/terms", "/contact"];
+const PUBLIC_PATHS = ["/login", "/signup", "/verify-email", "/verify-2fa", "/forgot-password", "/reset-password", "/api/auth", "/r/", "/c/", "/u/", "/s/", "/docs", "/api/v1/openapi.json", "/api/webhooks", "/api/v1/invites/", "/api/internal/", "/features", "/pricing", "/about", "/privacy", "/terms", "/contact"];
// Exact-match only — "/" can't go in PUBLIC_PATHS's startsWith list, since
// every path starts with "/" and that would make the whole app public.
const PUBLIC_EXACT_PATHS = ["/"];
diff --git a/package.json b/package.json
index 2395022..c235a2c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "epicure",
- "version": "0.54.0",
+ "version": "0.54.1",
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",