@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.
- package/dist/bin.js +38 -18
- package/dist/index.d.ts +11 -4
- package/dist/index.js +1 -1
- package/dist/{production-Ct_qoeXk.js → production-WThC1aS2.js} +115 -23
- package/package.json +1 -1
- package/templates/backend/Dockerfile +7 -2
- package/templates/backend/config/settings.py +12 -0
- package/templates/backend/config/urls.py +11 -0
- package/templates/backend/content/admin.py +14 -5
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/.github/workflows/deploy.yml +10 -1
- package/templates/base/AGENTS.md +78 -0
- package/templates/base/Caddyfile +24 -0
- package/templates/base/README.md +71 -3
- package/templates/base/SECURITY.md +2 -0
- package/templates/base/compose.yaml +21 -2
- package/templates/base/env.example +25 -0
- package/templates/frontend-astro/package.json +1 -1
- package/templates/frontend-next/package.json +3 -3
- package/templates/frontend-nuxt/package.json +2 -2
- package/templates/frontend-sveltekit/package.json +2 -2
- 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
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
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 -->
|
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,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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@django-headless/client": "^1.1.1",
|
|
16
|
-
"@django-stack/next": "^0.
|
|
17
|
-
"@django-stack/react": "^0.
|
|
18
|
-
"@django-stack/utils": "^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.
|
|
19
|
-
"@django-stack/vue": "^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.
|
|
23
|
-
"@django-stack/utils": "^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",
|
|
File without changes
|
|
@@ -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>
|