Skip to content

Landing Page & Public Site - Deep Dive Documentation

Generated: 2026-02-25 Scope: Full-stack public-facing pages: landing page, blog (listing/detail/author/category), contact, dynamic pages, navigation, and supporting backend modules Files Analyzed: 81 (22 backend + 13 landing components + 7 route pages + 7 page components + 7 layout components + 7 API modules + 2 server actions + 1 middleware + 3 services + 12 DB entities) Lines of Code: ~8,200+ Workflow Mode: Exhaustive Deep-Dive


Overview

The public site is the unauthenticated, visitor-facing portion of the SaaS boilerplate. It comprises a static marketing landing page, a server-rendered blog system (listing, detail, author, category pages), a contact form, and a dynamic page system for arbitrary content pages (about, terms, privacy, etc.). The frontend is a Next.js 15 App Router application using React Server Components for data fetching and "use client" components for interactivity.

Purpose: Provide the public-facing experience for unauthenticated visitors -- marketing, content discovery, SEO, lead capture.

Key Responsibilities: - Landing page marketing content (hero, features, pricing, FAQ, testimonials, CTA) - Blog content discovery and reading (listing, search by author/category, single post) - Contact form submission - Dynamic content pages (about, terms, privacy, etc.) - Navigation (header/footer with admin-configured menu links) - SEO metadata generation

