Skip to main content
SiteShi p

Reference

Third-Party Embeds

The engine builds the Content Security Policy for each site from site.yaml. Hosts in allowedDomains are merged into script-src, frame-src, and connect-src automatically; the hosted runtime emits the policy on every response.

Adding an external service requires two things: declaring the domain in your site config so the security policy allows it, and writing the embed HTML. The engine handles the rest.

The two-step process

Step 1 — Allow the domain in data/site.yaml:

{
  "allowedDomains": ["calendly.com", "maps.googleapis.com"]
}

Step 2 — Write the embed HTML on the page.

That's it. The Worker reads allowedDomains and automatically adds those domains to the Content Security Policy (script-src, frame-src, connect-src). Without this, the browser will block the external script/iframe.


Service-specific embed patterns

Calendly (booking widget)

Ask the user for their Calendly URL (e.g. https://calendly.com/your-name/30min).

"allowedDomains": ["calendly.com", "assets.calendly.com"]
<!-- Inline widget (recommended — stays on page) -->
<div class="calendly-inline-widget" data-url="https://calendly.com/YOUR-SLUG/30min" style="min-width:320px;height:700px;"></div>
<script async src="https://assets.calendly.com/assets/external/widget.js"></script>

Google Maps

Ask the user for their business name or address to generate the embed URL.

"allowedDomains": ["maps.googleapis.com", "maps.google.com", "www.google.com"]
<iframe
  src="https://www.google.com/maps/embed?pb=YOUR_EMBED_URL"
  width="100%" height="400" style="border:0;" allowfullscreen loading="lazy"
  referrerpolicy="no-referrer-when-downgrade">
</iframe>

To get the embed URL: Google Maps → search location → Share → Embed a map → copy the src URL from the iframe code.

YouTube

"allowedDomains": ["www.youtube.com", "youtube.com", "youtu.be"]
<div class="aspect-video">
  <iframe
    src="https://www.youtube.com/embed/VIDEO_ID"
    class="w-full h-full rounded-xl"
    allowfullscreen loading="lazy">
  </iframe>
</div>

Replace VIDEO_ID with the ID from the video URL (the part after v=).

Vimeo

"allowedDomains": ["player.vimeo.com", "vimeo.com"]
<div class="aspect-video">
  <iframe
    src="https://player.vimeo.com/video/VIDEO_ID"
    class="w-full h-full rounded-xl"
    allowfullscreen loading="lazy">
  </iframe>
</div>

Typeform

Ask the user for their Typeform URL.

"allowedDomains": ["YOUR-SLUG.typeform.com", "embed.typeform.com"]
<div data-tf-live="YOUR_FORM_ID"></div>
<script src="https://embed.typeform.com/next/embed.js" async></script>

Crisp (live chat)

Ask the user for their Crisp website ID from the Crisp dashboard.

"allowedDomains": ["client.crisp.chat", "settings.crisp.chat"]
<script>
  window.$crisp=[];
  window.CRISP_WEBSITE_ID="YOUR-CRISP-ID";
  (function(){var d=document;var s=d.createElement("script");
  s.src="https://client.crisp.chat/l.js";s.async=1;d.getElementsByTagName("head")[0].appendChild(s);})();
</script>

Place this near the bottom of the page or in templates/layout.html if you want chat on every page.

Tawk.to (live chat)

Ask the user for their Tawk.to property ID and widget ID.

"allowedDomains": ["embed.tawk.to", "va.tawk.to"]
<script>
var Tawk_API=Tawk_API||{}, Tawk_LoadStart=new Date();
(function(){var s1=document.createElement("script"),s0=document.getElementsByTagName("script")[0];
s1.async=true;
s1.src='https://embed.tawk.to/PROPERTY_ID/WIDGET_ID';
s1.charset='UTF-8';s1.setAttribute('crossorigin','*');
s0.parentNode.insertBefore(s1,s0);})();
</script>

Loom (video)

"allowedDomains": ["www.loom.com"]
<div class="aspect-video">
  <iframe src="https://www.loom.com/embed/VIDEO_ID" class="w-full h-full rounded-xl" allowfullscreen></iframe>
</div>

Cal.com (booking)

"allowedDomains": ["cal.com", "app.cal.com"]
<div id="my-cal-inline" style="width:100%;height:700px;overflow:scroll"></div>
<script>
(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar); return; } p(cal, ar); }; })(window, "https://app.cal.com/embed/embed.js", "init");
Cal("init", {origin:"https://cal.com"});
Cal("inline", { elementOrSelector:"#my-cal-inline", calLink: "YOUR-USERNAME/30min" });
</script>

HubSpot (forms / chat)

Ask the user for their HubSpot portal ID.

"allowedDomains": ["js.hsforms.net", "js.hs-scripts.com", "forms.hubspot.com"]
<!-- HubSpot tracking + chat widget (add to layout.html for site-wide) -->
<script id="hs-script-loader" async defer src="//js.hs-scripts.com/YOUR_PORTAL_ID.js"></script>

If the user provides their own embed code

  1. Identify all external domains in the embed code (src=, href=, fetch calls)
  2. Add those domains to allowedDomains
  3. Paste the embed HTML into the page
  4. If it's a <script> that should load site-wide (chat widget, analytics), put it in templates/layout.html before </body>
  5. If it's page-specific (booking widget, video), put it in the page file

Rules

  • Always add the domain to allowedDomains before adding the embed HTML — without it the browser will block the resource
  • Use loading="lazy" on all iframes
  • Wrap iframes in <div class="aspect-video"> for 16:9 video — this keeps the proportions correct at all screen sizes
  • For chat widgets and analytics that should appear on every page, add to templates/layout.html; for everything else, add to the specific page
  • Ask the user for their account URL, ID, or embed code before writing anything — don't make up placeholder IDs

Found something out of date? Open an issue. ← All docs