Skip to content

Blog/CMS System - Deep Dive Documentation

Generated: 2026-03-26 Scope: Full-stack Blog/CMS system — backend modules, entities, public API + frontend dashboard, public pages, API hooks Files Analyzed: 79 (67 originally listed + 12 missed dependencies found by adversarial review) Lines of Code: ~14,978 (~13,250 original + ~1,728 missed files) Workflow Mode: Exhaustive Deep-Dive Known Issues Found: 236 (26 Critical, 38 High, 66 Medium, 50 Low, 18 Dead Code, 20 Missing Features, 8 Accessibility, 10 Observations) Adversarial Review: Completed 2026-03-27 — 70 new issues found, 2 claims corrected, 1 fabrication removed

Overview

The Blog/CMS system is a full-stack content management feature spanning 5 backend modules, 5 entities, a public API surface, and a Next.js 15 frontend with both dashboard CRUD and public-facing SSR pages.

Purpose: Provide a complete blogging platform with categories, tags, SEO, image uploads, draft/publish workflow, soft-delete/trash/restore lifecycle, view tracking, and public SSR rendering with ISR caching.

Key Responsibilities: - Blog post CRUD with rich text (BlockNote), image upload, SEO metadata - Blog categories (hierarchical parent/child) and tags - Generic pages (CMS-style pages: About, Terms, Privacy, etc.) - Slug generation and uniqueness across entities - Public blog listing, detail, author pages, category pages - Public dynamic page rendering via catch-all [slug] route - ISR caching with tag-based revalidation

Integration Points: Auth system (ownership-based permissions), Permission system (hybrid global.blog-* + blog.*), Storage system (image uploads), Menu Builder (pages as menu items), BlockNote editor (rich text), DOMPurify (XSS prevention)


Complete File Inventory

Backend Files (39 files, ~6,847 LOC)

Blog Module (saas-boilerplate/src/app/modules/v1/Blog/)

File LOC Purpose
blog.controller.ts 227 10 HTTP handlers: CRUD, bulk ops, restore, permanent delete
blog.service.ts 651 Business logic with ownership-based permission model
blog.routes.ts 58 Express routes mounted at /api/v1/blogs
blog.validation.ts 89 Zod schemas for create, update, bulk ops
blog.utils.ts 55 Blog-specific slug generation (duplicates shared utility)

BlogCategory Module (saas-boilerplate/src/app/modules/v1/BlogCategory/)

File LOC Purpose
blog_category.controller.ts 156 10 handlers: CRUD + hierarchy management
blog_category.service.ts 542 Hierarchical category logic with parent/child validation
blog_category.routes.ts 53 Routes at /api/v1/blog-categories
blog_category.validation.ts 97 Zod schemas (BUG: parentId outside body)

BlogTag Module (saas-boilerplate/src/app/modules/v1/BlogTag/)

File LOC Purpose
blog_tag.controller.ts 135 9 handlers: CRUD + bulk ops
blog_tag.service.ts 234 Tag business logic
blog_tag.routes.ts 53 Routes at /api/v1/blog-tags
blog_tag.validation.ts 52 Zod schemas

Slug Module (saas-boilerplate/src/app/modules/v1/Slug/)

File LOC Purpose
slug.controller.ts 24 Single endpoint: generate unique slug for any entity
slug.service.ts 58 Maps table names to entity classes
slug.routes.ts 16 Route at /api/v1/slugs
slug.validation.ts 13 Zod schema (no tableName enum restriction)

GenericPage Module (saas-boilerplate/src/app/modules/v1/GenericPage/)

File LOC Purpose
generic-page.controller.ts 212 10 handlers: CRUD + image upload + bulk ops
generic-page.service.ts 342 Page CRUD with analytics on every list call
generic-page.routes.ts 63 Routes at /api/v1/pages
generic-page.validation.ts 45 Zod schemas
File LOC Purpose
public.controller.ts 111 Public blog endpoints (no auth)
public.service.ts 853 Complex queries: home page, trending, featured, top blogs, monthly archive
public.routes.ts 23 Routes at /api/v1/public

Entities

File LOC Purpose
Blog.ts 146 Blog entity with indexes, ManyToMany categories, OneToMany tags
BlogCategory.ts 72 Self-referential hierarchy, ManyToMany with Blog
BlogTags.ts 44 ManyToOne with Blog (each tag belongs to ONE blog — unusual design)
BlogView.ts 39 View count tracking entity
GenericPage.ts 57 CMS pages with SEO metadata

Supporting Files

File LOC Purpose
shared/generateUniqueSlug.ts 47 Shared slug generation using slugify library
helper/saveBufferToFile.ts 38 Image buffer to disk
helper/checkPermissionAndThrow.ts 24 Permission check helper (returns 400 not 403)

Dependencies & Infrastructure (added by adversarial review)

File LOC Purpose
app/builder/QueryBuilder.ts 221 BaseQueryBuilder class — powers ALL blog/category/tag/page queries
app/builder/getQueryBuilder.ts 17 Factory for QueryBuilder instances
app/factories/sanitizer/sanitizer.factory.ts 10 Blog content XSS sanitization via DOMPurify
shared/validatePostTypeAndId.ts 119 References Blog entity for menu item type validation
seed/data/permission.ts 453 All blog/category/tag/page permission seed strings
app/routes/v1/index.ts 165 Master route file mounting blog/category/tag/page/slug/public
app/interfaces/index.d.ts 58 Blog-related TypeScript interfaces

Frontend Files (40 files, ~8,177 LOC)

Dashboard Blog Components (src/components/pages/blogs/)

File LOC Purpose
blogs-page.jsx 943 Blog listing with DataTable, filters, tabs, pagination, bulk actions
blogs-form.jsx 826 Shared create/edit form: BlockNote editor, image upload, SEO, preview
blog-create-page.jsx 26 Thin wrapper: useCreateBlog -> BlogsForm
blog-edit-page.jsx 32 Thin wrapper: useBlogById + useUpdateBlog -> BlogsForm
blog-categories-page.jsx 179 Category CRUD via CustomizeManager
blog-tags-page.jsx 81 Tag CRUD via CustomizeManager (uses LEGACY hook)
blog-card.jsx 91 Public blog card component
blog-page-frontend.jsx 196 Public blog detail: hero, content, sidebar, related posts

Dashboard Page Components (src/components/pages/pages/)

