Happy House - Ecommerce Docs
Developer ResourcesContent

Content Module API & Integration Guide

Admin and public contracts for section-wise CMS payloads with Tiptap JSON validation for Happy House.

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()
  • Base routes:
    • Admin: /api/admin/content/pages
    • Public: /api/content/pages
    • Mobile mirror: /api/mobile/content/pages
  • 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/:pageKey and GET /api/mobile/content/pages/:pageKey use public keyspace content:page:public:
  • GET /api/admin/content/pages/:pageKey uses admin keyspace content:page:admin:

TTL envs:

  • CONTENT_PAGE_PUBLIC_CACHE_TTL_SECONDS (default 300)
  • CONTENT_PAGE_ADMIN_CACHE_TTL_SECONDS (default 120)

Invalidation:

  • successful PATCH /meta, PUT /sections/:sectionKey, and DELETE /sections/:sectionKey invalidate 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

  • home
  • about
  • faq
  • privacy_policy
  • terms_of_service
  • return_policy
  • global_footer
  • global_header
  • contact
  • blogs

global_header is the site's top utility bar — the contact points, the promotional strip and the utility links. It exists so that bar has an owner; before it, every value in it was hard-coded in the storefront.

terms_of_service is declared but deliberately unseeded: it needs approved legal text, and an invented terms page is worse than an absent one.

2.2 Section keys

  • hero
  • how_it_works
  • faq_intro
  • related_blogs_intro
  • cta_banner
  • quote
  • story_blocks
  • why_choose
  • document
  • data_flow_summary
  • brand_blurb
  • quick_links
  • legal_links
  • copyright
  • contact_info
  • contact_form
  • about_stats
  • our_mission
  • announcement_bar

2.3 Page-to-section compatibility

Page keyAllowed sections
homehero, how_it_works, faq_intro, related_blogs_intro, cta_banner
abouthero, how_it_works, about_stats, why_choose, our_mission
faqhero, faq_intro
privacy_policydocument, data_flow_summary
terms_of_servicedocument
return_policydocument
global_footerbrand_blurb, quick_links, contact_info, legal_links, copyright
global_headerannouncement_bar, contact_info, quick_links
contacthero, contact_info, contact_form
blogshero

The header reuses the footer's contact_info and quick_links rather than declaring header-only equivalents. The map above is a per-page allowlist, so reuse costs nothing and a section behaves identically wherever it appears.

Social icons are deliberately absent from global_header. They are owned by the config module's social-links surface, which the footer already renders from; a second copy here would be two places to change one set of links.

Invalid pair behavior:

  • HTTP 400
  • errorCode CONTENT_SECTION_KEY_INVALID

3. Admin Endpoints

3.1 Get page (admin)

AspectValue
MethodGET
Path/api/admin/content/pages/:pageKey
AuthJwtAuthGuard + RoleGuard
PermissionContent_READ
Throttle30/minute
ResponseResponseDto<ContentPageResponseDto>

Behavior:

  • auto-creates page if missing (with module default title)
  • returns all sections, including isEnabled=false

3.2 Update page meta

AspectValue
MethodPATCH
Path/api/admin/content/pages/:pageKey/meta
PermissionContent_UPDATE
Throttle10/minute
BodyUpdateContentPageMetaDto
ResponseResponseDto<ContentPageResponseDto>

Body fields:

  • title?: string (max 255)
  • seoId?: string | null (UUID or null)

SEO validation:

  • if non-null seoId does not exist -> 400 CONTENT_SEO_NOT_FOUND

3.3 Upsert section

AspectValue
MethodPUT
Path/api/admin/content/pages/:pageKey/sections/:sectionKey
PermissionContent_UPDATE
Throttle10/minute
BodyUpsertContentSectionDto
ResponseResponseDto<ContentSectionResponseDto>

Body fields:

  • payloadJson: Record<string, unknown> (required)
  • position?: number (default 0, min 0)
  • isEnabled?: boolean (default true)

Behavior:

  • validates sectionKey compatibility with pageKey
  • validates payloadJson against section schema
  • auto-creates page if missing
  • upserts by unique (pageId, sectionKey)

3.4 Delete section

AspectValue
MethodDELETE
Path/api/admin/content/pages/:pageKey/sections/:sectionKey
PermissionContent_DELETE
Throttle10/minute
ResponseResponseDto<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)

