---
sidebar_position: 13
title: "Server-rendered page components (iframe SSR)"
sidebar_label: Page components — SSR
slug: /learn/page-component-ssr
description: "Render a Myop page component's <iframe> on your server so it starts loading with the first bytes of your HTML, hand the SDK the config your server already fetched with hydrateV1Components, and let it adopt the frame without reloading it. Every view is still counted."
keywords: [myop iframe ssr, page as component ssr, hydrateV1Components, myop.hydrateV1, adopt iframe, prefetch=1, myop nextjs iframe, server render iframe, v1 component performance, flow auto]
---
# Server-rendered page components (iframe SSR)

A [page component](/docs/learnMyop/PageAsComponent) 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 visible | fully loaded |
|---|---:|---:|
| config fetched in the browser, iframe created after hydration (default) | 3,603 ms | 4,511 ms |
| **iframe in the server HTML + `hydrateV1Components`** | **1,723 ms** | **2,640 ms** |

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

:::info 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:

```bash
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.

```ts title="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.

```tsx title="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.

```tsx title="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`:

```html
<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:

```html
<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](/docs/troubleshooting/cdn-chunk-loading#versions-and-caching) for pinning the
SDK to an exact version.
