Overview

Configuration is the per-block counterpart to global, theme-level settings. Where the Breakpoints panel ships one record per theme, block configuration lives inside every block_version_settings object — covering SEO meta, header/footer SDK script tags, and (via the dev Code panel) the block's custom JavaScript that runs after SDK init.

Configuration splits across two surfaces in the editor:

  • Block Settings dialog — opened by the gear icon next to a block. Renders two tabs (SEO, Library / SDK) when the block is a page; renders a "coming soon" placeholder for non-page block types (component, dialog, header, footer).
  • Code panel (dev mode) — a three-column Monaco editor with HTML / CSS / JS panels per tab (Page tab + one tab per imported global block) plus an Elements inspector and a sandboxed iframe preview. The JS column writes jsCode on the page state slice.

Both surfaces feed back to the same record: the dialog persists block_version_settings via useBlockSettings.saveSettings, while the Code panel persists jsCode through the normal page save path. The SSR renderer reads both at request time and stitches the SDK arrays into <head> / pre-</body>, and inlines jsCode alongside the parsed HTML/CSS.

How it works

The Block Settings dialog lives at common/components/commmerce-editor/block-settings/. The BlockSettingsDialog component reads the active block's block_id, block_version_code, block_type, and block_version_settings from useCommmerceEditor (the page store), seeds two refs (currentSEORef, currentSdkRef) with the loaded settings on open, and renders the tabs only for block_type === 'page'. All other block types fall through to a centered placeholder.

