I’ve been hacking away at Microblot lately, and I’m using Django’s “sites” framework for the different blogs. However, there are some exceptions to this, and I put together a short dispatch view to handle it.
This post assumes you understand the following Djangoisms:
- Django maps model-view-controller to model-template-view. The remapping of “view” is relevant here.
- A site in Django is just the built-in
Site
model with adomain
andname
.CurrentSiteMiddleware
adds it to therequest
object for use in views. It’s used to serve multiple domains from the same Django instance, and Microblot uses it in query filtering. - A URL in Django is likely described as a route in other frameworks. It’s the path pattern following the domain.
- URLs in Django are computed at app start time, and cannot be dynamically changed at request time. They require a suitable callable defined ahead of time.
Background + Problem
Microblot is a blogging platform, and a registered blog gets its own subdomain (e.g.,
foo.microblot.io).
All requests made to all subdomains of microblot.io are sent to the same Django app,
which then uses the subdomain to find the relevant Site
and Blog
entries.
Easy enough, and exactly the kind of use the “sites” framework is intended for, from
what I can tell.
The first exception is the main site, which isn’t one of the blogs, but rather a few static pages—home page, privacy policy, terms of use, etc.—and the Slack integration API. Since the blogs are fundamentally something different, www.microblot.io/privacy and www.microblot.io/api/slack/new should work, but foo.microblot.io/privacy and foo.microblot.io/api/slack/new should return a 404, for example. As such, a different set of URLs and views is needed.
The second exception is the built-in URL shortener. For a full post URL foo.microblot.io/posts/asdf1234, the short URL blot.click/asdf1234 should redirect to it. This means the redirects need their own set of URLs and views as well.
Solution
Since the number of exceptions is actually fairly small and well known, I put together a function based view that acts as a dispatcher and determines which actual view to return based on the request domain.
Here it is in its entirety:
# microblot/core/views.py
from django.conf import settings
from django.http import Http404
def dispatch(request, main_class=None, cms_class=None, short_class=None, **kwargs):
classes = {
settings.FULL_DOMAIN: main_class,
f"www.{settings.FULL_DOMAIN}": main_class,
settings.SHORT_DOMAIN: short_class,
f"www.{settings.SHORT_DOMAIN}": short_class,
}
target_class = classes.get(request.site.domain, cms_class)
if target_class is None:
raise Http404()
return target_class.as_view()(request, **kwargs)
The logic is pretty simple:
- If the current domain is the full one (e.g., microblot.io or www.microblot.io), return the supplied main site class.
- If the current domain is the shortener one (e.g., blot.click or www.blot.click), return the supplied shortener class.
- If it’s some other domain, assume it’s a blog and return the supplied CMS class.
- If the determined view is
None
, the URL isn’t available on this domain, and thus return a 404.
Here it is in action:
# config/urls.py
from django.urls import include, path
from microblot.cms.views import BlogHomeView
from microblot.core.views import dispatch
from microblot.main.views import MainHomeView
from microblot.shortener.views import ShortenerHomeRedirectView
urlpatterns = [
path(
"",
dispatch,
{
"main_class": MainHomeView,
"cms_class": BlogHomeView,
"short_class": ShortenerHomeRedirectView,
},
name="dispatch-home",
),
path("", include("microblot.cms.urls")),
path("", include("microblot.shortener.urls")),
]
# microblot/cms/urls.py
from django.urls import path
from microblot.core.views import dispatch
from .views import BlogPostView
# NOTE: See config/urls.py for some shared routes.
urlpatterns = [
path(
"posts/<post_short_code>",
dispatch,
{
"cms_class": BlogPostView,
},
name="dispatch-post",
),
]
# microblot/shortener/urls.py
from django.urls import path
from microblot.core.views import dispatch
from .views import ShortenerPostRedirectView
# NOTE: See config/urls.py for some shared routes.
urlpatterns = [
path(
"<post_short_code>",
dispatch,
{
"short_class": ShortenerPostRedirectView,
},
name="dispatch-short",
),
]
The real value is shown in the first example, in config/urls.py
, where the same URL pattern is supported by multiple parts of the site.
In this case, the “home pages” of www.microblot.io, foo.microblot.io, and blot.click all do different things:
- www.microblot.io is the main “marketing” site home page, with general Microblot information.
- foo.microblot.io is the foo blog home page, with the most recent posts on the Foo blog.
- blot.click redirects to www.microblot.io.
Another example is the upcoming category support.
Microblot will support specifying a category for each post, and said categories will get their own pages.
Microblot may also have a global blog and feed, which would also support categories.
In light of the above, a URL of category/<category_slug>
may be supported by both the main site and the CMS, resulting in the following definition:
# added to config.urls.py
from django.urls import path
from microblot.main.views import MainCategoryView
from microblot.cms.views import BlogCategoryView
urlpatterns = [
path(
"category/<category_slug>",
dispatch,
{
"main_class": MainCategoryView,
"cms_class": BlogCategoryView,
},
name="dispatch-category",
),
]
There is one major downside to this approach: it doesn’t scale all that well if you have a lot of URLs that need to account for multiple classes, since those should probably live somewhere neutral, thus moving them away from their app.
Since I only have two exceptions, and at the moment only the top URL needs to account for more than one class, I think it’s fine. It’s certainly way cleaner than anything else I thought of, like decorating view classes all over the place to limit for which domains they run.
I probably wouldn’t say I love it, but it works! :D