How should I handle URL references that are relative to the root of my application, without knowing the application's root URL?

241 views Asked by At

I have an application I'm writing using CherryPy, Mako HTML templates, and JavaScript. I want to allow it to be installed to any URL - that is, I want someone to be able to install it to http://example.com, or http://example.com/app, or http://this.is.an.example.com/some/application/whatever. I also want it to work just as well as a WSGI application behind Apache or using CherryPy's built in webserver. However, I'm having trouble dealing with the URLs my templates and JavaScript must use.

That is, if I want to access a resource that is relative to my application root, like api/something or static/application.js, how can I make sure that the reference will work no matter what my app's base URL is?

My initial, naive solution was to use relative URLs, but this stopped working when I wanted to return the same Mako template at multiple endpoints. That is, if I have a template used for displaying an Item, maybe I will want to display that Item somewhere in the page at the root (/), and maybe somewhere else also (like maybe /item, and maybe /username/items too). If the template has a relative URL in it, that URL is relative to that location - which means that, since my static CSS/JS/image resources are relative only to root, the links will be broken for the templates at the /item and /username/items locations.

Mako templates

I have found that I can wrap my URLs in cherrypy.url() in my Mako templates, which solves this problem. For instance, this example template will reference the /link URL properly no matter what:

<%!
    import cherrypy
%>
<a href="${cherrypy.url('/link')}">Click here!</a>

This is fine, but it's kind of cumbersome, and it seems weird to import cherrypy in my templates. Is this the right way to do it? Is there a better way? It would be nice if it were automatic, so I don't have to remember to wrap the URLs myself.

JavaScript

I'm serving static .js files using CherryPy's tools.staticdir option. That's working fine, but I am making a couple of AJAX calls, and like the static resources in my Mako templates, the URLs are relative to my application root. For instance, if CherryPy exposes an /api/something URL, and I want to reach it from JavaScript, how can I write my JS so that it's still accessible no matter where my CherryPy application is mounted?

My first thought was that I could add some kind of hidden HTML element or comment to my templates that contains the value of cherrypy.url() called with no arguments, which would result in the root URL of my application, and that I could traverse the DOM, grab that value, and prepend it to any URL I wanted before I tried to make an HTTP request. The upside is that this would be pretty transparent in JavaScript; the downside is that it would be easy to forget to include the magic hidden HTML element as I add more and more templates to the application. I suppose I could solve this by making all templates dependent on a root template, but it still seems like a hack.

My second thought was to turn my JavaScript files themselves into Mako templates, not use tools.staticdir to serve them, and use the same cherrypy.url() method that I use in my existing Mako HTML templates. This has the appeal of consistency and doesn't require a magic HTML element in my existing templates, but it means that files have to go through a whole template rendering process before they can be served at the theoretical loss of some speed, and it also feels kinda wrong.

Is there a better option?

Other concerns

Although I don't have this problem currently, I suppose in the future I might want to use application-relative URLs in my static CSS files too.

Code smell problem?

I have spent some time on Google and SO and in the CherryPy documentation trying to find a solution to this problem, but I haven't turned up anything. Is this an indication that I'm doing something weird, and that there is some pattern or best practice which avoids this problem that I'm just not following?

1

There are 1 answers

0
Micah R Ledbetter On BEST ANSWER

I ended up converting my bare JavaScript files to Mako templates of JavaScript files, and passing in a baseurl variable, set to the value of cherrypy.url('/'). Because of the existing structure of my application, I was able to do this automatically for every rendered template, which mostly satisfies my needs.

How I am using Mako

First, note that I was using a MakoHandler and a MakoLoader class from the Mako page on the CherryPy wiki. Using those classes look like this (lightly edited for brevity):

import cherrypy
from mako.lookup import TemplateLookup

class MakoHandler(cherrypy.dispatch.LateParamPageHandler):
    def __init__(self, template, next_handler):
        self.template = template
        self.next_handler = next_handler
    def __call__(self):
        env = globals().copy()
        env.update(self.next_handler())
        try:
            return self.template.render(**env)
        except:
            cherrypy.response.status = "500"
            return exceptions.html_error_template().render()

class MakoLoader(object):
    def __init__(self):
        self.lookups = {}
    def __call__(self, filename, directories, module_directory=None,
                 collection_size=-1):
        key = (tuple(directories), module_directory)
        try:
            lookup = self.lookups[key]
        except KeyError:
            lookup = TemplateLookup(directories=directories,
                                    module_directory=module_directory,
                                    collection_size=collection_size)
            self.lookups[key] = lookup
        cherrypy.request.lookup = lookup
        cherrypy.request.template = t = lookup.get_template(filename)
        cherrypy.request.handler = MakoHandler(t, cherrypy.request.handler)

main = MakoLoader()
cherrypy.tools.mako = cherrypy.Tool('on_start_resource', main)

Which then allows you to reference the templates in CherryPy like this:

@cherrypy.expose
@cherrypy.tools.mako(filename="index.html")
def index(name=None):
    return {'username': name}

Adding a default set of variable substitutions

Now, with this code, you can add variable substitutions to all templates by modifying the env variable that MakoHandler.__call__() passes to self.template.render. With that in mind, we can change the MakoHandler class to look like this:

class MakoHandler(cherrypy.dispatch.LateParamPageHandler):
    def __init__(self, template, next_handler):
        self.template = template
        self.next_handler = next_handler
    def __call__(self):
        env = globals().copy()
        env.update(self.next_handler())
        env.update({'baseurl': cherrypy.url('/')})
        try:
            return self.template.render(**env)
        except:
            cherrypy.response.status = "500"
            return exceptions.html_error_template().render()

With that, the baseurl variable is set to the root of the application within all templates - exactly what I wanted. It even works with templates which I <%include.../> within another template (see below).

Side benefits

Before, in my HTML, I had a <script> tag that pointed to a static JS file served by CherryPy, and the browser made a separate HTTP request to go get that file. But when I moved to using Mako to template my JavaScript in order to add the baseurl variable, I realized I could just <%include.../> it in my HTML directly, eliminating one round trip.