Write in Jupyter Notebooks. Publish anywhere.
Project description
nb2wb
Write in notebooks. Publish anywhere.
nb2wb converts:
- Jupyter notebooks (
.ipynb) - Quarto documents (
.qmd) - Markdown files (
.md)
into platform-ready HTML for:
- Substack
- Medium
- X Articles
The output is designed for copy/paste workflows where platforms often break MathJax and code formatting.
Why nb2wb
Most publishing editors strip or mangle:
- LaTeX
- code blocks
- notebook outputs
nb2wb preserves fidelity by rendering complex parts as images and converting inline math to Unicode.
| Content type | Converted as |
|---|---|
Inline math $...$ |
Unicode + light HTML formatting |
Display math ($$...$$, \[...\], \begin{...}) |
PNG |
| Code input | Syntax-highlighted PNG (or copyable text snippet) |
| Text outputs / errors | PNG |
image/png outputs |
Embedded as image data URI |
image/svg+xml outputs |
Sanitized SVG as image data URI |
text/html outputs |
Sanitized HTML fragment |
Feature Overview
- Converts
.ipynb,.qmd, and.mdfrom one CLI. - Platform-specific page wrappers for Substack, Medium, and X.
- One-click copy toolbar in generated HTML.
- Medium/X per-image copy buttons for reliable image transfer.
- Optional
--servemode (local server + ngrok URL). - Equation labels and cross-references:
\label{...}in display math\eqref{...}replaced with(N)
- LaTeX rendering strategy:
- tries full
latex + dvipng - falls back to matplotlib mathtext
- tries full
- Inline LaTeX conversion pipeline:
- unicode command replacement
- superscript/subscript expansion
- variable italicization
- Code rendering controls:
- Pygments theme
- line numbers
- font size
- padding / border radius
- Cell-level visibility and behavior tags:
hide-cell,hide-input,hide-output,latex-preamble,text-snippet
- Markdown directives in
.mdvia<!-- nb2wb: ... -->. - Quarto
#|options mapped to notebook-style tags. - Security hardening for image fetching and HTML/SVG embedding.
Installation
pip install nb2wb
Development install:
git clone https://github.com/the-palindrome/nb2wb.git
cd nb2wb
pip install -e ".[dev]"
Quick Start
nb2wb notebook.ipynb
nb2wb notebook.ipynb -t medium
nb2wb notebook.ipynb -t x
nb2wb notebook.ipynb -o article.html
nb2wb notebook.ipynb --open
nb2wb notebook.ipynb --serve
nb2wb article.md
nb2wb article.md --execute
nb2wb report.qmd
Default output path is <input_basename>.html.
CLI Reference
nb2wb <input.{ipynb|qmd|md}> [options]
| Option | Meaning |
|---|---|
-t, --target {substack,medium,x} |
Target platform (substack default) |
-c, --config PATH |
YAML config file |
-o, --output PATH |
Output HTML path |
--open |
Open generated HTML in browser |
--serve |
Extract images, start local server, expose via ngrok |
--execute |
Execute code cells for .md files (ignored for .qmd, which are always executed) |
Platform Behavior
| Platform | Paste workflow | Image behavior |
|---|---|---|
| Substack | One-click copy/paste | Embedded images transfer directly |
| Medium | Copy/paste + optional per-image copy | Base64 images may be stripped by editor |
| X Articles | Copy/paste + optional per-image copy | Base64 images may be stripped by editor |
--serve mode
--serve helps Medium/X workflows by replacing embedded data URIs with public HTTP URLs.
What it does:
- Extracts supported image MIME types from the generated HTML into
images/ - Rewrites
<img src="...">to those files - Starts local HTTP server
- Starts ngrok tunnel and opens the public URL
Requirements:
ngrokinstalledngrokauthenticated (ngrok config add-authtoken <TOKEN>)
Input Format Support
.ipynb
- Uses notebook cells and outputs directly.
- Supports notebook tags in
cell.metadata.tags. - Uses notebook/kernel metadata to infer language for syntax highlighting.
.md
Supported features:
- Optional YAML front matter
- Fenced code blocks with backticks or tildes
- Per-fence tags (
```python hide-inputstyle) - Directive comments:
<!-- nb2wb: hide-input --><!-- nb2wb: hide-output --><!-- nb2wb: hide-cell --><!-- nb2wb: text-snippet -->- comma-separated combinations are supported
- Special fence language:
latex-preamble
Execution:
- Not executed by default
- Executed when
--executeis provided
.qmd
Supported features:
- Optional YAML front matter
- Quarto fenced chunks (
```{python}etc.) - Quarto options mapped to tags:
#| echo: false->hide-input#| output: false->hide-output#| include: falseor#| eval: false->hide-cell#| tags: [tag1, tag2]-> tags
- Special chunk languages:
latex-preambleoutput(attaches stdout to the immediately preceding code cell)
Execution:
.qmdcode execution is always attempted by design
Cell Tags
| Tag | Effect |
|---|---|
hide-cell |
Hide entire cell (input + output) |
hide-input |
Hide code input |
hide-output |
Hide outputs |
latex-preamble |
Use cell/chunk content as LaTeX preamble and hide it |
text-snippet |
Render code as <pre><code> instead of PNG |
hide-cell applies to markdown cells too.
LaTeX Features
Display math
Detected forms include:
$$...$$\[...\]\begin{equation}...\end{equation}\begin{align}...\end{align}\begin{gather}...\end{gather}\begin{multline}...\end{multline}\begin{eqnarray}...\end{eqnarray}- starred variants where applicable
Equation numbering and references
\label{eq:name}assigns equation number\eqref{eq:name}is replaced with(N)across the document
Inline math
Inline $...$ expressions are converted to Unicode-oriented text with script handling.
LaTeX Preamble Sources
All of these are combined:
- Config (
latex.preamble) - Notebook markdown cells tagged
latex-preamble .mdfenced blocks labeledlatex-preamble.qmdchunks{latex-preamble}
Note:
- preamble only affects full LaTeX (
try_usetex: truepath) - matplotlib mathtext fallback ignores custom preamble
Output Rendering Details
For code cells:
- Source code -> syntax-highlighted PNG
- Footer includes execution count and language label
- Text output / tracebacks -> muted output PNG block
Rich outputs:
image/png-> embedded directlyimage/svg+xml-> sanitized and embedded asdata:image/svg+xml;base64,...text/html-> sanitized HTML fragment
Raw notebook cells are skipped.
Configuration
Pass with:
nb2wb notebook.ipynb -c config.yaml
Complete config schema (with defaults)
# Global defaults
image_width: 1920
border_radius: 14
code:
font_size: 48
theme: "monokai"
line_numbers: true
font: "DejaVu Sans Mono"
image_width: 1920
padding_x: 100
padding_y: 100
separator: 0
background: "" # empty = use theme background
border_radius: 14
latex:
font_size: 48
dpi: 150
color: "black"
background: "white"
padding: 68 # pixels
image_width: 1920
try_usetex: true
preamble: ""
border_radius: 0
Inheritance behavior:
code.image_widthandlatex.image_widthinherit top-levelimage_widthunless overriddencode.border_radiusandlatex.border_radiusinherit top-levelborder_radiusunless overridden
Platform defaults applied automatically
When target is medium or x, defaults are adjusted for narrower layouts:
- top-level
image_width:680 code.font_size:42code.image_width:1200code.padding_x:30code.padding_y:30code.separator:0latex.font_size:35latex.padding:50latex.image_width:1200
Substack keeps base defaults unless your config overrides them.
Code themes
Any Pygments style works. Example:
python -c "from pygments.styles import get_all_styles; print(sorted(get_all_styles()))"
Security Model
nb2wb includes guardrails for image ingestion and HTML embedding:
Image URL/file safety
- Only
http/httpsremote image URLs are allowed - Requests to private/loopback hosts are blocked (SSRF protection)
- Redirect targets are re-validated (no public-to-private redirect bypass)
- Download timeout and max size checks are enforced
- MIME type allowlist is enforced for fetched/read images
- Local image paths:
- absolute paths rejected
..traversal rejected- symlink escape outside current working directory rejected
Embedded content sanitization
- Markdown-generated HTML is sanitized before embedding
text/htmloutputs are sanitized- SVG outputs are sanitized, then embedded via image data URI
- Dangerous tags/attributes/URI schemes are stripped or neutralized
Important:
- Notebook execution (
.qmdor--execute) runs code. Treat untrusted notebooks as untrusted code. - Sanitization is best-effort, not a browser sandbox.
Requirements
Core dependencies:
- Python
>=3.9 nbformatnbconvertipykernelmatplotlibPillowPygmentsPyYAMLmarkdownunicodeit
Optional system tools:
- LaTeX +
dvipng(for highest-fidelity display math rendering) ngrok(for--serve)
Development
Run tests:
pytest
pytest tests/unit/
pytest tests/integration/
pytest tests/workflow/
Format:
black nb2wb tests
isort nb2wb tests
Limitations
- Platforms can change paste behavior without notice.
- Medium/X may still require per-image copy depending editor behavior.
- Extremely complex custom HTML can be altered by sanitization.
.qmdexecution requires a working Jupyter kernel setup.
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.2.tar.gz.
File metadata
- Download URL: nb2wb-0.1.2.tar.gz
- Upload date:
- Size: 40.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c21572a2e7e5317c02bdc62453e08cfda9feb6adac5292e0509daaa5400e0c3
|
|
| MD5 |
d2eb07aa06227cb333ee67a1c65aa596
|
|
| BLAKE2b-256 |
953e2ed1fffbdf606ebc48097c104b5708ddfd910cc37961e7c24719b9f8c313
|
Provenance
The following attestation bundles were made for nb2wb-0.1.2.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.2.tar.gz -
Subject digest:
6c21572a2e7e5317c02bdc62453e08cfda9feb6adac5292e0509daaa5400e0c3 - Sigstore transparency entry: 973390151
- Sigstore integration time:
-
Permalink:
the-palindrome/nb2wb@ff7cfc4ce97932ffe3c4e162456dcc06211c00d4 -
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@ff7cfc4ce97932ffe3c4e162456dcc06211c00d4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file nb2wb-0.1.2-py3-none-any.whl.
File metadata
- Download URL: nb2wb-0.1.2-py3-none-any.whl
- Upload date:
- Size: 45.9 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 |
1f1d6be72d5690700f78a6e152cc76658ad558fa13694e706f637614099fcebd
|
|
| MD5 |
d5ca4910f1d5552e05b315e30060a6d7
|
|
| BLAKE2b-256 |
e8e8efafc91e3cdf285508133f9b71e86fc5783bb5069c26a2cae0a3b8d3dcb9
|
Provenance
The following attestation bundles were made for nb2wb-0.1.2-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.2-py3-none-any.whl -
Subject digest:
1f1d6be72d5690700f78a6e152cc76658ad558fa13694e706f637614099fcebd - Sigstore transparency entry: 973390153
- Sigstore integration time:
-
Permalink:
the-palindrome/nb2wb@ff7cfc4ce97932ffe3c4e162456dcc06211c00d4 -
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@ff7cfc4ce97932ffe3c4e162456dcc06211c00d4 -
Trigger Event:
push
-
Statement type: