Second customer-account.profile.block.render block never mounts — wrapper created but stays empty (no iframe), always assigned placementReference: "PAGE_TITLE"

We have two customer-account UI extensions targeting the same extension point, customer-account.profile.block.render, both added to the Profile page’s Main section via the checkout & accounts editor. One works correctly the other doesn’t. The other’s extension-point wrapper is created in the DOM, but nothing ever mounts inside it — no iframe, no content, and our extension’s own code (confirmed via debug logging) never even runs. This reproduces regardless of where in the section the block is placed, and regardless of whether the other, working block is present or removed entirely.

Setup

  • api_version = "2026-07" for both extensions.
  • Both extensions declare the identical target in shopify.extension.toml:
[[extensions.targeting]]
module = "./src/Block.jsx"
target = "customer-account.profile.block.render"

[extensions.capabilities]
network_access = true

  • Both use the same entry-point shape:
import "@shopify/ui-extensions/preact";
import { render } from "preact";

export default async () => {
  render(<Extension />, document.body);
};

  • “Loyalty Birthday” (handle: customer-account-birthday) — works correctly, renders every time.
  • “Store Credit Tier” (handle: customer-account-store-credit-tier) — never renders, in the editor preview or on the live storefront account page, for any test customer.

What we’ve tested

  1. Confirmed the backend/data side is fine. Store Credit Tier’s extension calls our own app backend over network_access to read a value and display it. We added logging on our backend and watched it live while reloading the account page — zero requests ever arrive, even though the working extension’s equivalent backend call fires normally.

  2. Confirmed the extension script itself loads. The Network tab shows the compiled extension bundle (.js) being fetched successfully (200, served from Shopify’s extension CDN), so the extension is registered and its code is being served.

  3. Added an unconditional debug render — an <s-text> with no dependency on any fetched data, rendered synchronously on mount regardless of state. It never appears anywhere on the page, in the editor or live. This rules out a data/timing bug in our own render logic; the component’s render function never appears to execute at all, or has nowhere to render into.

  4. Ruled out placement/positioning in the editor UI. We tried every position within the same section (above, below, every reorder), including removing the working “Loyalty Birthday” block entirely so “Store Credit Tier” was the only block in that section. No change in behavior.

  5. Inspected the registered extension config directly via the page’s extensions-data serialized script tag. This is where we found the actual difference — both extensions declare the same target, but the placementReference assigned by the editor is consistently different, every time, regardless of position or what else is present:

// Loyalty Birthday (renders correctly)
"targets": [{
  "target": "customer-account.profile.block.render",
  "placementReference": "PROFILE1",
  "position": 0
}]

// Store Credit Tier (never renders)
"targets": [{
  "target": "customer-account.profile.block.render",
  "placementReference": "PAGE_TITLE",
  "position": 0
}]

  1. Inspected the actual rendered DOM for both. Birthday’s wrapper is fully populated with real rendered content:
<div data-testid="ui-extension-point-wrapper">
  <div ...>
    <h2>Birthday</h2>
    <!-- full rendered form content -->
  </div>
</div>

Store Credit Tier’s wrapper (with Birthday removed, so this is the only block in the section) is created, but completely empty — no iframe, no shadow content, nothing:

<div data-testid="ui-extension-point-wrapper">
  <div data-inspector-id="[redacted]"></div>
</div>

  1. Checked the official docs ( Customer account UI extensions ) to confirm we weren’t missing a separate/more specific target string for a different region of the Profile page. There’s only one documented target for the whole page (customer-account.profile.block.render), with sub-placement described as fully merchant-controlled via the editor — i.e. this is documented as something that should just work for any block dropped anywhere on the page.

What we think is going on

Given the wrapper element is created (so the anchor point itself is recognized) but never gets an iframe or any content mounted into it — even though our extension’s script loads successfully and is confirmed active in extensions-data — this looks like the account-web runtime doesn’t actually mount a third-party app block for the PAGE_TITLE placement on the Profile page, while it does for PROFILE1. Since the editor consistently assigns our block to PAGE_TITLE no matter where we drop it (even as the only block in the section), we don’t have a way to force PROFILE1 from our side — this isn’t something controllable from shopify.extension.toml or the extension source, since both extensions declare the identical target.

What we’re hoping to learn

  • Is PAGE_TITLE a currently-unsupported/reserved placement for third-party app blocks on customer-account.profile.block.render, and is there any way to avoid the editor assigning it?
  • Is this a known issue, and if so is there a workaround or fix timeline?

Hey @Jeff_Nelon, thank you for a detailed report of the issue. PAGE_TITLE is a supported placement for customer-account.profile.block.render.

Since the wrapper is being created but the extension never appears to mount, I wonder if there is a runtime failure going on for this activation. Can you DM me a test shop with the reproduction, the app and extension identifiers so I can take a closer look?

@avocadomayo, stupid question, How do I DM you? The only thing I’ve ever done is post reply’s on this forum before and I’m hesitant to drop app specific details into the public forum.

No worries, if you click on my avatar, do you see a Message button?

No I do not. also don’t see one when I go to your profile either.

Strange! I sent you a DM. Can you see if you are able to message me back?

I was able to resolve the issue on my own. Here’s our fix in case anyone else runs into something similar. Our block’s wrapper mounted, but stayed empty too — no error, no failed network call, nothing. We traced it to a state-driven re-render: useState/useEffect where the effect fetches data and then calls setState to swap loading→loaded. The first render (before the effect runs) paints fine; the re-render triggered by that later setState never reaches the DOM.

Fix: don’t render before your data is ready. Resolve everything in the extension’s export default async () => {...} entry point, then call render() exactly once with final props — no second render for the bug to break:

export default async () => {
  const data = await fetchSomething();
  render(<Extension data={data} />, document.body);
};

function Extension({ data }) {
  return <RealContent data={data} />;
}

Also worth checking: every extension needs its own tsconfig.json with "jsxImportSource": "preact". We had one extension missing that file, and it never mounted at all — not even a static string with no logic. Once we noticed a working sibling extension had this file and the broken one didn’t, adding it fixed that one.