AspectValue
MethodGET
Path/api/content/pages/:pageKey
AuthPublic
ResponseResponseDto<ContentPageResponseDto>

Behavior:

  • does not auto-create pages
  • missing page -> 404 CONTENT_NOT_FOUND
  • returns only enabled sections
  • ordered by position ASC, then id ASC

4.2 Get page (mobile-composed)

AspectValue
MethodGET
Path/api/mobile/content/pages/:pageKey
AuthPublic
ResponseResponseDto<ContentPageResponseDto>

Behavior is identical to /api/content/pages/:pageKey.

5. DTO Contract

5.1 ContentPageResponseDto

{
  "id": 1,
  "pageKey": "home",
  "title": "Home Page",
  "seoId": null,
  "seo": null,
  "sections": [],
  "createdAt": "2026-07-23T12:00:00.000Z",
  "updatedAt": "2026-07-23T12:15:00.000Z"
}

5.2 ContentSectionResponseDto

{
  "id": 11,
  "sectionKey": "hero",
  "position": 0,
  "isEnabled": true,
  "payloadJson": {
    "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Happy House" }] }] },
    "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Furniture and homeware for every room." }] }] }
  },
  "createdAt": "2026-07-23T12:00:00.000Z",
  "updatedAt": "2026-07-23T12: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:

  • type must be doc
  • content must be non-empty

6.2 URL fields

Allowed:

  • https://...
  • http://...
  • /relative-path

Rejected:

  • ftp://...
  • javascript:...
  • empty strings

6.3 Field-size constraints

Field classLimit
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 alt255 chars
iconKey80 chars

7. Section Payload Schemas and Examples

All examples below are valid payloadJson values for PUT /sections/:sectionKey.

7.1 hero

Required:

  • heading (Tiptap)
  • description (Tiptap)

No images, no CTA, no label — minimal hero across pages.

{
  "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Happy House" }] }] },
  "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Furniture and homeware for every room." }] }] }
}

7.2 how_it_works

Required:

  • heading (Tiptap)
  • steps[] length 1..12

Each step:

  • title (Tiptap)
  • description (Tiptap)
  • optional icon (string)

Optional:

  • description (Tiptap)
{
  "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "How It Works" }] }] },
  "steps": [
    {
      "title": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Enter Letters" }] }] },
      "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Type your rack of letters." }] }] }
    }
  ]
}

7.3 faq_intro

Required:

  • heading (Tiptap)
  • description (Tiptap)

Optional:

  • label (Tiptap)

FAQ items are fetched from the backend FAQs API — the CMS only stores the intro copy.

{
  "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Frequently Asked Questions" }] }] },
  "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Everything you need to know about shopping with Happy House." }] }] }
}

Required:

  • heading (Tiptap)

Blog data is fetched from the backend blogs API.

{
  "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "From Our Blog" }] }] }
}

7.5 cta_banner

Required:

  • title (Tiptap)
  • description (Tiptap)
  • ctaLabel (string)
  • href (safe URL)
{
  "title": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Ready to Furnish Your Home?" }] }] },
  "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Browse our latest furniture and homeware collection." }] }] },
  "ctaLabel": "Shop Now",
  "href": "/sofas"
}

7.6 quote

Required:

  • text (Tiptap)

Optional:

  • author (Tiptap)
{
  "text": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "A house is made of walls and beams; a home is built with love and dreams." }] }] },
  "author": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Anonymous" }] }] }
}

7.7 story_blocks

Required:

  • blocks[] length 1..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": "Our Story" }] }] },
      "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Built for Homemakers" }] }] },
      "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Happy House started as a small showroom for people who care about their homes." }] }] },
      "image": { "src": "https://cdn.example.com/about-story.webp", "alt": "About us" }
    }
  ]
}

7.8 why_choose

Required:

  • heading (Tiptap)
  • cards[] length 1..12

Card schema:

  • title (Tiptap)
  • description (Tiptap)
  • iconKey (string)

Optional:

  • label (Tiptap)
  • description (Tiptap)
{
  "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Why Choose Us" }] }] },
  "cards": [
    {
      "title": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Wide Selection" }] }] },
      "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Thousands of furniture and homeware products in stock." }] }] },
      "iconKey": "catalog"
    }
  ]
}

Used by:

  • privacy_policy
  • terms_of_service

