feat: allow external URLs in st.Page for navigation sidebar #13691
feat: allow external URLs in st.Page for navigation sidebar #13691sfc-gh-bnisco merged 27 commits intostreamlit:developfrom
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
|
Thanks for contributing to Streamlit! 🎈 Please make sure you have read our Contributing Guide. You can find additional information about Streamlit development in the wiki. The review process:
We're receiving many contributions and have limited review bandwidth — please expect some delay. We appreciate your patience! 🙏 |
There was a problem hiding this comment.
Pull request overview
This pull request adds support for external web links in st.Page for navigation menus, allowing users to include external URLs alongside internal pages. The implementation addresses feature request #9025 by enabling external links that open in new tabs when clicked.
Changes:
- Added external URL detection and validation in
st.Pageconstructor with proper error handling for missing titles and invalid default page configuration - Extended the AppPage protobuf message with
is_externalandexternal_urlfields for client-server communication - Updated frontend navigation components (SidebarNavLink, TopNav, TopNavSection, SidebarNav) to render external links with
target="_blank"and proper security attributes
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| proto/streamlit/proto/AppPage.proto | Added is_external and external_url fields to AppPage message for backward-compatible external URL support |
| lib/streamlit/navigation/page.py | Implemented _is_url() helper, external URL validation, and new properties is_external and external_url; modified run() to skip execution for external pages |
| lib/streamlit/commands/navigation.py | Updated navigation command to set protobuf fields for external URL pages |
| lib/tests/streamlit/navigation/page_test.py | Added comprehensive test suite (11 tests) covering external URL functionality including edge cases |
| frontend/app/src/components/Navigation/SidebarNavLink.tsx | Added external link support with target="_blank", rel="noopener noreferrer", and conditional onClick handling |
| frontend/app/src/components/Navigation/TopNav.tsx | Updated to detect and handle external pages by checking isExternal flag and early-returning from onClick |
| frontend/app/src/components/Navigation/TopNavSection.tsx | Modified dropdown items to support external links with proper click handling and popover closing |
| frontend/app/src/components/Navigation/SidebarNav.tsx | Added external link detection and handling to sidebar navigation links |
| href={isExternal && externalUrl ? externalUrl : pageUrl} | ||
| onClick={isExternal ? undefined : onClick} | ||
| aria-current={isActive ? "page" : undefined} | ||
| {...(isExternal | ||
| ? { target: "_blank", rel: "noopener noreferrer" } | ||
| : {})} |
There was a problem hiding this comment.
The SidebarNavLink component now supports external links with new props isExternal and externalUrl, but there are no tests covering this new functionality. The existing test file (SidebarNavLink.test.tsx) has comprehensive coverage for other features.
Add tests to verify:
- External links render with
target="_blank"andrel="noopener noreferrer"attributes - External links use
externalUrlas the href whenisExternalis true - External links do not call
onClickhandler (it should be undefined) - Internal links continue to work as before (negative assertion: verify
targetattribute is not present whenisExternalis false)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| href={pageUrl} | ||
| onClick={onClick} | ||
| href={isExternal && externalUrl ? externalUrl : pageUrl} | ||
| onClick={isExternal ? undefined : onClick} |
There was a problem hiding this comment.
TopNav dropdown popover doesn't close for external links
Medium Severity
When clicking an external link in the TopNavSection dropdown popover, the popover remains open. In TopNavSection, the handleClick function includes setOpen(false) to close the popover for external links. However, SidebarNavLink sets onClick={isExternal ? undefined : onClick}, which means for external links the handler is never attached to the DOM element. The setOpen(false) call intended to close the popover is never reached.
Additional Locations (1)
lib/streamlit/commands/navigation.py
Outdated
| p.is_default = page._default | ||
| p.section_header = section_header | ||
| p.url_pathname = page.url_path | ||
| # External URL support (Issue #9025) |
There was a problem hiding this comment.
External URL can become default page implicitly
High Severity
When no page explicitly has default=True, _navigation() selects the first page as default by directly setting _default = True on the instance. This bypasses the constructor validation that prevents external URLs from being default pages. If an external URL is first in the pages list, it becomes the default, and calling run() returns immediately without rendering any content, leaving the app with a blank main content area.
Additional Locations (1)
8697ec7 to
01f7b2e
Compare
|
Behavior and API sound good! Let me know once you fixed the most important issues above and I can give this a try.
Yeah I think that would be cool! Can you try adding a small material icon on the right side of it, and post a screenshot of what it looks like? Then we can decide based on that. |
|
@jrieke Thank you for the review! I've addressed the feedback and added a visual indicator for external links:
Ready for your review when you have a chance! |
|
Hm okay I think the icon actually looks a bit weird, given that page icons are on the left side. I'd say we maybe we just leave this out and the dev can put an icon themselves on the left side if they want to show an indicator of this being an external page. Did you already work on addressing the comments from Cursor above? If yes, happy to try it out locally. |
Thanks for the feedback! The external link icon (launch icon) is placed on the right side, following the common UI pattern where left icons indicate content type and right icons indicate actions/behavior (similar to Material Design guidelines). Could you clarify what feels off — is it having two icons per row, or something else about the styling? Happy to adjust.
Yes, both Cursor comments have been addressed
|
|
@jrieke How about aligning the nav items in a three-column layout?
This way:
I prototyped this with a small change — just fixed the width of StyledSidebarNavIcon and used margin-left: auto on the launch icon. All existing tests pass. What do you think? Happy to push this if you'd like to see it in action. |
40d5f05 to
d92949e
Compare
|
Appreciate the effort but let's just remove the icon. I think altering the layout adds too much complexity for a small feature. |
|
@jrieke Thanks for the feedback! I've removed the launch icon — external links now use the same layout as internal pages (icon + text only). Both Cursor comments are also addressed:
Ready for re-review when you have a chance. |
efb52ca to
065ac90
Compare
SummaryThis PR adds support for external web links (http/https URLs) in Code QualityThe code is generally well-structured and follows existing patterns. A few observations:
Test CoveragePython unit tests ( Frontend unit tests ( Significant gaps:
Backwards CompatibilityProtobuf: Adding fields 8 ( Critical interaction issues:
Security & Risk
Accessibility
Recommendations
VerdictCHANGES REQUESTED: The feature concept is solid, but there are critical interaction bugs with This is an automated AI review by |
Source fixes: - page.py: fix mypy no-redef errors by using conditional assignments - navigation.py: remove redundant cast calls (mypy redundant-cast) - execution_control.py: reject st.switch_page for external URL pages - button.py: support external StreamlitPage in st.page_link Test additions: - execution_control_test.py: add is_external=False to existing mocks, add test for switch_page with external page - page_link_test.py: add tests for external StreamlitPage support - page_test.py: fix test assertions for updated page behavior - proto_compatibility_test.py: add new proto fields to known list
MagicMock(spec=StreamlitPage) creates mock attributes that are truthy by default. Without explicitly setting is_external=False, the test enters the external page code path and fails with TypeError.
…ation
- Fix RUF043 lint error: use raw string with escaped metacharacters in
pytest.raises match parameter
- Fix url_path="" vs None inconsistency: use explicit None check instead
of falsy-or to distinguish "not provided" from "explicitly empty"
- Add test for explicit empty url_path on external URL pages
- Add E2E tests for external links in sidebar and top navigation (8 tests)
- Remove {…item} spread in TopNavSection to prevent proto field DOM leakage
- Fix handleClick return type from boolean to void to match prop interface
- Remove Issue streamlit#9025 reference comments from 5 files
Reformat long lines in page.py and execution_control_test.py to satisfy ruff line-length rules.
…ormalization Address maintainer review feedback (sfc-gh-bnisco): - Prevent external pages from being selected as runnable target on direct URL access - Unify url_path normalization between external and internal pages - Add regression tests for both fixes
7434f83 to
33c21d1
Compare
Replace external navigation flags with an internal/external destination oneof to simplify AppPage semantics and avoid invalid state combinations. Also centralize frontend external-page detection helpers and remove unused open behavior metadata. Made-with: Cursor
…l docs Treat pages as external based on destination oneof presence instead of URL truthiness so malformed payloads do not get misclassified as internal navigation targets. Clarify st.Page and st.switch_page docs to document that hidden external pages can only be opened via st.page_link. Made-with: Cursor
- Add visually-hidden "(opens in new tab)" text for screen readers - Guard isActive so external pages never show as active - Use nullish coalescing (??) in getExternalPageUrl for precision - Add tests for external/internal url_path collision detection Co-Authored-By: Claude Opus 4.6 <[email protected]>
Remove redundant comments, consolidate repetitive tests using parameterized expansion, and simplify conditional prop spreading in SidebarNavLink. Streamline _set_page_destination with direct None-check instead of redundant is_external guard. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Cover external link click handling in the TopNavSection popover dropdown, verifying that handlePageChange is not called for external links, is called for internal links, and that external links render with correct href/target/rel attributes while internal links do not. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Verify that dangerous URL schemes (javascript:, vbscript:, data:) are rejected by st.Page and that hidden external pages are excluded from both SidebarNav and TopNav rendering. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Consolidated Code ReviewSummaryThis PR adds first-class support for external web links (http/https URLs) in Both reviewers approved this PR. No critical or blocking issues were identified. Code QualityReviewers agree: The implementation is clean, well-structured, and follows existing patterns consistently across the stack. Key strengths noted by both:
Minor suggestions (non-blocking):
Test CoverageReviewers agree: Coverage is thorough, well-structured, and appropriately layered.
Minor gap (opus-4.6-thinking): No test for Backwards CompatibilityReviewers agree: No breaking changes. All changes are purely additive.
Security & RiskReviewers agree: Security posture is strong.
No security issues found. External Test RecommendationReviewers disagree:
Resolution: I agree with opus-4.6-thinking's assessment. The E2E tests verify DOM attributes and local navigation behavior using a specific app script structure. They never actually navigate to external URLs. The tests cannot run against an arbitrary externally hosted app, making AccessibilityReviewers agree: Good accessibility practices throughout.
Enhancement suggestion (opus-4.6-thinking): No visual indicator (icon) distinguishes external links for sighted users. A follow-up could add a small external-link icon (e.g., E2E External URLsReviewers disagree:
Resolution: opus-4.6-thinking's analysis is correct. The URLs are string literals checked via attribute assertions, not HTTP requests. No network dependency is introduced. The current approach is acceptable, though using placeholder URLs (e.g., Recommendations
VerdictAPPROVED — Both reviewers approved. The feature is implemented coherently across backend/frontend/proto layers with comprehensive test coverage, proper security safeguards, good accessibility practices, and fully backward-compatible changes. All recommendations are minor, non-blocking enhancements suitable for follow-up work. Consolidated review by 📋 Review by `gpt-5.3-codex-high`SummaryThis PR adds first-class external URL support to Code QualityThe implementation is clean and consistent across backend/frontend boundaries, with good use of helper abstractions ( No blocking code-quality defects were identified. Minor follow-up (non-blocking):
Test CoverageCoverage is strong and appropriately layered:
Net: the changed behavior is well covered for local CI contexts. Backwards CompatibilityBackwards compatibility looks good:
Security & RiskSecurity posture is improved/maintained:
No critical security issues found. External test recommendation
AccessibilityAccessibility handling is generally good:
No blocking a11y regressions found. Recommendations
VerdictAPPROVED: The feature is implemented coherently across backend/frontend/proto layers with solid coverage and no blocking regressions identified. This is an automated AI review by 📋 Review by `opus-4.6-thinking`SummaryThis PR adds support for external web links in Code QualityThe code is well-structured and follows existing patterns consistently across the stack. Backend (Python):
Frontend (TypeScript/React):
Protobuf:
Test CoverageTest coverage is thorough and well-structured. Python unit tests:
Frontend unit tests:
E2E tests:
Minor gaps:
Backwards CompatibilityProtobuf: The changes are purely additive — new fields 8 and 9 on Python API: Frontend: The No breaking changes identified. Security & RiskURL scheme restriction: The
Direct URL access prevention: External pages cannot be accessed by navigating directly to their URL path (
No server-side request risk: The external URL is only passed to the frontend as metadata; the backend never fetches or follows the URL. All navigation happens client-side. Low regression risk: Changes to existing files are minimal and well-guarded by feature detection ( External test recommendationRecommendation: No — Triggered categories: None. Key evidence:
Confidence: High. The tests are inherently local-only due to their dependence on the app script's navigation structure. AccessibilityGood practices:
Potential improvement:
Recommendations
VerdictAPPROVED: Well-implemented feature with comprehensive test coverage, proper security safeguards, good accessibility practices, and fully backward-compatible changes. The recommendations are minor enhancements that can be addressed in follow-up work. This is an automated AI review by |
Replace falsy check with nullish coalescing (`??`) so that an external page with an empty-string URL is not incorrectly routed to the internal pageUrl. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Update the `visibility` parameter docstring in `StreamlitPage` to reflect that hidden external pages are not URL-accessible or switchable via `st.switch_page`, unlike hidden internal pages. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
@t0k0shi Just a heads up that I pushed some changes to this branch intentionally to fix up a few edge cases and add some additional tests. |
proto/streamlit/proto/AppPage.proto
Outdated
| // Destination for this page. | ||
| // Internal pages target app code. External pages target a URL. | ||
| // The navigation payload backend always sets one of these fields. | ||
| oneof destination { | ||
| InternalDestination internal = 8; | ||
| ExternalDestination external = 9; | ||
| } | ||
|
|
||
| message InternalDestination {} | ||
|
|
||
| message ExternalDestination { | ||
| // External URL that should be opened for this page. | ||
| string url = 1; | ||
| } |
There was a problem hiding this comment.
nitpick: I think this could also be simplified by just having a single optional string url property, if set, its an external destination. However, this is a bit more explicit, but maybe would benefit from moving internal-only props into the InternalDestination, e.g., page_script_hash?
There was a problem hiding this comment.
Yeah, this might be over-engineering. I pushed an update that simplifies it just optional string external_url
Add support for external web links in st.navigation, allowing users to include external URLs alongside internal pages in both sidebar and top navigation modes. This implements the feature requested in #9025. Changes Backend (lib/streamlit/navigation/page.py) - Detect HTTP/HTTPS URLs using existing url_util.is_url() - Handle external URLs in StreamlitPage.__init__ with title validation and URL path sanitization - Add is_external and external_url properties - Skip code execution for external links in run() - Use explicit None check for url_path to correctly reject empty strings Backend (lib/streamlit/commands/execution_control.py, lib/streamlit/elements/widgets/button.py) - st.switch_page: raise StreamlitAPIException for external pages (internal navigation only) - st.page_link: render external StreamlitPage objects as external links Protobuf (proto/streamlit/proto/AppPage.proto) - Adds optional `external_url ` field Frontend - Update SidebarNavLink to handle external links with target="_blank" and rel="noopener noreferrer" - Update SidebarNav, TopNav, TopNavSection to pass external link info - Pass props explicitly in TopNavSection instead of spreading proto fields Tests - Add unit tests for external URL support in page_test.py (URL detection, title validation, url_path sanitization, edge cases) - Add is_external=False to existing switch_page test mocks - Add test for st.switch_page rejecting external pages - Add tests for st.page_link with external StreamlitPage objects - Add unit tests for external link click behavior in SidebarNav.test.tsx and TopNav.test.tsx - Add 8 E2E tests for external links in sidebar and top navigation - Add proto compatibility test fields Example Usage import streamlit as st def home(): st.title("Home") pages = [ st.Page(home, title="Home", icon="🏠", default=True), st.Page("https://docs.streamlit.io", title="Docs", icon="📚"), st.Page("https://github.com/streamlit/streamlit", title="GitHub", icon="🐙"), ] pg = st.navigation(pages) pg.run() External links: - Require a title parameter (cannot be inferred from URL) - Cannot be set as the default page - Open in a new tab (target="_blank") - st.switch_page raises an error (internal navigation only) - st.page_link renders them as external links Demo https://github.com/user-attachments/assets/76a3d917-051c-4e2b-9f1f-fdc33072ddd4 External links in the sidebar open in a new tab. Internal pages navigate normally. Closes #9025 --------- Co-authored-by: Ubuntu <azureuser@dev-keru09.yuc22wcrz1luho2fqzdqlkjrgg.bx.internal.cloudapp.net> Co-authored-by: t0k0shi <[email protected]> Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: Bob Nisco <[email protected]>
Add support for external web links in st.navigation, allowing users to include external URLs alongside internal pages in both sidebar and top navigation modes.
This implements the feature requested in #9025.
Changes
Backend (lib/streamlit/navigation/page.py)
Backend (lib/streamlit/commands/execution_control.py, lib/streamlit/elements/widgets/button.py)
Protobuf (proto/streamlit/proto/AppPage.proto)
external_urlfieldFrontend
Tests
Example Usage
import streamlit as st
def home():
st.title("Home")
pages = [
st.Page(home, title="Home", icon="🏠", default=True),
st.Page("https://docs.streamlit.io", title="Docs", icon="📚"),
st.Page("https://github.com/streamlit/streamlit", title="GitHub", icon="🐙"),
]
pg = st.navigation(pages)
pg.run()
External links:
Demo
naviside.mp4
External links in the sidebar open in a new tab. Internal pages navigate normally.
Closes #9025