Shop It Docs
Developer ResourcesBlog

Blog Module Backend Documentation

Blog Module - Backend Documentation

1. Backend Scope and Boundaries

Blog backend owns:

  • post authoring lifecycle (draft/published)
  • category lifecycle with deletion safeguards
  • slug generation and history fallback compatibility
  • related-entity persistence for products/FAQs/CTA
  • related-entity persistence for products/FAQs/CTA/videos
  • public read models for list/detail/categories
  • cache key generation and invalidation semantics for public reads

Blog backend does not own:

  • sitemap build/ping jobs
  • GA4/Pixel event emission
  • frontend rendering strategy for article HTML and JSON-LD script injection

2. Module Composition (Aggregate + Leaf)

BlogModule composes:

  • BlogAdminModule
    • BlogPostAdminModule
    • BlogCategoryAdminModule
  • BlogPublicModule
    • BlogCustomerModule

Wiring:

  • Registered in app root module (AppModule) for /api routes.
  • Public module additionally composed into MobileModule under /api/mobile prefix.

3. Data Model (Drizzle / PostgreSQL)

Primary tables:

  • blog_category
  • blog_post
  • blog_slug_history
  • blog_post_product
  • blog_post_faq
  • blog_cta_block
  • blog_post_video
  • seo (reused relation)

Key relation notes:

  • blog_post.category_id -> blog_category.id
  • blog_post.seo_id -> seo.id
  • blog_post.author_id -> admin_users.id
  • blog_post_product unique pair: (blog_post_id, product_id)
  • blog_cta_block.blog_post_id is one-to-one unique with post
  • blog_post_video.blog_post_id -> blog_post.id
  • blog_post_video.seo_id -> seo.id
  • blog_post_video.seo_id is unique (one SEO row per video row)
  • blog_slug_history.old_slug unique

4. Runtime Rules and Domain Invariants

4.1 Publish invariant

  • Public surface always filters by blog_post.status = 'published'.
  • Draft posts never appear in list/detail fallback paths.

4.2 Slug uniqueness invariant

  • Candidate slug cannot conflict with either:
    • current blog_post.slug
    • blog_slug_history.old_slug

4.3 Category deletion invariant

  • Category deletion blocked if any rows exist in blog_post with that category_id.

4.4 Product-linking invariant

  • Admin supplied productIds are deduped.
  • Linked product IDs must be currently published products.
  • Junction rows are rewritten in deterministic position order.

4.5 FAQ/CTA write strategy

  • FAQ array is replace-on-write (delete existing, insert submitted set).
  • CTA is replace-on-write; null removes CTA row.

4.6 Video write strategy

  • Video array is replace-on-write (delete existing, insert submitted set).
  • Every video must include a valid seoId.
  • Video payload cannot contain duplicate seoId.
  • A seoId already linked to another blog video is rejected.

5. Public Read Shapes

5.1 List shape

Contains summary-friendly fields only:

  • id, title, slug, excerpt
  • image fields
  • displayDate / publishedAt
  • category name/slug

5.2 Detail shape

Contains:

  • full content fields (body, heroIntro, etc.)
  • linked products with pricing/thumb
  • ordered FAQ array
  • nullable CTA object
  • video array (id, title, videoUrl, seoId, description, thumbnailUrl, position)
  • structured data object (article, optional faq, breadcrumb)

6. Caching Strategy

Key prefixes (via CacheKeyUtil):

  • blog:posts:list:
  • blog:post:slug:
  • blog:categories:

Invalidation pattern:

  • mutation services call invalidatePattern("blog:*") via cache utility wrapper.

TTL source:

  • BLOG_CACHE_TTL_SECONDS (default fallback 300s in service constructor)

7. Search and Pagination Strategy

Both admin and public blog searches use:

  • escaped ILIKE pattern match
  • similarity(column, search) > 0.3 trigram-style relevance helper

7.2 Pagination

  • Uses shared PaginationUtil normalization and drizzle params.
  • Response envelope follows standard ResponseDto pagination metadata pattern.

7.3 Validation controls

  • QueryDto.size has upper bound @Max(100).
  • Blog search fields require minimum length (@MinLength(2)), preventing low-signal scans.

8. Auth and Guarding

Admin controllers:

  • JwtAuthGuard + RoleGuard
  • method-level @Permissions(...)
  • method-level IpThrottlerGuard + @IpThrottle(...)

Public controller:

  • @Public()
  • no JWT required

9. Error Contract

Blog-specific codes:

  • BLOG_POST_NOT_FOUND
  • BLOG_SLUG_ALREADY_EXISTS
  • BLOG_POST_CREATE_FAILED
  • BLOG_POST_UPDATE_FAILED
  • BLOG_POST_DELETE_FAILED
  • BLOG_POST_INVALID_STATUS_TRANSITION
  • BLOG_CATEGORY_NOT_FOUND
  • BLOG_CATEGORY_NOT_EMPTY
  • BLOG_CATEGORY_SLUG_ALREADY_EXISTS

Error payload style:

  • { message, errorCode }

10. Performance Notes

  • Public reads are cache-backed and avoid overfetch on list endpoints.
  • Detail joins are explicit and relation queries are selective.
  • Mutation paths invalidate coarse namespace (blog:*) for correctness over partial-key complexity.

11. File Map

Core backend files:

  • apps/api/src/modules/blog/admin/post/*
  • apps/api/src/modules/blog/admin/category/*
  • apps/api/src/modules/blog/customer/blog/*
  • apps/api/src/modules/blog/blog.module.ts
  • apps/api/src/modules/blog/blog-admin.module.ts
  • apps/api/src/modules/blog/blog-customer.module.ts

Schema files:

  • packages/db/src/schema/blog/*

12. Environment Variables

VariableDefaultDescription
BLOG_CACHE_TTL_SECONDS300Cache TTL for public blog reads
FRONTEND_BASE_URLapp defaultUsed to build canonical URL fields in structured data

See Also