This guide explains how the standalone HTML export works after the sectioned page API refactor.
Use it when you are:
- adding export support for a page
- changing selector behavior in export
- changing the Python-to-JavaScript payload/runtime contract
The export path does not save the live Panel app directly.
Instead it:
- resolves the enabled leaf pages
- instantiates the same
DashboardPageobjects used by live mode - reads registered selectors and sections from each page instance
- pre-renders the needed selector variants per section
- serializes the resulting page shell and region snapshots into JSON
- renders that JSON with
dashboard/export/assets/export_runtime.js
The important architectural change is that export metadata is no longer supposed to come from separate page-owned PAGE.selectors or PAGE.export_regions declarations for newly authored pages. It should come from runtime selector(...) and section(...) registration.
For new-style pages, export support is derived from:
PAGE = DashboardPageDefinition(...)DashboardPage.build_page()self.selector(...)self.section(...)
PAGE still owns:
- page identity
- ordering
- group membership
- summary/prepared-data requirements
The page instance owns:
- selector widgets
- section dependencies
- exportable section boundaries
The serializer handles these common Panel types well:
pn.Columnpn.Rowpn.Cardpn.Tabspn.pane.Markdownpn.pane.HTMLpn.pane.Plotlypn.widgets.Tabulatorpn.widgets.Selectpn.widgets.RadioButtonGrouppn.Spacer
A good interactive export page usually has:
- a small bounded selector option set
- stable widget instances created in
build_page() - sections whose content can be fully pre-rendered
Registered selectors become export selector metadata.
Registered sections become export regions when export=True.
Section selector dependencies do double duty:
- they drive live section-aware refresh
- they define the selector dimensions that need offline variants
export=False on a section keeps it static in the exported page shell.
exportable=False on a selector keeps the control visible in live mode while disabling offline interactivity for that selector.
For export-aware pages:
- create widgets in
build_page() - register selectors with stable ids
- register sections with stable ids
- keep section render functions deterministic for a given widget state
- reconcile widget options and defaults in
sync_controls()
Do not add new export-only registries or duplicate selector metadata in PAGE.
Standalone page selectors are configured under:
dashboard:
export:
pages:
trip_mode:
tour_purpose: allGrouped page selectors are configured under:
dashboard:
export:
pages:
trip_summaries:
children:
trip_mode:
tour_purpose: allGrouped pages are identified by leaf page_id, not child_id.
Export config validation checks:
- unknown page ids
- unknown group ids
- unknown selector ids
- unknown part ids
For migrated pages, selector ids and part ids should resolve from the runtime registration graph first, with legacy PAGE.selectors and legacy export-part metadata only serving as compatibility fallback for untouched pages.
If you change the payload or runtime behavior:
- update
dashboard/export/types.py - update
dashboard/export/payload.py - update
dashboard/export/serializer.pyif node serialization changed - update
dashboard/export/assets/export_runtime.js - update
docs/export_html_schema.md - update the export payload and smoke tests
The generated runtime asset is now built from dashboard/export/js_runtime/ by
dashboard/export/build_export_runtime.py. Make source changes in js_runtime/, then
rebuild the committed asset.
At minimum, touch the relevant subset of:
tests/test_export_payload.pytests/test_export_html.pytests/test_export_html_smoke.pytests/test_export_serializer.pytests/test_export_warnings.py
Good export-focused assertions now include:
- selector metadata derives from
selector(...) - region metadata derives from
section(...) - grouped descriptors use
default_page_id - grouped config resolves children by leaf
page_id - unrelated sections do not produce unnecessary selector variants