Required:

  • eyebrow (string)
  • title (string)
  • summary (string)
  • lastUpdated (string)
  • sections[] (length 1..50)

sections[] item:

  • id (kebab-case string)
  • title (string)
  • paragraphs[] (length 1..20)
  • optional bullets[] (length 1..40)
{
  "eyebrow": "Privacy",
  "title": "Privacy Policy",
  "summary": "We respect your privacy and protect your personal information.",
  "lastUpdated": "Last updated: July 20, 2026",
  "sections": [
    {
      "id": "information-we-collect",
      "title": "1. Information We Collect",
      "paragraphs": [
        "We collect account and usage data to improve our services."
      ],
      "bullets": [
        "Email address",
        "Order history",
        "Device information"
      ]
    }
  ]
}

7.10 data_flow_summary

Required:

  • items[] length 1..20

Each item:

  • title (string)
  • description (string)
{
  "items": [
    { "title": "Collection", "description": "We collect only data you provide." },
    { "title": "Processing", "description": "Data is processed to deliver order and account features." }
  ]
}

brand_blurb

Required:

  • logo: { src, alt }
  • description (Tiptap)
{
  "logo": { "src": "/logo.svg", "alt": "Happy House" },
  "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Furniture and homeware, delivered to your door." }] }] }
}

Required:

  • columns[] length 1..10

Column: title (string) + links[] each { label, href }

{
  "columns": [
    {
      "title": "Shop",
      "links": [
        { "label": "Sofas", "href": "/sofas" },
        { "label": "Living Room", "href": "/living-room" }
      ]
    }
  ]
}

Used on global_footer and on global_header. Shape: title + contacts[] each { label, href?, icon }, plus optional address and openingHours.

icon is a two-value enum, phone | email. That is why the address and the opening hours are their own fields rather than more contact rows — there is no icon for them, and an address is not a contact method.

address is a single free-text block rather than structured lines, because its consumers are a rendered paragraph and a map query string, neither of which wants components. The embedded map derives its URL entirely from this string, so setting it is also what makes the map editable.

openingHours is capped at seven rows: it describes a week.

Both new fields are optional, and deliberately so — every footer payload seeded before they existed remains valid, where a required field would have failed every render for one cache TTL after deploy.

{
  "title": "Contact Us",
  "contacts": [
    { "label": "Email", "href": "https://example.com/contact", "icon": "email" },
    { "label": "Phone", "icon": "phone" }
  ],
  "address": "New Road, Kathmandu 44600",
  "openingHours": [
    { "label": "Sun – Fri", "value": "10:00 – 19:00" },
    { "label": "Saturday", "value": "Closed" }
  ]
}

href accepts absolute http(s) or site-relative URLs only. tel: and mailto: are rejected by safeUrlSchema, so a renderer builds those from the label — which is why the seeded rows carry no href.

contact_info (contact-page variant)

Used on contact page. Shape: email, phone, responseTime.

{
  "email": "support@happyhouse.example",
  "phone": "+1-800-555-0199",
  "responseTime": "Within 24 hours"
}

contact_info is a union of the two shapes above, and the validator picks an arm by trying both — it does not know which page it is validating. The section is allowed on global_footer, global_header and contact, so either shape validates on any of them.

A renderer must branch on the shape it receives rather than assume contacts exists. Discriminating the union per page would be the stricter model, but it changes validation for already-seeded contact rows, which is a content migration rather than a schema change.

announcement_bar

Used on global_header. The promotional strip across the top of the site.

{
  "text": "Free delivery on orders over Rs. 50,000",
  "href": "/shipping",
  "isDismissible": true
}
FieldRules
textrequired, 1–160 chars, plain text
hrefoptional, absolute http(s) or site-relative
isDismissibleoptional boolean, a rendering hint

Plain text rather than a Tiptap document: the strip renders on one line inside a fixed-height bar, and a rich-text field there would let an operator insert a heading or a list the bar cannot lay out.

href is optional because the strip is usually a statement, not a link — requiring one would force an operator to invent a destination for a sentence.

This section states an offer; it does not create one. There is no free-delivery threshold anywhere in cart, checkout or shipping — shipping_rate carries only district, fee and an active flag, and free shipping exists solely as a FREESHIP coupon type. Nothing reads this text to decide a price.

To end a campaign, disable the section (isEnabled on the section row) rather than deleting it — deleting loses the copy.

