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.
titlestringOptional 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
filestringRequired. 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/

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.

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.