Commmerce Editor
Page Settings
Per-page SEO metadata and SDK injection — configured from the Page Settings dialog (the 3-dot button next to the page selector). Stored alongside the page block in block_version_settings so each version of a page can carry its own meta and script payload, and applied by the SSR renderer at request time without rebuilding the theme.
Overview
The Page Settings dialog is the per-page configuration surface for the Commmerce Editor. It exposes two tabs for block_type === 'page' entries: SEO (title, prefix, suffix, description, keywords, OG image, plus a live Google-style search preview and a social-card preview) and Library / SDK (two ordered lists of script tags or URLs injected into the rendered page's <head> or before </body>).
The dialog also opens from non-page blocks (components, dialogs), but only the page variant renders tabs today — every other block_type shows a "Block settings coming soon" placeholder, per the conditional in BlockSettingsDialog.tsx (line 144). The dialog is intentionally scoped to per-version data: theme-wide SEO defaults live separately in the EditorSettings.seo branch of the theme settings store, and the SSR renderer layers page-level values on top of those defaults.
How it works
The dialog lives at common/components/commmerce-editor/block-settings/BlockSettingsDialog.tsx and is opened from the PageSelector 3-dot button (or anywhere a consumer mounts it with variant="separate" + the gear button). On open it reads block_id, block_version_code, block_type, and block_version_settings off the editor store (createBlockSlice.ts) and seeds two local drafts: one for SEO (seedSEO) and one for Library/SDK (seedSdk). Both seeds fall back to defaultBlockSEOSettings / defaultBlockLibrarySdkSettings from block-settings/types.ts when the saved payload is empty.
The two tab components (BlockSEOTab.tsx and BlockLibrarySdkTab.tsx) drive their own local state and push changes back through stable refs (currentSEORef, currentSdkRef) so the dialog avoids re-render loops while the user types. Each tab also re-syncs its internal draft state whenever initialSettings changes, so re-opening the dialog reflects the latest saved values. On Save the dialog merges the two ref values into the existing block_version_settings object — { ...(block_version_settings ?? {}), seo, headerSdk, footerSdk } — preserving any unknown keys it doesn't manage, and persists via the useBlockSettings hook.
useBlockSettings in block-settings/useBlockSettings.ts picks the correct save flow based on R.env.project and the ThemeEditorContext.isUnbundled flag — branching between the Dev Portal save, the unbundled Store Editor save, and the draft Store Editor save depending on the context. On success the dialog calls setBlockVersionSettings(merged) so subsequent opens reflect the new values without a reload, shows a Block settings saved toast, and closes; on failure it shows Failed to save block settings and stays open with the user's in-memory draft intact.
The backend stores the merged payload in the block_version_settings column (a GCS URL pointing at a JSON blob) so the SSR renderer can read SEO meta and inject the header/footer SDK lists into the live page output without re-bundling the theme.
Key features
- Per-version settings — Settings are keyed by
(block_id, block_version_code), so different versions of the same page can carry different SEO and SDK payloads — useful when running an A/B or seasonal variant alongside the live page. - Live search preview — The SEO tab renders a Google-style preview card with the composed title (
prefix + title + suffix) and description as you type, so you can see how the page appears in SERPs without leaving the editor. - Live social preview — When
defaultMetaImageis set, a social-card preview renders the image plus title and description (image is silently hidden viaonErrorif the URL fails to load). - Character counters — Title and description inputs show n/max counters that turn red past 60 / 160 characters (Google's truncation thresholds for desktop SERP).
- SEO recommendations panel —
validateSEOSettingsfromtypes/settings.types.tsruns on every keystroke and surfaces a yellow callout box listing breached length / keyword / URL rules. - Keyword chips — Keywords use an Ant Design
Select mode="tags"with a commatokenSeparator— paste a comma-separated list, type and press Enter, or backspace to remove. The chip count is shown next to the label. - Media library integration — The OG image field opens the editor's
UploadMediadialog (fromdialogs/UploadMedia.tsx) for picking an existing asset or uploading a new one. - Header / Footer SDK lists — Two side-by-side columns let you maintain ordered lists of script tags or URLs injected into
<head>and before</body>respectively. Each row has its own trash button; the input accepts Enter to add. - Unknown-key preservation — The save step spreads the existing
block_version_settingsfirst, so future or external keys aren't stripped — integrations can park their own keys alongsideseo/headerSdk/footerSdksafely. - Project-aware routing — Save automatically targets the Dev Portal, the unbundled Store Editor, or the draft Store Editor API based on
R.env.projectandthemeCtx.isUnbundled.
SEO fields
All SEO fields are typed by the SEOSettings interface in common/components/commmerce-editor/types/settings.types.ts. Defaults come from defaultBlockSEOSettings in block-settings/types.ts — every field defaults to an empty string. Soft length limits (60 / 160 / 20) are surfaced as warnings via validateSEOSettings; the hard maxLength caps come from the Ant Design input props in BlockSEOTab.tsx.
defaultTitlestring""Primary <title> for the page. Soft-limited to 60 characters (Google's desktop SERP truncation threshold); the input enforces maxLength=100 as a hard cap. Use case: set a unique, keyword-rich page title for SEO and browser-tab readability, e.g. "Men's Running Shoes - Free Shipping".prefixTitlestring""Optional string prepended to the title before render (e.g. Shop |). Hard maxLength=30. Use case: apply a consistent storefront brand prefix across every page title — set it once and the SSR renderer prepends it automatically.suffixTitlestring""Optional string appended to the title (e.g. | My Store). Hard maxLength=30. Use case: append a brand tagline or store name to every page title — common SEO pattern is Page Name | Brand so the brand always appears even when the title is truncated.defaultDescriptionstring""Meta description for the page, surfaced in the search and social preview cards. Soft-limited to 160 characters; the input enforces maxLength=300. Use case: write a 120-160 character sales pitch that answers "what is this page" — this is the snippet shown under the title in Google results and drives click-through rate.defaultKeywordsstring""Comma-separated keyword list, edited as Ant Design tag chips. Validation warns past 20 keywords (per validateSEOSettings); the chip count is shown next to the label. Use case: tag the page with internal search and merchandising keywords (e.g. summer, sale, dress, womens) — used by some legacy crawlers and by the store's own search/recommendation systems.defaultMetaImagestring (URL)""Absolute URL of the Open Graph share image. Validated by new URL() — invalid strings raise a warning. The inline upload button opens UploadMedia to pick from the asset library. Use case: set the social card image that appears when the page is shared on Facebook / Slack / WhatsApp / Twitter — typically a 1200x630 product hero, brand banner, or campaign creative.Composition note: the SSR renderer produces the final <title> by concatenating ${prefixTitle} ${defaultTitle} ${suffixTitle} (with spaces inserted only when the prefix / suffix is non-empty). The same composition powers the search-preview card inside the dialog — what you see is what the renderer will emit.
Library / SDK fields
Library / SDK fields are typed by BlockLibrarySdkSettings in block-settings/types.ts. Each list is an ordered array of opaque strings — entries are inserted verbatim by the SSR layer, so the value can be a full <script src="..."></script> tag, a bare URL, a <link rel="..."> tag, an inline <style> block, or arbitrary meta markup.
headerSdkstring[][]Ordered list of script / link / meta strings injected into <head>. Use case: add early-loading payloads — Google Tag Manager bootstrap, Hotjar / Mixpanel analytics, font preloads, custom <link rel="preconnect">, structured-data <script type="application/ld+json">, or per-page <meta> overrides that must run before the body parses.footerSdkstring[][]Ordered list of scripts injected immediately before </body>. Use case: add deferred / non-critical scripts — Intercom or Drift chat widgets, conversion / retargeting pixels (Facebook, TikTok, Pinterest), A/B-test loaders, on-page review widgets, or anything that can wait for the DOM to be fully rendered.Each input commits via the Enter key or the trailing + button; entries are appended to the bottom of the list, displayed in a stack of cards, and each row carries an inline trash button to remove it. There is no in-place edit — to fix a typo, remove the entry and re-add it.
SEO validation rules
validateSEOSettings(seo: SEOSettings): string[] in types/settings.types.ts runs on every keystroke inside BlockSEOTab and returns a list of human-readable warnings. When the array is non-empty the dialog renders a yellow callout box ("SEO Recommendations:") with each warning as a bullet.
defaultTitle > 60soft60 chars"Default Title should be under 60 characters for optimal SEO" — Google truncates desktop SERP titles at ~60 chars. Use case: keep the title compact so the full message renders in search results and tweet previews.defaultDescription > 160soft160 chars"Default Description should be under 160 characters for optimal SEO" — desktop SERP snippet limit. Use case: trim the description so the closing call-to-action is visible in the snippet.keywords > 20soft20 chips"Consider limiting keywords to 20 or fewer" — counted after .split(',').filter(k => k.trim()). Use case: avoid keyword-stuffing penalties on legacy crawlers and keep the chip set focused.defaultMetaImage invalidsoftparsable URL"Default Meta Image should be a valid URL" — validated via new URL() in the isValidURL helper. Use case: catch typos in absolute URLs before they break social previews on Facebook / Slack / WhatsApp.All four rules are soft — they surface in the recommendations callout but never block Save. The maxLength caps on the inputs (100 / 30 / 30 / 300) are the only hard constraints, and even those allow the SEO limits to be exceeded on the title and description fields.
Workflows
1. Setting up SEO for a page
- Open the page in the editor, then click the 3-dot icon next to the page-selector pill in the toolbar (
PageSelector.tsxopensBlockSettingsDialogwith the current block in context). - The dialog opens on the SEO tab. Enter a Default Title (under 60 chars) and (optionally) a Prefix / Suffix to apply a brand pattern.
- Write a 120-160 character Description. Watch the counter — it turns red over 160.
- Type keywords in the Keywords chip field; press Enter or comma to commit each one (Ant Design's
mode="tags"with commatokenSeparator). - Paste an OG image URL into Meta Image, or click the upload icon to pick from the media library via
UploadMedia. - Review the search and social preview cards. Resolve any yellow warnings — they reflect
validateSEOSettingsoutput. - Click Save. The dialog merges into
block_version_settings, runs the project-appropriate save flow, updates the store, and closes on success.
2. Injecting analytics or chat widgets
- Open Page Settings and switch to the Library / SDK tab.
- Paste the script tag or URL into the Header SDK input for scripts that must run early (GTM bootstrap, font preloads, analytics tracker init).
- Click the
+button or press Enter to commit it to the list — entries are appended to the bottom. - For late-loading scripts (chat widgets, retargeting pixels, A/B test loaders) use the Footer SDK column instead — these inject before
</body>so they don't block the first paint. - Remove a row with the inline trash icon. To fix a typo, remove and re-add — there is no in-place edit.
- Click Save. The SSR renderer will inject the lists into the next request for this
(block_id, block_version_code).
3. Customizing settings per page version
- From the page selector, expand the page's submenu and switch to a different version of the same page.
- Open Page Settings. The dialog reads
block_version_settingsfor the active version offcreateBlockSlice, so each version maintains its own SEO and SDK payload. - Edit and save. Only the open version is updated — sibling versions and the active live version are untouched.
- Use Make live on the page selector to promote a version with the updated settings to production.
4. Running a seasonal / campaign variant
- Duplicate the page version (or create a new draft) so you have an alternate version code to edit.
- Open Page Settings on the new version. Override Default Title and Default Description with the campaign copy (e.g. "Holiday Sale - 30% Off Everything").
- Swap Meta Image to the campaign creative so social shares pick up the seasonal artwork.
- Add any campaign-specific tracking script (e.g. an attribution pixel) to Footer SDK.
- Save and Make live when the campaign starts; switch back by activating the previous version when it ends. The base SEO settings on the original version are preserved.
5. Adding structured data (JSON-LD)
- Generate the JSON-LD payload for the page (Product, Article, BreadcrumbList, FAQPage, etc.) using a generator or hand-written schema.
- Wrap it in
<script type="application/ld+json">...</script>. - Open Page Settings → Library / SDK and paste the full tag into Header SDK so it lands in
<head>. - Save. Validate the rendered output with Google's Rich Results Test by visiting the live URL.
6. Store Editor: draft vs unbundled theme
- For a freshly-installed theme that hasn't been unbundled yet, edits in Page Settings save through the draft theme save flow into the dev-portal draft DB.
- Once the merchant unbundles the theme,
themeCtx.isUnbundledflips totrueand the same Save button now routes through the unbundled Store Editor save flow. - No manual switching required — the routing logic in
useBlockSettings.tspicks the right save flow at call time based on the context flag.
Examples
Saved block_version_settings payload after editing both tabs:
{
"seo": {
"defaultTitle": "Mens Running Shoes - Free Shipping",
"prefixTitle": "Shop |",
"suffixTitle": "| Acme",
"defaultDescription": "Discover the latest mens running shoes with free shipping and 60-day returns. Cushioned, lightweight, road-ready.",
"defaultKeywords": "running shoes, mens, athletic, shipping, returns",
"defaultMetaImage": "https://cdn.example.com/og/running-shoes.jpg"
},
"headerSdk": [
"<script async src=\"https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX\"></script>",
"<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>"
],
"footerSdk": [
"<script src=\"https://widget.intercom.io/widget/abc123\"></script>",
"<script>!function(f,b,e){/* Facebook Pixel */}(...);</script>"
]
}Tips & gotchas
- The dialog only renders tabs for
block_type === "page". Opening it from a non-page block (component, dialog) currently shows a "Block settings coming soon" placeholder. - Edits are only persisted on Save — closing or cancelling discards the dialog's in-memory drafts. There is no autosave.
- Saved settings update
block_version_settingson the open block in the store immediately viasetBlockVersionSettings, so subsequent opens of the dialog reflect the new values without a reload or refetch. - Character limits in the UI (maxLength=100 / 300 / 30 / 30) are hard caps on input; the soft SEO limits (60 / 160 / 20 / valid URL) come from
validateSEOSettingsand only surface as recommendations. - The Library / SDK lists are stored as raw strings — invalid HTML or broken URLs will reach the live page as-is, so validate scripts before saving (especially script tags pulled from a vendor dashboard).
- Theme-wide SEO defaults live in a separate
EditorSettings.seobranch (DEFAULT_SEO_SETTINGSintypes/settings.types.ts); page-level settings are layered on top of those by the SSR renderer, so an empty page-level field falls through to the theme default. - The save step merges with any unknown keys already on
block_version_settings— external integrations can park custom keys (e.g.experiment_id,localized_seo) alongside the dialog-managed fields without fear of being stripped. - The dialog seeds its drafts only when
openflips to true — the dependency array is intentionally[open]to avoid re-seeding mid-edit if a store update fires. If you mutateblock_version_settingsfrom elsewhere while the dialog is open, your changes won't appear until next open. - The Social Preview card silently hides the image on load failure (
img onError) — if the preview disappears, the URL is probably wrong or the host is blocking hotlinks. - Keyword chips de-duplicate visually but the underlying string is just
values.join(', ')— pasting the same word twice produces a single chip but the comma-joined storage is preserved.