@django-stack/cli 0.8.1 → 0.10.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 +840 -54
- package/dist/index.d.ts +43 -4
- package/dist/index.js +1 -1
- package/dist/{production-CGkI7oNe.js → production-Ct_qoeXk.js} +210 -7
- package/package.json +1 -1
- package/templates/backend/Dockerfile +4 -5
- package/templates/backend/config/settings.py +3 -0
- package/templates/backend/config/studio.py +4 -0
- package/templates/backend/content/apps.py +5 -1
- package/templates/backend/content/models.py +2 -2
- package/templates/backend/content/notifications.py +36 -0
- package/templates/backend/content/tests/test_api.py +32 -2
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/AGENTS.md +8 -8
- package/templates/base/Caddyfile +7 -1
- package/templates/base/README.md +3 -3
- package/templates/base/compose.yaml +37 -2
- package/templates/base/env.example +29 -0
- package/templates/frontend-astro/AGENTS.md +3 -3
- package/templates/frontend-astro/Dockerfile +7 -4
- 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 +6 -3
- 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 +6 -3
- 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 +7 -4
- 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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { reportError } from "@django-stack/utils";
|
|
2
|
+
|
|
3
|
+
/** Server errors go to Sentry with ERRORS=sentry; Nuxt logs them as well. */
|
|
4
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
5
|
+
nitroApp.hooks.hook("error", async (error, { event }) => {
|
|
6
|
+
const status = (error as { statusCode?: number }).statusCode ?? 500;
|
|
7
|
+
if (status < 500) return;
|
|
8
|
+
await reportError(error, {
|
|
9
|
+
method: event?.method,
|
|
10
|
+
path: event?.path,
|
|
11
|
+
requestId: event?.headers.get("x-request-id"),
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { captchaEnabled } from "@django-stack/utils";
|
|
2
|
+
|
|
1
3
|
/** What every page shows: the site settings and the menus. */
|
|
2
4
|
export default defineEventHandler(async (event) => {
|
|
3
5
|
const data = await cachedContent(event, async () => {
|
|
@@ -14,5 +16,7 @@ export default defineEventHandler(async (event) => {
|
|
|
14
16
|
preview: previewToken(event) !== null,
|
|
15
17
|
// Page analytics: Umami's tracker, served from this site at /_a/.
|
|
16
18
|
analytics: process.env.UMAMI_WEBSITE_ID || null,
|
|
19
|
+
// The captcha widget of the contact form, unless CAPTCHA=off.
|
|
20
|
+
captcha: captchaEnabled(process.env.CAPTCHA),
|
|
17
21
|
};
|
|
18
22
|
});
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
The contact form. It posts to Django's public create route for form
|
|
3
3
|
submissions (PUBLIC_CREATE in backend/config/settings.py), which is rate
|
|
4
|
-
limited per IP address
|
|
4
|
+
limited per IP address and, unless CAPTCHA=off, needs the token of the
|
|
5
|
+
captcha widget. Submissions appear in the admin.
|
|
5
6
|
-->
|
|
6
7
|
<script setup lang="ts">
|
|
8
|
+
import { resetCaptcha } from "@django-stack/utils";
|
|
9
|
+
import { Captcha } from "@django-stack/vue";
|
|
10
|
+
|
|
11
|
+
const { data: layout } = useLayoutContent();
|
|
7
12
|
const status = ref("");
|
|
8
13
|
const sending = ref(false);
|
|
9
14
|
|
|
@@ -18,8 +23,15 @@ async function submit(event: Event) {
|
|
|
18
23
|
body: JSON.stringify(Object.fromEntries(new FormData(form))),
|
|
19
24
|
});
|
|
20
25
|
if (response.status === 429) throw new Error("Too many messages; try again in a minute.");
|
|
21
|
-
if (!response.ok)
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const errors = await response.json().catch(() => ({}));
|
|
28
|
+
// A token is valid once, so the widget verifies again.
|
|
29
|
+
resetCaptcha(form);
|
|
30
|
+
if (errors.captcha) throw new Error("Verify that you are human, then send the message again.");
|
|
31
|
+
throw new Error("The message could not be sent. Check the fields and try again.");
|
|
32
|
+
}
|
|
22
33
|
form.reset();
|
|
34
|
+
resetCaptcha(form);
|
|
23
35
|
status.value = "Thank you, your message was sent.";
|
|
24
36
|
} catch (error) {
|
|
25
37
|
status.value = error instanceof Error ? error.message : String(error);
|
|
@@ -34,6 +46,7 @@ async function submit(event: Event) {
|
|
|
34
46
|
<label>Name <input name="name" required maxlength="200" autocomplete="name" /></label>
|
|
35
47
|
<label>Email <input name="email" type="email" required autocomplete="email" /></label>
|
|
36
48
|
<label>Message <textarea name="message" required maxlength="5000" rows="6"></textarea></label>
|
|
49
|
+
<Captcha v-if="layout?.captcha !== false" />
|
|
37
50
|
<button class="button" type="submit" :disabled="sending">Send</button>
|
|
38
51
|
<p class="status" role="status" aria-live="polite">{{ status }}</p>
|
|
39
52
|
</form>
|
|
@@ -13,7 +13,7 @@ src/routes/+layout.svelte the page shell: SEO tags, menus, footer, preview ba
|
|
|
13
13
|
src/lib/components/ the post list and the contact form
|
|
14
14
|
src/lib/server/api.ts getContent(): content helpers with the read-only key, drafts in preview
|
|
15
15
|
src/lib/server/cache.ts the in-memory page cache that /revalidate empties
|
|
16
|
-
src/lib/headless.ts generated API types; do not edit, run `
|
|
16
|
+
src/lib/headless.ts generated API types; do not edit, run `{{pm_run}} generate:types`
|
|
17
17
|
src/hooks.server.ts page cache, editor-managed redirects, security headers
|
|
18
18
|
vite.config.ts SvelteKit's options (Node adapter, Content-Security-Policy) and the dev proxy to Django
|
|
19
19
|
```
|
|
@@ -49,5 +49,5 @@ Run them in the `frontend` container (`docker compose exec frontend <command>`):
|
|
|
49
49
|
|
|
50
50
|
| Task | Command |
|
|
51
51
|
|------|---------|
|
|
52
|
-
| Type-check | `
|
|
53
|
-
| Test | `
|
|
52
|
+
| Type-check | `{{pm_run}} check` |
|
|
53
|
+
| Test | `{{pm_run}} test` |
|
|
@@ -6,10 +6,13 @@ FROM node:22-alpine AS base
|
|
|
6
6
|
WORKDIR /app
|
|
7
7
|
|
|
8
8
|
FROM base AS deps
|
|
9
|
-
|
|
9
|
+
{{pm_setup}}
|
|
10
|
+
COPY {{pm_manifests}} ./
|
|
10
11
|
COPY vendor/ vendor/
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
# No BuildKit cache mounts: Railway rejects them, and the layer is cached anyway.
|
|
13
|
+
RUN {{pm_install}}
|
|
14
|
+
# The package manager only installs: npm, which comes with Node.js, runs
|
|
15
|
+
# the scripts of package.json below.
|
|
13
16
|
|
|
14
17
|
FROM deps AS dev
|
|
15
18
|
EXPOSE 3000
|
|
@@ -17,7 +20,7 @@ CMD ["npm", "run", "dev"]
|
|
|
17
20
|
|
|
18
21
|
FROM deps AS build
|
|
19
22
|
COPY . .
|
|
20
|
-
RUN npm run build &&
|
|
23
|
+
RUN npm run build && {{pm_prune}}
|
|
21
24
|
|
|
22
25
|
FROM base AS prod
|
|
23
26
|
# The proxy in front sets X-Forwarded-Proto, so links and cookies use https.
|
|
@@ -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.10.0",
|
|
23
|
+
"@django-stack/utils": "^0.10.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@sveltejs/adapter-node": "^6.0.1",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# pnpm's settings for the frontend (pnpm 11 and later).
|
|
2
|
+
|
|
3
|
+
# Resolve only versions published at least a day ago, so a compromised
|
|
4
|
+
# release is usually caught before it reaches you. DjangoStack's own
|
|
5
|
+
# packages are exempt, so a project made on a release day installs.
|
|
6
|
+
minimumReleaseAge: 1440
|
|
7
|
+
minimumReleaseAgeExclude:
|
|
8
|
+
- "@django-stack/*"
|
|
9
|
+
|
|
10
|
+
# Which dependencies may run install scripts; pnpm refuses others.
|
|
11
|
+
allowBuilds: {}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { reportError } from "@django-stack/utils";
|
|
1
2
|
import { redirect } from "@sveltejs/kit";
|
|
2
|
-
import type { Handle } from "@sveltejs/kit/hooks";
|
|
3
|
+
import type { Handle, HandleServerError } from "@sveltejs/kit/hooks";
|
|
3
4
|
import { getContent, PREVIEW_COOKIE } from "#lib/server/api.ts";
|
|
4
5
|
import { getCached, store } from "#lib/server/cache.ts";
|
|
5
6
|
import { dev } from "$app/env";
|
|
@@ -68,3 +69,14 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
|
68
69
|
}
|
|
69
70
|
return response;
|
|
70
71
|
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Errors of the site's code are logged, as SvelteKit does by default, and
|
|
75
|
+
* go to Sentry with ERRORS=sentry. Errors such as a 404 are not.
|
|
76
|
+
*/
|
|
77
|
+
export const handleError: HandleServerError = async ({ kind, error, issues, event }) => {
|
|
78
|
+
if (kind === "validation") console.error("Remote function schema validation failed:", issues);
|
|
79
|
+
if (kind !== "unknown") return;
|
|
80
|
+
console.error(error instanceof Error ? error.stack : String(error));
|
|
81
|
+
await reportError(error, { request: event.request });
|
|
82
|
+
};
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
The contact form. It posts to Django's public create route for form
|
|
3
3
|
submissions (PUBLIC_CREATE in backend/config/settings.py), which is rate
|
|
4
|
-
limited per IP address
|
|
4
|
+
limited per IP address and, unless CAPTCHA=off, needs the token of the
|
|
5
|
+
captcha widget. Submissions appear in the admin.
|
|
5
6
|
-->
|
|
6
7
|
<script lang="ts">
|
|
8
|
+
import { Captcha } from "@django-stack/svelte";
|
|
9
|
+
import { resetCaptcha } from "@django-stack/utils";
|
|
10
|
+
|
|
11
|
+
let { captcha = true }: { captcha?: boolean } = $props();
|
|
7
12
|
let status = $state("");
|
|
8
13
|
let sending = $state(false);
|
|
9
14
|
|
|
@@ -19,8 +24,15 @@
|
|
|
19
24
|
body: JSON.stringify(Object.fromEntries(new FormData(form))),
|
|
20
25
|
});
|
|
21
26
|
if (response.status === 429) throw new Error("Too many messages; try again in a minute.");
|
|
22
|
-
if (!response.ok)
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const errors = await response.json().catch(() => ({}));
|
|
29
|
+
// A token is valid once, so the widget verifies again.
|
|
30
|
+
resetCaptcha(form);
|
|
31
|
+
if (errors.captcha) throw new Error("Verify that you are human, then send the message again.");
|
|
32
|
+
throw new Error("The message could not be sent. Check the fields and try again.");
|
|
33
|
+
}
|
|
23
34
|
form.reset();
|
|
35
|
+
resetCaptcha(form);
|
|
24
36
|
status = "Thank you, your message was sent.";
|
|
25
37
|
} catch (error) {
|
|
26
38
|
status = error instanceof Error ? error.message : String(error);
|
|
@@ -34,6 +46,7 @@
|
|
|
34
46
|
<label>Name <input name="name" required maxlength="200" autocomplete="name" /></label>
|
|
35
47
|
<label>Email <input name="email" type="email" required autocomplete="email" /></label>
|
|
36
48
|
<label>Message <textarea name="message" required maxlength="5000" rows="6"></textarea></label>
|
|
49
|
+
{#if captcha}<Captcha />{/if}
|
|
37
50
|
<button class="button" type="submit" disabled={sending}>Send</button>
|
|
38
51
|
<p class="status" role="status" aria-live="polite">{status}</p>
|
|
39
52
|
</form>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captchaEnabled } from "@django-stack/utils";
|
|
1
2
|
import { getContent, PREVIEW_COOKIE, SITE_URL } from "#lib/server/api.ts";
|
|
2
3
|
import type { LayoutServerLoad } from "./$types";
|
|
3
4
|
|
|
@@ -16,5 +17,7 @@ export const load: LayoutServerLoad = async ({ cookies }) => {
|
|
|
16
17
|
siteUrl: SITE_URL,
|
|
17
18
|
// Page analytics: Umami's tracker, served from this site at /_a/.
|
|
18
19
|
analytics: process.env.UMAMI_WEBSITE_ID || null,
|
|
20
|
+
// The captcha widget of the contact form, unless CAPTCHA=off.
|
|
21
|
+
captcha: captchaEnabled(process.env.CAPTCHA),
|
|
19
22
|
};
|
|
20
23
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CAPTCHA_CSP } from "@django-stack/utils";
|
|
1
2
|
import adapter from "@sveltejs/adapter-node";
|
|
2
3
|
import { sveltekit } from "@sveltejs/kit/vite";
|
|
3
4
|
import { defineConfig } from "vitest/config";
|
|
@@ -7,6 +8,10 @@ const backend = process.env.BACKEND_INTERNAL_URL ?? "http://localhost:8000";
|
|
|
7
8
|
// Umami, for page analytics: only its tracker and page views, under /_a/.
|
|
8
9
|
const umami = process.env.UMAMI_URL || "http://umami:3000";
|
|
9
10
|
|
|
11
|
+
// SvelteKit takes keyword sources without quotes, such as self.
|
|
12
|
+
const unquoted = (sources: readonly string[]) =>
|
|
13
|
+
sources.map((source) => source.replaceAll("'", "")) as never[];
|
|
14
|
+
|
|
10
15
|
export default defineConfig({
|
|
11
16
|
plugins: [
|
|
12
17
|
sveltekit({
|
|
@@ -15,13 +20,15 @@ export default defineConfig({
|
|
|
15
20
|
// A Content-Security-Policy on every page: scripts and styles only from
|
|
16
21
|
// this site, with hashes for the ones SvelteKit inlines (hashes, not
|
|
17
22
|
// nonces, so cached pages stay valid). src/hooks.server.ts adds the
|
|
18
|
-
// origin of the media files.
|
|
23
|
+
// origin of the media files. CAPTCHA_CSP allows the captcha widget of
|
|
24
|
+
// the contact form: its solver, its style and its workers.
|
|
19
25
|
csp: {
|
|
20
26
|
mode: "hash",
|
|
21
27
|
directives: {
|
|
22
28
|
"default-src": ["self"],
|
|
23
|
-
"script-src": ["self"],
|
|
24
|
-
"style-src": ["self"],
|
|
29
|
+
"script-src": ["self", ...unquoted(CAPTCHA_CSP["script-src"])],
|
|
30
|
+
"style-src": ["self", ...unquoted(CAPTCHA_CSP["style-src"])],
|
|
31
|
+
"worker-src": unquoted(CAPTCHA_CSP["worker-src"]),
|
|
25
32
|
// Inline style attributes, such as the focal point of an image.
|
|
26
33
|
"style-src-attr": ["unsafe-inline"],
|
|
27
34
|
"base-uri": ["self"],
|