Published at

Integrating a Vite Frontend with Django

Integrating a Vite Frontend with Django

A guide to integrating a Vite frontend with a Django backend, focusing on static assets and automating HTML generation.

Authors
  • avatar
    Name
    James Lau
    Twitter
  • Indie App Developer at Self-employed
Sharing is caring!
Table of Contents

: A Step-by-Step Guide

This post details how to integrate a Vite-built frontend with a Django backend, focusing on serving static assets correctly and automating the generation of the main HTML file.

Project Structure and Assumptions

  • Your Vite build output directory is dist, located inside Django’s static folder (static/dist). This means the static URL root for your build is /static/dist.
  • Your Django project’s root URL for the vendor CMS is /vendor.

1. Base Template Setup (templates/vendor/base.html)

Create a base HTML template within your Django app (e.g., djangoapp/templates/vendor/base.html). This template will serve as the foundation for your frontend. It includes a div with id="root" where your Vite app will be mounted, and crucially, the {% render_vite_bundle %} template tag.

{% load render_vite_bundle %}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Myapp CMS</title>

</head>

<body>

  <div id="root"></div>

  {% render_vite_bundle %}

</body>

</html>

Important: The render_vite_bundle tag is key to injecting the necessary <script> and <link> tags for your Vite assets in production. You’ll need to configure settings.py with the appropriate Vite app directory. (See below the render_vite_bundle.py code for explanation)

2. Handling Static Files and manifest.json

Vite generates a manifest.json file in the dist/.vite directory. This file maps your source files to their corresponding bundled filenames. A crucial step is ensuring that static file paths within your JavaScript code are correctly resolved in the production environment.

Problem: Original static file URLs might look like URL/assets/transactions/pending.svg.

Solution: These URLs need to be transformed to URL/static/dist/assets/transactions/pending.svg. This is primarily handled within the render_vite_bundle.py template tag (see below).

3. render_vite_bundle.py Template Tag

Create a custom template tag to render the Vite bundle. Place this file in your Django app’s templatetags directory (e.g., home/templatetags/render_vite_bundle.py).

# This template tag is needed for production
# Add it to one of your django apps (/appdir/templatetags/render_vite_bundle.py, for example)

import os
import json

from django import template
from django.conf import settings
from django.utils.safestring import mark_safe

register = template.Library()

@register.simple_tag
def render_vite_bundle():
    """
    Template tag to render a vite bundle.
    Supposed to only be used in production.
    For development, see other files.
    """

    try:
        fd = open(f"{settings.VITE_APP_DIR}/dist/.vite/manifest.json", "r")
        manifest = json.load(fd)

    except:
        raise Exception(
            f"Vite manifest file not found or invalid. Maybe your {settings.VITE_APP_DIR}/dist/.vite/manifest.json file is empty?"
        )

    imports_files = [
        '<script type="module" src="' + "{% static 'dist/" + manifest[file]["file"] + "' |safe %}" + '"></script>'
        for file in manifest["index.html"]["dynamicImports"]
    ]
    exclude = r'\\'
    import_strings = ""
    for f in imports_files:
        f = f.replace(exclude, '')
        import_strings += f + '\n'

    main_js_file = manifest['index.html']['file']
    main_css_file = manifest['index.html']['css'][0]

    if 'static/dist/assets' not in main_js_file:
        main_js_file = main_js_file.replace('assets', '/dist/assets')

    if 'static/dist/assets' not in main_css_file:
        main_css_file = main_css_file.replace('assets', '/dist/assets')

    css_href = "{% static '" + main_css_file + "' %}"
    js_href = "{% static '" + main_js_file + "' %}"

    with open("static" + main_js_file, 'r+') as f:
        content = f.read()

        if 'static/dist/assets' not in content:
            content = content.replace('assets', 'static/dist/assets')
            f.seek(0)  # Move the file pointer to the beginning
            f.write(content)
            f.truncate()

    return mark_safe(
        "{% load static %}" + f"""<script type="module" src="{js_href}"></script>
        <link rel="stylesheet" type="text/css" href="{css_href}" />
        {import_strings}"""
    )

Explanation:

  • The tag reads the manifest.json file to determine the correct filenames for your JavaScript and CSS bundles.
  • settings.VITE_APP_DIR should point to the root directory of your vite app, where the dist folder resides. For example, if your vite app is in a folder named frontend, then you would set VITE_APP_DIR = os.path.join(BASE_DIR, 'frontend') in your settings.py.
  • It constructs the appropriate <script> and <link> tags using Django’s {% static %} template tag to correctly reference the static files.
  • It modifies the content of the main js file by replacing assets with static/dist/assets to ensure correct static file resolution.
  • The mark_safe function is crucial; it tells Django that the returned HTML is safe to render without further escaping.

4. Automating index.html Generation

To streamline the process, create a custom Django management command to generate the index.html file from your base template. This ensures that the latest Vite assets are always included.

Create a file named generate_vendor_cms.py inside your Django app’s management/commands directory (e.g., home/management/commands/generate_vendor_cms.py).

from django.core.management.base import BaseCommand
from django.template.loader import get_template
from django.template import TemplateDoesNotExist

class Command(BaseCommand):
    help = "Generate Vendor CMS"

    def handle(self, *args, **kwargs):
        try:
            base_template = get_template('vendor/base.html')
            index_template = base_template.render()
            with open('templates/vendor/index.html', 'w') as file:
                file.write(index_template)
            self.stdout.write(self.style.SUCCESS('index.html generated successfully'))
        except TemplateDoesNotExist:
            self.stdout.write(self.style.ERROR('base.html template does not exist'))

To run this command every time the server starts, add the following lines to your urls.py file:

from django.core.management import execute_from_command_line

execute_from_command_line(["manage.py", "generate_vendor_cms"])

Alternatively, you can manually run the command using:

python manage.py generate_vendor_cms

5. URL Configuration

Finally, set up the URL endpoint for your vendor CMS.

from django.shortcuts import render

# Example view (myapp/views.py)
from ratelimit.decorators import ratelimit
from ratelimit import keyfunc

def get_client_ip(request):
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip

@ratelimit(key=get_client_ip, rate="60/h", block=True)

def vendor(request):
    return render(
        request,
        "vendor/index.html"
    )
# Example URL configuration (myapp/urls.py or project urls.py)
from myapp import views
from django.urls import include, re_path

re_path(r"^vendor.*?$", views.vendor, name="vendor_cms"),

This setup serves the generated index.html file when a user navigates to a URL matching the pattern /vendor. The @ratelimit decorator is optional but recommended for security.

Summary

Integrating Vite with Django involves correctly serving static assets, using manifest.json for production builds, and automating index.html generation. This approach ensures your frontend and backend work seamlessly together.

Sharing is caring!