Minimal configuration static site generator for Django
django-distillis a minimal configuration static site generator and publisher for Django. Most Django versions are supported, however up to date versions are advised including the Django 3.x releases.
django-distillas of the 1.7 release only supports Python 3. Python 2 support has been dropped. If you require Python 2 support please pin
django-distillto version 1.6 in your requirements.txt or Pipfile. Python 3.6 or above is advised.
django-distillextends existing Django sites with the ability to export fully functional static sites. It is suitable for sites such as blogs that have a mostly static front end but you still want to use a CMS to manage the content.
django-distilliterates over URLs in your Django project using easy to write iterable functions to yield the parameters for whatever pages you want to save as static HTML. These static files can be automatically uploaded to a bucket-style remote container such as Amazon S3 or Googe Cloud Files, or, written to a local directory as a fully working local static version of your project. The site generation, or distillation process, can be easily integrated into CI/CD workflows to auto-deploy static sites on commit.
django-distillcan be defined as an extension to Django to make Django projects compatible with "Jamstack"-style site architecture.
django-distillplugs directly into the existing Django framework without the need to write custom renderers or other more verbose code. You can also integrate
django-distillwith existing dynamic sites and just generate static pages for a small subsection of pages rather than the entire site.
For static files on CDNs you can use the following 'cache buster' library to allow for fast static media updates when pushing changes:
https://github.com/meeb/django-cachekiller
There is a complete example site that creates a static blog and uses
django-distillwith
django-cachekillervia continuous deployment on Netlify available here:
https://github.com/meeb/django-distill-example
Install from pip:
$ pip install django-distill
Add
django_distillto your
INSTALLED_APPSin your
settings.py:
INSTALLED_APPS = [ # ... other apps here ... 'django_distill', ]
That's it.
django-distillgenerates static pages and therefore only views which allow
GETrequests that return an
HTTP 200status code are supported.
It is assumed you are using URI parameters such as
/blog/123-abcand not querystring parameters such as
/blog?post_id=123&title=abc. Querystring parameters do not make sense for static page generation for obvious reasons.
Additionally With one-off static pages dynamic internationalisation won't work so all files are generated using the
LANGUAGE_CODEvalue in your
settings.py.
Static media files such as images and style sheets are copied from your static media directory defined in
STATIC_ROOT. This means that you will want to run
./manage.py collectstaticbefore you run
./manage.py distill-localif you have made changes to static media.
django-distilldoesn't chain this request by design, however you can enable it with the
--collectstaticargument.
Assuming you have an existing Django project, edit a
urls.pyto include the
distill_pathfunction which replaces Django's standard
pathfunction and supports the new keyword arguments
distill_funcand
distill_file. The
distill_funcargument should be provided with a function or callable class that returns an iterable or None. The
distill_fileargument is entirely optional and allows you to override the URL that would otherwise be generated from the reverse of the URL regex. This allows you to rename URLs like
/exampleto any other name like
example.html. As of v0.8 any URIs ending in a slash
/are automatically modified to end in
/index.html. An example distill setup for a theoretical blogging app would be:
# Replaces the standard django.conf.path, identical syntax from django_distill import distill_pathViews and models from a theoretical blogging app
from blog.views import PostIndex, PostView, PostYear from blog.models import Post
def get_index(): # The index URI path, '', contains no parameters, named or otherwise. # You can simply just return nothing here. return None
def get_all_blogposts(): # This function needs to return an iterable of dictionaries. Dictionaries # are required as the URL this distill function is for has named parameters. # You can just export a small subset of values here if you wish to # limit what pages will be generated. for post in Post.objects.all(): yield {'blog_id': post_id, 'blog_title': post.title}
def get_years(): # You can also just return an iterable containing static strings if the # URL only has one argument and you are using positional URL parameters: return (2014, 2015) # This is really just shorthand for ((2014,), (2015,))
urlpatterns = ( # e.g. / the blog index distill_path('', PostIndex.as_view(), name='blog-index', distill_func=get_index, # / is not a valid file name! override it to index.html distill_file='index.html'), # e.g. /post/123-some-post-title using named parameters distill_path('post/-', PostView.as_view(), name='blog-post', distill_func=get_all_blogposts), # e.g. /posts-by-year/2015 using positional parameters distill_path('posts-by-year/', PostYear.as_view(), name='blog-year', distill_func=get_years), )
Your site will still function identically with the above changes. Internally the
distill_funcand
distill_fileparameters are removed and the URL is passed back to Django for normal processing. This has no runtime performance impact as this happens only once upon starting the application.
You can use the
distill_re_pathfunction as well, which replaces the default
django.urls.re_pathfunction. Its usage is identical to the above:
from django_distill import distill_re_pathurlpatterns = ( distill_re_path(r'some/regex' SomeOtherView.as_view(), name='url-other-view', distill_func=some_other_func), )
If you are using an older version of Django in the 1.x series you can use the
distill_urlfunction instead which replaces the
django.conf.urls.urlor
django.urls.urlfunctions. Its usage is identical to the above:
from django_distill import distill_urlurlpatterns = ( distill_url(r'some/regex' SomeView.as_view(), name='url-view', distill_func=some_func), )
Note
django-distillwill mirror whatever your installed version of Django supports, therefore at some point the
distill_urlfunction will cease working in the future when Django 2.x itself depreciates the
django.conf.urls.urland
django.urls.urlfunctions. You can use
distill_re_pathas a drop-in replacement. It is advisable to use
distill_pathor
distill_re_pathif you're building a new site now.
distill-localcommand
Once you have wrapped the URLs you want to generate statically you can now generate a complete functioning static site with:
$ ./manage.py distill-local [optional /path/to/export/directory]
Under the hood this simply iterates all URLs registered with
distill_urland generates the pages for them using parts of the Django testing framework to spoof requests. Once the site pages have been rendered then files from the
STATIC_ROOTare copied over. Existing files with the same name are replaced in the target directory and orphan files are deleted.
distill-localsupports the following optional arguments:
--collectstatic: Automatically run
collectstaticon your site before rendering, this is just a shortcut to save you typing an extra command.
--quiet: Disable all output other than asking confirmation questions.
--force: Assume 'yes' to all confirmation questions.
--exclude-staticfiles: Do not copy any static files at all, only render output from Django views.
Note If any of your views contain a Python error then rendering will fail then the stack trace will be printed to the terminal and the rendering command will exit with a status code of 1.
distill-publishcommand
$ ./manage.py distill-publish [optional destination here]
If you have configured at least once publishing destination (see below) you can use the
distill-publishcommand to publish the site to a remote location.
This will perform a full synchronisation, removing any remote files that are no longer present in the generated static site and uploading any new or changed files. The site will be built into a temporary directory locally first when publishing which is deleted once the site has been published. Each file will be checked that it has been published correctly by requesting it via the
PUBLIC_URL.
distill-publishsupports the following optional arguments:
--collectstatic: Automatically run
collectstaticon your site before rendering, this is just a shortcut to save you typing an extra command.
--quiet: Disable all output other than asking confirmation questions.
--force: Assume 'yes' to all confirmation questions.
--exclude-staticfiles: Do not copy any static files at all, only render output from Django views.
Note that this means if you use
--forceand
--quietthat the output directory will have all files not part of the site export deleted without any confirmation.
Note If any of your views contain a Python error then rendering will fail then the stack trace will be printed to the terminal and the rendering command will exit with a status code of 1.
distill-test-publishcommand
$ ./manage.py distill-test-publish [optional destination here]
This will connect to your publishing target, authenticate to it, upload a randomly named file, verify it exists on the
PUBLIC_URLand then delete it again. Use this to check your publishing settings are correct.
distill-test-publishhas no arguments.
You can set the following optional
settings.pyvariables:
DISTILL_DIR: string, default directory to export to:
DISTILL_DIR = '/path/to/export/directory'
DISTILL_PUBLISH: dictionary, like Django's
settings.DATABASES, supports
default:
DISTILL_PUBLISH = { 'default': { ... options ... }, 'some-other-target': { ... options ... }, }
You can automatically publish sites to various supported remote targets through backends just like how you can use MySQL, SQLite, PostgreSQL etc. with Django by changing the backend database engine. Currently the engines supported by
django-distillare:
djangodistill.backends.amazons3: Publish to an Amazon S3 bucket. Requires the Python library
boto3(
$ pip install boto3). The bucket must already exist (use the AWS control panel). Options:
'some-s3-container': { 'ENGINE': 'django_distill.backends.amazon_s3', 'PUBLIC_URL': 'http://.../', 'ACCESS_KEY_ID': '...', 'SECRET_ACCESS_KEY': '...', 'BUCKET': '...', },
djangodistill.backends.googlestorage: Publish to a Google Cloud Storage bucket. Requires the Python libraries
google-api-python-clientand
google-cloud-storage(
$ pip install google-api-python-client google-cloud-storage). The bucket must already exist and be set up to host a public static website (use the Google Cloud control panel). Options:
'some-google-storage-bucket': { 'ENGINE': 'django_distill.backends.google_storage', 'PUBLIC_URL': 'https://storage.googleapis.com/[bucket.name.here]/', 'JSON_CREDENTIALS': '/path/to/some/credentials.json', 'BUCKET': '[bucket.name.here]', },
There is a minimal test suite, you can run it by cloing this repository, installing the required dependancies in
requirements.txtthen execuiting:
# ./run-tests.py
All properly formatted and sensible pull requests, issues and comments are welcome.