You shipped the walkthrough. The prose landed. The interesting bit often lived across three files, so you pasted a raw.githubusercontent.com link and hoped the reader would puzzle out how the pieces touch. I've done that. So have most of us who write long technical posts. The link is honest; the reading experience often isn't.
This piece is about Embedacode, a Lit custom element named <embeda-code> that renders a read-only tree, **Prism** highlighting, copy, ZIP download, and optional per-file blurbs under the code. GitHub is optional: you can feed static files only and never touch the API. Either way, readers get a focused viewer instead of a full IDE or a live playground.
I unpack why “just link GitHub” frays, how configuration merges when you add repoUrl, what the pipeline does before paint (normalization, byte caps, stale fetch abandonment), and where embedacode still leaves gaps today: English-only chrome, six bundled languages with a JavaScript fallback, no responsive layout story promised in the pinned README, and the limits of what v2.1.0 actually ships versus what I’m planning on my side (more on that in the roadmap section).
If you’re comparing against CodePen or StackBlitz, the distinction is simple: those tools optimize for running code. Embedacode optimizes for reading code. That’s a different threat model (no eval of user bundles), different bundle cost, and different expectations about dependencies.
Why snippets and repo links fight you in tutorials
Short code fences are great for punchy examples. They’re terrible when the point is *relationships*: the test beside the module, the config beside the entrypoint, the README that names the run order. Copy-paste snippets drift from source control; the post becomes the buggy fork. Linking the whole repository externalizes the problem to the reader—new tab, GitHub chrome, mobile zoom gymnastics.
Version skew hurts both sides. You tweak the snippet for clarity; the repo moves on; nobody updates the post. A viewer that points at a public tree—or at least shows multiple paths together—cuts the “open five tabs” ritual even when it doesn’t replace GitHub entirely.
“Just embed GitHub” sounds clean until you count the moving parts. Public API calls ride GitHub’s 60 requests per hour bucket for unauthenticated use; the README tells heavy sites to front the API with a tokened proxy. Tree mode caps at 2500 entries client-side; folders start collapsed; with repoUrl and no explicit defaultFile, the first README.md wins when present (root, then nested). Raw file fetches and the tree API are browser-friendly from a CORS perspective, but rate limits are still real—plan demos accordingly.
<embeda-code> → tree + highlight + copy, still read-onlyThe diagram is crude on purpose. The win isn’t animation; it’s orientation without spinning up a runtime that executes arbitrary dependencies. Playgrounds sell execution. Documentation usually needs structure, contrast, and a copy button.
Static site generators and Next.js pages can treat the element like any other client-side widget: load the standalone script once per layout, grab the node after DOMContentLoaded, assign config, and let Lit handle the rest.
Embedacode — request path
Hover or focus a step to trace the read-only viewer pipeline
Read-only tree, Prism, and shadow DOM
Feature map
UI chrome, syntax coverage, and config front doors
Themes
- light / dark on host + toolbar toggle
- persistence:
localStorage· prefers-color-scheme - skin: CSS variables on
:host
i18n
Toolbar copy is English-only in source today — no locale knob or translation bundle in the pinned README story.
Six grammars
- TypeScript
- JavaScript
- CSS
- JSON
- Bash
- Markdown
Other extensions fall back to JavaScript grammar unless you override language in merged files.
files[]
Static tree: path + inline content string.
No GitHub API · best for tutorials and decks.
repoUrl
Public repo: tree API + raw.githubusercontent.com bodies.
Merges with files; local entries win on path clash.
Remote content
content as URL → fetch on selection; LRU cache bounded by remoteCacheMaxEntries.
CORS must allow the embedding origin.
Embedacode’s stack is TypeScript, Lit 3.x, PrismJS 1.x, JSZip, Vite, Vitest, Playwright, ESLint 9—see package.json and the README’s layout table. Directories map cleanly: src/editor/ hosts the element and its styles; src/highlight/ wires Prism and prepareCodeView; src/tree/ builds the nested tree and merge rules; src/github/ lists repositories through the GitHub API; examples/ and e2e/fixtures/ hold runnable HTML for local checks.
The element keeps styles inside the shadow root so your blog’s pre rules don’t stage a coup. Theme it with the theme attribute and CSS variables on :host when you want the panel to match brand colors. Toolbar actions include copy, ZIP download, theme toggle with persistence in localStorage under embedacode-theme, and fullscreen with Escape to exit.
Distribution is pragmatic. Ship a standalone UMD file that bundles Lit and Prism: dist/embedacode.standalone.js, pinned in README to the v2.1.0 release URL. Or consume @mikehenken/embedacode@2.1.0 from GitHub Packages with the scoped .npmrc registry line. The library build marks lit and prismjs as externals for ES/UMD consumers who already carry those peers; the standalone build inlines them so one script tag registers the custom element.
src/index.ts re-exports types, the highlighter helpers, and the element class. The public custom element tag stays embeda-code; registration happens on import.
Two front doors: files vs repoUrl
The README’s config intro is the contract: use `files` alone for snippets and static trees; add `repoUrl` only when pulling a public GitHub repository. When both are present, `mergeFileLists` keeps remote paths but your `files` entries override on path clash—handy for swapping a README or injecting a corrected excerpt without forking the upstream repo.
Each file object needs path and content (string or URL fetched when the user selects the file). Optional language sets the Prism id; optional description renders under the code for that active file. Paths with / segments become nested folders in the sidebar.
Demo A — hand-built tree (TypeScript tutorial fabric)
For posts that don’t need GitHub at all, pass a small array. This is codebase A: invented paths, inline strings only, no tree API traffic.
The reader sees src/ collapse and expand like a real repo. Descriptions show only for the active file—use them to narrate *why* a file matters.
Pointing at a public repo without cloning the reader
When repoUrl is set, the element flips into async mode: loading state, GitHub tree fetch, merge, cap, and rebuild the sidebar. A load-sequence counter throws away stale completions if the author updates config mid-flight. Sidebar filter is substring match on paths and names—the README calls it Go to file. Errors from GitHub surface as messages; the component can fall back to files-only content if the fetch fails.
Data flow
Data flow
Config drives GitHub fetch and merge into _files, then tree, resolved content, prepareCodeView, and Lit render. With files only, the GitHub stages are skipped.
- config property
- fetchGitHubRepo
- mergeFileLists
- _files
- treeNodes + expandedDirs
- resolvedContent LRU
- prepareCodeView
- Lit render
Selection resolves remote or inline content, then highlighting runs before the shell paints lines.
Demo B — repoUrl on a different codebase
Codebase B points at Embedacode’s own GitHub repository: readers browse src/editor, src/highlight, and src/github while reading about the viewer.
If you need a narrative README that disagrees with upstream for one lesson, override just that path via files—the merge rule keeps your copy.
Remote content: pin a canonical file without vendoring the repo
Sometimes you want one upstream file to stay authoritative—package.json, an OpenAPI fragment, a policy YAML—without mirroring the whole tree. The README shows content as a raw.githubusercontent.com string; the viewer fetches on selection. URLs must be CORS-friendly from the browser.
Default caching keeps one remote body; raise remoteCacheMaxEntries (clamped between 1 and 256) if you expect readers to flip among several hot-linked files. AbortControllers cancel in-flight fetches when the user switches files quickly.
Demo C — two orgs, two JSON bodies
Codebase C uses remote bodies only. The live demo pulls `microsoft/TypeScript` main package.json and `cloudflare/workers-sdk` main package.json as raw GitHub URLs; nested under upstream/, the two JSON bodies make the ecosystem contrast obvious without vendoring either repo.
Authors sometimes worry about hot-linking raw.githubusercontent.com. In practice it’s the same surface area as any tutorial that already embeds that host; the viewer centralizes fetches behind one component. Pin a tag or commit SHA in the raw URL if you need stability more than “latest main.”
From config to prepareCodeView
Once text is inline or resolved from a remote URL, prepareCodeView normalizes: strip BOM, normalize CRLF to LF, then enforce hard caps—450,000 bytes, 12,000 lines, 200,000 Prism characters—with explicit truncation messaging. Word wrap toggles structure: grid-per-line layout when wrapping is on; horizontal scroll when wordWrap is false.
Highlighting uses Prism output injected through unsafeHTML in trusted-content scenarios. Oversized or erroring highlight paths fall back to escaped plain text. Remote resolution stores bodies in a bounded map with LRU semantics tied to remoteCacheMaxEntries.
Bundled languages, themes, and the honesty corner
Syntax coverage isn’t “all of Prism.” Out of the box, embedacode highlights six languages—TypeScript, JavaScript, CSS, JSON, Bash, Markdown—through Prism’s component packs. highlight() falls back to JavaScript highlighting when an id isn’t loaded. GitHub’s language map knows JSX, TSX, and HTML extensions, but those Prism bundles aren’t imported—so repo-fetched .tsx may color like JS unless you override language through merged files.
Today’s theme story is light / dark on the host, localStorage persistence, then prefers-color-scheme. Toolbar copy is English-only in source, and the README doesn’t promise responsive breakpoints; the sidebar stays desktop-first unless you wrap it yourself.
Build, CI, and how releases leave the repo
Build · CI · publish
From TypeScript sources to package consumers
push + PR to main — mandatory gate (no continue-on-error)
App or docs site: npm install @mikehenken/embedacode with .npmrc registry line — or ship embedacode.standalone.js for one script tag.
The build script chains tsc, declaration emit, library Vite build, then standalone UMD without externals so one file carries everything. CI on main runs lint, typecheck, unit, integration, then build + Playwright Chromium E2E as a mandatory gate. Publishing triggers on release published or workflow_dispatch, publishing to GitHub Packages.
Where I’m taking embedacode next
I’m Mike Henken; I created embedacode and ship it as open source. Everything above—caps, merge semantics, toolbar behavior, CI, and the v2.1.0 standalone bundle—is what you can verify in the repo today. The pinned README doesn’t spell out a dated multi-release roadmap, so I’ll be explicit here about what I’m aiming for versus what is already shipped.
Already shipped in v2.1.0: read-only tree, Prism-backed highlighting for the six bundled languages, copy and ZIP, theme toggle with persistence, repoUrl plus files merge rules, remote raw URLs, and the hard limits documented in prepareCodeView.
Planned direction (honest caveats: priorities shift, no ETAs): I want more highlighter languages (for example TSX, JSX, and HTML) so fewer repo files need manual language overrides. Multi-language UI matters to me: today’s toolbar strings are English-only, and I’d like proper i18n when I can justify the work. Mobile and responsive layout are weak spots—the sidebar is desktop-first, and I intend to improve small-screen ergonomics rather than pretend the README already promises it. I’m also thinking about a richer theme engine (presets and safer author hooks without ballooning the default bundle) and a careful plugin or extension surface for custom toolbar actions or file metadata—something teams could adopt without fork churn.
None of that replaces the README contract; it’s how I’m steering the project in my own planning notes until those items land in tagged releases.
When to reach for it—and when to wrap something else
Good fit: tutorials, courses, documentation sites, companion public repositories where read-only browsing beats execution; static files for conference decks; remote raw URLs when you need a living upstream single file.
Skip or wrap: private GitHub without your own proxy; localized UI requirements; heavy reliance on JSX/TSX/HTML coloring without supplying language overrides; anything that needs a sandboxed runtime.
Own the reading experience
Embedacode doesn’t try to be the smartest thing in your stack. It tries to be the legible thing: tree on the left, code on the right, copy and zip on the toolbar, shadow DOM keeping your design system out of a fistfight with Prism’s styles.
Pick the integration path that matches your liability budget. Static files keep the page self-contained. repoUrl trades simplicity for freshness when your companion repo is public. Remote content splits the difference for single-file truth.
Repo: github.com/mikehenken/embedacode. Install via GitHub Packages as documented in the README; pin the standalone script to v2.1.0 if you want immutable behavior from release assets.
Examples
The guides below are not strawmen. They are some of the best teaching and reference material on the web. They earn trust by staging concepts step by step. That same staging almost always splits code across files, repos, or external tools. I built embedacode so readers can stay beside the narrative and still see the whole project as a navigable surface: multiple files, a sane theme, and when it helps, a straight line to raw source on GitHub. The point is not to replace official sandboxes or CLIs. It is to cut the copy-paste scavenger hunt that technical authors know too well.
Next.js Learn (App Router dashboard course)
The Next.js Learn dashboard track from Vercel is exactly the kind of curriculum teams assign internally. Chapters introduce routing, data fetching, and auth with tight prose, while the runnable app accumulates in a companion tree. That separation is pedagogically sound and still taxing. Readers jump between the lesson and the `next-learn` repo to see where a fence snippet lands.
What worked: Clear chapter boundaries, incremental feature introduction, and a real multi-route app instead of toy fragments.
Where friction shows up: The mental map is split: article, then repo branch or folder, then back. You reconstruct app/, lib/, and server actions from memory more than from a single view.
How embedacode helps: I keep the official order in prose and open one embed that mirrors the chapter’s file set (or the delta for that chapter). Readers scan filenames and imports next to the text. If they need the canonical upstream, raw GitHub stays one hop away for the paths we pin.
Voice note: This is the workflow I wanted when I was writing long-form Next.js notes and still needed newcomers to trust the file tree.
Auth0 React SPA quickstart
Auth0’s Add Login to Your React Application guide is thorough: CLI steps, .env, Auth0Provider, several components, and styling blocks in sequence. The official samples live in auth0-react-samples on GitHub. The doc is not wrong to use many fences; auth touches many files by default.
What worked: Precise sequencing, environment clarity, and maintained sample repos that track common SPA stacks.
Where friction shows up: Tutorial sprawl. You paste into main.tsx, then App, then CSS, then wonder which import broke. Doc engineers updating commits across doc and sample feel the same drift.
How embedacode helps: I bundle the same files the quickstart names in one synchronized editor chrome: entry, provider, route or guard, UI shell. File tabs make boundaries obvious so the narrative order matches the dependency graph readers actually need.
Voice note: Mike Henken (that’s me): I care about parity between public docs and internal runbooks. One embed reduces “doc says X, repo shows Y” arguments.
React docs: Tic-Tac-Toe tutorial
The React Tic-Tac-Toe tutorial on react.dev is a canonical on-ramp. It pairs long explanation with incremental edits and points readers toward a CodeSandbox-style workspace (fork, edit App.js / styles.css, optionally download). The companion here is an interactive editor surface, not a single advertised GitHub app in the hero of the page.
What worked: Hooks-first pedagogy, tight feedback loops inside the sandbox, and status as a shared assignment readers can compare notes on.
Where friction shows up: Engineers who think in repos still translate sandbox files into a folder layout. Offline or locked-down environments can block the hosted editor path.
How embedacode helps: I can mirror staged snapshots (early board, lift state up, winner logic) as multi-file bundles anchored in the article. Readers who want a repo-shaped mental model get it without losing React’s recommended sequence. Sandboxes remain valid for “live tinker”; embedacode fills the documentation-shaped view.
Kubernetes Guestbook (PHP + Redis)
The Kubernetes docs’ Guestbook example walks through Redis followers, a leader service, and a PHP front end with multiple manifests and kubectl apply -f https://k8s.io/examples/... URLs. Source examples also live under the kubernetes/website tree (paths linked as raw from the tutorial).
What worked: Operations realism, trust in official examples, and commands tied to real YAML.
Where friction shows up: Several similar-looking manifests and services in a row. “Which file did I just apply?” is a common question in workshops. Platform authors paste fragments into wikis and lose ordering.
How embedacode helps: I line up Deployment, Service, and frontend manifests in one tabbed view that follows the section order of the doc. Readers compare labels and selectors without alt-tabbing across browser tabs. Raw paths from k8s.io/examples stay cited so auditors can verify bytes.
Voice note: This is the class of guide I hand to documentation engineers who own internal Kubernetes golden paths.
Stripe: Accept a payment (web)
Stripe’s Accept a payment doc is the default reference for web integrations. It is long, switches platform targets, and splits server and client responsibilities across headings. Client snippets reference stripe-js and related materials rather than one obvious “article repo” in the opening.
What worked: Depth, compliance awareness, and clear separation of publishable key vs secret key duties.
Where friction shows up: Teams internalize one vertical slice (say Node + React) but the page still forces assembly from several sections. Internal wikis often duplicate Stripe’s headings and still scatter files.
How embedacode helps: I collapse the slice for one chosen stack into a single embed: server route or API handler plus client checkout component plus config stub. Headings in prose can match Stripe’s for traceability without retyping their paragraphs.
Voice note: I am not duplicating Stripe’s legal text; I am giving engineers a coherent file lens for the integration they are actually shipping.
MDN: Getting started with React
MDN’s Getting started with React lesson is authoritative: Vite-centric setup, JSX context, and onward links across the React module. MDN also points readers to the Scrimba learn React partner track for video-first practice.
What worked: Neutral tone, standards-aligned terminology, and a structured learning path across multiple MDN pages.
Where friction shows up: The “one repo in the hero” pattern is not the point of MDN; hands-on learners may bounce to Scrimba or reconstruct Vite files locally. Air-gapped teams need an offline-friendly file picture.
How embedacode helps: I can attach a consistent Vite + React starter that tracks MDN’s filenames and lesson order while keeping readers on MDN’s pages. Where Scrimba is blocked, the embed still shows src/main.jsx, components, and CSS in one place.
Voice note: I still send people to MDN first. In-article embeds are the companion that matches how I outline internal workshops.
Closing stance
Trusted tutorials fragment because they must. In-article embed blocks are my answer to the aggregation gap: same pedagogy, less context switching, clearer file boundaries, and room for theme and GitHub fidelity where teams need receipts.
References
- Henken M. Embedacode README and repository. https://github.com/mikehenken/embedacode
- Embedacode v2.1.0 release assets (standalone bundle). https://github.com/mikehenken/embedacode/releases/tag/v2.1.0
- GitHub Docs — Working with the npm registry (GitHub Packages). https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry
- PrismJS. https://prismjs.com/
- Lit. https://lit.dev/