@django-stack/cli 0.10.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 (48) hide show
  1. package/dist/bin.js +38 -18
  2. package/dist/index.d.ts +11 -4
  3. package/dist/index.js +1 -1
  4. package/dist/{production-Ct_qoeXk.js → production-WThC1aS2.js} +115 -23
  5. package/package.json +1 -1
  6. package/templates/backend/Dockerfile +7 -2
  7. package/templates/backend/config/settings.py +12 -0
  8. package/templates/backend/config/urls.py +11 -0
  9. package/templates/backend/content/admin.py +14 -5
  10. package/templates/backend/pyproject.toml +1 -1
  11. package/templates/base/.github/workflows/deploy.yml +10 -1
  12. package/templates/base/AGENTS.md +78 -0
  13. package/templates/base/Caddyfile +24 -0
  14. package/templates/base/README.md +71 -3
  15. package/templates/base/SECURITY.md +2 -0
  16. package/templates/base/compose.yaml +21 -2
  17. package/templates/base/env.example +25 -0
  18. package/templates/frontend-astro/package.json +1 -1
  19. package/templates/frontend-next/package.json +3 -3
  20. package/templates/frontend-nuxt/package.json +2 -2
  21. package/templates/frontend-sveltekit/package.json +2 -2
  22. package/templates/frontend-templates/template.json +5 -0
  23. package/templates/frontend-templates/website/__init__.py +0 -0
  24. package/templates/frontend-templates/website/apps.py +6 -0
  25. package/templates/frontend-templates/website/context_processors.py +16 -0
  26. package/templates/frontend-templates/website/forms.py +20 -0
  27. package/templates/frontend-templates/website/static/website/favicon.svg +1 -0
  28. package/templates/frontend-templates/website/static/website/global.css +232 -0
  29. package/templates/frontend-templates/website/templates/website/404.html +10 -0
  30. package/templates/frontend-templates/website/templates/website/base.html +44 -0
  31. package/templates/frontend-templates/website/templates/website/blocks/call_to_action.html +5 -0
  32. package/templates/frontend-templates/website/templates/website/blocks/image.html +4 -0
  33. package/templates/frontend-templates/website/templates/website/blocks/quote.html +4 -0
  34. package/templates/frontend-templates/website/templates/website/blocks/text.html +2 -0
  35. package/templates/frontend-templates/website/templates/website/home.html +8 -0
  36. package/templates/frontend-templates/website/templates/website/includes/contact_form.html +21 -0
  37. package/templates/frontend-templates/website/templates/website/includes/post_card.html +9 -0
  38. package/templates/frontend-templates/website/templates/website/includes/post_list.html +11 -0
  39. package/templates/frontend-templates/website/templates/website/page.html +10 -0
  40. package/templates/frontend-templates/website/templates/website/post.html +20 -0
  41. package/templates/frontend-templates/website/templates/website/post_list.html +11 -0
  42. package/templates/frontend-templates/website/templates/website/robots.txt +5 -0
  43. package/templates/frontend-templates/website/templates/website/rss.xml +16 -0
  44. package/templates/frontend-templates/website/templates/website/sitemap.xml +8 -0
  45. package/templates/frontend-templates/website/tests/__init__.py +0 -0
  46. package/templates/frontend-templates/website/tests/test_site.py +102 -0
  47. package/templates/frontend-templates/website/urls.py +17 -0
  48. package/templates/frontend-templates/website/views.py +176 -0
@@ -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
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
@@ -33,16 +57,38 @@ Run them at the project root. They need Docker; Node is only needed for the `{{p
33
57
  | Start everything | `docker compose up --build` (or `{{pm_run}} dev`) |
34
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` |
60
+ <!-- @if frontend -->
36
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 -->
37
66
  | Run the tests | `{{pm_run}} test` |
67
+ <!-- @if frontend -->
38
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`.
103
+ <!-- @if frontend -->
57
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,19 +49,41 @@ 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` |
81
+ <!-- @if frontend -->
41
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 -->
42
87
  | Run the tests | `{{pm_run}} test` |
43
88
  | Check the setup | `{{pm_run}} doctor` |
44
89
 
@@ -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
@@ -52,6 +55,7 @@ x-frontend-env: &frontend-env
52
55
  SENTRY_DSN: ${SENTRY_DSN:-}
53
56
  SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production}
54
57
  SENTRY_RELEASE: ${SENTRY_RELEASE:-}
58
+ # @end
55
59
 
56
60
  x-backend-depends: &backend-depends
57
61
  db:
@@ -98,7 +102,13 @@ services:
98
102
  - ./backend:/app
99
103
  - media:/app/media
100
104
  ports:
105
+ # @if frontend
101
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
102
112
  depends_on: *backend-depends
103
113
  healthcheck: *backend-health
104
114
  logging: *logging
@@ -124,6 +134,7 @@ services:
124
134
  logging: *logging
125
135
  restart: unless-stopped
126
136
 
137
+ # @if frontend
127
138
  frontend:
128
139
  profiles: [dev]
129
140
  build:
@@ -140,6 +151,7 @@ services:
140
151
  condition: service_healthy
141
152
  logging: *logging
142
153
  restart: unless-stopped
154
+ # @end
143
155
 
144
156
  mailpit:
145
157
  profiles: [dev]
@@ -224,6 +236,7 @@ services:
224
236
  logging: *logging
225
237
  restart: unless-stopped
226
238
 
239
+ # @if frontend
227
240
  frontend-prod:
228
241
  profiles: [prod]
