The result is a page with the working bokeh app and text from the header, but the header logo image does not render. Console shows the error “404 GET /images/logo.png”
The built-in Bokeh server implemented by bokeh serve will not add routes for anything outside the app directory. I can think of two options:
Have some other site external to the Bokeh server host the content, and link to it with URL in your Bokeh app template
The Bokeh server can be used programmatically from APIs. Doing that you affords access to the underlying Tornado configuration, so you can add custom routes anwhere you like.
So potentially you could modify, e.g. toplevel_routes (in place append) to include your new special route(s), before you start your own Server in your code.
Also just FYI for reference, the standard “startup” code for bokeh serve is here:
So I think the method of modifying toplevel_patterns in urls.py will probably work the best for me. But I’m confused about how the root is being handled in that script. From the description at the top, it looks like tornado will view “/” as “<prefix>/”
So if I fork urls.py and add (r"/…"), to toplevel_patterns: URLRoutes = [
Will it know how to handle that for navigating to /images or any other directory I might add?
IIRC you would add an entry that maps a URL path like "/images" to a handler. In this case I think you’d want a Tornado StaticFileHandler configured to point to whatever arbitrary filesystem location you want to serve from.
#-----------------------------------------------------------------------------
# Copyright (c) Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
''' Define standard endpoints and their associated views for a Bokeh Server
application.
These will be added by the :class:`~bokeh.server.tornado.BokehTornado`
application. The Bokeh Tornado application can then be served using Tornado's
built-in ``HTTPServer``.
.. data:: toplevel_patterns
:annotation:
Top-level routes, independent of any applications. They will be prefixed
with any configured prefix.
.. code-block:: python
[
( r'/?', RootHandler ), # <prefix>/
( r'/static/(.*)', StaticHandler ), # <prefix>/static/
]
.. data:: per_app_patterns
:annotation:
Per-application routes. These be prefixed with the application path, as
well as with any configured prefix.
.. code-block:: python
[
( r'/?', DocHandler ), # <prefix>/<app>/
( r'/ws', WSHandler ), # <prefix>/<app>/ws
( r'/metadata', MetadataHandler ), # <prefix>/<app>/metadata
( r'/autoload.js', AutoloadJsHandler ), # <prefix>/<app>/autoload.js
]
'''
# Please update the docstring above if any changes are made below
#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations
import logging # isort:skip
log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
# Standard library imports
from typing import Any, TypeAlias
# External imports
from tornado.web import RequestHandler
from tornado.web import StaticFileHandler
# Bokeh imports
from ..embed.bundle import extension_dirs
from .views.autoload_js_handler import AutoloadJsHandler
from .views.doc_handler import DocHandler
from .views.metadata_handler import MetadataHandler
from .views.multi_root_static_handler import MultiRootStaticHandler
from .views.root_handler import RootHandler
from .views.static_handler import StaticHandler
from .views.ws import WSHandler
#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------
__all__ = (
'per_app_patterns',
'toplevel_patterns',
)
#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------
RouteContext: TypeAlias = dict[str, Any]
URLRoutes: TypeAlias = list[
tuple[str, type[RequestHandler]] |
tuple[str, type[RequestHandler], RouteContext],
]
toplevel_patterns: URLRoutes = [
(r'/?', RootHandler),
(r'/static/extensions/(.*)', MultiRootStaticHandler, dict(root=extension_dirs)),
(r'/static/(.*)', StaticHandler),
(r'/images/(.*)', StaticFileHandler, {"path": "/images"}),
]
per_app_patterns: URLRoutes = [
(r'/?', DocHandler),
(r'/ws', WSHandler),
(r'/metadata', MetadataHandler),
(r'/autoload.js', AutoloadJsHandler),
]
#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
I’m glad that worked out. IMO it wouldn’t be a huge lift to add some kind of hook to make it possible to extend the list of routes with standard bokeh serve. In case you are interested to propose this feature, and especially if you might be interested to work on a PR for it, please feel free to open a GitHub Issue.