Browse 50 demo/seeded portfolios on Showcase →
Field NotesHosting SCORM9 min read

How to embed a SCORM course in plain HTML.

No CMS, no plugin — one tag, hosted playback, and the headers that actually break it.

No WordPress. No page builder. Just a folder of HTML you push to a static host, or a docs portal your team hand-maintains, and a SCORM module that needs to live on one of its pages. Good news: this is the easiest embed there is. You own every line of markup, Training OS runs the package, and the whole job is one tag. The traps are not in the HTML — they're in the headers and the build step.

I've wired hosted modules into Jekyll sites, Hugo docs, a hand-rolled intranet, and one terrifying 2009-era SharePoint page. The snippet was identical every time. What broke was always the same short list: a Content-Security-Policy header, a markdown processor quietly escaping the iframe, or someone testing while logged in. Let's skip those afternoons.

Get the launch URL working in a bare browser tab first. If it doesn't play there, no amount of HTML will fix it.— the rule that saves you a debugging session

Get the launch URL working first.

Before you write a single tag, paste your Training OS URL straight into the address bar and run the module. Debugging is dramatically faster when the player is known-good in isolation — you want to know the course works beforeyou add your own page, your own headers, and your own build pipeline to the list of suspects. Training OS extracts the package and serves the runtime over HTTPS with a stable launch URL; your page's only job is to send people to it.

If you're still deciding where the package should live in the first place, start with where to host a SCORM file and come back once you have a URL that plays.

Two things to check first.

There are exactly two prerequisites, and skipping either is the cause of most "it works on my machine" tickets.

1. Serve the parent page over HTTPS.

If your Training OS launch URL is HTTPS (it is), the page embedding it must be HTTPS too. An HTTP page loading an HTTPS resource trips mixed-content protection — browsers block the script or the frame in predictable, silent ways. Most static hosts give you HTTPS for free; turn it on before you embed.

2. Know whether your deployment target injects CSP.

Plenty of internal portals and corporate hosts add a Content-Security-Policyheader that restricts which origins an iframe may load. If yours does, an iframe to Training OS will render as nothing until someone allows the domain. On a public static host this is usually a non-issue; on an intranet, find out early — you may need IT to whitelist the Training OS origin, and that's a request with a lead time.

The two snippets, ranked.

Once the URL plays in a bare tab, you have two ways to put it on the page. Ranked by how rarely they break:

1

A direct link that opens in a new tab.

Best for: static sites, docs portals, anything behind a CSP you don't control

Recommended

The simplest integration is an anchor. Set target="_blank" so it opens in a dedicated window — the same way most LMSes launch content — and always pair it with rel="noopener noreferrer". The noopener half closes a tab-nabbing hole that any _blank link leaves open; noreferrer adds stricter referrer privacy. Write real CTA copy on the link, not a naked URL.

What's good

  • No iframe height tuning and no CSP frame-ancestors fight
  • Full-window playback — how Storyline and Rise expect to run
  • Survives markdown processors that strip raw HTML

What's not

  • Sends the learner to a new tab rather than inline
  • Not the embedded-on-the-page feel a long article wants
Direct link · paste anywhere in your HTMLHTML
<a
  href="https://app.trainingos.com/m/your-module"
  target="_blank"
  rel="noopener noreferrer"
>Launch the module</a>
2

A standard iframe.

Best for: long-form docs pages where the module should sit inline

If you've validated CSP

Use a normal <iframe> with src set to your launch URL and width="100%". Give it a fixed height tall enough for the player — usually 600–800px depending on chrome and aspect ratio. Add allow="fullscreen" when your authoring tool uses the fullscreen API, and a descriptive titlefor accessibility. Then test under the exact same domain policy you'll ship to production — not just localhost.

What's good

  • Module feels like part of the page
  • Good for documentation pages with an inline demo

What's not

  • CSP and X-Frame-Options headers love to blank it out
  • Markdown processors escape raw iframe tags at build time
  • Cramped on narrow mobile layouts if you under-size the height