File LOC Purpose
pages-page.jsx 781 Page listing with DataTable, filters, bulk actions
page-form.jsx 541 Shared create/edit form: BlockNote, image, SEO
page-create.jsx 27 Thin wrapper: useCreatePage -> PageForm
page-edit.jsx 31 Thin wrapper: usePageById + useUpdatePage -> PageForm

Dynamic Page (src/components/pages/dynamic-page/)

File LOC Purpose
dynamic-page.jsx 31 Public page renderer: breadcrumb + DOMPurify-sanitized HTML

API Hooks (src/api/)

File LOC Purpose
api/blogs/index.js 394 12 React Query hooks for blog CRUD
api/blogs/blog-categories.js 122 DEAD CODE — legacy useEffect hook, 0 importers
api/blogs/blog-tags.js 60 LEGACY — still imported by blog-tags-page.jsx
api/pages/index.js 236 10 React Query hooks for page CRUD
api/public/blogs.js 57 Server-side: getBlogs(), getBlogDetails()
api/public/dynamic-page.js 25 Server-side: getDynamicPage()

Route Pages

File LOC Route
(dashboard)/blogs/page.js 9 /dashboard/blogs
(dashboard)/blogs/create/page.js 19 /dashboard/blogs/create
(dashboard)/blogs/[slug]/page.js 19 /dashboard/blogs/[slug]
(dashboard)/blogs/categories/page.js 9 /dashboard/blogs/categories
(dashboard)/blogs/tags/page.js 9 /dashboard/blogs/tags
(dashboard)/pages/page.js 9 /dashboard/pages
(dashboard)/pages/create/page.js 9 /dashboard/pages/create
(dashboard)/pages/[slug]/page.js 10 /dashboard/pages/[slug]
(main-layout)/blogs/page.js 79 /blogs (public listing)
(main-layout)/blogs/[slug]/page.js 52 /blogs/[slug] (public detail)
(main-layout)/author/[slug]/page.js 62 /author/[slug]
(main-layout)/category/[slug]/page.js 62 /category/[slug]
(main-layout)/[slug]/page.js 55 /[slug] (dynamic page)

Supporting

File LOC Purpose
actions/revalidate.js 47 Server actions for Next.js cache tag revalidation
hooks/useCheckSlug.js 17 Slug uniqueness check via POST /slugs
ui/sanitized-html-preview.jsx 13 DOMPurify wrapper for HTML rendering

Blog Consumers & Dead Code (added by adversarial review)

File LOC Purpose
components/blog-card.jsx (root) 46 DEAD CODE — duplicate of pages/blogs/blog-card.jsx, zero importers
pages/dashboard/widgets/blogs-stat-card.jsx 149 Dashboard widget: blog statistics
pages/dashboard/widgets/blog-by-author.jsx 158 Dashboard widget: blogs by author
pages/dashboard/recent-blogs-table.jsx 132 Dashboard table: recent blog posts
dashboard/settings/revalidate/revalidate-tags-page.jsx ~200 Imports useBlogs, manages blog cache tag revalidation

Architecture & Design Patterns

Code Organization

The Blog/CMS system follows a module-per-entity pattern on the backend (controller/service/routes/validation) and a page-per-feature pattern on the frontend. The Public module is a separate concern providing unauthenticated read-only endpoints.

Design Patterns

  • Ownership-based access control (Blog): Uses hasPermission("global.blog-*") for admin bypass, filters to own posts otherwise. NOTE: All checkPermissionAndThrow calls are commented out — module-level permissions are not enforced.
  • Service-level permission checks (Category/Tag/Page): Uses checkPermissionAndThrow() actively — properly enforced.
  • Soft-delete lifecycle: All entities support soft-delete → trash → restore → permanent-delete via @DeleteDateColumn.
  • Slug generation: Two implementations coexist — blog.utils.ts (Blog-specific) and shared/generateUniqueSlug.ts (shared, used by Category/Tag/Page/Slug module). Blog's generateUniqueSlug is imported but never used — the slug comes from the frontend.
  • ManyToMany categories: Blog ↔ BlogCategory via blog_categories_pivot junction table.
  • ManyToOne tags (unusual): BlogTag → Blog means each tag instance is tied to ONE blog. Tags are NOT shared across blogs — "SEO" tag for Blog A and "SEO" tag for Blog B are separate rows.
  • ISR caching (frontend): Public pages use next.revalidate + tags on native fetch() for ISR.
  • React Query (dashboard): All dashboard hooks use @tanstack/react-query for client-side state.

Entity Relationships

Blog (1) ←→ (*) BlogTag         [OneToMany, cascade, onDelete: CASCADE]
Blog (*) ←→ (*) BlogCategory    [ManyToMany via blog_categories_pivot, cascade]
Blog (*) ←→ (1) User            [ManyToOne via authorId, onDelete: CASCADE]
BlogViewCount (*) ←→ (1) Blog   [ManyToOne via blogId, onDelete: CASCADE]
  ⚠ Blog entity has NO inverse @OneToMany for views — public.service references non-existent "blog.views" relation

BlogCategory (1) ←→ (*) BlogCategory  [Self-referential parent/children, onDelete: CASCADE]
  ⚠ validateParentId restricts each parent to ONE child (artificial limit)

GenericPage — standalone (no FK relationships)
  ⚠ MenuItem import + OneToMany/ManyToMany imports are dead code (planned but unimplemented)

State Management Strategy

  • Dashboard: React Query for server state, react-hook-form for form state, Redux for auth/userId only
  • Public: Next.js server components with ISR, no client-side state except Suspense boundaries

Error Handling Philosophy

  • Backend: catchAsync wrapper + ApiError throws with httpStatus codes
  • Frontend dashboard: useFormSubmit hook maps backend validation errors to form fields
  • Frontend public: API fetchers silently return null on error — no user feedback, no error boundaries

Data Flow

Blog Creation Flow

Dashboard Form → FormData (multipart) → POST /blogs
  → auth() (global) → contextMiddleware (global) → upload.single("image")
    → contextMiddleware (DUPLICATE) → validateRequest(createBlog)
  → BlogController.createBlog
    → handleFileUpload (processes uploaded file)
    → CreateBlogService.createBlog(payload):
      1. CategoryRepo.find(In(categories))    ← categories fetched first
      2. Slug uniqueness check
      3. BlogTagRepo.find(In(tags))           ← tags fetched second
      4. SanitizerFactory("dompurify").sanitize(content)  ← sanitization third
      5. BlogRepo.create() → BlogRepo.save()
    → saveBufferToFile (raw fs.writeFile — no format conversion)  ← AFTER DB save
  → 201 response → React Query invalidates ["blogs"]
  → blog-create-page.jsx onSuccess calls revalidate('blogs') (ISR cache IS invalidated)