Required:

  • links[] length 1..40, each { label, href }
{
  "links": [
    { "label": "Privacy Policy", "href": "/privacy-policy" },
    { "label": "Terms of Service", "href": "/terms-of-service" }
  ]
}

Required:

  • text (Tiptap)
{
  "text": {
    "type": "doc",
    "content": [
      { "type": "paragraph", "content": [{ "type": "text", "text": "© 2026 Happy House. All rights reserved." }] }
    ]
  }
}

7.12 contact_form

Required:

  • heading (Tiptap)
  • description (Tiptap)
  • submitLabel (string)
{
  "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Send Us a Message" }] }] },
  "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "We'd love to hear from you." }] }] },
  "submitLabel": "Send"
}

7.13 about_stats

Required:

  • stats[] length 1..20, each { value, label }
{
  "stats": [
    { "value": "10K+", "label": "Products in Catalog" },
    { "value": "7", "label": "Warehouses Nationwide" }
  ]
}

7.14 our_mission

Required:

  • title (string)
  • paragraphs[] (Tiptap list) length 1..10
{
  "title": "Our Mission",
  "paragraphs": [
    { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Make furnishing your home easy, affordable, and enjoyable." }] }] }
  ]
}

8. End-to-End Flow Examples

8.1 Initial admin authoring flow

  1. Admin fetches page:
    • GET /api/admin/content/pages/home
  2. Page row is auto-created if missing.
  3. Admin writes each section with PUT /sections/:sectionKey.
  4. Frontend can consume from public endpoint immediately.

8.2 SEO attach/clear flow

Attach:

  • PATCH /api/admin/content/pages/home/meta with {"seoId": "<uuid>"}

Clear:

  • PATCH /api/admin/content/pages/home/meta with {"seoId": null}

8.3 Hide section flow

Update section:

  • PUT /api/admin/content/pages/home/sections/hero with isEnabled: false

Result:

  • section still visible in admin page response
  • section omitted from public/mobile response

9. Error Code Mapping

errorCodeTypical HTTPCause
CONTENT_NOT_FOUND404Missing page on public read, or missing page/section on delete
CONTENT_SECTION_KEY_INVALID400Section key not allowed for selected page
CONTENT_SECTION_PAYLOAD_INVALID400Payload schema invalid (Tiptap/URL/shape/constraints)
CONTENT_SEO_NOT_FOUND400seoId not found in SEO table
RATE_LIMIT_EXCEEDED429Admin 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": {
          "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "About Happy House" }] }] },
          "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Our journey in building a better home shopping experience." }] }] }
        },
        "createdAt": "2026-07-23T12:00:00.000Z",
        "updatedAt": "2026-07-23T12:00:00.000Z"
      }
    ],
    "createdAt": "2026-07-23T12:00:00.000Z",
    "updatedAt": "2026-07-23T12: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": {
          "heading": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "About Happy House" }] }] },
          "description": { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Our journey in building a better home shopping experience." }] }] }
        },
        "createdAt": "2026-07-23T12:00:00.000Z",
        "updatedAt": "2026-07-23T12:00:00.000Z"
      }
    ],
    "createdAt": "2026-07-23T12:00:00.000Z",
    "updatedAt": "2026-07-23T12: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:

  1. Resolve page key by route.
  2. Call /api/content/pages/:pageKey.
  3. If 404 CONTENT_NOT_FOUND, load local fallback content.
  4. Use runtime feature APIs for lists:
    • FAQ API for FAQ items
    • blog API for blog cards
    • banner API for ad placements
  5. 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/DELETE permissions.
  • 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_FOUND is implemented.
  • Mobile app uses /api/mobile/content/pages/:pageKey route where needed.

14. Endpoint Summary Table

MethodPathPermissionNotes
GET/api/admin/content/pages/:pageKeyContent_READAuto-create page if missing
PATCH/api/admin/content/pages/:pageKey/metaContent_UPDATEUpdates title and/or seoId
PUT/api/admin/content/pages/:pageKey/sections/:sectionKeyContent_UPDATEUpserts payload + position + visibility
DELETE/api/admin/content/pages/:pageKey/sections/:sectionKeyContent_DELETEDeletes existing section row
GET/api/content/pages/:pageKeyPublicEnabled sections only
GET/api/mobile/content/pages/:pageKeyPublicMobile-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