Skip to content

feat: support serving a gallery of notebooks#8056

Merged
mscolnick merged 12 commits intomarimo-team:mainfrom
peter-gy:ptr/notebook-gallery
Jan 30, 2026
Merged

feat: support serving a gallery of notebooks#8056
mscolnick merged 12 commits intomarimo-team:mainfrom
peter-gy:ptr/notebook-gallery

Conversation

@peter-gy
Copy link
Contributor

📝 Summary

Enables running multiple marimo notebook files as a gallery of apps in the below style:

marimo run folder
marimo run folder another_folder
marimo run file_a.py file_b.py folder

When pointed to a local clone of https://github.com/marimo-team/learn this gives us:

Screenshot 2026-01-29 at 19 24 02

Closes #3257

🔍 Description of Changes

I based this work on #4961, borrowing the idea of reusing the existing home page infrastructure used in marimo edit mode and adding gallery as a new frontend view in marimo run mode.

  • Gallery item links are encoded through /?file=<encoded> to avoid introducing new routing / mounting logic
  • Navigation is restricted so that we don't accept arbitrary /?file=<encoded> values
    • extended marimo._server.file_router.ListOfFilesAppFileRouter to behave like an allowlist router
  • Preserved existing marimo run app.py -- --arg value behavior working while enabling marimo run file_a.py file_b.py folder and still allowing notebook args to be explicitly separated
  • Added a root directory heuristic for nicer URLs and less path leakage (via marimo._cli.cli.py._resolve_root_dir)
    • e.g. if I run uv run marimo run /Users/petergy/Projects/opensource/marimo-team/learn. the learn repo is recognized as gallery root, so opening /Users/petergy/Projects/opensource/marimo-team/learn/functional_programming/05_functors.py will be available under http://localhost:2720/?file=functional_programming%2F05_functors.py
  • Gallery entries have auto-generated title and subtitle for now (notebook dir and file name piped through titleCase)

TODO before undrafting this

  • define --sandbox semantics
    • per-notebook or per-gallery venv?
    • should we create sandbox eagerly (we create all the venvs at cmd execution time) or lazily (create venv only on notebook visit)?

📋 Checklist

  • I have read the contributor guidelines.
  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • Tests have been added for the changes made.
  • Documentation has been updated where applicable, including docstrings for API changes.
  • Pull request title is a good summary of the changes - it will be used in the release notes.

@vercel
Copy link

vercel bot commented Jan 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Jan 30, 2026 11:41am

Request Review

@mscolnick
Copy link
Contributor

cc @dmadisetti, its still in draft but can you check out the Sandbox semantics here

@mscolnick mscolnick requested a review from dmadisetti January 29, 2026 21:36
Copy link
Collaborator

@dmadisetti dmadisetti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a great start. Followups that might be useful:

  • return to home from an app page
  • thumbnails

As is, gallery sandbox falls back to ipc sandbox, which I think is fine. It provides consistency in experience.

We do need to fail fast if pyzmq is not installed like in sandboxed home.

But as is, even with pyzmq- this doesn't seem to work for --sandbox (at least locally, I'm getting blank screens for things that should otherwise work) and also --check.

I'd be OK raising an error for these flags, and following up in another PR. I think marimo run --sandbox falls under some of my wheelhouse so I can grab it if you'd like

