Skip to content

Recipicity Frontend Architecture Review

Reviewer: Frontend Architecture Reviewer Date: 2026-02-25 Codebase: /opt/development/recipicity/staging/recipicity-frontend/ Stack: Next.js 16.1.6, React 19.2.3, Tailwind CSS 4, TypeScript 5


Executive Summary

The Recipicity frontend is a well-structured Next.js App Router application with a clean separation between server and client components. The architecture demonstrates strong fundamentals: a BFF (Backend-for-Frontend) proxy pattern for secure auth, proper SEO with structured data, a cohesive design system, and thoughtful component decomposition. However, there are significant opportunities to improve performance by converting several unnecessarily client-rendered pages to server components, reducing the client-side JavaScript bundle, and adding missing loading states. The dependency count is impressively lean (only 5 runtime deps), which is a major strength.

Overall Grade: B+


1. Next.js App Router Usage: Server vs Client Components

Strengths

  • Recipe detail page is a server component (/[username]/[slug]/page.tsx): This is the most SEO-critical page and correctly fetches data server-side with serverFetch, then passes data down to interactive client sub-components (RecipeActions, CommentsSection, ServingScaler, etc.). This is the ideal pattern.
  • Discover page is a server component with parallel data fetching via Promise.all([fetchSection, fetchCuisines, fetchAllRecipes]) -- good for TTFB.
  • Search page is a server component with proper searchParams handling and parallel fetches.
  • Auth pages use Suspense boundaries correctly (e.g., LoginPage wraps LoginForm in <Suspense>).
  • Interactive sub-components are properly extracted: login-form.tsx, register-form.tsx, search-bar.tsx, pricing-cta.tsx, about-cta.tsx, profile-social.tsx -- keeping the page-level server component and pushing "use client" to the leaf.

Issues

CRITICAL: home-content.tsx is an unnecessary client component

File: src/app/home-content.tsx:1

The HomeContent component has "use client" but the only reason is useAuth() to conditionally show "Join Free" vs "Add a Recipe" CTA buttons. The entire homepage hero, features grid, and trending recipes section are forced into the client bundle because of two conditional links.

Impact: The homepage is the most important page for LCP and SEO. Making it fully client-rendered means: - No server-side HTML for the hero, features, or trending recipes - Larger JS bundle shipped to client - Slower LCP because content must wait for JS hydration

Recommendation: Split into a server-rendered HomeContent with a small <HomeCTA /> client component that only handles the auth-conditional buttons. The recipe grid, hero text, and features are all static/prop-driven content that should render on the server.

HIGH: Most (protected) pages are entirely client components

Files: 18 pages under src/app/(protected)/ are marked "use client" at the page level.

Examples: - bookmarks/page.tsx -- could be a server component that fetches bookmarks and passes to a client list - collections/page.tsx -- same pattern - meal-plans/page.tsx -- same pattern - grocery-lists/page.tsx -- same pattern - notifications/page.tsx -- same pattern

While these are authenticated pages (less SEO-critical), making them client components means: - No streaming SSR -- the entire page is blank until JS loads - No benefit from React Server Component data fetching - No progressive rendering with <Suspense> boundaries

Recommendation: For each protected page, extract the data-fetching into a server component wrapper that reads the auth cookie via serverFetch, then passes data to a client component for interactivity. This is the pattern already used successfully on the recipe detail page.

MEDIUM: (public)/users/page.tsx is a client component

This is a publicly accessible page that lists users. It should be a server component for SEO and performance, similar to how the search page works.

"use client" Audit Summary

Category Count Verdict
Interactive form components (login, register, search-bar, recipe-form) 8 Justified -- forms need state
Interactive UI components (header, bottom-nav, modals, image-carousel) 16 Justified -- need event handlers, state
Auth/theme/feature-flag providers 5 Justified -- context providers must be client
Protected page-level components 18 Questionable -- most could be split
HomeContent 1 Should be server with small client island
Public users page 1 Should be server
Utility hooks (use-local-storage, use-subscription) 2 Justified -- browser APIs

2. Component Architecture and Reusability

Strengths

  • Clean Atomic Design-influenced structure:
  • components/ui/ -- primitives (Avatar, StarRating, LoadingSpinner)
  • components/layout/ -- structural (Header, Footer, BottomNav, PageLayout)
  • components/recipes/ -- domain-specific (RecipeCard, RecipeForm, RecipeActions)
  • components/modals/ -- overlay patterns
  • components/seo/ -- SEO concerns
  • components/privacy/ -- compliance concerns
  • components/analytics/ -- tracking

  • PageLayout is a clean composition wrapper that composes Header + Footer + BottomNav. Used consistently across all pages.

  • RecipeCard is a well-built server component (no "use client" directive). It uses Next.js Image with proper sizes attribute, semantic <article> tag, and accessibility via aria-label. Tags within the card are interactive links with proper z-index layering over the stretched card link.

  • RecipeGrid is also a server component using CSS-only masonry (columns-*). No JS dependency, SSR-safe.

  • Feature Flag system (FeatureGate + useFeatureFlags) is well-implemented with module-level caching and TTL.

Issues

MEDIUM: Duplicated API error class

Both src/lib/api/client.ts and src/lib/api/server-client.ts define their own ApiError class with identical signatures. This should be extracted to a shared module (e.g., src/lib/api/errors.ts).

LOW: PageLayout imported in error.tsx

error.tsx:3 imports PageLayout which includes Header (a client component) and Footer (a server component). If the Header itself errors, this could create a cascading error. Consider a simpler error layout that doesn't depend on the full navigation.

LOW: Ingredient drag-and-drop UI hint without implementation

recipe-form.tsx:666 renders <GripVertical> icons suggesting drag-and-drop reordering, but there is no actual drag implementation. This is a misleading affordance.


3. State Management Patterns

Strengths

  • No heavy state management library -- React Context + local state is used throughout, which is appropriate for this application's complexity level. Dependencies are remarkably lean (no Redux, Zustand, react-query, etc.).
  • AuthProvider uses a clean pattern with useCallback for memoized auth methods and proper cleanup with cancelled flag.
  • ThemeProvider uses useSyncExternalStore correctly for both localStorage reads and prefers-color-scheme media query -- this is the modern, correct approach and avoids the common FOUC issues.
  • useLocalStorageValue is a well-implemented reactive localStorage hook using useSyncExternalStore with cross-tab sync via the storage event and same-tab sync via a custom event emitter. Clean implementation.
  • Optimistic updates in RecipeActions (like/bookmark) with proper rollback on failure.

Issues

HIGH: AuthProvider fetches user on every page navigation

auth-provider.tsx:44-65 -- The useEffect in AuthProvider calls /bff/auth/me on mount. Since AuthProvider is in the root layout, this fires on every full page load. However, there is no caching, SWR, or stale-while-revalidate pattern. Every navigation that triggers a full render will re-fetch the user profile.

Recommendation: Add a short TTL cache (similar to the feature flags pattern) or use the BFF cookie's presence as a heuristic to avoid unnecessary network requests. Alternatively, consider using useSyncExternalStore with a module-level cache like the feature flags implementation.

MEDIUM: Feature flag polling missing

Feature flags are fetched once on mount with a 5-minute TTL cache, but there's no periodic refresh. If a flag changes server-side, the user won't see it until they refresh the page or the cache expires on a re-mount. For feature flags, this is acceptable, but adding a long-interval poll (e.g., 15 minutes) would improve responsiveness to flag changes.

MEDIUM: Notification polling in Header

header.tsx:71-83 polls getUnreadCount() every 60 seconds with setInterval. This is functional but: - Continues polling even when the tab is in the background (wasting bandwidth) - Does not use document.visibilityState to pause/resume - Consider using requestIdleCallback or the Page Visibility API


4. Data Fetching Patterns

Strengths

  • Dual API client architecture is excellent:
  • server-client.ts -- Used in Server Components, reads httpOnly cookie directly, calls internal API URL
  • client.ts -- Used in Client Components, routes through BFF proxy (/bff/api/[...path]), cookie sent via credentials: "include"
  • This separation is clean, secure, and well-documented

  • BFF proxy (/bff/api/[...path]/route.ts) is well-implemented:

  • Path allowlist prevents access to admin/internal endpoints
  • CSRF origin checking on mutating requests
  • Proper body streaming for POST/PUT/PATCH
  • Converts httpOnly cookie to Bearer token

  • Server components use parallel data fetching (e.g., Discover page uses Promise.all, Search page fetches recipes/users/collections in parallel)

  • Graceful degradation -- Server component data fetching is wrapped in try/catch with empty fallbacks (e.g., HomePage returns empty trending recipes array on error)

