Getting Started

This page walks you through using readrun with your own Markdown notes.

Install

bash
bun install -g github:EdwardAstill/readrun

When working from a source checkout, link that checkout instead of installing a second global copy:

bash
bun install
bun link

Start the dev server

Run rr with no arguments to serve the current folder:

bash
cd your-notes-folder
rr

Or pass a path directly:

bash
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:

bash
rr docs

To see the same source content with wiki navigation:

bash
rr docs-wiki

Deploy

From a git repo, build the static site and write the deploy config in one command:

bash
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:

bash
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:

python
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:

code
[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.

python
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:

code
[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:

code
[jsx=scripts/counter.jsx]
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:

code
[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:

python
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.

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.