Skip to content

Blog/CMS Sprint Plan — Cross-Functional War Room Output

Context: 236 issues from Blog/CMS System Deep-Dive Generated: 2026-04-02 | Last Updated: 2026-04-02 | Method: Cross-Functional War Room (PM + Engineer + Designer) Participants: PM (business impact), Engineer (effort/risk), Designer (UX impact) Related: Deep-Dive | RBAC Sprint Plan (format reference) Scope: Full-stack Blog/CMS system — 5 backend modules (Blog, BlogCategory, BlogTag, Slug, GenericPage), 5 entities, public API, Next.js 15 frontend (dashboard CRUD + public SSR pages) Files Analyzed: 79 files, ~14,978 LOC


Triage Summary

Bucket Criteria Count Effort
Ship Blocker Cannot deploy. SQL injection, permission bypass, data corruption, crash-on-render. 25 ~2.5 hrs
Sprint 1 Blog CMS doesn't work for real users. Permission model, CRUD reliability, public pages. 22 ~6.5 hrs
Sprint 2 SEO fixes, dead code removal, backend cleanup, testing foundation. ~30 ~10 hrs
Backlog Low-impact cosmetics, accessibility, missing features, minor inconsistencies. ~159 Ongoing

Ship Blockers (Pre-Release Gate)

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.

Security Critical

Order Issue Description File Change Effort
1 NC1 SQL Injection via orderBygetSortQuery accepts arbitrary column names, TypeORM interpolates raw into SQL. Affects ALL 15 services using it. shared/getSortQuery.ts Add column whitelist: const ALLOWED = ["createdAt","title","updatedAt","name","views"]; if (!ALLOWED.includes(field)) throw ApiError(400) 15 min
2 C1 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/blog.service.ts:51,106,352,404,475,515,552,579,610 Uncomment all 9 await checkPermissionAndThrow(...) calls 15 min
3 C8/NC8 createBlog controller body spread lets authorId override authenticated user's ID — impersonation risk. L16-18: authorId = req.user?.id \|\| body.authorId still reads body. Blog/blog.controller.ts:16-18 Change to authorId = req.user?.id — remove body.authorId fallback entirely 5 min
4 H1 GET /blogs/:id defined BEFORE router.use(auth()) — no auth, leaks drafts and unpublished posts to anonymous users. Blog/blog.routes.ts:17 Move route after router.use(auth()) on line 18, or add explicit auth() middleware 5 min
5 H2 GET /pages/ and GET /pages/:id fully public — defined before router.use(auth()). Exposes draft/archived CMS pages to anonymous users. GenericPage/generic-page.routes.ts:10-11 Add status filter (where: { status: "published" }) for unauthenticated requests, or move after auth 10 min
6 C11 Slug service error response reveals ALL table names in the system — information disclosure. Slug/slug.service.ts:40 Replace with generic error: "Invalid entity type" 5 min
7 NH1 No pagination limit cap — ?limit=999999999 forces unbounded DB query, potential DoS via memory exhaustion. helper/parsePaginationQuery.ts Add Math.min(limit, 100) cap 5 min

Data Corruption Critical

Order Issue Description File Change Effort
8 C2 bulkBlogUpdateByCategory soft-deletes the old category as side-effect (L303) when reassigning blogs — destroys category for all other blogs using it. Blog/blog.service.ts:303 Remove categoryRepo.softDelete(oldCategoryId) call 5 min
9 C3 updateBlog loads blog without relations — categories and tags are undefined — length comparison crashes at runtime. Blog/blog.service.ts:407-453 Add relations: ["categories", "tags"] to findOne options 5 min
10 C4 permanentlyDeleteBlog calls blogRepository.delete(id) twice (L543 + L546) — redundant, indicates copy-paste error. Blog/blog.service.ts:543,546 Remove duplicate .delete(id) call at L546 2 min
11 NC4/M6 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 10 min

Frontend Critical

Order Issue Description File Change Effort
14 NC2 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. components/pages/blogs/blog-page-frontend-v2.jsx:132 V2 uses blog.image — fixed 5 min Done
17 NC7 Category links broken on blog detail — API returns {id, name} without slug — all links render as /category/undefined. Backend: public.service.ts (getBlogByIdOrSlug category select) 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 resultsnormalizeText() 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. app/(main-layout)/blogs/[slug]/page.js:16, app/(main-layout)/category/[slug]/page.js:24 Change "ttps:// to "https:// 1 min Done

