Commit e7ba7bc
authored
feat: rich notebook previews through OpenGraph metadata (#8097)
## 📝 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()
```1 parent 892a44d commit e7ba7bc
File tree
19 files changed
+1888
-27
lines changed- frontend/src/components/pages
- marimo
- _cli
- development
- tools
- _metadata
- _server
- api/endpoints
- export
- models
- templates
- packages/openapi
- src
- tests
- _metadata
- _server
- api/endpoints
19 files changed
+1888
-27
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
36 | 45 | | |
37 | 46 | | |
38 | 47 | | |
| |||
43 | 52 | | |
44 | 53 | | |
45 | 54 | | |
46 | | - | |
47 | | - | |
48 | 55 | | |
49 | 56 | | |
| 57 | + | |
| 58 | + | |
50 | 59 | | |
51 | 60 | | |
52 | 61 | | |
53 | 62 | | |
54 | 63 | | |
55 | 64 | | |
56 | 65 | | |
57 | | - | |
| 66 | + | |
| 67 | + | |
58 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
59 | 77 | | |
60 | 78 | | |
61 | 79 | | |
62 | 80 | | |
63 | 81 | | |
| 82 | + | |
| 83 | + | |
64 | 84 | | |
65 | 85 | | |
66 | 86 | | |
67 | | - | |
| 87 | + | |
68 | 88 | | |
69 | 89 | | |
70 | 90 | | |
| |||
130 | 150 | | |
131 | 151 | | |
132 | 152 | | |
133 | | - | |
134 | | - | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
135 | 161 | | |
136 | 162 | | |
137 | 163 | | |
| |||
141 | 167 | | |
142 | 168 | | |
143 | 169 | | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
144 | 175 | | |
145 | 176 | | |
146 | 177 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
| |||
1476 | 1477 | | |
1477 | 1478 | | |
1478 | 1479 | | |
| 1480 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| |||
132 | 133 | | |
133 | 134 | | |
134 | 135 | | |
| 136 | + | |
135 | 137 | | |
136 | 138 | | |
137 | 139 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
0 commit comments