Summary
There is no official documentation or example showing how to embed a Bitrix24 CRM Web Form (the two-script snippet from the portal admin) on an external site built with @bitrix24/b24ui-nuxt. This issue proposes a ready-to-use recipe and documents the pitfalls discovered during implementation.
Real-world implementation this is based on: bx-shef/Lp#29
Background
When you create a Web Form in Bitrix24 (CRM → Forms → Embed), the portal generates a two-script snippet:
<!-- Marker: tells the B24 loader where to render the form -->
<script
data-b24-form="inline/{formId}/{formSecret}"
data-skip-moving="true"
></script>
<!-- Loader IIFE: appends a second <script> pointing to loader_1.js -->
<script>
(function(w, d, u) {
var s = d.createElement('script');
s.async = true;
s.src = u + '?' + (Date.now() / 180000 | 0);
var h = d.getElementsByTagName('script')[0];
h.parentNode.insertBefore(s, h);
})(window, document, 'https://{portal}.bitrix24.{tld}/upload/crm/form/loader_1.js');
</script>
The loader finds the [data-b24-form] element in the DOM, fetches its CSS/JS bundle, and renders the full form widget in place.
Approach 1 — Direct script injection (naïve, problematic)
<script setup lang="ts">
onMounted(() => {
const marker = document.createElement('script')
marker.setAttribute('data-b24-form', `inline/${formId}/${formSecret}`)
marker.setAttribute('data-skip-moving', 'true')
document.body.appendChild(marker)
const loader = document.createElement('script')
loader.async = true
loader.src = `${loaderUrl}?${Math.floor(Date.now() / 180000)}`
document.body.appendChild(loader)
})
</script>
Problems:
- Conflicts with
Content-Security-Policy: script-src — the B24 loader injects further dynamic <script> tags from CDN subdomains that can change (bel.bitrix24.by, cdn-ru.bitrix24.by, …), forcing script-src https: and effectively disabling any script CSP on the host page.
- The loader injects global
<link rel="stylesheet"> tags and modifies document.body styles, causing style leakage into the host page.
- In dev, Vite HMR re-triggers
onMounted and inserts duplicate scripts.
- The B24 bundle registers global event listeners and
window.* variables that can conflict with host-page code.
Approach 2 — <iframe srcdoc> isolation ✅ recommended
Render the two-script snippet inside a self-contained mini HTML document passed as the srcdoc attribute of an <iframe>. The iframe creates its own browsing context — the B24 loader's scripts, styles, and globals are completely isolated from the host page.
Full component
<!-- components/BriefForm.vue -->
<script setup lang="ts">
const config = useRuntimeConfig()
const ID_RE = /^[a-zA-Z0-9_-]+$/
const B24_HOST_ALLOWLIST = [
'.bitrix24.com',
'.bitrix24.by',
'.bitrix24.ru',
'.bitrix24.kz',
'.bitrix24.tech',
] as const
function isAllowedB24Host(rawUrl: string): boolean {
try {
const u = new URL(rawUrl)
if (u.protocol !== 'https:') return false
return B24_HOST_ALLOWLIST.some(suffix => u.hostname.endsWith(suffix))
} catch {
return false
}
}
const srcdoc = ref('')
onMounted(() => {
const { b24FormScriptUrl, b24FormId, b24FormSecret } = config.public
if (!b24FormScriptUrl || !b24FormId || !b24FormSecret) return
if (!isAllowedB24Host(b24FormScriptUrl)) {
console.warn('[BriefForm] script URL is not from the Bitrix24 allowlist — form not loaded')
return
}
if (!ID_RE.test(b24FormId) || !ID_RE.test(b24FormSecret)) {
console.warn('[BriefForm] invalid b24FormId or b24FormSecret — form not loaded')
return
}
const loaderSrc = `${b24FormScriptUrl}?${Math.floor(Date.now() / 180000)}`
const formAttr = `inline/${b24FormId}/${b24FormSecret}`
// Split </script> so the HTML parser inside the srcdoc string
// does not terminate the surrounding JS string prematurely.
const closeScript = '<' + '/script>'
srcdoc.value =
`<!doctype html>` +
`<meta charset="utf-8">` +
`<meta name="viewport" content="width=device-width,initial-scale=1">` +
`<style>*{box-sizing:border-box}body{margin:0;padding:0;background:#1e2226}</style>` +
`<body>` +
`<script data-b24-form="${formAttr}" data-skip-moving="true">${closeScript}` +
`<script src="${loaderSrc}" async>${closeScript}` +
`</body>`
})
</script>
<template>
<div class="rounded-2xl bg-black/30 backdrop-blur-sm">
<iframe
v-if="srcdoc"
:srcdoc="srcdoc"
class="w-full min-h-[800px] sm:min-h-[600px] border-0 rounded-2xl"
title="Contact form"
loading="lazy"
/>
<div v-else class="p-8 text-white/50 text-sm">
Form not configured — set NUXT_PUBLIC_B24_FORM_* environment variables.
</div>
</div>
</template>
nuxt.config.ts
runtimeConfig: {
public: {
b24FormScriptUrl: process.env.NUXT_PUBLIC_B24_FORM_SCRIPT_URL ?? '',
b24FormId: process.env.NUXT_PUBLIC_B24_FORM_ID ?? '',
b24FormSecret: process.env.NUXT_PUBLIC_B24_FORM_SECRET ?? '',
}
}
.env
# Bitrix24 CRM Web Form
# Find formId + formSecret in: CRM → Forms → Edit → Embed code
NUXT_PUBLIC_B24_FORM_SCRIPT_URL=https://your-portal.bitrix24.by/upload/crm/form/loader_1.js
NUXT_PUBLIC_B24_FORM_ID=1
NUXT_PUBLIC_B24_FORM_SECRET=abc123
Non-obvious details
| Detail |
Why it matters |
Date.now() / 180000 |
Matches B24's own cache-busting: new value every 3 minutes, same formula as the original IIFE |
data-skip-moving="true" |
Prevents the B24 loader from relocating the marker <script> in the DOM after it finishes loading |
'<' + '/script>' in srcdoc string |
Without splitting, the HTML parser exits the outer <script> block at the literal </script>, corrupting the string |
isAllowedB24Host allowlist |
Prevents open-redirect / script injection if the URL is supplied via an env var or config |
ID_RE regex on formId / formSecret |
Prevents attribute injection into the srcdoc HTML string |
No sandbox attribute on iframe |
B24 forms require allow-scripts and allow-forms; omitting sandbox entirely is simpler and equivalent for a trusted, self-generated srcdoc |
| No CSP header changes needed on host |
The iframe's origin is null (srcdoc), so B24 scripts run under their own context; the host CSP is unaffected |
Where this belongs in the docs
Suggested location in the b24ui-nuxt documentation site:
docs/
recipes/
crm-form-embed.md ← new file
Or as a sub-page under an existing Integrations section.
Proposed sections for crm-form-embed.md
- What is a Bitrix24 CRM Web Form? — where to find the embed code (formId, formSecret, loader URL)
- Why not use the raw embed snippet? — CSP breakage, style leakage, HMR duplicates
- Recommended approach:
iframe srcdoc — how isolation solves all three problems
- Full
BriefForm.vue component — copy-paste ready
nuxt.config.ts and .env setup — the three b24Form* public vars, where to find them in the portal
- Key implementation notes — the table above
Proposed skills to ship alongside
| Skill name |
Description |
b24-crm-form-embed |
Scaffolds BriefForm.vue + runtime config block into an existing b24ui-nuxt project |
b24-form-env-setup |
Generates .env.example entries with comments pointing to the Bitrix24 admin screen |
Summary
There is no official documentation or example showing how to embed a Bitrix24 CRM Web Form (the two-script snippet from the portal admin) on an external site built with
@bitrix24/b24ui-nuxt. This issue proposes a ready-to-use recipe and documents the pitfalls discovered during implementation.Background
When you create a Web Form in Bitrix24 (CRM → Forms → Embed), the portal generates a two-script snippet:
The loader finds the
[data-b24-form]element in the DOM, fetches its CSS/JS bundle, and renders the full form widget in place.Approach 1 — Direct script injection (naïve, problematic)
Problems:
Content-Security-Policy: script-src— the B24 loader injects further dynamic<script>tags from CDN subdomains that can change (bel.bitrix24.by,cdn-ru.bitrix24.by, …), forcingscript-src https:and effectively disabling any script CSP on the host page.<link rel="stylesheet">tags and modifiesdocument.bodystyles, causing style leakage into the host page.onMountedand inserts duplicate scripts.window.*variables that can conflict with host-page code.Approach 2 —
<iframe srcdoc>isolation ✅ recommendedRender the two-script snippet inside a self-contained mini HTML document passed as the
srcdocattribute of an<iframe>. The iframe creates its own browsing context — the B24 loader's scripts, styles, and globals are completely isolated from the host page.Full component
nuxt.config.ts.envNon-obvious details
Date.now() / 180000data-skip-moving="true"<script>in the DOM after it finishes loading'<' + '/script>'in srcdoc string<script>block at the literal</script>, corrupting the stringisAllowedB24HostallowlistID_REregex onformId/formSecretsandboxattribute on iframeallow-scriptsandallow-forms; omittingsandboxentirely is simpler and equivalent for a trusted, self-generated srcdocnull(srcdoc), so B24 scripts run under their own context; the host CSP is unaffectedWhere this belongs in the docs
Suggested location in the
b24ui-nuxtdocumentation site:Or as a sub-page under an existing Integrations section.
Proposed sections for
crm-form-embed.mdiframe srcdoc— how isolation solves all three problemsBriefForm.vuecomponent — copy-paste readynuxt.config.tsand.envsetup — the threeb24Form*public vars, where to find them in the portalProposed skills to ship alongside
b24-crm-form-embedBriefForm.vue+ runtime config block into an existing b24ui-nuxt projectb24-form-env-setup.env.exampleentries with comments pointing to the Bitrix24 admin screen