@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.
Files changed (78) hide show
  1. package/dist/bin.js +65 -20
  2. package/dist/index.d.ts +24 -9
  3. package/dist/index.js +1 -1
  4. package/dist/{production-M2SsuuBb.js → production-WThC1aS2.js} +267 -30
  5. package/package.json +1 -1
  6. package/templates/backend/Dockerfile +7 -2
  7. package/templates/backend/config/settings.py +13 -0
  8. package/templates/backend/config/urls.py +11 -0
  9. package/templates/backend/content/admin.py +14 -5
  10. package/templates/backend/content/models.py +2 -2
  11. package/templates/backend/content/tests/test_api.py +15 -3
  12. package/templates/backend/pyproject.toml +1 -1
  13. package/templates/base/.github/workflows/deploy.yml +10 -1
  14. package/templates/base/AGENTS.md +86 -8
  15. package/templates/base/Caddyfile +24 -0
  16. package/templates/base/README.md +74 -6
  17. package/templates/base/SECURITY.md +2 -0
  18. package/templates/base/compose.yaml +28 -2
  19. package/templates/base/env.example +37 -0
  20. package/templates/frontend-astro/AGENTS.md +3 -3
  21. package/templates/frontend-astro/Dockerfile +6 -3
  22. package/templates/frontend-astro/astro.config.mjs +16 -5
  23. package/templates/frontend-astro/package.json +1 -1
  24. package/templates/frontend-astro/pnpm-workspace.yaml +17 -0
  25. package/templates/frontend-astro/src/components/ContactForm.astro +19 -3
  26. package/templates/frontend-astro/src/middleware.ts +9 -1
  27. package/templates/frontend-next/AGENTS.md +3 -3
  28. package/templates/frontend-next/Dockerfile +5 -2
  29. package/templates/frontend-next/package.json +3 -3
  30. package/templates/frontend-next/pnpm-workspace.yaml +11 -0
  31. package/templates/frontend-next/src/app/[...path]/page.tsx +2 -1
  32. package/templates/frontend-next/src/components/ContactForm.tsx +14 -3
  33. package/templates/frontend-next/src/instrumentation.ts +12 -0
  34. package/templates/frontend-next/src/proxy.ts +4 -1
  35. package/templates/frontend-nuxt/AGENTS.md +3 -3
  36. package/templates/frontend-nuxt/Dockerfile +5 -2
  37. package/templates/frontend-nuxt/nuxt.config.ts +7 -3
  38. package/templates/frontend-nuxt/package.json +2 -2
  39. package/templates/frontend-nuxt/pnpm-workspace.yaml +13 -0
  40. package/templates/frontend-nuxt/server/plugins/errors.ts +14 -0
  41. package/templates/frontend-nuxt/server/routes/_content/layout.get.ts +4 -0
  42. package/templates/frontend-nuxt/src/components/ContactForm.vue +15 -2
  43. package/templates/frontend-sveltekit/AGENTS.md +3 -3
  44. package/templates/frontend-sveltekit/Dockerfile +6 -3
  45. package/templates/frontend-sveltekit/package.json +2 -2
  46. package/templates/frontend-sveltekit/pnpm-workspace.yaml +11 -0
  47. package/templates/frontend-sveltekit/src/hooks.server.ts +13 -1
  48. package/templates/frontend-sveltekit/src/lib/components/ContactForm.svelte +15 -2
  49. package/templates/frontend-sveltekit/src/routes/+layout.server.ts +3 -0
  50. package/templates/frontend-sveltekit/src/routes/[...path]/+page.svelte +1 -1
  51. package/templates/frontend-sveltekit/vite.config.ts +10 -3
  52. package/templates/frontend-templates/template.json +5 -0
  53. package/templates/frontend-templates/website/__init__.py +0 -0
  54. package/templates/frontend-templates/website/apps.py +6 -0
  55. package/templates/frontend-templates/website/context_processors.py +16 -0
  56. package/templates/frontend-templates/website/forms.py +20 -0
  57. package/templates/frontend-templates/website/static/website/favicon.svg +1 -0
  58. package/templates/frontend-templates/website/static/website/global.css +232 -0
  59. package/templates/frontend-templates/website/templates/website/404.html +10 -0
  60. package/templates/frontend-templates/website/templates/website/base.html +44 -0
  61. package/templates/frontend-templates/website/templates/website/blocks/call_to_action.html +5 -0
  62. package/templates/frontend-templates/website/templates/website/blocks/image.html +4 -0
  63. package/templates/frontend-templates/website/templates/website/blocks/quote.html +4 -0
  64. package/templates/frontend-templates/website/templates/website/blocks/text.html +2 -0
  65. package/templates/frontend-templates/website/templates/website/home.html +8 -0
  66. package/templates/frontend-templates/website/templates/website/includes/contact_form.html +21 -0
  67. package/templates/frontend-templates/website/templates/website/includes/post_card.html +9 -0
  68. package/templates/frontend-templates/website/templates/website/includes/post_list.html +11 -0
  69. package/templates/frontend-templates/website/templates/website/page.html +10 -0
  70. package/templates/frontend-templates/website/templates/website/post.html +20 -0
  71. package/templates/frontend-templates/website/templates/website/post_list.html +11 -0
  72. package/templates/frontend-templates/website/templates/website/robots.txt +5 -0
  73. package/templates/frontend-templates/website/templates/website/rss.xml +16 -0
  74. package/templates/frontend-templates/website/templates/website/sitemap.xml +8 -0
  75. package/templates/frontend-templates/website/tests/__init__.py +0 -0
  76. package/templates/frontend-templates/website/tests/test_site.py +102 -0
  77. package/templates/frontend-templates/website/urls.py +17 -0
  78. 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 `pnpm test` at the project root.
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.9.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
@@ -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 `npm run generate:types`
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 `npm run` shortcuts.
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 `npm run dev`) |
34
- | Run a manage.py command | `docker compose exec backend python manage.py <command>` (or `npm run backend -- <command>`) |
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
- | Regenerate the API types after a model change | `npm run generate:types` |
37
- | Run the tests | `npm test` |
38
- | Lint and type-check | `npm run lint` |
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
- 4. Run `npm run generate:types`, then read it in the frontend as
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 -->
@@ -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
@@ -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
- | Regenerate the API types | `npm run generate:types` |
42
- | Run the tests | `npm test` |
43
- | Check the setup | `npm run doctor` |
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
- - `API_SECRET_KEY` can write: keep it for scripts and never give it to the
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:17-alpine
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
- - pgdata:/var/lib/postgresql/data
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 `npm run generate:types`
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 | `npm run check` |
43
- | Test | `npm 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
- COPY package.json package-lock.json* ./
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 if [ -f package-lock.json ]; then npm ci; else npm install; fi
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 && npm prune --omit=dev
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