Integration Points: - Backend Public module (/api/v1/public/*) -- unauthenticated blog & menu endpoints - Backend Blog module (/api/v1/blogs/*) -- authenticated blog CRUD (admin) - Backend GenericPage module (/api/v1/pages/*) -- dynamic page retrieval - Backend Setting module (/api/v1/settings/*) -- site settings (logo, meta, contact info) - Backend Contact module (/api/v1/contacts) -- contact form submission - Backend Menu/MenuItem modules -- navigation link data - 11 DB entities: Blog, BlogCategory, BlogTags, BlogView, GenericPage, Setting, AppConfig, ContactMessage, Menu, MenuItem, User


Known Issues Summary

Severity Count Key Theme
Critical 7 unstable_cache broken (5), Stored XSS (2)
High 10 Dead CTA buttons (3), No homepage SEO, Blog view count manipulation, Missing status filter, Author query not slug-based, GenericPage exposes all content (2), Author page false-positive 200
Medium 30 Missing SEO on 3 pages, console.log in production (3), duplicate API calls, data duplication, no error boundaries, sequential API calls, no SSG/sitemap, a11y gaps, form issues, null safety, pagination edge cases, etc.
Low 23 Naming conventions, breadcrumb display, dead code, typos, canonical URL errors, ineffective Suspense, Redux on public pages
Total 70 (45 original + 25 from Expert Panel)

Critical Issues (7)

C1. unstable_cache Key Missing Parameters -- getBlogs (CRITICAL)

  • File: Sass-boilerplate-frontend-v1/src/api/public/blogs.js:5
  • Impact: ALL blog listing queries (different pages, authors, categories) share cache key ["blogs"]. The first call's result is cached and returned for ALL subsequent calls regardless of parameters. Page 2 returns page 1 content. Author "John" page returns "Jane" page content. Category filtering is completely broken.
  • Root Cause: unstable_cache second argument is the cache key, not a label. Must include parameters: ["blogs", JSON.stringify(params)]
  • Affected Routes: /blogs, /author/[slug], /category/[slug]
  • Severity Justification: Every blog-related page shows wrong content after first cache population.

C2. unstable_cache Key Missing Parameters -- getBlogDetails (CRITICAL)

  • File: Sass-boilerplate-frontend-v1/src/api/public/blogs.js:36
  • Impact: Cache key ["blog-details"] shared across ALL slugs. First blog fetched is returned for every subsequent blog detail page. Blog A's content appears on Blog B, C, D, etc.
  • Root Cause: Cache key must include slug: ["blog-details", slug]
  • Affected Routes: /blogs/[slug]

C3. unstable_cache Key Missing Parameters -- getDynamicPage (CRITICAL)

  • File: Sass-boilerplate-frontend-v1/src/api/public/dynamic-page.js:5
  • Impact: Cache key ["dynamic-page"] shared across ALL dynamic page slugs. /about content shown for /terms, /privacy, etc.
  • Root Cause: Cache key must include slug: ["dynamic-page", slug]
  • Affected Routes: /[slug] (all dynamic pages)

C4. unstable_cache Key Missing Parameters -- getSettingByGroup (CRITICAL)

  • File: Sass-boilerplate-frontend-v1/src/api/public/settings.js:5
  • Impact: Cache key ["settings-group"] shared across ALL setting groups. getSettingByGroup("contact") and getSettingByGroup("site") return the same result. Contact page may show site settings or vice versa.
  • Root Cause: Cache key must include group: ["settings-group", group]
  • Affected Routes: All pages (layout fetches "site"), /contact (fetches "contact")
  • File: Sass-boilerplate-frontend-v1/src/api/public/navigation-links.js:6
  • Impact: Cache key ["navigation-links"] shared across types. getNavigationLinks("navbar"), getNavigationLinks("footer"), and getNavigationLinks("bottombar") all return the same cached result. Footer shows navbar links or navbar shows footer links.
  • Root Cause: Cache key must include type: ["navigation-links", type]
  • Affected Routes: All pages (layout fetches navbar + footer + bottombar)

C1-C5 Root Cause Analysis: The developer misunderstood Next.js unstable_cache API. The second argument (key array) serves as the unique cache identifier, not merely a descriptive tag. Every function wrapping unstable_cache that accepts parameters MUST include those parameters in the key array. This is the single most impactful bug in the public site -- it makes the entire caching layer produce incorrect data.

C6. Stored XSS -- Blog Content via dangerouslySetInnerHTML (CRITICAL)

  • File: Sass-boilerplate-frontend-v1/src/components/pages/blogs/blog-page-frontend.jsx:94
  • Code: dangerouslySetInnerHTML={{ __html: blog.content }}
  • Impact: Blog content (HTML from DB) is rendered directly without sanitization. If an admin (or attacker who gains blog-create permissions) injects <script> tags or event handlers, they execute in every visitor's browser. Combined with the known self-role-change vulnerability (PATCH /users/profile), an attacker could: create account -> escalate to admin -> inject malicious blog content -> compromise all visitors.
  • Fix: Use DOMPurify to sanitize HTML before rendering, or render blog content through a safe Markdown/rich-text renderer.

C7. Stored XSS -- Dynamic Page Content via dangerouslySetInnerHTML (CRITICAL)

  • File: Sass-boilerplate-frontend-v1/src/components/pages/dynamic-page/dynamic-page.jsx:20
  • Code: dangerouslySetInnerHTML={{ __html: data?.description }}
  • Impact: Same as C6 but for generic/dynamic pages. Any page content (about, terms, privacy) rendered as raw HTML from the API.
  • Fix: Same as C6 -- sanitize with DOMPurify.

High Issues (7)

H1. All Landing Page CTA Buttons Are Non-Functional

  • Files: hero-section.jsx, cta-section.jsx, pricing-section.jsx
  • Count: 7 buttons total across the landing page
  • Impact: "Start Free Trial" (x2), "Book a Demo", "Schedule a Demo", and 3 pricing plan CTAs have no href, no onClick, no navigation. Users clicking them see a ripple animation but nothing happens. This is the primary conversion path for the landing page.
  • Buttons: All use either custom/button.jsx (renders <button type="button">) or ui/button.jsx (same) with no action attached.

H2. Homepage Has No SEO Metadata

  • File: Sass-boilerplate-frontend-v1/src/app/(main-layout)/page.js
  • Impact: The most important page for SEO has no generateMetadata or metadata export. No title, description, keywords, or Open Graph tags specific to the homepage. Relies entirely on root layout's generic settings fetch. If the API is down, metadata falls back to "Sass Boilerplate" (with inconsistent casing).

H3. Blog View Count Increment Has No Protection

  • File: saas-boilerplate/src/app/modules/v1/Public/public.service.ts
  • Impact: Every time a blog detail page is loaded, BlogView count is incremented unconditionally. There is no:
  • Rate limiting per IP/session
  • Cookie-based deduplication
  • Time-window throttling
  • Exploitation: Simple script can inflate any blog's view count to arbitrary numbers, skewing analytics and "top blogs" ranking.

H4. Public Blog Endpoint Serves Unpublished Content

  • File: saas-boilerplate/src/app/modules/v1/Public/public.service.ts
  • Impact: The getAllBlogs method in PublicService does not filter by status = 'published'. Draft and archived blogs may appear in public listings unless the QueryBuilder happens to exclude them (depends on implementation -- needs verification). The getBlogByIdOrSlug method similarly may serve draft content to the public.
  • Verification needed: Check if there's a .where("blog.status = :status", { status: "published" }) clause in the query builder.

H5. Author Query Uses Name String, Not ID or Slug

  • File: Sass-boilerplate-frontend-v1/src/app/(main-layout)/author/[slug]/page.js:14
  • Code: const author = decodeURIComponent(slug)?.split("-").join(" ")
  • Impact: Author URL slugs are converted by splitting hyphens and joining with spaces, then passed as the author query parameter. This is fragile:
  • Author "Jean-Pierre Dupont" would be decoded as "Jean Pierre Dupont" (loses actual hyphen)
  • Authors with special characters in names will break
  • No author entity/ID is used -- pure string matching
  • Case sensitivity depends on DB collation

H6. fetchMetaData Error Handler Returns Wrong Type

  • File: Sass-boilerplate-frontend-v1/src/app/layout.js:15
  • Impact: On API error, fetchMetaData returns the string "SASS Boilerplate" instead of an object. generateMetadata then accesses settings?.admin?.site_name on a string, producing undefined. The title.template becomes "%s | undefined", causing all child page titles to display as "Blogs | undefined", "Contact | undefined", etc.

H7. Non-OK API Response Falls Through to .json()

  • File: Sass-boilerplate-frontend-v1/src/api/public/blogs.js:15-16
  • Impact: When !response.ok, the code logs "Failed to fetch blogs" but does NOT return or throw. Execution continues to response.json() which will crash if the error response body is not valid JSON (e.g., HTML error page from a proxy). Same pattern in all 4 public API modules.

Medium Issues (17)

M1. 404 Pages Return HTTP 200 Status

  • Files: blogs/[slug]/page.js, [slug]/page.js
  • Impact: When a blog or dynamic page is not found, <NotFound /> component renders but HTTP status remains 200. Search engines index these as valid pages. Should use notFound() from next/navigation to return proper 404 status.

M2. No SEO Metadata on Author Pages

  • File: author/[slug]/page.js
  • No generateMetadata export.

M3. No SEO Metadata on Category Pages

  • File: category/[slug]/page.js
  • No generateMetadata export.

M4. No SEO Metadata on Contact Page

  • File: contact/page.js
  • No generateMetadata export.

M5. console.log Left in Production -- Category Page

  • File: category/[slug]/page.js:19
  • Code: console.log("category blogs", data?.blogs) -- logs full blog data array to server console on every request.

M6. console.log Left in Production -- Dynamic Page

  • File: [slug]/page.js:42
  • Code: console.log("slug", slug) -- logs slug on every dynamic page request.

M7. console.log Left in Production -- Blog Detail

  • File: blog-page-frontend.jsx:10
  • Code: console.log("Blog data:", blog) -- logs full blog object.

M8. Duplicate API Calls in generateMetadata + Page Body

  • Files: blogs/[slug]/page.js, [slug]/page.js
  • Both generateMetadata and the page component call the same API function. While unstable_cache may help, after cache expiration both calls independently re-fetch.

M9. Annual Pricing Data Hardcoded Inline

  • File: pricing-section.jsx:115-155
  • Monthly plans come from pricingPlans constant but annual plans are a separate inline array. Feature lists, plan names, and prices are duplicated and can drift. Annual prices are not computed from monthly (stored as strings like "$29" preventing calculation).

M10. No Error Boundary in (main-layout)

  • File: (main-layout)/layout.js
  • No error.js file exists at any level in the (main-layout) route group. If HeaderWrapper or FooterWrapper throw during SSR (API down, network timeout), the entire page crashes with an unhandled error.

M11. [slug] Route Catch-All Conflict Potential

  • File: (main-layout)/[slug]/page.js
  • This dynamic route at the layout root catches any path not handled by a static route. Future routes added without dedicated directories will be silently caught by this handler instead of 404-ing.

M12. Inconsistent Field Names Between Metadata and Component

  • File: blogs/[slug]/page.js
  • Metadata uses blogData?.image but BlogPageFrontend uses blog?.imageUrl. These may refer to different fields depending on API response shape.

M13. Two Different Button Components Used Across Landing Page

  • Files: hero-section.jsx, cta-section.jsx use custom/button.jsx (default export, ripple effect, no href support); pricing-section.jsx uses ui/button.jsx (named export, shadcn/Radix, supports asChild for links). Different APIs, different visual behavior, used on the same page.

M14. custom/button.jsx -- secondary Variant Defined Twice

  • File: custom/button.jsx:67,70
  • Object literal has secondary key twice. First definition (bg-secondary text-white) is overwritten by second (bg-gray-200 text-gray-800). Dead code.

M15. custom/button.jsx -- Focus Ring Classes Non-Functional

  • File: custom/button.jsx
  • focus:ring-* classes in primary and secondary variants have no effect because focus:outline-none is applied in base styles but no focus:ring-2 base is set. Focus rings never render.

M16. Backend baseUrl Construction in Public Controller

  • File: saas-boilerplate/src/app/modules/v1/Public/public.controller.ts
  • baseUrl is constructed from req.protocol + "://" + req.get("host") and passed to service methods for image URL transformation. Behind a reverse proxy without proper X-Forwarded-* headers, this produces incorrect URLs (e.g., http:// instead of https://).

M17. useQuery Imported in Server-Side Module

  • File: Sass-boilerplate-frontend-v1/src/api/public/navigation-links.js:2
  • useQuery from @tanstack/react-query imported at top level of a module also used server-side. While the hook is only called client-side, the import bloats the server bundle.

Low Issues (14)

L1. Page Function Naming Convention

  • Files: All page.js files in (main-layout)/
  • Functions named page (lowercase) instead of Page (PascalCase React convention).

L2. Breadcrumb Shows Raw Slug on Author Pages

  • File: author/[slug]/page.js:27
  • { label: slug } displays john-doe instead of John Doe.

L3. Breadcrumb Shows Raw Slug on Category Pages

  • File: category/[slug]/page.js:27
  • Same issue as L2.

L4. Category Title Derived from Wrong Source

  • File: category/[slug]/page.js:24
  • Title from data?.blogs[0]?.categories[0]?.name may show wrong category if blog has multiple categories.

L5. CSS Typo in Contact Page

  • File: contact-page.jsx:293
  • Class breaka-all should be break-all.

L6. Dead Code -- Commented-Out Skeleton in Contact Page

  • File: contact-page.jsx:43-134
  • ~90 lines of commented-out skeleton code.

L7. Dead Code -- Commented-Out NoScrollRestoration Import

  • File: (main-layout)/layout.js:6,13

L8. HeaderSkeleton Broken CSS Class

  • File: header-skeleton.jsx:16
  • className="size-9 -full" -- missing rounded prefix; should be rounded-full.

L9. (main-layout)/layout.js Named RootLayout

  • Misleading function name; this is not the root layout.

L10. Unnecessary async on Homepage page()

  • File: (main-layout)/page.js
  • Declared async but performs no await operations.

L11. Animation Variants Defined Inside Component Body

  • File: featured-section.jsx
  • container and item animation objects recreated on every render. Should be module-level constants.

L12. Both Dashboard Images Loaded with priority

  • File: hero-section.jsx
  • Light and dark dashboard screenshots both have priority={true}. Only one is ever visible; the hidden variant wastes bandwidth.

L13. Partner Logos Use brightness-0 (Pure Black)

  • File: hero-section.jsx
  • All partner logos forced to pure black in light mode. Colored logos lose all visual identity.

L14. Inconsistent CTA Label Text

  • Files: hero-section.jsx vs cta-section.jsx
  • "Book a Demo" (hero) vs "Schedule a Demo" (CTA) -- same intent, different wording.

Complete File Inventory

Backend Files (22 files, ~2,500 LOC)

saas-boilerplate/src/app/modules/v1/Public/public.controller.ts (111 LOC)

  • Purpose: Controller for all unauthenticated public blog and menu endpoints. Delegates to PublicService and MenuItemService.
  • Exports: default PublicController object with 8 methods: getAllBlogs, getCategoriesWithBlogCount, getTopBlogs, getHomePageBlog, getBlogByIdOrSlug, getCategoriesByFeatured, getMenuItemByType, getMenuList
  • Dependencies: catchAsync, sendResponse, PublicService, MenuItemService
  • Used By: public.routes.ts
  • Side Effects: All methods read from DB via service layer
  • Error Handling: catchAsync wrapper passes errors to globalErrorHandler

saas-boilerplate/src/app/modules/v1/Public/public.routes.ts (~30 LOC)

  • Purpose: Registers 8 public GET routes with no auth middleware.
  • Routes (all GET, no auth required): | Path | Handler | Purpose | |------|---------|---------| | /public/blogs | getAllBlogs | Blog listing with pagination, author/category filters | | /public/blogs/categories | getCategoriesWithBlogCount | All categories with post counts | | /public/blogs/top-blogs | getTopBlogs | Most-viewed blogs | | /public/blogs/home-page-blog | getHomePageBlog | Homepage blog selection | | /public/blogs/:id | getBlogByIdOrSlug | Single blog by ID or slug | | /public/blogs/category/:categoryNameOrSlug | getCategoriesByFeatured | Blogs by category | | /public/menus | getMenuItemByType | Navigation menu items by type | | /public/menus/list | getMenuList | All menus |
  • Used By: Main router registration in v1/ routes

saas-boilerplate/src/app/modules/v1/Public/public.service.ts (~300 LOC)

  • Purpose: Service layer for public blog and menu queries. Uses TypeORM QueryBuilder for complex blog queries with joins, sorting, pagination, and image URL transformation.
  • Key Methods:
  • getAllBlogs(query, baseUrl) -- Paginated blog listing with category/author filtering, joins BlogCategory and User, transforms image URLs, increments view count
  • getTopBlogs() -- Top 5 blogs by view count
  • getBlogByIdOrSlug(id, baseUrl) -- Single blog with author, categories, tags, view count increment
  • getHomePageBlogs(baseUrl) -- Featured blogs for homepage
  • getCategoriesWithBlogCount() -- Categories with blog counts
  • categoriesByFeatured(slug, baseUrl) -- Blogs filtered by category slug
  • getMenu(type) -- Menu items filtered by type with tree structure
  • Side Effects: Increments BlogView count on every blog detail fetch (no dedup)
  • DB Entities Accessed: Blog, BlogCategory, BlogTags, BlogView, User, Menu, MenuItem

saas-boilerplate/src/app/modules/v1/Blog/blog.controller.ts (~150 LOC)

  • Purpose: Admin CRUD controller for blogs. Requires authentication.
  • Exports: Blog CRUD operations (create, update, delete, list, get by ID)

saas-boilerplate/src/app/modules/v1/Blog/blog.routes.ts (~50 LOC)

  • Purpose: Authenticated blog routes for admin dashboard.
  • Auth: All routes require JWT auth middleware

saas-boilerplate/src/app/modules/v1/Blog/blog.service.ts (~250 LOC)

  • Purpose: Blog CRUD service with TypeORM operations.
  • Key Operations: Create blog with slug generation, update, soft delete, restore, bulk operations

saas-boilerplate/src/app/modules/v1/GenericPage/genericPage.controller.ts (~80 LOC)

  • Purpose: Controller for CMS-style generic pages (about, terms, privacy, etc.).
  • Exports: CRUD + getBySlug for public access

saas-boilerplate/src/app/modules/v1/GenericPage/genericPage.routes.ts (~30 LOC)

  • Purpose: Routes for generic pages. GET /pages/:slug is publicly accessible.

saas-boilerplate/src/app/modules/v1/GenericPage/genericPage.service.ts (~100 LOC)

  • Purpose: GenericPage CRUD service. getBySlug finds page by slug column.

saas-boilerplate/src/app/modules/v1/Setting/setting.controller.ts (~100 LOC)

  • Purpose: Settings controller. getByGroupPrefix endpoint used by public pages.

saas-boilerplate/src/app/modules/v1/Setting/setting.routes.ts (~30 LOC)

  • Purpose: Settings routes. GET /settings/:group/prefix is publicly accessible.

saas-boilerplate/src/app/modules/v1/Setting/setting.service.ts (~120 LOC)

  • Purpose: Settings CRUD. Returns array of {key, value} pairs for a given group prefix.

saas-boilerplate/src/app/modules/v1/MenuBuilder/Menu/menu.controller.ts (~80 LOC)

  • Purpose: Menu CRUD controller. Used by admin for managing navigation menus.

saas-boilerplate/src/app/modules/v1/MenuBuilder/Menu/menu.routes.ts (~30 LOC)

  • Purpose: Menu routes. Most require auth; public access via /public/menus.

saas-boilerplate/src/app/modules/v1/MenuBuilder/Menu/menu.service.ts (~150 LOC)

  • Purpose: Menu CRUD + tree building for nested menu items.

saas-boilerplate/src/app/modules/v1/Contact/contact.controller.ts (~60 LOC)

  • Purpose: Contact form submission handler + admin contact list management.

saas-boilerplate/src/app/modules/v1/Contact/contact.routes.ts (~20 LOC)

  • Purpose: POST /contacts is publicly accessible (form submission). GET/DELETE routes require auth.

saas-boilerplate/src/app/modules/v1/Contact/contact.service.ts (~80 LOC)

  • Purpose: Creates ContactMessage entity. Admin can list, delete, restore.

Frontend Landing Page Files (13 files, ~1,305 LOC)

Sass-boilerplate-frontend-v1/src/app/(main-layout)/page.js (35 LOC)

  • Purpose: Homepage route (/). Assembles 7 landing page sections in order: Hero, Featured, HowItWorks, Testimonials, Pricing, FAQ, CTA.
  • Type: Server Component (renders client children)
  • Data Fetching: None. All content from static constants.
  • SEO: None (no generateMetadata export) -- Issue H2
  • Issues: Unnecessary async, lowercase function name

Sass-boilerplate-frontend-v1/src/app/(main-layout)/layout.js (28 LOC)

  • Purpose: Layout wrapper for all public pages. Provides header, footer, tooltip context.
  • Type: Server Component with Suspense boundaries
  • Data Fetching: Delegates to HeaderWrapper (navbar links + site settings) and FooterWrapper (footer links + bottombar links + site settings)
  • Structure: TooltipProvider > Suspense(Header) > <main> > Suspense(Footer + FooterCredit)
  • Issues: Named RootLayout (misleading), no error.js boundary, FooterCredit inside Footer Suspense

Sass-boilerplate-frontend-v1/src/app/layout.js (97 LOC)

  • Purpose: Root layout. Sets up font (Poppins), providers (Redux, React Query, Auth, Theme), dynamic metadata from settings API.
  • Provider Hierarchy: ReduxProvider > QueryProvider > AuthInitializer > ThemeProvider > Toaster > {children}
  • SEO: generateMetadata fetches from /settings/admin/prefix with ISR. Full OG + Twitter cards.
  • Issues: Error handler returns string instead of object (H6), inconsistent fallback casing ("Sass"/"SaaS"/"SASS"), cz-shortcut-listen="true" hardcoded

Sass-boilerplate-frontend-v1/src/components/pages/landing-page/hero-section.jsx (125 LOC)

  • Purpose: Primary hero with badge, headline, 2 CTA buttons, trust indicators, dashboard screenshot (light/dark), partner logos bar.
  • Type: Client Component ("use client")
  • Data: partners array (6 items) from constants + 2 static dashboard images
  • Animation: Framer Motion animate on mount (not scroll-triggered)
  • Issues: Both CTA buttons non-functional (H1), both images priority-loaded (L12), partner logos forced to black (L13)
  • Hardcoded Content: Badge "Launching Soon", heading "Elevate Your Workflow with SaaSify", all button labels, trust indicators

Sass-boilerplate-frontend-v1/src/components/pages/landing-page/featured-section.jsx (75 LOC)

  • Purpose: 6 feature cards in responsive grid (½/3 columns). Staggered reveal animation.
  • Type: Client Component
  • Data: features array (6 items with icon, title, description)
  • Anchoring: id="features" -- navigable via #features
  • Issues: Animation variants defined inside component body (L11)

Sass-boilerplate-frontend-v1/src/components/pages/landing-page/how-it-works.jsx (60 LOC)

  • Purpose: 3-step process flow with numbered circles and connecting line.
  • Type: Client Component
  • Data: steps array (3 items: "01", "02", "03")
  • Issues: No id attribute (not navigable via anchor), no semantic <ol>/<li> markup

Sass-boilerplate-frontend-v1/src/components/pages/landing-page/testimonial-section.jsx (78 LOC)

  • Purpose: 6 testimonial cards with star ratings, quotes, author info.
  • Type: Client Component
  • Data: testimonials array (6 items, all rating: 5)
  • Anchoring: id="testimonials"
  • Issues: No <blockquote>/<cite> semantic markup, star ratings have no ARIA labels, all ratings are 5 (artificial)

Sass-boilerplate-frontend-v1/src/components/pages/landing-page/pricing-section.jsx (220 LOC)

  • Purpose: 3 pricing plans with monthly/annual tab switching. "Most Popular" badge on Professional plan.
  • Type: Client Component
  • Data: Monthly from pricingPlans constant; Annual HARDCODED inline (M9)
  • Anchoring: id="pricing"
  • Tab Component: Radix Tabs (defaultValue="monthly")
  • Issues: Annual data duplicated (M9), CTA buttons non-functional (H1), uses ui/button while others use custom/button (M13)

Sass-boilerplate-frontend-v1/src/components/pages/landing-page/faq-section.jsx (63 LOC)

  • Purpose: 6-item accordion FAQ. Single-item open, collapsible.
  • Type: Client Component
  • Data: faqs array (6 items)
  • Anchoring: id="faq"
  • Cleanest component -- no significant issues found.

Sass-boilerplate-frontend-v1/src/components/pages/landing-page/cta-section.jsx (52 LOC)

  • Purpose: Final CTA section with headline, description, 2 buttons, "no credit card" note.
  • Type: Client Component
  • Data: Fully hardcoded
  • Issues: Buttons non-functional (H1), inconsistent label "Schedule a Demo" vs hero's "Book a Demo" (L14), no id attribute

Sass-boilerplate-frontend-v1/src/constants/landing-page-data.js (214 LOC)

  • Purpose: Centralized data store for all landing page content.
  • Exports: partners (6), features (6), testimonials (6), pricingPlans (3), faqs (6), steps (3)
  • Issues: JSX in .js file (not .jsx), prices stored as strings preventing calculation, partner names are real brands (Airbnb -- misleading for boilerplate)

Sass-boilerplate-frontend-v1/src/constants/metadata.js (139 LOC)

  • Purpose: Static demo data for admin dashboard. NOT used by landing page at all.
  • Note: Contains postimg.cc URLs for user avatars. Irrelevant to public site scope.

Sass-boilerplate-frontend-v1/src/components/custom/button.jsx (119 LOC)

  • Purpose: Custom button with ripple effect, loading state, press-scale animation, 6 variants.
  • Props: variant, size, className, children, onClick, isLoading, disabled
  • Issues: secondary variant defined twice (M14), focus rings non-functional (M15), setTimeout leak on unmount, type="button" hardcoded (can never submit forms)

Frontend Route & Page Component Files (14 files, ~1,600 LOC)

Sass-boilerplate-frontend-v1/src/app/(main-layout)/blogs/page.js (79 LOC)

  • Route: /blogs
  • Type: Server Component
  • Data: getBlogs({ page, limit: 9 }) via unstable_cache (BROKEN -- C1)
  • SEO: Yes -- static generateMetadata with title "Blogs", OG tags
  • Pagination: Server-side via PaginationServer component using searchParams.page
  • Components: Breadcrumb2, BlogCard, PaginationServer

Sass-boilerplate-frontend-v1/src/app/(main-layout)/blogs/[slug]/page.js (52 LOC)

  • Route: /blogs/:slug
  • Type: Server Component
  • Data: getBlogDetails(slug) via unstable_cache (BROKEN -- C2), called TWICE (M8)
  • SEO: Yes -- dynamic generateMetadata from blog data (title, excerpt, image, keywords)
  • 404: Renders <NotFound /> component (HTTP 200, not 404 -- M1)
  • XSS: dangerouslySetInnerHTML on blog content (C6)

Sass-boilerplate-frontend-v1/src/app/(main-layout)/author/[slug]/page.js (62 LOC)

  • Route: /author/:slug
  • Type: Server Component
  • Data: getBlogs({ author }) (BROKEN -- C1)
  • SEO: None (M2)
  • Slug Parsing: decodeURIComponent(slug).split("-").join(" ") -- fragile name reconstruction (H5)
  • Title: From first blog's author name, falls back to "Author"

Sass-boilerplate-frontend-v1/src/app/(main-layout)/category/[slug]/page.js (62 LOC)

  • Route: /category/:slug
  • Type: Server Component
  • Data: getBlogs({ category: slug }) (BROKEN -- C1)
  • SEO: None (M3)
  • Issues: console.log in production (M5), breadcrumb shows raw slug (L3)

Sass-boilerplate-frontend-v1/src/app/(main-layout)/contact/page.js (15 LOC)

  • Route: /contact
  • Type: Server Component wrapper for client ContactPage
  • Data: getSettingByGroup("contact") (BROKEN -- C4)
  • SEO: None (M4)
  • Form Fields: First Name (required), Last Name (optional), Email (required), Message (required min 10)
  • Submission: POST /contacts with {name, email, details}

Sass-boilerplate-frontend-v1/src/app/(main-layout)/[slug]/page.js (55 LOC)

  • Route: /:slug (catch-all dynamic)
  • Type: Server Component
  • Data: getDynamicPage(slug) (BROKEN -- C3), called TWICE (M8)
  • SEO: Yes -- dynamic from page data
  • XSS: dangerouslySetInnerHTML on page description (C7)
  • Issues: console.log in production (M6), route conflict potential (M11)

Sass-boilerplate-frontend-v1/src/app/(main-layout)/not-found.js (20 LOC)

  • Purpose: 404 component (not Next.js route-level not-found.js). Manually rendered by blogs/[slug] and [slug] pages.
  • Navigation: "Go back to Home" links to /
  • Issue: Returns HTTP 200, not 404 (M1)

Sass-boilerplate-frontend-v1/src/components/pages/blogs/blog-card.jsx (90 LOC)

  • Purpose: Blog card for listing pages. Shows image, categories (up to 3), title, excerpt, author avatar+name, date.
  • Type: Server Component
  • Links: Card links to /blogs/{slug}, category badges to /category/{slug}, author to /author/{normalizeText(name)}
  • Image: Uses next/image with transformImageUrl() helper

Sass-boilerplate-frontend-v1/src/components/pages/blogs/blog-page-frontend.jsx (196 LOC)

  • Purpose: Full blog post display. Hero image with overlay, category badges, content, sidebar with author info and related articles.
  • Type: Server Component
  • XSS Risk: dangerouslySetInnerHTML={{ __html: blog.content }} (C6)
  • Related Articles: Fetches from getBlogs({ limit: 5 }) (also BROKEN by C1)

Sass-boilerplate-frontend-v1/src/components/pages/contact-frontend/contact-page.jsx (307 LOC)

  • Purpose: Contact form with validation, settings display (phone, email, address).
  • Type: Client Component ("use client")
  • Form: Uses useState + custom validation. useSubmitContactForm() mutation.
  • Issues: Dead skeleton code (~90 lines), CSS typo breaka-all (L5)

Sass-boilerplate-frontend-v1/src/components/pages/contact-frontend/contact-skeleton.jsx (97 LOC)

  • Purpose: Skeleton loading UI for contact page.
  • Type: Server Component

Sass-boilerplate-frontend-v1/src/components/pages/dynamic-page/dynamic-page.jsx (28 LOC)

  • Purpose: Renders generic page content with breadcrumb.
  • Type: Server Component
  • XSS Risk: dangerouslySetInnerHTML={{ __html: data?.description }} (C7)

Sass-boilerplate-frontend-v1/src/components/custom/breadcrumb2.jsx (62 LOC)

  • Purpose: Breadcrumb navigation with title and subtitle display.
  • Type: Server Component
  • Links: Each breadcrumb item links to item.href

Sass-boilerplate-frontend-v1/src/components/custom/pagination.jsx (334 LOC)

  • Purpose: Dual-export pagination component.
  • Exports: PaginationServer (URL-based, SSR), PaginationClient (state-based, client)
  • Type: PaginationServer is Server Component; PaginationClient is Client Component

Frontend API & Data Fetching Files (9 files, ~600 LOC)

Sass-boilerplate-frontend-v1/src/api/public/blogs.js (68 LOC)

  • Exports: getBlogs, getBlogDetails
  • Endpoints: GET /public/blogs, GET /public/blogs/{slug}
  • Cache: unstable_cache with BROKEN keys (C1, C2)
  • Auth: None (bare fetch())

Sass-boilerplate-frontend-v1/src/api/public/settings.js (37 LOC)

  • Exports: getSettingByGroup
  • Endpoint: GET /settings/{group}/prefix
  • Cache: unstable_cache with BROKEN key (C4)
  • Transform: Reduces {key, value} array to nested object by splitting key on .

Sass-boilerplate-frontend-v1/src/api/public/dynamic-page.js (30 LOC)

  • Exports: getDynamicPage
  • Endpoint: GET /pages/{slug}
  • Cache: unstable_cache with BROKEN key (C3)

Sass-boilerplate-frontend-v1/src/api/public/navigation-links.js (66 LOC)

  • Exports: getNavigationLinks (server), useNavigationLinks (client React Query hook)
  • Endpoint: GET /public/menus?type={type}
  • Cache: Server: unstable_cache with BROKEN key (C5). Client: React Query with staleTime/gcTime based on CACHE_TIME.
  • Issue: useQuery imported at module level affecting server bundle (M17)

Sass-boilerplate-frontend-v1/src/api/contacts/index.js (201 LOC)

  • Exports: 8 hooks. Only useSubmitContactForm relevant to public site.
  • Public Endpoint: POST /contacts with {name, email, details}
  • Auth: Uses authenticated axios instance -- but POST /contacts backend route has no auth requirement. The axios interceptor adds auth headers unnecessarily but the backend ignores them.

Sass-boilerplate-frontend-v1/src/actions/navigation-links.js (~30 LOC)

  • Purpose: Next.js Server Action for revalidating navigation link cache tags.
  • Exports: revalidateNavigationLinks() -- calls revalidateTag("navigation-links")
  • Used By: Admin menu builder after creating/updating/deleting navigation items

Sass-boilerplate-frontend-v1/src/actions/revalidate.js (~20 LOC)

  • Purpose: Generic revalidation server action.
  • Exports: revalidatePath(path) -- calls Next.js revalidatePath()
  • Used By: Admin actions that modify public content

Sass-boilerplate-frontend-v1/src/interceptors/axiosInstance.js (~80 LOC)

  • Purpose: Authenticated axios instance with JWT token from cookie, 401 interceptor for redirect.
  • Note: Used by contacts API for form submission, but the backend /contacts POST route requires no auth. Auth headers are sent but ignored.

Sass-boilerplate-frontend-v1/src/middleware.js (23 LOC)

  • Purpose: Next.js middleware for route protection. Only guards /dashboard/* and redirects authenticated users from /login, /register.
  • Public Route Impact: NONE. All (main-layout) routes pass through untouched.
  • Matcher: ["/login", "/register", "/dashboard/:path*"]

Frontend Layout Components (7 files, ~550 LOC)

Sass-boilerplate-frontend-v1/src/components/layout/header-wrapper.jsx (9 LOC)

  • Purpose: Async Server Component. Fetches navbar links + site settings, passes to Header.
  • Data: getNavigationLinks("navbar"), getSettingByGroup("site")

Sass-boilerplate-frontend-v1/src/components/layout/footer-wrapper.jsx (16 LOC)

  • Purpose: Async Server Component. Fetches footer + bottombar links + site settings, passes to Footer.
  • Data: getNavigationLinks("footer"), getNavigationLinks("bottombar"), getSettingByGroup("site")

Sass-boilerplate-frontend-v1/src/components/layout/header.jsx (178 LOC)

  • Purpose: Client Component. Responsive header with logo, navigation links, theme toggle, auth buttons.
  • Props: navLinks, settings
  • State: isMenuOpen (mobile menu toggle), isScrolled (scroll-based header styling)
  • Links: Logo links to /, nav items to their configured URLs, "Log in" to /login, "Get Started" button (dead -- no href)

Sass-boilerplate-frontend-v1/src/components/layout/footer.jsx (209 LOC)

  • Purpose: Server Component. Multi-column footer with logo, description, navigation sections, social links, bottom bar.
  • Props: footerLinks, bottomLinks, settings

Sass-boilerplate-frontend-v1/src/components/layout/header-skeleton.jsx (29 LOC)

  • Purpose: Loading skeleton for header during Suspense.
  • Issue: Broken CSS class "size-9 -full" (L8)

Sass-boilerplate-frontend-v1/src/components/layout/footer-skeleton.jsx (44 LOC)

  • Purpose: Loading skeleton for footer during Suspense.

Sass-boilerplate-frontend-v1/src/components/layout/footer-credit.jsx (38 LOC)

  • Purpose: Client Component. Shows copyright year, site name, "Hand-crafted" text.
  • Data: Reads site settings from Redux via useSelector
  • Default Link: https://algorizetech.com

DB Entity Files (12 files, ~1,500 LOC)

saas-boilerplate/src/entity/Blog.ts (145 LOC)

  • Table: blogs
  • Columns: id, title, slug (unique), content (text), excerpt (text, nullable), imageUrl, imageAlt, categoryId (ORPHAN -- commented-out relation), authorName (REDUNDANT -- also via author relation), status (draft/published/archived, default "draft"), publishedAt, createdAt, updatedAt, authorId, isDeleted (boolean -- REDUNDANT with DeleteDateColumn), deletedAt, metaTitle, metaDescription, metaKeywords (text, nullable), isFeatured (default false), viewCount (default 0)
  • Relations:
  • ManyToOne -> User (author, via authorId)
  • ManyToMany -> BlogCategory (via join table blog_categories_blog_category)
  • ManyToMany -> BlogTags (via join table blog_tags_blog_tag)
  • OneToMany -> BlogView (view tracking)
  • Soft Delete: Yes (@DeleteDateColumn)
  • Issues: Redundant isDeleted boolean alongside @DeleteDateColumn, orphan categoryId column (old ManyToOne relic), redundant authorName string alongside author relation, status stored as plain varchar not enum

saas-boilerplate/src/entity/BlogCategory.ts (~60 LOC)

  • Table: blog_category
  • Columns: id, name, slug (unique), description (nullable), isFeatured (default false), imageUrl (nullable), createdAt, updatedAt, deletedAt
  • Relations: ManyToMany -> Blog
  • Soft Delete: Yes

saas-boilerplate/src/entity/BlogTags.ts (~50 LOC)

  • Table: blog_tags
  • Columns: id, name, slug (unique), createdAt, updatedAt
  • Relations: ManyToMany -> Blog
  • Soft Delete: No
  • Issue: Entity named BlogTags (plural) but represents a single tag. Convention should be BlogTag.

saas-boilerplate/src/entity/BlogView.ts (~40 LOC)

  • Table: blog_views
  • Columns: id, blogId, viewCount (default 0), lastViewedAt, createdAt, updatedAt
  • Relations: ManyToOne -> Blog
  • Purpose: Tracks view counts per blog. Incremented on every public detail page visit (H3).
  • Issue: No IP/session deduplication mechanism at the entity level.

saas-boilerplate/src/entity/GenericPage.ts (~80 LOC)

  • Table: generic_pages
  • Columns: id, title, slug (unique), description (text -- stores HTML), status (draft/published), imageUrl (nullable), metaTitle, metaDescription, metaKeywords (text), createdAt, updatedAt, deletedAt
  • Soft Delete: Yes
  • Used By: Dynamic page routes (/[slug])

saas-boilerplate/src/entity/Setting.ts (~40 LOC)

  • Table: settings
  • Columns: id, key (unique), value (text, nullable), group (nullable), createdAt, updatedAt
  • Purpose: Key-value store for site settings. Groups: "site", "contact", "admin", etc.
  • Used By: Settings API, metadata generation, header/footer rendering

saas-boilerplate/src/entity/AppConfig.ts (~35 LOC)

  • Table: app_configs
  • Columns: id, key (unique), value (text), description (nullable), createdAt, updatedAt
  • Purpose: Application-level configuration. Separate from user-facing settings.

saas-boilerplate/src/entity/ContactMessage.ts (~45 LOC)

  • Table: contact_messages
  • Columns: id, name, email, details (text), isRead (default false), createdAt, updatedAt, deletedAt
  • Soft Delete: Yes
  • Used By: Contact form submission + admin contact management

saas-boilerplate/src/entity/Menu.ts (~50 LOC)

  • Table: menus
  • Columns: id, name, type (navbar/footer/bottombar), description (nullable), isActive (default true), createdAt, updatedAt, deletedAt
  • Relations: OneToMany -> MenuItem
  • Soft Delete: Yes

saas-boilerplate/src/entity/MenuItem.ts (~70 LOC)

  • Table: menu_items
  • Columns: id, label, url (nullable), target (_self/_blank), icon (nullable), order (default 0), parentId (nullable -- self-referencing tree), menuId, isActive (default true), createdAt, updatedAt, deletedAt
  • Relations: ManyToOne -> Menu, ManyToOne -> MenuItem (parent), OneToMany -> MenuItem (children)
  • Soft Delete: Yes
  • Tree Structure: Self-referencing parentId for nested menus

saas-boilerplate/src/entity/User.ts (~130 LOC)

  • Table: users
  • Relevant Fields: id, name, email, profileImage -- used in blog author display
  • Relations: OneToMany -> Blog (author relation)

saas-boilerplate/src/db/connection.ts (~30 LOC)

  • Purpose: TypeORM data source configuration. PostgreSQL connection using env vars.

ER Diagram (Public Site Entities)

                    ┌──────────────┐
                    │     User     │
                    │──────────────│
                    │ id (PK)      │
                    │ name         │
                    │ email        │
                    │ profileImage │
                    └──────┬───────┘
                           │ 1
                           │ N
                    ┌──────┴───────┐
                    │     Blog     │
                    │──────────────│
                    │ id (PK)      │
                    │ title        │
                    │ slug (UQ)    │
                    │ content      │
                    │ excerpt      │
                    │ imageUrl     │
                    │ authorId(FK) │
                    │ authorName   │ ← REDUNDANT
                    │ categoryId   │ ← ORPHAN
                    │ status       │
                    │ isFeatured   │
                    │ viewCount    │
                    │ isDeleted    │ ← REDUNDANT
                    │ deletedAt    │
                    │ metaTitle    │
                    │ metaDesc     │
                    │ metaKeywords │
                    └──┬───┬───┬───┘
                       │   │   │
              N:M──────┘   │   └──────N:M
              │            │ 1         │
    ┌─────────┴──────┐    │    ┌──────┴────────┐
    │  BlogCategory  │    │    │   BlogTags    │
    │────────────────│    │    │───────────────│
    │ id (PK)        │    │    │ id (PK)       │
    │ name           │    │    │ name          │
    │ slug (UQ)      │    │    │ slug (UQ)     │
    │ isFeatured     │    │    └───────────────┘
    │ imageUrl       │    │ N
    └────────────────┘    │
                   ┌──────┴───────┐
                   │   BlogView   │
                   │──────────────│
                   │ id (PK)      │
                   │ blogId (FK)  │
                   │ viewCount    │
                   │ lastViewedAt │
                   └──────────────┘

    ┌──────────────┐     ┌────────────────┐
    │ GenericPage  │     │ ContactMessage │
    │──────────────│     │────────────────│
    │ id (PK)      │     │ id (PK)        │
    │ title        │     │ name           │
    │ slug (UQ)    │     │ email          │
    │ description  │     │ details        │
    │ status       │     │ isRead         │
    │ metaTitle    │     │ deletedAt      │
    │ metaDesc     │     └────────────────┘
    │ metaKeywords │
    │ deletedAt    │
    └──────────────┘

    ┌──────────────┐     ┌──────────────┐
    │    Menu      │ 1:N │   MenuItem   │
    │──────────────│─────│──────────────│
    │ id (PK)      │     │ id (PK)      │
    │ name         │     │ label        │
    │ type         │     │ url          │
    │ isActive     │     │ target       │
    │ deletedAt    │     │ order        │
    └──────────────┘     │ parentId(FK) │─┐ self-ref
                         │ menuId (FK)  │ │ (tree)
                         │ isActive     │←┘
                         │ deletedAt    │
                         └──────────────┘

    ┌──────────────┐     ┌──────────────┐
    │   Setting    │     │  AppConfig   │
    │──────────────│     │──────────────│
    │ id (PK)      │     │ id (PK)      │
    │ key (UQ)     │     │ key (UQ)     │
    │ value        │     │ value        │
    │ group        │     │ description  │
    └──────────────┘     └──────────────┘

Data Flow

Flow 1: Landing Page Load (/)

Browser GET /
  → Next.js matches (main-layout)/page.js
  → (main-layout)/layout.js renders:
    ├── HeaderWrapper (Server Component, async)
    │   ├── getNavigationLinks("navbar") → unstable_cache → fetch GET /public/menus?type=navbar
    │   └── getSettingByGroup("site") → unstable_cache → fetch GET /settings/site/prefix
    ├── <main>
    │   └── page.js (Server Component)
    │       └── 7 static sections (no API calls, all from constants)
    └── FooterWrapper (Server Component, async)
        ├── getNavigationLinks("footer") → unstable_cache → fetch GET /public/menus?type=footer
        ├── getNavigationLinks("bottombar") → unstable_cache → fetch GET /public/menus?type=bottombar
        └── getSettingByGroup("site") → unstable_cache → fetch GET /settings/site/prefix

API calls per page load: 5 (all cached via unstable_cache, but cache keys are broken so wrong data may be served)

Flow 2: Blog Listing (/blogs?page=2)

Browser GET /blogs?page=2
  → Next.js matches (main-layout)/blogs/page.js
  → Layout: same 5 API calls as Flow 1
  → Page body:
    └── getBlogs({ page: 2, limit: 9 })
        → unstable_cache(key: ["blogs"])  ← BUG: returns page 1 cached data
        → fetch GET /public/blogs?limit=9&page=2
        → Backend: PublicService.getAllBlogs(query, baseUrl)
          → TypeORM QueryBuilder
            → SELECT blogs.*, user.*, blogCategory.*, blogView.*
            → FROM blogs
            → LEFT JOIN users ON blogs.authorId = users.id
            → LEFT JOIN blog_categories_blog_category ON ...
            → LEFT JOIN blog_views ON blogs.id = blogView.blogId
            → ORDER BY blogs.createdAt DESC
            → LIMIT 9 OFFSET 9
          → transformImageUrl(blog.imageUrl, baseUrl)
        → Response: { data: { blogs: [...], meta: { total, page, limit } } }
  → Render: BlogCard[] + PaginationServer

Flow 3: Blog Detail (/blogs/my-post)

Browser GET /blogs/my-post
  → Next.js matches (main-layout)/blogs/[slug]/page.js
  → generateMetadata({ params: { slug: "my-post" } })
    └── getBlogDetails("my-post")  ← API call #1
  → Page body:
    └── getBlogDetails("my-post")  ← API call #2 (duplicate)
        → unstable_cache(key: ["blog-details"])  ← BUG: returns first-cached blog
        → fetch GET /public/blogs/my-post
        → Backend: PublicService.getBlogByIdOrSlug("my-post", baseUrl)
          → Blog findOne with relations (author, categories, tags)
          → BlogView increment (no dedup) ← ISSUE H3
          → transformImageUrl
        → Response: { data: { blog object } }
  → Render: BlogPageFrontend
    └── dangerouslySetInnerHTML={{ __html: blog.content }}  ← XSS RISK C6

Flow 4: Contact Form Submission

User fills form on /contact
  → ContactPage (client component) validates fields
  → useSubmitContactForm() mutation
    → POST /api/v1/contacts (via authenticated axios, but backend requires no auth)
    → Body: { name: "First Last", email: "...", details: "..." }
    → Backend: ContactController.create
      → ContactService.create
        → INSERT INTO contact_messages (name, email, details, isRead)
          VALUES (?, ?, ?, false)
    → Success: toast message + form reset
    → Error: toast error message

Flow 5: Dynamic Page (/about)

Browser GET /about
  → Next.js checks static routes: /blogs (no), /contact (no), /author (no) ...
  → Falls through to (main-layout)/[slug]/page.js
  → generateMetadata({ params: { slug: "about" } })
    └── getDynamicPage("about")  ← API call #1
  → Page body:
    └── getDynamicPage("about")  ← API call #2 (duplicate)
        → unstable_cache(key: ["dynamic-page"])  ← BUG: returns first-cached page
        → fetch GET /pages/about
        → Backend: GenericPageService.getBySlug("about")
          → SELECT * FROM generic_pages WHERE slug = 'about' AND deletedAt IS NULL
        → Response: { data: { page object } }
  → Render: DynamicPage
    └── dangerouslySetInnerHTML={{ __html: data?.description }}  ← XSS RISK C7

Integration Points

APIs Consumed (Frontend → Backend)

Endpoint Method Auth Used By Purpose
/api/v1/public/blogs GET None blogs/page.js, author/[slug], category/[slug], blog-page-frontend Blog listing with filters
/api/v1/public/blogs/:slug GET None blogs/[slug]/page.js Single blog detail
/api/v1/public/blogs/categories GET None (not used by current frontend) Categories with counts
/api/v1/public/blogs/top-blogs GET None (not used by current frontend) Top blogs by views
/api/v1/public/blogs/home-page-blog GET None (not used by current frontend) Homepage featured blog
/api/v1/public/blogs/category/:slug GET None (not used by current frontend) Blogs by category
/api/v1/public/menus?type= GET None HeaderWrapper, FooterWrapper Navigation links
/api/v1/public/menus/list GET None (not used by current frontend) All menus
/api/v1/pages/:slug GET None [slug]/page.js Dynamic page content
/api/v1/settings/:group/prefix GET None Root layout, contact page Site settings
/api/v1/contacts POST None* Contact form Form submission

*Contact POST uses authenticated axios instance but the backend route has no auth middleware -- headers sent but ignored.

Unused Backend Endpoints: 4 out of 11 public endpoints are not consumed by any frontend page: categories, top-blogs, home-page-blog, category/:slug (category page uses the main /blogs?category= instead), menus/list. These represent dead API surface area or planned future features.

Database Access Patterns

Entity Operation Trigger Indexes Used
Blog SELECT with JOINs Blog listing, detail slug (unique), authorId
BlogCategory SELECT via JOIN Blog listing, category page slug (unique)
BlogTags SELECT via JOIN Blog detail slug (unique)
BlogView SELECT + UPDATE Blog detail blogId
User SELECT via JOIN Blog author info id (PK)
GenericPage SELECT by slug Dynamic pages slug (unique)
Setting SELECT by group prefix Layout, contact page key (unique)
ContactMessage INSERT Contact form (none needed)
Menu SELECT by type Navigation (no index on type)
MenuItem SELECT by menuId + tree Navigation menuId, parentId

Missing Indexes: - Menu.type -- queried on every page load but no index exists - Blog.status -- should be indexed for filtering published/draft - BlogView.blogId -- frequently joined but index not confirmed


Dependency Graph

Entry Points (Next.js Routes):
  page.js (/)
  blogs/page.js (/blogs)
  blogs/[slug]/page.js (/blogs/:slug)
  author/[slug]/page.js (/author/:slug)
  category/[slug]/page.js (/category/:slug)
  contact/page.js (/contact)
  [slug]/page.js (/:slug)
    ├── api/public/blogs.js ──────────→ Backend: GET /public/blogs, GET /public/blogs/:slug
    ├── api/public/settings.js ───────→ Backend: GET /settings/:group/prefix
    ├── api/public/dynamic-page.js ───→ Backend: GET /pages/:slug
    ├── api/public/navigation-links.js → Backend: GET /public/menus?type=
    └── api/contacts/index.js ────────→ Backend: POST /contacts
    ├── components/pages/landing-page/ (7 sections, all static)
    │   └── constants/landing-page-data.js (data source)
    ├── components/pages/blogs/ (blog-card, blog-page-frontend)
    ├── components/pages/contact-frontend/ (contact-page, contact-skeleton)
    ├── components/pages/dynamic-page/ (dynamic-page)
    ├── components/custom/ (breadcrumb2, pagination, button)
    ├── components/ui/ (badge, card, tabs, accordion, tooltip, loading, skeleton, button)
    └── components/layout/ (header-wrapper, footer-wrapper, header, footer, skeletons, footer-credit)

Circular Dependencies

None detected.


Testing Analysis

Test Coverage Summary

  • Statements: 0%
  • Branches: 0%
  • Functions: 0%
  • Lines: 0%

No test files exist for any public site code -- not for backend public module, not for frontend pages, not for API hooks, not for components. Zero automated test coverage.

Testing Gaps (Critical)

  1. unstable_cache key correctness -- no test verifies cache key uniqueness per parameters
  2. Blog content sanitization -- no test for XSS prevention in dangerouslySetInnerHTML
  3. Public API response shapes -- no contract tests between frontend expectations and backend responses
  4. SEO metadata generation -- no test verifies correct OpenGraph/Twitter tags
  5. Contact form validation -- no test for form field validation logic
  6. 404 handling -- no test verifies correct HTTP status codes
  7. View count manipulation -- no test for rate limiting
  8. Navigation link type differentiation -- no test for correct navbar vs footer separation

Similar Features Elsewhere

  • Email Template visual builder uses dangerouslySetInnerHTML in the same unsafe way (documented in email template deep-dive C1)
  • Blog admin CRUD shares service layer with public blog -- changes to BlogService affect both admin and public
  • Menu Builder admin shares entity/service with public navigation
  • Dashboard analytics consumes BlogView counts generated by public page visits

Reusable Utilities Available

  • transformImageUrl() (shared/normalizeImageSrc.ts) -- already used by blog service for image URL normalization
  • generateUniqueSlug() (shared/generateUniqueSlug.ts) -- slug generation for blogs and pages
  • catchAsync() (shared/catchAsync.ts) -- already used by all controllers
  • sendResponse() (shared/sendResponse.ts) -- standardized API response format

Patterns to Follow

  • Server Component data fetching: Reference HeaderWrapper/FooterWrapper for async Server Component pattern with Suspense
  • Pagination: Reference PaginationServer for URL-based SSR pagination
  • Contact form: Reference ContactPage for client-side form with validation and mutation

Modification Guidance

To Fix Critical Cache Issues (C1-C5)

  1. In each file under src/api/public/, modify the unstable_cache call to include parameters in the key array:
    // BEFORE (broken):
    export const getBlogs = unstable_cache(async (params = {}) => { ... }, ["blogs"], { ... });
    
    // AFTER (fixed):
    export const getBlogs = unstable_cache(async (params = {}) => { ... }, ["blogs", JSON.stringify(params)], { ... });
    
  2. Apply same fix to getBlogDetails (add slug), getDynamicPage (add slug), getSettingByGroup (add group), getNavigationLinks (add type).
  3. Estimated effort: 30 minutes. 5 files, 1-line change each.

To Fix XSS Issues (C6-C7)

  1. Install DOMPurify: npm install dompurify (+ @types/dompurify for TypeScript)
  2. In blog-page-frontend.jsx and dynamic-page.jsx, sanitize before rendering:
    import DOMPurify from "dompurify";
    // ...
    dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(blog.content) }}
    
  3. Estimated effort: 15 minutes. 2 files + 1 dependency.

To Fix Dead CTA Buttons (H1)

  1. Decide on button destinations (e.g., /register for "Start Free Trial", /contact for "Book a Demo")
  2. In hero-section.jsx and cta-section.jsx, wrap buttons in <Link href="../..."> or add onClick with router.push()
  3. In pricing-section.jsx, link pricing CTAs to the registration page or Stripe checkout
  4. Estimated effort: 30 minutes.

To Add Missing SEO Metadata (H2, M2-M4)

  1. Add generateMetadata or metadata exports to: page.js (homepage), author/[slug]/page.js, category/[slug]/page.js, contact/page.js
  2. For homepage, use fetched site settings or hardcode defaults
  3. Estimated effort: 45 minutes. 4 files.

Testing Checklist for Changes

  • Verify unstable_cache returns different data for different parameters
  • Verify blog content is sanitized before rendering
  • Verify CTA buttons navigate to correct destinations
  • Verify SEO metadata appears in page source for all public routes
  • Verify 404 pages return HTTP 404 status (not 200)
  • Verify navigation links are correct (navbar shows navbar items, footer shows footer items)
  • Verify contact form submission creates a ContactMessage record
  • Verify dynamic pages show correct content for each slug
  • Verify blog listing pagination shows correct page data
  • Verify author and category pages filter correctly
  • Test with API server down -- verify graceful error handling
  • Run accessibility audit (axe-core or Lighthouse) on landing page

Contributor Checklist

Risks & Gotchas

  1. unstable_cache is a Next.js experimental API -- behavior may change across Next.js versions. Consider migrating to fetch with next.revalidate option or use cache directive (Next.js 15+).
  2. All public API functions return null on error with no error propagation. Callers must handle null gracefully.
  3. dangerouslySetInnerHTML is used in 2 locations -- any new rich-content rendering MUST sanitize.
  4. Landing page content is 100% static -- no CMS integration. Content changes require code deploys.
  5. Author pages use string-based name matching -- highly fragile for non-English names or names with hyphens.
  6. Blog view counts are unprotected -- do not use for any business-critical metrics without adding deduplication.

Pre-Change Verification Steps

  1. Check unstable_cache key arrays include all varying parameters
  2. Verify any HTML rendering uses sanitization
  3. Confirm new routes don't conflict with [slug] catch-all
  4. Test with expired/missing cache to ensure fresh data is fetched correctly
  5. Verify SEO metadata in page source (View Source, not DevTools)

Suggested Tests Before PR

  1. Cache key uniqueness: fetch page 1 and page 2 of blogs, verify different content
  2. XSS: create blog with <script>alert(1)</script> content, verify it does not execute
  3. Navigation: verify header shows navbar links, footer shows footer links (not swapped)
  4. 404: visit /blogs/nonexistent-slug, verify HTTP 404 status code
  5. Contact: submit form, verify record in database
  6. SEO: check all routes have correct title/description in page source

Elicitation Analyses

Red Team vs Blue Team (2026-02-25)

  • Document: docs/extra-docs/landing-page-attack-scenarios.md
  • 7 attack chains identified, exploiting cross-module vulnerability combinations
  • Most dangerous: Chain 5 "Full Platform Takeover" -- registration to super_admin to mass XSS to data exfiltration in 8 steps
  • P0 fix: Remove user.roleId = payload.role from profile update (15 min, blocks 3 chains)
  • Full hardening roadmap: 16 fixes, ~4h 40m total, critical chains blocked in ~2h 30m
  • Key finding: Dynamic page description has ZERO sanitization (unlike blogs which use sanitize-html), creating a pure stored XSS vector that bypasses all blog content defenses

Expert Panel Review (2026-02-25)

Three domain experts reviewed the complete deep-dive and Phase 1 elicitation findings. Each expert focused on their specialty to identify issues missed by the original 45-issue analysis.


Winston (Architect) -- Findings

#46. FooterWrapper Makes 3 Sequential API Calls Without Parallelization (Medium) - File: Sass-boilerplate-frontend-v1/src/components/layout/footer-wrapper.jsx:6-8 - Code:

const footerLinks = await getNavigationLinks("footer");
const bottomLinks = await getNavigationLinks("bottombar");
const settings = await getSettingByGroup("site");
- Impact: Three sequential await calls in a Server Component. Each call waits for the previous to complete before starting. With a 100ms round-trip per API call, footer data takes 300ms+ minimum. Should use Promise.all(). - Same problem in HeaderWrapper (header-wrapper.jsx:6-7): 2 sequential calls instead of parallel. - Total wasted time per page load: ~200-300ms (3-5 calls could run in parallel; only adds latency of the slowest call instead of sum of all).

#47. GenericPage Endpoint Serves Draft/Archived Content to Public (High) - File: saas-boilerplate/src/app/modules/v1/GenericPage/generic-page.service.ts:122-138 - Code: getPageByIdOrSlug does pageRepo.findOne({ where: { ...whereCondition } }) with NO status filter. - Impact: Unlike the blog listing which defaults to status = "published", the GenericPage /:slug endpoint returns pages in ANY status -- draft, archived, or published. An attacker who discovers or guesses a draft page's slug (e.g., /upcoming-feature) can read unreleased content. The GET /pages/ listing endpoint is also unauthenticated (routes line 10-11 are before the router.use(auth()) on line 12). - Evidence: generic-page.routes.ts:10-12 shows router.get("/", ...) and router.get("/:id", ...) BEFORE the router.use(auth(), contextMiddleware) middleware. Both endpoints are fully public with no authentication. - Severity Justification: Unauthenticated access to all GenericPages (including drafts and the full listing) is an information disclosure vulnerability. The original deep-dive's H4 only noted this for blogs, not for dynamic pages.

#48. GenericPage Listing Endpoint Publicly Exposes ALL Pages With Full Description HTML (High) - File: saas-boilerplate/src/app/modules/v1/GenericPage/generic-page.routes.ts:10 - Code: router.get("/", PageController.getAllPages); -- no auth, no contextMiddleware. - Impact: GET /api/v1/pages/ returns a paginated list of ALL GenericPages with full description (HTML body), image, metaTitle, metaDescription, metaKeywords, status, and timestamps. This is designed as an admin listing endpoint but is completely public. Combined with the orderBy SQL injection vector (from getSortQuery), an attacker can enumerate and exfiltrate all CMS content. - Note: The frontend never calls this endpoint (it only calls GET /pages/:slug), so this is a dormant but exploitable API surface.

#49. No generateStaticParams on Any Dynamic Route -- No SSG for Blog/Page Content (Medium) - Files: All [slug] pages in (main-layout)/ - Impact: None of the dynamic routes (/blogs/[slug], /[slug], /author/[slug], /category/[slug]) export generateStaticParams. This means: - Zero static generation at build time - Every page request is SSR (server-rendered on demand) - No ISR/SSG benefit for blog content that changes infrequently - Higher TTFB for all content pages - Greater server load under traffic - Fix: Export generateStaticParams that fetches all published blog/page slugs at build time.

#50. No robots.txt or sitemap.xml Generation (Medium) - Files: Sass-boilerplate-frontend-v1/src/app/ -- no robots.ts, robots.txt, sitemap.ts, or sitemap.xml - Impact: Search engines receive no crawling guidance and no sitemap. Blog posts, category pages, and dynamic pages are not discoverable by search engines unless they follow internal links. For a public-facing marketing site with a blog, this is a significant SEO gap. - Fix: Add app/robots.ts and app/sitemap.ts using Next.js Metadata API.

#51. unstable_cache With Dynamic process.env.NEXT_PUBLIC_CACHE_TIME Evaluated at Build Time (Medium) - Files: All 4 API modules in src/api/public/ - Code: revalidate: (parseInt(process.env.NEXT_PUBLIC_CACHE_TIME) || 1) * 60 * 60 - Impact: NEXT_PUBLIC_* environment variables are inlined at build time in Next.js. Changing NEXT_PUBLIC_CACHE_TIME after build has NO effect. The developer likely intended this to be configurable at runtime, but the NEXT_PUBLIC_ prefix makes it a build-time constant. Additionally, if the env var is not set during build, the parseInt(undefined) returns NaN, and NaN || 1 gives 1, so the fallback works -- but the runtime-configurability intention is silently broken.

#52. canonicalUrl in Backend Uses Wrong Path Patterns (Low) - File: saas-boilerplate/src/app/modules/v1/Public/public.service.ts:216,227 - Code:

canonicalUrl: `${process.env.CLIENT_URL}/blog/${blog.slug}`,      // line 216
canonicalUrl: `${process.env.CLIENT_URL}/articles/${similarBlog.slug}`, // line 227
- Impact: The backend generates canonical URLs using /blog/ (singular) and /articles/ (completely different) paths. The frontend uses /blogs/ (plural). These canonical URLs are wrong and will cause SEO confusion if ever used. The main blog uses /blog/, related blogs use /articles/ -- three different path conventions for the same resource type.


Sally (UX Designer) -- Findings

#53. Zero ARIA Landmarks on Landing Page Sections (Medium) - Files: All 7 landing page components in src/components/pages/landing-page/ - Impact: None of the landing page sections have role, aria-label, or aria-labelledby attributes. Screen reader users cannot navigate between sections (Features, Pricing, FAQ, Testimonials) using landmark navigation. The <section> elements have id attributes for anchor links (#features, #pricing, #faq, #testimonials) but no accessible names. - WCAG Violation: WCAG 1.3.1 (Info and Relationships) -- sections are visually distinct but not programmatically labeled. - Fix: Add aria-labelledby referencing each section's <h2> (which would need an id).

#54. Mobile Navigation Menu Has No Focus Trap or Escape Key Handler (Medium) - File: Sass-boilerplate-frontend-v1/src/components/custom/mobile-navigation.jsx - Impact: When the mobile hamburger menu opens: - No focus trap: tab key navigates to elements behind the overlay - No Escape key handler to close the menu - No aria-modal or role="dialog" on the overlay - The toggle button (<Button variant="ghost" size="icon"> in header.jsx:151-162) has no aria-expanded attribute (unlike the dropdown chevron which correctly has it) - WCAG Violation: WCAG 2.4.3 (Focus Order), 2.1.1 (Keyboard) - Fix: Add focus trap (e.g., @radix-ui/react-focus-scope), Escape handler, aria-expanded on hamburger button.

#55. Contact Form Submit Button Label Mismatch: "Get Started" for a Contact Form (Medium) - File: Sass-boilerplate-frontend-v1/src/components/pages/contact-frontend/contact-page.jsx:241 - Code: "Get Started >" as the submit button label on a contact form - Impact: Users filling out a contact form with name, email, and message expect a submit label like "Send Message" or "Submit". "Get Started" implies account creation or onboarding. This creates cognitive friction and reduces form conversion confidence. - UX Anti-pattern: Call-to-action text does not match user intent.

#56. Contact Form Has No CSRF Protection (Medium) - File: Sass-boilerplate-frontend-v1/src/api/contacts/index.js:175-190 - Impact: The useSubmitContactForm mutation calls api.post("/contacts", formData) through the authenticated axios interceptor (which adds JWT tokens if present), but the backend's POST /contacts route has no auth middleware (it's the public endpoint). There is: - No CSRF token validation - No rate limiting on the backend endpoint - No CAPTCHA/honeypot for bot protection - Exploitation: Automated scripts can spam the contact form endpoint with unlimited submissions, filling the admin's contact queue with junk and potentially causing DB bloat.

#57. Blog Card Image Crashes If data.author Is Null (Medium) - File: Sass-boilerplate-frontend-v1/src/components/pages/blogs/blog-card.jsx:55 - Code: data.author.avatar (no optional chaining) and data.author.name (line 58, also no optional chaining) - Impact: If a blog's author is deleted (user deletion cascades to null author reference) or the API returns a blog without an author object, data.author.avatar throws TypeError: Cannot read properties of null (reading 'avatar'), crashing the entire blog listing page. The BlogPageFrontend component correctly uses blog?.author?.avatar with optional chaining, but BlogCard does not. - Severity Justification: One deleted author crashes the entire blog listing page for all visitors.

#58. No Loading/Empty State for Blog Detail Page (Low) - File: Sass-boilerplate-frontend-v1/src/app/(main-layout)/blogs/[slug]/page.js:46 - Code: <Suspense fallback={<Loading fullScreen />}> - Impact: The Suspense boundary wraps BlogPageFrontend which is NOT a lazy-loaded component and does NOT itself perform any async operations (it's a regular function component receiving blog as a prop). The Suspense boundary is therefore never triggered -- the Loading fallback is never shown. The actual async operation (getBlogDetails) happens in the parent Server Component, which has no loading state. Users see a blank page during data fetch.

#59. rel="canonical" Misused on Related Blog Links (Low) - File: Sass-boilerplate-frontend-v1/src/components/pages/blogs/blog-page-frontend.jsx:163 - Code: <Link rel="canonical" key={article?.slug} href={...}> - Impact: rel="canonical" on a <Link> (Next.js component) or <a> tag tells search engines "this is the canonical URL for this page." Placed on related article links in a sidebar, it tells search engines every related article is the canonical version of the CURRENT page. This is backwards -- rel="canonical" should only appear in <head> via metadata, not on navigation links. This could confuse search engine crawlers about which URL is authoritative.

#60. Dark Mode: CTA Section Text Uses text-light Class Without Dark Variant (Low) - File: Sass-boilerplate-frontend-v1/src/components/pages/landing-page/cta-section.jsx:9,23 - Code: className="text-light" and className="text-light-4" - Impact: These appear to be custom utility classes (not Tailwind defaults). If text-light renders as a light gray color, it would be invisible against the white background in light mode. The CTA section applies text-light to the heading and text-light-4 to the paragraph, but has no dark-mode variant override. The contact page uses the same pattern (text-light, text-light-4, text-light-5) suggesting this is intentional, but the class names suggest colors designed for dark backgrounds.


Quinn (QA Engineer) -- Findings

#61. Author Page Crashes on data.author.name When No Blogs Found (High) - File: Sass-boilerplate-frontend-v1/src/app/(main-layout)/author/[slug]/page.js:13 - Code: const author = decodeURIComponent(slug)?.split("-").join(" "); - Impact: The page sends the reconstructed author name to the API. If zero blogs match (typo in URL, deleted author), data?.blogs is empty/null. The breadcrumb title falls back to "Author" (line 24), but the bigger issue is that there's no SEO metadata, no structured "author not found" message, and the empty state just says "No blogs found." -- providing no indication that the author doesn't exist vs. simply has no posts. From a QA perspective, this creates a false-positive: any URL like /author/literally-anything returns HTTP 200 with a valid-looking page.

#62. Category Page categories[0] Assumes First Category Is the Queried One (Medium) - File: Sass-boilerplate-frontend-v1/src/app/(main-layout)/category/[slug]/page.js:24 - Code: title={data?.blogs?.length ? data?.blogs[0]?.categories[0]?.name : "Category"} - Impact: A blog may belong to multiple categories. The code takes categories[0] from the first blog in the results. If the queried category is not the first in the blog's category list, the page title shows the wrong category name. For example, querying category "JavaScript" might show "React" in the breadcrumb if a blog is tagged with both and "React" is at index 0.

#63. PaginationServer Uses useSearchParams -- Requires Client Boundary Wrapping (Medium) - File: Sass-boilerplate-frontend-v1/src/components/custom/pagination.jsx:190 - Impact: PaginationServer (despite its name) is a client component using useSearchParams(), useRouter(), and usePathname(). It is rendered inside Server Components (blogs/page.js, author/[slug]/page.js, category/[slug]/page.js). In Next.js 15, useSearchParams() without a Suspense boundary triggers a warning and may cause the entire route to be client-rendered. The blog listing pages don't wrap PaginationServer in Suspense, which means the initial server render may fail or deopt the entire page to CSR.

#64. Pagination Allows Arbitrary Page Numbers Beyond Valid Range (Medium) - File: Sass-boilerplate-frontend-v1/src/app/(main-layout)/blogs/page.js:31 - Code: const currentPage = Number(params?.page) || 1; - Impact: No upper-bound validation. /blogs?page=99999 sends page=99999 to the backend, which runs a query with OFFSET 899991. Even though results will be empty, the query still executes. No lower-bound validation either: /blogs?page=-5 results in Number("-5") = -5 which is truthy, so it sends page=-5 to the backend, producing a negative OFFSET. - Also: Non-numeric values like /blogs?page=abc evaluate to NaN || 1 = 1, which is the only case handled correctly.

#65. Contact Form Sends Via Authenticated Axios Instance to Unauthenticated Endpoint (Medium) - File: Sass-boilerplate-frontend-v1/src/api/contacts/index.js:179 - Code: const res = await api.post("/contacts", formData); - Impact: The api instance is the authenticated axiosInstance with interceptors that: 1. Add Authorization: Bearer {token} header (if logged in) 2. On 401/403 error: delete cookie, dispatch Redux logout, redirect to /login - For an unauthenticated visitor, this works fine. But if a logged-in user visits the contact page and their token expires mid-session, submitting the contact form will trigger the 401 interceptor, delete their auth cookie, and redirect them to /login -- losing their form data. The contact form should use a plain fetch() call instead of the authenticated interceptor.

#66. Backend getBlogByIdOrSlug Accepts Numeric IDs, Exposing Enumeration (Medium) - File: saas-boilerplate/src/app/modules/v1/Public/public.service.ts:169-172 - Code: /^\d+$/.test(identifier.toString()) -- if slug is numeric, queries by id - Impact: An attacker can enumerate all blogs by ID: /api/v1/public/blogs/1, /api/v1/public/blogs/2, etc. This reveals: - Total number of blogs (including unpublished ones that return null) - Whether a given ID exists (different response for null vs. found) - Sequential IDs leak business intelligence (blog creation velocity) - The frontend equivalent: /blogs/1 would trigger getBlogDetails(1) which passes to the backend and resolves by ID. - Combined with H4 (no published-only filter on detail endpoint): Can enumerate AND read draft blogs by ID.

#67. getSettingByGroup Error Log Message Is Copy-Pasted from Navigation (Low) - File: Sass-boilerplate-frontend-v1/src/api/public/settings.js:16,28 - Code: console.log("Failed to fetch navigation links") and console.error("Error fetching navigation links:") - Impact: When settings API fails, the error log says "navigation links" instead of "settings". This was the copy-paste development root cause identified in the 5 Whys analysis. Makes debugging production issues harder because logs point to the wrong module.

#68. FooterCredit Uses Redux Store on Public Pages -- Unnecessary Client-Side Hydration (Low) - File: Sass-boilerplate-frontend-v1/src/components/layout/footer-credit.jsx:9 - Code: const siteSettings = useSelector(getSettingByGroup("site")); - Impact: FooterCredit reads from the Redux store for site settings. But the footer is rendered inside a Suspense boundary in the layout, and FooterWrapper (a Server Component) already fetches site settings and passes them to Footer. The FooterCredit component re-reads the same data from Redux, which requires: 1. Full Redux hydration on public pages 2. The settings to have been previously loaded into Redux (they're only loaded via the admin/dashboard flow) - Result: On public pages, siteSettings is likely undefined because no Redux action populates it during public page visits. The footer credit falls back to hardcoded "Algorize Tech" and "https://algorizetech.com".

#69. Suspense Boundary Around Contact Page Component Is Ineffective (Low) - File: Sass-boilerplate-frontend-v1/src/app/(main-layout)/contact/page.js:9 - Code: <Suspense fallback={<ContactSkeleton />}> - Impact: Same issue as #58. The ContactPage component is a synchronous client component -- it doesn't suspend. The async getSettingByGroup("contact") call happens in the parent Server Component at line 7, outside the Suspense boundary. The ContactSkeleton fallback is never displayed.

#70. No maxLength Validation on Contact Form Frontend Fields (Low) - File: Sass-boilerplate-frontend-v1/src/components/pages/contact-frontend/contact-page.jsx:160-224 - Impact: The frontend validation has minLength checks but no maxLength checks. The backend validates name max 100, email max 255, details max 5000. Without frontend maxLength, a user can type thousands of characters in the name field and only discover the error on submit (backend rejects it). This creates a poor UX for edge cases.


Panel Consensus

New Issues Found: 25 (numbered #46 through #70)

Severity Original (1-45) New (46-70) Updated Total
Critical 7 0 7
High 7 3 (#47, #48, #61) 10
Medium 17 13 (#46, #49, #50, #51, #53, #54, #55, #56, #57, #62, #63, #64, #65, #66) 30
Low 14 9 (#52, #58, #59, #60, #67, #68, #69, #70) 23
Total 45 25 70

TOP 5 Missed Issues by Impact:

  1. #47 (High) -- GenericPage serves draft/archived content to public. The entire dynamic page system (/about, /terms, /privacy) has no status filter. Combined with #48 (public listing), an attacker can enumerate and read ALL pages regardless of publication status. This was somehow missed in the original analysis which only flagged blogs (H4).

  2. #48 (High) -- GenericPage listing endpoint is fully public. GET /api/v1/pages/ returns ALL pages with full HTML content, no auth. This is an accidental data leak of the entire CMS, never called by the frontend but exploitable by any HTTP client.

  3. #57 (Medium) -- Blog card crashes on null author. A single deleted author crashes the entire blog listing page for all visitors. Unlike the blog detail page which uses optional chaining, the blog card component directly accesses data.author.avatar without null checks. This is a site-wide availability issue.

  4. #46 (Medium) -- Footer/Header sequential API calls. Every page load wastes 200-300ms of unnecessary latency by making 3-5 API calls sequentially instead of in parallel. This is the single easiest performance win: change await chains to Promise.all().

  5. #54 (Medium) -- Mobile menu has no focus trap or keyboard support. The mobile navigation is visually functional but keyboard-inaccessible: no focus trap, no Escape handler, no aria-expanded on the toggle. This blocks keyboard-only users and screen reader users from navigating the site on mobile.

Updated Issue Count: 70 total


Elicitation Analyses Summary

5 advanced elicitation methods applied to this deep-dive:

1. Red Team vs Blue Team (2026-02-25)

  • Document: docs/extra-docs/landing-page-attack-scenarios.md
  • 7 attack chains identified, exploiting cross-module vulnerability combinations
  • Most dangerous: Chain 5 "Full Platform Takeover" -- register → self-role-change → inject XSS in dynamic pages → steal all visitor sessions (8 steps)
  • P0 fix: Remove user.roleId = payload.role from profile update (15 min, blocks 3 chains)
  • Full hardening roadmap: 16 fixes, ~4h 40m total, critical chains blocked in ~2h 30m
  • Key finding: Dynamic page description has ZERO backend sanitization (unlike blogs which use sanitize-html)

2. Pre-mortem Analysis (2026-02-25)

  • Document: docs/extra-docs/landing-page-pre-mortem.md
  • 9 failure scenarios ordered by business impact
  • Top 3: "Content Shuffle" (CERTAIN, cache bug C1-C5), "Silent Storefront" (CERTAIN, dead CTAs H1), "XSS Worm" (HIGH, C6-C7 + privilege escalation)
  • Ship-Blockers: C1-C5, C6, C7, H1, H2, H6, H7, M1, M10 (~3 hours total fix time)
  • Verdict: unstable_cache key bug (C1-C5) is the single most important fix -- 30 min prevents 6 of 9 failure scenarios

3. Expert Panel Review (2026-02-25)

  • Findings: Inline above (issues #46-#70)
  • 3 experts: Winston (Architect, 7 issues), Sally (UX Designer, 8 issues), Quinn (QA Engineer, 10 issues)
  • 25 new issues discovered, bringing total from 45 to 70
  • Top missed issues: GenericPage serves drafts publicly (#47), GenericPage listing fully unauthenticated (#48), BlogCard crashes on null author (#57)

4. Architecture Decision Records (2026-02-25)

  • Document: docs/extra-docs/landing-page-adrs.md
  • 5 ADRs covering the key architectural decisions:
  • ADR-1: Migrate from unstable_cache to fetch() with next.revalidate (resolves C1-C5, H7, M8, M17 -- 8 issues, 3-4h)
  • ADR-2: Dual-layer HTML sanitization: backend sanitize-html + frontend <SafeHTML> with DOMPurify (resolves C6, C7 -- 4-5h)
  • ADR-3: Wire CTAs to /register and /contact, unify pricing data source (resolves H1, M9, M13-M15, L14 -- 6 issues, 3-4h)
  • ADR-4: Dynamic SEO on all routes + sitemap.js + robots.js + notFound() (resolves H2, H6, M1-M4 -- 6 issues, 5-6h)
  • ADR-5: Husky + ESLint + Vitest + GitHub Actions CI pipeline (systemic prevention, 8-12h)
  • Total estimated effort: 23-31 hours, directly resolving 22 of 45 original issues plus systemic prevention

5. Five Whys Root Cause Analysis (2026-02-25)

  • Document: docs/extra-docs/landing-page-root-cause-analysis.md
  • 6 patterns analyzed: broken cache (A), unsanitized HTML (B), zero tests (C), missing SEO (D), dead CTAs (E), console.log in production (F)
  • 4 systemic root causes identified:
  • No automated quality gates (no CI, no pre-commit, no lint enforcement) -- enables patterns A, B, C, F
  • Copy-paste development (settings.js has "navigation links" error messages from navigation-links.js) -- enables patterns A, F
  • Visual-demo mindset (project aims to LOOK like SaaS, not BE one) -- enables patterns D, E
  • No security engineering discipline (security assumed to be "the framework's job") -- enables pattern B
  • Fix-one-fix-many: CI pipeline with pre-merge quality gate prevents 4 of 6 patterns from recurring
  • Key insight: "This codebase has no feedback loops. Without them, every fix is a one-time patch that will regress."
  • 3 root cause analysis documents combined: cache copy-paste chain proven by error message forensics, pricing data divergence (\(29/\)79/$199 vs \(19/\)29/\(59/\)99), zero testing infrastructure confirmed (no test runner, no test files, no test command)

Generated by document-project workflow (deep-dive mode) Base Documentation: docs/index.md Scan Date: 2026-02-25 Analysis Mode: Exhaustive Elicitation Methods Applied: 5 (Red Team, Pre-mortem, Expert Panel, ADRs, 5 Whys)