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_cachesecond 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./aboutcontent 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")andgetSettingByGroup("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")
C5. unstable_cache Key Missing Parameters -- getNavigationLinks (CRITICAL)¶
- File:
Sass-boilerplate-frontend-v1/src/api/public/navigation-links.js:6 - Impact: Cache key
["navigation-links"]shared across types.getNavigationLinks("navbar"),getNavigationLinks("footer"), andgetNavigationLinks("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, noonClick, 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">) orui/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
generateMetadataormetadataexport. 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,
BlogViewcount 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
getAllBlogsmethod in PublicService does not filter bystatus = 'published'. Draft and archived blogs may appear in public listings unless the QueryBuilder happens to exclude them (depends on implementation -- needs verification). ThegetBlogByIdOrSlugmethod 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
authorquery 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,
fetchMetaDatareturns the string"SASS Boilerplate"instead of an object.generateMetadatathen accessessettings?.admin?.site_nameon a string, producingundefined. Thetitle.templatebecomes"%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 toresponse.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 usenotFound()fromnext/navigationto return proper 404 status.
M2. No SEO Metadata on Author Pages¶
- File:
author/[slug]/page.js - No
generateMetadataexport.
M3. No SEO Metadata on Category Pages¶
- File:
category/[slug]/page.js - No
generateMetadataexport.
M4. No SEO Metadata on Contact Page¶
- File:
contact/page.js - No
generateMetadataexport.
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
generateMetadataand the page component call the same API function. Whileunstable_cachemay 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
pricingPlansconstant 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.jsfile exists at any level in the(main-layout)route group. IfHeaderWrapperorFooterWrapperthrow 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?.imagebutBlogPageFrontendusesblog?.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.jsxusecustom/button.jsx(default export, ripple effect, nohrefsupport);pricing-section.jsxusesui/button.jsx(named export, shadcn/Radix, supportsasChildfor 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
secondarykey 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 inprimaryandsecondaryvariants have no effect becausefocus:outline-noneis applied in base styles but nofocus:ring-2base is set. Focus rings never render.
M16. Backend baseUrl Construction in Public Controller¶
- File:
saas-boilerplate/src/app/modules/v1/Public/public.controller.ts baseUrlis constructed fromreq.protocol + "://" + req.get("host")and passed to service methods for image URL transformation. Behind a reverse proxy without properX-Forwarded-*headers, this produces incorrect URLs (e.g.,http://instead ofhttps://).
M17. useQuery Imported in Server-Side Module¶
- File:
Sass-boilerplate-frontend-v1/src/api/public/navigation-links.js:2 useQueryfrom@tanstack/react-queryimported 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.jsfiles in(main-layout)/ - Functions named
page(lowercase) instead ofPage(PascalCase React convention).
L2. Breadcrumb Shows Raw Slug on Author Pages¶
- File:
author/[slug]/page.js:27 { label: slug }displaysjohn-doeinstead ofJohn 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]?.namemay show wrong category if blog has multiple categories.
L5. CSS Typo in Contact Page¶
- File:
contact-page.jsx:293 - Class
breaka-allshould bebreak-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"-- missingroundedprefix; should berounded-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
asyncbut performs no await operations.
L11. Animation Variants Defined Inside Component Body¶
- File:
featured-section.jsx containeranditemanimation 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.jsxvscta-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
PublicServiceandMenuItemService. - Exports:
default PublicControllerobject 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:
catchAsyncwrapper passes errors toglobalErrorHandler
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 countgetTopBlogs()-- Top 5 blogs by view countgetBlogByIdOrSlug(id, baseUrl)-- Single blog with author, categories, tags, view count incrementgetHomePageBlogs(baseUrl)-- Featured blogs for homepagegetCategoriesWithBlogCount()-- Categories with blog countscategoriesByFeatured(slug, baseUrl)-- Blogs filtered by category sluggetMenu(type)-- Menu items filtered by type with tree structure- Side Effects: Increments
BlogViewcount 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 +
getBySlugfor public access
saas-boilerplate/src/app/modules/v1/GenericPage/genericPage.routes.ts (~30 LOC)¶
- Purpose: Routes for generic pages.
GET /pages/:slugis publicly accessible.
saas-boilerplate/src/app/modules/v1/GenericPage/genericPage.service.ts (~100 LOC)¶
- Purpose: GenericPage CRUD service.
getBySlugfinds page by slug column.
saas-boilerplate/src/app/modules/v1/Setting/setting.controller.ts (~100 LOC)¶
- Purpose: Settings controller.
getByGroupPrefixendpoint used by public pages.
saas-boilerplate/src/app/modules/v1/Setting/setting.routes.ts (~30 LOC)¶
- Purpose: Settings routes.
GET /settings/:group/prefixis 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 /contactsis 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
generateMetadataexport) -- 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) andFooterWrapper(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:
generateMetadatafetches from/settings/admin/prefixwith 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:
partnersarray (6 items) from constants + 2 static dashboard images - Animation: Framer Motion
animateon 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:
featuresarray (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:
stepsarray (3 items: "01", "02", "03") - Issues: No
idattribute (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:
testimonialsarray (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
pricingPlansconstant; 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/buttonwhile others usecustom/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:
faqsarray (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
idattribute
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
.jsfile (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.ccURLs 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:
secondaryvariant defined twice (M14), focus rings non-functional (M15),setTimeoutleak 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 })viaunstable_cache(BROKEN -- C1) - SEO: Yes -- static
generateMetadatawith title "Blogs", OG tags - Pagination: Server-side via
PaginationServercomponent usingsearchParams.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)viaunstable_cache(BROKEN -- C2), called TWICE (M8) - SEO: Yes -- dynamic
generateMetadatafrom blog data (title, excerpt, image, keywords) - 404: Renders
<NotFound />component (HTTP 200, not 404 -- M1) - XSS:
dangerouslySetInnerHTMLon 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.login 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 /contactswith{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:
dangerouslySetInnerHTMLon page description (C7) - Issues:
console.login 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 byblogs/[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/imagewithtransformImageUrl()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:
PaginationServeris Server Component;PaginationClientis 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_cachewith 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_cachewith 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_cachewith 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_cachewith BROKEN key (C5). Client: React Query withstaleTime/gcTimebased on CACHE_TIME. - Issue:
useQueryimported at module level affecting server bundle (M17)
Sass-boilerplate-frontend-v1/src/api/contacts/index.js (201 LOC)¶
- Exports: 8 hooks. Only
useSubmitContactFormrelevant to public site. - Public Endpoint:
POST /contactswith{name, email, details} - Auth: Uses authenticated axios instance -- but
POST /contactsbackend 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()-- callsrevalidateTag("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.jsrevalidatePath() - 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
/contactsPOST 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 tableblog_categories_blog_category)ManyToMany -> BlogTags(via join tableblog_tags_blog_tag)OneToMany -> BlogView(view tracking)- Soft Delete: Yes (
@DeleteDateColumn) - Issues: Redundant
isDeletedboolean alongside@DeleteDateColumn, orphancategoryIdcolumn (old ManyToOne relic), redundantauthorNamestring alongsideauthorrelation,statusstored 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 beBlogTag.
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)¶
unstable_cachekey correctness -- no test verifies cache key uniqueness per parameters- Blog content sanitization -- no test for XSS prevention in
dangerouslySetInnerHTML - Public API response shapes -- no contract tests between frontend expectations and backend responses
- SEO metadata generation -- no test verifies correct OpenGraph/Twitter tags
- Contact form validation -- no test for form field validation logic
- 404 handling -- no test verifies correct HTTP status codes
- View count manipulation -- no test for rate limiting
- Navigation link type differentiation -- no test for correct navbar vs footer separation
Related Code & Reuse Opportunities¶
Similar Features Elsewhere¶
- Email Template visual builder uses
dangerouslySetInnerHTMLin the same unsafe way (documented in email template deep-dive C1) - Blog admin CRUD shares service layer with public blog -- changes to
BlogServiceaffect both admin and public - Menu Builder admin shares entity/service with public navigation
- Dashboard analytics consumes
BlogViewcounts generated by public page visits
Reusable Utilities Available¶
transformImageUrl()(shared/normalizeImageSrc.ts) -- already used by blog service for image URL normalizationgenerateUniqueSlug()(shared/generateUniqueSlug.ts) -- slug generation for blogs and pagescatchAsync()(shared/catchAsync.ts) -- already used by all controllerssendResponse()(shared/sendResponse.ts) -- standardized API response format
Patterns to Follow¶
- Server Component data fetching: Reference
HeaderWrapper/FooterWrapperfor async Server Component pattern with Suspense - Pagination: Reference
PaginationServerfor URL-based SSR pagination - Contact form: Reference
ContactPagefor client-side form with validation and mutation
Modification Guidance¶
To Fix Critical Cache Issues (C1-C5)¶
- In each file under
src/api/public/, modify theunstable_cachecall to include parameters in the key array: - Apply same fix to
getBlogDetails(add slug),getDynamicPage(add slug),getSettingByGroup(add group),getNavigationLinks(add type). - Estimated effort: 30 minutes. 5 files, 1-line change each.
To Fix XSS Issues (C6-C7)¶
- Install DOMPurify:
npm install dompurify(+@types/dompurifyfor TypeScript) - In
blog-page-frontend.jsxanddynamic-page.jsx, sanitize before rendering: - Estimated effort: 15 minutes. 2 files + 1 dependency.
To Fix Dead CTA Buttons (H1)¶
- Decide on button destinations (e.g.,
/registerfor "Start Free Trial",/contactfor "Book a Demo") - In
hero-section.jsxandcta-section.jsx, wrap buttons in<Link href="../...">or addonClickwithrouter.push() - In
pricing-section.jsx, link pricing CTAs to the registration page or Stripe checkout - Estimated effort: 30 minutes.
To Add Missing SEO Metadata (H2, M2-M4)¶
- Add
generateMetadataormetadataexports to:page.js(homepage),author/[slug]/page.js,category/[slug]/page.js,contact/page.js - For homepage, use fetched site settings or hardcode defaults
- Estimated effort: 45 minutes. 4 files.
Testing Checklist for Changes¶
- Verify
unstable_cachereturns 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¶
unstable_cacheis a Next.js experimental API -- behavior may change across Next.js versions. Consider migrating tofetchwithnext.revalidateoption oruse cachedirective (Next.js 15+).- All public API functions return
nullon error with no error propagation. Callers must handlenullgracefully. dangerouslySetInnerHTMLis used in 2 locations -- any new rich-content rendering MUST sanitize.- Landing page content is 100% static -- no CMS integration. Content changes require code deploys.
- Author pages use string-based name matching -- highly fragile for non-English names or names with hyphens.
- Blog view counts are unprotected -- do not use for any business-critical metrics without adding deduplication.
Pre-Change Verification Steps¶
- Check
unstable_cachekey arrays include all varying parameters - Verify any HTML rendering uses sanitization
- Confirm new routes don't conflict with
[slug]catch-all - Test with expired/missing cache to ensure fresh data is fetched correctly
- Verify SEO metadata in page source (View Source, not DevTools)
Suggested Tests Before PR¶
- Cache key uniqueness: fetch page 1 and page 2 of blogs, verify different content
- XSS: create blog with
<script>alert(1)</script>content, verify it does not execute - Navigation: verify header shows navbar links, footer shows footer links (not swapped)
- 404: visit
/blogs/nonexistent-slug, verify HTTP 404 status code - Contact: submit form, verify record in database
- 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.rolefrom 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
descriptionhas ZERO sanitization (unlike blogs which usesanitize-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");
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
/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:
-
#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). -
#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. -
#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.avatarwithout null checks. This is a site-wide availability issue. -
#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
awaitchains toPromise.all(). -
#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-expandedon 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.rolefrom 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
descriptionhas ZERO backend sanitization (unlike blogs which usesanitize-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_cachekey 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_cachetofetch()withnext.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
/registerand/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)