How to serve static files with django that has hardcoded relative paths on Heroku/OpenShift/etc?

3.4k views Asked by At

I have HTML/JS/CSS files provided by a third party (whom I have no control over) that serves as a single page app that communicates with a backend built with Django and django-rest-framework.

I'm wanting to host this on Heroku and thus these static assets are being served by Django. These files contains relative paths to each other. For example, the index.html contains:

<link rel="stylesheet" type="text/css" media="screen" href="styles/css/bootstrap.min.css">

Which leads to a 404 because styles/css/bootstrap.min.css is not routed by django.

The only way I know of to serve the index.html from my domain root www.domain.com is with an url config like:

url(r'^$', TemplateView.as_view(template_name='index.html'), name='home'),

...even though it's not really a template, it's just plain HTML.

The problem arises from the fact that all the urls in the other assets are relative to this index.html and of course Django doesn't work like that. If I was developing this front-end application I'd be using the static template tag and one of the various ways to get urls to javascript.

I don't mind switching from Heroku to another PaaS if they offer a solution to this problem, but manually editing all these files does not sound like a fun job...especially considering the fact that I'll be receiving updates to these files going forward.

I think the way to solve this on a regular old server would be to configure the web server to resolve these urls correctly, but that option doesn't seem to be available on Heroku.

1

There are 1 answers

2
metakermit On BEST ANSWER

Here's how to set up Django to serve your static files and index.html on / while still having the possibility to use Django views for the admin dashboard, registration etc.

from django.conf.urls import include, url
from django.contrib import admin
from django.contrib.staticfiles.views import serve
from django.views.generic import RedirectView

admin.autodiscover()

urlpatterns = [

    # / routes to index.html
    url(r'^$', serve,
        kwargs={'path': 'index.html'}),

    # static files (*.css, *.js, *.jpg etc.) served on /
    # (assuming Django uses /static/ and /media/ for static/media urls)
    url(r'^(?!/?static/)(?!/?media/)(?P<path>.*\..*)$',
        RedirectView.as_view(url='/static/%(path)s', permanent=False)),

    # other views still work too
    url(r'^admin/', include(admin.site.urls)),
]

I specify urlpatterns as a list, as Django 1.10 requires. The redirects are not permanent by default since 1.9, so you need to explicitly set permanent=True if you want browsers to cache this, though when debugging I find it better to start with False.

This approach allows you to use something like create-react-app or Yeoman frontend generators that package a built, minified frontend app into a single folder (like dist/). You then e.g. use a script to move that into Django's static files folder (e.g. myproject/static/) and serve it from Heroku.

What Ian wrote about wanting to use something like S3 for your static files stands, but sometimes you just want to start simple with one repository, one Heroku dyno and still be able to use Django + a SPA. Also, using something like WhiteNoise makes serving static files from Python pretty OK and allows you to later on easily put a CDN in front of your static files.

Note: for user-uploaded files you should still use an outside service like Amazon S3 or Backblaze B2 (which is 4 lines of code to integrate).

Serving Media Files

WhiteNoise is not suitable for serving user-uploaded “media” files. For one thing, as described above, it only checks for static files at startup and so files added after the app starts won’t be seen. More importantly though, serving user-uploaded files from the same domain as your main application is a security risk (this blog post from Google security describes the problem well). And in addition to that, using local disk to store and serve your user media makes it harder to scale your application across multiple machines.

For all these reasons, it’s much better to store files on a separate dedicated storage service and serve them to users from there. The django-storages library provides many options e.g. Amazon S3, Azure Storage, and Rackspace CloudFiles.

Note 2: on production with DEBUG=False there are issues, so check out this WhiteNoise issue for a solution.

Note 3: Turned this answer into a blog post here.

Note 4: since writing this, I've been tweaking the solution more and more for frontend routing, so I ended up releasing a new django-spa package suited for simple serving of single-page apps from Django.