Serving Next.js pre-rendered HTML pages as Django templates

Charlie Murphy
4 min readSep 17, 2020

--

I’ve been building a site that uses a React frontend and Django backend, and I’ve finally gotten around to thinking about how to improve the marketing pages (i.e. landing page, etc.). However, some of the well-known problems with using React for a landing page are (1) worse SEO and (2) likely slower page loading times (which also impacts google ranking).

So I wanted to instead pre-render the marketing pages. Two major solutions that are compatible with my React code are Gatsby and Next.js. And after some googling around, it seemed Next.js could do everything Gatsby could do and would be easier to setup. So I chose Next.js.

But the real challenge I had was figuring out the relationship between Next.js and my Django backend. In short, I decided to use Next.js to pre-render my marketing pages and serve them as Django templates. Below I’ll show you how I did it.

Caveat: I am definitely not a seasoned software engineer (I am a computational biologist by training), but what I came up with works just fine for me. However, some alternative solutions some people might suggest are: (1) just use wordpress for marketing pages, or (2) decouple the marketing pages from django and instead serve it with Next.js on a subdomain or something. Nonetheless, if you still have any thoughts/criticism, I am more than happy to hear them.

Next.js compatibility

The first big hassle was to bring all the React code for my marketing pages into its own Django app and make it compatible with Next.js. While Next.js has some pretty good documentation on how to do this, it was still quite annoying (especially since I did not spend much time learning Next.js in-depth :P).

Once I was able to successfully build the pages (via npx next build), Next.js has a command to export the pages to static HTML (via npx next export -o output_folder).

Integrating with Django

Next.js pre-rendered HTML files are small and bare-bones, but they still need some accompanying js and css bundles to render correctly (listed within the <head> section). Here is one example:

<link as="script" href="/_next/static/chunks/main-5f4f60561676956d0ca5.js"rel="preload"/>

However, when I serve the HTML page as a Django template, it will not be able to find the js/css bundles. Since Django normally looks for static files in the static directory, I needed to add the static template tags so Django knows where to find the js/css files. Like so:

<link as="script" href='{% static "/_next/static/chunks/main-5f4f60561676956d0ca5.js" %}' rel="preload"/>

But since I don’t want to manually make those edits, I wrote a python script to do it for me using BeautifulSoup (which is an HTML parsing library):

import argparse
import glob
import os
from bs4 import BeautifulSoupdef parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
'-d',
'--dir',
type=str,
required=True)
args = parser.parse_args() return args
def search_and_parse(soup, tag): for x in soup.find_all(tag): if x.get('src') and x['src'].startswith('/_next/'):
x['src'] = "{% static \"" + x['src'] + "\" %}"
if x.get('href') and x['href'].startswith('/_next/'):
x['href'] = "{% static \"" + x['href'] + "\" %}"

def main():
args = parse_args() for html_file in glob.glob(os.path.join(args.dir, '*html')): soup = BeautifulSoup(open(html_file), 'html.parser')
soup.insert(0, '{% load static %}')
# add script to store user info into window on page load
# is a workaround so i don't need redux
django_script = BeautifulSoup("<script>window.django = {is_authenticated: \"{{ request.user.is_authenticated }}\" === \"True\" ? true : false, user_plan: \"{{user_plan}}\"};</script>", 'html.parser')
head = soup.find('head')
head.append(django_script)
search_and_parse(soup, 'link')
search_and_parse(soup, 'script')
fout = open(html_file, 'wb')
fout.write(soup.prettify('utf-8'))
fout.close()
main()

You’ll notice a couple extra things:

  1. I added {% load static %} at the top of the HTML file, which Django needs in order for the static files to work correctly.
  2. I use the template context variables to store some information into the browser’s window object. I need this so I know if the user is logged in and what their payment plan is while they view the marketing pages.

Another important step is actually placing all the js/css bundles within the static folder. Oddly, running npx next export -o static does not work and causes some strange bug. So I instead did this npx next export -o output && mv output static.

The final step is to actually place the HTML pages within Django’s required template directory. However, instead of directly moving the HTML files from the static folder to the template folder, I created soft links to them. For example, while I am within the template directory, I ran the following ln -s ../static/index.html index.html. The advantage is that I don’t need to commit the pre-rendered HTML files to my git repo, I only need to commit the soft links. In fact, none of the output files from running npx next export are in my git repo, only the React files that produce them are in my git repo.

Edit: For the above paragraph, I later found that while using links works fine for the local dev environment, I had to actually copy over the HTML files into the template directory to get it to work on AWS-Elastic Beanstalk.

Final result

I then combined all the above commands and scripts into one simple build command. Here is what my packages.json scripts section looks like:

"scripts": {
"dev": "next dev",
"build": "next build",
"deploy": "rm -rf output static && next export -o output && mv output static && python ../bin/finalize_marketing_build.py -d static",
"start": "next start"
},

--

--