"Using the latest App Bridge script loaded from Shopify’s CDN" Failed

Hi everyone,

I’m developing an embedded Shopify app using Next.js and I’m failing one of the Embedded app checks. I can’t figure out what’s wrong.

Current status

:white_check_mark: Using session tokens for user authentication → passed
:cross_mark: Using the latest App Bridge script loaded from Shopify’s CDN → failed

The app loads correctly inside the iframe and works normally.


layout.tsx

This is what I render in my root layout:

...
<head>
  <meta
    name="shopify-api-key"
    content={process.env.NEXT_PUBLIC_SHOPIFY_API_KEY}
  />
  <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
...
</head>
...

Frame source output

When I inspect the iframe source, both the meta tag and script are present:

...<meta name="shopify-api-key" content="xxx"/><meta name="application-name" content="xxx"/><meta name="apple-mobile-web-app-capable" content="yes"/><meta name="apple-mobile-web-app-status-bar-style" content="default"/><meta name="apple-mobile-web-app-title" content="xxx"/><meta name="mobile-web-app-capable" content="yes"/><meta name="theme-color" content="#000000"/><title>Dashboard | xxx</title><meta name="description" content="xxx Dashboard - Dashboard"/><link rel="author" href="https://xxx.co"/><meta name="author" content="xxx"/><link rel="manifest" href="/manifest.webmanifest"/><meta name="creator" content="xxx"/><meta name="publisher" content="xxx"/><meta name="robots" content="noindex, nofollow"/><link rel="canonical" href="https://xxx.co/dashboard"/><link rel="alternate" hrefLang="tr" href="https://xxx.co/dashboard"/><link rel="alternate" hrefLang="en" href="https://xxx.co/en/dashboard"/><meta name="format-detection" content="telephone=no, address=no, email=no"/><meta property="og:title" content="Dashboard"/><meta property="og:description" content="xxx Dashboard - Dashboard"/><meta property="og:url" content="https://xxx.co/dashboard"/><meta property="og:site_name" content="xxx"/><meta property="og:locale" content="xxx"/><meta property="og:image" content="https://xxx.co/og-image.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Dashboard"/><meta property="og:locale:alternate" content="en_US"/><meta property="og:type" content="website"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:title" content="Dashboard"/><meta name="twitter:description" content="xxx Dashboard - Dashboard"/><meta name="twitter:image" content="https://xxx.co/og-image.png"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="icon" href="/favicon.ico" sizes="32x32"/><link rel="icon" href="/icon-16.png" sizes="16x16" type="image/png"/><link rel="icon" href="/icon-32.png" sizes="32x32" type="image/png"/><link rel="icon" href="/icon-48.png" sizes="48x48" type="image/png"/><link rel="icon" href="/icon-192.png" sizes="192x192" type="image/png"/><link rel="icon" href="/icon-512.png" sizes="512x512" type="image/png"/><link rel="apple-touch-icon" href="/apple-touch-icon.png"/><link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" type="image/png"/><script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>...

package.json

I intentionally do not install:

  • @shopify/app-bridge

  • @shopify/app-bridge-react

I’m using the CDN version according to Shopify documentation.


Environment

  • Next.js App Router

  • layout.tsx renders the head

  • app works inside iframe

  • session tokens pass validation

  • only the App Bridge CDN check fails


Question

Since the CDN script is clearly present in the rendered HTML, I don’t understand why Shopify’s check fails.

Has anyone experienced something similar?

Have you tried using curl to load your app home page to make sure that the app bridge script is SSR’d and not rendered on the client side?

That small detail might be causing the race condition.

Good catch — I tested this and confirmed something interesting.

Previously the route was redirecting to login, but I fixed the middleware so /dashboard/shopify always returns HTML and never redirects.

Now when I run:

curl -s x.com/dashboard/shopify

Note: My base App URL: x.com/dashboard/shopify

I get the full SSR HTML response, and it clearly contains both:

  • the shopify-api-key meta tag
  • the App Bridge CDN script

So the script is definitely server-rendered now, not client-injected.

However, even after this change — and after waiting quite a while for caches/checks to refresh — the Embedded App Check still fails with:

Using the latest App Bridge script loaded from Shopify’s CDN

So at this point:

:white_check_mark: SSR HTML contains the script
:white_check_mark: no redirect happens
:white_check_mark: curl confirms correct output
:cross_mark: Shopify check still reports failure

Does the checker expect the shopify-api-key meta tag and the App Bridge CDN script to be adjacent in the head (back-to-back), or in a specific order?

Right now they are both present in the initial HTML, but not necessarily next to each other. I’m wondering if the validator has stricter parsing requirements than documented.

Happy to provide more debugging info if needed.

Just following up in case anyone has seen this before — still stuck on the App Bridge CDN check.

Any help would be appreciated.

Hey @John_Doe could you share the full curl output with headers (curl -sI https://your-app-url) for the exact URL that’s configured as your App URL in the Partners dashboard? I just want to see if it’s returning a clean 200 with text/html content type.

Also, one thing worth trying, in your rendered HTML, the App Bridge script tag appears after some meta and link tags (PWA manifest, OG tags, icons, etc.). If you’re not already doing so, can you try moving both the shopify-api-key meta tag and the <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script> to be the very first elements inside <head>, before everything else. Just a a quick test that could unblock you. Let me know how it goes! If the issue persists, I can look into this further internally for you for sure.

@Alan_G Thanks for the suggestions — here’s the full curl output for the exact App URL configured in the Partners dashboard:

curl -sI https://my-domain.co/dashboard/shopify

HTTP/2 200 
date: Mon, 09 Feb 2026 21:28:58 GMT
content-type: text/html; charset=utf-8
server: cloudflare
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
strict-transport-security: max-age=31536000; includeSubDomains
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://static.cloudflareinsights.com https://www.paytr.com https://embed.tawk.to https://*.tawk.to https://cdn.shopify.com; style-src 'self' 'unsafe-inline' https://*.tawk.to; img-src 'self' data: blob: https: https://*.tawk.to; font-src 'self' data: https: https://*.tawk.to; connect-src 'self' https://my-domain.co https://www.google-analytics.com https://www.googletagmanager.com https://cloudflareinsights.com https://www.paytr.com https://api.ipify.org https://va.tawk.to https://*.tawk.to wss://*.tawk.to https://*.myshopify.com https://admin.shopify.com; frame-ancestors 'self' https://admin.shopify.com https://*.myshopify.com; frame-src 'self' https://www.paytr.com https://*.tawk.to; media-src 'self' data: blob: https://*.tawk.to; worker-src 'self' blob:; base-uri 'self'; form-action 'self'
x-locale: tr
x-pathname: /dashboard/shopify
x-shopify-embedded: true
x-middleware-rewrite: /dashboard/shopify
x-powered-by: Next.js
cache-control: private, no-cache, no-store, max-age=0, must-revalidate
cf-cache-status: DYNAMIC

It’s returning a clean 200 with text/html, as expected.


About the App Bridge script position

I’m using Next.js (App Router) and I’m explicitly trying to pin the Shopify meta tag and App Bridge script to the very top of <head>. Here is my root layout head section:

<head>
  <meta
    name="shopify-api-key"
    content={process.env.NEXT_PUBLIC_SHOPIFY_API_KEY}
  />
  <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
</head>

However, when I inspect the rendered HTML from the iframe source, Next.js still injects other meta tags before it:

<meta name="next-size-adjust" content=""/>
<meta name="shopify-api-key" content="..."/>
...
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>

So even though I place the tags first in my layout, Next.js is reordering parts of the <head> and I can’t fully control the final output.


What I need help with

Is there a Shopify-recommended or officially supported way to guarantee that:

  • shopify-api-key meta

  • App Bridge script

are literally the first elements in <head> when using frameworks like Next.js?

If you’ve seen a known workaround or best practice for React / Next.js embedded apps, I’d really appreciate guidance. I’m happy to adjust my setup — I just want to make sure I’m aligning with Shopify’s expectations.

I also tried a few alternative approaches like injecting via dangerouslySetInnerHTML and splitting the head into a dedicated head.tsx file, but in those cases the App Bridge script didn’t render at all and the CDN script completely disappeared from the final HTML. The more I tried to force the meta/script to the very top, the worse the output became, so I reverted back to a stable version.

Thanks again for looking into this!

Hey @John_Doe - thanks for sharing all of that, really helpful. One thing I noticed in your curl headers, your content secure connect-src directive doesn’t include https://cdn.shopify.com. The script tag will load fine since it’s in your script-src, but App Bridge may need to make runtime connections after loading that would get blocked by that. Could be what’s causing the checker to fail even though the tag is clearly present in the HTML.

As a quick test, could you try either adding https://cdn.shopify.com to your connect-src or temporarily stripping the CSP header entirely just to see if the check passes? That’ll help narrow things down. If that doesn’t do it I may need to dig into this a bit more on our end to see exactly what the checker is validating. Let me know how it goes!

Hi @Alan_G — thanks again for the help, really appreciate you digging into this.

I tried both of the tests you suggested:

  1. I added https://cdn.shopify.com to connect-src

  2. I also temporarily removed the CSP header entirely

After each change I redeployed, opened the embedded app, and waited more than 2 hours to make sure any caching/check delays weren’t involved. Unfortunately the checker is still failing in both cases.

Here are the headers when CSP is enabled and includes cdn.shopify.com:

curl -sI https://my-domain.co/dashboard/shopify
HTTP/2 200 
date: Tue, 10 Feb 2026 23:24:52 GMT
content-type: text/html; charset=utf-8
server: cloudflare
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
strict-transport-security: max-age=31536000; includeSubDomains
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://static.cloudflareinsights.com https://www.paytr.com https://embed.tawk.to https://*.tawk.to https://cdn.shopify.com; style-src 'self' 'unsafe-inline' https://*.tawk.to; img-src 'self' data: blob: https: https://*.tawk.to; font-src 'self' data: https: https://*.tawk.to; connect-src 'self' https://my-domain.co https://www.google-analytics.com https://www.googletagmanager.com https://cloudflareinsights.com https://www.paytr.com https://api.ipify.org https://va.tawk.to https://*.tawk.to wss://*.tawk.to https://*.myshopify.com https://admin.shopify.com https://cdn.shopify.com; frame-ancestors 'self' https://admin.shopify.com https://*.myshopify.com; frame-src 'self' https://www.paytr.com https://*.tawk.to; media-src 'self' data: blob: https://*.tawk.to; worker-src 'self' blob:; base-uri 'self'; form-action 'self'
x-locale: tr
x-pathname: /dashboard/shopify
x-shopify-embedded: true
x-middleware-rewrite: /dashboard/shopify
x-powered-by: Next.js
cache-control: private, no-cache, no-store, max-age=0, must-revalidate
cf-cache-status: DYNAMIC

And here are the headers after completely removing CSP:

curl -sI https://my-domain.co/dashboard/shopify
HTTP/2 200 
date: Tue, 10 Feb 2026 23:33:34 GMT
content-type: text/html; charset=utf-8
server: cloudflare
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
strict-transport-security: max-age=31536000; includeSubDomains
x-locale: tr
x-pathname: /dashboard/shopify
x-shopify-embedded: true
x-middleware-rewrite: /dashboard/shopify
x-powered-by: Next.js
cache-control: private, no-cache, no-store, max-age=0, must-revalidate
cf-cache-status: DYNAMIC

Since the checker still fails even with CSP fully removed, it seems like something else is being validated. Happy to provide anything else you need to debug this further on your side.

Thanks again for your help!

Hi @Alan_G,

I’m still stuck on this issue and can’t move forward. I’d really appreciate any guidance if you have a moment.

Thanks in advance!

Hey @John_Doe, sorry for the wait here, and thanks for testing all of that. I think we can rule out CSP being the issue at this point.

One thing I noticed is that your app is sitting behind Cloudflare. I can’t say for sure if this is what’s happening, but their bot protection features are known to intercept automated requests with challenge pages. Could you try temporarily setting your Cloudflare security level to “Essentially Off” (or adding a firewall rule to allow automated requests through to your app URL) and see if the check passes after that?

Also, one more thing that would be super helpful, could you run curl -s against your App URL and share the HTML body output if possible? Just want to confirm what the checker is actually parsing when it fetches the page. That’ll help us narrow this down.