Public Blog Detail Flow

Browser → GET /blogs/[slug] (Next.js server component)
  → generateMetadata() calls getBlogDetails(slug) → fetch /public/blogs/${slug} with ISR tags
  → Page component calls getBlogDetails(slug) (deduplicated by Next.js)
  → Backend returns up to 5 relatedBlogs (shared categories) in sidebar
  → Renders BlogPageFrontend → SanitizedHTMLPreview (DOMPurify.sanitize → dangerouslySetInnerHTML)
  ⚠ Uses blog.imageUrl but API returns "image" — hero ALWAYS shows placeholder
  ⚠ View count NOT incremented (incrementBlogViewCount is dead code — zero callers anywhere)
  ⚠ Returns HTTP 200 even for not-found (renders <NotFound> component, doesn't call notFound())
  ⚠ Category links broken — API returns {id, name} without slug — all links go to /category/undefined

Blog Edit Flow

Dashboard → GET /blogs/${blogId} (by slug, despite param named "blogId")
  → useBlogById transforms categories/tags to {label, value}
  → BlogsForm loads data into react-hook-form
  → BlockNote editor loads HTML via tryParseHTMLToBlocks()
  → User edits → FormData → PATCH /blogs/${blogId}
    → hasPermission("global.blog-edit") for ownership bypass
    → BlogRepo.findOne({id}) ← NO relations loaded ← categories/tags are undefined
    ⚠ Category/tag length comparison crashes if categories/tags omitted from payload
      (Frontend always sends them, but direct API callers will crash)
  → blog-edit-page.jsx onSuccess calls revalidate('blog-${slug}') + revalidate('blogs')

Data Entry Points

  • Blog create/update: FormData via multipart POST/PATCH
  • Public blog read: Native fetch with ISR to /public/blogs
  • Category/Tag CRUD: JSON body via axios
  • Slug generation: POST /slugs with {tableName, slug}

Data Exit Points

  • Dashboard API: JSON responses via axios interceptor (client-side)
  • Public API: JSON via native fetch (server-side ISR)
  • Public rendering: DOMPurify-sanitized HTML via dangerouslySetInnerHTML

API Endpoints (42 total)

Blog Routes (/api/v1/blogs) — 11 endpoints

Method Path Auth Permission Notes
GET / blog.view (filter only, not enforced) sanitizeQuery middleware
GET /:id NO AUTH None ⚠ Leaks drafts to anonymous users
POST / blog.create COMMENTED OUT multipart, double contextMiddleware
PATCH /:id blog.update COMMENTED OUT multipart
PATCH /:id/restore global.blog-restore (ownership)
PATCH /bulk-update-category blog.update (only active check) ⚠ Soft-deletes old category
PATCH /bulk-delete global.blog-delete (ownership)
PATCH /bulk-restore global.blog-restore (ownership)
DELETE /:id global.blog-delete (ownership) Soft delete
DELETE /:id/permanent global.blog-permanent-delete .delete() called twice

BlogCategory Routes (/api/v1/blog-categories) — 10 endpoints

Method Path Auth Permission Notes
GET /all NONE Returns all categories, no perm check
GET / blog-category.view Paginated list
GET /:id blog-category.read
POST / blog-category.create
PATCH /:id blog-category.update
PATCH /:id/restore blog-category.restore
PATCH /bulk-delete blog-category.delete
PATCH /bulk-restore blog-category.restore
DELETE /:id blog-category.delete
DELETE /:id/permanent blog-category.delete

BlogTag Routes (/api/v1/blog-tags) — 9 endpoints

Method Path Auth Permission Notes
GET / blog-tag.view
GET /:id blog-tag.read
POST / blog-tag.create
PATCH /:id blog-tag.update
PATCH /:id/restore blog-tag.restore
PATCH /bulk-delete blog-tag.delete
PATCH /bulk-restore blog-tag.restore
DELETE /:id blog-tag.delete
DELETE /:id/permanent blog-tag.delete ⚠ Safety check bypassed (tag.blogs undefined)

GenericPage Routes (/api/v1/pages) — 10 endpoints

Method Path Auth Permission Notes
GET / PUBLIC None ⚠ Exposes drafts/archived to anonymous
GET /:id PUBLIC None Accepts slug or numeric ID
POST / page.create multipart
PATCH /:id page.update multipart
PATCH /:id/restore page.restore
PATCH /bulk-delete page.delete
PATCH /bulk-restore generic-page.restore NOT SEEDED — always 403
DELETE /:id page.delete
DELETE /:id/permanent page.delete
POST /bulk-permanent-delete page.delete ⚠ Double auth() middleware

Slug Routes (/api/v1/slugs) — 1 endpoint

Method Path Auth Permission Notes
POST / None ⚠ Error reveals all table names

Public Routes (/api/v1/public) — 6 blog endpoints

Method Path Auth Permission Notes
GET /blogs None Paginated, filterable
GET /blogs/:id None By slug, returns related blogs
GET /home-page-blogs None Home page aggregate
GET /top-blogs None Today/week/month by views
GET /blogs/:categoryNameOrSlug/featured None ⚠ References non-existent blog.views relation
GET /blogs/featured-categories None Categories with blog counts

Known Issues (168 deduplicated)

CRITICAL (18)

# Area Description File(s) Impact
C1 Blog Service 9 of 10 checkPermissionAndThrow calls COMMENTED OUT — any authenticated user can CRUD any blog. Only bulkBlogUpdateByCategory (L260) has an active check. blog.service.ts Privilege escalation
C2 Blog Service bulkBlogUpdateByCategory soft-deletes the old category as side-effect (L303: categoryRepo.softDelete(oldCategoryId)) blog.service.ts:303 Data destruction
C3 Blog Service updateBlog loads blog without relations — blog.categories and blog.tags are undefined — length comparison crashes at runtime blog.service.ts:407-453 Runtime crash
C4 Blog Service permanentlyDeleteBlog calls blogRepository.delete(id) twice (L543 + L546) blog.service.ts:543,546 Redundant (harmless)
C5 Public Service categoriesByFeatured references "blog.views" relation — Blog entity has NO views property — runtime crash public.service.ts:342 Endpoint crash
C6 Blog Service getAllBlogs permission OR logic inverted — users with only one of two required permissions are still filtered to own blogs blog.service.ts:108-111 Wrong access control
C7 BlogTag Service permanentlyDeleteBlogTag checks tag.blogs (plural) but entity has blog (singular ManyToOne) — always undefined — safety check bypassed blog_tag.service.ts:210 Tags deleted while linked
C8 BlogCategory Validation parentId is validated OUTSIDE body object — Express reads from req.bodyparentId is never validated blog_category.validation.ts:33-37 Invalid parentId accepted
C9 GenericPage Routes bulkGenericPageRestore checks "generic-page.restore" permission — NOT seeded — always 403 for non-super-admin generic-page.service.ts:287 Feature broken
C10 GenericPage Routes Double auth() middleware on bulk-permanent-delete route generic-page.routes.ts:24 Double token validation
C11 Slug Service Error message reveals ALL available table names — information disclosure slug.service.ts:40 Security leak
C12 Public Routes Blog detail and dynamic page routes return HTTP 200 for not-found — render <NotFound> component instead of calling notFound() blogs/[slug]/page.js, [slug]/page.js SEO poison (404s indexed as 200)
C13 Public Routes Blog hero image always shows default placeholder — API returns image field but frontend reads imageUrl blog-page-frontend.jsx:17 Broken UI
C14 Public Routes Author pages return empty resultsnormalizeText() lowercases name, backend does exact-match on mixed-case DB values author/[slug]/page.js:13 Feature broken
C15 Public Routes No public view count incrementincrementBlogViewCount only in authenticated module — view analytics meaningless Architectural Dead feature
C16 Frontend Pages Status checkbox always submits "published"formData.status ? "published" : "draft" where "draft" is truthy page-form.jsx:177 Cannot save draft pages
C17 Frontend Pages Title field has NO required validation — empty titles can be submitted page-form.jsx:261 Data integrity
C18 GenericPage Service updatePage null-check AFTER property access (L168 uses page?.slug, L171 checks if (!page)) + L169 is a no-op (data.slug = data.slug) generic-page.service.ts:168-171 Potential crash + broken slug update

HIGH (28)

# Area Description
H1 Blog Routes GET /blogs/:id has no auth middleware — defined before router.use(auth()) — leaks draft content to anonymous users
H2 GenericPage Routes GET /pages/ and GET /pages/:id are fully public — expose draft/archived pages to anonymous users
H3 Blog Routes POST /blogs has contextMiddleware applied twice (once via router.use, once inline)
H4 Blog Entity Orphaned categoryId column from old ManyToOne relation + stale IDX_BLOG_CATEGORY_STATUS index
H5 Blog Entity slug has BOTH column-level unique: true AND a partial index WHERE deletedAt IS NULL — column constraint blocks slug reuse after soft-delete
H6 Blog Service bulkBlogUpdateByCategory has no userId param — no ownership check on updated blogs
H7 Blog Service getAllBlogs returns permissions field — leaks user's global permission set to client
H8 BlogCategory Service updateCategory sets category.name = payload.name — on partial update, payload.name is undefined — DB NOT NULL crash
H9 BlogCategory Validation deleteCategoryByIds field named blogIds for category IDs — confusing API contract
H10 BlogTag Validation deleteByIds field named blogIds for tag IDs — same naming mismatch
H11 BlogCategory Service deleteCategory returns HTTP 200 even when category is NOT deleted (linked to blogs) — client thinks delete succeeded
H12 checkPermissionAndThrow Returns HTTP 400 (BAD_REQUEST) instead of 403 (FORBIDDEN) for permission denied — affects entire app
H13 BlogCategory Service getAllCategories has no permission check — any authenticated user enumerates all categories
H14 BlogCategory Service validateParentId restricts each parent to ONE child — artificial limit contradicts @OneToMany
H15 Frontend API Blog delete/restore/bulk mutations never revalidate public SSR cache. Create and edit DO revalidate via page-level onSuccess callbacks (blog-create-page.jsx, blog-edit-page.jsx), but the mutation hooks in api/blogs/index.js do not.
H16 Frontend API Legacy blog-tags.js hook has double base URL bug — api.get(\${NEXT_PUBLIC_API_URL}/blog-tags`)` with axios baseURL already set — requests 404
H17 Frontend API useBlogById has no permission check — enabled solely by !!blogId — may leak blog data
H18 Frontend API blog-categories.js is fully dead code (0 importers, 122 LOC)
H19 Frontend Components getSortIcon called with wrong signature on 3 columns — sort icons always show default, sorting appears broken
H20 Frontend Components useBulkRestoreBlogs() and useBulkPermanentDeleteBlogs() instantiated inline (not at top level) — creates new mutation instances every render
H21 Frontend Components useBlogById strips category slugs — blogs-form.jsx tries cat.slug for preview but it's undefined in edit mode
H22 Public Routes Author pages have zero SEO metadata — no generateMetadata function
H23 Public Routes Category pages have zero SEO metadata — no generateMetadata function
H24 Public Routes No error.js files in (main-layout) routes — unhandled errors show default error page
H25 Public Routes No loading.js files for listing/author/category pages — no loading states
H26 Public Routes THREE conflicting canonical URL patterns: backend getAllBlogs/public uses /blog/, backend getBlogByIdOrSlug/similar blogs uses /articles/, frontend route is /blogs/ — none match, field ignored entirely
H27 Public Routes Blog card shows publishedAt for date but detail page shows createdAt — inconsistent dates
H28 Frontend Pages Preview URL uses /en/${slug} but actual public route has no /en/ prefix — form preview will 404

MEDIUM (52)

# Area Description
M1 Blog Service console.log("blogIds", blogIds) left in production code (L259)
M2 Blog Service deleteFileByPathName called without await — fire-and-forget file deletion
M3 Blog Service Inconsistent canonical URLs: /blog/ vs /articles/ in different methods
M4 Blog Service Monthly analytics N+1 problem — up to 12 separate queries per month
M5 Blog Service blogAnalytics() runs on EVERY getAllBlogs call — aggregate query overhead
M6 Blog Validation slug field missing from Zod schema — slug passed without validation
M7 Blog Validation min(0) allows ID 0 in bulk operations — should be min(1)
M8 Blog Service blog.author listed twice in select array (L214 + L220)
M9 Blog Service getAllBlogs status "all" deletes filter — may cause QueryBuilder issues
M10 Blog Controller createBlog mutates req.body directly
M11 BlogCategory Service description field cannot be cleared — \|\| operator treats empty string as falsy
M12 BlogCategory Service bulkBlogCategoryRestore does NOT check for name/slug conflicts before restoring
M13 BlogTag Service Case-sensitivity inconsistency — toLocaleLowerCase() vs toLowerCase() on same field
M14 BlogTag Service No soft-deleted duplicate check on create (unlike categories which check and restore)
M15 BlogTag Service restoreBlogTag does NOT check name/slug conflicts before restoring
M16 BlogTag Service bulkBlogTagRestore does NOT check conflicts
M17 BlogTag Service IBlogTag interface has id: string but entity uses id: number
M18 BlogTag/Category Seeded permissions *.trash are never checked in code
M19 BlogCategory Validation updateCategory has no slug field — slug never validated on update
M20 BlogCategory Validation bulkBlogCategoryRestore allows min(0) — zero is valid ID
M21 GenericPage Service pageAnalytics() runs on EVERY getAllPages call — aggregate overhead
M22 GenericPage Controller Inconsistent != "local" (loose) vs === "local" (strict) comparisons
M23 GenericPage Routes Bulk permanent delete reuses bulkGenericPageRestore validation schema — semantically misleading
M24 GenericPage Service No slug uniqueness check on updatePage — duplicate slugs possible
M25 Slug Validation No tableName enum restriction — any string accepted
M26 Frontend API keepPreviousData deprecated in React Query v5 — silently ignored (14 hooks across app)
M27 Frontend API Inconsistent response unwrapping — getBlogs/getBlogDetails return data.data, getDynamicPage returns data
M28 Frontend API Inconsistent bulk delete payload shapes — blogIds/pageIds vs ids for different operations
M29 Frontend API usePages has leftover console.log("hasPermission page", hasPermission)
M30 Frontend API useBlogTags in index.js has no status parameter — cannot fetch trashed tags
M31 Frontend API Missing "use client" directive in api/blogs/index.js and api/pages/index.js — fragile
M32 Frontend API useCreatePage does NOT revalidate public cache — new pages invisible until ISR expires
M33 Frontend API Page delete/restore operations lack revalidation — only useUpdatePage revalidates
M34 Frontend API Public dynamic page endpoint uses /pages/${slug} (no /public/ prefix) — inconsistent with blogs
M35 Frontend Components selectedTag initialized as undefined but selectedCategory as null — inconsistent
M36 Frontend Components Tag filter uses slug but category filter uses label for API param
M37 Frontend Components router (useRouter) declared but never used in blogs-form.jsx
M38 Frontend Components CSS typo: stray single quote 'break-words in blogs-form.jsx
M39 Frontend Pages previewOpen state set but never read in JSX (page-form.jsx)
M40 Frontend Pages user from Redux extracted but never used (page-form.jsx)
M41 Frontend Pages Soft delete modal says "cannot be undone" — misleading (recoverable)
M42 Frontend Pages OG images: [data?.image] may contain null/undefined
M43 Frontend Pages Double revalidation on page update — useUpdatePage.onSuccess + PageEdit.onSuccess both call revalidate
M44 Frontend Pages usePageById receives slug but parameter named pageId — semantically incorrect
M45 Public Routes console.log("category blogs", data?.blogs) in production
M46 Public Routes console.log("slug", slug) in production
M47 Public Routes Category name from blogs[0].categories[0].name — may show wrong category for multi-category blogs
M48 Public Routes Raw slug in breadcrumb (shows "john-doe" not "John Doe") for author and category pages
M49 Public Routes Variable page shadows function name in [slug]/page.js
M50 Public Routes rel="canonical" used on <a> tags — semantically incorrect, has no effect
M51 Public Routes Static blog listing metadata — no page number in title for paginated pages
M52 Public Routes NEXT_PUBLIC_CACHE_TIME exposed to client bundle — should be server-only

LOW (40)

# Area Description
L1 Blog Service Export named CreateBlogService — misleading, handles all operations
L2 Blog Service blog.utils.ts generateUniqueSlug imported but never used in service
L3 BlogView Entity Has @DeleteDateColumn but view counts are never soft-deleted
L4 Blog Service Error message says "edit" when deleting (L488-490)
L5 Blog Service ICreateBlogPayload.publishedAt typed as string but used as Date
L6 Public Service tranding typo (L53) — should be trending
L7 Public Service getTopBlogs formatRaw function defined but never called
L8 BlogCategory Routes Unused upload import
L9 BlogCategory Service Unused DeepPartial, getQueryBuilder, BaseQueryBuilder imports
L10 BlogCategory Controller Grammar: "linked with an existing blogs"
L11 BlogTag Service Function name typo getBLogTagList (capital L)
L12 BlogTag Service Variable typo existngTagName (missing 'i')
L13 BlogTag Service permanentlyDeleteBlogTag returns string, discarded by controller
L14 BlogCategory Validation required_error on optional fields — unreachable messages
L15 GenericPage Routes Copy-paste comment says "contact messages" for generic pages (L21)
L16 GenericPage Entity Unused imports: OneToMany, ManyToMany, MenuItem
L17 GenericPage Service Unused import: toSlug
L18 GenericPage Controller Unused import: normalizeImageSrc
L19 GenericPage Service Inconsistent naming: GenericPageService vs PageController vs PageValidation
L20 GenericPage Validation min(0) allows zero in bulk restore — message says "positive number"
L21 Frontend Components robotsIndex/robotsFollow in form but NO UI input — always empty strings
L22 Frontend Components Commented-out Reading Time input field (786-801)
L23 Frontend Components Commented-out preview link (293-307)
L24 Frontend Components Commented-out bulk permanent delete button in blogs-page
L25 Frontend Components Commented-out tab count badges (4 places)
L26 Frontend Components Triple permission check redundancy on create/edit flows (3 layers each)
L27 Frontend Components isActionLoading prop declared but never read (page-form.jsx)
L28 Frontend Components control and trigger from useForm destructured but unused (page-form.jsx)
L29 Frontend Components No client-side file size validation despite "Max 2MB" label (both blog and page forms)
L30 Frontend Components useBlogById has staleTime: 0 — every navigation triggers fresh fetch
L31 Frontend Components Grammar: "delete All this blog" in bulk delete modal
L32 Public Routes Commented-out author description section
L33 Public Routes Commented-out newsletter subscription UI
L34 Public Routes Commented-out read time display
L35 Public Routes Commented-out NoScrollRestoration in layout
L36 Public Routes CSS typo: to gray-50 missing hyphen in breadcrumb2
L37 Public Routes blog-card.jsx L55: Missing optional chaining on data.author.avatar — throws if author null
L38 Public Routes Unnecessary absolute overlay div inside BlogCard Link
L39 Public Routes Suspense wrapping DynamicPage is a no-op — data already resolved server-side
L40 Public Routes FooterCredit inside Footer Suspense — hidden if footer fetch fails

DEAD CODE (18 items)

# Item File LOC
D1 blog-categories.js legacy hook api/blogs/blog-categories.js 122
D2 blog.utils.ts import in service blog.service.ts:10
D3 LessThanOrEqual/MoreThanOrEqual imports blog.service.ts
D4 normalizeImageSrc import in controller generic-page.controller.ts:9
D5 toSlug import in service generic-page.service.ts:8
D6 permanentlyDeleteGenericPagesByIds method generic-page.service.ts:267-283 16
D7 OneToMany/ManyToMany/MenuItem imports GenericPage.ts
D8 upload import in routes blog_category.routes.ts:2
D9 DeepPartial/getQueryBuilder/BaseQueryBuilder imports blog_category.service.ts
D10 handleText no-op function TextEditor
D11 previewOpen state + handlePreview callback page-form.jsx
D12 user from Redux page-form.jsx
D13 control/trigger from useForm page-form.jsx
D14 isActionLoading prop page-form.jsx
D15 Commented-out bulk permanent delete button pages-page.jsx
D16 router (useRouter) unused blogs-form.jsx
D17 robotsIndex/robotsFollow form fields with no UI blogs-form.jsx
D18 incrementBlogViewCount service method (no route) blog.service.ts

MISSING FEATURES (12)

# Description Backend Support
F1 No search bar on public blog listing searchTerm param supported
F2 No tag-based filtering UI tag param supported
F3 No category index page (/category/) N/A
F4 No sorting options (newest, popular) order/orderBy supported
F5 No author profile page (just filtered listing) Partial
F6 No reading time estimate display Commented out
F7 No social share buttons N/A
F8 No "Back to blogs" navigation on detail N/A
F9 No tag display on blog detail ✅ Tags returned in API
F10 No view counter display ✅ View count exists but never incremented
F11 No newsletter subscription Commented out
F12 No generateStaticParams for build-time pre-generation N/A

ACCESSIBILITY (8)

# Description File
A1 Tab filter buttons lack role="tab", aria-selected, tablist wrapper blogs-page.jsx
A2 Icon-only action buttons have no aria-label blogs-page.jsx
A3 Sort column headers are <div onClick> — no role="button", tabIndex, or keyboard handlers blogs-page.jsx
A4 Checkboxes have no associated <label> blogs-page.jsx
A5 Image remove button lacks aria-label blogs-form.jsx
A6 BlogCard has nested <Link> inside parent <Link> — invalid HTML, breaks screen readers blog-card.jsx
A7 Preview modal focus trapping unknown blogs-form.jsx
A8 DataTable semantics unknown — if not <table>, screen readers cannot parse blogs-page.jsx

NEW ISSUES FROM ADVERSARIAL REVIEW (70 issues: 8C, 10H, 26M, 19L, 7 observations)

Found by adversarial review 2026-03-27 — issues the original deep-dive missed

New Critical (8)

# Area Description
NC1 Backend QueryBuilder SQL Injection via orderBygetSortQuery does NOT validate column names. TypeORM orderBy() interpolates identifiers raw into SQL. ?orderBy=id;DROP TABLE blogs-- is injectable. Affects ALL paginated endpoints (blogs, categories, tags, pages).
NC2 Frontend blogs-form Rules of Hooks violation — 4 watch() calls (L257-260) appear AFTER if (loading) return <Loading /> early return (L253). When loading toggles, hooks execute in different order — React crash.
NC3 Blog Service getAllBlogs crashes when blog.author is null — L236 blog.author.avatar without optional chaining. A single deleted author crashes the entire blog list response.
NC4 Blog Service Blogs with no slug crash on createslug is not in createBlogSchema validation. generateUniqueSlug is imported but never called. payload.slug can be undefined, inserting NULL into unique non-nullable column.
NC5 Blog Service Race condition in incrementBlogViewCount — read-then-write not atomic. Concurrent requests cause duplicate records or lost updates. (Moot: function has zero callers.)
NC6 Frontend pages getDynamicPage hits authenticated /pages/${slug} endpoint — no /public/ prefix unlike blogs. Server-side fetch has no auth cookie — if backend enforces auth, all CMS pages break for anonymous visitors.
NC7 Public Routes Category links broken on blog detail — public API getBlogByIdOrSlug returns categories as {id, name} without slug. Frontend blog-page-frontend.jsx L32 links to /category/${category?.slug} — all links render as /category/undefined.
NC8 Blog Service createBlog controller: body authorId can override authenticated user's ID — L16-18: const authorId = req.user?.id || body.authorId then ...body spread can overwrite the correctly-set value.

New High (10)

# Area Description
NH1 Backend No pagination limit capparsePaginationQuery has no maximum. ?limit=999999999 forces unbounded query — DoS via memory exhaustion on every paginated endpoint.
NH2 Blog Service deleteFileByPathName called BEFORE blogRepository.save in updateBlog (L443 vs L469) — old image permanently lost if save fails.
NH3 BlogCategory deleteByCategoryIds and updateBlog use deprecated findByIds — may not respect soft-delete filters in TypeORM 0.3+.
NH4 BlogCategory bulkBlogCategoryRestore skips name/slug conflict checks — single-restore properly checks, bulk restore does not.
NH5 GenericPage updatePage slug uniqueness check absent — L169 is dead self-assignment data.slug = data.slug. Users can set duplicate slugs.
NH6 Frontend blog-page-frontend.jsx hero Image uses fill without sizes — Next.js Image with fill requires sizes for responsive optimization. Largest variant always downloaded, degrades LCP on mobile.
NH7 Frontend handlePreview useCallback has stale dependency array referencing commented-out logic — unnecessary re-creation and misleading.
NH8 Frontend toast.error() passes error object as second argument (page-form.jsx L156) — react-hot-toast treats 2nd arg as options, not message. Error silently swallowed.
NH9 Frontend SanitizedHTMLPreview may crash during SSRDOMPurify.sanitize() requires window/document. During SSR prerendering, this crashes or produces hydration mismatch. Should use isomorphic-dompurify.
NH10 Frontend Blog edit slug check can corrupt existing slugshandleSlugCheck() fires onBlur even in edit mode (unlike title which guards with if (!edit)). Can change an existing slug to a deduplicated variant.

New Medium (26)

# Description
NM1 ILIKE wildcard injection — searchTerm interpolated into %${searchTerm}% without escaping % and _ wildcards. % matches everything.
NM2 formatRaw defined but never called in getTopBlogsweek/month return raw DB column names vs today entity names.
NM3 tranding typo in public.service — validates query.tranding (misspelled) but filters on query.isTrending.
NM4 Blog cascade: true on tags OneToMany — saving Blog can INSERT/UPDATE any BlogTag objects in array. Combined with updateBlog merge, can create phantom tags.
NM5 contextMiddleware applied twice on most Blog routes (global + inline).
NM6 bulkBlogRestore doesn't filter for actually-deleted blogs — loads with withDeleted: true and restores all IDs.
NM7 GenericPage getPageByIdOrSlug returns drafts/archived to unauthenticated users (no status check).
NM8 getTopBlogs returns inconsistent shapes — today returns entities, week/month return raw objects with different property names.
NM9 Blog ownership checks use httpStatus.BAD_REQUEST (400) for ownership failures — separate from checkPermissionAndThrow 400-not-403 issue.
NM10 getLatestCategoryBlogs returns on FIRST category with blogs — for loop exits immediately. Only one category ever returned.
NM11 categoriesByFeatured destructures authorId from query result but it was never selected.
NM12 Blog slug validation allows arbitrary characters — z.string().min(3) with no format/regex.
NM13 updateCategory has no slug format validation — toLowerCase().trim() but no URL-safe character check.
NM14 getLatestCategoryBlogs loads full blogs relation then ignores it — separate query fetches per-category.
NM15 Category page title from first blog's first category — may show wrong category for multi-category blogs.
NM16 blog-categories-page.jsx parent/reassignment search share state — typing in one dropdown changes the other.
NM17 No file size validation on image upload despite "Max 2MB" label (both blog and page forms).
NM18 robotsIndex/robotsFollow are ghost form fields — in state, serialized to API, but no input in JSX. Always empty strings.
NM19 Blog form date field has zero validation — accepts any string or empty.
NM20 useBlogs default status "published" silently filters for dashboard consumers that forget to pass status.
NM21 Blog form authorName in defaultValues but no input — always uses Redux state. Dead form field.
NM22 Both [slug]/page.js and blogs/[slug]/page.js fetch data twice (metadata + component). Next.js fetch dedup may not apply across render phases.
NM23 permanentlyDeleteGenericPagesByIds — dead code, defined but NOT in export object (L267).
NM24 Blog entity GenericPage imports MenuItem but never uses it — dead import.
NM25 Blog/Page controllers save DB record BEFORE writing file to disk — if file write fails, record references non-existent image.
NM26 Variable shadowing: const page = await getDynamicPage(slug) inside function named page in [slug]/page.js.

New Low (19)

# Description
NL1 santizer variable misspelling (missing 'i') in blog.service.ts L382, L456
NL2 blogAnalytics exported but only used internally — unnecessary API surface
NL3 blog.author selected twice in getAllBlogs (L214, L220)
NL4 CSS syntax error — stray single quote 'break-words in blogs-form.jsx L395
NL5 blog-page-frontend.jsx returns empty string "" instead of null for missing categories
NL6 pages-page.jsx mixes number/string types for itemsPerPage (number init, string options)
NL7 Unused router in page-form.jsx and blogs-form.jsx
NL8 Unused trigger from useForm in blogs-form.jsx
NL9 generateMetadata on blogs listing is static — no page number in title for paginated pages
NL10 blog-card.jsx inconsistent optional chaining — L55 data.author.avatar (no ?.) vs L71 data?.author?.name. Crashes if author null.
NL11 blog-page-frontend.jsx uses array index as key for category iteration (L29)
NL12 deleteBlog error message says "edit" instead of "delete" (L489)
NL13 Unused blog.views relation call in categoriesByFeatured (L342) — produces 0 or crashes
NL14 getLatestCategoryBlogs loads full blogs relation then never uses loaded data
NL15 Root-level components/blog-card.jsx is dead code duplicate of pages/blogs/blog-card.jsx (zero importers)
NL16 ICreateBlogPayload.publishedAt typed as string but used as Date
NL17 BlogViewCount entity has @DeleteDateColumn but view counts are never soft-deleted
NL18 Unused previewOpen state and handlePreview callback in page-form.jsx
NL19 Suspense wrapping DynamicPage is a no-op — data already resolved server-side

New Observations (7)

# Description
NO1 Triple-nested PermissionWrapper on blog create/edit (3 identical checks per flow)
NO2 useBulkRestoreBlogs()/useBulkPermanentDeleteBlogs() instantiated inline as arguments — new mutation instance every render
NO3 Legacy blog-tags.js is actively used by tags page — not dead code, live bug via double URL
NO4 No file size validation on either form despite "Max 2MB" labels
NO5 blog-page-frontend.jsx hero Image fill without sizes — perf degradation
NO6 blog-categories-page.jsx parent and reassignment search share state — typing in one changes the other
NO7 incrementBlogViewCount has zero callers — not just "only in authenticated module" but completely dead code

Contributor Checklist

Risks & Gotchas

  • 9 of 10 blog permission checks are commented out — any authenticated user can CRUD any blog. Only bulkBlogUpdateByCategory has an active check. Ownership-based hasPermission provides a partial barrier.
  • BlogTag is ManyToOne, not ManyToMany — tags are NOT shared across blogs. Creating "SEO" tag for two blogs creates two separate rows.
  • Blog entity has dual unique constraint on slug — column-level unique: true prevents reusing slugs from soft-deleted blogs, defeating the partial index.
  • bulkBlogUpdateByCategory destructively soft-deletes the old category — this is almost certainly unintended.
  • Public blog/page GETs expose drafts — no status filter on public GET endpoints.
  • Blog hero image field mismatch — API sends image, frontend reads imageUrl — hero always shows placeholder.
  • Author pages case-sensitivitynormalizeText() lowercases, but backend exact-matches against mixed-case DB values.
  • SQL Injection via orderBygetSortQuery does NOT validate column names. TypeORM interpolates them raw into SQL. Affects ALL paginated endpoints.
  • Rules of Hooks violationblogs-form.jsx has 4 watch() calls after conditional early return — React crash when loading toggles.
  • Category links broken on blog detail — public API returns {id, name} without slug — all category links render as /category/undefined.
  • No pagination limit cap — sending ?limit=999999999 causes unbounded query and memory exhaustion.

Pre-change Verification Steps

  1. Check if checkPermissionAndThrow has been re-enabled before assuming permissions are enforced
  2. Verify tag relationship type (ManyToOne) before adding tag-sharing features
  3. Test slug reuse after soft-delete — dual constraint may block
  4. Verify bulkBlogUpdateByCategory behavior in staging before using in production
  5. Confirm public endpoints filter by status: "published" before deploying SEO fixes
  6. Test ?orderBy= parameter with SQL injection payloads — validate whitelist exists
  7. Verify blogs-form.jsx hook order — all hooks must come before any conditional returns

Suggested Tests Before PR

  • Blog CRUD as non-author user without global permissions (should be blocked but currently isn't)
  • Category bulk update — verify old category is NOT soft-deleted
  • Permanent tag delete with linked blog — verify safety check works (currently bypassed)
  • Public blog detail with draft blog — verify 404 behavior
  • Author page with mixed-case author name — verify results returned
  • Blog edit → update categories → verify no runtime crash from undefined relations
  • Page create with status checkbox → verify draft can be saved
  • SQL injection via ?orderBy=id;DROP TABLE-- on all paginated endpoints
  • Blog list with a deleted/null author — verify no crash on blog.author.avatar
  • Blog create without slug in payload — verify no NOT NULL crash
  • ?limit=999999 on any blog/page list endpoint — verify reasonable cap
  • Category link click on blog detail — verify not /category/undefined

Testing Analysis

Test Coverage Summary

  • Backend: 0% — Zero test files exist for Blog, BlogCategory, BlogTag, GenericPage, Slug, or Public modules
  • Frontend: 0% — Zero test files exist for any blog/page component, hook, or route

Testing Gaps

  • No unit tests for any service method
  • No integration tests for any API endpoint
  • No component tests for any React component
  • No E2E tests for any user flow
  • No permission boundary tests
  • No validation edge case tests
  • DOMPurify sanitization config not tested with adversarial inputs

Similar Features Elsewhere

  • Email Template System: Same pattern of commented-out checkPermissionAndThrow, same permission-seeding gap, same dangerouslySetInnerHTML + DOMPurify pattern
  • Menu Builder: Uses BlogCategory and Blog entities via menu items — postType: "pages" references GenericPage IDs
  • Settings system: Same CRUD pattern with soft-delete lifecycle

Reusable Utilities Available

  • shared/generateUniqueSlug.ts — already used by Category/Tag/Page; Blog's duplicate in blog.utils.ts should be removed
  • SanitizedHTMLPreview — reused correctly across blog detail, page detail, and dashboard previews
  • useFormSubmit — shared form submission hook used by both Blog and Page forms
  • useDeleteAction/useBulkDeleteAction — shared action hooks for all CRUD modals

Patterns to Follow

  • GenericPage permission model as reference for fixing Blog permissions (active checkPermissionAndThrow)
  • useUpdatePage revalidation as reference for adding ISR revalidation to blog mutations

Modification Guidance

To Add New Functionality

  1. Follow the Category/Tag permission pattern (active checkPermissionAndThrow) — do NOT follow Blog's commented-out approach
  2. For new public routes, always call notFound() (not render <NotFound> component) for proper 404 status
  3. Add generateMetadata for SEO on every public route page
  4. Add error.js and loading.js files to all route groups
  5. When adding tags, remember they are ManyToOne (per-blog) — if shared tags are needed, migration to ManyToMany is required

To Fix Critical Issues (Priority Order)

  1. Add orderBy whitelist to getSortQuery — validate column name against allowed fields before interpolation. Blocks SQL injection on ALL paginated endpoints. (15 min)
  2. Move watch() calls before early return in blogs-form.jsx — all hooks must precede conditional returns to satisfy Rules of Hooks. (10 min)
  3. Re-enable checkPermissionAndThrow in blog.service.ts — uncomment 9 commented-out calls (15 min)
  4. Remove categoryRepo.softDelete(oldCategoryId) from bulkBlogUpdateByCategory (5 min)
  5. Add relations to findOne in updateBlog — add relations: ["categories", "tags"] (5 min)
  6. Fix tag.blogstag.blog in permanentlyDeleteBlogTag (5 min)
  7. Add ?. to blog.author.avatar in getAllBlogs L236 — prevents null author crash (2 min)
  8. Add slug to createBlogSchema or call generateUniqueSlug in service — prevent NOT NULL crash (10 min)
  9. Add pagination limit cap in parsePaginationQuery — e.g. Math.min(limit, 100) (5 min)
  10. Move parentId inside body in category create validation (5 min)
  11. Change notFound() calls on public routes — import and call notFound() from next/navigation (10 min)
  12. Fix imageUrlimage in blog-page-frontend.jsx (5 min)
  13. Add category slug to public blog detail response — fix /category/undefined links (5 min)
  14. Add ISR revalidation to blog mutations — call revalidate('blogs') and revalidate('blog-details') (15 min)
  15. Fix status checkbox in page-form.jsx — status === "published" ? "published" : "draft" (5 min)

Total estimated fix time for all critical issues: ~85 minutes

Testing Checklist for Changes

  • SQL injection via ?orderBy=id;DROP TABLE-- on all paginated endpoints
  • Blog CRUD as non-author without global permissions
  • Category bulk update does not soft-delete old category
  • Tag permanent delete with linked blog is blocked
  • Public blog detail returns HTTP 404 for non-existent slug
  • Blog hero image shows actual uploaded image (not placeholder)
  • Author page returns results for mixed-case author names
  • Blog edit preserves existing categories and tags
  • Draft page not accessible via public [slug] route
  • ISR cache invalidated after blog delete/restore/bulk ops (create/edit already work)
  • Page status saves as "draft" when checkbox unchecked
  • Blog list with null/deleted author does not crash
  • Blog create without slug in payload does not crash
  • ?limit=999999 returns a reasonable capped result
  • Category links on blog detail page navigate correctly (not /category/undefined)
  • blogs-form.jsx loading toggle does not crash React (hooks order)

Generated by document-project workflow (deep-dive mode) Base Documentation: docs/index.md Scan Date: 2026-03-26 Adversarial Review: 2026-03-27 — 70 new issues added, 2 claims corrected, 1 fabrication removed Analysis Mode: Exhaustive — 7 parallel sub-agents, 79 files, ~14,978 LOC