Tabs maintain their own useState draft and pass changes up via stable onChange callbacks that mutate the refs without re-rendering the modal. On Save, the dialog merges any unknown keys already on block_version_settings (so it never strips fields it doesn't manage) before calling saveSettings(blockId, blockVersionCode, merged).

useBlockSettings routes the save to one of three flows based on R.env.project and theme context:

  • Dev Portal — writes per-block settings into the dev-portal store.
  • Store Editor (unbundled theme) — once the theme has been unbundled into store_master_db, saves go through the unbundled-theme save flow.
  • Store Editor (draft theme) — before unbundling the theme still lives in dev_portal_db as a draft, so saves take the draft Store Editor route.

The Code panel (common/components/commmerce-editor/dev/index.tsx) consumes the same per-block settings to build its iframe srcDoc. The headerSdk array is joined and dropped into the iframe <head> next to the SDK preloads; footerSdk is injected before </body>; jsCode follows the footer SDKs and runs after SDK init.

Key features

  • Per-block SEO override — page title, prefix, suffix, description, keywords, OG image — saved to block_version_settings.seo.
  • Per-page header SDK + footer SDK arrays for analytics, fonts, tracking pixels and late-loading scripts.
  • A live Search Preview and Social Preview card in the SEO tab that mirrors how the page will render in Google results and social embeds.
  • Inline SEO recommendations driven by validateSEOSettings — over-length titles / descriptions and invalid OG image URLs surface a yellow warning panel.
  • Three-way save routing through useBlockSettings so the same dialog works in Dev Portal, draft Store Theme Editor, and unbundled Store Theme Editor without UI branching.
  • A sandboxed iframe preview in the Code panel that stitches headerSdk, footerSdk, jsCode, the theme stylesheet, and STORE_THEME_SETTINGS into a real srcDoc — what you see is what SSR will emit.
  • Metadata round-trip protection — createCodeSlice.setJsonData and the Code panel's saveBlockCode walk both rebuild block metadata (block_id, display_order, etc.) by matching CSS class names so the HTML/CSS parser doesn't drop those fields.
  • Non-page blocks safely no-op the dialog — the "Block settings coming soon" placeholder prevents silent data loss for component / dialog blocks.

SEO tab — fields reference

Stored under block_version_settings.seo. Defaults come from defaultBlockSEOSettings in block-settings/types.ts.

FieldTypeDefaultDescription & use case
defaultTitlestring""Page <title> shown in browser tabs and Google results. Live character counter turns red over 60 chars; maxLength=100 on input. Use to override the theme's default title for a hero landing page or product launch.
prefixTitlestring""Optional prefix prepended to the title in the rendered search preview (e.g. Shop |). maxLength=30. Use for store-name branding without polluting the editable title field.
suffixTitlestring""Optional suffix appended after the title (e.g. | My Store). maxLength=30. Lets you keep the brand at the end of the title regardless of the page-specific text.
defaultDescriptionstring""Meta description used by og:description, twitter:description, and Google snippets. 3-row textarea; counter turns red over 160 chars; maxLength=300. Use for the marketing tagline you want surfaced in search results.
defaultKeywordsstring (CSV)""Comma-separated keyword list rendered as Ant Design tags. The validator surfaces a warning at > 20 keywords. Mostly useful for internal taxonomy / faceted search; SEO impact is minor.
defaultMetaImageURL string""Open Graph + Twitter Card image. Input is paired with an Upload button that opens the UploadMedia dialog so authors can pick from theme assets. Validator warns if the value isn't a parseable URL.

Library / SDK tab — fields reference

Stored under block_version_settings.headerSdk and block_version_settings.footerSdk as string arrays. Each entry is a raw <link> / <script> tag that the SSR renderer (and the iframe preview) injects verbatim.

FieldTypeDefaultDescription & use case
headerSdkstring[][]Injected into <head> before the SDK loader. Use for Google Analytics, GTM, font preloads, pixel snippets that need to fire early, or third-party stylesheets that must beat the SDK's CSS variables.
footerSdkstring[][]Injected before </body>, after the SDK loader. Use for late-loading tracking pixels, deferred analytics, A/B test SDKs, and any script that can wait until the page is interactive.

The tab UI is a two-column grid — header column on the left, footer on the right — with an input field + plus icon at the top and a scrollable list (max-h-64) below. Pressing Enter in the input or clicking the plus icon trims whitespace and appends the entry to the matching array. Each list row carries a trash icon that calls filter((_, i) => i !== index).

Code panel — per-block code

The Code panel lives at common/components/commmerce-editor/dev/index.tsx and is the second configuration surface. It owns three Monaco editors per active tab and a fourth pane for the Elements / Console inspector.

EditorSourcePersisted asDescription & use case
HTMLparser outputchildren[] treeFormatted via formatHTML. On Run, htmlCssToJsonWithGlobals re-parses the markup and createCodeSlice.setJsonData merges block metadata back by class-name so block_id / display_order survive the round-trip. Use to hand-craft attributes the visual editor doesn't expose (data attributes, ARIA labels).
CSSparser outputchildren[] stylesGenerated by jsonToHtmlCss with consolidated media queries. On Run, parsed back into styles, responsiveStyles, customResponsiveStyles, pseudoClassStyles on each node. Use for cascading rules and pseudo-elements the panel UI doesn't author.
JSjsCode (slice)""Raw JavaScript string saved to jsCode on the page store via createCodeSlice.setJsCode. SSR injects it before </body> after the SDK; the iframe preview embeds it the same way. Always minimized by default on entry; click the vertical "JS" strip to open it.

The panel persists per-user UI preferences via localStorage keys: devEditor-devPanelHeight, devEditor-codeEditorVerticalSizes, devEditor-codeEditorsHorizontalSizes, devEditor-minimizedPanels. These do not affect the saved block payload.

Validation rules

SEO validation is non-blocking — it surfaces warnings, never refuses to save. From validateSEOSettings in types/settings.types.ts:

ThresholdValueSeverityDescription & use case
defaultTitle.length> 60warning"Default Title should be under 60 characters for optimal SEO." Google truncates titles around 60 chars on desktop SERPs. The character counter beside the input turns red at the same threshold.
defaultDescription.length> 160warning"Default Description should be under 160 characters for optimal SEO." Matches Google's snippet cap; counter beside the textarea turns red at 160.
defaultKeywords (tag count)> 20warning"Consider limiting keywords to 20 or fewer." Empty keywords are filtered out before counting.
defaultMetaImagenon-URLwarning"Default Meta Image should be a valid URL." Triggered by isValidURL(seo.defaultMetaImage); an empty string is allowed.

Library / SDK entries are stored verbatim — no sanitization happens client-side. The input is trimmed before append; empty strings are rejected.

The Save button calls useBlockSettings.saveSettings which returns response.success === true; on failure the message "Failed to save block settings" is shown and the dialog stays open so the user doesn't lose the draft.

Workflows

1. Set per-page SEO for a marketing landing page

  1. Open the page in the editor and click the gear icon on the page row (left sidebar) to launch the Block Settings dialog.
  2. On the SEO tab, type a 50–60 character Default Title; the live counter shows you the budget remaining.
  3. Paste a 120–160 character description in Default Description. The Search Preview card below renders the title + description as Google would.
  4. Click Upload next to Meta Image (OG) and pick a 1200×630 PNG; the Social Preview card lights up with the image.
  5. Click Save. The dialog runs the project-appropriate save flow (Dev Portal, unbundled store theme, or draft store theme) — chosen automatically.

2. Inject Google Analytics on a single page

  1. Open Block Settings on the target page.
  2. Switch to Library / SDK.
  3. Paste the GA snippet (<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>) into the Header SDK input and press Enter.
  4. Paste the inline init <script> as a second entry in Header SDK so it runs after the loader.
  5. Save. SSR will splice both tags into the page's <head>; the iframe preview already reflects the change.

3. Add a late-loading tracking pixel

  1. Open Block Settings → Library / SDK.
  2. Paste the pixel <script> tag into the Footer SDK input and press Enter.
  3. Save. The pixel will fire after the SDK has booted, avoiding blocking the SDK init.

4. Hand-edit block HTML in the Code panel

  1. Switch the editor to code mode (toolbar toggle).
  2. Use the Page tab for the page block, or click a global block's tab in the inner tab bar to edit that block in isolation.
  3. Tweak HTML and/or CSS. Each Monaco panel has its own Run button at the top.
  4. Run HTML → htmlCssToJsonWithGlobals parses the markup, createCodeSlice.setJsonData merges back block metadata by class name, and the canvas re-renders. The iframe preview rebuilds.

5. Attach custom JavaScript to a page

  1. Open the Code panel and click the vertical JS strip to expand the column (JS is collapsed by default).
  2. Type the JS body (no need to wrap in <script> — the SSR template wraps it for you).
  3. Click Run on the JS pane → createCodeSlice.setJsCode writes jsCode to the page store and the iframe rebuilds with the new script injected after the SDK.
  4. Save the page from the main toolbar. The next SSR request will include the snippet pre-</body>.

6. Open Block Settings on a component / dialog block

  1. Open a non-page block (component, dialog, header, footer).
  2. The dialog title reads "Block Settings" instead of "Page Settings" and the body renders the "Block settings coming soon." placeholder.
  3. Save is a no-op for these block types — the dialog never strips fields you can't see, so unknown keys are preserved.

Examples

Empty block_version_settings (initial state)

{
  "seo": {
    "defaultTitle": "",
    "prefixTitle": "",
    "suffixTitle": "",
    "defaultKeywords": "",
    "defaultMetaImage": "",
    "defaultDescription": ""
  },
  "headerSdk": [],
  "footerSdk": []
}

Page block with SEO + GA + a footer pixel

{
  "seo": {
    "defaultTitle": "Summer Sale - 30% Off Everything",
    "prefixTitle": "Shop |",
    "suffixTitle": "| Clash & Love",
    "defaultKeywords": "summer sale, fashion, discount",
    "defaultMetaImage": "https://cdn.example.com/og/summer-sale.png",
    "defaultDescription": "Save 30% across our summer collection — free shipping on orders over $50."
  },
  "headerSdk": [
    "<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXX\"></script>",
    "<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-XXXX');</script>"
  ],
  "footerSdk": [
    "<img src=\"https://t.example.com/p?id=42\" width=\"1\" height=\"1\">"
  ]
}

How SSR stitches the block into the page

<!DOCTYPE html>
<html>
<head>
  <title>Shop | Summer Sale - 30% Off Everything | Clash & Love</title>
  <meta name="description" content="Save 30% across our summer collection...">
  <meta property="og:image" content="https://cdn.example.com/og/summer-sale.png">
  <!-- block_version_settings.headerSdk[*] -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>
  <script>window.dataLayer=...gtag('config','G-XXXX');</script>
  <style>/*   parser-generated CSS   */</style>
</head>
<body>
  <!-- parser-generated HTML -->
  <script src="...commmerce sdk loader..." defer></script>
  <!-- block_version_settings.footerSdk[*] -->
  <img src="https://t.example.com/p?id=42" width="1" height="1">
  <!-- jsCode from createCodeSlice -->
  <script>/*   user JS   */</script>
</body>
</html>

Tips & gotchas

  • The Library / SDK tab is page-only. Only blocks with block_type === 'page' get the tabbed UI; everything else sees the "coming soon" placeholder. If you need head/footer scripts on a non-page surface, push them into the parent page's settings instead.
  • SDK strings are emitted verbatim. There is no sanitization — paste tags exactly as you'd write them in HTML. Watch out for missing quote escapes and stray </script> in inline JSON; the iframe preview escapes </script> only for the STORE_THEME_SETTINGS injection, not for arbitrary SDK entries.
  • SEO validation is non-blocking. Over-limit titles and descriptions still save — they just show a yellow warning panel. The merchant can choose to ignore it if SEO isn't a priority for the page.
  • The dialog preserves unknown keys. BlockSettingsDialog.handleSave spreads the existing block_version_settings into merged before overwriting seo / headerSdk / footerSdk, so any custom field a future feature adds keeps round-tripping safely.
  • Save routing depends on project + theme state. useBlockSettings reads R.env.project + useOptionalThemeEditorContext().isUnbundled to pick between three save flows. If your store theme says it's a draft (not unbundled), saves hit dev_portal_db; once unbundled they hit store_master_db via the unbundled route.
  • Code-panel JS lives on the page store, not in settings. jsCode is a top-level field on the page slice — the Save button on the JS pane writes to it via createCodeSlice.setJsCode and the normal page save round-trips it. It is not part of block_version_settings.
  • HTML round-trip preserves block metadata. createCodeSlice.setJsonData caches block metadata (block_id, block_version_code, display_order, isFullWidth, layoutConfig editor-only fields) by CSS class name and re-applies them after parsing, so editing in code mode doesn't lose the bookkeeping fields HTML can't encode.
  • Per-block iframe preview uses the dialog's values live. The dev-mode iframe pulls headerSdk / footerSdk from useCommmerceEditor.block_version_settings and rebuilds the srcDoc whenever they change — no Save needed to see the SDK in action, but the dialog's draft only commits on Save.