Issues

HIGH: No next: { revalidate } or caching on server-side fetches

server-client.ts does not set any caching options on fetch() calls. In Next.js App Router, this means every request is dynamic by default (no caching). The sitemap.ts is the only file that uses next: { revalidate: 3600 }.

Impact: Every page visit triggers a fresh API call, even for public data like the recipe listing on /discover or the homepage trending recipes. This increases TTFB and API server load.

Recommendation: Add next: { revalidate: 60 } (or similar) for public data fetches, particularly: - Homepage trending recipes - Discover page sections - Public recipe detail pages - Tag browse pages

MEDIUM: Sitemap fetches all recipes in a single request

sitemap.ts:23 fetches up to 1000 recipes in one API call. This could time out or consume excessive memory for a growing recipe database.

Recommendation: Implement pagination in the sitemap generation or use a sitemap index with multiple sitemap files.

LOW: No error retry logic in server-client

The serverFetch function fails immediately on network errors with no retry. For transient failures (e.g., API pod restarting during deployment), a single retry with backoff would improve reliability.


5. Bundle Size and Performance

Strengths

  • Extremely lean dependency tree: Only 5 runtime dependencies (next, react, react-dom, lucide-react, react-hook-form, react-hot-toast). This is exceptional for an application of this complexity.
  • No CSS-in-JS runtime -- Tailwind CSS is compile-time only, zero runtime cost.
  • output: "standalone" in next.config.ts enables optimized Docker builds.
  • Inter font loaded via next/font/google with display: "swap" -- prevents FOIT (Flash of Invisible Text).

Issues

HIGH: lucide-react icon imports are not tree-shaken optimally

Throughout the codebase, icons are imported individually (e.g., import { Search, Plus, Bell } from "lucide-react"). While lucide-react supports tree-shaking, the named export pattern from the barrel lucide-react index can still pull in more code than necessary with some bundlers.

The header.tsx alone imports 15 icons: Search, Plus, Bell, LogOut, User, Settings, BookOpen, Calendar, ShoppingCart, Bookmark, ChevronDown, Sparkles, Users, Import, Compass, Sun, Moon, Monitor. This is repeated across many components.

Recommendation: Verify with next build --analyze (or @next/bundle-analyzer) that tree-shaking is working properly. If not, switch to deep imports: import { Search } from "lucide-react/dist/esm/icons/search".

MEDIUM: No dynamic imports for heavy components

The recipe form (recipe-form.tsx, ~765 lines), cooking session client, and import recipe page are loaded eagerly. These are behind authentication and not needed for the initial page load.

Recommendation: Use next/dynamic for: - RecipeForm (only loaded on /add-recipe/new and /{username}/{slug}/edit) - Modals (AddToCollectionModal, AddToMealPlanModal, CalendarSubscriptionModal, BugReportModal) - AvatarCropper

LOW: react-hot-toast in root layout

Toaster is rendered in the root layout, meaning the react-hot-toast library is loaded on every page, even pages that never show a toast (e.g., static policy pages).


6. Core Web Vitals Optimization

LCP (Largest Contentful Paint)

CRITICAL: Homepage hero uses CSS background-image instead of Next.js Image

home-content.tsx:57-59:

style={{
  backgroundImage: "url('https://images.unsplash.com/photo-1556910103-1c02745aae4d?w=1600&q=80')",
}}

Problems: 1. External Unsplash URL -- not optimized by Next.js Image Optimization 2. CSS background-image is not preloadable via <link rel="preload"> 3. No loading="eager" or priority hint 4. The image URL is hardcoded with w=1600 -- no responsive sizing 5. The images.unsplash.com domain is in CSP but not in next.config.ts images.remotePatterns for Image optimization

Impact: This is the LCP element on the homepage. It will load slowly because the browser cannot discover it until CSS is parsed.

Recommendation: Replace with a Next.js <Image> component with priority prop, or at minimum add a <link rel="preload"> in the layout metadata for this image. Better yet, host the hero image locally or on the same CDN.

