@django-stack/cli 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +65 -20
- package/dist/index.d.ts +24 -9
- package/dist/index.js +1 -1
- package/dist/{production-M2SsuuBb.js → production-WThC1aS2.js} +267 -30
- package/package.json +1 -1
- package/templates/backend/Dockerfile +7 -2
- package/templates/backend/config/settings.py +13 -0
- package/templates/backend/config/urls.py +11 -0
- package/templates/backend/content/admin.py +14 -5
- package/templates/backend/content/models.py +2 -2
- package/templates/backend/content/tests/test_api.py +15 -3
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/.github/workflows/deploy.yml +10 -1
- package/templates/base/AGENTS.md +86 -8
- package/templates/base/Caddyfile +24 -0
- package/templates/base/README.md +74 -6
- package/templates/base/SECURITY.md +2 -0
- package/templates/base/compose.yaml +28 -2
- package/templates/base/env.example +37 -0
- package/templates/frontend-astro/AGENTS.md +3 -3
- package/templates/frontend-astro/Dockerfile +6 -3
- package/templates/frontend-astro/astro.config.mjs +16 -5
- package/templates/frontend-astro/package.json +1 -1
- package/templates/frontend-astro/pnpm-workspace.yaml +17 -0
- package/templates/frontend-astro/src/components/ContactForm.astro +19 -3
- package/templates/frontend-astro/src/middleware.ts +9 -1
- package/templates/frontend-next/AGENTS.md +3 -3
- package/templates/frontend-next/Dockerfile +5 -2
- package/templates/frontend-next/package.json +3 -3
- package/templates/frontend-next/pnpm-workspace.yaml +11 -0
- package/templates/frontend-next/src/app/[...path]/page.tsx +2 -1
- package/templates/frontend-next/src/components/ContactForm.tsx +14 -3
- package/templates/frontend-next/src/instrumentation.ts +12 -0
- package/templates/frontend-next/src/proxy.ts +4 -1
- package/templates/frontend-nuxt/AGENTS.md +3 -3
- package/templates/frontend-nuxt/Dockerfile +5 -2
- package/templates/frontend-nuxt/nuxt.config.ts +7 -3
- package/templates/frontend-nuxt/package.json +2 -2
- package/templates/frontend-nuxt/pnpm-workspace.yaml +13 -0
- package/templates/frontend-nuxt/server/plugins/errors.ts +14 -0
- package/templates/frontend-nuxt/server/routes/_content/layout.get.ts +4 -0
- package/templates/frontend-nuxt/src/components/ContactForm.vue +15 -2
- package/templates/frontend-sveltekit/AGENTS.md +3 -3
- package/templates/frontend-sveltekit/Dockerfile +6 -3
- package/templates/frontend-sveltekit/package.json +2 -2
- package/templates/frontend-sveltekit/pnpm-workspace.yaml +11 -0
- package/templates/frontend-sveltekit/src/hooks.server.ts +13 -1
- package/templates/frontend-sveltekit/src/lib/components/ContactForm.svelte +15 -2
- package/templates/frontend-sveltekit/src/routes/+layout.server.ts +3 -0
- package/templates/frontend-sveltekit/src/routes/[...path]/+page.svelte +1 -1
- package/templates/frontend-sveltekit/vite.config.ts +10 -3
- package/templates/frontend-templates/template.json +5 -0
- package/templates/frontend-templates/website/__init__.py +0 -0
- package/templates/frontend-templates/website/apps.py +6 -0
- package/templates/frontend-templates/website/context_processors.py +16 -0
- package/templates/frontend-templates/website/forms.py +20 -0
- package/templates/frontend-templates/website/static/website/favicon.svg +1 -0
- package/templates/frontend-templates/website/static/website/global.css +232 -0
- package/templates/frontend-templates/website/templates/website/404.html +10 -0
- package/templates/frontend-templates/website/templates/website/base.html +44 -0
- package/templates/frontend-templates/website/templates/website/blocks/call_to_action.html +5 -0
- package/templates/frontend-templates/website/templates/website/blocks/image.html +4 -0
- package/templates/frontend-templates/website/templates/website/blocks/quote.html +4 -0
- package/templates/frontend-templates/website/templates/website/blocks/text.html +2 -0
- package/templates/frontend-templates/website/templates/website/home.html +8 -0
- package/templates/frontend-templates/website/templates/website/includes/contact_form.html +21 -0
- package/templates/frontend-templates/website/templates/website/includes/post_card.html +9 -0
- package/templates/frontend-templates/website/templates/website/includes/post_list.html +11 -0
- package/templates/frontend-templates/website/templates/website/page.html +10 -0
- package/templates/frontend-templates/website/templates/website/post.html +20 -0
- package/templates/frontend-templates/website/templates/website/post_list.html +11 -0
- package/templates/frontend-templates/website/templates/website/robots.txt +5 -0
- package/templates/frontend-templates/website/templates/website/rss.xml +16 -0
- package/templates/frontend-templates/website/templates/website/sitemap.xml +8 -0
- package/templates/frontend-templates/website/tests/__init__.py +0 -0
- package/templates/frontend-templates/website/tests/test_site.py +102 -0
- package/templates/frontend-templates/website/urls.py +17 -0
- package/templates/frontend-templates/website/views.py +176 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Smoke tests of the content API: every exposed model answers the read-only
|
|
3
3
|
key, drafts stay hidden, and the contact form takes submissions.
|
|
4
|
-
Run them with `
|
|
4
|
+
Run them with `{{pm_run}} test` at the project root.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
from django.apps import apps
|
|
9
9
|
from django.conf import settings
|
|
10
|
+
from django_stack.contrib.captcha.testing import captcha_token
|
|
10
11
|
from rest_framework.test import APIClient
|
|
11
12
|
|
|
12
13
|
from content.models import FormSubmission, Page, Post
|
|
@@ -63,13 +64,24 @@ def test_post_by_slug(reader, seeded):
|
|
|
63
64
|
def test_contact_form(seeded):
|
|
64
65
|
response = APIClient().post(
|
|
65
66
|
"/api/content.formsubmission",
|
|
66
|
-
{"name": "Ada", "email": "ada@example.com", "message": "Hello"},
|
|
67
|
+
{"name": "Ada", "email": "ada@example.com", "message": "Hello", "captcha": captcha_token()},
|
|
67
68
|
format="json",
|
|
68
69
|
)
|
|
69
70
|
assert response.status_code == 201
|
|
70
71
|
assert FormSubmission.objects.filter(email="ada@example.com").exists()
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
@pytest.mark.skipif(not settings.DJANGO_STACK["CAPTCHA"], reason="the captcha is off")
|
|
75
|
+
def test_contact_form_needs_a_captcha(seeded):
|
|
76
|
+
response = APIClient().post(
|
|
77
|
+
"/api/content.formsubmission",
|
|
78
|
+
{"name": "Ada", "email": "ada@example.com", "message": "Hello"},
|
|
79
|
+
format="json",
|
|
80
|
+
)
|
|
81
|
+
assert response.status_code == 400
|
|
82
|
+
assert "captcha" in response.json()
|
|
83
|
+
|
|
84
|
+
|
|
73
85
|
@pytest.mark.skipif(not apps.is_installed("content_studio.notifications"), reason="notifications are off")
|
|
74
86
|
def test_contact_form_notifies_the_editors(seeded, django_capture_on_commit_callbacks):
|
|
75
87
|
from content_studio.notifications.models import Notification
|
|
@@ -79,7 +91,7 @@ def test_contact_form_notifies_the_editors(seeded, django_capture_on_commit_call
|
|
|
79
91
|
with django_capture_on_commit_callbacks(execute=True):
|
|
80
92
|
APIClient().post(
|
|
81
93
|
"/api/content.formsubmission",
|
|
82
|
-
{"name": "Ada", "email": "ada@example.com", "message": "100% sure"},
|
|
94
|
+
{"name": "Ada", "email": "ada@example.com", "message": "100% sure", "captcha": captcha_token()},
|
|
83
95
|
format="json",
|
|
84
96
|
)
|
|
85
97
|
notification = Notification.objects.get(recipient=editor, event="site.form_submitted")
|
|
@@ -5,7 +5,7 @@ description = "The Django backend of {{project_name}}."
|
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"Django>=6.1,<6.2",
|
|
8
|
-
"django-stack[server,s3]>=0.
|
|
8
|
+
"django-stack[server,s3]>=0.11.0",
|
|
9
9
|
"django-headless[schema]>=1.1.1",
|
|
10
10
|
"django-content-studio[images,huey]>=1.11.0",
|
|
11
11
|
]
|
|
@@ -24,7 +24,9 @@ permissions:
|
|
|
24
24
|
|
|
25
25
|
env:
|
|
26
26
|
BACKEND_IMAGE: ghcr.io/${{ github.repository }}-backend:${{ github.sha }}
|
|
27
|
+
# @if frontend
|
|
27
28
|
FRONTEND_IMAGE: ghcr.io/${{ github.repository }}-frontend:${{ github.sha }}
|
|
29
|
+
# @end
|
|
28
30
|
|
|
29
31
|
jobs:
|
|
30
32
|
deploy:
|
|
@@ -38,9 +40,11 @@ jobs:
|
|
|
38
40
|
- name: Build and push the images
|
|
39
41
|
run: |
|
|
40
42
|
docker build --target prod --tag "${BACKEND_IMAGE,,}" backend
|
|
41
|
-
docker build --target prod --tag "${FRONTEND_IMAGE,,}" frontend
|
|
42
43
|
docker push "${BACKEND_IMAGE,,}"
|
|
44
|
+
# @if frontend
|
|
45
|
+
docker build --target prod --tag "${FRONTEND_IMAGE,,}" frontend
|
|
43
46
|
docker push "${FRONTEND_IMAGE,,}"
|
|
47
|
+
# @end
|
|
44
48
|
|
|
45
49
|
- name: Set up SSH
|
|
46
50
|
run: |
|
|
@@ -63,7 +67,12 @@ jobs:
|
|
|
63
67
|
set -e
|
|
64
68
|
cd '$DEPLOY_PATH'
|
|
65
69
|
echo '$REGISTRY_TOKEN' | docker login ghcr.io --username '${{ github.actor }}' --password-stdin
|
|
70
|
+
# @if frontend
|
|
66
71
|
export BACKEND_IMAGE='${BACKEND_IMAGE,,}' FRONTEND_IMAGE='${FRONTEND_IMAGE,,}'
|
|
72
|
+
# @end
|
|
73
|
+
# @if !frontend
|
|
74
|
+
export BACKEND_IMAGE='${BACKEND_IMAGE,,}'
|
|
75
|
+
# @end
|
|
67
76
|
docker compose pull --quiet --ignore-buildable
|
|
68
77
|
docker compose up -d --no-build --remove-orphans
|
|
69
78
|
docker logout ghcr.io
|
package/templates/base/AGENTS.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
# {{project_name}}: guide for AI agents
|
|
2
2
|
|
|
3
|
+
<!-- @if frontend -->
|
|
3
4
|
A website built with DjangoStack: a Django backend (Django Headless serves
|
|
4
5
|
the content API, Django Content Studio is the admin) and a frontend built
|
|
5
6
|
with {{frontend_label}} that renders the content, run with Docker Compose.
|
|
6
7
|
`frontend/AGENTS.md` describes the frontend.
|
|
8
|
+
<!-- @end -->
|
|
9
|
+
<!-- @if headless -->
|
|
10
|
+
A headless project built with DjangoStack: a Django backend (Django Headless
|
|
11
|
+
serves the content API, Django Content Studio is the admin), run with Docker
|
|
12
|
+
Compose. It has no frontend; other apps read the API with the read-only key.
|
|
13
|
+
<!-- @end -->
|
|
14
|
+
<!-- @if templates -->
|
|
15
|
+
A website built with DjangoStack: a Django backend (Django Headless serves
|
|
16
|
+
the content API, Django Content Studio is the admin) that also renders the
|
|
17
|
+
site with Django templates, in the `website` app, run with Docker Compose.
|
|
18
|
+
There is no Node and no frontend service: the pages read the content with
|
|
19
|
+
the ORM.
|
|
20
|
+
<!-- @end -->
|
|
7
21
|
|
|
8
22
|
## Layout
|
|
9
23
|
|
|
@@ -16,8 +30,18 @@ backend/ Django project
|
|
|
16
30
|
content/admin.py how each model looks in the admin (studios)
|
|
17
31
|
content/seed.py demo content
|
|
18
32
|
content/tests/ pytest tests of the API
|
|
33
|
+
<!-- @if templates -->
|
|
34
|
+
website/views.py the site's pages; they read content with published(), which hides drafts outside preview
|
|
35
|
+
website/urls.py the site's routes, included last in config/urls.py
|
|
36
|
+
website/templates/website/ base.html (the layout), the pages, includes/ and blocks/ (one template per block type)
|
|
37
|
+
website/static/website/global.css the site's stylesheet
|
|
38
|
+
website/forms.py the contact form, with the captcha
|
|
39
|
+
website/tests/ pytest tests of the pages
|
|
40
|
+
<!-- @end -->
|
|
41
|
+
<!-- @if frontend -->
|
|
19
42
|
frontend/ the {{frontend_label}} site; frontend/AGENTS.md lists its files
|
|
20
|
-
src/lib/headless.ts generated API types; do not edit, run `
|
|
43
|
+
src/lib/headless.ts generated API types; do not edit, run `{{pm_run}} generate:types`
|
|
44
|
+
<!-- @end -->
|
|
21
45
|
compose.yaml every service; COMPOSE_PROFILES in .env picks dev or prod
|
|
22
46
|
Caddyfile the prod profile's reverse proxy and HTTPS
|
|
23
47
|
.github/workflows/deploy.yml optional deploy to a server over SSH
|
|
@@ -26,23 +50,45 @@ Caddyfile the prod profile's reverse proxy and HTTPS
|
|
|
26
50
|
|
|
27
51
|
## Commands
|
|
28
52
|
|
|
29
|
-
Run them at the project root. They need Docker; Node is only needed for the `
|
|
53
|
+
Run them at the project root. They need Docker; Node is only needed for the `{{pm_run}}` shortcuts.
|
|
30
54
|
|
|
31
55
|
| Task | Command |
|
|
32
56
|
|------|---------|
|
|
33
|
-
| Start everything | `docker compose up --build` (or `
|
|
34
|
-
| Run a manage.py command | `docker compose exec backend python manage.py <command>` (or `
|
|
57
|
+
| Start everything | `docker compose up --build` (or `{{pm_run}} dev`) |
|
|
58
|
+
| Run a manage.py command | `docker compose exec backend python manage.py <command>` (or `{{pm_backend}} <command>`) |
|
|
35
59
|
| Make migrations after a model change | `docker compose exec backend python manage.py makemigrations` |
|
|
36
|
-
|
|
37
|
-
|
|
|
38
|
-
|
|
60
|
+
<!-- @if frontend -->
|
|
61
|
+
| Regenerate the API types after a model change | `{{pm_run}} generate:types` |
|
|
62
|
+
<!-- @end -->
|
|
63
|
+
<!-- @if !frontend -->
|
|
64
|
+
| Write the OpenAPI schema to `backend/openapi.json` | `{{pm_run}} generate:types` |
|
|
65
|
+
<!-- @end -->
|
|
66
|
+
| Run the tests | `{{pm_run}} test` |
|
|
67
|
+
<!-- @if frontend -->
|
|
68
|
+
| Lint and type-check | `{{pm_run}} lint` |
|
|
69
|
+
<!-- @end -->
|
|
70
|
+
<!-- @if !frontend -->
|
|
71
|
+
| Lint | `{{pm_run}} lint` |
|
|
72
|
+
<!-- @end -->
|
|
39
73
|
| Logs | `docker compose logs --follow backend` |
|
|
40
74
|
| Production settings for a server | `npx @django-stack/cli production --domain example.com` |
|
|
41
75
|
|
|
76
|
+
<!-- @if frontend -->
|
|
42
77
|
In production (`COMPOSE_PROFILES=prod,db`) the services are `backend-prod`,
|
|
43
78
|
`worker-prod` (which also runs the daily backup), `frontend-prod` and
|
|
44
79
|
`proxy`, with `umami` for page analytics when the `analytics` profile is on;
|
|
45
80
|
their containers are
|
|
81
|
+
<!-- @end -->
|
|
82
|
+
<!-- @if headless -->
|
|
83
|
+
In production (`COMPOSE_PROFILES=prod,db`) the services are `backend-prod`,
|
|
84
|
+
`worker-prod` (which also runs the daily backup) and `proxy`; their
|
|
85
|
+
containers are
|
|
86
|
+
<!-- @end -->
|
|
87
|
+
<!-- @if templates -->
|
|
88
|
+
In production (`COMPOSE_PROFILES=prod,db`) the services are `backend-prod`,
|
|
89
|
+
`worker-prod` (which also runs the daily backup) and `proxy`, with `umami`
|
|
90
|
+
for page analytics when the `analytics` profile is on; their containers are
|
|
91
|
+
<!-- @end -->
|
|
46
92
|
read-only, so a change goes through the code and a new image, never an edit
|
|
47
93
|
in a running container.
|
|
48
94
|
|
|
@@ -54,23 +100,55 @@ in a running container.
|
|
|
54
100
|
`ModelStudio`.
|
|
55
101
|
3. Run `makemigrations`; the backend migrates on its next start, or run
|
|
56
102
|
`migrate`.
|
|
57
|
-
|
|
103
|
+
<!-- @if frontend -->
|
|
104
|
+
4. Run `{{pm_run}} generate:types`, then read it in the frontend as
|
|
58
105
|
`frontend/AGENTS.md` shows.
|
|
106
|
+
<!-- @end -->
|
|
107
|
+
<!-- @if headless -->
|
|
108
|
+
4. Its routes are under `/api/`; `/api/schema` describes them.
|
|
109
|
+
<!-- @end -->
|
|
110
|
+
<!-- @if templates -->
|
|
111
|
+
4. Show it on the site: a view in `backend/website/views.py` that reads it
|
|
112
|
+
with `published(Model.objects.all(), request)` when it has a status, a
|
|
113
|
+
route in `website/urls.py` and a template in `website/templates/website/`.
|
|
114
|
+
Decorate the view with `@site_page`, which caches the page until content
|
|
115
|
+
changes and sets the Content-Security-Policy.
|
|
116
|
+
<!-- @end -->
|
|
59
117
|
5. Add an API smoke test to `backend/content/tests/test_api.py`.
|
|
60
118
|
|
|
61
119
|
## How to add a block type
|
|
62
120
|
|
|
63
121
|
1. Add a `Block` subclass in `backend/content/blocks.py` and add it to
|
|
64
122
|
`BODY_BLOCKS`. Blocks need no migration.
|
|
123
|
+
<!-- @if frontend -->
|
|
65
124
|
2. Render it in the frontend as `frontend/AGENTS.md` shows.
|
|
125
|
+
<!-- @end -->
|
|
126
|
+
<!-- @if templates -->
|
|
127
|
+
2. Add `backend/website/templates/website/blocks/<type>.html`; it gets the
|
|
128
|
+
block's fields as `value`. `{% blocks page.body %}` renders nothing for
|
|
129
|
+
a type without a template.
|
|
130
|
+
<!-- @end -->
|
|
66
131
|
|
|
67
132
|
## Rules
|
|
68
133
|
|
|
69
134
|
- Never edit `.env` values you were not asked to change, and never print secrets.
|
|
70
135
|
- Never write migrations by hand; use `makemigrations`.
|
|
136
|
+
<!-- @if !templates -->
|
|
71
137
|
- The frontend only ever holds `API_READ_KEY`. Never give it `API_SECRET_KEY`.
|
|
138
|
+
<!-- @end -->
|
|
72
139
|
- API routes are closed by default. Opening a model to anonymous writes goes
|
|
73
140
|
through `DJANGO_STACK["PUBLIC_CREATE"]` in `config/settings.py`, and is
|
|
74
141
|
rate limited.
|
|
142
|
+
<!-- @if frontend -->
|
|
75
143
|
- Django owns `/admin`, `/api`, `/media` and `/static`; frontend routes must
|
|
76
144
|
not use these prefixes.
|
|
145
|
+
<!-- @end -->
|
|
146
|
+
<!-- @if templates -->
|
|
147
|
+
- `/admin`, `/api`, `/media`, `/static` and `/_a` are taken; site routes
|
|
148
|
+
must not use these prefixes.
|
|
149
|
+
- The site's pages use no inline scripts or styles: the
|
|
150
|
+
Content-Security-Policy of `@site_page` blocks them. Add files to
|
|
151
|
+
`website/static/website/` instead.
|
|
152
|
+
- HTML from an `HTMLField` (`|safe`) is sanitized when it is saved; never
|
|
153
|
+
mark other text safe.
|
|
154
|
+
<!-- @end -->
|
package/templates/base/Caddyfile
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
# {{project_name}}: the reverse proxy of the prod profile.
|
|
2
2
|
#
|
|
3
|
+
# @if frontend
|
|
3
4
|
# One domain: Django serves /admin, /api and /static; uploads on the server's
|
|
4
5
|
# disk are served from /media; Umami's tracker from /_a/; the frontend
|
|
5
6
|
# serves everything else. Caddy
|
|
7
|
+
# @end
|
|
8
|
+
# @if headless
|
|
9
|
+
# One domain: Django serves everything (the API, the admin, /static and a
|
|
10
|
+
# start page); uploads on the server's disk are served from /media. Caddy
|
|
11
|
+
# @end
|
|
12
|
+
# @if templates
|
|
13
|
+
# One domain: Django serves everything (the pages, the API, the admin and
|
|
14
|
+
# /static); uploads on the server's disk are served from /media; Umami's
|
|
15
|
+
# tracker from /_a/. Caddy
|
|
16
|
+
# @end
|
|
6
17
|
# gets and renews the HTTPS certificate for SITE_URL by itself.
|
|
7
18
|
#
|
|
8
19
|
# Behind a platform's proxy (Coolify, Dokploy), CADDY_ADDRESS=:80 serves
|
|
@@ -54,6 +65,7 @@
|
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
# @if frontend
|
|
57
69
|
@django path /admin /admin/* /api /api/* /static/*
|
|
58
70
|
handle @django {
|
|
59
71
|
reverse_proxy backend:8000
|
|
@@ -62,6 +74,18 @@
|
|
|
62
74
|
handle {
|
|
63
75
|
reverse_proxy frontend:3000
|
|
64
76
|
}
|
|
77
|
+
# @end
|
|
78
|
+
# @if !frontend
|
|
79
|
+
# The health checks are for Docker and the platforms, not the public.
|
|
80
|
+
@health path /healthz /readyz /statusz
|
|
81
|
+
handle @health {
|
|
82
|
+
respond 404
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
handle {
|
|
86
|
+
reverse_proxy backend:8000
|
|
87
|
+
}
|
|
88
|
+
# @end
|
|
65
89
|
|
|
66
90
|
log {
|
|
67
91
|
output stdout
|
package/templates/base/README.md
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
# {{project_name}}
|
|
2
2
|
|
|
3
|
+
<!-- @if frontend -->
|
|
3
4
|
A website with a Django backend and a frontend built with {{frontend_label}}, created with
|
|
4
5
|
[DjangoStack](https://github.com/dwarsbit/django-stack).
|
|
6
|
+
<!-- @end -->
|
|
7
|
+
<!-- @if headless -->
|
|
8
|
+
A content API and an admin with Django, created with
|
|
9
|
+
[DjangoStack](https://github.com/dwarsbit/django-stack). It has no frontend:
|
|
10
|
+
build your own against the API, with the read-only key.
|
|
11
|
+
<!-- @end -->
|
|
12
|
+
<!-- @if templates -->
|
|
13
|
+
A website with Django, created with
|
|
14
|
+
[DjangoStack](https://github.com/dwarsbit/django-stack): Django renders the
|
|
15
|
+
pages with its templates, from the content the API also serves.
|
|
16
|
+
<!-- @end -->
|
|
5
17
|
|
|
6
18
|
- [Django Headless](https://djangoheadless.org) serves the content API.
|
|
7
19
|
- [Django Content Studio](https://djangocontentstudio.org) is the admin.
|
|
20
|
+
<!-- @if frontend -->
|
|
8
21
|
- [{{frontend_label}}]({{frontend_url}}) renders the site.
|
|
22
|
+
<!-- @end -->
|
|
23
|
+
<!-- @if templates -->
|
|
24
|
+
- [Django templates]({{frontend_url}}) render the site: the `website` app
|
|
25
|
+
in `backend/website/`.
|
|
26
|
+
<!-- @end -->
|
|
9
27
|
- Postgres stores the content, and by default also the cache and the task
|
|
10
28
|
queue (`CACHE_URL` and `TASKS_URL` in `.env`); Valkey can take those over
|
|
11
29
|
with the `valkey` profile.
|
|
@@ -18,7 +36,12 @@ You need Docker.
|
|
|
18
36
|
docker compose up --build
|
|
19
37
|
```
|
|
20
38
|
|
|
39
|
+
<!-- @if site -->
|
|
21
40
|
- Site: http://localhost:{{site_port}}
|
|
41
|
+
<!-- @end -->
|
|
42
|
+
<!-- @if headless -->
|
|
43
|
+
- API: http://localhost:{{site_port}}/api/ (with the `X-Secret-Key` header set to `API_READ_KEY` in `.env`), its OpenAPI schema at `/api/schema`
|
|
44
|
+
<!-- @end -->
|
|
22
45
|
- Admin: http://localhost:{{site_port}}/admin/ (user `admin`, password `ADMIN_PASSWORD` in `.env`)
|
|
23
46
|
- Outgoing mail: http://localhost:{{mail_port}}
|
|
24
47
|
|
|
@@ -26,21 +49,43 @@ The ports are `SITE_PORT` and `MAIL_PORT` in `.env`. When you change
|
|
|
26
49
|
`SITE_PORT`, change `SITE_URL` and `BACKEND_URL` with it.
|
|
27
50
|
|
|
28
51
|
The first start migrates the database, creates the admin user and adds demo
|
|
52
|
+
<!-- @if site -->
|
|
29
53
|
content. Edit a page in the admin, publish, and reload the site.
|
|
54
|
+
<!-- @end -->
|
|
55
|
+
<!-- @if headless -->
|
|
56
|
+
content. Edit a page in the admin, publish, and read it from the API.
|
|
57
|
+
<!-- @end -->
|
|
30
58
|
|
|
31
59
|
## Develop
|
|
32
60
|
|
|
61
|
+
<!-- @if frontend -->
|
|
33
62
|
Both sides reload on save: the backend runs Django's development server and
|
|
34
63
|
the frontend {{frontend_label}}'s, with the source mounted from `backend/` and
|
|
35
64
|
`frontend/`. `AGENTS.md` lists the layout and the commands.
|
|
65
|
+
<!-- @end -->
|
|
66
|
+
<!-- @if headless -->
|
|
67
|
+
The backend reloads on save: it runs Django's development server, with the
|
|
68
|
+
source mounted from `backend/`. `AGENTS.md` lists the layout and the commands.
|
|
69
|
+
<!-- @end -->
|
|
70
|
+
<!-- @if templates -->
|
|
71
|
+
The site reloads on save, templates and CSS included: the backend runs
|
|
72
|
+
Django's development server, with the source mounted from `backend/`. The
|
|
73
|
+
pages are in `backend/website/`; `AGENTS.md` lists the layout and the
|
|
74
|
+
commands.
|
|
75
|
+
<!-- @end -->
|
|
36
76
|
|
|
37
77
|
| Task | Command |
|
|
38
78
|
|------|---------|
|
|
39
79
|
| Run a manage.py command | `docker compose exec backend python manage.py <command>` |
|
|
40
80
|
| Make migrations | `docker compose exec backend python manage.py makemigrations` |
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
81
|
+
<!-- @if frontend -->
|
|
82
|
+
| Regenerate the API types | `{{pm_run}} generate:types` |
|
|
83
|
+
<!-- @end -->
|
|
84
|
+
<!-- @if !frontend -->
|
|
85
|
+
| Write the OpenAPI schema to `backend/openapi.json` | `{{pm_run}} generate:types` |
|
|
86
|
+
<!-- @end -->
|
|
87
|
+
| Run the tests | `{{pm_run}} test` |
|
|
88
|
+
| Check the setup | `{{pm_run}} doctor` |
|
|
44
89
|
|
|
45
90
|
With the `dstack` command installed (`npm i -g @django-stack/cli`),
|
|
46
91
|
`dstack dev`, `dstack check`, `dstack manage <command>` and `dstack types` do
|
|
@@ -53,9 +98,23 @@ Pages and posts have a status. Drafts and posts with a future publish date
|
|
|
53
98
|
are not served by the API. The **Preview** button in the admin opens the
|
|
54
99
|
site with drafts shown, for an hour.
|
|
55
100
|
|
|
101
|
+
<!-- @if frontend -->
|
|
56
102
|
When an editor saves, Django tells the frontend (`REVALIDATE_URL`), which
|
|
57
103
|
empties its page cache, so the change shows on the next visit.
|
|
58
|
-
|
|
104
|
+
<!-- @end -->
|
|
105
|
+
<!-- @if headless -->
|
|
106
|
+
The button opens `/preview` on `SITE_URL`, which your frontend serves; set
|
|
107
|
+
`REVALIDATE_URL` to the endpoint where your frontend empties its page cache,
|
|
108
|
+
and Django calls it when an editor saves. See
|
|
109
|
+
[Preview and revalidation](https://dwarsbit.github.io/django-stack/docs/preview-revalidation).
|
|
110
|
+
<!-- @end -->
|
|
111
|
+
<!-- @if templates -->
|
|
112
|
+
The site keeps each page in Django's cache, and empties it when an editor
|
|
113
|
+
saves, so the change shows on the next visit. In preview the cache is
|
|
114
|
+
skipped.
|
|
115
|
+
<!-- @end -->
|
|
116
|
+
|
|
117
|
+
<!-- @if site -->
|
|
59
118
|
## Analytics
|
|
60
119
|
|
|
61
120
|
When `COMPOSE_PROFILES` in `.env` has `analytics`, the `umami` service
|
|
@@ -64,6 +123,7 @@ counts page views without cookies, and the admin shows them under
|
|
|
64
123
|
page views pass through the site at `/_a/`. To turn it off, remove
|
|
65
124
|
`analytics` from `COMPOSE_PROFILES` and empty `UMAMI_URL`. See
|
|
66
125
|
[Page analytics](https://dwarsbit.github.io/django-stack/docs/analytics).
|
|
126
|
+
<!-- @end -->
|
|
67
127
|
|
|
68
128
|
## Deploy
|
|
69
129
|
|
|
@@ -91,9 +151,17 @@ has every step.
|
|
|
91
151
|
|
|
92
152
|
## Security
|
|
93
153
|
|
|
154
|
+
<!-- @if !templates -->
|
|
94
155
|
- The API answers only requests with a key. The frontend holds the
|
|
95
|
-
read-only key (`API_READ_KEY`) and uses it on the server only
|
|
96
|
-
|
|
156
|
+
read-only key (`API_READ_KEY`) and uses it on the server only, never in
|
|
157
|
+
the browser.
|
|
158
|
+
<!-- @end -->
|
|
159
|
+
<!-- @if templates -->
|
|
160
|
+
- The API answers only requests with a key. The site reads the content
|
|
161
|
+
in Django and needs none; the read-only key (`API_READ_KEY`) is for other
|
|
162
|
+
apps, on their server, never in a browser.
|
|
163
|
+
<!-- @end -->
|
|
164
|
+
- `API_SECRET_KEY` can write: keep it for scripts and never give it to a
|
|
97
165
|
frontend.
|
|
98
166
|
- `.env` holds every secret. It is generated with random values and is not
|
|
99
167
|
committed.
|
|
@@ -9,4 +9,6 @@ Problems in the packages it is built on go to their projects:
|
|
|
9
9
|
- Django Headless: https://github.com/dwarsbit/django-headless/security
|
|
10
10
|
- Django Content Studio: https://github.com/dwarsbit/django-content-studio/security
|
|
11
11
|
- Django: https://www.djangoproject.com/security/
|
|
12
|
+
<!-- @if frontend -->
|
|
12
13
|
- {{frontend_label}}: {{frontend_security}}
|
|
14
|
+
<!-- @end -->
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
# `s3` in development for a local S3 service (see .env). `analytics` runs
|
|
11
11
|
# Umami for page analytics, `valkey` the Valkey service for CACHE_URL or
|
|
12
12
|
# TASKS_URL.
|
|
13
|
+
# @if frontend
|
|
13
14
|
#
|
|
14
15
|
# The frontend only receives the variables it needs: never the full-access
|
|
15
16
|
# API key or the database password.
|
|
17
|
+
# @end
|
|
16
18
|
|
|
17
19
|
name: {{project_name}}
|
|
18
20
|
|
|
@@ -34,6 +36,7 @@ x-logging: &logging
|
|
|
34
36
|
max-size: 10m
|
|
35
37
|
max-file: "5"
|
|
36
38
|
|
|
39
|
+
# @if frontend
|
|
37
40
|
x-frontend-env: &frontend-env
|
|
38
41
|
SITE_URL: ${SITE_URL}
|
|
39
42
|
API_URL: http://backend:8000/api
|
|
@@ -45,6 +48,14 @@ x-frontend-env: &frontend-env
|
|
|
45
48
|
# Page analytics: the tracker loads from /_a/ on the site when set.
|
|
46
49
|
UMAMI_WEBSITE_ID: ${UMAMI_WEBSITE_ID:-}
|
|
47
50
|
UMAMI_URL: ${UMAMI_URL:-}
|
|
51
|
+
# The captcha widget of the contact form, unless CAPTCHA=off.
|
|
52
|
+
CAPTCHA: ${CAPTCHA:-on}
|
|
53
|
+
# Server errors to Sentry with ERRORS=sentry, as the backend's.
|
|
54
|
+
ERRORS: ${ERRORS:-email}
|
|
55
|
+
SENTRY_DSN: ${SENTRY_DSN:-}
|
|
56
|
+
SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production}
|
|
57
|
+
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
|
|
58
|
+
# @end
|
|
48
59
|
|
|
49
60
|
x-backend-depends: &backend-depends
|
|
50
61
|
db:
|
|
@@ -91,7 +102,13 @@ services:
|
|
|
91
102
|
- ./backend:/app
|
|
92
103
|
- media:/app/media
|
|
93
104
|
ports:
|
|
105
|
+
# @if frontend
|
|
94
106
|
- "127.0.0.1:${BACKEND_PORT:-8000}:8000"
|
|
107
|
+
# @end
|
|
108
|
+
# @if !frontend
|
|
109
|
+
# The site is the backend: the API, the admin and the pages.
|
|
110
|
+
- "127.0.0.1:${SITE_PORT:-3000}:8000"
|
|
111
|
+
# @end
|
|
95
112
|
depends_on: *backend-depends
|
|
96
113
|
healthcheck: *backend-health
|
|
97
114
|
logging: *logging
|
|
@@ -117,6 +134,7 @@ services:
|
|
|
117
134
|
logging: *logging
|
|
118
135
|
restart: unless-stopped
|
|
119
136
|
|
|
137
|
+
# @if frontend
|
|
120
138
|
frontend:
|
|
121
139
|
profiles: [dev]
|
|
122
140
|
build:
|
|
@@ -133,6 +151,7 @@ services:
|
|
|
133
151
|
condition: service_healthy
|
|
134
152
|
logging: *logging
|
|
135
153
|
restart: unless-stopped
|
|
154
|
+
# @end
|
|
136
155
|
|
|
137
156
|
mailpit:
|
|
138
157
|
profiles: [dev]
|
|
@@ -217,6 +236,7 @@ services:
|
|
|
217
236
|
logging: *logging
|
|
218
237
|
restart: unless-stopped
|
|
219
238
|
|
|
239
|
+
# @if frontend
|
|
220
240
|
frontend-prod:
|
|
221
241
|
profiles: [prod]
|
|
222
242
|
image: ${FRONTEND_IMAGE:-{{project_name}}-frontend}
|
|
@@ -237,6 +257,7 @@ services:
|
|
|
237
257
|
memory: 512M
|
|
238
258
|
logging: *logging
|
|
239
259
|
restart: unless-stopped
|
|
260
|
+
# @end
|
|
240
261
|
|
|
241
262
|
# The only service with public ports: HTTPS for SITE_URL with a
|
|
242
263
|
# certificate from Let's Encrypt, routing to Django and the frontend.
|
|
@@ -262,8 +283,10 @@ services:
|
|
|
262
283
|
depends_on:
|
|
263
284
|
backend-prod:
|
|
264
285
|
condition: service_healthy
|
|
286
|
+
# @if frontend
|
|
265
287
|
frontend-prod:
|
|
266
288
|
condition: service_healthy
|
|
289
|
+
# @end
|
|
267
290
|
deploy:
|
|
268
291
|
resources:
|
|
269
292
|
limits:
|
|
@@ -306,13 +329,14 @@ services:
|
|
|
306
329
|
|
|
307
330
|
db:
|
|
308
331
|
profiles: [db]
|
|
309
|
-
image: postgres:
|
|
332
|
+
image: postgres:{{postgres_version}}-alpine
|
|
310
333
|
environment:
|
|
311
334
|
POSTGRES_DB: ${POSTGRES_DB:-app}
|
|
312
335
|
POSTGRES_USER: ${POSTGRES_USER:-app}
|
|
313
336
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
314
337
|
volumes:
|
|
315
|
-
|
|
338
|
+
# A new major version needs a dump and a restore: see the backups docs.
|
|
339
|
+
- pgdata:{{postgres_data}}
|
|
316
340
|
healthcheck:
|
|
317
341
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
|
318
342
|
interval: 3s
|
|
@@ -353,6 +377,8 @@ volumes:
|
|
|
353
377
|
media:
|
|
354
378
|
backups:
|
|
355
379
|
s3data:
|
|
380
|
+
# @if frontend
|
|
356
381
|
frontend_node_modules:
|
|
382
|
+
# @end
|
|
357
383
|
caddy_data:
|
|
358
384
|
caddy_config:
|
|
@@ -12,9 +12,20 @@ PROJECT_NAME={{project_name}}
|
|
|
12
12
|
# the Valkey service.
|
|
13
13
|
COMPOSE_PROFILES=dev,db
|
|
14
14
|
|
|
15
|
+
# @if frontend
|
|
15
16
|
# The public URL of the site. In development the frontend's dev server also
|
|
16
17
|
# serves /admin, /api and /media, so both URLs are the frontend's. In
|
|
17
18
|
# production set both to https://your-domain: Caddy gets the certificate.
|
|
19
|
+
# @end
|
|
20
|
+
# @if headless
|
|
21
|
+
# The public URL of the API and the admin. In production set both to
|
|
22
|
+
# https://your-domain: Caddy gets the certificate.
|
|
23
|
+
# @end
|
|
24
|
+
# @if templates
|
|
25
|
+
# The public URL of the site, which Django renders with the API and the
|
|
26
|
+
# admin. In production set both to https://your-domain: Caddy gets the
|
|
27
|
+
# certificate.
|
|
28
|
+
# @end
|
|
18
29
|
SITE_URL=http://localhost:3000
|
|
19
30
|
BACKEND_URL=http://localhost:3000
|
|
20
31
|
|
|
@@ -42,6 +53,10 @@ POSTGRES_DB=app
|
|
|
42
53
|
POSTGRES_USER=app
|
|
43
54
|
POSTGRES_PASSWORD={{secret:postgres}}
|
|
44
55
|
DATABASE_URL=postgres://app:{{secret:postgres}}@db:5432/app
|
|
56
|
+
# Production only: an external Postgres for the server, such as a managed
|
|
57
|
+
# one. `dstack production` makes it DATABASE_URL in .env.production and
|
|
58
|
+
# leaves out `db`; development keeps the bundled Postgres above.
|
|
59
|
+
PRODUCTION_DATABASE_URL=
|
|
45
60
|
|
|
46
61
|
# Cache and task queue: db:// keeps them in tables in the database, so
|
|
47
62
|
# nothing else runs. For the Valkey service add `valkey` to COMPOSE_PROFILES
|
|
@@ -66,7 +81,17 @@ API_READ_KEY={{secret:api_read}}
|
|
|
66
81
|
# Shared between Django and the frontend: preview links and revalidation.
|
|
67
82
|
PREVIEW_SECRET={{secret:preview}}
|
|
68
83
|
REVALIDATE_SECRET={{secret:revalidate}}
|
|
84
|
+
# @if frontend
|
|
69
85
|
REVALIDATE_URL=http://frontend:3000/revalidate
|
|
86
|
+
# @end
|
|
87
|
+
# @if headless
|
|
88
|
+
# Your frontend's endpoint that empties its page cache when content changes.
|
|
89
|
+
REVALIDATE_URL=
|
|
90
|
+
# @end
|
|
91
|
+
# @if templates
|
|
92
|
+
# Empty: Django renders the site and empties its own page cache.
|
|
93
|
+
REVALIDATE_URL=
|
|
94
|
+
# @end
|
|
70
95
|
|
|
71
96
|
# The first admin user, created on the first start.
|
|
72
97
|
ADMIN_USERNAME=admin
|
|
@@ -118,12 +143,24 @@ BACKUP_S3_BUCKET=
|
|
|
118
143
|
# an Uptime Kuma push monitor, which alerts you when the pings stop.
|
|
119
144
|
BACKUP_PING_URL=
|
|
120
145
|
|
|
146
|
+
# The captcha of the contact form: a proof of work the visitor's browser
|
|
147
|
+
# solves, served by Django at /api/cap/, with no third party. CAPTCHA=off
|
|
148
|
+
# turns it off for the backend and the frontend.
|
|
149
|
+
CAPTCHA=on
|
|
150
|
+
|
|
121
151
|
# Error reports (with DJANGO_DEBUG=0): each new error of the backend, the
|
|
122
152
|
# worker or a backup is mailed to ERROR_EMAIL (comma separated) or, when it
|
|
123
153
|
# is empty, to the superusers, and shows in the admin's notifications. The
|
|
124
154
|
# same error is mailed once an hour at most. ERRORS=off turns it off.
|
|
125
155
|
ERRORS=email
|
|
126
156
|
ERROR_EMAIL=
|
|
157
|
+
# ERRORS=sentry sends the errors of the backend, the worker and the
|
|
158
|
+
# frontend's server to Sentry instead: create a Django project on sentry.io
|
|
159
|
+
# and paste its DSN here. SENTRY_TRACES_SAMPLE_RATE (0 to 1) also sends that
|
|
160
|
+
# share of the backend's requests as performance traces.
|
|
161
|
+
SENTRY_DSN=
|
|
162
|
+
SENTRY_ENVIRONMENT=production
|
|
163
|
+
SENTRY_TRACES_SAMPLE_RATE=0
|
|
127
164
|
|
|
128
165
|
# Logs: one JSON object per line in production, plain text in development
|
|
129
166
|
# (LOG_FORMAT=json or plain). LOG_ACCESS=0 turns off Django's access log.
|
|
@@ -12,7 +12,7 @@ src/layouts/Base.astro the page shell: head, menus, footer, preview banner
|
|
|
12
12
|
src/components/ Blocks.astro maps block types to components in components/blocks/
|
|
13
13
|
src/lib/api.ts getContent(): content helpers with the read-only key, drafts in preview
|
|
14
14
|
src/lib/cache.ts the in-memory page cache that /revalidate empties
|
|
15
|
-
src/lib/headless.ts generated API types; do not edit, run `
|
|
15
|
+
src/lib/headless.ts generated API types; do not edit, run `{{pm_run}} generate:types`
|
|
16
16
|
src/middleware.ts page cache, editor-managed redirects, security headers
|
|
17
17
|
astro.config.mjs server output, Content-Security-Policy, the dev proxy to Django
|
|
18
18
|
```
|
|
@@ -39,5 +39,5 @@ Run them in the `frontend` container (`docker compose exec frontend <command>`):
|
|
|
39
39
|
|
|
40
40
|
| Task | Command |
|
|
41
41
|
|------|---------|
|
|
42
|
-
| Type-check | `
|
|
43
|
-
| Test | `
|
|
42
|
+
| Type-check | `{{pm_run}} check` |
|
|
43
|
+
| Test | `{{pm_run}} test` |
|
|
@@ -7,10 +7,13 @@ WORKDIR /app
|
|
|
7
7
|
ENV ASTRO_TELEMETRY_DISABLED=1
|
|
8
8
|
|
|
9
9
|
FROM base AS deps
|
|
10
|
-
|
|
10
|
+
{{pm_setup}}
|
|
11
|
+
COPY {{pm_manifests}} ./
|
|
11
12
|
COPY vendor/ vendor/
|
|
12
13
|
# No BuildKit cache mounts: Railway rejects them, and the layer is cached anyway.
|
|
13
|
-
RUN
|
|
14
|
+
RUN {{pm_install}}
|
|
15
|
+
# The package manager only installs: npm, which comes with Node.js, runs
|
|
16
|
+
# the scripts of package.json below.
|
|
14
17
|
|
|
15
18
|
FROM deps AS dev
|
|
16
19
|
EXPOSE 3000
|
|
@@ -21,7 +24,7 @@ CMD ["sh", "-c", "rm -f .astro/dev.json && exec npm run dev"]
|
|
|
21
24
|
|
|
22
25
|
FROM deps AS build
|
|
23
26
|
COPY . .
|
|
24
|
-
RUN npm run build &&
|
|
27
|
+
RUN npm run build && {{pm_prune}}
|
|
25
28
|
|
|
26
29
|
FROM base AS prod
|
|
27
30
|
ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000
|