Git Product home page Git Product logo

Comments (11)

merwok avatar merwok commented on August 15, 2024

Honestly, the best practice there would be to not have django serve static files.
A full-featured HTTP server like nginx can do it much better, or even a specialized static hosting with separate domains or sub-domains for app and backend.

from whitenoise.

zachsiegel-capsida avatar zachsiegel-capsida commented on August 15, 2024

Another option is to expose a serve function to be used in views that is suitable for production, unlike django.contrib.staticfiles.views.serve. Such as django.contrib.whitenoise.views.serve. Then users can indeed have a catchall route and serve from their views.

My impression, though, is that you wanted Whitenoise to be used as middleware only and not appear anywhere in an app except for settings.py.

from whitenoise.

zachsiegel-capsida avatar zachsiegel-capsida commented on August 15, 2024

@merwok thank you for the response! Did I misinterpret the Whitenoise documentation? It seemed to me to imply "sometimes Whitenoise is actually better than setting up Nginx yourself, unless you are an expert". Based on the SO posts I linked to, there are many people who

  • are not experts (i.e. they don't already have this set up in Nginx)
  • probably would prefer an easier-to-maintain stack (Nginx layer is more than they care to set up)
  • want to use Whitenoise in a plug-and-play manner
  • want to enable frontend routing

(Note that all of those describe me.)

from whitenoise.

zachsiegel-capsida avatar zachsiegel-capsida commented on August 15, 2024

Can this be achieved by directly using Whitenoise with a Django wsgi application instead of as middleware, as in this SO answer?

from my_django_project import MyWSGIApp

application = MyWSGIApp()
application = WhiteNoise(application, root='/path/to/static/files')
application.add_files('/path/to/more/static/files', prefix='more-files/',root='something')

The WhiteNoise class has both a prefix and a root parameter that might enable this already. Either way, I think it should be possible to do a PR that exclusively edits that class to enable frontend routing via the non-middleware interface in this comment.

from whitenoise.

merwok avatar merwok commented on August 15, 2024

Did I misinterpret the Whitenoise documentation? It seemed to me to imply "sometimes Whitenoise is actually better than setting up Nginx yourself, unless you are an expert".

That is what the doc says! And I (just a user of whitenoise note, not a maintainer) do agree with it for django static files.
But I wouldn’t try to serve a single-page app with a catch-all route using whitenoise.

from whitenoise.

zachsiegel-capsida avatar zachsiegel-capsida commented on August 15, 2024

I think I came up with a decent solution, leveraging the way WhiteNoise middleware manages its own configuration.

views.py:

from django.conf import settings
from whitenoise.middleware import WhiteNoiseMiddleware
wn = WhiteNoiseMiddleware()

def frontend(request, route):
	output = wn.process_request(request)
	if output is not None:
		return output
	if wn.autorefresh:
		static_file = wn.find_file(settings.STATIC_URL)
	else:
		static_file = wn.files.get(settings.STATIC_URL)
	return wn.serve(static_file, request)

urls.py:

from . import views
from django.conf import settings
urlpatterns = [
    ...,
    path(f"{settings.STATIC_URL_BASE}<path:route>", views.frontend, name="frontend")
]
  • Note that this route captures a "path" keyword parameter called route that the frontend function ignores because the WhiteNoiseMiddleware object is already getting it from request.path_info in its WhiteNoiseMiddleware.process_request.

settings.py

WHITENOISE_INDEX_FILE = True
  • The last part is necessary just so that when you visit settings.STATIC_URL, the index.html file is automatically served if there is one. That's already part of Whitenoise.

Please comment on whether this is reasonable for production! Thank you.

from whitenoise.

zachsiegel-capsida avatar zachsiegel-capsida commented on August 15, 2024

When I say "reasonable in production", please know that I understand you ideally don't want a Python if statement in between users and their static content, but since Whitenoise already does this, I am really just asking if this introduces additional issues beyond what is unavoidable with Whitenoise. As I mentioned above, "just use Nginx instead of Whitenoise" ignores the versioning that Whitenoise automatically manages. I basically want Whitenoise to be exactly what it already is but to allow frontend routing in SPAs. I think this achieves that.

Really hoping a Whitenoise developer can comment. Thank you.

from whitenoise.

zachsiegel-capsida avatar zachsiegel-capsida commented on August 15, 2024

If anyone comes across this, you can create a custom middleware that directs to your SPA's routes in your app that extends WhiteNoise as so:

yourapp/middleware/WhiteNoiseSPAMiddleware

from whitenoise.middleware import WhiteNoiseMiddleware
from django.http import HttpResponseNotFound
from django.conf import settings


class WhiteNoiseSPAMiddleware(WhiteNoiseMiddleware):
	def __call__(self, request):
		response = self.process_request(request)
		if response is None:
			response = self.get_response(request)
		if settings.WHITENOISE_CUSTOM_FRONTEND_ROUTING and (
			isinstance(response, HttpResponseNotFound) or response.status_code == 404
			) and request.path_info.startswith(settings.STATIC_URL):

			# serve index, route from frontend
			if self.autorefresh:
				static_file = self.find_file(settings.STATIC_URL)
			else:
				static_file = self.files.get(settings.STATIC_URL)
			return self.serve(static_file, request)
		return response

Then, in your settings.py file, replace the Whitenoise middleware with this, and add the WHITENOISE_CUSTOM_FRONTEND_ROUTING setting I created. This could easily be configured differently, but this configuration makes sense for the routes in my SPA.

MIDDLEWARE = [
    ...,
    # "whitenoise.middleware.WhiteNoiseMiddleware", # remove - replace with extension
    'yourapp.middleware.WhiteNoiseSPAMiddleware',
    ...,
    ]
...
WHITENOISE_CUSTOM_FRONTEND_ROUTING = True

With this middleware, you don't have to add ANYTHING to your urls.py to make sure frontend routing is handled by your frontend. This basically does the following:

  1. Checks if the route is handled by Django. If so, return the response.
  2. Checks if the route corresponds to a static file (using exactly the method WhiteNoise uses). If so, return the static file.
  3. If neither is true, but the route starts with your "frontend" prefix (like /app or /static), then just serve the frontend (e.g. /static/index.html), which presumably handles the routing.

The obvious drawback to this: if your frontend refers to a ./something.js file, it will receive your index.html file, which will definitely not work. This is bad, but it's kind of bad anyway if your frontend requests a file that doesn't exist.

I suppose you could prevent Django from serving your index.html file if the requested route ends in something like /*.html or '/*.js'. That would probably make sense. Maybe don't allow this to happen if the last sub-route (*/.../sub_route) contains a period at all, because that would probably indicate it is requesting a file. Seems to be getting jankier the more I futz with it but it does the job for me for now.

