Why isn't django serving my SPA static files correctly?
Asked Answered
O

6

13

I'm building a website using a Django backend and Vuejs frontend. In development I started the backend and the frontend separately with python manage.py runserver and yarn serve respectively. This worked great and I now want to deploy the website. To do this I ran yarn build, which created a dist/ folder in my frontend folder. So my structure was like this:

cockpit
├── backend/
│   ├── cockpit/
│   │   ├── views.py
│   │   ├── css/
│   │   └── etc..
│   ├── settings/
│   │   └── settings.py
│   └── manage.py
└── frontend/
    └── dist/
        ├── index.html
        ├── css/
        └── js/

I now want to serve the sources in frontend/dist/ from my django project so that I can run everything using uwsgi. To do this I'm trying to follow this description. I have the following settings/urls.py

from django.contrib import admin
from django.urls import include, path, re_path
from django.views.generic import TemplateView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('cockpit/', include("cockpit.urls")),
    re_path('', TemplateView.as_view(template_name='index.html')),
]

and set the following settings in my settings.py:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['static'],  # <== ADDED THIS
        'APP_DIRS': True,
        'OPTIONS': {
            # removed to keep this example small
        },
    },
]
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, '../frontend/dist'),
]
STATIC_ROOT = os.path.join(BASE_DIR, "static/")

print("BASE_DIR:", BASE_DIR)
print("STATIC_ROOT:", STATIC_ROOT)
print("STATICFILES_DIRS:", STATICFILES_DIRS)

And the prints show me this:

BASE_DIR: /home/kramer65/repos/cockpit/backend
STATIC_ROOT: /home/kramer65/repos/cockpit/backend/static/
STATICFILES_DIRS: ['/home/kramer65/repos/cockpit/backend/../frontend/dist']

