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.
If you're comparing against CodePen or StackBlitz, the distinction is simple: those tools optimize for running code. Embedacode optimizes for reading code.
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.
<embeda-code> → tree + highlight + copy, still read-onlyStatic 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
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.
Feature map
Feature map
Surface area → input modes → what the reader experiences
UI chrome + constraints
What’s visible (or intentionally absent) regardless of where code comes from.
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 unless you override language in merged files.
Three front doors
All paths become the same internal file tree - but they differ in where content is resolved and what can fail.
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.
Regardless of input, the reader ends up in a read-only shell with a tree, syntax highlight, and copy affordances - without leaving the article context.
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
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.
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.
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, your files entries override on path clash.
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.
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.
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.
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. URLs must be CORS-friendly from the browser.
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.
From config to prepareCodeView
Once text is inline or resolved from a remote URL, prepareCodeView normalizes and enforces hard caps (bytes, lines, Prism chars) with explicit truncation messaging.
Bundled languages, themes, and the honesty corner
Syntax coverage isn't “all of Prism.” Out of the box, embedacode highlights six languages and falls back to JavaScript when an id isn't loaded. Toolbar copy is English-only in source today.
Build, CI, and how releases leave the repo
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.
Where I’m taking embedacode next
Everything above is what you can verify in the repo today. Planned direction (no ETAs): more highlighter languages (TSX/JSX/HTML), proper i18n, and better responsive layout ergonomics.
When to reach for it - and when to wrap something else
Good fit: tutorials, docs sites, public companion repos where read-only browsing beats execution. Skip or wrap: private GitHub without a proxy; localized UI requirements; heavy reliance on TSX/JSX/HTML coloring without overrides; anything that needs a sandboxed runtime.
Own the reading experience
Embedacode optimizes for legibility: tree on the left, code on the right, copy and ZIP on the toolbar, and shadow DOM to keep your blog CSS from fighting Prism.
Examples
The guides below are not strawmen. They’re some of the best teaching and reference material on the web. Embed blocks are my answer to the aggregation gap: same pedagogy, less context switching, clearer file boundaries.
Closing stance
Trusted tutorials fragment because they must. In-article embed blocks keep readers beside the narrative while still letting them see multi-file context without inheriting a full IDE.
References
- Henken M. Embedacode README and repository. github.com/mikehenken/embedacode
- Embedacode v2.1.0 release assets (standalone bundle). Releases · v2.1.0
- GitHub Docs - Working with the npm registry (GitHub Packages). docs.github.com
- PrismJS. prismjs.com
- Lit. lit.dev