Skip to main content

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 EmbedServer-Side Adapter
SEONo — content renders in the browserYes — HTML is in the initial page response
CMS-native editingNoYes — adapter reads from CMS content fields
Implementation effortLowHigher
CMS dependencyNoneRequires 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 typeExample URL sent to Module7
Listinghttps://module7.example.com/listing/type=Hotel/topic=family
Detailhttps://module7.example.com/detail/id=abc123
Maphttps://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
info

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

HeaderValueNotes
Integration-KeyYour integration keyProvided by Venus AI
Acceptapplication/jsonTells Module7 to return JSON, not a full HTML page
Content-Typeapplication/json
Accept-Encodinggzip, deflate, brEnables compressed responses
Accept-LanguageFrom browser requestFalls back to de-DE if absent
User-AgentFrom browser requestFalls back to a generic browser string if absent
X-Original-URLFull original browser URLRequired 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.

Timeout typeRecommended value
Connection timeout2 seconds
Total request timeout20 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:

NameAction
og:titleSet the page <title> and og:title meta tag
og:imageSet the og:image meta tag
descriptionSet the description meta tag
relative-canonicalConvert to absolute URL and set as canonical <link>

Step 4: Handle special response codes

HTTP statusMeaningRecommended handling
200Normal — process response as described above
204No content (e.g. entity not found)Render nothing, do not show an error
Any otherServer or configuration errorShow 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}"]

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.