CMS & Pages
Quark Commerce includes a headless CMS for building dynamic pages with content blocks. Pages are composed of reusable components configured through the Backoffice.
Get a Page
GET /api/storefront/pages/{slug}
X-Store-Code: us-store
X-Language-Code: en
Response:
{
"id": "guid",
"title": "Home",
"slug": "home",
"contentBlocks": [
{
"id": "guid",
"componentType": "hero-banner",
"sortOrder": 0,
"data": {
"title": "Summer Collection",
"subtitle": "Up to 50% off",
"imageUrl": "https://api.example.com/media/banners/summer.jpg",
"buttonText": "Shop Now",
"buttonLink": "/c/s-new-arrivals"
}
},
{
"id": "guid",
"componentType": "product-grid",
"sortOrder": 1,
"data": {
"title": "Featured Products",
"categoryId": "guid",
"maxItems": 8,
"columns": 4
}
},
{
"id": "guid",
"componentType": "text-block",
"sortOrder": 2,
"data": {
"content": "<p>Welcome to our store...</p>"
}
}
]
}
Content Block Types
The CMS supports various component types. Your frontend renders each type with the appropriate component:
| Component Type | Description |
|---|---|
hero-banner | Full-width banner with image, text, and CTA |
product-grid | Grid of product cards from a category |
text-block | Rich text content (HTML) |
image-gallery | Image carousel/gallery |
category-grid | Grid of category cards |
custom-html | Raw HTML block |
Rendering Strategy
Build a component registry in your frontend that maps componentType strings to UI components. This pattern allows new block types to be added without code changes — just register a new component for the type.
// Example: Component mapping (adapt to your framework)
const componentMap = {
'hero-banner': HeroBannerComponent,
'product-grid': ProductGridComponent,
'text-block': TextBlockComponent,
};
Page Resolution
Pages can be resolved by slug. The Backoffice defines which pages exist and their content. Common pages:
home— homepageabout— about us- Custom pages created by the admin
The CMS is fully headless — the API provides structured data, and your frontend is responsible for rendering. This means you have complete control over the visual output.
Typed Pages and Platform Targeting
Besides slug pages, the UI builder's page types have their own endpoints — GET /api/storefront/pages/home, /pages/category/{categoryId}, /pages/product/{productId}, /pages/brand/{brandId}, /pages/search and /pages/global. They return the page's content blocks grouped by zone (zones.main[]), each block with its type, order and data (a JSON string written by the block's editor).
Send the client platform with every page request:
GET /api/storefront/pages/home
X-Store-Code: us-store
X-Language-Code: en
X-Platform: ios
X-Platform is web (the default when the header is absent), ios, android or any other value a channel defines. A page's Platforms setting in the UI builder decides who receives it:
- no platforms selected — the page serves every platform;
- platforms selected — only those platforms receive it;
mobilecovers bothiosandandroid; - when both exist, the platform-targeted page wins over the all-platforms page. Store, customer-group and context specificity apply as before among equals.
So an app gets its own Home by creating a second Home page configuration targeted at mobile, while the web storefront keeps the general one. Responses are cached per platform.
Carousel slides may carry an optional videoUrl next to imageUrl: channels that support it play the video muted and looping over the image (its poster); all others show the image.
A carousel block may carry "height": "screen" (the editor's Full screen (opening hero)): every slide should then fill the whole screen, with the client's own header — greeting, search — laid over it rather than above it. It is meant for the first block of a page and for portrait images or video (imageUrlMobile where present). Absent or "auto" means the client's standard carousel band; a client that does not know the option simply shows the band.
Page responses are served with Cache-Control: max-age=300 and an ETag. A client that should show backoffice edits on its next refresh asks with Cache-Control: no-cache (in fetch: cache: 'no-cache') — the API then answers 304 Not Modified when nothing changed, so it costs a round trip, not a download.