Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
59 changes: 59 additions & 0 deletions docs/features/frontend-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,65 @@ 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 with Docker (Recommended):**
```bash
docker-compose up # Starts Django + Vite dev server together
```

**Development without Docker:**
```bash
# Terminal 1: Start Vite dev server
cd frontend
npm install
npm run dev

# Terminal 2: Start Django
python manage.py runserver
```

**Building for Production:**
```bash
# Build optimized assets locally
cd frontend
npm run build # Outputs to static/dist/

# Or let Docker handle it
docker build -t myapp . # Multi-stage build includes npm run build
```

**Troubleshooting:**

| Issue | Solution |
|-------|----------|
| Vite HMR not working in Docker | Ensure `VITE_DEV_SERVER_HOST=vite` is set for the web service |
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 troubleshooting entry is slightly misleading. The VITE_DEV_SERVER_HOST environment variable should be set for the Django web service (as shown in docker-compose.yml.jinja line 31), not the Vite service. Consider clarifying this by stating 'Ensure VITE_DEV_SERVER_HOST=vite is set as an environment variable in the web service configuration'.

Suggested change
| Vite HMR not working in Docker | Ensure `VITE_DEV_SERVER_HOST=vite` is set for the web service |
| Vite HMR not working in Docker | Ensure `VITE_DEV_SERVER_HOST=vite` is set as an environment variable in the web service configuration |

Copilot uses AI. Check for mistakes.
| Styles not updating | Check Tailwind is scanning `../templates/**/*.html` |
| Assets 404 in production | Run `python manage.py collectstatic` after building |
| `django_vite` template errors | Ensure `DEBUG=False` uses built manifest, not dev server |

**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)

### 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 == 'htmx-tailwind' and frontend_bundling == 'vite' -%}
/static/dist
{% endif -%}

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

# 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 == 'htmx-tailwind' and frontend_bundling == 'vite' -%}
# 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/ ./

# Create output directory and build assets
RUN mkdir -p /app/static && npm run build
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 frontend builder creates /app/static but the COPY command on lines 66 and 91 copies from /app/static/dist to ./static/dist. The npm run build already outputs to ../static/dist per vite.config.js (line 8), so creating /app/static here is redundant. Consider removing mkdir -p /app/static && since Vite will create the necessary directories.

Suggested change
RUN mkdir -p /app/static && npm run build
RUN npm run build

Copilot uses AI. Check for mistakes.

{% 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 == 'htmx-tailwind' and frontend_bundling == 'vite' -%}
# 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 == 'htmx-tailwind' and frontend_bundling == 'vite' -%}
# 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 == 'htmx-tailwind' and frontend_bundling == 'vite' -%}
"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 == 'htmx-tailwind' and frontend_bundling == 'vite' -%}
# 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 == 'htmx-tailwind' and frontend_bundling == 'vite' %}
- 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 == 'htmx-tailwind' and frontend_bundling == 'vite' %}

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/alpinejs@3.13.3/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
28 changes: 27 additions & 1 deletion tests/test_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,42 @@ 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.

# CDN mode should NOT create frontend build files
frontend_dir = project / "frontend"
assert not (frontend_dir / "package.json").exists()
assert not (frontend_dir / "vite.config.js").exists()
assert not (frontend_dir / "tailwind.config.js").exists()


def test_nextjs_frontend_generated(generate):
Expand Down
Loading