Skip to main content

Server-rendered page components (iframe SSR)

A page component is an <iframe> of one of your own pages. By default your host app creates that iframe in the browser, and only after three things have happened in order:

  1. your app's JavaScript has loaded and rendered,
  2. the SDK has fetched the component's config from cloud.myop.dev/flow,
  3. the SDK has created the <iframe>, and the page inside it starts loading.

On a client-rendered page (for example a Next.js page with ssr: false), steps 1–2 can take seconds before the page inside the frame even starts. This page shows how to put the <iframe> in your server's HTML instead, so the browser starts loading it with the first bytes, and how to give the SDK the config your server already has.

Measured on a real customer page shape (Next.js host, client-only page component, a WordPress page inside the iframe; 5 cold loads each, median):

widget visiblefully loaded
config fetched in the browser, iframe created after hydration (default)3,603 ms4,511 ms
iframe in the server HTML + hydrateV1Components1,723 ms2,640 ms

The iframe loads once, and the browser makes no request to /flow.

Requires @myop/sdk 0.4.0-beta.6 or later

hydrateV1Components and adopting a server-rendered iframe are new in 0.4.0-beta.6, published on the beta tag. Install the exact version:

npm install @myop/sdk@0.4.0-beta.6

Pin it exactly (no ^, not @beta) so a later beta cannot arrive unannounced.

How it works​

your server                           browser
─────────── ───────
GET /flow?id=…&auto=true&prefetch=1 (cached ~60 s on your server)
render <iframe src="…"> ───────────► starts loading the page immediately
embed the config as JSON ───────────► hydrateV1Components({ [id]: config })
loadComponent(config, theIframe) → adopts it, no reload
show() when it resolves
  • prefetch=1 tells Myop the request comes from your server, not from a visitor, so it is not counted as a view. The view is reported by the browser when it actually renders the component.
  • for=<page URL> (optional, recommended) tells Myop which of your pages the config is for. Without it a server-side request carries no page information. Only the origin and path are stored — the query string is dropped.
  • Adopt, don't reload. When you pass an <iframe> that already shows the component's URL, the SDK keeps it as it is. A trailing-slash difference (/page vs /page/) counts as the same page. An iframe that points somewhere else, or has no src, is navigated as before.

Step 1 — fetch the config on your server​

Fetch /flow once per component and cache it. The config only changes when you publish the component, so a 60-second cache is a good default.

lib/myop-config.ts (server only)
const cache = new Map<string, { at: number; flow: any }>();

export async function getMyopV1Config(componentId: string, pageUrl: string) {
const hit = cache.get(componentId);
if (hit && Date.now() - hit.at < 60_000) return hit.flow;

const url = `https://cloud.myop.dev/flow?id=${componentId}&auto=true&prefetch=1&for=${encodeURIComponent(pageUrl)}`;
const flow = await (await fetch(url)).json();
cache.set(componentId, { at: Date.now(), flow });
return flow;
}

/** The URL the iframe should show: the component's URL plus your own query parameters. */
export function iframeUrl(flow: any, params: Record<string, string> = {}) {
const type = flow.item.components[0].type;
const u = new URL(type.skins[type.defaultSkin ?? 0].loader.url);
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
return u.toString();
}

Step 2 — render the <iframe> in the server HTML​

Render the iframe in a part of the page that is server-rendered. In Next.js, that means outside any dynamic(..., { ssr: false }) component.

pages/info-page/[pageId].tsx (Next.js pages router)
import dynamic from 'next/dynamic';
import { getMyopV1Config, iframeUrl } from '../../lib/myop-config';

const PageClient = dynamic(() => import('../../components/PageClient'), { ssr: false });

export async function getServerSideProps({ params, req, resolvedUrl }) {
const page = await loadPage(params.pageId); // your own data
const pageUrl = `https://${req.headers.host}${resolvedUrl}`;
const myopFlow = await getMyopV1Config(page.component_id, pageUrl);
const myopSrc = iframeUrl(myopFlow, { shaliachId: String(page.shaliachId) });
return { props: { page, myopFlow, myopSrc } };
}

export default function InfoPage({ page, myopFlow, myopSrc }) {
return (
<>
{/* server-rendered: the browser starts loading this with the first bytes of the HTML */}
<iframe id="myop-widget" src={myopSrc} style={{ width: '100%', height: 900, border: 0 }} />
<PageClient page={page} myopFlow={myopFlow} />
</>
);
}

Step 3 — hand the config to the SDK and adopt the iframe​

On the client, seed the config before asking for the component, then pass the server-rendered iframe as the container.

components/PageClient.tsx
import { useEffect } from 'react';
import { hostSDK } from '@myop/sdk/host';
import { CloudRepository, hydrateV1Components } from '@myop/sdk/helpers';

export default function PageClient({ page, myopFlow }) {
useEffect(() => {
hostSDK.init(); // once per page; a no-op if already done
hydrateV1Components({ [page.component_id]: myopFlow }); // no /flow request from the browser

(async () => {
const config = await CloudRepository.Main.fetchComponent(page.component_id); // answered from memory
const iframe = document.getElementById('myop-widget') as HTMLIFrameElement;
const component = await hostSDK.loadComponent(config, iframe, { timeout: 30000 });
component?.show?.();
})();
}, []);
// ...the rest of your page
}

If you already append your own query parameters to loader.url before loadComponent (for example a shaliachId), keep doing it — as long as the result equals the iframe's src, the frame is adopted.

Without a bundler (<script> tag)​

The CDN build exposes the same function as myop.hydrateV1:

<script>window.__MYOP_FLOW__ = /* the JSON your server fetched */;</script>
<script>
myop.hydrateV1({ '<componentId>': window.__MYOP_FLOW__ });
</script>

Things to know​

  • Visible from the first paint. The SDK only hides iframes it creates itself (hidden: true). An iframe your server rendered is visible while it loads. If you want a placeholder until the component is ready, style the iframe yourself (for example opacity: 0) and reveal it when loadComponent resolves.
  • Every view is still counted. A hydrated view is reported by the browser in a small batched request when the page is hidden (/clientUsage), and appears in your usage like any other view. Your server's prefetch=1 requests are recorded separately and never count as views.
  • Publishing a change. With a 60-second server cache, a newly published component reaches visitors within a minute. Clear your cache if you need it sooner.
  • One config per component id. hydrateV1Components takes several: { [idA]: flowA, [idB]: flowB }.
  • No server? Everything here is optional. Without it the SDK behaves exactly as before: it fetches /flow in the browser and creates the iframe itself.

The page inside the iframe​

The page you embed loads the Myop iframe SDK too. Load it async, in <head>, so the component can talk to your host as early as possible instead of waiting for every script above it:

<script async src="https://cdn.myop.dev/sdk/next/myop_sdk.min.js"
onload="(async()=>{const {IframeSDK}=await window.myop.rootSDK.getIframeModule();new IframeSDK().init();})()"></script>

On the measured page this alone made the widget visible ~23% sooner. Serving that page from a page cache (it is usually identical for every visitor) gave another ~23%. Avoid "defer all JavaScript" options for this page: they delay the SDK too, and the widget appeared later (+24%) in the same test.

See CDN: versions and caching for pinning the SDK to an exact version.