{"success":true,"data":{"block":{"id":"script-embed","name":"Script Embed","title":"Script Embed","category":"Advanced","categorySlug":"advanced","description":"Runs a third-party JavaScript widget snippet the site owner supplied — chat widget, menu embed, booking engine, review or loyalty widget — inside the standard section chrome. Guarantees load order (companion markup, then stylesheets, then scripts, then inline code), deduplicates across re-mounts, and can contain legacy document.write output. Only use it when the user actually pasted a script snippet.","thumbnail":{"desktop":"https://cdn.ing/assets/i/r/316267/xinz5cepqms1q6zryqukvzs0r5e3/script-embed-desktop.jpg","mobile":"https://cdn.ing/assets/i/r/316270/bzmwycxy85y9u529cdsdojr67k53/script-embed-mobile.jpg"},"componentPath":"blocks/advanced/script-embed.tsx","code":"import { ScriptEmbed } from \"@opensite/ui/blocks/advanced/script-embed\";\n\n// Self-contained demo of the load-order contract: `companionHtml` is in the DOM\n// before anything executes, so the inline script always finds its mount point.\n// A real embed splits the owner's snippet across the props — `<script src>` goes\n// in `scriptUrl` (further ones in `additionalScriptUrls`), and the body of an\n// inline `<script>` goes in `inlineScriptHtml` WITHOUT the surrounding tags.\n//\n// EXTRACTOR CONSTRAINT (enforced by scripts/verify-registry-contract.mjs):\n// the registry `code` below is republished as the canonical example, sliced\n// from the returned JSX and cut at the FIRST close-paren-then-semicolon pair.\n// The inline script therefore relies on automatic semicolon insertion instead\n// of ending a statement immediately after a closing parenthesis.\nexport default function Demo() {\n  return (\n    <ScriptEmbed\n      title=\"Our Menu\"\n      subtitle=\"Script Embed\"\n      companionHtml={\n        '<div id=\"os-script-embed-demo\" class=\"rounded-xl border border-border p-8 text-center text-muted-foreground\">Companion markup rendered. Waiting for the script…</div>'\n      }\n      inlineScriptHtml={\n        'var el = document.getElementById(\"os-script-embed-demo\")\\n' +\n        'if (el) { el.textContent = \"The inline script ran and found its container.\" }'\n      }\n      scriptKey=\"opensite-showcase-script-embed-demo\"\n      loadStrategy=\"afterInteractive\"\n      containMode=\"auto\"\n    />\n  );\n}","propsSchema":{"scriptUrl":{"type":"string","description":"URL of the third-party script to load. Copy the `src` value out of the\nsnippet the site owner supplied, EXACTLY as given. Never invent a script\nURL and never use this block unless the owner provided the code.","required":false},"additionalScriptUrls":{"type":"array","description":"Additional script URLs, loaded strictly one at a time AFTER `scriptUrl`\nfinishes. Use when the owner's snippet contains several `<script src>` tags\nthat must run in order.","items":{"type":"string","description":""},"required":false,"maxItems":6},"inlineScriptHtml":{"type":"string","description":"Inline JavaScript from the owner's snippet (the contents of a `<script>`\ntag with no `src`). Runs after every URL script has loaded. Paste the code\nverbatim, without the surrounding `<script>` tags.","required":false},"companionHtml":{"type":"string","description":"Companion markup from the owner's snippet — the `<div>`/`<span>` mount\npoint the widget looks for. It is placed in the DOM BEFORE any script runs,\nso widgets that search for their container at start-up find it.","required":false},"stylesheetUrls":{"type":"array","description":"Stylesheet URLs from the owner's snippet. Loaded before any script runs.","items":{"type":"string","description":""},"required":false,"maxItems":4},"loadStrategy":{"type":"string","description":"\"afterInteractive\" (default) starts the chain as soon as the block mounts.\n\"lazyOnload\" waits for browser idle time — use it for chat widgets, social\nfeeds and anything else that is not needed for the first interaction.","typeLabel":"\"afterInteractive\" | \"lazyOnload\"","enum":["afterInteractive","lazyOnload"],"required":false},"allowDocumentWrite":{"type":"boolean","description":"Set true ONLY for legacy widgets that call `document.write`. It redirects\nthose writes into this block's container instead of letting them wipe the\npage. Default false.","required":false},"scriptKey":{"type":"string","description":"Explicit dedupe key for the EXTERNAL script. Defaults to `scriptUrl`, or a\nstable hash of `inlineScriptHtml` when there is no URL. Set it when the\nsame widget is embedded on several pages with slightly different URLs and\nmust only initialise once. Inline code is always additionally keyed by a\nhash of itself, so two blocks sharing one loader URL (or one `scriptKey`)\nstill each run their own init.","required":false},"runOnEveryMount":{"type":"boolean","description":"When false (default) the inline script runs once per page load, and after\nclient-side navigation back to this block the widget's existing DOM is\nre-attached to the fresh container so it repaints without re-running.\nSet true for widgets that must genuinely re-initialise on every mount:\nthe container is rebuilt from `companionHtml` and the inline code runs\nagain. External script URLs are loaded only once either way.","required":false},"containMode":{"type":"string","description":"How the embed area is sized. \"auto\" (default) lets the widget size itself,\n\"fixed-height\" reserves a pixel height (prevents layout shift), and\n\"full-screen\" fills the viewport edge to edge.","typeLabel":"\"auto\" | \"fixed-height\" | \"full-screen\"","enum":["auto","fixed-height","full-screen"],"required":false},"fixedHeight":{"type":"string","description":"CSS height used when `containMode=\"fixed-height\"`, e.g. \"480px\".\nDefaults to \"600px\".","required":false},"embedClassName":{"type":"string","description":"Additional CSS classes for the container the widget renders into.","required":false},"emptyStateLabel":{"type":"string","description":"Short line shown when no script, inline code, or companion markup is\nconfigured yet. With nothing configured and no label the block renders no\ncontent at all — it never fabricates a placeholder widget.","required":false},"sectionId":{"type":"string","description":"Optional Section ID.","required":false},"title":{"type":"string","description":"Optional heading rendered above the embed by the Section.","required":false},"subtitle":{"type":"string","description":"Optional kicker rendered above the heading by the Section.","required":false},"background":{"type":"string","description":"Section background variant.","typeLabel":"SectionBackground","enum":["default","white","gray","dark","gradient","primary","secondary","transparent","muted"],"required":false},"spacing":{"type":"string","description":"Section spacing variant. Defaults to \"none\" when\n`containMode=\"full-screen\"`.","typeLabel":"SectionSpacing","enum":["none","sm","md","lg","xl","hero"],"required":false},"pattern":{"type":"string","description":"Pattern background key.","typeLabel":"PatternName","enum":["squareAltGrid","grid1","noise","dotPattern","dotPattern2","circles","waves","crossPattern","architect","tinyCheckers","p6","dots","circuitBoardBasic","circuitBoardFadeTop","circuitBoardFadeBottom","circuitBoardFadeCenter","circuitBoardFadeTopLeft","circuitBoardFadeTopRight","circuitBoardFadeBottomLeft","circuitBoardFadeBottomRight","dashedGridBasic","dashedGridFadeTop","dashedGridFadeBottom","dashedGridFadeCenter","dashedGridFadeTopLeft","dashedGridFadeTopRight","dashedGridFadeBottomLeft","dashedGridFadeBottomRight","diagonalCrossBasic","diagonalCrossFadeTop","diagonalCrossFadeBottom","diagonalCrossFadeCenter","diagonalCrossFadeTopLeft","diagonalCrossFadeTopRight","diagonalCrossFadeBottomLeft","diagonalCrossFadeBottomRight","gridBasic","gridFadeTop","gridFadeBottom","gridFadeCenter","gridFadeTopLeft","gridFadeTopRight","gridFadeBottomLeft","gridFadeBottomRight","gridDotsBasic","gridDotsFadeCenter","gradientGlowTop","gradientGlowBottom","spotlightLeft","spotlightRight","radialGradientTop","radialGradientBottom"],"required":false},"patternOpacity":{"type":"number","description":"Pattern opacity (0-1).","required":false},"className":{"type":"string","description":"Additional CSS classes for the section.","required":false},"containerClassName":{"type":"string","description":"Additional CSS classes for the container.","required":false},"style":{"type":"object","description":"Inline styles for the section element.","typeLabel":"React.CSSProperties","required":false}},"exampleProps":{"title":"Our menu","scriptUrl":"https://embed-menu-preloader.untappdapi.com/embed-menu-preloader.min.js","companionHtml":"<div id=\"untappd-menu-container\"></div>","inlineScriptHtml":"PreloadEmbedMenu('untappd-menu-container', 12345, 678);","loadStrategy":"lazyOnload"},"dependencies":["@opensite/ui"],"tags":["script","embed","widget","third-party","javascript","chat","menu","booking","integration","external-content","snippet","advanced"],"performance":{},"importantUsageNotes":"Use ONLY when the site owner supplied a <script>-based widget snippet (chat widget, menu embed, booking engine, review widget). VERBATIM CODE ONLY: copy the embed code, URL, and ids EXACTLY as the site owner supplied them — never invent, guess, shorten, complete, or 'correct' them. Never use this block unless the user actually supplied the embed. If no embed code was provided, choose a different block or leave the section out. Split the owner's snippet across the props instead of pasting it as one blob: <script src> becomes scriptUrl (further ones go in additionalScriptUrls, in the original order), the contents of an inline <script> becomes inlineScriptHtml WITHOUT the surrounding tags, any <div>/<span> mount point becomes companionHtml, and <link rel=\"stylesheet\"> hrefs go in stylesheetUrls. Load order is guaranteed: companionHtml is in the DOM first, then stylesheets, then scriptUrl, then additionalScriptUrls one at a time, then inlineScriptHtml. Use loadStrategy=\"lazyOnload\" for anything below the fold. Set allowDocumentWrite only for legacy widgets that actually call document.write. Two script-embed blocks may share one scriptUrl (e.g. a food menu and a drinks menu from the same loader): give each its own companionHtml mount point and its own inlineScriptHtml — the loader runs the shared script once and each block's init separately. After client-side navigation back to the page the widget's DOM is re-attached automatically; set runOnEveryMount only when the widget must genuinely re-initialise from scratch on every mount. If the embed is a plain iframe, use advanced/iframe-embed instead. This block renders third-party content, not media-library assets. It declares no media slots; do not route images, logos, or videos into its props.","usageRequirements":{"requiredProps":[],"propConstraints":{"scriptUrl":{"note":"Absolute https URL taken verbatim from the owner's <script src>. Loaded first."},"additionalScriptUrls":{"maxItems":6,"note":"Further <script src> URLs in their original order. Loaded strictly one at a time AFTER scriptUrl."},"inlineScriptHtml":{"note":"Body of the owner's inline <script>, without the surrounding tags. Runs after every URL script has loaded."},"companionHtml":{"note":"The mount-point markup from the owner's snippet. Guaranteed to be in the DOM before any script runs."},"stylesheetUrls":{"maxItems":4,"note":"Stylesheet hrefs from the owner's snippet. Loaded before any script runs."},"allowDocumentWrite":{"note":"Set true ONLY for legacy widgets that call document.write; it redirects those writes into this block instead of wiping the page."},"scriptKey":{"note":"Explicit dedupe key. Defaults to scriptUrl, or a stable hash of inlineScriptHtml. inlineScriptHtml is ALWAYS additionally keyed by a hash of its own source, so two blocks sharing one scriptUrl each still run their own init. Set it when the same widget appears with slightly different URLs and must initialise once."},"runOnEveryMount":{"note":"Leave false unless the widget must genuinely re-initialise from scratch. On client-side navigation back to this block the previous mount's widget DOM is re-attached automatically, so a blank widget is not a reason to set this."},"fixedHeight":{"note":"Only meaningful when containMode=\"fixed-height\". A CSS length such as \"480px\"; a unitless value like \"480\" is read as pixels. Reserves space and prevents layout shift. Defaults to \"600px\"."},"emptyStateLabel":{"maxWords":12,"note":"Short honest line shown when no snippet is configured. Omit it and the block renders nothing rather than a fake widget."}},"mediaSlots":{},"requiresSiteCapabilities":[],"notes":["VERBATIM CODE ONLY: copy the embed code, URL, and ids EXACTLY as the site owner supplied them — never invent, guess, shorten, complete, or 'correct' them. Never use this block unless the user actually supplied the embed. If no embed code was provided, choose a different block or leave the section out.","This block renders third-party content, not media-library assets. It declares no media slots; do not route images, logos, or videos into its props.","Supply scriptUrl and/or inlineScriptHtml. companionHtml alone renders markup but runs nothing.","Never wrap inlineScriptHtml in <script> tags — pass only the JavaScript body."]}}},"timestamp":"2026-08-27T08:34:32.041Z"}