Skip to content

docs: recipe — embedding a Bitrix24 CRM Web Form on an external site (Nuxt + b24ui-nuxt #52

@IgorShevchik

Description

@IgorShevchik

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

  1. What is a Bitrix24 CRM Web Form? — where to find the embed code (formId, formSecret, loader URL)
  2. Why not use the raw embed snippet? — CSP breakage, style leakage, HMR duplicates
  3. Recommended approach: iframe srcdoc — how isolation solves all three problems
  4. Full BriefForm.vue component — copy-paste ready
  5. nuxt.config.ts and .env setup — the three b24Form* public vars, where to find them in the portal
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions