TL;DR
When you’re trying to integrate Builder.io CMS content inside an existing website using SSR technology (e.g. PHP), you can use a micro frontend setup using modern JavaScript technology: include the CMS content inside an <iframe>
tag inside your existing (PHP) page and include your CMS content using an invisible iframe.
To render the CMS content at full scrollHeight
, AFAIK, there is no “content completely rendered” event yet in a Builder Content component, but you can use browser events to emulate this functionality, see the steps below.
Steps
As in my original post, the main page is an existing PHP webpage (or similar SSR technologies). The Builder CMS content page is built separately using modern JavaScript technology (in this case a Vue.js page, but React, Svelte, … pages will work too).
In your PHP page, add the following JS code inside a <script>
tag:
<!DOCTYPE html>
<html lang="nl">
<head>
<script type="text/javascript">
window.addEventListener("message", (e) => {
const data = e.data;
if (data.msg == 'postScrollHeight') {
console.log(`postScrollHeight ${data.scrollHeight} received at ${Date.now()}`)
document.getElementById("ifrmBuilder").height = height;
}
});
</script>
<style>
iframe {
display: block;
background: #FFF;
margin: 0;
border: none; /* Reset default border */
width: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div>
<div class="container-fluid pt-md-2">
<div class="p-1">
<iframe id="ifrmBuilder" src="http://localhost:5173" scrolling="no" height="100%"></iframe>
</div>
</div>
</div>
</body>
</html>
You’ll notice the Vue.js page with the Builder CMS content is served from the default npm run dev
url http://localhost:5173
.
In your Vue.js file App.vue
, Builder’s sdk-vue
is used to display the CMS content:
<script setup>
import { Content, fetchOneEntry, isPreviewing, getBuilderSearchParams } from '@builder.io/sdk-vue';
import { onMounted, ref } from 'vue';
// polyfill ResizeObserver on older Apple Safari browsers
import ResizeObserverPF from 'resize-observer-polyfill';
// import debounce composable from @vueuse core lib
import { useDebounceFn } from '@vueuse/core';
import MyCounter from './components/MyCounter.vue';
// ponyfill ResizeObserver on Safari < 13.1 (don't polyfill on modern browsers)
if (!window.ResizeObserver) {
console.log('ResizeObserver ponyfilled!')
window.ResizeObserver = ResizeObserverPF
}
const content = ref(null);
const apiKey = '<your api key>';
const canShowContent = ref(false);
const model = '<your Builder content model>';
// an example Vue.js custom component MyCounter will show up in Builder's drag & drop Visual Editor:
const customComponents = [
{
component: MyCounter,
name: 'My Counter',
inputs: [
{
name: 'title',
type: 'string'
}
],
}
]
// "debounce" callbacks for 100 ms to avoid the parent iframe resizing multiple times during async CMS content rendering
// send a message to the parent page window
const debouncedResizeFn = useDebounceFn((entries) => {
for (const entry of entries) {
console.log(`resize scrollHeight: ${entry.target.scrollHeight} at ${Date.now()}`)
if (window.parent) {
window.parent.postMessage({
msg: 'postScrollHeight',
scrollHeight: entry.target.scrollHeight
}, '*')
}
}
}, 100)
onMounted(async () => {
content.value = await fetchOneEntry({
model,
apiKey,
options: getBuilderSearchParams(
new URL(location.href).searchParams
),
userAttributes: {
urlPath: window.location.pathname,
},
});
// create a ResizeObserver instance to trap body resize events when the CMS content is rendered
const resizeObserver = new ResizeObserver(debouncedResizeFn);
// observe the document body of the Vue.js app
resizeObserver.observe(document.body);
canShowContent.value = content.value ? true : isPreviewing();
});
</script>
<template>
<h1>Builder.io CMS Vue demo</h1>
<Content
v-if="canShowContent"
:model="model"
:content="content"
:api-key="apiKey"
:customComponents="customComponents"
/>
<div v-else>
Content loading (or loading failed) ...
</div>
</template>
<style scoped>
</style>
Btw, if you’re wondering why I’m using a ResizeObserver
instance: you can’t use Vue’s nextTick()
method because the Content component is still rendering the content asynchronously (changing the scrollHeight
) after the fetchOneEntry()
is complete and setting canShowContent.value
.
FYI, the very simple MyCounter
example component:
<script setup>
import { ref } from 'vue'
defineProps({
title: {
type: String,
required: true
}
})
const count = ref(0)
</script>
<template>
<div class="bold">{{ title }} is: {{ count }}</div>
</template>
<style scoped>
.bold {
font-weight: bold;
}
</style>
This Micro Frontend integration method using iframes can help in gradually modernizing existing websites using other technologies: the CMS content can use all functionality of the latest JS tech stacks & you can include the JS app in existing webpages. Using windows.postMessage()
, you can communicate between the parent page and the iframe container.