Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/guides/publishing/opengraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ For execution and sandbox options (and for Playwright installation instructions)

## Dynamic metadata

For dynamic previews, you can provide a generator function in script metadata. Generators must be [top-level functions](../reusing_functions.md) (decorated with `@app.function`) and cannot depend on values defined in regular cells, so that they can be safely executed in the marimo CLI without running the whole notebook.
For dynamic previews, you can provide a generator function in script metadata. Dynamic metadata executes Python while the server resolves previews, so enable it only for notebooks you trust. Run `marimo run --execute-opengraph-generators folder/`, or pass `execute_opengraph_generators=True` to `marimo.create_asgi_app()`.

Generators must be [top-level functions](../reusing_functions.md). They run outside normal cell execution, so values defined in regular cells are unavailable at metadata resolution time.

In script metadata, point `generator` at the name of a function defined in the notebook:

Expand Down
8 changes: 8 additions & 0 deletions marimo/_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,11 @@ def _create_run_workspace(
hidden=True,
help="Custom asset URL for loading static resources. Can include {version} placeholder.",
)
@click.option(
"--execute-opengraph-generators",
Comment thread
peter-gy marked this conversation as resolved.
is_flag=True,
help="Execute OpenGraph generators for trusted notebooks.",
)
@click.option(
"--show-tracebacks/--no-show-tracebacks",
is_flag=True,
Expand Down Expand Up @@ -1137,6 +1142,7 @@ def run(
trusted: bool | None,
server_startup_command: str | None,
asset_url: str | None,
execute_opengraph_generators: bool,
show_tracebacks: bool | None,
name: str,
args: tuple[str, ...],
Expand Down Expand Up @@ -1170,6 +1176,7 @@ def run(
"run",
port=port,
debug=GLOBAL_SETTINGS.DEVELOPMENT_MODE,
execute_opengraph_generators=execute_opengraph_generators,
)
return

Expand Down Expand Up @@ -1281,6 +1288,7 @@ def run(
sandbox_mode=sandbox_mode,
startup_tip=choose_startup_tip(click.get_current_context()),
show_tracebacks=show_tracebacks,
execute_opengraph_generators=execute_opengraph_generators,
)


Expand Down
3 changes: 3 additions & 0 deletions marimo/_cli/run_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def run_in_docker(
*,
port: int | None,
debug: bool = False,
execute_opengraph_generators: bool = False,
) -> None:
echo(f"Starting {green('containerized')} marimo notebook")

Expand Down Expand Up @@ -144,6 +145,8 @@ def run_in_docker(
host,
]
)
if execute_opengraph_generators:
container_command.append("--execute-opengraph-generators")
if file_path is not None:
container_command.append(file_path)

Expand Down
3 changes: 2 additions & 1 deletion marimo/_metadata/opengraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ def resolve_opengraph_metadata(
*,
app_title: str | None = None,
context: OpenGraphContext | None = None,
execute_generator: bool = False,
) -> OpenGraphMetadata:
"""Resolve OpenGraph metadata from config, defaults, and a generator hook."""
declared = read_opengraph_from_file(filepath) or OpenGraphConfig()
Expand All @@ -505,7 +506,7 @@ def resolve_opengraph_metadata(
image=image,
)

if declared.generator:
if declared.generator and execute_generator:
Comment thread
peter-gy marked this conversation as resolved.
ctx = context or OpenGraphContext(filepath=filepath)
dynamic = _run_opengraph_generator(
declared.generator, context=ctx, parent=resolved
Expand Down
6 changes: 6 additions & 0 deletions marimo/_server/api/endpoints/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ def og_thumbnail(*, request: Request) -> Response:
base_url=app_state.base_url,
mode=app_state.mode.value,
),
execute_generator=(
app_state.session_manager.execute_opengraph_generators
),
)
title = opengraph.title or "marimo"
image = opengraph.image
Expand Down Expand Up @@ -407,6 +410,9 @@ async def index(request: Request) -> Response:
else None,
asset_url=app_state.asset_url,
html_head=app_state.html_head,
execute_opengraph_generators=(
app_state.session_manager.execute_opengraph_generators
),
)

# Inject service worker registration with the notebook ID
Expand Down
3 changes: 3 additions & 0 deletions marimo/_server/api/endpoints/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ def get_files_with_metadata() -> list[FileInfo]:
base_url=base_url,
mode=mode,
),
execute_generator=(
session_manager.execute_opengraph_generators
),
)
result.append(
FileInfo(
Expand Down
4 changes: 4 additions & 0 deletions marimo/_server/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ def create_asgi_app(
redirect_console_to_browser: bool = False,
show_tracebacks: bool = False,
html_head: str | None = None,
execute_opengraph_generators: bool = False,
Comment thread
peter-gy marked this conversation as resolved.
) -> ASGIAppBuilder:
"""Public API to create an ASGI app that can serve multiple notebooks.
This only works for application that are in Run mode.
Expand All @@ -384,6 +385,8 @@ def create_asgi_app(
This is useful for adding global analytics scripts, custom stylesheets, meta tags, etc.
When a notebook also has its own `html_head_file` config, the global `html_head` is injected first,
followed by the per-notebook content.
execute_opengraph_generators (bool, optional): Execute notebook-defined OpenGraph generators while resolving metadata.
Enable this for notebooks from directories you trust.

Returns:
ASGIAppBuilder: A builder object to create multiple ASGI apps
Expand Down Expand Up @@ -562,6 +565,7 @@ def _create_app_for_file(base_url: str, file_path: str) -> ASGIApp:
isolate_apps=config_reader.experimental.get(
"isolate_apps", False
),
execute_opengraph_generators=execute_opengraph_generators,
)
enable_auth = not AuthToken.is_empty(auth_token)
app = create_starlette_app(
Expand Down
2 changes: 2 additions & 0 deletions marimo/_server/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def __init__(
watch: bool = False,
sandbox_mode: SandboxMode | None = None,
isolate_apps: bool = False,
execute_opengraph_generators: bool = False,
) -> None:
# Core configuration
self.workspace = workspace
Expand All @@ -98,6 +99,7 @@ def __init__(
self.redirect_console_to_browser = redirect_console_to_browser
self._config_manager = config_manager
self.sandbox_mode = sandbox_mode
self.execute_opengraph_generators = execute_opengraph_generators

# When running multiple apps, each app runs in an isolated host
# process, to avoid collisions in sys.modules and other Python global
Expand Down
2 changes: 2 additions & 0 deletions marimo/_server/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def start(
sandbox_mode: SandboxMode | None = None,
startup_tip: CliTip | None = None,
show_tracebacks: bool | None = None,
execute_opengraph_generators: bool = False,
) -> None:
"""
Start the server.
Expand Down Expand Up @@ -322,6 +323,7 @@ def start(
watch=watch,
sandbox_mode=sandbox_mode,
isolate_apps=isolate_apps,
execute_opengraph_generators=execute_opengraph_generators,
)

log_level = "info" if development_mode else "error"
Expand Down
4 changes: 4 additions & 0 deletions marimo/_server/templates/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def opengraph_metadata_template(
app_config: _AppConfig,
filename: str | None,
filepath: str | None,
execute_opengraph_generators: bool = False,
) -> str:
"""Return OpenGraph `<meta>` tags for a notebook, or an empty string."""
if not filepath:
Expand All @@ -221,6 +222,7 @@ def opengraph_metadata_template(
base_url=base_url,
mode=mode.value,
),
execute_generator=execute_opengraph_generators,
Comment thread
peter-gy marked this conversation as resolved.
)
except Exception:
return ""
Expand Down Expand Up @@ -277,6 +279,7 @@ def notebook_page_template(
runtime_config: list[dict[str, Any]] | None = None,
asset_url: str | None = None,
html_head: str | None = None,
execute_opengraph_generators: bool = False,
) -> str:
html = html.replace("{{ base_url }}", base_url)

Expand Down Expand Up @@ -337,6 +340,7 @@ def notebook_page_template(
app_config=app_config,
filename=filename,
filepath=filepath,
execute_opengraph_generators=execute_opengraph_generators,
)
if opengraph_tags:
html = html.replace("</head>", f"{opengraph_tags}\n</head>")
Expand Down
6 changes: 3 additions & 3 deletions tests/_metadata/test_opengraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def generate_opengraph(ctx, parent):
path = tmp_path / "notebook.py"
path.write_text(script, encoding="utf-8")

resolved = resolve_opengraph_metadata(str(path))
resolved = resolve_opengraph_metadata(str(path), execute_generator=True)
assert resolved.title == "Static Title"
assert resolved.image == "https://example.com/generated.png"

Expand All @@ -225,7 +225,7 @@ def generate_opengraph(ctx, parent, extra):
path = tmp_path / "notebook.py"
path.write_text(script, encoding="utf-8")

resolved = resolve_opengraph_metadata(str(path))
resolved = resolve_opengraph_metadata(str(path), execute_generator=True)
assert resolved.title == "Static Title"
assert resolved.image is None

Expand Down Expand Up @@ -254,6 +254,6 @@ def generate_opengraph(ctx, parent):
path = tmp_path / "notebook.py"
path.write_text(script, encoding="utf-8")

resolved = resolve_opengraph_metadata(str(path))
resolved = resolve_opengraph_metadata(str(path), execute_generator=True)
assert resolved.title == "Static Title"
assert resolved.image == "https://example.com/3.png"
19 changes: 18 additions & 1 deletion tests/_server/api/endpoints/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,23 @@ def test_workspace_files_in_run_mode(client: TestClient) -> None:
session_manager.mode = SessionMode.RUN

with tempfile.TemporaryDirectory() as temp_dir:
marker = Path(temp_dir) / "marker.txt"
marimo_file = Path(temp_dir) / "notebook.py"
marimo_file.write_text("import marimo\napp = marimo.App()")
marimo_file.write_text(
f"""# /// script
# [tool.marimo.opengraph]
# title = "Static Title"
# generator = "generate_opengraph"
# ///
import marimo
app = marimo.App()
with app.setup:
open({str(marker)!r}, "w").write("executed")
def generate_opengraph(context, parent):
return {{"title": "Dynamic Title"}}
""",
encoding="utf-8",
)

non_marimo_file = Path(temp_dir) / "text.txt"
non_marimo_file.write_text("This is not a marimo file")
Expand All @@ -145,6 +160,8 @@ def test_workspace_files_in_run_mode(client: TestClient) -> None:
assert len(files) == 1
assert body["root"] == temp_dir
assert files[0]["path"] == marimo_file.name
assert files[0]["opengraph"]["title"] == "Static Title"
assert not marker.exists()


@with_session(SESSION_ID)
Expand Down
Loading