An opinionated way to structure and ship modern frontend assets in Django using Vite. It provides:
- A
manage.py scaffoldcommand to bootstrap afrontend/workspace. - Template tags for injecting Vite assets (dev server in
DEBUG, manifest in production). - A lightweight “Page” abstraction that keeps templates, JS/CSS entrypoints, and views aligned.
Most Django + Vite integrations solve “include the scripts”. Django Frontend Kit also nudges you into a consistent project layout:
frontend/layouts/...for shared shells (base template + base JS/CSS).frontend/pages/...for page-level templates and entrypoints.- Optional “custom entries” for React/Vue/Alpine widgets without adopting a full SPA.
- Vite dev server in development (
DEBUG=True) with automatic@vite/clientinjection. - Production asset resolution via Vite
manifest.json+ Djangostatic()URLs. - Modulepreload + stylesheet tags generated from the manifest (better performance by default).
- Scaffolding for a working
frontend/structure and avite.config.js.
- Python
>= 3.9 - Django
>= 4.2(including 6.0) - Node.js + npm/pnpm/yarn (for Vite)
pip install django-frontend-kitAlternative installers:
uv add django-frontend-kitpoetry add django-frontend-kitsettings.py:
INSTALLED_APPS = [
# ...
"frontend_kit",
]From the same directory as manage.py:
python manage.py scaffoldThis creates:
frontend/(templates + entrypoints + Python modules)vite.config.js(configured for this kit)
npm init -y
npm install --save-dev vite @iamwaseem99/vite-plugin-django-frontend-kitAdd scripts to package.json:
{
"scripts": {
"dev": "vite",
"build": "vite build"
}
}At minimum:
DJFK_FRONTEND_DIR = BASE_DIR / "frontend"
VITE_OUTPUT_DIR = BASE_DIR / "dist"
VITE_DEV_SERVER_URL = "http://localhost:5173/"
DJFK_DEV_ENV = True
TEMPLATES = [
{
# ...
"DIRS": [DJFK_FRONTEND_DIR],
}
]
STATICFILES_DIRS = [VITE_OUTPUT_DIR]Notes:
DJFK_FRONTEND_DIRmust exist on disk (the scaffold command creates it).VITE_OUTPUT_DIRmust match your Vitebuild.outDir(seevite.config.js).DJFK_DEV_ENVcontrols dev behavior. WhenTrue, assets are served from the Vite dev server; whenFalse, assets are resolved from the manifest.
In two terminals:
npm run devpython manage.py runserver- When
DJFK_DEV_ENV=True, asset tags point to the Vite dev server and HMR works as usual. - When
DJFK_DEV_ENV=False, Django Frontend Kit readsVITE_OUTPUT_DIR/.vite/manifest.jsonand emits:<link rel="modulepreload" ...>for imported chunks<link rel="stylesheet" ...>for CSS<script type="module" ...></script>for the entry module
If DJFK_DEV_ENV=True and request.csp_nonce is available in the template context, Django Frontend Kit (Django 6+ compatible) adds a nonce attribute to:
<script type="module"><link rel="stylesheet">
No nonce is added when DJFK_DEV_ENV=False (production/manifest mode), and modulepreload tags never receive a nonce.
After scaffolding, you’ll have a structure like:
frontend/
layouts/
base/
__init__.py
index.html
entry.head.ts # optional, loaded in <head>
entry.ts # loaded at end of <body>
main.css
pages/
home/
__init__.py
index.html
entry.ts
Important: frontend/ is a Python package. Keep __init__.py files so Django can import your Page classes.
Layouts are just Page subclasses. By convention they define shared HTML + shared entrypoints.
frontend/layouts/base/__init__.py:
from frontend_kit.page import Page
class BaseLayout(Page): ...frontend/layouts/base/index.html (scaffolded):
{% load fk_tags %}
<!doctype html>
<html>
<head>
{% fk_preloads %}
{% fk_stylesheets %}
{% fk_head_scripts %}
</head>
<body>
{% block body %}{% endblock %}
{% fk_body_scripts %}
</body>
</html>frontend/pages/home/__init__.py:
from frontend.layouts.base import BaseLayout
class HomePage(BaseLayout):
def __init__(self, name: str) -> None:
super().__init__()
self.name = namefrontend/pages/home/index.html:
{% extends "layouts/base/index.html" %}
{% block body %}
<h1>Hello {{ page.name }}</h1>
{% endblock %}And in a view:
from django.http import HttpRequest, HttpResponse
from django.views import View
from frontend.pages.home import HomePage
class HomeView(View):
def get(self, request: HttpRequest) -> HttpResponse:
return HomePage(name="User").as_response(request=request)Load tags with {% load fk_tags %}. All tags expect a page object in template context (the Page base class provides it).
{% fk_preloads %}: modulepreload links (production only){% fk_stylesheets %}: CSS links{% fk_head_scripts %}:<script type="module">tags intended for<head>{% fk_body_scripts %}:<script type="module">tags intended for end of<body>{% fk_custom_entry "name" %}: loadsname.entry.ts/name.entry.jsrelative to the current page directory
The included example project uses Tailwind v4 via the official Vite plugin.
- Install dependencies:
npm install --save-dev tailwindcss @tailwindcss/vite
- Add Tailwind to
vite.config.js(beforeDjangoFrontendKit()):import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [tailwindcss(), DjangoFrontendKit()], });
- Add a Tailwind config (example
tailwind.config.cjs):export default { content: ["./frontend/**/*.{html,js,ts,jsx,tsx,vue,py}"], theme: { extend: {} }, plugins: [], };
- Import Tailwind in your base CSS (scaffolded
frontend/layouts/base/main.css):@import "tailwindcss";
- Ensure the CSS is imported by a Vite entrypoint that’s loaded on your pages (scaffolded
frontend/layouts/base/entry.head.tsis a good place):import "./main.css";
- Install dependencies:
npm install react react-dom npm install --save-dev @vitejs/plugin-react
- Enable the React plugin in
vite.config.js:import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react(), DjangoFrontendKit()], });
- Create a custom entry file in a page directory (must end with
.entry.jsor.entry.tsso it’s included in the manifest):frontend/pages/home/react.entry.js:import React from "react"; import { createRoot } from "react-dom/client"; function App() { return <div>Hello from React</div>; } const el = document.getElementById("react-app"); if (el) createRoot(el).render(<App />);
- Add a mount point + include the entry in your template:
<div id="react-app"></div> {% fk_custom_entry "react" %}
- Install dependencies:
npm install vue npm install --save-dev @vitejs/plugin-vue
- Enable the Vue plugin in
vite.config.js:import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [vue(), DjangoFrontendKit()], });
- Create a Vue component + an entry file (entry must end with
.entry.jsor.entry.ts):frontend/pages/home/HelloVue.vue:<template> <div>Hello from Vue</div> </template>
frontend/pages/home/vue.entry.ts:import { createApp } from "vue"; import HelloVue from "./HelloVue.vue"; const el = document.getElementById("vue-app"); if (el) createApp(HelloVue).mount(el);
- Add a mount point + include the entry in your template:
<div id="vue-app"></div> {% fk_custom_entry "vue" %}
The Vite plugin bundled with this project only treats these files as build inputs:
entry.js/entry.tsentry.head.js/entry.head.ts*.entry.js/*.entry.ts
So keep “entry” files as .js/.ts (they can import .jsx, .tsx, .vue, CSS, etc.).
- Build assets:
npm run build
- Ensure
DEBUG=False. - Ensure Django can serve static assets in production (e.g. WhiteNoise, CDN, or your platform’s static hosting).
- Collect static files:
python manage.py collectstatic
If you use WhiteNoise, consider CompressedManifestStaticFilesStorage so hashed assets are served efficiently.
DJFK_FRONTEND_DIR is not set / does not exist: set it insettings.pyand runpython manage.py scaffold.VITE_OUTPUT_DIR is not set / does not exist: set it insettings.pyand ensure it matches your Vite build output directory.manifest.jsonnot found: runnpm run buildand verifyVITE_OUTPUT_DIR/.vite/manifest.jsonexists....was not included in Vite manifest: ensure the file is part of Vite build inputs (entrypoints must be reachable/declared).
This repo includes a working example in example/ showing:
- A base layout (
frontend/layouts/base/) withentry.head.tsandentry.ts - A page (
frontend/pages/home/) that renders Django template HTML and mounts optional JS widgets
This project is currently in Beta. Expect occasional breaking changes until 1.0.0.
- Run linting:
ruff check . - Run type checking:
mypy .
PRs are welcome. If you’re proposing a behavior change, include a short rationale and an example project diff.
MIT — see LICENSE.