Console.log Cleanup

Order Issue Description File Change Effort
22 M1 console.log("blogIds", blogIds) — leaks blog IDs in production logs Blog/blog.service.ts:259 Delete line 1 min
23 console.log({src}) — logs every image normalization call shared/normalizeImageSrc.ts:11 Delete line 1 min
24 M29 console.log("hasPermission page", hasPermission) in usePages hook api/pages/index.js Delete line 1 min Done
25 M45-46 console.log("category blogs", ...) and console.log("slug", slug) in public route pages app/(main-layout)/category/, app/(main-layout)/blogs/ page files Delete 2 console.log statements 2 min Done

Verification Checklist

After completing all 25 ship blockers:

  • GET /blogs?orderBy=id;DROP TABLE users-- returns 400, not SQL error
  • POST /blogs without blog.create permission returns 403 (not 200)
  • PATCH /blogs/:id without blog.update permission returns 403
  • DELETE /blogs/:id without blog.delete permission returns 403
  • POST /blogs { authorId: 999 } as user 1 creates blog with authorId: 1 (not 999)
  • GET /blogs/:id without auth token returns 401 (not draft content)
  • GET /pages/ as anonymous returns only published pages (no drafts)
  • GET /blogs?limit=999999 returns at most 100 results
  • PATCH /blogs/bulk-update-category does NOT soft-delete the old category
  • PATCH /blogs/:id with categories/tags does not crash
  • POST /blogs with title but no slug auto-generates a valid slug
  • DELETE /blog-tags/:id/permanent correctly checks tag.blog relation
  • /blogs/nonexistent-slug returns HTTP 404 (not blank 200)
  • 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")
  • Author page /author/John-Doe returns matching blogs (case-insensitive) — Blocked: backend needs ILIKE/LOWER match
  • Anonymous user can view published CMS pages at /about, /terms, etc. — Blocked: backend needs public endpoint
  • No console.log output in server or browser console during normal operations
  • Slug error messages do not reveal table names

Sprint 1: Blog CMS Actually Works (Week 1)

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.

Backend Architecture

# Issue(s) Description File(s) Effort
S1-1 C6 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. BlogCategory/blog_category.service.ts (updateCategory) 15 min
S1-4 H9/H10 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. BlogCategory/blog_category.validation.ts, BlogTag/blog_tag.validation.ts 15 min
S1-5 H11 Fix deleteCategory returning 200 when not deleted — category linked to blogs is not actually deleted but client thinks success. No affected-rows check. BlogCategory/blog_category.service.ts (deleteCategory) 15 min
S1-6 H14 Fix category hierarchy ONE-child-per-parent limitvalidateParentId 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). Blog/blog.validation.ts, BlogCategory/blog_category.validation.ts, BlogTag/blog_tag.validation.ts 15 min
S1-9 NH2 Fix file deletion before database savedeleteFileByPathName called BEFORE blogRepository.save in updateBlog. If save fails, old image is already gone. Move deletion after successful save. Blog/blog.service.ts (updateBlog ~L443), GenericPage/generic-page.service.ts (updatePage) 15 min
S1-10 C9 Seed missing generic-page.restore permissionbulkGenericPageRestore checks this permission but it's NOT in the seed file. Always returns 403. seed/data/permission.ts 5 min
S1-11 C10 Remove double auth() on GenericPage bulk-permanent-deleterouter.use(auth()) at L12 already applies, L24 adds second auth(). Causes double token verification. GenericPage/generic-page.routes.ts:23-24 5 min
S1-12 H5 Fix blog slug dual unique constraint — column-level unique: true AND partial index WHERE deletedAt IS NULL clash. Column constraint blocks slug reuse after soft-delete. Remove column-level unique, keep partial index. entity/Blog.ts (slug column) + Blog/blog.service.ts (slug check logic) 30 min

Frontend Sprint 1