MEDIUM: No priority on first recipe card image in lists

RecipeCard does not set priority on any images. On the Discover page and Search page, the first visible recipe card image is likely the LCP element.

Recommendation: Add a priority prop to RecipeCard and set it to true for the first 1-2 cards rendered above the fold.

CLS (Cumulative Layout Shift)

  • Good: aspect-[4/3] and aspect-[16/10] are used consistently for image containers, preventing CLS from image loading.
  • Good: Font uses display: "swap" with a system font fallback.
  • Minor concern: The cookie consent banner appears at the bottom of the page and could cause CLS if it pushes content. However, since it's position: fixed, it overlays rather than shifts. This is fine.

FID / INP (Interaction to Next Paint)

  • Good: No heavy synchronous operations in event handlers.
  • Good: Optimistic updates for like/bookmark prevent perceived latency.
  • Concern: The Header component has 3 dropdown menus with individual state and click-outside handlers. Consider consolidating the dropdown logic or using a single open-menu state.

7. Image Optimization

Strengths

  • Next.js Image component used throughout with proper fill + sizes attributes on recipe cards and carousels.
  • remotePatterns configured for staging.recipicity.com, recipicity.com, and lh3.googleusercontent.com.
  • Avatar component uses unoptimized for Google profile images (which already have their own CDN optimization).
  • Placeholder images provided for missing recipe images (/placeholder-recipe.svg) and avatars (/default-avatar.svg).

Issues

HIGH: Missing images.unsplash.com in remotePatterns

The homepage hero image comes from images.unsplash.com but this domain is NOT in the next.config.ts images.remotePatterns. If the hero image were converted to use <Image>, it would fail. This also means any user-submitted content from Unsplash would not be optimized.

MEDIUM: No blur placeholder for images

Recipe card images and carousel images have no placeholder="blur" or blurDataURL. Adding a low-quality image placeholder (LQIP) would improve perceived performance during image loading.

image-carousel.tsx only loads the current image. When the user clicks next/prev, there's a flash while the new image loads. Consider preloading the next and previous images.


8. CSS/Tailwind Patterns and Consistency

Strengths

  • Well-defined design system in globals.css with:
  • Custom color palette (sage, cream, terracotta, stone)
  • Design tokens for border-radius (--radius-card, --radius-button)
  • Shadow scale (--shadow-soft, --shadow-lift, --shadow-elevated)
  • Animation keyframes (fade-in, slide-up)
  • Tailwind CSS v4 with @theme inline for custom properties -- modern approach.
  • Dark mode implemented via class strategy (@custom-variant dark) with consistent application across all components.
  • Custom scrollbar styling with dark mode support.
  • scrollbar-hide utility properly implemented with both -webkit-scrollbar and Firefox scrollbar-width: none.

Issues

MEDIUM: Inconsistent color usage between Tailwind utilities and custom properties

Some components use text-gray-700 dark:text-gray-200 (Tailwind defaults) while others use text-charcoal dark:text-gray-200 (custom property) or text-stone-500 dark:text-stone-400 (custom stone scale). The design system defines --color-charcoal: #2d2d2d but standard Tailwind gray-* colors are also used extensively.

Recommendation: Establish and document which color scale to use where: - Body text: charcoal / gray-100-200 - Secondary text: stone-500 / stone-400 - Muted text: stone-400 / stone-500 - Backgrounds: cream-* / gray-800-900 - Avoid mixing gray-700 with charcoal for the same purpose.

LOW: Long Tailwind class strings

Many components have very long class strings (e.g., header.tsx dropdown items). Consider extracting repeated patterns into CSS utility classes or Tailwind @apply directives for frequently-used combinations like the dropdown menu items.

LOW: Dark mode override in globals.css

globals.css:155-161 overrides text-stone-500 and text-stone-400 in dark mode using plain CSS. This is fragile and could break if Tailwind's internals change. Consider using Tailwind's built-in dark mode variants consistently instead.


9. Error Boundaries and Loading States

Strengths

  • Root error boundary (error.tsx) with reset functionality.
  • Custom 404 page (not-found.tsx) with proper navigation back to home.
  • Loading skeletons for tag pages (tags/loading.tsx, tags/[tagname]/loading.tsx) with matching layout structure.

Issues

