diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c2810b..221fd25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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.48.0 — 2026-07-18 09:45 + +### Added +- Public marketing pages — Home, Features, About, Privacy, Terms, Contact — visible to logged-out visitors at the root domain, plus a sitemap and robots.txt. Logged-in users hitting "/" still land straight in the app, unchanged. + +Note: Privacy and Terms are structural drafts, not lawyer-reviewed (see `VITRINE_PLAN.md` §4). Pricing page intentionally not included yet (waiting on Stripe Checkout per `STRIPE_PLAN.md`). + ## 0.47.0 — 2026-07-18 00:30 Renamed the "Team" billing tier to "Family" (free/pro/family) — same limits, same admin editing, just the name. Existing Team users keep their tier/limits unaffected; the DB enum value itself was renamed in place (no data migration needed). diff --git a/apps/web/app/(marketing)/about/page.tsx b/apps/web/app/(marketing)/about/page.tsx new file mode 100644 index 0000000..44c3c48 --- /dev/null +++ b/apps/web/app/(marketing)/about/page.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; +import { getMessages } from "@/lib/i18n/server"; + +export const metadata: Metadata = { + title: "About — Epicure", + description: "The story behind Epicure.", +}; + +export default function AboutPage() { + const m = getMessages(undefined); + const t = m.marketing.about; + + return ( +
+

{t.title}

+
+ {t.paragraphs.map((p, i) => ( +

{p}

+ ))} +
+
+ ); +} diff --git a/apps/web/app/(marketing)/contact/page.tsx b/apps/web/app/(marketing)/contact/page.tsx new file mode 100644 index 0000000..54d6a81 --- /dev/null +++ b/apps/web/app/(marketing)/contact/page.tsx @@ -0,0 +1,29 @@ +import type { Metadata } from "next"; +import { Mail } from "lucide-react"; +import { getMessages } from "@/lib/i18n/server"; + +export const metadata: Metadata = { + title: "Contact — Epicure", + description: "Get in touch with the Epicure team.", +}; + +const SUPPORT_EMAIL = "support@epicure.app"; + +export default function ContactPage() { + const m = getMessages(undefined); + const t = m.marketing.contact; + + return ( +
+

{t.title}

+

{t.subtitle}

+ + + {SUPPORT_EMAIL} + +
+ ); +} diff --git a/apps/web/app/(marketing)/features/page.tsx b/apps/web/app/(marketing)/features/page.tsx new file mode 100644 index 0000000..0eda37d --- /dev/null +++ b/apps/web/app/(marketing)/features/page.tsx @@ -0,0 +1,54 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { Sparkles, Calendar, Package, MessageCircle, Users, ChefHat, Camera, ListChecks } from "lucide-react"; +import { getMessages } from "@/lib/i18n/server"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export const metadata: Metadata = { + title: "Features — Epicure", + description: "AI recipe generation, meal planning, pantry tracking, and a cooking assistant that can act on your behalf.", +}; + +const FEATURES = [ + { icon: Sparkles, key: "ai" }, + { icon: Camera, key: "photoImport" }, + { icon: MessageCircle, key: "assistant" }, + { icon: Calendar, key: "mealPlan" }, + { icon: Package, key: "pantry" }, + { icon: ListChecks, key: "shoppingList" }, + { icon: Users, key: "social" }, + { icon: ChefHat, key: "batchCook" }, +] as const; + +export default function FeaturesPage() { + const m = getMessages(undefined); + const t = m.marketing.features; + + return ( +
+
+

{t.title}

+

{t.subtitle}

+
+ +
+ {FEATURES.map(({ icon: Icon, key }) => ( +
+
+ +
+

{t.items[key].title}

+

{t.items[key].description}

+
+ ))} +
+ +
+ + {t.cta} + +
+
+ ); +} diff --git a/apps/web/app/(marketing)/layout.tsx b/apps/web/app/(marketing)/layout.tsx new file mode 100644 index 0000000..c8e4e66 --- /dev/null +++ b/apps/web/app/(marketing)/layout.tsx @@ -0,0 +1,17 @@ +import { headers } from "next/headers"; +import { auth } from "@/lib/auth/server"; +import { MarketingNav } from "@/components/marketing/marketing-nav"; +import { MarketingFooter } from "@/components/marketing/marketing-footer"; + +export default async function MarketingLayout({ children }: { children: React.ReactNode }) { + const session = await auth.api.getSession({ headers: await headers() }).catch(() => null); + const locale = (session?.user as { locale?: string } | undefined)?.locale; + + return ( +
+ +
{children}
+ +
+ ); +} diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx new file mode 100644 index 0000000..5405470 --- /dev/null +++ b/apps/web/app/(marketing)/page.tsx @@ -0,0 +1,68 @@ +import { redirect } from "next/navigation"; +import { headers } from "next/headers"; +import Link from "next/link"; +import { auth } from "@/lib/auth/server"; +import { getMessages } from "@/lib/i18n/server"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { Sparkles, Calendar, Package, MessageCircle, Users, ChefHat } from "lucide-react"; + +const HIGHLIGHTS = [ + { icon: Sparkles, key: "ai" }, + { icon: MessageCircle, key: "assistant" }, + { icon: Calendar, key: "mealPlan" }, + { icon: Package, key: "pantry" }, + { icon: Users, key: "social" }, + { icon: ChefHat, key: "batchCook" }, +] as const; + +export default async function MarketingHomePage() { + const session = await auth.api.getSession({ headers: await headers() }); + if (session) redirect("/recipes"); + + const m = getMessages(undefined); + const t = m.marketing.home; + + return ( +
+
+

+ {t.heroTitle} +

+

+ {t.heroSubtitle} +

+
+ + {t.ctaPrimary} + + + {t.ctaSecondary} + +
+
+ +
+
+ {HIGHLIGHTS.map(({ icon: Icon, key }) => ( +
+
+ +
+

{t.highlights[key].title}

+

{t.highlights[key].description}

+
+ ))} +
+
+ +
+

{t.bottomCtaTitle}

+

{t.bottomCtaSubtitle}

+ + {t.ctaPrimary} + +
+
+ ); +} diff --git a/apps/web/app/(marketing)/privacy/page.tsx b/apps/web/app/(marketing)/privacy/page.tsx new file mode 100644 index 0000000..a3f74c4 --- /dev/null +++ b/apps/web/app/(marketing)/privacy/page.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from "next"; +import { AlertTriangle } from "lucide-react"; + +export const metadata: Metadata = { + title: "Privacy Policy — Epicure", + description: "How Epicure collects, uses, and protects your data.", +}; + +export default function PrivacyPage() { + return ( +
+
+ +

+ Draft — not yet reviewed by counsel. This page is a + placeholder structure, not a finished, legally-binding policy. Replace + before accepting real signups/payments (see STRIPE_PLAN.md §10). +

+
+ +

Privacy Policy

+ +
+
+

Data we collect

+

Account information (email, name), recipes and content you create, usage data (AI calls, storage), and payment information (processed by Stripe — we never store card details ourselves).

+
+
+

How we use it

+

To provide the service (store your recipes, run AI features you request, process subscriptions), and nothing beyond that — no data is sold to third parties.

+
+
+

Third parties

+

AI providers (OpenAI/Anthropic/OpenRouter/Ollama, depending on your configuration), Stripe for payments, and your own configured integrations (webhooks, API keys) that you explicitly set up.

+
+
+

Your rights

+

Access, export, correct, or delete your data at any time from Settings, or by contacting us (see Contact page). EU/EEA users have rights under GDPR.

+
+
+

Contact

+

Questions about this policy — see the Contact page.

+
+
+
+ ); +} diff --git a/apps/web/app/(marketing)/terms/page.tsx b/apps/web/app/(marketing)/terms/page.tsx new file mode 100644 index 0000000..c6fc55a --- /dev/null +++ b/apps/web/app/(marketing)/terms/page.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from "next"; +import { AlertTriangle } from "lucide-react"; + +export const metadata: Metadata = { + title: "Terms of Service — Epicure", + description: "The terms governing use of Epicure.", +}; + +export default function TermsPage() { + return ( +
+
+ +

+ Draft — not yet reviewed by counsel. This page is a + placeholder structure, not finished, legally-binding terms. Replace + before accepting real signups/payments (see STRIPE_PLAN.md §10). +

+
+ +

Terms of Service

+ +
+
+

Using Epicure

+

You're responsible for the content you create and share, and for keeping your account credentials secure.

+
+
+

Subscriptions

+

Paid tiers (Pro, Family) renew automatically until cancelled. Cancel anytime from Settings — access continues until the end of the paid period.

+
+
+

Content ownership

+

You own what you create. We only use it to provide the service to you (and, if you set a recipe public/unlisted/followers-visible, to whoever you've chosen to share it with).

+
+
+

AI-generated content

+

AI-generated recipes are provided as-is — always use your own judgment, especially around allergens and food safety.

+
+
+

Changes

+

We may update these terms; meaningful changes will be communicated in-app.

+
+
+
+ ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx deleted file mode 100644 index 5618f09..0000000 --- a/apps/web/app/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from "next/navigation"; - -export default function RootPage() { - redirect("/recipes"); -} diff --git a/apps/web/app/robots.ts b/apps/web/app/robots.ts new file mode 100644 index 0000000..40a4adb --- /dev/null +++ b/apps/web/app/robots.ts @@ -0,0 +1,17 @@ +import type { MetadataRoute } from "next"; + +const BASE_URL = process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: ["/", "/features", "/pricing", "/about", "/privacy", "/terms", "/contact"], + // /r/ and /s/ are unlisted share links (recipe/shopping-list) — "unlisted" + // means reachable by direct link only, not meant to be crawled/indexed. + // /u/ (public profiles) is deliberately left crawlable. + disallow: ["/api/", "/admin/", "/recipes", "/explore", "/meal-plan", "/pantry", "/shopping-lists", "/settings", "/collections", "/nutrition", "/r/", "/s/"], + }, + sitemap: `${BASE_URL}/sitemap.xml`, + }; +} diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts new file mode 100644 index 0000000..a826a9d --- /dev/null +++ b/apps/web/app/sitemap.ts @@ -0,0 +1,13 @@ +import type { MetadataRoute } from "next"; + +const BASE_URL = process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"; + +export default function sitemap(): MetadataRoute.Sitemap { + const routes = ["", "/features", "/pricing", "/about", "/privacy", "/terms", "/contact"]; + return routes.map((route) => ({ + url: `${BASE_URL}${route}`, + lastModified: new Date(), + changeFrequency: route === "" ? "weekly" : "monthly", + priority: route === "" ? 1 : 0.5, + })); +} diff --git a/apps/web/components/marketing/marketing-footer.tsx b/apps/web/components/marketing/marketing-footer.tsx new file mode 100644 index 0000000..dcf9b0b --- /dev/null +++ b/apps/web/components/marketing/marketing-footer.tsx @@ -0,0 +1,28 @@ +import Link from "next/link"; +import { getMessages, formatMessage } from "@/lib/i18n/server"; + +const LINKS = [ + { href: "/about", key: "about" }, + { href: "/privacy", key: "privacy" }, + { href: "/terms", key: "terms" }, + { href: "/contact", key: "contact" }, +] as const; + +export function MarketingFooter({ locale }: { locale?: string }) { + const m = getMessages(locale); + + return ( + + ); +} diff --git a/apps/web/components/marketing/marketing-nav.tsx b/apps/web/components/marketing/marketing-nav.tsx new file mode 100644 index 0000000..13ed9c6 --- /dev/null +++ b/apps/web/components/marketing/marketing-nav.tsx @@ -0,0 +1,51 @@ +import Link from "next/link"; +import { headers } from "next/headers"; +import { ChefHat } from "lucide-react"; +import { auth } from "@/lib/auth/server"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { getMessages } from "@/lib/i18n/server"; + +const LINKS = [ + { href: "/features", key: "features" }, + { href: "/about", key: "about" }, +] as const; + +export async function MarketingNav() { + const session = await auth.api.getSession({ headers: await headers() }).catch(() => null); + const m = getMessages((session?.user as { locale?: string } | undefined)?.locale); + + return ( +
+
+ + + Epicure + + +
+ {session ? ( + + {m.marketing.nav.openApp} + + ) : ( + <> + + {m.marketing.nav.login} + + + {m.marketing.nav.signup} + + + )} +
+
+
+ ); +} diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts index ebe4b28..44ee65d 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.47.0"; +export const APP_VERSION = "0.48.0"; export type ChangelogEntry = { version: string; @@ -11,6 +11,14 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.48.0", + date: "2026-07-18 09:45", + added: [ + "Public marketing pages — Home, Features, About, Privacy, Terms, Contact — visible to logged-out visitors at the root domain, plus a sitemap and robots.txt. Logged-in users hitting \"/\" still land straight in the app, unchanged.", + ], + notes: "Privacy and Terms are structural drafts, not lawyer-reviewed — see VITRINE_PLAN.md §4. Pricing page intentionally not included yet (waiting on Stripe Checkout per STRIPE_PLAN.md).", + }, { version: "0.47.0", date: "2026-07-18 00:30", diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 5c1b16b..153ce9f 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1,4 +1,62 @@ { + "marketing": { + "nav": { + "features": "Features", + "about": "About", + "openApp": "Open app", + "login": "Log in", + "signup": "Sign up" + }, + "footer": { + "about": "About", + "privacy": "Privacy", + "terms": "Terms", + "contact": "Contact", + "copyright": "© {year} Epicure" + }, + "home": { + "heroTitle": "Your AI-powered recipe book", + "heroSubtitle": "Generate, organize, and share recipes — plan meals, track your pantry, and cook with an assistant that can act on your behalf.", + "ctaPrimary": "Get started free", + "ctaSecondary": "See features", + "bottomCtaTitle": "Start cooking smarter", + "bottomCtaSubtitle": "Free to start. No credit card required.", + "highlights": { + "ai": { "title": "AI recipe generation", "description": "Generate a full recipe from an idea, a photo, or a URL — vision and text models work together to write the details." }, + "assistant": { "title": "Cooking assistant", "description": "Ask cooking questions anytime — it can even draft a recipe or shopping list right in the conversation, for you to review and confirm." }, + "mealPlan": { "title": "Meal planning", "description": "Plan your week, generate a plan with AI, and turn it straight into a shopping list." }, + "pantry": { "title": "Pantry tracking", "description": "Know what you have on hand — scan it in, and let recipes account for it automatically." }, + "social": { "title": "Share & discover", "description": "Follow other cooks, rate and comment on recipes, and control exactly who can see yours." }, + "batchCook": { "title": "Batch cooking", "description": "Cook once, eat all week — dedicated batch-cook planning with fridge/freezer guidance." } + } + }, + "features": { + "title": "Everything you need to cook smarter", + "subtitle": "From a single idea to a finished, organized recipe collection.", + "cta": "Try it free", + "items": { + "ai": { "title": "AI recipe generation", "description": "Generate a recipe from a short idea, a URL, or a description — dish or drink, automatically detected." }, + "photoImport": { "title": "Import from a photo", "description": "A vision model recognizes what's in the photo, then a text model writes the full recipe from that description." }, + "assistant": { "title": "Cooking assistant with tool-calling", "description": "A conversational assistant for cooking questions that can also draft a recipe or shopping-list additions for you to confirm — nothing saves without your say." }, + "mealPlan": { "title": "AI meal planning", "description": "Generate a week of meals around your preferences, dietary needs, and what's already in your pantry." }, + "pantry": { "title": "Pantry tracking", "description": "Track what you have — scan barcodes or photos — and let it inform meal planning and shopping lists." }, + "shoppingList": { "title": "Shopping lists", "description": "Multiple lists, shared with others, auto-populated from recipes or meal plans." }, + "social": { "title": "Follows, ratings & comments", "description": "A social layer for recipes — follow other cooks, rate and review, with granular visibility controls including followers-only." }, + "batchCook": { "title": "Batch cooking", "description": "Plan cook-once-eat-all-week sessions with per-dish fridge/freezer guidance." } + } + }, + "about": { + "title": "About Epicure", + "paragraphs": [ + "Epicure started as a simple idea: a recipe book that actually keeps up with how you cook — planning, pantry, and AI all in one place instead of five different apps.", + "It's an independent project, built and maintained by one person, growing based on what's actually useful to the people using it." + ] + }, + "contact": { + "title": "Get in touch", + "subtitle": "Questions, feedback, or something broken — email us and we'll get back to you." + } + }, "nav": { "menu": "Menu", "recipes": "Recipes", diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json index bcd15ed..caccaf4 100644 --- a/apps/web/messages/fr.json +++ b/apps/web/messages/fr.json @@ -1,4 +1,62 @@ { + "marketing": { + "nav": { + "features": "Fonctionnalités", + "about": "À propos", + "openApp": "Ouvrir l'application", + "login": "Connexion", + "signup": "Inscription" + }, + "footer": { + "about": "À propos", + "privacy": "Confidentialité", + "terms": "Conditions", + "contact": "Contact", + "copyright": "© {year} Epicure" + }, + "home": { + "heroTitle": "Votre carnet de recettes propulsé par l'IA", + "heroSubtitle": "Générez, organisez et partagez vos recettes — planifiez vos repas, suivez votre garde-manger, et cuisinez avec un assistant capable d'agir pour vous.", + "ctaPrimary": "Commencer gratuitement", + "ctaSecondary": "Voir les fonctionnalités", + "bottomCtaTitle": "Cuisinez plus intelligemment", + "bottomCtaSubtitle": "Gratuit pour commencer. Aucune carte bancaire requise.", + "highlights": { + "ai": { "title": "Génération de recettes par IA", "description": "Générez une recette complète à partir d'une idée, d'une photo ou d'une URL — modèles de vision et de texte travaillent ensemble pour rédiger les détails." }, + "assistant": { "title": "Assistant culinaire", "description": "Posez vos questions de cuisine à tout moment — il peut même rédiger une recette ou une liste de courses directement dans la conversation, pour que vous la validiez." }, + "mealPlan": { "title": "Planification de repas", "description": "Planifiez votre semaine, générez un plan avec l'IA, et transformez-le directement en liste de courses." }, + "pantry": { "title": "Suivi du garde-manger", "description": "Sachez ce que vous avez sous la main — scannez-le, et laissez les recettes en tenir compte automatiquement." }, + "social": { "title": "Partager & découvrir", "description": "Suivez d'autres cuisiniers, notez et commentez les recettes, et contrôlez précisément qui peut voir les vôtres." }, + "batchCook": { "title": "Cuisine en lots", "description": "Cuisinez une fois, mangez toute la semaine — planification dédiée avec conseils de conservation au frigo/congélateur." } + } + }, + "features": { + "title": "Tout ce qu'il faut pour cuisiner plus intelligemment", + "subtitle": "D'une simple idée à une collection de recettes organisée.", + "cta": "Essayer gratuitement", + "items": { + "ai": { "title": "Génération de recettes par IA", "description": "Générez une recette à partir d'une courte idée, d'une URL ou d'une description — plat ou boisson, détecté automatiquement." }, + "photoImport": { "title": "Importer depuis une photo", "description": "Un modèle de vision identifie ce qui est sur la photo, puis un modèle de texte rédige la recette complète à partir de cette description." }, + "assistant": { "title": "Assistant culinaire avec actions", "description": "Un assistant conversationnel pour vos questions de cuisine, capable aussi de rédiger une recette ou d'ajouter des articles à une liste de courses pour que vous les validiez — rien n'est enregistré sans votre accord." }, + "mealPlan": { "title": "Planification de repas par IA", "description": "Générez une semaine de repas selon vos préférences, besoins alimentaires, et ce qui est déjà dans votre garde-manger." }, + "pantry": { "title": "Suivi du garde-manger", "description": "Suivez ce que vous avez — scannez codes-barres ou photos — pour informer la planification et les listes de courses." }, + "shoppingList": { "title": "Listes de courses", "description": "Plusieurs listes, partageables, remplies automatiquement à partir des recettes ou des plans de repas." }, + "social": { "title": "Abonnements, notes & commentaires", "description": "Une couche sociale pour les recettes — suivez d'autres cuisiniers, notez et commentez, avec un contrôle de visibilité précis, y compris réservé aux abonnés." }, + "batchCook": { "title": "Cuisine en lots", "description": "Planifiez des sessions de cuisine en lots avec conseils de conservation par plat." } + } + }, + "about": { + "title": "À propos d'Epicure", + "paragraphs": [ + "Epicure est né d'une idée simple : un carnet de recettes qui suit vraiment votre façon de cuisiner — planification, garde-manger et IA réunis en un seul endroit plutôt qu'éparpillés dans cinq applications différentes.", + "C'est un projet indépendant, conçu et maintenu par une seule personne, qui évolue en fonction de ce qui est réellement utile à ses utilisateurs." + ] + }, + "contact": { + "title": "Nous contacter", + "subtitle": "Questions, retours, ou un problème rencontré — écrivez-nous, nous vous répondrons." + } + }, "nav": { "menu": "Menu", "recipes": "Recettes", diff --git a/apps/web/package.json b/apps/web/package.json index 7bf86f8..4e11ffb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@epicure/web", - "version": "0.47.0", + "version": "0.48.0", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index 7c0d1a9..879b43c 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -2,7 +2,10 @@ 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/"]; +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"]; +// 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 = ["/"]; const ADMIN_PATHS = ["/admin"]; // A public-editable shopping list's own items endpoint — no session cookie to @@ -44,7 +47,7 @@ export async function proxy(request: NextRequest) { // Only treat the items endpoint as public for requests with no session — // an authenticated collaborator's own polling/edits should never be bucketed // into the anonymous-visitor IP rate limit below. - const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p)) || (isShoppingListItems && !sessionCookie); + const isPublic = PUBLIC_PATHS.some((p) => pathname.startsWith(p)) || PUBLIC_EXACT_PATHS.includes(pathname) || (isShoppingListItems && !sessionCookie); const isAdmin = ADMIN_PATHS.some((p) => pathname.startsWith(p)); const isApi = pathname.startsWith("/api/v1"); diff --git a/package.json b/package.json index 6ec88c0..a7379a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicure", - "version": "0.47.0", + "version": "0.48.0", "private": true, "scripts": { "dev": "pnpm --filter web dev",