Building a CMS Adapter
This guide explains how to build a server-side CMS adapter for Module7. The TYPO3 adapter is the reference implementation — this guide extracts the underlying concepts so you can apply them to any CMS or server-side environment.
When to use this approach instead of the JS embed:
| JS Embed | Server-Side Adapter | |
|---|---|---|
| SEO | No — content renders in the browser | Yes — HTML is in the initial page response |
| CMS-native editing | No | Yes — adapter reads from CMS content fields |
| Implementation effort | Low | Higher |
| CMS dependency | None | Requires server-side code per CMS |
How it works
The adapter runs on the CMS server for every page request. It fetches the rendered HTML from the Module7 server, processes the response, and injects everything into the outgoing CMS page — before the browser receives anything.
Browser request
└─ CMS handles the request
└─ Adapter calls Module7 (POST)
Headers: Integration-Key, Accept-Language, X-Original-URL, ...
Body: { mode, configOverrides, ... }
└─ Module7 renders Vue SSR → returns JSON
{ appHtml, assets, dataBlocks, metaTags }
└─ Adapter processes response:
├─ Injects appHtml into page template
├─ Adds assets (CSS/JS) to <head>
├─ Injects dataBlocks as <script> tags
└─ Updates page meta tags (title, og:image, canonical, ...)
└─ CMS sends complete HTML to browser
└─ Browser receives fully rendered page (SEO-ready)
No loader.js is needed. No client-side hydration script is required from the adapter — the Module7 response already contains everything the browser needs to make the page interactive.
Step 1: Build the request URL
The URL sent to the Module7 server determines which view and which data is rendered. It is constructed from two parts:
{module7-server-url}/{view-path}{filter-link}?{query-params}
View path and filter link come from the CMS content element configuration. Examples:
| Content type | Example URL sent to Module7 |
|---|---|
| Listing | https://module7.example.com/listing/type=Hotel/topic=family |
| Detail | https://module7.example.com/detail/id=abc123 |
| Map | https://module7.example.com/maps/type=poi |
Query parameters from the original browser request are forwarded as-is. This is what enables deep-link support — a browser URL like /listing?city=Berlin results in:
https://module7.example.com/listing/type=Hotel?city=Berlin
The Module7 server URL and your Integration Key are provided by Venus AI.
Step 2: Make the POST request
Send a POST request to the constructed URL with the following headers and body.
Required headers
| Header | Value | Notes |
|---|---|---|
Integration-Key | Your integration key | Provided by Venus AI |
Accept | application/json | Tells Module7 to return JSON, not a full HTML page |
Content-Type | application/json | |
Accept-Encoding | gzip, deflate, br | Enables compressed responses |
Accept-Language | From browser request | Falls back to de-DE if absent |
User-Agent | From browser request | Falls back to a generic browser string if absent |
X-Original-URL | Full original browser URL | Required when the adapter sits behind a reverse proxy — allows Module7 to reconstruct correct routing |
Request body
{
"mode": "server-side",
"configOverrides": {
"VIEWS.general.headline_level": "h2",
"VIEWS.general.headline": true,
"VIEWS.listing.menu": true,
"THEME.colors.light.primary": "#ffffff",
"THEME.colors.light.secondary": "#000000",
"DATA.global_filter.channels": ["channel-id-1", "channel-id-2"],
"VIEWS.listing.sort.function": "distance",
"VIEWS.listing.sort.order": "asc",
"VIEWS.listing.sort.latitude": "48.373",
"VIEWS.listing.sort.longitude": "10.889"
}
}
All fields in the body are optional. Send an empty object {} if no overrides are needed.
configOverrides allows the adapter to pass per-element configuration that overrides the defaults from the Integration Key — useful for things like theming, channel scoping, or sorting per content element.
Recommended timeouts
| Timeout type | Recommended value |
|---|---|
| Connection timeout | 2 seconds |
| Total request timeout | 20 seconds |
Step 3: Process the response
A successful response is a JSON object with four fields:
{
"appHtml": "<div data-module7-root ...>...</div>",
"assets": [
{ "src": "https://module7-server/module7/assets/main.css", "mime": "text/css" },
{ "src": "https://module7-server/module7/entries/app.mjs", "mime": "text/javascript" }
],
"dataBlocks": [
{ "hash": "abc123", "data": { ... } }
],
"metaTags": [
{ "name": "og:title", "content": "Hotel Beispiel" },
{ "name": "og:image", "content": "https://..." },
{ "name": "description", "content": "..." },
{ "name": "relative-canonical", "content": "/detail/id=abc123" }
]
}
appHtml — inject into the page
Inject the value of appHtml raw (unescaped) into your page template at the position where Module7 content should appear. Wrap it in a container element so your CMS can style or position it:
<div class="module7-container">
<!-- appHtml goes here, rendered without escaping -->
</div>
assets — add to <head>
For each entry in assets, add the corresponding tag to the page <head>. Deduplicate by src to avoid loading the same asset twice if multiple Module7 elements appear on the same page.
mime: text/css → <link rel="stylesheet" href="{src}">
mime: text/javascript → <script type="module" src="{src}"></script>
dataBlocks — inject as script tags
Data blocks carry the serialized Vue SSR state. Inject each as a <script> tag in the page body. Deduplicate by hash:
<script type="application/json" data-module7-data-block>
<!-- JSON.stringify(dataBlock.data) -->
</script>
These blocks must be present in the page HTML for the Vue client app to hydrate correctly. Without them, the client app will re-fetch data from the gateway unnecessarily.
metaTags — update page head
Apply meta tags returned by Module7 to the page. The following names have defined semantics:
| Name | Action |
|---|---|
og:title | Set the page <title> and og:title meta tag |
og:image | Set the og:image meta tag |
description | Set the description meta tag |
relative-canonical | Convert to absolute URL and set as canonical <link> |
Step 4: Handle special response codes
| HTTP status | Meaning | Recommended handling |
|---|---|---|
200 | Normal — process response as described above | — |
204 | No content (e.g. entity not found) | Render nothing, do not show an error |
| Any other | Server or configuration error | Show an error message or return a 502 |
If the response body is not valid JSON, treat it as a server error.
Step 5: Caching
The Module7 server already caches SSR renders internally, but adding a cache layer in the adapter reduces latency and avoids redundant network calls to the Module7 server on every page request.
Cache key
Include everything that influences the rendered output:
{page-id}_{content-element-id}_{hash(url + request-body)}
Hashing the full URL (including query parameters) and the request body (including configOverrides) ensures that different filter states and configurations produce separate cache entries.
TTL
A TTL of 600 seconds (10 minutes) is a reasonable default. This can be made configurable per content element — for example, form elements should always bypass the cache to ensure fresh rendering.
Invalidation
Tag cache entries with the CMS page ID so they can be bulk-invalidated when an editor clears the page cache:
cache tags: ["page_{page-id}"]
Routing and deep-link support
For deep-linked URLs to work (e.g. /detail/id=abc123 or /listing?city=Berlin), the CMS must be able to serve a page at those URLs. The adapter then forwards the path and query string to Module7 when constructing the request URL.
If your CMS uses URL routing that rewrites or normalises paths before the adapter runs, set the X-Original-URL header to the full original browser URL. Module7 uses this to reconstruct the correct internal routing context.
X-Original-URL: https://www.example.com/listing?city=Berlin&topic=museums
For static embeds where deep-link support is not required, the URL can be fully constructed from the content element configuration without reading the browser URL at all — the view is fixed and filters are static.