# Issue(s) Description File(s) Effort
S1-13 H15 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. api/blogs/index.js (delete/restore/bulk mutation hooks) 30 min Done
S1-14 H16 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 leveluseBulkRestoreBlogs() 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 SanitizedHTMLPreviewDOMPurify.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 blurhandleSlugCheck() fires onBlur even in edit mode. Added !edit guard to slug onBlur handler. components/pages/blogs/blogs-form.jsx (slug onBlur handler) 15 min Done
S1-20 C17 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 swallowingtoast.error() passes Error object as second argument. Fixed in page-form.jsx to use error?.message || "..." pattern. components/pages/pages/page-form.jsx 5 min Done

Sprint 1 Acceptance Test

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

Sprint 2: Clean Foundation (Week 2-3)

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.

SEO & Public Pages (3 hrs)

# Issue(s) Description Effort
S2-1 H22 Add generateMetadata to author pages — currently zero SEO metadata (no title, description, OG tags). Use author name + blog count. 30 min
S2-2 H23 Add generateMetadata to category pages — currently zero SEO metadata. Use category name and description. 30 min
S2-3 H26 Standardize canonical URLs — THREE conflicting patterns: backend getAllBlogs uses /blog/, getBlogByIdOrSlug uses /articles/, frontend route is /blogs/. Unify to ${NEXT_PUBLIC_FRONTEND_URL}/blogs/${slug}. 30 min
S2-4 H27 Standardize date display — blog cards show publishedAt but detail page shows createdAt. Use publishedAt when available, fall back to createdAt. 15 min
S2-5 H28 Fix preview URL — dashboard uses /en/${slug} but app has no /en/ prefix route. Change to /blogs/${slug}. 10 min
S2-6 NH6 Add sizes attribute to blog hero <Image fill> — without it Next.js serves unnecessarily large images on mobile, degrades LCP. 10 min
S2-7 M51 Add page number to blog listing metadata — paginated pages (page 2+) show same title as page 1. 15 min
S2-8 H24/H25 Add error.js and loading.js files to blog route segments — unhandled errors show Next.js default error page, no loading skeleton during navigation. 30 min

Backend Cleanup (2 hrs)

# Issue(s) Description Effort
S2-9 M4/M5 Fix blog analytics N+1 queryblogAnalytics() 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 injectionsearchTerm 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. 15 min
S2-13 NH3 Replace deprecated findByIds — TypeORM 0.3+ deprecation. Use findBy({ id: In(ids) }) instead. Affects blog, category services. 15 min

Frontend Cleanup (2 hrs)

# Issue(s) Description Effort
S2-14 D1/H18 Delete dead blog-categories.js (122 LOC) — legacy useState hook, fully replaced by React Query hooks in api/blogs/index.js. Zero importers. 5 min
S2-15 D2-D18 Remove dead code: unused imports (blog.utils.ts, LessThanOrEqual, normalizeImageSrc, toSlug, etc.), orphaned functions (permanentlyDeleteGenericPagesByIds, incrementBlogViewCount), commented-out components. 30 min
S2-16 M26 Replace deprecated keepPreviousData import — TanStack Query v5 moved it to placeholderData: keepPreviousData. Affects 14 hooks across app. 30 min
S2-17 M31 Add missing "use client" directives to api/blogs/index.js and api/pages/index.js — fragile in Next.js 15 strict mode without it. 15 min
S2-18 M32/M33 Add ISR revalidateTag calls to page (GenericPage) mutations — create/delete/restore pages never invalidate ISR cache. Only useUpdatePage revalidates. 30 min
S2-19 M35-M38 Fix inconsistencies: selectedTag initialized as undefined vs selectedCategory as null (M35), tag filter uses slug but category uses label (M36), unused router import (M37), CSS typo 'break-words (M38). 30 min

Testing (2 hrs)

# Description Effort
S2-20 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. 2 hrs

Backlog (Ongoing)

Fix opportunistically when touching adjacent files. No dedicated sprint time needed.

Low Priority (L1-L40)

Issue(s) Description Fix When
L1 Export named CreateBlogService — misleading, handles all operations Touching blog.service.ts
L2 blog.utils.ts generateUniqueSlug imported but never used Done in Sprint 2 dead code removal
L3 BlogView entity has @DeleteDateColumn but view counts never soft-deleted Touching BlogView entity
L4 Error message says "edit" when deleting (L488-490) Touching blog.service.ts
L5 ICreateBlogPayload.publishedAt typed as string but used as Date Touching blog interfaces
L6 tranding typo (should be trending) in public.service.ts:53 Touching public.service.ts
L7-L10 Unused imports, grammar errors, function name typos (getBLogTagList, existngTagName) Touching respective files
L11-L20 Entity dead imports, copy-paste comments, unused validation messages Touching respective files
L21-L30 Frontend: ghost form fields, commented-out UI sections, stale dependencies Touching respective components
L31-L40 CSS typos, missing optional chaining, unnecessary Suspense wrapping Touching respective files
NL1-NL19 Adversarial review low items: variable misspellings, type inconsistencies, dead code Touching respective files

Accessibility (A1-A8)

Issue(s) Description Fix When
A1-A2 Tab filter buttons lack role="tab", aria-selected; icon buttons have no aria-label Touching blogs-page.jsx
A3-A4 Sort headers are <div onClick> without keyboard handlers; checkboxes have no <label> Touching blogs-page.jsx
A5 Image remove button lacks aria-label Touching blogs-form.jsx
A6 Nested <Link> inside parent <Link> in BlogCard — invalid HTML, breaks screen readers Touching blog-card.jsx
A7-A8 Focus trapping in preview modal; DataTable semantic structure unknown Touching respective components

Missing Features (F1-F12)

Issue(s) Description Fix When
F1 No search bar on public blog listing (backend searchTerm param already supported) Feature planning
F2 No tag-based filtering UI (backend tag param already supported) Feature planning
F3 No category index page (/category/) Feature planning
F4 No sorting options UI (newest, popular — backend order/orderBy supported) Feature planning
F5-F6 No reading time estimate, no social share buttons Feature planning
F7-F9 No view counter display, no newsletter subscription, no generateStaticParams Feature planning
F10-F12 No back-to-blogs navigation on detail, no tag display on detail page, no RSS feed Feature planning

Remaining Medium Items

Issue(s) Description Fix When
M2-M3 deleteFileByPathName without await, inconsistent canonical URLs Touching blog.service.ts
M8-M15 Duplicate select fields, status filter issues, case inconsistencies, restore conflict checks Touching respective services
M16-M25 Validation gaps, unused imports, deprecated methods, naming inconsistencies Touching respective files
M39-M52 Frontend: unused state, double revalidation, variable shadowing, exposed env vars Touching respective components
NM2-NM26 Adversarial review medium items: dead functions, cascade risks, N+1 queries, ghost form fields Touching respective files

Key Decisions Log

Decision PM Engineer Designer Outcome
Fix order for ship blockers SQL injection first — legal liability Agrees — NC1 is 15-min fix with highest blast radius across 15 services Users won't notice security fixes, but will notice crashes Security → Data Corruption → Frontend Crashes → Console.logs
Permission checks: uncomment or rewrite? Ship fast — uncomment now, refactor later Uncomment is 15 min, rewrite is 2+ hrs Invisible to users either way Uncomment now (C1), evaluate rewrite in Sprint 2
Blog slug: frontend or backend generation? Backend — single source of truth Backend — frontend can't guarantee uniqueness in concurrent scenarios Frontend slug preview is nice UX, generation must be backend Backend generates slug on create if not provided. Frontend can suggest.
Public pages auth: add auth or status filter? Status filter — public pages must work for anonymous Status filter simpler, matches GenericPage intent Anonymous access is core requirement Status filter for listings (H2), auth for draft access (H1)
Dead code: remove in Sprint 1 or 2? Sprint 2 — don't block the ship Sprint 2 — dead code doesn't break anything Dead code confuses future devs but not user-facing Sprint 2 (S2-14, S2-15)
ISR revalidation scope All mutations — stale public pages damage SEO All mutations — inconsistent revalidation worse than none Stale content confusing for users previewing changes Blog mutations in Sprint 1 (S1-13), page mutations in Sprint 2 (S2-18)
DOMPurify SSR crash fix Whatever ships faster isomorphic-dompurify is drop-in replacement SSR crash affects SEO crawlers — must fix isomorphic-dompurify (S1-18)

Sprint Board Visualization

