Content Module API & Integration Guide
Admin and public contracts for section-wise CMS payloads with Tiptap JSON validation.
Audience: Frontend/admin-panel engineers and backend integrators Scope: Content page metadata, section payload CRUD, public/mobile page reads
Content Module - API & Integration Guide
1. Quick Metadata
- Module:
Content - Auth models:
- Admin routes:
JwtAuthGuard + RoleGuard + @Permissions(...) - Public route:
@Public()
- Admin routes:
- Base routes:
- Admin:
/api/admin/content/pages - Public:
/api/content/pages - Mobile mirror:
/api/mobile/content/pages
- Admin:
- Response envelope:
ResponseDto<T> - Swagger tags:
Content Pages (Admin)Content Pages
1.1 Read Caching (Redis)
Content read endpoints are cache-aside:
GET /api/content/pages/:pageKeyandGET /api/mobile/content/pages/:pageKeyuse public keyspacecontent:page:public:GET /api/admin/content/pages/:pageKeyuses admin keyspacecontent:page:admin:
TTL envs:
CONTENT_PAGE_PUBLIC_CACHE_TTL_SECONDS(default300)CONTENT_PAGE_ADMIN_CACHE_TTL_SECONDS(default120)
Invalidation:
- successful
PATCH /meta,PUT /sections/:sectionKey, andDELETE /sections/:sectionKeyinvalidate both admin and public cache keys for that page. - Redis failures do not fail API responses; service falls back to DB flow and logs warnings.
2. Page and Section Keys
2.1 Page keys
homeaboutfaqprivacy_policyterms_of_serviceproducts_thangkasproducts_singing_bowlsproducts_statuesproducts_jewelleryglobal_footer
2.2 Section keys
herotop_pickscategoriesheritagewhy_usfaq_introblogsquotewhy_choosestory_blocksproducts_headerblogs_headerbrand_blockquick_linkscollections_linkssupport_linksfeatured_collections_cardscopyrightdocument
2.3 Page-to-section compatibility
| Page key | Allowed sections |
|---|---|
home | hero, top_picks, categories, heritage, why_us, faq_intro, blogs |
about | hero, quote, why_choose, story_blocks, faq_intro, blogs |
faq | faq_intro |
privacy_policy | document |
terms_of_service | document |
products_thangkas | hero, products_header, blogs_header |
products_singing_bowls | hero, products_header, blogs_header |
products_statues | hero, products_header, blogs_header |
products_jewellery | hero, products_header, blogs_header |
global_footer | brand_block, quick_links, collections_links, support_links, featured_collections_cards, copyright |
Invalid pair behavior:
- HTTP
400 - errorCode
CONTENT_SECTION_KEY_INVALID
3. Admin Endpoints
3.1 Get page (admin)
| Aspect | Value |
|---|---|
| Method | GET |
| Path | /api/admin/content/pages/:pageKey |
| Auth | JwtAuthGuard + RoleGuard |
| Permission | Content_READ |
| Throttle | 30/minute |
| Response | ResponseDto<ContentPageResponseDto> |
Behavior:
- auto-creates page if missing (with module default title)
- returns all sections, including
isEnabled=false
3.2 Update page meta
| Aspect | Value |
|---|---|
| Method | PATCH |
| Path | /api/admin/content/pages/:pageKey/meta |
| Permission | Content_UPDATE |
| Throttle | 10/minute |
| Body | UpdateContentPageMetaDto |
| Response | ResponseDto<ContentPageResponseDto> |
Body fields:
title?: string (max 255)seoId?: string | null(UUID or null)
SEO validation:
- if non-null
seoIddoes not exist ->400 CONTENT_SEO_NOT_FOUND
3.3 Upsert section
| Aspect | Value |
|---|---|
| Method | PUT |
| Path | /api/admin/content/pages/:pageKey/sections/:sectionKey |
| Permission | Content_UPDATE |
| Throttle | 10/minute |
| Body | UpsertContentSectionDto |
| Response | ResponseDto<ContentSectionResponseDto> |
Body fields:
payloadJson: Record<string, unknown>(required)position?: number(default0, min0)isEnabled?: boolean(defaulttrue)
Behavior:
- validates
sectionKeycompatibility withpageKey - validates
payloadJsonagainst section schema - auto-creates page if missing
- upserts by unique
(pageId, sectionKey)
3.4 Delete section
| Aspect | Value |
|---|---|
| Method | DELETE |
| Path | /api/admin/content/pages/:pageKey/sections/:sectionKey |
| Permission | Content_DELETE |
| Throttle | 10/minute |
| Response | ResponseDto<void> |
Behavior:
- page must already exist
- section row must already exist
- otherwise returns
404 CONTENT_NOT_FOUND
4. Public + Mobile Endpoints
4.1 Get page (public)
| Aspect | Value |
|---|---|
| Method | GET |
| Path | /api/content/pages/:pageKey |
| Auth | Public |
| Response | ResponseDto<ContentPageResponseDto> |
Behavior:
- does not auto-create pages
- missing page ->
404 CONTENT_NOT_FOUND - returns only enabled sections
- ordered by
position ASC, thenid ASC
4.2 Get page (mobile-composed)
| Aspect | Value |
|---|---|
| Method | GET |
| Path | /api/mobile/content/pages/:pageKey |
| Auth | Public |
| Response | ResponseDto<ContentPageResponseDto> |
Behavior is identical to /api/content/pages/:pageKey.
5. DTO Contract
5.1 ContentPageResponseDto
{
"id": 1,
"pageKey": "home",
"title": "Home Page",
"seoId": "018f3c98-8dcf-7b1c-a8b9-c32b3f6de101",
"seo": {
"id": "018f3c98-8dcf-7b1c-a8b9-c32b3f6de101",
"metaTitle": "Thangka Home",
"metaDescription": "Authentic handmade thangkas.",
"metaKeywords": "thangka,nepal,art",
"canonicalUrl": "https://thangka.shop",
"robotsIndex": true,
"robotsFollow": true,
"robotsAdvanced": null,
"ogTitle": "Thangka Home",
"ogDescription": "Authentic handmade thangkas.",
"ogType": "website",
"ogUrl": "https://thangka.shop",
"ogImageUrl": "https://thangka.shop/og-home.jpg",
"ogSiteName": "Thangka",
"twitterCard": "summary_large_image",
"twitterSite": "@thangka",
"twitterCreator": "@thangka",
"twitterTitle": "Thangka Home",
"twitterDescription": "Authentic handmade thangkas.",
"twitterImageUrl": "https://thangka.shop/twitter-home.jpg",
"alternates": null,
"structuredDataJsonLd": null
},
"sections": [],
"createdAt": "2026-05-15T12:00:00.000Z",
"updatedAt": "2026-05-15T12:15:00.000Z"
}5.2 ContentSectionResponseDto
{
"id": 11,
"sectionKey": "hero",
"position": 0,
"isEnabled": true,
"payloadJson": {
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Handcrafted" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Sacred Art from Nepal" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Traditional lineage, modern delivery." }] }] }
},
"createdAt": "2026-05-15T12:00:00.000Z",
"updatedAt": "2026-05-15T12:15:00.000Z"
}6. Validation Rules
6.1 Display text fields
Display text is Tiptap JSON and must look like:
{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Example" }]
}
]
}Rules:
typemust bedoccontentmust be non-empty
6.2 URL fields
Allowed:
https://...http://.../relative-path
Rejected:
ftp://...javascript:...- empty strings
6.3 Field-size constraints
| Field class | Limit |
|---|---|
| label (rich) | 60 chars equivalent |
| heading (rich) | 110 chars equivalent |
| description (rich) | 320 chars equivalent |
| card title (rich) | 60 chars equivalent |
| card description (rich) | 220 chars equivalent |
| image alt | 255 chars |
| iconKey | 80 chars |
7. Section Payload Schemas and Examples
All examples below are valid payloadJson values for
PUT /sections/:sectionKey.
7.1 hero
Required:
label(Tiptap)heading(Tiptap)description(Tiptap)
Optional:
images.main/top/bottom(src,alt)cta(labelTiptap,hrefsafe URL)
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Authentic" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Handmade Thangkas" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Rooted in Himalayan lineage." }] }] },
"images": {
"main": { "src": "https://cdn.example.com/hero-main.webp", "alt": "Main hero" },
"top": { "src": "/images/hero-top.webp", "alt": "Top accent" },
"bottom": { "src": "/images/hero-bottom.webp", "alt": "Bottom accent" }
},
"cta": {
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Explore" }] }] },
"href": "/products/thangkas"
}
}7.2 top_picks and categories
Required:
heading(Tiptap)
Optional:
label(Tiptap)description(Tiptap)
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Curated" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Top Picks" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Most loved this month." }] }] }
}7.3 heritage
Required:
label,heading,description,quote(all Tiptap)
Optional:
images.main/collageTop/collageBottom/logo
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Heritage" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Tradition in Every Stroke" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Each piece follows traditional iconography." }] }] },
"quote": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Art as devotion." }] }] },
"images": {
"main": { "src": "https://cdn.example.com/heritage-main.webp", "alt": "Heritage main" },
"collageTop": { "src": "/images/heritage-top.webp", "alt": "Collage top" },
"collageBottom": { "src": "/images/heritage-bottom.webp", "alt": "Collage bottom" },
"logo": { "src": "/images/heritage-logo.svg", "alt": "Heritage logo" }
}
}7.4 why_us and why_choose
Required:
heading(Tiptap)cards[]length1..12
Card schema:
title(Tiptap)description(Tiptap)iconKey(string)
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Why Us" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Trust by Design" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Authenticity, care, and provenance." }] }] },
"cards": [
{
"title": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Verified Craft" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Direct from lineage artists." }] }] },
"iconKey": "verified-craft"
}
]
}7.5 faq_intro and blogs
faq_intro required:
label,heading,description
blogs required:
heading
Both can include optional CTA.
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "FAQ" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Questions Answered" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Everything about shipping and authenticity." }] }] },
"cta": {
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Read More" }] }] },
"href": "/faq"
}
}7.6 quote
Required:
text(Tiptap)
Optional:
author(Tiptap)
{
"text": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "A thangka is meditation made visible." }] }] },
"author": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "House of Thangka" }] }] }
}7.7 story_blocks
Required:
blocks[]length1..10
Block schema:
label,heading,description(Tiptap)image(src,alt)
Optional:
overlayLines[](up to 6 Tiptap lines)reverse(boolean)
{
"blocks": [
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Mission" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Preserve Living Traditions" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "We partner with craftsmen to sustain lineage work." }] }] },
"image": { "src": "https://cdn.example.com/about-mission.webp", "alt": "Mission image" },
"overlayLines": [
{ "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Craft" }] }] },
{ "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Culture" }] }] }
],
"reverse": false
}
]
}7.8 products_header and blogs_header
Required:
heading(Tiptap)
Optional:
label,description,cta
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Category" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Featured Thangkas" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Handpicked by masters." }] }] },
"cta": {
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "View All" }] }] },
"href": "/products/thangkas"
}
}7.9 Footer sections
brand_block
Required:
heading,tagline(Tiptap)
Optional:
legalLabel(Tiptap)
{
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Thangka" }] }] },
"tagline": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Authentic Himalayan art." }] }] },
"legalLabel": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "All rights reserved" }] }] }
}quick_links, collections_links, support_links
Required:
title(Tiptap)items[]length1..20with{ label: Tiptap, href: safeUrl }
{
"title": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Quick Links" }] }] },
"items": [
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "About" }] }] },
"href": "/about"
},
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Contact" }] }] },
"href": "/contact"
}
]
}featured_collections_cards
Required:
items[]length1..12
Item schema:
label(Tiptap)href(safe URL)
Optional:
title(Tiptap)
{
"title": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Featured" }] }] },
"items": [
{
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Green Tara" }] }] },
"href": "/products/thangkas?collection=green-tara"
}
]
}copyright
Required:
text(Tiptap)
{
"text": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "© 2026 Thangka. All rights reserved." }]
}
]
}
}7.10 document (legal pages)
Used by:
privacy_policyterms_of_service
Required:
eyebrow(string)title(string)summary(string)lastUpdated(string)sections[](length1..50)
sections[] item:
id(kebab-case string)title(string)paragraphs[](length1..20)- optional
bullets[](length1..40)
{
"eyebrow": "Privacy",
"title": "Privacy Policy",
"summary": "We respect your privacy and protect your personal information.",
"lastUpdated": "Last updated: April 1, 2026",
"sections": [
{
"id": "information-we-collect",
"title": "1. Information We Collect",
"paragraphs": [
"We collect account and order details needed to process purchases.",
"We also collect limited usage telemetry to improve reliability."
],
"bullets": [
"Name and email",
"Shipping details",
"Order metadata"
]
}
]
}8. End-to-End Flow Examples
8.1 Initial admin authoring flow
- Admin fetches page:
GET /api/admin/content/pages/home
- Page row is auto-created if missing.
- Admin writes each section with
PUT /sections/:sectionKey. - Frontend can consume from public endpoint immediately.
8.2 SEO attach/clear flow
Attach:
PATCH /api/admin/content/pages/home/metawith{"seoId": "<uuid>"}
Clear:
PATCH /api/admin/content/pages/home/metawith{"seoId": null}
8.3 Hide section flow
Update section:
PUT /api/admin/content/pages/home/sections/why_uswithisEnabled: false
Result:
- section still visible in admin page response
- section omitted from public/mobile response
9. Error Code Mapping
| errorCode | Typical HTTP | Cause |
|---|---|---|
CONTENT_NOT_FOUND | 404 | Missing page on public read, or missing page/section on delete |
CONTENT_SECTION_KEY_INVALID | 400 | Section key not allowed for selected page |
CONTENT_SECTION_PAYLOAD_INVALID | 400 | Payload schema invalid (Tiptap/URL/shape/constraints) |
CONTENT_SEO_NOT_FOUND | 400 | seoId not found in SEO table |
RATE_LIMIT_EXCEEDED | 429 | Admin endpoint throttling exceeded |
10. Request/Response Examples
10.1 Admin get page
GET /api/admin/content/pages/about
{
"message": "Content page fetched successfully",
"data": {
"id": 2,
"pageKey": "about",
"title": "About Page",
"seoId": null,
"seo": null,
"sections": [
{
"id": 14,
"sectionKey": "hero",
"position": 0,
"isEnabled": true,
"payloadJson": {
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "About" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Our Story" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Rooted in tradition." }] }] }
},
"createdAt": "2026-05-15T12:00:00.000Z",
"updatedAt": "2026-05-15T12:00:00.000Z"
}
],
"createdAt": "2026-05-15T12:00:00.000Z",
"updatedAt": "2026-05-15T12:00:00.000Z"
}
}10.2 Public get page
GET /api/content/pages/about
{
"message": "Content page fetched successfully",
"data": {
"id": 2,
"pageKey": "about",
"title": "About Page",
"seoId": null,
"seo": null,
"sections": [
{
"id": 14,
"sectionKey": "hero",
"position": 0,
"isEnabled": true,
"payloadJson": {
"label": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "About" }] }] },
"heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Our Story" }] }] },
"description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Rooted in tradition." }] }] }
},
"createdAt": "2026-05-15T12:00:00.000Z",
"updatedAt": "2026-05-15T12:00:00.000Z"
}
],
"createdAt": "2026-05-15T12:00:00.000Z",
"updatedAt": "2026-05-15T12:00:00.000Z"
}
}10.3 Validation failure response
PUT /api/admin/content/pages/home/sections/hero with invalid payload:
{
"payloadJson": {
"heading": "plain-string-not-tiptap"
}
}Typical response:
{
"statusCode": 400,
"errorCode": "CONTENT_SECTION_PAYLOAD_INVALID",
"message": "Invalid payload for section 'hero': ..."
}11. Frontend Integration Notes
Recommended frontend pattern:
- Resolve page key by route.
- Call
/api/content/pages/:pageKey. - If
404 CONTENT_NOT_FOUND, load local fallback content. - Use runtime feature APIs for lists:
- products for
top_picks/ category product grid - FAQ API for FAQ items
- blog API for blog cards
- products for
- Render sections in backend-provided order.
12. Environment and Runtime Notes
No new environment variables are required by the Content module itself.
Dependencies:
- database availability
- permission seed consistency
- SEO table availability for metadata linking
13. API Release Checklist
- Admin role has
Content_READ/UPDATE/DELETEpermissions. - Client page keys match backend enums exactly.
- Admin panel sends valid Tiptap JSON docs.
- Link/image URLs satisfy safe URL validation.
- Public fallback behavior for
CONTENT_NOT_FOUNDis implemented. - Mobile app uses
/api/mobile/content/pages/:pageKeyroute where needed.
14. Endpoint Summary Table
| Method | Path | Permission | Notes |
|---|---|---|---|
GET | /api/admin/content/pages/:pageKey | Content_READ | Auto-create page if missing |
PATCH | /api/admin/content/pages/:pageKey/meta | Content_UPDATE | Updates title and/or seoId |
PUT | /api/admin/content/pages/:pageKey/sections/:sectionKey | Content_UPDATE | Upserts payload + position + visibility |
DELETE | /api/admin/content/pages/:pageKey/sections/:sectionKey | Content_DELETE | Deletes existing section row |
GET | /api/content/pages/:pageKey | Public | Enabled sections only |
GET | /api/mobile/content/pages/:pageKey | Public | Mobile-composed mirror |
15. Integration Diagram
Time fields in this module are stored as timezone-aware values and should be handled as ISO-8601 instants by API consumers.
See Also
- Feature Guide: See Content Module - Feature List for coverage map and usage matrix.
- Backend Guide: See Content Module - Backend Documentation for internals and migration details.