How it works Examples Docs Pricing Blog WP Blocks Install free

Blog /

Iframe vs inline: picking the right isolation mode for your DSGo App

Every DSGo App declares an isolation field in its manifest. The two values are iframe and inline. Most readers default to iframe (it’s the starter default), build something useful, and never think about the choice again. That’s fine. But there’s a class of apps where inline is the right answer, and missing that flips a “tool I added to my site” into “a real page my customers find on Google.”

Take the simplest example: a calculator built in iframe mode lives at /apps/your-calculator/, but the document the visitor’s browser renders at that URL is a thin parent page mounting the calculator inside a sandboxed iframe. View source on the URL: not much HTML. Crawler-friendly? No. Indexable? No. If you want Google to surface the calculator for searches that match it, iframe mode is the wrong call. Switching to inline mode is one field change in dsgo-app.json; after that, the calculator is rendered server-side as part of the WordPress page response and the crawler sees full HTML.

This post is the trade table for that decision: when iframe is the right call (most of the time), when inline is the right call (whenever the page needs to be findable), and the small handful of constraints in each mode that, if you don’t know about them, will surprise you.

What iframe actually does

isolation: "iframe" is the original DSGo runtime, and the starter scaffold’s default. The app’s bundle gets served at /apps/{slug}/, but the document the visitor’s browser renders at that URL is a thin parent page that mounts your bundle inside an <iframe sandbox="allow-scripts" csp="…">. The browser treats the iframe as an opaque-origin context. Your app cannot reach into the parent page. The parent page cannot reach into your app. The two communicate only over the postMessage bridge described in BRIDGE-API.md.

What you get:

  • Strong isolation. A misbehaving app crashes its own iframe and nothing else. Memory leaks, infinite loops, third-party scripts that throw, none of it touches the rest of the site. If an app you generated with Claude turns out to have a runaway setInterval, the worst case is the visitor closes the tab.
  • A clean security story for the install dialog. Permissions are gated at the bridge boundary. Outside of approved scopes there is nothing your app can reach, and the install dialog’s permission buckets reflect the actual capability surface, not a wishful description.
  • Predictable rendering. The iframe has its own document, its own CSS scope, its own JavaScript globals. No theme styles bleed in. No other plugin’s wp_enqueue_script lands inside your runtime. Your <style> block does what you said.
  • First-class fit for tools and widgets. Calculators, configurators, dashboards, internal admin utilities, anything an authenticated user opens, uses, and closes.

What you give up:

  • The parent page is mostly empty HTML. Search crawlers see an iframe shell, not your content. If discoverability matters, iframe is the wrong call.
  • Theme integration is opt-in only. Your iframe does not inherit the WP theme’s typography, colors, or chrome. You ship that yourself, or accept that the app looks like an embedded tool sitting inside the page.
  • Tab title and meta come from the parent page. Iframe-mode apps cannot set their own <title> or social meta tags directly. You set those on the wrapper, or you accept whatever the wrapper defaults to.
  • Some integrations are awkward. A visitor’s “back” button operates on the parent page, not the iframe. Anchor links inside the iframe don’t update the parent URL. If those matter to your UX, you reach for dsgo.router.* and do explicit work; in inline mode they’d be free.

What inline does

isolation: "inline" is the second mode. Instead of mounting your bundle in an iframe, the plugin server-renders your app’s HTML directly into the WordPress page response. Your CSS and JavaScript get enqueued normally. A per-request Content Security Policy locks down what the rendered page can reach, but the document is, structurally, a real WordPress page that happens to have your app inside it.

What you get:

  • Real HTML at the URL. View source on /apps/your-slug/whatever and there’s your content. Crawlers index it. Social previews unfurl correctly. The OpenGraph meta tags work. Google actually returns your page for searches that match it.
  • Theme wrap. Your app sits inside the active WordPress theme’s header and footer. Branding, navigation, and chrome match the rest of the site for free. If you’d rather have full control, set theme.wrap: "none" and the theme stays out of your way.
  • Sitemap injection. The plugin can announce your inline-mode routes in the site’s XML sitemap, so search engines find your pages without relying on internal links. Yoast SEO and Rank Math both pick this up automatically.
  • Multi-page apps that read like multi-page sites. Each route in the manifest becomes its own crawlable URL with its own <title> and meta. The Astro-on-WordPress workflow lives here.