┌─────────────────────────────────────────────────────────────────────┐
│                    SHIP BLOCKERS (pre-release)                      │
│                    25 items · ~2.5 hrs · 1 developer                │
│                                                                     │
│  [SEC]  NC1(SQLi) C1(perms) NC8(authorId) H1(draft-leak)          │
│         H2(page-leak) C11(info-disc) NH1(DoS)                      │
│  [DATA] C2(cat-delete) C3(relations) C4(dupe-delete) NC4(slug)     │
│         C7(tag-check) C5(views-crash)                               │
│  [FE]   NC2(hooks) C12(404) C13(image) NC7(cat-slug) C16(draft)   │
│         C14(author) NC6(anon-page) SEO(typo)                       │
│  [LOG]  M1 normalizeImg M29 M45-46                                 │
│                                                                     │
│  EXIT: No injection. Permissions active. No data corruption.        │
│        No crash-on-render. No console.log.                          │
└──────────────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│                    SPRINT 1 — "Blog CMS Works" (week 1)            │
│                    22 items · ~6.5 hrs · 1 developer                │
│                                                                     │
│  [BE]   C6(perm-logic) H3(dupe-mw) H8(partial-update)             │
│         H9/H10(naming) H11(delete-200) H14(hierarchy)              │
│         C8(parentId) M7(min-0) NH2(file-before-save)               │
│         C9(seed) C10(dupe-auth) H5(slug-reuse)                     │
│  [FE]   H15(ISR) H16(double-url) H19(sort-icon) H20(mutation)     │
│         H21(cat-slug) NH9(SSR) NH10(slug-blur) C17(title-req)     │
│         C18(null-check) NH8(toast)                                  │
│                                                                     │
│  EXIT: Admin can CRUD blogs/categories/tags/pages.                  │
│        Public pages render correctly for anonymous visitors.         │
└──────────────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│                    SPRINT 2 — "Clean Foundation" (week 2-3)        │
│                    ~30 items · ~10 hrs · 1-2 developers             │
│                                                                     │
│  [SEO]  H22(author-meta) H23(cat-meta) H26(canonical)             │
│         H27(dates) H28(preview-url) NH6(sizes) M51(pagination)    │
│         H24/H25(error+loading)                                      │
│  [BE]   M4/M5(N+1) NM1(ILIKE) NM6(restore) NM7(drafts) NH3(dep)  │
│  [FE]   D1(dead) D2-D18(dead) M26(dep) M31(use-client)           │
│         M32/M33(ISR) M35-M38(inconsistencies)                      │
│  [TEST] Integration test ★ (most important item)                   │
│                                                                     │
│  EXIT: SEO metadata on all pages. Dead code gone. N+1 fixed.       │
│        First integration test passes.                               │
└──────────────────────────┬──────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│                    BACKLOG (ongoing)                                 │
│                    ~159 items · fix when touching adjacent files     │
│                                                                     │
│  L1-L40 + NL1-NL19: Low-priority code quality                      │
│  A1-A8: Accessibility improvements                                  │
│  F1-F12: Missing features (search, RSS, social share, tags UI)     │
│  M2-M52 + NM2-NM26: Remaining medium items                        │
└─────────────────────────────────────────────────────────────────────┘

Total Effort Estimate

Phase Items Effort Developers Timeline
Ship Blockers 25 ~2.5 hrs 1 Day 1 (before any deployment)
Sprint 1 22 ~6.5 hrs 1 Week 1
Sprint 2 ~30 ~10 hrs 1-2 Week 2-3
Backlog ~159 ~20+ hrs 1 Ongoing
Total 236 ~39 hrs ~3-4 weeks (active sprints)

236 issues from deep-dive. Ship Blockers + Sprint 1 + Sprint 2 address 77 issues (~33%). Remaining 159 issues are backlog — low-impact items to fix opportunistically.

Issue references (C1, H1, NC1, etc.) map to Blog/CMS System Deep-Dive numbering. "C" = Critical, "H" = High, "M" = Medium, "NC" = New Critical (adversarial review), "NH" = New High, "NM" = New Medium, "D" = Dead Code, "L" = Low, "A" = Accessibility, "F" = Missing Feature.


Generated by BMAD Document Project workflow v1.2.0 — Cross-Functional War Room, 2026-04-02 Cross-reference: Deep-Dive: Blog/CMS System — 236 issues across 79 files