Getting Started
This page walks you through using readrun with your own Markdown notes.
Install
bun install -g github:EdwardAstill/readrun
When working from a source checkout, link that checkout instead of installing a second global copy:
bun install
bun link
Start the dev server
Run rr with no arguments to serve the current folder:
cd your-notes-folder
rr
Or pass a path directly:
rr . # serve the current folder
rr ./my-notes # serve a specific folder
rr intro.md # open a single file
rr notes/lecture-1.md # open a single file by path
When given a folder, readrun serves the whole directory. When given a .md file, it serves that file’s folder and opens the browser directly on that page.
Open http://localhost:3001 and you’ll see your notes rendered as a navigable website with a sidebar built from your folder structure.
Try it without your own notes
If you just want to see what readrun does, serve the built-in docs project:
rr docs
To see the same source content with wiki navigation:
rr docs-wiki
Deploy
From a git repo, build the static site and write the deploy config in one command:
rr deploy github docs/ # builds docs/ → site/dist/, writes .github/workflows/deploy.yml
rr deploy vercel . # builds . → site/dist/, writes vercel.json
rr deploy netlify notes/ # builds notes/ → site/dist/, writes netlify.toml
Each deployment gets a site/ folder at the git repo root. rr deploy
generates its package.json, installs dependencies (creating bun.lock and
ignored node_modules/), and writes static output to ignored site/dist/.
Host configuration such as vercel.json stays at the repository root; hosts
install from site/ with the frozen lockfile. Everything runs in the browser
— no server needed.
For simple private sharing on Vercel, add a repository-local password file before deploying:
mkdir -p .readrun
printf 'shared-password\n' > .readrun/pw.txt
rr deploy vercel .
When .readrun/pw.txt exists, ReadRun also emits .vercel/output/ with login middleware protecting the site. Readers get a password-only login page, and pw.txt can contain multiple passwords (one per line).
Adding runnable code
There are two ways to add executable code blocks.
Inline code
Wrap code in bracket blocks and readers can run it inline:
print("Hello from readrun!")
for i in range(1, 6):
print("*"* i)File references
You can also keep code in separate files under .readrun/assets/scripts/ and reference them by path:
[python=scripts/variables.py]
The code is loaded from .readrun/assets/scripts/variables.py, displayed on the page, and made runnable — exactly like an inline block. This keeps your markdown clean when scripts get longer.
x = 42
name = "Alice"
pi = 3.14159
is_active = True
for var_name, var_val in [("x", x), ("name", name), ("pi", pi), ("is_active", is_active)]:
print(f"{var_name} = {var_val} ({type(var_val).__name__})")
JSX blocks
JSX blocks render directly in the page — no iframe, no run button. React and Tailwind are loaded automatically. Use the built-in render() helper to mount your component:
[jsx]
function App() {
return <h1 className="text-2xl font-bold">Hello!</h1>;
}
render(<App />);
[/jsx]
Reference a .jsx file from .readrun/assets/scripts/ the same way as any other file — it auto-renders on page load:
[jsx=scripts/counter.jsx]
function Counter() {
const [count, setCount] = React.useState(0);
const btn = "px-4 py-2 rounded-lg font-medium text-white transition-colors";
return (
<div className="p-6 flex flex-col items-center gap-4">
<span className="text-6xl font-light tabular-nums tracking-tight" style={{ fontFamily: "system-ui, sans-serif" }}>
{count}
</span>
<div className="flex gap-2">
<button className={`${btn} bg-gray-400 hover:bg-gray-500`} onClick={() => setCount(count - 1)}>−</button>
<button className={`${btn} bg-gray-200 hover:bg-gray-300 text-gray-700`} onClick={() => setCount(0)}>Reset</button>
<button className={`${btn} bg-blue-500 hover:bg-blue-600`} onClick={() => setCount(count + 1)}>+</button>
</div>
</div>
);
}
render(<Counter />);
Images
Place images in .readrun/assets/images/ and reference them the same way:
[image=images/diagram.svg]
Here’s the “how it works” diagram from the welcome page, embedded via [image=images/how-it-works.svg]:
Images are embedded directly in the page. Click any image to enlarge it.
Preloaded data assets
Files under .readrun/assets/data/ are copied into Pyodide’s filesystem when Python starts. This demo includes .readrun/assets/data/student.json:
import json
with open("data/student.json") as f:
student = json.load(f)
print(student["name"], "is taking", student["course"])
print("Average score:", round(sum(student["scores"]) / len(student["scores"]), 1))
print(student["notes"])Standard markdown code blocks (triple backticks) are displayed but not runnable — useful for showing bash commands, config snippets, or code you don’t want readers to execute.
Links between pages
Standard Markdown links work as navigation. For example, Code links to the authoring page for runnable Python. readrun rewrites .md links automatically so they work in the rendered site.
Next steps
Head to Code to learn how Python imports work, or go back to the welcome page.