Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### API & Frontend Options
- API frameworks: Django REST Framework, Strawberry GraphQL, both, or none
- Frontend: HTMX + Tailwind CSS, Next.js, or headless
- Frontend bundling for HTMX+Tailwind: Vite (production-ready) or CDN (simple)
- Authentication: django-allauth, JWT, or both

#### Background Task Processing
Expand Down
9 changes: 9 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ frontend:
{%- elif project_type == 'internal-tool' -%}htmx-tailwind
{%- else -%}none{%- endif -%}

frontend_bundling:
type: str
help: Frontend asset bundling
when: "{{ frontend == 'htmx-tailwind' }}"
choices:
Vite (Production-ready, bundled assets): vite
CDN (Simple, no build step): cdn
default: "vite"

# Async & Tasks
background_tasks:
type: str
Expand Down
33 changes: 33 additions & 0 deletions docs/features/frontend-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@ Modern, minimal-JavaScript approach with server-rendered HTML.
- **Tailwind CSS** for styling
- **Alpine.js** for lightweight JavaScript

#### Asset Bundling Options

When you select HTMX + Tailwind, you can choose how assets are delivered:

| Option | Best For | Build Step | External Dependencies |
|--------|----------|------------|----------------------|
| **Vite** (default) | Production apps | Yes | None |
| **CDN** | Prototypes, MVPs | No | Yes (3 CDNs) |

**Vite (Recommended for Production)**

- Bundles Tailwind CSS, HTMX, and Alpine.js locally
- No external CDN dependencies in production
- Proper CSS purging for smaller bundles
- Hot Module Replacement (HMR) in development
- Uses `django-vite` for seamless Django integration

Development:
```bash
docker-compose up # Starts Django + Vite dev server
```

Production:
- Assets are built during Docker image build
- Served via WhiteNoise from `/static/dist/`

**CDN (Simple, No Build Step)**

- Assets loaded from external CDNs (tailwindcss, unpkg, jsdelivr)
- Zero configuration needed
- Good for quick prototypes
- Not recommended for production (external dependencies)
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The documentation mentions using docker-compose up but doesn't provide guidance on how to build frontend assets for local development outside of Docker, or how to run the Vite dev server independently. Consider adding sections for:

  1. Local development setup (npm install, npm run dev)
  2. Building production assets locally (npm run build)
  3. Troubleshooting common Vite/Django integration issues

Copilot uses AI. Check for mistakes.

### Next.js

Full-featured React framework for building modern web applications.
Expand Down
9 changes: 9 additions & 0 deletions template/.gitignore.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ db.sqlite3-journal
/media
/staticfiles
/static_root
{% if frontend_bundling == 'vite' -%}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - frontend_bundling is undefined when frontend != 'htmx-tailwind'. Use {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} or {% if frontend_bundling is defined and frontend_bundling == 'vite' -%}.

Copilot uses AI. Check for mistakes.
/static/dist
{% endif -%}

# Environment
.env
Expand All @@ -42,6 +45,12 @@ db.sqlite3-journal
.venv/
poetry.lock
{% endif -%}
{% if frontend_bundling == 'vite' -%}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - frontend_bundling is undefined when frontend != 'htmx-tailwind'. Use {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} or {% if frontend_bundling is defined and frontend_bundling == 'vite' -%}.

Copilot uses AI. Check for mistakes.

# Node
node_modules/
frontend/node_modules/
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The entry frontend/node_modules/ on line 52 is redundant since node_modules/ on line 51 already matches all node_modules directories recursively. Consider removing line 52 for simplicity.

Suggested change:

# Node
node_modules/
Suggested change
frontend/node_modules/

Copilot uses AI. Check for mistakes.
{% endif -%}

# IDEs
.vscode/
Expand Down
29 changes: 29 additions & 0 deletions template/Dockerfile.jinja
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
# syntax=docker/dockerfile:1
{% if frontend_bundling == 'vite' -%}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - frontend_bundling is undefined when frontend != 'htmx-tailwind'. Use {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} or {% if frontend_bundling is defined and frontend_bundling == 'vite' -%}.

Copilot uses AI. Check for mistakes.
# Build frontend assets
FROM node:20-slim AS frontend-builder

WORKDIR /app/frontend

# Copy frontend files
COPY frontend/package*.json ./

# Install dependencies
RUN npm ci

# Copy rest of frontend
COPY frontend/ ./

# Build assets
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build output directory should be explicitly created before running npm run build to ensure the parent /app/static directory exists. Add a line like RUN mkdir -p /app/static before the build command to prevent potential build failures if the directory doesn't exist in the builder stage.

Suggested change
# Build assets
# Build assets
RUN mkdir -p /app/static

Copilot uses AI. Check for mistakes.
RUN npm run build

{% endif -%}
FROM python:{{ python_version }}-slim as base

# Set environment variables
Expand Down Expand Up @@ -42,6 +61,11 @@ RUN uv sync --no-dev
# Copy rest of application
COPY . .

{% if frontend_bundling == 'vite' -%}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - frontend_bundling is undefined when frontend != 'htmx-tailwind'. Use {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} or {% if frontend_bundling is defined and frontend_bundling == 'vite' -%}.

Copilot uses AI. Check for mistakes.
# Copy built frontend assets
COPY --from=frontend-builder /app/static/dist ./static/dist
{% endif -%}

{% else -%}
# Install poetry
RUN pip install poetry==1.7.0
Expand All @@ -61,6 +85,11 @@ RUN poetry config virtualenvs.create false \

# Copy rest of application
COPY . .

{% if frontend_bundling == 'vite' -%}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - frontend_bundling is undefined when frontend != 'htmx-tailwind'. Use {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} or {% if frontend_bundling is defined and frontend_bundling == 'vite' -%}.

Copilot uses AI. Check for mistakes.
# Copy built frontend assets
COPY --from=frontend-builder /app/static/dist ./static/dist
{% endif -%}
{% endif -%}

# Change ownership
Expand Down
16 changes: 16 additions & 0 deletions template/config/settings/base.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ DJANGO_APPS = [
]

THIRD_PARTY_APPS = [
{% if frontend_bundling == 'vite' -%}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frontend_bundling variable is only defined when frontend == 'htmx-tailwind' (per copier.yml line 124). When other frontend options are selected, this variable will be undefined and referencing it will cause template rendering errors. Add a check: {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} or use {% if frontend_bundling is defined and frontend_bundling == 'vite' -%} to safely handle cases where frontend_bundling is not set.

Suggested change
{% if frontend_bundling == 'vite' -%}
{% if frontend_bundling is defined and frontend_bundling == 'vite' -%}

Copilot uses AI. Check for mistakes.
"django_vite",
{% endif -%}
{% if api_style in ['drf', 'both'] -%}
"rest_framework",
"drf_spectacular",
Expand Down Expand Up @@ -242,6 +245,19 @@ STORAGES = {
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

{% if frontend_bundling == 'vite' -%}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - frontend_bundling is undefined when frontend != 'htmx-tailwind'. Use {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' -%} or {% if frontend_bundling is defined and frontend_bundling == 'vite' -%}.

Suggested change
{% if frontend_bundling == 'vite' -%}
{% if frontend_bundling is defined and frontend_bundling == 'vite' -%}

Copilot uses AI. Check for mistakes.
# Django Vite
DJANGO_VITE = {
"default": {
"dev_mode": DEBUG,
"manifest_path": BASE_DIR / "static" / "dist" / "manifest.json",
"static_url_prefix": "dist/",
"dev_server_host": env("VITE_DEV_SERVER_HOST", default="localhost"),
"dev_server_port": 5173,
}
}
{% endif -%}

{% if media_storage == 'aws-s3' -%}
# AWS S3 Storage
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default="")
Expand Down
15 changes: 15 additions & 0 deletions template/docker-compose.yml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ services:
- EMAIL_PORT=1025
{% if dependency_manager == 'uv' %}
- UV_NO_CACHE=1
{% endif %}
{% if frontend_bundling == 'vite' %}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - frontend_bundling is undefined when frontend != 'htmx-tailwind'. Use {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %} or {% if frontend_bundling is defined and frontend_bundling == 'vite' %}.

Suggested change
{% if frontend_bundling == 'vite' %}
{% if frontend_bundling is defined and frontend_bundling == 'vite' %}

Copilot uses AI. Check for mistakes.
- VITE_DEV_SERVER_HOST=vite
{% endif %}
env_file:
- .env
Expand Down Expand Up @@ -65,6 +68,18 @@ services:
ports:
- "1025:1025"
- "8025:8025"
{% if frontend_bundling == 'vite' %}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - frontend_bundling is undefined when frontend != 'htmx-tailwind'. Use {% if frontend == 'htmx-tailwind' and frontend_bundling == 'vite' %} or {% if frontend_bundling is defined and frontend_bundling == 'vite' %}.

Suggested change
{% if frontend_bundling == 'vite' %}
{% if frontend_bundling is defined and frontend_bundling == 'vite' %}

Copilot uses AI. Check for mistakes.

vite:
image: node:20-slim
working_dir: /app/frontend
volumes:
- ./frontend:/app/frontend
- ./static:/app/static
ports:
- "5173:5173"
command: sh -c "npm install && npm run dev"
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using npm ci instead of npm install for more deterministic and faster installs in the development container. npm ci is designed for automated environments and provides reproducible builds.

Suggested change:

command: sh -c "npm ci && npm run dev"

This aligns with the production Dockerfile which uses npm ci (line 12).

Suggested change
command: sh -c "npm install && npm run dev"
command: sh -c "npm ci && npm run dev"

Copilot uses AI. Check for mistakes.
{% endif %}
{% if background_tasks in ['temporal', 'both'] %}

temporal:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "{{ project_slug }}-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"htmx.org": "^1.9.10",
"alpinejs": "^3.13.3"
},
"devDependencies": {
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"vite": "^5.0.10"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Import styles
import "./styles.css";

// Import HTMX
import htmx from "htmx.org";
window.htmx = htmx;

// Import Alpine.js
import Alpine from "alpinejs";
window.Alpine = Alpine;
Alpine.start();
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"../templates/**/*.html",
"./src/**/*.js",
],
theme: {
extend: {},
},
plugins: [],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineConfig } from "vite";
import { resolve } from "path";

