Default page when the requested page is not in the navigation page list

Hi everyone! I’m facing a need. I have my Streamlit app working fine but I noticed a behaviour I would like to tackle.
My Streamlit app is a multipage app and I’m using an entry point to dynamically add pages based on some condition.
The setup:

  • when the condition is false I add pages A, B, C (using streamlit.Page class)
  • when the condition is true I add pages A, B, D (also using streamlit.Page class)

The problem. If I directly access to the url http://<>/A or http://<>/B everything is fine because A and B pages are added in both situations. If I directly access to http://<>/C and for some reason the condition is true then I get the popup saying
You have requested page, but no corresponding file was found in the app's pages/ directory
which makes sense of course. But also would be great to be able to customise this behaviour.
Is there a way to customise this behaviour? I was looking for something like add the requested page in the navigation and make the page redirect but it looks like is impossible because I cannot find the requested page in the streamlit.session_state.
I think it could work also to be able to have a default page if the requested one is not in the navigation instead of having the popup.

I do not think that is possible but it would be a nice improvement to Streamlit. There is a similar*-ish* enhancement request to customize 404s (Custom 404 / Error Page / Maintenance Page that is displayed whenever a bug is pushed or the app encounters and issue on boot · Issue #8713 · streamlit/streamlit · GitHub).

That’s bad. If a user already went on a page /A so it knows it exists, it would be good to personalise the message at least (for example, in case the navigation menu changes because user is not logged in). Even worse if the user bookmarked the page.
The user would be confused if it logs in and then found out the page is actually there.
It would be very easy to add a custom logic if it was possible to get the user requested url (I don’t mean through javascript, because you need to wait the page is rendered which is not the case if you are doing this in the entry point acting like a router). Is it so complex to expose the requested url in the session or in the client retrieved by st.runtime.get_instance().get_client(get_script_run_ctx().session_id)?

In that case, I would put all the pages in the navigation definition and create a custom navigation menu based on whether the user is logged in. If the user attempts to enter an unavailable page, we can show an error message and then redirected them to another page. Here is small example:

unavailable_page

Code:
from itertools import compress
from time import sleep

import streamlit as st


### Login helpers

if "logged_in" not in st.session_state:
    st.session_state.logged_in = False


def login():
    st.session_state.logged_in = True


def logout():
    st.session_state.logged_in = False


### Page definitions
def page_main():
    st.title("Main page")


def page_a():
    st.title("Page A")


def page_b():
    st.title("Page B")


def page_c():
    if not st.session_state.logged_in:
        st.error("You are not logged in. Redirecting to main page...")
        sleep(2)
        st.switch_page(pages[0])
        return

    st.title("Page C")


def page_d():
    if st.session_state.logged_in:
        st.error("You are logged in. Redirecting to main page...")
        sleep(2)
        st.switch_page(pages[0])
        return

    st.title("Page D")


pages = [
    st.Page(page_main, title="Main"),
    st.Page(page_a, title="A"),
    st.Page(page_b, title="B"),
    st.Page(page_c, title="C"),
    st.Page(page_d, title="D"),
]

nav = st.navigation(pages=pages, position="hidden")

### App layout

with st.sidebar:
    # Dummy login/logout system
    st.toggle("Logged in?", on_change=logout if st.session_state.logged_in else login)

    # Custom navigation depending on login:
    st.header("Navigation")
    if st.session_state.logged_in:
        mask = [True, True, True, True, False]  # Hide page D
    else:
        mask = [True, True, True, False, True]  # Hide page C

    for page in compress(pages, mask):
        st.page_link(page)

nav.run()

Exactly what I was doing so far but I don’t want to show the pages if the user is not logged in

In the example, page C is hidden when the user is not logged in, and page D is hidden when they are logged in. You can select which pages to show in the navigation by tweaking the mask in the code.

Oh yeah, I didn’t see that detail. Ok, I absolutely need to know how this works! ahah
How do you modify the pages list after this is set? I can see you call once st.navigation passing all the pages.
I understand this

for page in compress(pages, mask):
        st.page_link(page)

is the piece of code which does the trick but how does it work? I don’t see you reassigning the pages to st.navigation (which I think you cannot even do because you can call it once during the page run). Could you explain me how this works please? I understand the for loop loops over only the pages you want to show thanks to the filter applied by the compress function, but I don’t get how the pages are reassigned to the st.navigation

The pages list was never modified.


We could split what st.navigation does into two separate tasks:

  1. It declares all the pages that will be available in the app.
  2. [Optional] It generates a navigation menu in the sidebar.

Step 1 is mandatory, and we pass all the possible pages the app can have. This way Streamlit knows that all of those exist and point to valid URLs. Step 2 runs by default, however, in this example, we opt out of Step 2 and hide the default navigation that would list all the pages using the position argument:

nav = st.navigation(pages=pages, position="hidden")

At this point, we have an app without a pages navigation menu, however, the URLs for all those pages will be valid! If we do want a page menu, we have to explicitly build it. That is what the for loop does in the example.

I understand the “show” part is done by st.page_link(page), right? How does it work? Is st.page_link basically changing internally some attributes on the page object which is the same object passed to st.navigation? What I am missing is how the show part reflect on st.navigation

I think I got it. The pages passed to st.navigation are just to tell streamlit the allowed url, as you pointed out. Then, what you do is to show the menu not through the st.navigation anymore but adding the links using st.page_link using it inside with st.sidebar

1 Like