Then I ran `python manage.py collectstatic:

$ python manage.py collectstatic

150 static files copied to '/home/kramer65/repos/cockpit/backend/static'.

So it now looks like this:

cockpit
├── backend/
│   ├── cockpit/
│   │   ├── views.py
│   │   ├── css/
│   │   └── etc..
│   ├── settings/
│   │   └── settings.py
│   └── manage.py
│   └── static/
│       ├── index.html
│       ├── css/
│       └── js/
└── frontend/
    └── dist/
        ├── index.html
        ├── css/
        └── js/

I tested it by running the (node) http-server from the backend/static/ folder. In the browser the website loads and runs perfect. Below is the output from the command line:

$ http-server
Starting up http-server, serving ./
Available on:
  http://127.0.0.1:8080
  http://192.168.0.104:8080
Hit CTRL-C to stop the server
[2020-05-18T13:50:58.487Z]  "GET /" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
(node:5928) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
[2020-05-18T13:50:58.671Z]  "GET /css/chunk-vendors.2c7f3eba.css" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
[2020-05-18T13:50:58.679Z]  "GET /css/app.e15f06d0.css" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
[2020-05-18T13:50:58.681Z]  "GET /js/chunk-vendors.9c409057.js" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
[2020-05-18T13:50:58.687Z]  "GET /js/app.c930fce5.js" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"

I stopped this http-server, started the Django dev server and opened the browser. The terminal shows me this:

$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
May 18, 2020 - 17:57:00
Django version 3.0.6, using settings 'settings.settings'
Starting ASGI/Channels version 2.4.0 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
HTTP GET / 200 [0.22, 127.0.0.1:33224]
HTTP GET /static/debug_toolbar/css/print.css 200 [0.04, 127.0.0.1:33232]
HTTP GET /static/debug_toolbar/css/toolbar.css 200 [0.05, 127.0.0.1:33234]
HTTP GET /static/debug_toolbar/js/toolbar.js 200 [0.02, 127.0.0.1:33232]
HTTP GET /static/debug_toolbar/js/toolbar.timer.js 200 [0.04, 127.0.0.1:33234]
HTTP GET /js/chunk-vendors.9c409057.js 200 [0.80, 127.0.0.1:33228]
HTTP GET /css/chunk-vendors.2c7f3eba.css 200 [0.94, 127.0.0.1:33224]
HTTP GET /js/app.c930fce5.js 200 [0.98, 127.0.0.1:33230]
HTTP GET /css/app.e15f06d0.css 200 [0.99, 127.0.0.1:33226]
HTTP GET /favicon.ico 200 [0.09, 127.0.0.1:33226]

In the browser console I see the sources seem to be loaded, but some seem to be empty (0 bytes) and the screen doesn't show anything. Below is a screenshot of the results and a screenshot of the Static Files tab in the Django Debug Bar.

Why isn't it serving those files correctly in Django?

enter image description here

enter image description here

Edit

I just found that if I change

STATIC_URL = '/static/'

to

STATIC_URL = '/'

it works correctly when I go to http://127.0.0.1:8000/index.html, but now http://127.0.0.1:8000/ gives me this error:

enter image description here

[EDIT 2]

Ok, so following the advice of @geek_life I changed the values in my settings file to:

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_NAME = 'cockpit'
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, PROJECT_NAME, "static/")
STATICFILES_DIRS = [os.path.join(BASE_DIR, '../frontend/dist')]
print("## BASE_DIR:", BASE_DIR)
print("## STATIC_ROOT:", STATIC_ROOT)
print("## STATICFILES_DIRS:", STATICFILES_DIRS)

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['cockpit/static/'],  # <= THIS IS WHAT I CHANGED
        'APP_DIRS': True,
        'OPTIONS': {}  # And here some options
    },
]

Which prints out

## BASE_DIR: /home/kramer65/repos/cockpit/backend
## STATIC_ROOT: /home/kramer65/repos/cockpit/backend/cockpit/static/
## STATICFILES_DIRS: ['/home/kramer65/repos/cockpit/backend/../frontend/dist']

And in the file settings/urls.py I (still) got this:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('cockpit/', include("cockpit.urls")),
    re_path('', TemplateView.as_view(template_name='index.html')),
]

I then copied the static/ folder with the built vuejs-ap from cockpit/backend/ to cockpit/backend/cockpit/.

Unfortunately I still get the same result. The index.html loads, but the JSS and CSS files still don't. What can I try next?

Octavla answered 18/5, 2020 at 18:30 Comment(2)
I think the last part of your question is happening because of your url path. It is able to find index.html in the static files so it is working when you go to that directly... try this: re_path(r'^.*', TemplateView.as_view(template_name="index.html"))Marrymars
@Octavla did you ever solve this?Barnabas
P
4

Cause of errors

For the first case

STATIC_URL = '/static/'

Django tries to look for static files in the backend/static/ folder only in the case where anything with url with /static/ i.e. for e.g. /static/css/* or /static/js/* is mentioned in your index.html, but that is not the case here index.html has file references like /css/* and /js/*, hence they are not found.

The reason this case works in the blog example is due to the same reason i.e. their template files are kept under a '../frontend/build directory and static files are in '../frontend/build/static' hence the index.html will look for static/js/* instead of a /js/* hence /static/ url location is accessed in Django, which then looks for the files in backend/static correctly

This is why, in your code, setting it to second case i.e.

STATIC_URL = '/'

gets the urls for static files correctly to /css/* and /js/* and even your /index.html can be thought of as a static file i.e. all these urls are considered static and are searched in the backend/static/ folder and hence appearing correctly in the browser.

But now the URLs are messed up i.e. this:

re_path('', TemplateView.as_view(template_name='index.html')),

can be treated as Django looking for this location: /, but it is already reserved for searching static files and not including any file name after / means you are not looking for any file.

Possible Solution

Django allows custom url patterns i.e. you can create 2 new variables in settings.py i.e.

STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static/")

STATIC_JS_URL = "/js/"
STATIC_JS_ROOT = os.path.join(STATIC_ROOT, "js/")

STATIC_CSS_URL = "/css/"
STATIC_CSS_ROOT = os.path.join(STATIC_ROOT, "css/")

and then configure your urls.py

from django.contrib import admin
from django.urls import include, path, re_path
from django.views.generic import TemplateView
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('cockpit/', include("cockpit.urls")),
    re_path('', TemplateView.as_view(template_name='index.html')),
]

urlpatterns += static(settings.STATIC_JS_URL, document_root=settings.STATIC_JS_ROOT)
urlpatterns += static(settings.STATIC_CSS_URL, document_root=settings.STATIC_CSS_ROOT)
Pitiful answered 22/5, 2020 at 18:36 Comment(0)
J
1

Use of STATIC_URL

First of all, STATIC_URL will be the url where the static files will be served. So if you set STATIC_URL='/' and then navigate to http://127.0.0.1:8000/index.html it will essentially serve the index.html from within the static folder of your Django application. If you go to http://127.0.0.1:8000/ you're at your 'static folder root', and as you can see, a directory index is forbidden. So don't use STATIC_URL='/'

Location of the files

The tutorial you followed adds the build folder of the SPA to the templates in your Django settings.py file.

So you should try and add:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(os.path.dirname(BASE_DIR), 'frontend', 'dist')],  # <== ADDED THIS
        'APP_DIRS': True,
        'OPTIONS': {
            # removed to keep this example small
        },
]

or if you want to point to your static files as template directory:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'static')], 
        'APP_DIRS': True,
        'OPTIONS': {
            # removed to keep this example small
        },
]

However I don't think it is best practice since static files (images, JS, CSS) have their own qualification in Django.

Note on os.path

Also, for your STATICFILES_DIRS, that's not how os.path.join() works. The first argument should be a path you want to join with one or more path components, the components are then passed as the next arguments. Furthermore, BASE_DIRis not at the right directorylevel, as you can see when you printed out the BASE_DIR path. You need the parent of the BASE_DIRdirectory. To get the path of the parent directory, you can do os.path.dirname(BASE_DIR), because BASE_DIR is a path. So applying this we get:

STATICFILES_DIRS = [
    os.path.join(os.path.dirname(BASE_DIR), frontend, dist),
]

Final note

I do feel that it might be an anti pattern to have your SPA served by Django without it being completely self contained in the Django BASE_DIR (hence the name, base directory). But I don't know the source for it, or exactly how best practice this.

Jipijapa answered 22/5, 2020 at 16:58 Comment(0)
P
1

I had this problem a long time ago and I solved it with a simple solution. It's too simple. As I see, your static folder is not in your app root and it's in wrong place. Put it in your app root...because Django is looking for the static folder inside the main root of your app, where views.py is in there. Maybe you have 5 apps or more. Django doesn't care about the number of apps you have, Django only looks for your static folder in your app root. But your templates folder can be in your project root. So , put your static folder in your app root. In this case I mean inside cockpit root.

And then you have to add this changes in settings.py

PROJECT_NAME = '---Your projects name---'
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, PROJECT_NAME, 'static/')

EXTERNAL_APPS = [

     'django.contrib.staticfiles',
  ]

Good Luck.

Parietal answered 27/5, 2020 at 12:45 Comment(4)
I Think Its Solve the ProblemMutton
You are thinking in a djangoic way, however your suggestion adds another step to frontend building process. Since vuejs app is completely separated, the builded app resides in its directory. And what @Octavla does with STATIC_DIRS is very reasonable for his case. If he does what you suggest he will be building vue app, copping it under django app’s static folder and run collect static. And yet his problem still unsolved 🙃Paul
I added some points in my answer and these tips worked for me, however, he must first try the method I suggested to see what happens finally @smail-taha-aykaParietal
Thanks for your ideas. I tried it but still no luck. I added a description of what I changed to the question. Maybe I'm missing something stupid?Octavla
P
0

First things first! If you want to serve your UI from Django as static files for the production, it's a bad idea! You need to consider using nginx or similar web server to serve static files for you.

The TemplateView in your urls.py is a bit problematic. Since your static files are served under STATIC_URL = '/static/' in your first scenario, the urls for js and css files does not match. You might edit the files and put static template tags inside index.hml but it's not a good idea as well. When you do changes on vuejs side, you need to build and replace the package directly and your site is updated. So the this line needs to be deleted forever, index.html has to be treated as static file:

    re_path('', TemplateView.as_view(template_name='index.html')),

When you changed STATIC_URL = '/' you may access all of your static files. If you run django in debug = True it serves static files automatically. Otherwise you need to add static url conf to the file. (DO NOT FORGET, This is not good for production!)

from django.contrib import admin
from django.urls import include, path, re_path
from django.views.generic import TemplateView
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('cockpit/', include("cockpit.urls")),
]

urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

With this setup you need to be careful about your urls and static file namings. They might override each other.

P.S: Thank you for such a detailed question.

Paul answered 26/5, 2020 at 8:41 Comment(3)
Thanks for your comment. Just a question, why is serving my static files through Django a bad idea? I mean, I know nginx has caching and all that stuff. But this is a very low traffic site (maybe 100 views per day) so performance is not an issue. As far as I understand uwsgi is a perfectly fine server if it's a slow traffic personal website. Or am I missing something?Octavla
It's all about performance. If you don't have high performance expectations, than it's OK.Paul
Because every page yields several static files, that means several workers are going to be busy for one page view in addition to the page itself. Just one page view with 10 static files can tie you up completely. Even for one user that means delays. I'd say 2 people at the same time would already slow down a default gunicorn setup.Vullo
A
0

There is a dumb way to fix this issue.

This issue is about that html built by VueJS will get js and css from path like these /js/app.12312sd.js and /css/app.dsfeewj.css.

html built by vueJS

Default setting of static file root path is /static/, so you only need to change each /js/... and /css/... in index.html manually or use sed or something else to replace them to /static/js/.../ and /static/css/.../.

You cannot replace STATIC_URL to / because it the root url for your website.

After you edit it, it will look like this, and then server your django server it will work well.

enter image description here

Asteroid answered 26/5, 2020 at 16:47 Comment(0)
C
0

Before building your frontend put the css/ and js/ in a directory named 'static' leaving the index.html as it is. Then when you run yarn build your directory structure would be as follows -

cockpit
├── backend/
│   ├── cockpit/
│   │   ├── views.py
│   │   ├── css/
│   │   └── etc..
│   ├── settings/
│   │   └── settings.py
│   └── manage.py
└── frontend/
    └── dist/
        ├── static/
        │       ├── css/
        │       └── js/
        └── index.html

Setup your BASE_DIR and a FRONTEND_DIR to point towards 'cockpit/frontend/dist' for quick use later on.

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FRONTEND_DIR = os.path.join(os.path.dirname(BASE_DIR),'frontend','dist')

Now either copy frontend static files to your backend directory as you did earlier -

STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(FRONTEND_DIR,'static/'),
]
STATIC_ROOT = os.path.join(BASE_DIR, "static/")

OR you could directly point your backend to serve from the built frontend directory instead of copying it again. (I somehow always do this)

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(FRONTEND_DIR,'static/')

Include your built frontend directory in your template directories for django to be able to serve index.html

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [FRONTEND_DIR],
        'APP_DIRS': True,
        'OPTIONS': {
            # removed to keep this example small
        },
    },
]

Now run your server

$ python manage.py collectstatic
$ python manage.py runserver

If later you decide on deploying this online you could do that easily using this configuration itself by setting Nginx to serve static files from your static directory. Hope this helps :)

Clearing your confusion in your [EDIT]

STATIC_ROOT points to the directory on your system. STATIC_URL refers to the URL at which your static files that are present in your STATIC_ROOT would be accessed while using your website.

Let's say you have a file script.js in your directory as shown

cockpit
├── backend/
│   ├── cockpit/
│   │   ├── views.py
│   │   ├── css/
│   │   └── etc..
│   ├── settings/
│   │   └── settings.py
│   └── manage.py
└── frontend/
    └── dist/
        ├── static/
        │       ├── css/
        │       └── js/
        │          └── script.js
        └── index.html

and you have set your STATIC_ROOT to point towards 'cockpit/frontend/dist/static/'.

Now if you set your STATIC_URL as 'static/' you would be able to access that script at http://127.0.0.1:8000/static/js/script.js

Alternatively, if you set your STATIC_URL to 'xcsdf/' you would access your js file at http://127.0.0.1:8000/xcsdf/js/script.js

Cautious answered 27/5, 2020 at 6:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.