export default defineConfig({
base: "/static/",
build: {
manifest: "manifest.json",
outDir: resolve(__dirname, "../static/dist"),
rollupOptions: {
input: {
main: resolve(__dirname, "src/main.js"),
},
},
},
server: {
host: "0.0.0.0",
port: 5173,
origin: "http://localhost:5173",
},
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
<!DOCTYPE html>
{% if frontend_bundling == 'vite' %}{% raw %}{% load django_vite %}{% endraw %}
{% endif %}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% raw %}{% block title %}{% endraw %}{{ project_name }}{% raw %}{% endblock %}{% endraw %}</title>

{% if frontend_bundling == 'vite' %}
{% raw %}{% vite_hmr_client %}{% endraw %}
{% raw %}{% vite_asset 'src/main.js' %}{% endraw %}
{% else %}
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>

Expand All @@ -13,6 +18,7 @@

<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
{% endif %}

{% raw %}{% block extra_head %}{% endblock %}{% endraw %}
</head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ django-opensearch-dsl = "^0.6.0"
{% if use_i18n -%}
django-parler = "^2.3.0"
{% endif -%}
{% if frontend_bundling == 'vite' -%}
django-vite = "^3.0.0"
{% endif -%}
django-extensions = "^3.2.0"
python-dotenv = "^1.0.0"
django-alive = "^1.3.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ dependencies = [
{% endif -%}
{% if use_i18n -%}
"django-parler>=2.3.0",
{% endif -%}
{% if frontend_bundling == 'vite' -%}
"django-vite>=3.0.0",
{% endif -%}
"django-extensions>=3.2.0",
"python-dotenv>=1.0.0",
Expand Down
22 changes: 21 additions & 1 deletion tests/test_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,36 @@ def test_no_api_excludes_frameworks(generate):


def test_htmx_frontend_templates_generated(generate):
"""Test that HTMX frontend generates templates."""
"""Test that HTMX frontend generates templates with Vite (default)."""
project = generate(frontend="htmx-tailwind")

templates_dir = project / "templates"
assert (templates_dir / "base.html").exists()

base_html = (templates_dir / "base.html").read_text()
assert "{% block" in base_html
# Vite is the default, so check for vite tags
assert "django_vite" in base_html
assert "vite_asset" in base_html

# Vite frontend files should exist
frontend_dir = project / "frontend"
assert (frontend_dir / "package.json").exists()
assert (frontend_dir / "vite.config.js").exists()
assert (frontend_dir / "tailwind.config.js").exists()

Comment on lines 206 to +224
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding assertions to verify that django_vite is added to INSTALLED_APPS and that DJANGO_VITE settings are present in the generated settings file, similar to how test_drf_api_generated checks for DRF configuration. This would provide more comprehensive coverage.

Example additions:

# Check Django settings
settings = project / "config/settings/base.py"
settings_content = settings.read_text()
assert "django_vite" in settings_content
assert "DJANGO_VITE" in settings_content

Copilot uses AI. Check for mistakes.

def test_htmx_frontend_cdn_mode(generate):
"""Test that HTMX frontend with CDN mode uses CDN links."""
project = generate(frontend="htmx-tailwind", frontend_bundling="cdn")
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test test_all_features_enabled_generates_successfully in tests/test_features.py (line 390) uses frontend='htmx-tailwind' but doesn't specify frontend_bundling. Since Vite is the default, consider explicitly testing with frontend_bundling='vite' in that test or adding a comment explaining the default behavior is being tested.

Copilot uses AI. Check for mistakes.

templates_dir = project / "templates"
base_html = (templates_dir / "base.html").read_text()
assert "{% block" in base_html
assert "tailwindcss" in base_html
assert "htmx" in base_html
# Should not have vite tags
assert "django_vite" not in base_html
Comment on lines +232 to +242
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding a test that verifies CDN mode doesn't create frontend files (package.json, vite.config.js, etc.) to ensure the conditional file generation works correctly in both directions.

Copilot uses AI. Check for mistakes.


def test_nextjs_frontend_generated(generate):
Expand Down
Loading