HIGH: Only 2 loading.tsx files exist in the entire app

Only tags/loading.tsx and tags/[tagname]/loading.tsx have loading skeletons. All other routes have no loading.tsx, meaning: - /discover -- no loading skeleton during server data fetch - /search -- no loading skeleton - / (homepage) -- no loading skeleton - /{username}/{slug} (recipe detail) -- no loading skeleton

Impact: Users see a blank page or stale content during navigation until the server component resolves. This is especially noticeable on slower connections.

Recommendation: Add loading.tsx skeletons for at minimum: - / (homepage) - /discover - /search - /{username}/{slug} (recipe detail) - /(protected)/* pages

MEDIUM: No nested error boundaries

There is only one error.tsx at the root. If the comments section or reviews section fails to render on a recipe page, the entire page crashes.

Recommendation: Add error.tsx boundaries for: - (recipes)/ route group - (protected)/ route group - Individual high-risk sections (comments, reviews) could use React error boundaries at the component level

MEDIUM: Silent failures in many catch blocks

Multiple components silently swallow errors with empty catch {} blocks: - auth-provider.tsx:38 -- User fetch failure - infinite-recipe-grid.tsx:44 -- Infinite scroll load failure - home-content data fetching in page.tsx:21 - header.tsx:75 -- Notification count failure

While graceful degradation is good, there is no logging to any monitoring service. Silent failures make debugging production issues very difficult.


10. Accessibility (a11y) Compliance

Strengths

  • lang="en" on <html> element -- correct.
  • Semantic HTML used well: <article> for recipe cards, <nav> for navigation, <header>, <footer>, <main>, <section> throughout.
  • aria-label attributes on icon-only buttons (search, notifications, user menu, image carousel navigation).
  • sr-only text on stretched recipe card links.
  • Skip-to-content: The search link in the header and the Link with full label provides a keyboard path.
  • Form labels properly associated via htmlFor/id on auth forms.
  • aria-current="page" on search tabs.

Issues

HIGH: Dropdown menus lack keyboard navigation

header.tsx dropdowns (SageAI, Add Recipe, User Menu) only handle mouse clicks and click-outside dismissal. They are missing: - Escape key to close - ArrowDown/ArrowUp key navigation within menu items - role="menu" and role="menuitem" attributes - aria-expanded on trigger buttons - Focus trap within open dropdown

Recommendation: Implement WAI-ARIA Menu Button pattern or use a headless UI library like @headlessui/react or @radix-ui/react-dropdown-menu (which is already familiar since the admin app uses Headless UI).

cookie-consent.tsx:257-396 -- The preferences modal uses onClick on the backdrop to close but has no: - Focus trap (user can tab to elements behind the modal) - role="dialog" and aria-modal="true" - aria-labelledby pointing to the modal title - Escape key handling - Focus restoration to trigger element on close

image-carousel.tsx -- Previous/next buttons are accessible, but: - The dot indicators use <button> (good) but have no visible focus indicator - No keyboard shortcut hints (e.g., left/right arrow keys) - The carousel does not announce slide changes to screen readers

MEDIUM: Theme toggle lacks clear indication

header.tsx:92-95 cycles through themes on click but provides no visual indication of the current state other than a label. The button itself (cycleTheme) changes theme immediately without confirmation, which could be disorienting for users with motion sensitivities.

LOW: Color contrast concerns

The stone-400 color (#888888) on a cream-50 background (#fdfbf7) yields a contrast ratio of approximately 3.7:1, which fails WCAG AA for normal text (requires 4.5:1). This affects secondary text throughout the app.


11. SEO Implementation

Strengths

  • Comprehensive metadata setup:
  • Root layout sets default and template title (%s | recipicity)
  • metadataBase configured
  • OpenGraph and Twitter card defaults
  • Robots meta conditionally indexes only production

  • Recipe-specific SEO is excellent:

  • generateMetadata on recipe pages with title, description, OG images, canonical URL
  • recipeMetadata() helper builds proper OpenGraph article metadata
  • Schema.org Recipe JSON-LD with full structured data: ingredients, instructions, nutrition, aggregate ratings, author, prep/cook/total time, images
  • BreadcrumbList JSON-LD for recipe pages
  • Canonical URLs use clean /{username}/{slug} format (no IDs)

  • robots.ts correctly disallows staging and allows production with sensible disallow rules for private routes.

  • sitemap.ts generates dynamic sitemap from API data with proper priorities and change frequencies.

  • Search page uses robots: { index: false, follow: true } -- correct, prevents duplicate content from search result pages.

Issues

MEDIUM: No WebSite JSON-LD on homepage

buildWebsiteJsonLd() exists in jsonld.ts but is never used. The homepage should include WebSite structured data with the SearchAction for sitelinks search box in Google.

MEDIUM: No ProfilePage JSON-LD on user profile pages

User profile pages at /user/{username} should have Person or ProfilePage structured data.

LOW: Sitemap lacks user profile pages

The sitemap only includes static pages and recipes. Public user profile pages should also be included for better discoverability.

LOW: Missing alternates.languages for i18n

The app is English-only currently, but there's no hreflang setup. If internationalization is planned, this should be considered early.


12. Code Splitting and Lazy Loading

Strengths

  • Route-based code splitting is automatic with Next.js App Router -- each page/layout is its own chunk.
  • Route groups ((auth), (public), (protected), (recipes), (profiles)) organize code logically and enable per-group layouts.
  • Auth layout ((auth)/layout.tsx) is a minimal layout without the full header/footer, reducing bundle size for auth pages.

Issues

HIGH: No next/dynamic usage anywhere in the codebase

There are zero uses of next/dynamic for lazy-loaded components. Heavy components that should be lazy-loaded:

  1. Modals -- AddToCollectionModal, AddToMealPlanModal, CalendarSubscriptionModal, BugReportModal are imported eagerly but only rendered conditionally. Each modal should use next/dynamic with ssr: false.

  2. RecipeForm (~765 lines) -- Only needed on create/edit pages but is a heavy component with tag autocomplete, image upload, unit select, and drag handles.

  3. CookieConsent -- Loaded in root layout on every page but only shows once (until user dismisses). Could be lazy-loaded.

  4. AvatarCropper -- Only shown when user clicks to change avatar.

Recommendation:

// Example: Lazy-load modals
const AddToCollectionModal = dynamic(
  () => import("@/components/modals/add-to-collection-modal").then(m => ({ default: m.AddToCollectionModal })),
  { ssr: false }
);

GoogleAnalytics component is in the root layout and always mounts (to check consent and fetch config). Even though it conditionally renders the <Script> tags, it still: 1. Makes an API call to /bff/api/analytics-config on every page load 2. Reads localStorage

Recommendation: Move the analytics config fetch behind the consent check. Only fetch config after user has consented.


13. Security (Frontend-Specific)

Strengths

  • BFF proxy pattern prevents JWT token exposure to JavaScript -- token stored in httpOnly, secure, sameSite=lax cookie.
  • CSRF protection on BFF proxy via origin allowlist.
  • Path allowlist on BFF proxy prevents access to admin endpoints.
  • Open redirect prevention in login form (login-form.tsx:15-17) validates redirect URL starts with / and not //.
  • CSP headers configured in next.config.ts with reasonable restrictions.
  • Security headers (X-Frame-Options: DENY, X-Content-Type-Options: nosniff, HSTS, Referrer-Policy, Permissions-Policy).

Issues

MEDIUM: CSP allows 'unsafe-inline' and 'unsafe-eval' for scripts

next.config.ts:23:

script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://pagead2.googlesyndication.com

While 'unsafe-inline' is needed for Next.js inline scripts and 'unsafe-eval' may be needed for some dev tooling, this weakens XSS protection significantly.

Recommendation: Use nonce-based CSP for inline scripts (Next.js supports this) and remove 'unsafe-eval' in production builds.

LOW: dangerouslySetInnerHTML in JsonLd component

json-ld.tsx:2 uses dangerouslySetInnerHTML to inject JSON-LD. While the data comes from server-side recipe data (not user input directly), recipe titles and descriptions are user-generated. If a recipe title contains </script>, it could break out of the JSON-LD script tag.

Recommendation: Sanitize the JSON output by escaping </script> patterns, or use Next.js metadata API's built-in JSON-LD support.


14. Additional Observations

Strengths Worth Highlighting

  1. Privacy compliance is thorough: Cookie consent with granular categories, GDPR/CCPA compliance links, data export, data deletion, "Do Not Sell" link, consent synced to backend.

  2. Responsive design is well-implemented: Mobile-first with md: breakpoints, mobile bottom navigation, horizontal scroll on mobile with grid on desktop, touch-friendly targets.

  3. Design system is cohesive: Sage/cream/terracotta color palette is consistently applied. Custom CSS properties for radius, shadows, and animations create a unified feel.

  4. BFF architecture is production-grade: The separation of server-client and client-client API access patterns, with the BFF proxy handling auth, is a security best practice.

  5. Ingredient scaler with fraction parsing (mixed numbers, ranges, common cooking fractions) is a thoughtful feature with solid implementation.

Minor Issues

  • Dockerfile and .env.example are present but .env.local is committed (should be in .gitignore -- verify)
  • eslint.config.mjs exists but no evidence of CI enforcement
  • No test files visible in the codebase (0% test coverage)
  • next-env.d.ts should include a note about not editing it (Next.js generates it)

Top 10 Prioritized Recommendations

Priority Issue Impact Effort
1 Convert HomeContent to server component with client island for CTA LCP, SEO, bundle size Low
2 Add loading.tsx skeletons for major routes Perceived performance, UX Medium
3 Add next: { revalidate } caching to server-side fetches TTFB, API server load Low
4 Replace homepage hero CSS background-image with Next.js Image LCP Low
5 Add keyboard navigation to dropdown menus Accessibility (WCAG AA) Medium
6 Lazy-load modals and heavy components with next/dynamic Bundle size, TTI Medium
7 Add focus trap and ARIA attributes to cookie consent modal Accessibility (WCAG AA) Medium
8 Convert protected pages to server+client split pattern Streaming SSR, perceived perf High
9 Fix color contrast for stone-400 secondary text Accessibility (WCAG AA) Low
10 Add error monitoring/logging in catch blocks Debuggability, reliability Medium

Files Reviewed

Core Configuration

  • package.json, next.config.ts, tsconfig.json, postcss.config.mjs, eslint.config.mjs

App Router Pages & Layouts

  • src/app/layout.tsx, src/app/page.tsx, src/app/error.tsx, src/app/not-found.tsx
  • src/app/home-content.tsx
  • src/app/(auth)/layout.tsx, src/app/(auth)/login/page.tsx, src/app/(auth)/login/login-form.tsx
  • src/app/(public)/discover/page.tsx, src/app/(public)/discover/infinite-recipe-grid.tsx, src/app/(public)/discover/horizontal-recipe-scroll.tsx
  • src/app/(public)/search/page.tsx, src/app/(public)/search/search-bar.tsx
  • src/app/(public)/tags/loading.tsx, src/app/(public)/tags/[tagname]/loading.tsx
  • src/app/(recipes)/[username]/[slug]/page.tsx
  • src/app/(protected)/ai-recipe/page.tsx, src/app/(protected)/import-recipe/page.tsx
  • src/app/robots.ts, src/app/sitemap.ts

BFF Routes

  • src/app/bff/api/[...path]/route.ts
  • src/app/bff/auth/me/route.ts, src/app/bff/auth/login/route.ts

Components

  • src/components/layout/page-layout.tsx, header.tsx, footer.tsx, bottom-nav.tsx
  • src/components/recipes/recipe-card.tsx, recipe-form.tsx, recipe-actions.tsx, image-carousel.tsx, serving-scaler.tsx, comments-section.tsx, nutrition-label.tsx, recipe-grid.tsx
  • src/components/seo/json-ld.tsx
  • src/components/ui/avatar.tsx
  • src/components/analytics/google-analytics.tsx
  • src/components/privacy/cookie-consent.tsx
  • src/components/feature-gate.tsx

Libraries & Utilities

  • src/lib/api/client.ts, server-client.ts, types.ts, recipes.ts
  • src/lib/auth/auth-provider.tsx, index.ts
  • src/lib/theme/theme-provider.tsx
  • src/lib/hooks/use-local-storage.ts, use-subscription.ts
  • src/lib/seo/metadata.ts, jsonld.ts
  • src/lib/utils/image-url.ts, recipe-url.ts, formatters.ts, ingredient-scaler.ts
  • src/hooks/use-feature-flags.tsx
  • src/app/globals.css