What you give up:

  • A larger trust surface. The app’s HTML is part of the page response. The runtime mitigates this with wp_kses filtering, per-request CSP, and a strict allowlist of the bridge methods inline mode can call. But “the app’s bundle is a sandboxed iframe” is a stronger sentence than “the app’s HTML is filtered before render,” and you should know which one you’re operating under.
  • Some bridge methods are off the table. Inline mode runs too entangled with the page to host an isolated handler. Specifically, abilities.publishes (the publish side of Apps as Abilities) is iframe-mode only in v1. If your app needs to expose a function the site’s agent can call, you’re in iframe.
  • A bit more discipline at build time. Inline mode means the bundle is rendered through the WP page lifecycle, which means assumptions like “everything I import is available at runtime” get tested against wp_kses reality faster. CSP violations show up as 4xx in the network tab, not as silent failures.
  • Theme conflicts can surprise you. If the active theme aggressively enqueues <style> tags that override your spacing, you have a fight on your hands that iframe mode never has. theme.wrap: "none" skips the wrap entirely and resolves most of these; theme.wrap: "auto" keeps the chrome and accepts the conflict.

How to pick

A short decision tree:

Does the page need to show up in Google?

  • Yes: inline.
  • No: iframe.

Is the app something a logged-in user opens to do a task, then closes?

  • Yes: iframe is fine. Probably right.
  • No (it’s a marketing surface, a public-facing detail page, a multi-page mini-site): inline.

Does the app need to publish abilities for the site’s AI agent to call?

  • Yes: iframe. Publish-side abilities are iframe-only in v1.
  • No: either is fine.

Should the rendered page inherit the WP theme’s header, footer, and styling automatically?

  • Yes: inline.
  • No (the app brings its own visual language): iframe, or inline with theme.wrap: "none".

Does the app need its own <title>, meta description, and OpenGraph tags per route?

  • Yes: inline. Each route declares title and description in the manifest’s routes array, and the plugin renders them into the head.
  • No: iframe is simpler.

Is this a single-page app or a multi-page app?

  • Single page: either.
  • Multi-page (with real URLs per page): inline. Iframe mode supports client-side routing inside the iframe, but the URL bar doesn’t update, which kills shareable links and breaks the back button.

The combination that matters: inline + root mount

When you set isolation: "inline" and mount: { "mode": "root" }, the app stops living under /apps/{slug}/ and starts owning /. WordPress’s regular front-end keeps serving real WP pages, posts, archives, and feeds (root mode adds, it doesn’t shadow), but anything WordPress would otherwise 404 on falls through to your route table.

That combination is what makes “build the entire front-end of your WordPress site as a DSGo App” work. The visitor lands on /. Your inline-mode root app renders. Search crawlers see real HTML. The editor still uses wp-admin to publish posts, and those posts still appear at their canonical URLs. WordPress is the CMS, the auth layer, the data source. Your app is the front-end.

If that sounds familiar, it’s because the Astro on WordPress post walks through exactly this configuration. The website you’re reading this on (designsetgo.dev) is itself a root-mounted inline-mode DSGo App, with a build-time fetch from WordPress’s REST API for the blog index.

A small honest note

Iframe mode is the default for a reason. It has the strongest isolation guarantees, the simplest security model, and the broadest fit for the things people build first (tools, widgets, internal utilities). Most DSGo Apps will never need inline mode, and that’s fine. Picking iframe is rarely wrong; picking inline by accident, on a tool that didn’t need SEO, is sometimes wrong because it costs you the isolation guarantee for nothing.

Inline mode is a deliberate escalation. More capability, slightly more trust surface, real HTML at the URL. Use it when you want the page to be findable, theme-wrapped, and indexable. Don’t use it because it sounds more powerful.

The runtime doesn’t care which mode you picked. Same manifest schema. Same bridge surface (with the small inline-mode restrictions noted above). Same deploy command. Same install flow. The choice is about what the rendered URL should look like to the browser and to a crawler, not about which app is “real.”

For the field-level details on isolation, mount.mode, theme.wrap, and the inline-mode CSP knobs, see MANIFEST.md. For the rendering pipeline implementation, the relevant source files are class-iframe-loader.php and class-inline-renderer.php.