Markdown & MDX
EmbedHub renders .md and .mdx files in-page so project documentation is
readable without downloading or switching tools.
Upload a .md or .mdx to a project's Documents or Releases section
(via the web, embedhub push, or the GitHub Action) and a small eye icon
appears next to the file. Clicking it opens an in-page reader.
.mdrenders as plain markdown..mdxrenders markdown plus a small set of interactive components.
Plain markdown
.md files render with GitHub-flavored markdown: tables, task lists,
strikethrough, autolinks, fenced code blocks.
# Release 1.4
## What's new
- New flash mode for ESP32-S3
- Faster boot — typical ~1.2 s
- ~~Old serial logger~~ removed
## Test status
| Board | Boot | Network | Sensors |
| ----------- | ---- | ------- | ------- |
| esp32-s3 | ✅ | ✅ | ✅ |
| esp32-c6 | ✅ | ✅ | ⚠️ |
## Checklist
- [x] sign release tag
- [x] upload firmware
- [ ] update changelog
Diagrams
A fenced code block tagged mermaid renders as a diagram. This works in both
.md and .mdx files — flowcharts, sequence diagrams, state diagrams, entity
relationships, Gantt charts and the rest of the
mermaid syntax.
```mermaid
graph TD
A[CLI push] --> B{Authorized?}
B -->|yes| C[Stored in project]
B -->|no| D[Rejected]
C --> E[Activity log]
```
Renders as:
Colours
A plain fence needs no styling — the reader keeps diagrams on a light surface that stays readable whether the person viewing them has a light or dark browser. Most diagrams want nothing more than this.
If you do want your own palette, set it with an %%{init}%% directive on the
first line, or switch to one of mermaid's built-in themes (default,
neutral, dark, forest, base):
```mermaid
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#eef2f7',
'primaryTextColor': '#1a2332',
'primaryBorderColor': '#4a6fa5',
'lineColor': '#4a6fa5'
}}}%%
graph LR
A[Sensor] --> B[Gateway] --> C[Cloud]
```
A palette you set is used exactly as written and replaces the defaults, so
it's worth picking colours with enough contrast to read on their own — a
themeVariables block that sets fills but leaves the text colour at its
default is the usual way a diagram ends up readable on the machine it was
written on and washed out elsewhere. Setting primaryTextColor alongside
primaryColor, as above, avoids that.
Diagrams wider than the page scroll sideways rather than shrinking to illegibility, so a wide flowchart stays readable — though a diagram that needs a lot of scrolling usually reads better split into two.
If the diagram syntax is malformed the reader shows the error inline, in place of the diagram, so you can fix the source and re-upload.
MDX adds components
.mdx is markdown plus a small library of components you can drop into
the page. Save the file with an .mdx extension to opt in.
<Note>
A coloured callout for highlighting context. Variants: info, warn,
error. The body accepts markdown.
<Note variant="info" title="Heads up">
Run `embedhub config set api-key …` once before pushing from CI.
</Note>
<Note variant="warn">
This board ships with the bootloader **disabled** by default.
</Note>
<Note variant="error" title="Don't do this">
Never commit `.env` to source control.
</Note>
<Chart>
Renders an interactive chart inline. Hover for tooltips; click items in the legend to toggle individual series.
| Prop | Type | Default | Description |
|---|---|---|---|
type | "line" | "bar" | "area" | line | Chart style. |
data | Array<object> | — | Required. Each object is one row. |
xKey | string | "x" | Field on each row to use as the x-axis label. |
yKey | string | string[] | "y" | Field(s) to plot. Pass an array for multi-series. |
title | string | — | Optional caption above the chart. |
height | number | 280 | Chart height in pixels. |
Single-series bar:
<Chart
type="bar"
title="Defects per build"
xKey="build"
yKey="defects"
data={[
{ build: '#101', defects: 3 },
{ build: '#102', defects: 1 },
{ build: '#103', defects: 5 },
{ build: '#104', defects: 0 },
{ build: '#105', defects: 2 },
]}
/>
Single-series line:
<Chart
type="line"
title="Boot time (ms) by build"
xKey="build"
yKey="bootMs"
data={[
{ build: '1.0', bootMs: 1820 },
{ build: '1.1', bootMs: 1640 },
{ build: '1.2', bootMs: 1510 },
{ build: '1.3', bootMs: 1290 },
{ build: '1.4', bootMs: 1180 },
]}
/>
Single-series area:
<Chart
type="area"
title="Free heap (KB) over runtime"
xKey="t"
yKey="freeKb"
data={[
{ t: '0s', freeKb: 142 },
{ t: '10s', freeKb: 138 },
{ t: '20s', freeKb: 130 },
{ t: '30s', freeKb: 124 },
{ t: '40s', freeKb: 121 },
{ t: '50s', freeKb: 118 },
]}
/>
Multi-series line:
<Chart
type="line"
title="Monthly sales vs refunds"
xKey="month"
yKey={['sales', 'refunds']}
data={[
{ month: 'Jan', sales: 100, refunds: 8 },
{ month: 'Feb', sales: 120, refunds: 15 },
{ month: 'Mar', sales: 95, refunds: 10 },
{ month: 'Apr', sales: 140, refunds: 12 },
]}
/>
Multi-series bar:
<Chart
type="bar"
title="Pass / fail counts per board"
xKey="board"
yKey={['pass', 'fail']}
data={[
{ board: 'esp32-s3', pass: 42, fail: 3 },
{ board: 'esp32-c6', pass: 38, fail: 7 },
{ board: 'rp2040', pass: 45, fail: 1 },
{ board: 'nrf52840', pass: 40, fail: 5 },
]}
/>
Multi-series area:
<Chart
type="area"
title="CPU vs memory usage (%)"
xKey="t"
yKey={['cpu', 'mem']}
data={[
{ t: '0s', cpu: 12, mem: 30 },
{ t: '5s', cpu: 28, mem: 34 },
{ t: '10s', cpu: 65, mem: 42 },
{ t: '15s', cpu: 71, mem: 51 },
{ t: '20s', cpu: 48, mem: 49 },
{ t: '25s', cpu: 22, mem: 45 },
]}
/>
Pass chart data inline via data={[…]}. Sourcing data from a separate
project file isn't supported yet.
<LogicCapture>
Embeds a logic-analyzer capture as an interactive waveform viewer. Point it
at a .sr capture file (the format saved by
sigrok/PulseView) stored in the same project, and the
reader renders the digital channels inline — pan, zoom, and read off timing
without leaving the page.
| Prop | Type | Default | Description |
|---|---|---|---|
file | string | — | Required. Project-relative path to the .sr capture file. |
name | string | file's name | Optional label shown in the viewer's header. |
<LogicCapture file="captures/spi-sensor-read.sr" name="SPI sensor read" />
Download this sample four-channel SPI transfer —
spi-sensor-read.sr — push it to a project,
and reference it from an .mdx doc to see the viewer above rendered live:
embedhub push spi-sensor-read.sr myorg/myproject --path documents/captures/
The file path is resolved relative to the document's own folder, exactly
like embedded images (see below). So a doc at documents/protocol.mdx
referencing captures/i2c-boot.sr loads
documents/captures/i2c-boot.sr, and ../photos/… steps up out of
documents/. A leading / is treated as bucket-root-absolute.
The capture is fetched through the project's permission-checked file API and
parsed in the browser, so it stays in sync with the file in the project —
re-upload the .sr and refresh to see the new capture.
Viewer controls:
- Scroll / pinch over the waveform to zoom in and out around the cursor.
- Drag to pan along the timeline.
- Zoom in / out / fit buttons in the header, or double-click to fit the whole capture.
- The header shows the sample rate, channel count, total samples, and capture duration; hovering the waveform reads out the time at the cursor.
The header shows the channel names recorded in the capture. Both sigrok
session versions (v1 and v2) are supported, up to 32 channels. Very long
captures are truncated (the header marks them (truncated)) so a large file
can't lock up the tab.
In PulseView, use File → Save As and keep the default sigrok session
(.sr) format. Upload it to the project like any other file:
embedhub push i2c-boot.sr myorg/myproject --path documents/captures/
Embedding images from the project
An .mdx file can reference any image stored in the same project — useful
for screenshots, diagrams, or rendered state machines that should always
reflect the latest version in the project.
Reference images with a relative path, just like in a normal markdown file:


The viewer resolves the relative path against the document's location in the project bucket, then loads the image through the project's permission-checked file API. Update the source file and refresh the doc — the embedded image updates with it.
How the path is resolved
The path is resolved relative to the document's own folder, exactly like links in any markdown file — so where you put the image matters:
documents/connectors.mdx ← the doc
documents/connectors-assets/panel.png ← an asset next to it
photos/top.jpg ← a root-level photo
From documents/connectors.mdx | Resolves to |
|---|---|
 | documents/connectors-assets/panel.png |
 | photos/top.jpg |
A common pattern is to keep a doc's images in a sibling subfolder and push them
with --path:
embedhub push assets/ myorg/myproject --path documents/connectors-assets/
embedhub push connectors.mdx myorg/myproject --path documents/
<img>The viewer renders markdown but strips raw HTML, so an <img src="…"> tag
silently disappears — no image, no error. Always use the markdown form
. The same applies to <img> pointing at a data: URI: the tag
is dropped before the URL is ever read.
 <!-- ✅ renders -->
<img src="connectors-assets/panel.png" /> <!-- ❌ stripped, shows nothing -->
/A leading slash (/photos/top.jpg) is treated as a site-root URL, not a
project path — it bypasses the file API and 404s. Use a relative path
(connectors-assets/… or ../photos/…) instead.
Project-relative image paths only resolve in .mdx files. Inside a plain
.md file they're handed straight to the browser, which interprets them
relative to the page URL and fails to load. Use .mdx whenever you embed
images from the project.
Because raw HTML is stripped, there's no width/height attribute to size an
image — markdown images render at their natural pixel size. Resize the source
file before uploading (e.g. convert in.png -resize 150x out.png) to control
how large it appears, especially for thumbnails inside table cells.
Absolute URLs (https://…) work in both .md and .mdx.
Tips
- Links in MDX use
[text](url). MDX treats<as the start of a component tag, so<https://example.com>autolinks don't work in.mdxfiles. They still work in plain.md. - Component names are case-sensitive.
<Note>works;<note>is treated as an unknown HTML element. - Compile errors surface in the viewer. If the MDX is malformed, the modal shows the parser message with a line and column so you can fix the source and re-upload.
Uploading
.md and .mdx files are uploaded the same way as any other file:
embedhub push README.mdx myorg/myproject --path documents/
See the CLI reference for the full set of flags.