229
242
  image: ${FRONTEND_IMAGE:-{{project_name}}-frontend}
@@ -244,6 +257,7 @@ services:
244
257
  memory: 512M
245
258
  logging: *logging
246
259
  restart: unless-stopped
260
+ # @end
247
261
 
248
262
  # The only service with public ports: HTTPS for SITE_URL with a
249
263
  # certificate from Let's Encrypt, routing to Django and the frontend.
@@ -269,8 +283,10 @@ services:
269
283
  depends_on:
270
284
  backend-prod:
271
285
  condition: service_healthy
286
+ # @if frontend
272
287
  frontend-prod:
273
288
  condition: service_healthy
289
+ # @end
274
290
  deploy:
275
291
  resources:
276
292
  limits:
@@ -313,13 +329,14 @@ services:
313
329
 
314
330
  db:
315
331
  profiles: [db]
316
- image: postgres:17-alpine
332
+ image: postgres:{{postgres_version}}-alpine
317
333
  environment:
318
334
  POSTGRES_DB: ${POSTGRES_DB:-app}
319
335
  POSTGRES_USER: ${POSTGRES_USER:-app}
320
336
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
321
337
  volumes:
322
- - pgdata:/var/lib/postgresql/data
338
+ # A new major version needs a dump and a restore: see the backups docs.
339
+ - pgdata:{{postgres_data}}
323
340
  healthcheck:
324
341
  test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
325
342
  interval: 3s
@@ -360,6 +377,8 @@ volumes:
360
377
  media:
361
378
  backups:
362
379
  s3data:
380
+ # @if frontend
363
381
  frontend_node_modules:
382
+ # @end
364
383
  caddy_data:
365
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
@@ -16,7 +16,7 @@
16
16
  "@astrojs/node": "^11.1.9",
17
17
  "@astrojs/rss": "^4.0.19",
18
18
  "@django-headless/client": "^1.1.1",
19
- "@django-stack/utils": "^0.10.0",
19
+ "@django-stack/utils": "^0.11.0",
20
20
  "astro": "^7.4.5"
21
21
  },
22
22
  "devDependencies": {
@@ -13,9 +13,9 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@django-headless/client": "^1.1.1",
16
- "@django-stack/next": "^0.10.0",
17
- "@django-stack/react": "^0.10.0",
18
- "@django-stack/utils": "^0.10.0",
16
+ "@django-stack/next": "^0.11.0",
17
+ "@django-stack/react": "^0.11.0",
18
+ "@django-stack/utils": "^0.11.0",
19
19
  "next": "^16.4.4",
20
20
  "react": "^19.3.1",
21
21
  "react-dom": "^19.3.1",
@@ -15,8 +15,8 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@django-headless/client": "^1.1.1",
18
- "@django-stack/utils": "^0.10.0",
19
- "@django-stack/vue": "^0.10.0",
18
+ "@django-stack/utils": "^0.11.0",
19
+ "@django-stack/vue": "^0.11.0",
20
20
  "nuxt": "^4.6.3",
21
21
  "nuxt-security": "^2.7.0",
22
22
  "vue": "^3.5.44",
@@ -19,8 +19,8 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@django-headless/client": "^1.1.1",
22
- "@django-stack/svelte": "^0.10.0",
23
- "@django-stack/utils": "^0.10.0"
22
+ "@django-stack/svelte": "^0.11.0",
23
+ "@django-stack/utils": "^0.11.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@sveltejs/adapter-node": "^6.0.1",
@@ -0,0 +1,5 @@
1
+ {
2
+ "label": "Django templates",
3
+ "url": "https://docs.djangoproject.com/en/6.1/topics/templates/",
4
+ "security": "https://www.djangoproject.com/security/"
5
+ }
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class WebsiteConfig(AppConfig):
5
+ name = "website"
6
+ verbose_name = "Website"
@@ -0,0 +1,16 @@
1
+ from django.utils.functional import SimpleLazyObject
2
+
3
+ from content.models import Menu, SiteSettings
4
+
5
+
6
+ def menu(slug):
7
+ return Menu.objects.filter(slug=slug).first()
8
+
9
+
10
+ def site(request):
11
+ """The site settings and the menus, for base.html; read only when a template uses them."""
12
+ return {
13
+ "site": SimpleLazyObject(lambda: SiteSettings.objects.select_related("home_page").first()),
14
+ "main_menu": SimpleLazyObject(lambda: menu("main")),
15
+ "footer_menu": SimpleLazyObject(lambda: menu("footer")),
16
+ }
@@ -0,0 +1,20 @@
1
+ from django import forms
2
+ from django_stack.contrib.captcha.forms import CaptchaField
3
+
4
+ from content.models import FormSubmission
5
+
6
+
7
+ class ContactForm(forms.ModelForm):
8
+ """The contact form; submissions appear in the admin under Form submissions."""
9
+
10
+ # The widget of the captcha; CAPTCHA=off leaves it out.
11
+ captcha = CaptchaField(label="")
12
+
13
+ class Meta:
14
+ model = FormSubmission
15
+ fields = ["name", "email", "message"]
16
+ widgets = {
17
+ "name": forms.TextInput(attrs={"autocomplete": "name"}),
18
+ "email": forms.EmailInput(attrs={"autocomplete": "email"}),
19
+ "message": forms.Textarea(attrs={"rows": 6}),
20
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#0c4b33"/><path d="M8 9h10a6 6 0 0 1 0 12h-4v3H8z" fill="#44b78b"/><path d="M14 13h4a2 2 0 0 1 0 4h-4z" fill="#0c4b33"/></svg>