Timeline: ~2.5 hours, 1 developer
Exit criteria: No SQL injection. Permission checks active. No data corruption paths. No crash-on-render. No console.log in production.
9 of 10 checkPermissionAndThrow calls COMMENTED OUT in blog service — any authenticated user can CRUD any blog. Only bulkBlogUpdateByCategory (L260) has an active check.
Blog slug not in createBlogSchema, never generated server-side — inserts NULL slug, violates unique constraint, crashes. generateUniqueSlug imported but never called.
Blog/blog.validation.ts + Blog/blog.service.ts
Add slug to validation schema OR add server-side generateUniqueSlug(title) call in service before save
10 min
12
C7
permanentlyDeleteBlogTag checks tag.blogs (plural) but entity relation is blog (singular ManyToOne) — safety check always undefined, allows deleting tags with active blogs.
BlogTag/blog_tag.service.ts:210
Change tag.blogs to tag.blog
5 min
13
C5
categoriesByFeatured references non-existent "blog.views" relation — Blog entity has NO views property — runtime crash.
public.service.ts:342
Remove the join on "blog.views" or fix to correct relation name
Rules of Hooks violation — 4 watch() calls placed AFTER if (loading) return <Loading /> early return (L253). When loading toggles, hooks execute in different order — React crash.
components/pages/blogs/blogs-form.jsx:253-260
Move all watch() calls before any early return statement
10 min Done
15
C12 ✅
Public blog detail returns HTTP 200 with <NotFound> component for non-existent slugs — SEO poison (Google indexes blank pages as 200).
app/(main-layout)/blogs/[slug]/page.js
Import and call notFound() from next/navigation instead of rendering component
10 min Done
16
C13 ✅
Blog hero image always shows default placeholder — API returns image field but frontend reads imageUrl. Field name mismatch. V2 component uses blog?.image correctly. V1 only used for preview where imageUrl is passed.
Add slug field to category select in public blog detail response
5 min
18
C16 ✅
Page status checkbox always submits "published" — formData.status ? "published" : "draft" where "draft" is truthy. Cannot save draft pages.
components/pages/pages/page-form.jsx:177
Fix: status === "published" ? "published" : "draft" or use proper boolean toggle
5 min Done
19
C14
Author pages return empty results — normalizeText() lowercases name but backend does exact-match on mixed-case DB values.
Backend: author query in public.service.ts
Use case-insensitive match: LOWER(authorName) = LOWER(:name) or ILIKE
10 min
20
NC6
getDynamicPage hits authenticated /pages/${slug} endpoint — no /public/ prefix — CMS pages break for anonymous visitors when backend enforces auth.
api/public/dynamic-page.js
Change to /public/pages/${slug} or create dedicated public endpoint on backend
10 min
21
SEO ✅
URL typo in SEO fallback: "ttps://sass-frontend-main.vercel.app" — missing h in https. All metadata canonical/OG URLs broken when env var not set. Fixed in blog detail and category page.
Blog hero image displays actual uploaded image (not placeholder)
Category links on public blog detail navigate to /category/valid-slug (not /category/undefined) — Blocked: backend needs to add slug to category select
CMS page creation can save as "draft" (not forced to "published")
Timeline: ~6.5 hours, 1 developer
North Star: Permission model working, all CRUD operations reliable, public pages functional for real visitors.
Exit criteria: Authenticated user with blog permissions can create/edit/delete/restore blogs, categories, tags, and pages. Public blog listing, detail, category, and author pages render correctly.
Fix getAllBlogs permission OR logic inverted — users with only one of two required permissions are still filtered to own blogs instead of seeing all.
Blog/blog.service.ts:108-111
30 min
S1-2
H3/NM5
Remove duplicate contextMiddleware on Blog routes — router.use(auth(), contextMiddleware) at L18 already applies to all subsequent routes, but individual routes re-add it inline. Double context loading.
Blog/blog.routes.ts:22,26,29,39,48
15 min
S1-3
H8
Fix updateCategory partial update crash — sets name=undefined when name not in payload. category.name = payload.name overwrites with undefined → NOT NULL constraint violation.
Rename blogIds to correct entity names in category and tag validation — deleteCategoryByIds uses blogIds for category IDs, deleteByIds uses blogIds for tag IDs. Misleading API contract.
Fix deleteCategory returning 200 when not deleted — category linked to blogs is not actually deleted but client thinks success. No affected-rows check.
Fix category hierarchy ONE-child-per-parent limit — validateParentId rejects second child with findOne({ where: { parentId } }). Should allow multiple children per parent.
BlogCategory/blog_category.service.ts:182-243
30 min
S1-7
C8
Move parentId inside body in category validation — currently validated outside body object. Express reads from req.body, so parentId passes unvalidated.
BlogCategory/blog_category.validation.ts:33-37
10 min
S1-8
M7
Fix min(0) allowing entity ID 0 everywhere in Zod schemas — .min(0, { message: "must be a positive number" }) allows 0 which is an invalid ID. Change to .min(1).
Fix file deletion before database save — deleteFileByPathName called BEFORE blogRepository.save in updateBlog. If save fails, old image is already gone. Move deletion after successful save.
Add ISR revalidation to blog delete/restore/bulk mutations — create and edit already revalidate via page-level onSuccess, but mutation hooks in api/blogs/index.js do not. All mutation hooks already have await revalidate('blogs') in onSuccess.
Fix legacy blog-tags.js double base URL — constructs ${NEXT_PUBLIC_API_URL}/blog-tags but axios interceptor already prefixes base URL → 404. Legacy file now uses api.get('/blog-tags') correctly. Note: this file is dead code (replaced by React Query hook in api/blogs/index.js).
api/blogs/blog-tags.js
10 min Done
S1-15
H19 ✅
Fix getSortIcon wrong argument signature — called with incorrect params on 3 columns. Wrapper getSortIconForColumn(column) correctly passes (column, orderBy, order) to getSortIcon.
Blog table component (blogs-page.jsx)
15 min Done
S1-16
H20 ✅
Move mutation hooks to top level — useBulkRestoreBlogs() and useBulkPermanentDeleteBlogs() instantiated inline. Extracted to top-level const bulkRestoreBlogs = useBulkRestoreBlogs() and const bulkPermanentDeleteBlogs = useBulkPermanentDeleteBlogs().
components/pages/blogs/blogs-page.jsx
15 min Done
S1-17
H21 ✅
Preserve category slugs in useBlogById transformer — response transformer strips slugs from categories. Transformer already preserves slug: cat.slug at L200.
api/blogs/index.js (useBlogById)
15 min Done
S1-18
NH9 ✅
Fix SSR crash in SanitizedHTMLPreview — DOMPurify.sanitize() requires browser window/document. Already uses isomorphic-dompurify.
components/ui/sanitized-html-preview.jsx
20 min Done
S1-19
NH10 ✅
Fix slug corruption on edit blur — handleSlugCheck() fires onBlur even in edit mode. Added !edit guard to slug onBlur handler.
Add required validation to page title — can submit empty title. Already has required: "Page title is required" with minLength 3 validation.
components/pages/pages/page-form.jsx
5 min Done
S1-21
C18
Fix GenericPage updatePage null-check order — L168 uses page?.slug but L171 checks if (!page). Also L169 is no-op: data.slug = data.slug. Backend fix required.
GenericPage/generic-page.service.ts:168-171
15 min
S1-22
NH8 ✅
Fix toast error swallowing — toast.error() passes Error object as second argument. Fixed in page-form.jsx to use error?.message || "..." pattern.
1. yarn seed → Seeds blog permissions (blog.*, blog-category.*, blog-tag.*, generic-page.*)
2. Login as admin with blog permissions → Dashboard loads, blog section accessible
3. Create blog with title + content → Blog created with auto-generated slug, correct authorId
4. Edit blog: change title, add category → Update succeeds, categories/tags persist (no crash)
5. Create category with parent → Hierarchy allows multiple children per parent
6. Create tag, assign to blog → Tag linked correctly
7. Delete blog (soft-delete) → Blog moves to trash, public page returns 404
8. Restore blog from trash → Blog restored, public page renders again
9. Visit /blogs → Public listing shows only published blogs with images
10. Visit /blogs/:slug → Blog detail renders with hero image (not placeholder)
11. Visit /category/:slug → Category page lists blogs (slug not undefined)
12. Visit /author/:name → Author page returns matching blogs (case-insensitive)
13. Create CMS page, save as draft → Draft saved (not forced to published)
14. Visit /:page-slug as anonymous → Published CMS page renders correctly
15. No console errors in browser → Clean
Timeline: ~10 hours, 1-2 developers
North Star: SEO-ready public pages, zero dead code, backend hardened, first integration test.
Exit criteria: All public pages have proper metadata. Dead code removed. N+1 queries fixed. Integration test for blog CRUD lifecycle passes.
Fix blog analytics N+1 query — blogAnalytics() runs on EVERY getAllBlogs call with per-month separate queries (up to 12). Batch into single query with GROUP BY.
1 hr
S2-10
NM1
Fix ILIKE wildcard injection — searchTerm passed directly into %${searchTerm}% pattern. % matches everything, _ is single-char wildcard. Escape special chars in search input.
15 min
S2-11
NM6
Fix bulkBlogRestore — loads with withDeleted: true but doesn't filter to actually-deleted blogs. Could "restore" non-deleted blogs, triggering unnecessary updates.
15 min
S2-12
NM7
Add status filter to GenericPage getPageByIdOrSlug — returns drafts/archived pages to public callers. Add where: { status: "published" } for public-facing calls.
Set up first integration test (Jest + ts-jest or Vitest for backend). Test blog CRUD lifecycle: create with slug generation → update with categories/tags → soft-delete → restore → permanent-delete. Verify permission checks block unauthorized access. This is the single most important preventive measure against regressions.