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 withserverFetch, 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
searchParamshandling and parallel fetches. - Auth pages use Suspense boundaries correctly (e.g.,
LoginPagewrapsLoginFormin<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 patternscomponents/seo/-- SEO concernscomponents/privacy/-- compliance concerns-
components/analytics/-- tracking -
PageLayoutis a clean composition wrapper that composes Header + Footer + BottomNav. Used consistently across all pages. -
RecipeCardis a well-built server component (no"use client"directive). It uses Next.jsImagewith propersizesattribute, semantic<article>tag, and accessibility viaaria-label. Tags within the card are interactive links with properz-indexlayering over the stretched card link. -
RecipeGridis 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
useCallbackfor memoized auth methods and proper cleanup withcancelledflag. - ThemeProvider uses
useSyncExternalStorecorrectly for both localStorage reads andprefers-color-schememedia query -- this is the modern, correct approach and avoids the common FOUC issues. useLocalStorageValueis a well-implemented reactive localStorage hook usinguseSyncExternalStorewith cross-tab sync via thestorageevent 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 URLclient.ts-- Used in Client Components, routes through BFF proxy (/bff/api/[...path]), cookie sent viacredentials: "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.,
HomePagereturns 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.Interfont loaded vianext/font/googlewithdisplay: "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]andaspect-[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
Imagecomponent used throughout with properfill+sizesattributes on recipe cards and carousels. remotePatternsconfigured forstaging.recipicity.com,recipicity.com, andlh3.googleusercontent.com.- Avatar component uses
unoptimizedfor 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.
LOW: Image carousel doesn't preload adjacent images¶
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.csswith: - 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 inlinefor 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-hideutility properly implemented with both-webkit-scrollbarand Firefoxscrollbar-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-labelattributes on icon-only buttons (search, notifications, user menu, image carousel navigation).sr-onlytext 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/idon 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).
HIGH: Cookie consent modal lacks focus trap¶
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
MEDIUM: Image carousel keyboard accessibility¶
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) metadataBaseconfigured- OpenGraph and Twitter card defaults
-
Robots meta conditionally indexes only production
-
Recipe-specific SEO is excellent:
generateMetadataon recipe pages with title, description, OG images, canonical URLrecipeMetadata()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.tscorrectly disallows staging and allows production with sensible disallow rules for private routes. -
sitemap.tsgenerates 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:
-
Modals --
AddToCollectionModal,AddToMealPlanModal,CalendarSubscriptionModal,BugReportModalare imported eagerly but only rendered conditionally. Each modal should usenext/dynamicwithssr: false. -
RecipeForm(~765 lines) -- Only needed on create/edit pages but is a heavy component with tag autocomplete, image upload, unit select, and drag handles. -
CookieConsent-- Loaded in root layout on every page but only shows once (until user dismisses). Could be lazy-loaded. -
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 }
);
MEDIUM: Google Analytics loaded on every page regardless of consent¶
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.tswith 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¶
-
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.
-
Responsive design is well-implemented: Mobile-first with
md:breakpoints, mobile bottom navigation, horizontal scroll on mobile with grid on desktop, touch-friendly targets. -
Design system is cohesive: Sage/cream/terracotta color palette is consistently applied. Custom CSS properties for radius, shadows, and animations create a unified feel.
-
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.
-
Ingredient scaler with fraction parsing (mixed numbers, ranges, common cooking fractions) is a thoughtful feature with solid implementation.
Minor Issues¶
Dockerfileand.env.exampleare present but.env.localis committed (should be in.gitignore-- verify)eslint.config.mjsexists but no evidence of CI enforcement- No test files visible in the codebase (0% test coverage)
next-env.d.tsshould 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.tsxsrc/app/home-content.tsxsrc/app/(auth)/layout.tsx,src/app/(auth)/login/page.tsx,src/app/(auth)/login/login-form.tsxsrc/app/(public)/discover/page.tsx,src/app/(public)/discover/infinite-recipe-grid.tsx,src/app/(public)/discover/horizontal-recipe-scroll.tsxsrc/app/(public)/search/page.tsx,src/app/(public)/search/search-bar.tsxsrc/app/(public)/tags/loading.tsx,src/app/(public)/tags/[tagname]/loading.tsxsrc/app/(recipes)/[username]/[slug]/page.tsxsrc/app/(protected)/ai-recipe/page.tsx,src/app/(protected)/import-recipe/page.tsxsrc/app/robots.ts,src/app/sitemap.ts
BFF Routes¶
src/app/bff/api/[...path]/route.tssrc/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.tsxsrc/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.tsxsrc/components/seo/json-ld.tsxsrc/components/ui/avatar.tsxsrc/components/analytics/google-analytics.tsxsrc/components/privacy/cookie-consent.tsxsrc/components/feature-gate.tsx
Libraries & Utilities¶
src/lib/api/client.ts,server-client.ts,types.ts,recipes.tssrc/lib/auth/auth-provider.tsx,index.tssrc/lib/theme/theme-provider.tsxsrc/lib/hooks/use-local-storage.ts,use-subscription.tssrc/lib/seo/metadata.ts,jsonld.tssrc/lib/utils/image-url.ts,recipe-url.ts,formatters.ts,ingredient-scaler.tssrc/hooks/use-feature-flags.tsxsrc/app/globals.css