Skip to main content

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.

  • .md renders as plain markdown.
  • .mdx renders 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.

PropTypeDefaultDescription
type"line" | "bar" | "area"lineChart style.
dataArray<object>—Required. Each object is one row.
xKeystring"x"Field on each row to use as the x-axis label.
yKeystring | string[]"y"Field(s) to plot. Pass an array for multi-series.
titlestring—Optional caption above the chart.
heightnumber280Chart 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.

PropTypeDefaultDescription
filestring—Required. Project-relative path to the .sr capture file.
namestringfile's nameOptional label shown in the viewer's header.
<LogicCapture file="captures/spi-sensor-read.sr" name="SPI sensor read" />

Logic capture viewer showing a four-channel SPI transfer

Try it with a real capture

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.

Save from PulseView

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/

<Views>​

Shows several views of one thing in a single panel: an assembly, a cutaway, a top-down view, a photo-real render. Readers switch between them with tabs above the panel, so the variants sit in one place on the page instead of stacked one under another.

Each <View> inside it is one tab. A view is a 3D model, which readers can rotate, zoom and measure; a KiCad board or schematic, which readers can pan, zoom and inspect; or an image.

<Views title="Enclosure rev B">
<View label="Assembly" file="../releases/enclosure.glb" />
<View label="Cutaway" file="../releases/enclosure-section.glb" />
<View label="Top" file="../releases/enclosure.glb" angle="top" />
<View label="Render" file="../photos/enclosure-front.png" />
</Views>

A views panel showing the cutaway of an enclosure, with the circuit board inside, and tabs for Assembly, Cutaway, Top and Render

Try it with the sample enclosure

Download the three files the example uses — enclosure.glb, enclosure-section.glb and enclosure-front.png — push them to a project, then put the example above in a document under documents/:

embedhub push enclosure.glb myorg/myproject --path releases/
embedhub push enclosure-section.glb myorg/myproject --path releases/
embedhub push enclosure-front.png myorg/myproject --path photos/

<Views> props:

PropTypeDefaultDescription
titlestring—Optional caption above the tabs.
heightnumber500Panel height in pixels.

<View> props:

PropTypeDefaultDescription
filestring—Required. Path to a .glb, .gltf, .stl or .obj model, a .kicad_pcb, .kicad_sch or .kicad_pro KiCad file, or a .png, .jpg, .gif, .webp, .svg or .avif image.
labelstringfile's nameThe tab's text.
anglestringangleWhere the camera looks from on a model: top, bottom, front, back, left, right or angle (three-quarter).

Paths resolve relative to the document's own folder, like <LogicCapture> and embedded images.

Two views can point at the same model with different angles. The model loads once, and choosing the other tab moves the camera. A cutaway is a model of its own: export the sectioned assembly from your CAD tool and give it a tab.

A lone <View> outside <Views> is a panel with one view and no tabs.

KiCad files​

Push your KiCad files to the project as they are and point a view at them. They open in the same viewer as the PCB Viewer, with everything it offers. The file you name decides what readers see:

FileShows
.kicad_pcbThe board, with its layers.
.kicad_schThe schematic, with every other sheet in the same folder, so hierarchical designs open.
.kicad_proThe whole project: schematic, board and bill of materials.
<Views title="Programmer rev 2">
<View label="Board" file="../releases/programmer/programmer.kicad_pcb" />
<View label="Schematic" file="../releases/programmer/programmer.kicad_sch" />
<View label="3D" file="../releases/programmer/programmer.glb" />
<View label="Assembled" file="../photos/programmer-assembled.jpg" />
</Views>

The board viewer has a 3D tab of its own. It shows the model exported beside the board with the same name: export it with kicad-cli pcb export glb programmer.kicad_pcb and push programmer.glb to the same folder. The same .glb can also have a tab of its own, as above.

embedhub push programmer.kicad_pcb myorg/myproject --path releases/programmer/
embedhub push programmer.kicad_sch myorg/myproject --path releases/programmer/
embedhub push programmer.glb myorg/myproject --path releases/programmer/
STEP files are coming

STEP models will open in <Views> and the 3D viewer directly. Until then, export a .glb (or .stl) from your CAD tool alongside the .step.

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:

![Boot flow](../state-machines/diagrams/boot-and-idle.svg)
![Board top](../photos/top.jpg)

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.mdxResolves to
![](connectors-assets/panel.png)documents/connectors-assets/panel.png
![](../photos/top.jpg)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/
Use markdown image syntax, not raw <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 ![alt](path). The same applies to <img> pointing at a data: URI: the tag is dropped before the URL is ever read.

![Back panel](connectors-assets/panel.png) <!-- ✅ renders -->
<img src="connectors-assets/panel.png" /> <!-- ❌ stripped, shows nothing -->
Don't start the path with /

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.

MDX only

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.

Sizing

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.

Linking to other documents​

A document can link to any other file in the same project with a relative path, resolved against the document's own folder — the same rule as images:

See the [state machine notes](subjects/fsm.md) and the [BOM](../releases/bom.csv).
Jump to [the inbox](inbox.md#unsorted).
From documents/README.mdOpens
[…](subjects/fsm.md)documents/subjects/fsm.md
[…](../releases/bom.csv)releases/bom.csv
[…](inbox.md#unsorted)documents/inbox.md, scrolled to the unsorted heading

The link opens the file in the viewer without a page reload, and Ctrl/Cmd-click opens it in a new tab. This works in both .md and .mdx files. Links that start with /, https:// or # are left exactly as written.

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 .mdx files. 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.