# correctness check - don't start the server if we can't import the module
for path in validated_paths:
if Path(path).is_file():
check_app_correctness(path)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We maybe want to do this on launch and not stop the server itself from spawning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think about this is more of a UX tradeoff than a purely technical one. Based on #3257 my understanding is that in gallery mode we're aiming at potentially non‑technical end users, so I lean toward fail‑fast at launch: if an app check fails, I'd rather surface that to the developer than risk an end user launching a broken app. Happy to discuss alternatives ofc. Maybe we add a CLI flag to toggle between lazy and eager checks?

]
file_count = len(marimo_files)
has_more = file_count >= MAX_FILES
return WorkspaceFilesResponse(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should dynamically load more apps? Torn on this (see comment above)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see value in loading more apps dynamically but also see the associated concerns. Maybe let's consider adding a CLI flag to control this?

@dmadisetti dmadisetti added enhancement New feature or request release-highlight A feature or change to call out in upcoming release notes labels Jan 29, 2026
@peter-gy
Copy link
Contributor Author

peter-gy commented Jan 30, 2026

Thanks for the feedback @dmadisetti

I think this is a great start. Followups that might be useful:

  • return to home from an app page

For now I added a clickable marimo logo to go back.

Screen Recording 2026-01-30 at 11 03 32

Alternatively, we could go with a back arrow + "Gallery" text maybe or "Gallery / " breadcrumbs with Gallery clickable. I am not convinced though that everyone who deploys a multi-app env will want to call it "Gallery" (hence I just went with the logo for now)

  • thumbnails

That's exactly what we started to discuss with Myles. We'd want to generate OpenGraph content on the fly, (allowing users to overwrite it e.g. as PEP 723 metadata and add marimo tools thumbnails generate using playwright to snapshot the HTML output of a notebook and place it under __marimo__ folder. The developer experience around the metadata generation could be inspired by Next.js' generateMetadata: https://nextjs.org/docs/app/api-reference/functions/generate-metadata

As is, gallery sandbox falls back to ipc sandbox, which I think is fine. It provides consistency in experience.

We do need to fail fast if pyzmq is not installed like in sandboxed home.

But as is, even with pyzmq- this doesn't seem to work for --sandbox (at least locally, I'm getting blank screens for things that should otherwise work) and also --check.

I'd be OK raising an error for these flags, and following up in another PR. I think marimo run --sandbox falls under some of my wheelhouse so I can grab it if you'd like

That would be great. This way I can focus in this PR on better aesthetics through thumbnail (+ Partial<OpenGraph>) metadata generation. I modified the CLI to raise error on --sandbox and --check for now when running multiple apps.

@peter-gy
Copy link
Contributor Author

This CI error does not seem to be related to changes made in this PR: https://github.com/marimo-team/marimo/actions/runs/21514574296/job/61989661450?pr=8056

Copy link
Contributor

@mscolnick mscolnick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amazing stuff! great work

@mscolnick mscolnick merged commit 33ebb52 into marimo-team:main Jan 30, 2026
39 of 46 checks passed
@github-actions
Copy link

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.19.8-dev1

mscolnick pushed a commit that referenced this pull request Feb 4, 2026
## 📝 Summary

Adds notebook-level OpenGraph metadata (PEP 723 +
[Next.js-inspired](https://nextjs.org/docs/app/api-reference/functions/generate-metadata)
optional generator hook) with thumbnail generation plumbing. As a first
use case, wired it up to improve gallery cards in `marimo run <dir>`.

Follow-up of #8056.

## 🔍 Description of Changes

- Introduced `OpenGraphMetadata` for notebooks via PEP 723:
`[tool.marimo.opengraph]` with `title`, `description`, `image`, and
optional `generator`
  - `image` supports either an HTTPS URL or a notebook-relative path
- Added Next.js-style merge semantics for generators: static PEP 723
fields are the base; generator can override only the fields it returns
- Added a canonical thumbnail endpoint: `GET
/api/home/thumbnail?file=...`
  - redirects to HTTPS images
- serves notebook-relative images only from the notebook’s `__marimo__/`
directory
  - falls back to a branded placeholder SVG when no image exists
- Inject OG tags into notebook pages (`og:title`, `og:description`,
`og:image`)
- Gallery cards now prefer `FileInfo.opengraph`
(title/description/image) and fall back to the existing title-casing
behavior
- Added `marimo tools thumbnails generate ...` to write thumbnails to
`__marimo__/assets/<stem>/opengraph.png`
  - default is `--no-execute` (fast; no cell outputs)
  - opt-in `--execute` to include outputs
  - opt-in `--sandbox` (only applies with `--execute`)

## 📸 Thumbnails

> in the examples below `marimo-team/learn` is a local clone of
https://github.com/marimo-team/learn

### Auto-generated thumbnails without `playwright`

No file gets written to disk. The `/thumbnail` API endpoint calls
`DEFAULT_OPENGRAPH_PLACEHOLDER_IMAGE_GENERATOR` to construct an SVG
dynamically and serves it.

<img width="1111" height="1314" alt="Screenshot 2026-02-03 at 14 36 25"
src="https://github.com/user-attachments/assets/73cdaa8c-ba4a-4c4e-a27d-cb48163b17b5"
/>

### Auto-generated thumbnails with `playwright`, but without notebook
execution

Renders code blocks without outputs, similar to Observable, as suggested
by @manzt.

```
 marimo tools thumbnails generate marimo-team/learn
 marimo run marimo-team/learn
```

<img width="1111" height="1314" alt="Screenshot 2026-02-03 at 14 40 50"
src="https://github.com/user-attachments/assets/d02611bd-e094-4031-ac7b-fec82330960a"
/>

### Auto-generated thumbnails with `playwright`, with sandboxed notebook
execution for cell outputs

> If you have generated thumbnails, pass ` --overwrite` to `marimo tools
thumbnails` to ensure they get replaced.

```
marimo tools thumbnails generate marimo-team/learn --execute --sandbox
marimo run marimo-team/learn  
```

<img width="1111" height="1314" alt="Screenshot 2026-02-03 at 14 49 52"
src="https://github.com/user-attachments/assets/9d6e0ea4-3845-436f-9ab5-9defd274cdc6"
/>

## ⚡️ Generators

Users can define custom functions to return OG metadata based on bespoke
logic.

Below an example of a notebook (1) defining `generator =
"generate_opengraph"` in PEP 723 then (2) implementing `def
generate_opengraph(context, parent):` as `@app.function` to return the
metadata dynamically, yielding a custom card from with image from
https://placehold.co/1200x630/png.

<img width="374" height="345" alt="Screenshot 2026-02-03 at 14 58 42"
src="https://github.com/user-attachments/assets/478af1c1-e6f3-4fc7-a79f-e604f39b591d"
/>

```python
# /// script
# dependencies = [
#     "marimo>=0.19.0",
#     "pyzmq>=27.1.0",
# ]
# [tool.marimo.opengraph]
# description = "The description is static, but the title and image have been computed dynamically using a generator function defined within the notebook."
# generator = "generate_opengraph"
# ///

import marimo

__generated_with = "0.19.7"
app = marimo.App(width="medium")


@app.cell(hide_code=True)
def _(mo):
    mo.md("""
    # Dynamic OpenGraph Image
    """)
    return


@app.function
def generate_opengraph(context, parent):
    import datetime as dt
    from pathlib import Path
    from urllib.parse import quote_plus

    # Merge behavior: we return `title` and `image`, so static PEP 723 description
    # remains intact, as `description` is already present in `parent`
    label = quote_plus(dt.datetime.now().isoformat())
    return {
        "title": f"Dynamic OpenGraph",
        "image": f"https://placehold.co/1200x630/png?text={label}"
    }


@app.cell(hide_code=True)
def _():
    import marimo as mo
    return (mo,)


if __name__ == "__main__":
    app.run()
```
mscolnick pushed a commit that referenced this pull request Feb 10, 2026
## 📝 Summary

Docs for #8056 and #8097.

---------

Co-authored-by: Akshay Agrawal <akshaykagrawal7@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request release-highlight A feature or change to call out in upcoming release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-App Runner

3 participants