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 (
+
+ );
+}
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 (
+
+