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:
BlogAdminModuleBlogPostAdminModuleBlogCategoryAdminModule
BlogPublicModuleBlogCustomerModule
Wiring:
- Registered in app root module (
AppModule) for/apiroutes. - Public module additionally composed into
MobileModuleunder/api/mobileprefix.
3. Data Model (Drizzle / PostgreSQL)
Primary tables:
blog_categoryblog_postblog_slug_historyblog_post_productblog_post_faqblog_cta_blockblog_post_videoseo(reused relation)
Key relation notes:
blog_post.category_id -> blog_category.idblog_post.seo_id -> seo.idblog_post.author_id -> admin_users.idblog_post_productunique pair:(blog_post_id, product_id)blog_cta_block.blog_post_idis one-to-one unique with postblog_post_video.blog_post_id -> blog_post.idblog_post_video.seo_id -> seo.idblog_post_video.seo_idis unique (one SEO row per video row)blog_slug_history.old_slugunique
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
- current
4.3 Category deletion invariant
- Category deletion blocked if any rows exist in
blog_postwith thatcategory_id.
4.4 Product-linking invariant
- Admin supplied
productIdsare 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;
nullremoves 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
seoIdalready 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, optionalfaq,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
7.1 Search
Both admin and public blog searches use:
- escaped
ILIKEpattern match similarity(column, search) > 0.3trigram-style relevance helper
7.2 Pagination
- Uses shared
PaginationUtilnormalization and drizzle params. - Response envelope follows standard
ResponseDtopagination metadata pattern.
7.3 Validation controls
QueryDto.sizehas 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_FOUNDBLOG_SLUG_ALREADY_EXISTSBLOG_POST_CREATE_FAILEDBLOG_POST_UPDATE_FAILEDBLOG_POST_DELETE_FAILEDBLOG_POST_INVALID_STATUS_TRANSITIONBLOG_CATEGORY_NOT_FOUNDBLOG_CATEGORY_NOT_EMPTYBLOG_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.tsapps/api/src/modules/blog/blog-admin.module.tsapps/api/src/modules/blog/blog-customer.module.ts
Schema files:
packages/db/src/schema/blog/*
12. Environment Variables
| Variable | Default | Description |
|---|---|---|
BLOG_CACHE_TTL_SECONDS | 300 | Cache TTL for public blog reads |
FRONTEND_BASE_URL | app default | Used to build canonical URL fields in structured data |
See Also
- Feature Guide: Blog - Feature List
- API Guide: Blog - API & Integration Guide