iframe · paste into a raw-HTML blockHTML
<iframe
  src="https://app.trainingos.com/m/your-module"
  width="100%"  height="760"
  allow="fullscreen"
  title="Onboarding module — interactive"
></iframe>
If you skip this

If the parent page lives on a different site than the player, verify SCORM behavior before you trust the iframe. Some packages assume a top-level window for completion or bookmarking, so tracking can go flaky inside a frame even when the course renders fine. If it does, drop to the link pattern — the player becomes the top window and the problem evaporates.

CSP and the blank-box problem.

When an iframe renders as an empty rectangle, it is almost never the module. It's the page around it. Three culprits, in the order I actually hit them:

1. A frame-blocking header on the parent page.

Open the console on the live page. A frame-ancestors violation from a Content-Security-Policy header, or a legacy X-Frame-Optionsheader, will tell you exactly why the browser refused to paint the frame. Allow the Training OS origin in that policy — or, if you can't edit headers, use the link and move on.

2. The build step ate your iframe.

Static site generators run your content through a markdown processor that escapes or strips raw HTML by default. Your iframe looks perfect in the source file and is simply gone in the built page. Wrap it in whatever raw-HTML escape hatch your generator provides — an MDX block, a shortcode, a passthrough fence — so it survives the build.

3. Strict privacy settings break the session.

Test in Chrome and Safari with third-party cookies restricted. Some learners run hardened privacy settings that interfere with session state inside a cross-site iframe — playback starts, but resume or completion quietly fails. If your audience skews privacy-conscious, the link pattern is the safer default.

The mistake everyone makes

Editing the page while logged into your host's dashboard, seeing the module load, and shipping. Your authenticated session and your local preview both bypass the headers and the build pipeline that a real visitor hits. Always verify from a logged-out incognito window against the production URL— that's the only view that matches what a learner actually sees.

Before you commit and deploy.

Validate in staging, then run this once from a logged-out incognito window on production:

  • The link or iframe loads the running module — not a download prompt or a blank box.
  • The parent page is HTTPS, with no mixed-content warning in the console.
  • If you used an iframe, your generator did not escape it — view source and confirm the raw tag survived.
  • Completion, score, and resume behave the way the package was published to behave.
  • It looks right on a phone — narrow layouts crush short iframes, so bump the height or fall back to the link.

That's the whole job. For the CMS-flavored version of this, see embedding SCORM in WordPress; if completion is the thing misbehaving, the completion-status field is where to look next.

Frequently asked questions.

Do I need anything besides an anchor tag to launch a SCORM module?

No. If Training OS hosts the package, a single link with target=_blank and rel=noopener noreferrer launches it in a full window. That pattern dodges most cross-origin iframe headaches and matches how a real LMS opens content.

Why is my embedded module a blank box on the live site but fine locally?

Almost always a Content-Security-Policy frame-ancestors rule or an X-Frame-Options header on the parent page, or a markdown processor that stripped the raw iframe at build time. Open the console, look for a blocked-frame error, and confirm the launch URL works on its own in a new tab.

My static site generator keeps eating the iframe. What do I do?

Markdown processors escape raw HTML by default. Put the iframe inside an explicit raw-HTML block (MDX, a shortcode, or your generator's passthrough syntax) so it survives the build, or fall back to a plain link.

Completion is not reporting inside the iframe. Is that a hosting bug?

Usually not. Some packages assume a top-level window for completion and bookmarking. Test with strict third-party-cookie settings, and if tracking stays flaky, switch to the link pattern so the player runs as the top window.

/06  Try it for free

Drop a SCORM file. See it live in 11 minutes.

Free for 3 modules. No card. Lifetime is $149 once. You read the article, you know how this is supposed to work — see it in your own browser.

Upload your first SCORM →
JV

About the author

Jon Vig · Ex-LMS engineer · founder, Training OS. Jon spent years building LMS internals before founding Training OS. He writes Field Notes on hosting SCORM, xAPI, and shipping instructional design work that clients can actually open.

/07Keep readingRelated notes