Reference
Third-Party Embeds
The engine builds the Content Security Policy for each site from
site.yaml. Hosts inallowedDomainsare merged intoscript-src,frame-src, andconnect-srcautomatically; 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
- Identify all external domains in the embed code (
src=,href=, fetch calls) - Add those domains to
allowedDomains - Paste the embed HTML into the page
- If it's a
<script>that should load site-wide (chat widget, analytics), put it intemplates/layout.htmlbefore</body> - If it's page-specific (booking widget, video), put it in the page file
Rules
- Always add the domain to
allowedDomainsbefore 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