from whitenoise.

zachsiegel-capsida avatar zachsiegel-capsida commented on August 15, 2024

If you want to use the idea in my last comment, use this:

from whitenoise.middleware import WhiteNoiseMiddleware
from django.http import HttpResponseNotFound
from django.conf import settings


class WhiteNoiseSPAMiddleware(WhiteNoiseMiddleware):
	def __call__(self, request):
		response = self.process_request(request)
		if response is None:
			response = self.get_response(request)
		if settings.WHITENOISE_CUSTOM_FRONTEND_ROUTING and (
			isinstance(response, HttpResponseNotFound) or response.status_code == 404
			) and request.path_info.startswith(
			settings.STATIC_URL
			) and ('.' not in request.path_info.rsplit("/", 1)[-1]):

			# serve index, route from frontend
			if self.autorefresh:
				static_file = self.find_file(settings.STATIC_URL)
			else:
				static_file = self.files.get(settings.STATIC_URL)
			return self.serve(static_file, request)
		return response

This excludes routes like .../static/something.js so that if your frontend requests a dependency that does not exist (like <script src="./something.js"/>, with a "." in the filename), instead of accidentally serving index.html, it just does whatever a normal Django+Whitenoise app does (serve 404 and let the Javascript app respond appropriately)

from whitenoise.

zachsiegel-capsida avatar zachsiegel-capsida commented on August 15, 2024

Honestly I think this functionality makes a great case for Whitenoise as a project. I would much rather implement this in Python than figure out the Nginx config mini-language. I know Nginx is used all over the world and it's probably very easy to figure this out from documentation, but I don't see why we should have to.

from whitenoise.

adamchainz avatar adamchainz commented on August 15, 2024

If you want to serve the same HTML file at multiple URL's, or even a catch-all URL, use plain Django to do that. I think it's out of scope for Whitenoise, whose focus is on serving static files which have single URL's.

For example, you can write catch-all-paths routing like so:

from django.urls import path

from example.core.views import frontend_index

urlpatterns = [
    path("", frontend_index),
    path("<path:path>", frontend_index),
]

And then write a view that serves a pre-built index.html like so:

from django.conf import settings
from django.http import FileResponse
from django.views.decorators.http import require_GET


@require_GET
def frontend_index(request, path=""):
    index_file = (settings.BASE_DIR / "frontend" / "index.html").open("rb")
    return FileResponse(index_file)

There's no need to invoke Whitenoise to serve a file within Django, nor for whitenoise to provide a "serve" function. FileResponse is sufficient, although do consider caching headers.

It's probably better to use a Django template for index.html though, as within a template you can use {% static %} to refer to hashed, cacheable static files from Whitenoise. But if that doesn't suit, for whatever reason, you can serve a folder of arbitrary files (built JS etc.) with the WHITENOISE_ROOT setting - set an appropriate value for WHITENOISE_MAX_AGE.

For more examples on serving a few arbitrary files with vanilla Django see my favicons post.

from whitenoise.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.