Write in Jupyter Notebooks. Publish anywhere.
Project description
nb2wb
Write in Jupyter Notebooks. Publish anywhere.
nb2wb (short for notebook to web) converts Jupyter Notebooks (.ipynb), Quarto documents (.qmd),
and plain Markdown files (.md) into self-contained HTML files you can paste directly into publishing
platforms like Substack, Medium, and X Articles — with LaTeX, code, and outputs all rendered faithfully.
Why
Most web publishing platforms strip MathJax and code-block formatting. nb2wb sidesteps
this by turning complex content into images, and converting simple inline math into
Unicode text that pastes cleanly as prose.
| Notebook element | nb2wb output |
|---|---|
Inline LaTeX $...$ |
Unicode text (α, β, ∇, ℝ, …) |
Display math $$...$$, \[...\], \begin{equation} |
PNG image |
| Code cells | Syntax-highlighted PNG image |
| Cell outputs (text, repr, …) | PNG image |
| Cell outputs (matplotlib figure, …) | Embedded PNG / SVG |
Installation
pip install nb2wb
For development:
git clone https://github.com/the-palindrome/nb2wb.git
cd nb2wb
pip install -e ".[dev]"
Usage
nb2wb notebook.ipynb # → notebook.html (Substack by default)
nb2wb notebook.ipynb -t medium # → Medium format
nb2wb notebook.ipynb -t x # → X Articles format
nb2wb notebook.ipynb -c config.yaml # with custom config
nb2wb notebook.ipynb -o out.html # explicit output path
nb2wb notebook.ipynb --open # open in browser when done
nb2wb notebook.ipynb --serve # serve with ngrok tunnel (see below)
nb2wb article.md # → article.html from Markdown
nb2wb article.md --execute # execute code blocks via Jupyter kernel
nb2wb document.qmd # → document.html from Quarto
Then open the HTML file in your browser, click Copy to clipboard, and paste into your platform's draft editor.
Supported platforms
| Platform | Flag | How it works |
|---|---|---|
| Substack | -t substack (default) |
Copy and paste — images transfer automatically |
| Medium | -t medium |
Copy and paste text, then hover each image to copy it individually (or use --serve for one-click) |
| X Articles | -t x |
Copy and paste text, then hover each image to copy it individually (or use --serve for one-click) |
Platform tips
Substack works out of the box — click Copy to clipboard, paste into your draft, and all text and images transfer in one step.
Medium strips embedded images from pasted HTML, so the default output
includes per-image Copy image buttons that appear on hover. For a
smoother workflow, use --serve mode (see below) which gives images public
URLs that Medium can fetch automatically — one click, everything pastes.
X Articles also strips embedded images. The same two workflows apply:
hover-to-copy each image individually, or use --serve for one-click pasting.
Serve mode (--serve)
Some platforms (Medium, X Articles) strip base64-embedded images but will
fetch images from real HTTP URLs. The --serve flag starts a local HTTP
server and exposes it via an ngrok tunnel, giving every
image a public URL.
nb2wb notebook.ipynb -t medium --serve
This will:
- Extract embedded images to an
images/directory - Rewrite the HTML to reference those files
- Start a local HTTP server and an ngrok tunnel
- Open the public URL in your browser
Copy from the opened page and paste into your editor — images transfer automatically because the platform can fetch them from the public URL. Press Ctrl-C to stop the server when done.
Requirements: ngrok must be installed and
authenticated (ngrok config add-authtoken <TOKEN>). Serve mode works with
any target platform (-t substack, -t medium, -t x).
Configuration
All options are optional. Copy example_config.yaml and edit as needed:
code:
font_size: 14 # font size for code images
theme: "monokai" # any Pygments style: monokai, dracula, vs, solarized-dark, …
line_numbers: true # show line numbers
font: "DejaVu Sans Mono"
latex:
font_size: 16 # font size for LaTeX images
dpi: 150 # image resolution
color: "black"
background: "white"
padding: 0.15 # whitespace around each expression (inches)
try_usetex: true # use a full LaTeX install when available; falls back to mathtext
Pass it with -c:
nb2wb notebook.ipynb -c config.yaml
LaTeX rendering modes
| Mode | Requirement | Quality |
|---|---|---|
usetex: true (default) |
LaTeX + dvipng installed | Full LaTeX, best quality |
| mathtext fallback | None (matplotlib built-in) | Most standard expressions |
If try_usetex: true and a LaTeX installation is found, full LaTeX is used
automatically. Otherwise nb2wb falls back to matplotlib's mathtext, which
handles most common expressions without any extra dependencies.
Cell tags
Attach tags to cells to control what appears in the output.
| Tag | Effect |
|---|---|
hide-cell |
Entire cell omitted (input + output) |
hide-input |
Source code hidden; output shown |
hide-output |
Output hidden; source code shown |
latex-preamble |
Cell source used as LaTeX preamble; cell itself is hidden |
text-snippet |
Code rendered as copyable HTML text instead of a PNG image |
hide-cell also works on Markdown cells.
Notebook cell tags
In JupyterLab open View → Cell Toolbar → Tags; in Jupyter Notebook use View → Cell Toolbar → Tags or edit the cell metadata directly.
Markdown cell tags
In .md files there are two ways to attach tags to code blocks.
Fence-line tags — add tags after the language on the opening fence:
```python hide-input
secret = "not shown"
```
```python text-snippet
x = 1 + 1
print(x)
```
HTML comment directives — place a <!-- nb2wb: ... --> comment on its
own line immediately before the code block:
<!-- nb2wb: hide-input -->
```python
secret = "not shown"
Multiple tags can be combined in a single comment:
```markdown
<!-- nb2wb: hide-input, hide-output -->
```python
x = 1
Both methods can be used together — the tags are merged.
#### Quarto cell tags
In `.qmd` files, use the standard `#|` options inside code chunks:
````markdown
```{python}
#| echo: false
secret = "not shown"
See the [Quarto documentation](https://quarto.org/) for all available options.
### LaTeX preamble
Custom LaTeX packages and macros can be supplied in several ways (all are
combined when rendering):
**1. Notebook cell (`.ipynb`)**
Tag any Markdown cell with `latex-preamble`. Its raw source is injected into
every formula's LaTeX document. The cell is invisible in the output.
```
\usepackage{xcolor}
\definecolor{accent}{HTML}{E8C547}
```
**2. Markdown file (`.md`)**
Use a fenced code block with the special language `latex-preamble`:
````markdown
```latex-preamble
\usepackage{xcolor}
\definecolor{accent}{HTML}{E8C547}
```
The block is consumed by the converter and does not appear in the output.
3. Config file (for project-wide defaults)
latex:
preamble: |
\usepackage{xcolor}
\definecolor{accent}{HTML}{E8C547}
The preamble is only used when
try_usetex: trueand a LaTeX installation is found. The mathtext fallback ignores it.
Code themes
Any Pygments style works:
python -c "from pygments.styles import get_all_styles; print(list(get_all_styles()))"
Popular choices: monokai, dracula, nord, solarized-dark, vs,
github-dark, one-dark.
How it works
notebook.ipynb / document.qmd / article.md
│
▼
[nbformat] parse cells
│
├─ Markdown cell
│ ├─ display math → [matplotlib] → PNG image
│ ├─ inline math → [unicodeit] → Unicode text
│ └─ prose → [markdown] → HTML
│
└─ Code cell
├─ source → [PIL + Pygments] → PNG image
└─ outputs
├─ stream / text/plain → [PIL] → PNG image
├─ image/png → embedded as-is
└─ image/svg+xml → embedded as-is
│
▼
page.html (self-contained)
All images are base64-encoded and embedded in the HTML file — no external assets, no server required.
Requirements
Core (installed automatically)
- Python ≥ 3.9
- nbformat — notebook parsing
- nbconvert + ipykernel —
.qmdcell execution - matplotlib — LaTeX / mathtext rendering
- Pillow — image compositing
- Pygments — syntax highlighting
- PyYAML — config and
.qmdfront matter parsing - Markdown — prose conversion
- unicodeit — inline LaTeX → Unicode
Optional
| Extra | Install | What it adds |
|---|---|---|
examples |
pip install -e ".[examples]" |
NumPy — required by the bundled example notebooks |
dev |
pip install -e ".[dev]" |
pytest, black, isort |
For the best LaTeX rendering, also install a LaTeX distribution
(TeX Live or MiKTeX) plus
dvipng.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nb2wb-0.1.1.tar.gz.
File metadata
- Download URL: nb2wb-0.1.1.tar.gz
- Upload date:
- Size: 39.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36d35ef9314ef821a4f2832ce0f844d005b90a2288378e8fbd8f6115dcda39a8
|
|
| MD5 |
5e4a3096a573cb8d7a06a01154beaad7
|
|
| BLAKE2b-256 |
d1b5a6df3de8f7301f929b36ce54143c5a3603e7a5b4bf17a4b628689bd3ae24
|
Provenance
The following attestation bundles were made for nb2wb-0.1.1.tar.gz:
Publisher:
publish.yml on the-palindrome/nb2wb
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nb2wb-0.1.1.tar.gz -
Subject digest:
36d35ef9314ef821a4f2832ce0f844d005b90a2288378e8fbd8f6115dcda39a8 - Sigstore transparency entry: 973186865
- Sigstore integration time:
-
Permalink:
the-palindrome/nb2wb@147e106151d7f894f6eb508d076f0c855d3468fe -
Branch / Tag:
refs/heads/main - Owner: https://github.com/the-palindrome
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@147e106151d7f894f6eb508d076f0c855d3468fe -
Trigger Event:
push
-
Statement type:
File details
Details for the file nb2wb-0.1.1-py3-none-any.whl.
File metadata
- Download URL: nb2wb-0.1.1-py3-none-any.whl
- Upload date:
- Size: 44.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c037888a9ae027e464c494803e47371897ed2d28532a9f231c58a5d66164cec0
|
|
| MD5 |
0c7a876e45ae66bee4af3dcc69394596
|
|
| BLAKE2b-256 |
103195dab036408753888bf493bee3caf8add14542cc602100aeb957458a1f3c
|
Provenance
The following attestation bundles were made for nb2wb-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on the-palindrome/nb2wb
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nb2wb-0.1.1-py3-none-any.whl -
Subject digest:
c037888a9ae027e464c494803e47371897ed2d28532a9f231c58a5d66164cec0 - Sigstore transparency entry: 973186867
- Sigstore integration time:
-
Permalink:
the-palindrome/nb2wb@147e106151d7f894f6eb508d076f0c855d3468fe -
Branch / Tag:
refs/heads/main - Owner: https://github.com/the-palindrome
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@147e106151d7f894f6eb508d076f0c855d3468fe -
Trigger Event:
push
-
Statement type: