CHANGELOG.ts is a dev log (migration reminders, bug-fix jargon), not fit for public display. Removed the public changelog page from the vitrine's page list/rollout/PUBLIC_PATHS example. Added §6 flagging the real underlying need — a curated, in-app "What's New" surfaced to existing users (bell/badge, keyed off last-seen version) — as a separate, smaller feature to scope properly later, not folded into this plan.
8.1 KiB
Vitrine (Marketing Site) Plan
Status: planning only — nothing in this doc is implemented yet
Current state, confirmed by reading the actual code, not assumed:
apps/web/app/page.tsx—/unconditionallyredirect("/recipes"), no marketing content exists.apps/web/proxy.ts(the app's middleware,export default proxy+config.matcher) gates every path not inPUBLIC_PATHSbehind a session cookie, redirecting straight to/login./is not in that list — so today, an anonymous visitor hitting/never even reachespage.tsx's redirect; they're bounced to/loginfirst. This is the one integration point that actually matters — the vitrine is invisible to anonymous visitors untilPUBLIC_PATHSchanges.- No
/privacy,/terms,/about,/contact,/features, or/pricingroute exists anywhere. - No
sitemap.ts/robots.ts— Next.js supports both as file-based special routes (same convention already used forapp/manifest.ts, seen in that file) — currently missing entirely, worth adding as basic SEO hygiene regardless of how much content the vitrine ends up with.
1. Page list
| Route | Purpose |
|---|---|
/ |
Home — hero, value prop, 3-4 feature highlights with screenshots, primary CTA → /signup |
/features |
Deeper tour: AI recipe generation, meal planning, pantry tracking, social (follows/ratings/comments), the cooking assistant + its tool-calling (create recipe / add to shopping list from chat) |
/pricing |
Free/Pro/Family comparison table, CTA into Checkout once Stripe ships (STRIPE_PLAN.md §5/§7), or straight to /signup before it does |
/about |
Story/mission — why this exists, who's building it |
/privacy |
Privacy policy — needed once collecting emails regardless of Stripe timing (GDPR, since you're in France/EU per STRIPE_PLAN.md §10) |
/terms |
Terms of service |
/contact |
Support email / simple contact form |
Not in v1, deferred: a blog (/blog) — real SEO value but ongoing content-production cost that's a separate commitment from building the site itself; add later if organic-search growth becomes a priority.
2. Where it lives — same app, new route group
apps/web/app/(marketing)/ — a new route group alongside the existing (auth)/(app) ones, same conventions (shared layout.tsx for the marketing nav/footer, page-per-route). This reuses:
- the design system (
components/ui/*, Tailwind config, dark/light theming already wired app-wide), next-intli18n (lib/i18n/*,messages/en.json/fr.json) — marketing copy gets the sameen/frtreatment as the rest of the app, new keys under amarketingnamespace,- the existing deploy pipeline (
DEPLOY.md, Docker/Traefik) — no second build/deploy to maintain.
Root page change: apps/web/app/page.tsx currently always redirects to /recipes. Change to: authenticated session → redirect to /recipes (unchanged behavior for existing users); no session → render the (marketing) home page instead of redirecting. Concretely, move the marketing home content into app/(marketing)/page.tsx (or a shared component) and have root page.tsx branch on auth.api.getSession(...) the same way every other server component in this app already checks session (see app/(app)/recipes/page.tsx for the exact pattern to mirror).
Middleware change (the one that actually gates visibility) — add the marketing routes to PUBLIC_PATHS in apps/web/proxy.ts:
const PUBLIC_PATHS = [
"/login", "/signup", /* ...existing... */,
"/", "/features", "/pricing", "/about", "/privacy", "/terms", "/contact",
];
Note / needs an exact match, not startsWith — the current check is pathname.startsWith(p), which is fine for /features etc. (nothing else starts with those prefixes) but "/" as a prefix would make PUBLIC_PATHS.some(p => pathname.startsWith(p)) true for literally every path, since every path starts with /. This needs a small tweak to the matching logic, not just an array addition — e.g. check pathname === "/" as its own condition alongside the startsWith list, don't add bare "/" into the array.
Nav: the existing authenticated Nav component (apps/web/components/layout/nav.tsx) doesn't apply here — the marketing route group gets its own simple header/footer (logo, nav links to the pages above, Login/Signup buttons) inside (marketing)/layout.tsx, not reused from the app shell.
3. SEO basics (currently entirely missing)
app/sitemap.ts— list the 7 marketing routes (file-based Next.js route, same pattern as the existingapp/manifest.ts).app/robots.ts— allow marketing routes, disallow/api,/admin,(app)authenticated routes,/r///s///u/if those shouldn't be indexed (check current intent — they're public-but-unlisted-ish today, decide explicitly rather than defaulting).- Per-page
generateMetadata(title/description/OpenGraph image) — every existing app page already setsexport const metadata: Metadata = {}as a placeholder pattern; marketing pages should actually fill these in, they're the ones search engines/social shares see. - One shared OG image (or per-page) — doesn't need to be fancy, just present; a page with no OG image looks broken when shared on socials, more damaging than a plain one.
4. Content sourcing
- Screenshots — real app screenshots (recipe detail page, meal planner, the cooking-assistant proposal card) sell the product better than illustrations for a tool like this. Needs light art-direction (consistent browser-frame chrome, consistent sample data — a demo/seed account with good-looking recipes, not someone's messy real account) but no dedicated design tooling.
- Copy — feature descriptions can mostly be extracted from what already exists in
messages/en.json(feature names, help text) as a starting draft, then punched up for marketing tone rather than in-app instructional tone — those two registers are legitimately different and shouldn't just be copy-pasted. - Privacy/Terms — templated legal content exists (e.g. via a generator, or your accountant/lawyer per
STRIPE_PLAN.md§10 can point you to a template suited to a French micro-entreprise) — not something to draft from scratch or have generated without review, this is exactly the kind of content where a wrong clause has real consequences. Flag as needs non-engineering review before publishing, same spirit as the Stripe plan's tax-advice caveat.
5. Rollout order
sitemap.ts/robots.ts+ thePUBLIC_PATHS/root-page.tsxchange (§2–3) — plumbing first, still no real content, safe to ship and verify the routing/auth-gating logic works before writing copy./(home) +/features— the two pages that actually sell the product; ship these before pricing exists if Stripe isn't live yet (CTA → signup, not checkout)./privacy+/terms+/about+/contact— required-but-not-persuasive pages, batch together./pricing— onceSTRIPE_PLAN.mdpricing (§1c) is confirmed and ideally once Checkout exists, so the CTA goes somewhere real instead of just to signup.
6. Separate concern: advertising new features to existing users
Not part of this plan (in-app, not public/marketing), but flagged since it
came up here: apps/web/lib/changelog.ts's CHANGELOG array is written
for you, not users — entries mix real feature announcements with internal
notes ("not applied in sandbox — run pnpm db:migrate", bug-fix jargon,
migration reminders). That's correctly not public. But there's a real
gap: users have no clean way to learn what shipped since they last looked.
Direction for later, not scoped in detail here: a curated, user-facing
"What's New" — either a hand-picked subset/rewrite of CHANGELOG entries
(only the ones worth telling a user about, in plain language, not the raw
dev log) or a separate small table if the two need to diverge more than a
filter allows. Surfaced in-app — a badge/dot on a bell icon, a dismissible
panel on next login, keyed off "last version the user has seen" so it only
shows genuinely new items. This is a distinct, smaller feature from the
vitrine and can be scoped properly in its own pass.