@django-stack/cli 0.3.0 → 0.5.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 +3 -3
- package/dist/index.d.ts +52 -40
- package/dist/index.js +1 -1
- package/dist/{stack-CTyqYWCV.js → scaffold-DbgGYEsc.js} +216 -183
- package/package.json +1 -1
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/AGENTS.md +7 -11
- package/templates/base/README.md +3 -3
- package/templates/base/SECURITY.md +1 -1
- package/templates/base/gitignore +6 -0
- package/templates/base/package.json +1 -1
- package/templates/frontend-astro/AGENTS.md +43 -0
- package/templates/frontend-astro/package.json +1 -1
- package/templates/frontend-astro/template.json +5 -0
- package/templates/frontend-next/.dockerignore +3 -0
- package/templates/frontend-next/AGENTS.md +59 -0
- package/templates/frontend-next/Dockerfile +32 -0
- package/templates/frontend-next/next.config.ts +19 -0
- package/templates/frontend-next/package.json +34 -0
- package/templates/frontend-next/public/favicon.svg +1 -0
- package/templates/frontend-next/src/app/[...path]/page.tsx +28 -0
- package/templates/frontend-next/src/app/blog/[slug]/page.tsx +60 -0
- package/templates/frontend-next/src/app/blog/page.tsx +20 -0
- package/templates/frontend-next/src/app/category/[slug]/page.tsx +34 -0
- package/templates/frontend-next/src/app/error.tsx +13 -0
- package/templates/frontend-next/src/app/globals.css +250 -0
- package/templates/frontend-next/src/app/layout.tsx +70 -0
- package/templates/frontend-next/src/app/not-found.tsx +15 -0
- package/templates/frontend-next/src/app/page.tsx +35 -0
- package/templates/frontend-next/src/app/preview/exit/route.ts +3 -0
- package/templates/frontend-next/src/app/preview/route.ts +4 -0
- package/templates/frontend-next/src/app/revalidate/route.ts +8 -0
- package/templates/frontend-next/src/app/robots.txt/route.ts +13 -0
- package/templates/frontend-next/src/app/rss.xml/route.ts +19 -0
- package/templates/frontend-next/src/app/sitemap.xml/route.ts +28 -0
- package/templates/frontend-next/src/app/tag/[slug]/page.tsx +29 -0
- package/templates/frontend-next/src/components/ContactForm.tsx +55 -0
- package/templates/frontend-next/src/components/NavLink.tsx +15 -0
- package/templates/frontend-next/src/components/PostCard.tsx +25 -0
- package/templates/frontend-next/src/components/PostList.tsx +28 -0
- package/templates/frontend-next/src/lib/api.ts +71 -0
- package/templates/frontend-next/src/lib/headless.ts +344 -0
- package/templates/frontend-next/src/lib/rss.ts +44 -0
- package/templates/frontend-next/src/proxy.ts +34 -0
- package/templates/frontend-next/template.json +5 -0
- package/templates/frontend-next/test/rss.test.ts +17 -0
- package/templates/frontend-next/tsconfig.json +42 -0
- package/templates/frontend-next/vendor/.gitkeep +0 -0
- package/templates/frontend-next/vitest.config.ts +7 -0
- package/templates/frontend-nuxt/.dockerignore +4 -0
- package/templates/frontend-nuxt/AGENTS.md +64 -0
- package/templates/frontend-nuxt/Dockerfile +32 -0
- package/templates/frontend-nuxt/nuxt.config.ts +69 -0
- package/templates/frontend-nuxt/package.json +34 -0
- package/templates/frontend-nuxt/public/favicon.svg +1 -0
- package/templates/frontend-nuxt/server/plugins/media-origins.ts +27 -0
- package/templates/frontend-nuxt/server/routes/_content/category/[slug].get.ts +13 -0
- package/templates/frontend-nuxt/server/routes/_content/home.get.ts +10 -0
- package/templates/frontend-nuxt/server/routes/_content/layout.get.ts +13 -0
- package/templates/frontend-nuxt/server/routes/_content/page.get.ts +7 -0
- package/templates/frontend-nuxt/server/routes/_content/post/[slug].get.ts +7 -0
- package/templates/frontend-nuxt/server/routes/_content/posts.get.ts +4 -0
- package/templates/frontend-nuxt/server/routes/_content/redirect.get.ts +7 -0
- package/templates/frontend-nuxt/server/routes/_content/tag/[slug].get.ts +13 -0
- package/templates/frontend-nuxt/server/routes/preview/exit.get.ts +4 -0
- package/templates/frontend-nuxt/server/routes/preview/index.get.ts +26 -0
- package/templates/frontend-nuxt/server/routes/revalidate.post.ts +15 -0
- package/templates/frontend-nuxt/server/routes/robots.txt.get.ts +4 -0
- package/templates/frontend-nuxt/server/routes/rss.xml.get.ts +19 -0
- package/templates/frontend-nuxt/server/routes/sitemap.xml.get.ts +29 -0
- package/templates/frontend-nuxt/server/utils/cache.ts +48 -0
- package/templates/frontend-nuxt/server/utils/content.ts +41 -0
- package/templates/frontend-nuxt/server/utils/rss.ts +44 -0
- package/templates/frontend-nuxt/src/app.vue +5 -0
- package/templates/frontend-nuxt/src/assets/main.css +126 -0
- package/templates/frontend-nuxt/src/components/ContactForm.vue +66 -0
- package/templates/frontend-nuxt/src/components/PostCard.vue +43 -0
- package/templates/frontend-nuxt/src/components/PostList.vue +33 -0
- package/templates/frontend-nuxt/src/composables/useContent.ts +27 -0
- package/templates/frontend-nuxt/src/composables/useLayoutContent.ts +4 -0
- package/templates/frontend-nuxt/src/composables/usePageSeo.ts +33 -0
- package/templates/frontend-nuxt/src/error.vue +23 -0
- package/templates/frontend-nuxt/src/layouts/default.vue +99 -0
- package/templates/frontend-nuxt/src/lib/headless.ts +344 -0
- package/templates/frontend-nuxt/src/pages/[...path].vue +16 -0
- package/templates/frontend-nuxt/src/pages/blog/[slug].vue +43 -0
- package/templates/frontend-nuxt/src/pages/blog/index.vue +12 -0
- package/templates/frontend-nuxt/src/pages/category/[slug].vue +14 -0
- package/templates/frontend-nuxt/src/pages/index.vue +18 -0
- package/templates/frontend-nuxt/src/pages/tag/[slug].vue +13 -0
- package/templates/frontend-nuxt/template.json +5 -0
- package/templates/frontend-nuxt/test/cache.test.ts +20 -0
- package/templates/frontend-nuxt/test/rss.test.ts +17 -0
- package/templates/frontend-nuxt/tsconfig.json +9 -0
- package/templates/frontend-nuxt/vendor/.gitkeep +0 -0
- package/templates/frontend-nuxt/vitest.config.ts +7 -0
- package/templates/frontend-sveltekit/.dockerignore +4 -0
- package/templates/frontend-sveltekit/AGENTS.md +53 -0
- package/templates/frontend-sveltekit/Dockerfile +32 -0
- package/templates/frontend-sveltekit/package.json +39 -0
- package/templates/frontend-sveltekit/src/app.css +126 -0
- package/templates/frontend-sveltekit/src/app.d.ts +12 -0
- package/templates/frontend-sveltekit/src/app.html +12 -0
- package/templates/frontend-sveltekit/src/hooks.server.ts +70 -0
- package/templates/frontend-sveltekit/src/lib/components/ContactForm.svelte +65 -0
- package/templates/frontend-sveltekit/src/lib/components/PostCard.svelte +41 -0
- package/templates/frontend-sveltekit/src/lib/components/PostList.svelte +39 -0
- package/templates/frontend-sveltekit/src/lib/headless.ts +344 -0
- package/templates/frontend-sveltekit/src/lib/server/api.ts +28 -0
- package/templates/frontend-sveltekit/src/lib/server/cache.ts +58 -0
- package/templates/frontend-sveltekit/src/lib/server/rss.ts +44 -0
- package/templates/frontend-sveltekit/src/routes/+error.svelte +14 -0
- package/templates/frontend-sveltekit/src/routes/+layout.server.ts +18 -0
- package/templates/frontend-sveltekit/src/routes/+layout.svelte +112 -0
- package/templates/frontend-sveltekit/src/routes/+page.server.ts +10 -0
- package/templates/frontend-sveltekit/src/routes/+page.svelte +19 -0
- package/templates/frontend-sveltekit/src/routes/[...path]/+page.server.ts +9 -0
- package/templates/frontend-sveltekit/src/routes/[...path]/+page.svelte +12 -0
- package/templates/frontend-sveltekit/src/routes/blog/+page.server.ts +7 -0
- package/templates/frontend-sveltekit/src/routes/blog/+page.svelte +10 -0
- package/templates/frontend-sveltekit/src/routes/blog/[slug]/+page.server.ts +9 -0
- package/templates/frontend-sveltekit/src/routes/blog/[slug]/+page.svelte +41 -0
- package/templates/frontend-sveltekit/src/routes/category/[slug]/+page.server.ts +11 -0
- package/templates/frontend-sveltekit/src/routes/category/[slug]/+page.svelte +12 -0
- package/templates/frontend-sveltekit/src/routes/preview/+server.ts +27 -0
- package/templates/frontend-sveltekit/src/routes/preview/exit/+server.ts +8 -0
- package/templates/frontend-sveltekit/src/routes/revalidate/+server.ts +16 -0
- package/templates/frontend-sveltekit/src/routes/robots.txt/+server.ts +7 -0
- package/templates/frontend-sveltekit/src/routes/rss.xml/+server.ts +20 -0
- package/templates/frontend-sveltekit/src/routes/sitemap.xml/+server.ts +29 -0
- package/templates/frontend-sveltekit/src/routes/tag/[slug]/+page.server.ts +11 -0
- package/templates/frontend-sveltekit/src/routes/tag/[slug]/+page.svelte +11 -0
- package/templates/frontend-sveltekit/static/favicon.svg +1 -0
- package/templates/frontend-sveltekit/template.json +5 -0
- package/templates/frontend-sveltekit/test/cache.test.ts +22 -0
- package/templates/frontend-sveltekit/test/rss.test.ts +17 -0
- package/templates/frontend-sveltekit/tsconfig.json +5 -0
- package/templates/frontend-sveltekit/vendor/.gitkeep +0 -0
- package/templates/frontend-sveltekit/vite.config.ts +47 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { error } from "@sveltejs/kit";
|
|
2
|
+
import { getContent } from "#lib/server/api.ts";
|
|
3
|
+
import type { PageServerLoad } from "./$types";
|
|
4
|
+
|
|
5
|
+
export const load: PageServerLoad = async ({ params, cookies }) => {
|
|
6
|
+
const post = await getContent(cookies).getPost(params.slug);
|
|
7
|
+
if (!post) error(404, "Page not found");
|
|
8
|
+
return { post, seo: post, seoType: "article" as const };
|
|
9
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Blocks, Image } from "@django-stack/svelte";
|
|
3
|
+
import type { Author, Taxonomy } from "@django-stack/utils";
|
|
4
|
+
|
|
5
|
+
let { data } = $props();
|
|
6
|
+
const post = $derived(data.post);
|
|
7
|
+
const author = $derived(typeof post.author === "object" ? (post.author as Author | null) : null);
|
|
8
|
+
const categories = $derived(post.categories.filter((item): item is Taxonomy => typeof item === "object"));
|
|
9
|
+
const tags = $derived(post.tags.filter((item): item is Taxonomy => typeof item === "object"));
|
|
10
|
+
const date = $derived(post.published_at ? new Date(post.published_at) : null);
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<article class="narrow">
|
|
14
|
+
<p class="muted">
|
|
15
|
+
{#if date}<time datetime={date.toISOString()}>{date.toLocaleDateString("en", { dateStyle: "long" })}</time>{/if}
|
|
16
|
+
{#if author}<span> · {author.name}</span>{/if}
|
|
17
|
+
{#each categories as category (category.slug)}
|
|
18
|
+
<span> · <a href="/category/{category.slug}">{category.name}</a></span>
|
|
19
|
+
{/each}
|
|
20
|
+
</p>
|
|
21
|
+
<h1>{post.title}</h1>
|
|
22
|
+
<Image media={post.cover} size="content" sizes="(min-width: 48rem) 44rem, 100vw" class="cover" eager />
|
|
23
|
+
<Blocks blocks={post.body} />
|
|
24
|
+
{#if tags.length > 0}
|
|
25
|
+
<p class="tags">
|
|
26
|
+
{#each tags as tag (tag.slug)}<a href="/tag/{tag.slug}">#{tag.name}</a>{/each}
|
|
27
|
+
</p>
|
|
28
|
+
{/if}
|
|
29
|
+
</article>
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
article :global(.cover) {
|
|
33
|
+
width: 100%;
|
|
34
|
+
border-radius: 0.75rem;
|
|
35
|
+
}
|
|
36
|
+
.tags {
|
|
37
|
+
display: flex;
|
|
38
|
+
gap: 1rem;
|
|
39
|
+
margin-top: 3rem;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { error } from "@sveltejs/kit";
|
|
2
|
+
import { getContent, pageNumber } from "#lib/server/api.ts";
|
|
3
|
+
import type { PageServerLoad } from "./$types";
|
|
4
|
+
|
|
5
|
+
export const load: PageServerLoad = async ({ params, cookies, url }) => {
|
|
6
|
+
const content = getContent(cookies);
|
|
7
|
+
const category = await content.getCategory(params.slug);
|
|
8
|
+
if (!category) error(404, "Page not found");
|
|
9
|
+
const result = await content.getPosts({ category: params.slug, page: pageNumber(url), limit: 9 });
|
|
10
|
+
return { category, result, seo: { title: category.name, seo_description: category.description ?? "" } };
|
|
11
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import PostList from "#lib/components/PostList.svelte";
|
|
3
|
+
|
|
4
|
+
let { data } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<section class="container">
|
|
8
|
+
<p class="muted">Category</p>
|
|
9
|
+
<h1>{data.category.name}</h1>
|
|
10
|
+
{#if data.category.description}<p>{data.category.description}</p>{/if}
|
|
11
|
+
<PostList result={data.result} base="/category/{data.category.slug}" />
|
|
12
|
+
</section>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { verifyPreviewToken } from "@django-stack/utils";
|
|
2
|
+
import { redirect } from "@sveltejs/kit";
|
|
3
|
+
import { PREVIEW_COOKIE } from "#lib/server/api.ts";
|
|
4
|
+
import type { RequestHandler } from "./$types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Opens a page with drafts shown. The admin's Preview button links here with
|
|
8
|
+
* a token Django signed with PREVIEW_SECRET; the token is kept in a cookie
|
|
9
|
+
* and sent to the API with every request until it expires.
|
|
10
|
+
*/
|
|
11
|
+
export const GET: RequestHandler = async ({ url, cookies }) => {
|
|
12
|
+
const token = url.searchParams.get("token");
|
|
13
|
+
if (!(await verifyPreviewToken(process.env.PREVIEW_SECRET, token))) {
|
|
14
|
+
return new Response("The preview link is invalid or has expired.", { status: 401 });
|
|
15
|
+
}
|
|
16
|
+
const expires = Number(token?.split(".")[0]);
|
|
17
|
+
cookies.set(PREVIEW_COOKIE, token ?? "", {
|
|
18
|
+
path: "/",
|
|
19
|
+
httpOnly: true,
|
|
20
|
+
sameSite: "lax",
|
|
21
|
+
secure: url.protocol === "https:",
|
|
22
|
+
expires: new Date(expires * 1000),
|
|
23
|
+
});
|
|
24
|
+
const path = url.searchParams.get("path") ?? "/";
|
|
25
|
+
// Only paths on this site, never another origin.
|
|
26
|
+
redirect(302, path.startsWith("/") && !path.startsWith("//") ? path : "/");
|
|
27
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { redirect } from "@sveltejs/kit";
|
|
2
|
+
import { PREVIEW_COOKIE } from "#lib/server/api.ts";
|
|
3
|
+
import type { RequestHandler } from "./$types";
|
|
4
|
+
|
|
5
|
+
export const GET: RequestHandler = ({ cookies }) => {
|
|
6
|
+
cookies.delete(PREVIEW_COOKIE, { path: "/" });
|
|
7
|
+
redirect(302, "/");
|
|
8
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { verifyWebhook } from "@django-stack/utils";
|
|
2
|
+
import { json } from "@sveltejs/kit";
|
|
3
|
+
import { purgeAll } from "#lib/server/cache.ts";
|
|
4
|
+
import type { RequestHandler } from "./$types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Django calls this after editors change content (REVALIDATE_URL), signed
|
|
8
|
+
* with REVALIDATE_SECRET. It empties the page cache, so the next visit
|
|
9
|
+
* renders the new content.
|
|
10
|
+
*/
|
|
11
|
+
export const POST: RequestHandler = async ({ request }) => {
|
|
12
|
+
const { valid, payload } = await verifyWebhook(request, process.env.REVALIDATE_SECRET);
|
|
13
|
+
if (!valid) return json({ detail: "Invalid signature." }, { status: 401 });
|
|
14
|
+
const purged = purgeAll();
|
|
15
|
+
return json({ purged, changes: payload?.changes.length ?? 0 });
|
|
16
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SITE_URL } from "#lib/server/api.ts";
|
|
2
|
+
import type { RequestHandler } from "./$types";
|
|
3
|
+
|
|
4
|
+
export const GET: RequestHandler = () =>
|
|
5
|
+
new Response(`User-agent: *\nDisallow: /admin/\nDisallow: /preview\n\nSitemap: ${SITE_URL}/sitemap.xml\n`, {
|
|
6
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
7
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getContent, SITE_URL } from "#lib/server/api.ts";
|
|
2
|
+
import { rssFeed } from "#lib/server/rss.ts";
|
|
3
|
+
import type { RequestHandler } from "./$types";
|
|
4
|
+
|
|
5
|
+
export const GET: RequestHandler = async ({ cookies }) => {
|
|
6
|
+
const content = getContent(cookies);
|
|
7
|
+
const [settings, posts] = await Promise.all([content.getSiteSettings(), content.getPosts({ limit: 20 })]);
|
|
8
|
+
const body = rssFeed({
|
|
9
|
+
title: settings?.site_name ?? "Blog",
|
|
10
|
+
description: settings?.seo_description || settings?.tagline || "",
|
|
11
|
+
site: SITE_URL,
|
|
12
|
+
items: posts.data.map((post) => ({
|
|
13
|
+
title: post.title,
|
|
14
|
+
link: `/blog/${post.slug}`,
|
|
15
|
+
description: post.excerpt,
|
|
16
|
+
pubDate: post.published_at ? new Date(post.published_at) : null,
|
|
17
|
+
})),
|
|
18
|
+
});
|
|
19
|
+
return new Response(body, { headers: { "Content-Type": "application/rss+xml; charset=utf-8" } });
|
|
20
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getContent, SITE_URL } from "#lib/server/api.ts";
|
|
2
|
+
import type { RequestHandler } from "./$types";
|
|
3
|
+
|
|
4
|
+
export const GET: RequestHandler = async ({ cookies }) => {
|
|
5
|
+
const content = getContent(cookies);
|
|
6
|
+
const [pages, posts, settings] = await Promise.all([
|
|
7
|
+
content.getAllPages(),
|
|
8
|
+
content.getAllPosts(),
|
|
9
|
+
content.getSiteSettings(),
|
|
10
|
+
]);
|
|
11
|
+
const urls = [
|
|
12
|
+
{ loc: "/", lastmod: null },
|
|
13
|
+
{ loc: "/blog", lastmod: null },
|
|
14
|
+
...pages
|
|
15
|
+
.filter((page) => page.id !== settings?.home_page)
|
|
16
|
+
.map((page) => ({ loc: `/${page.path}`, lastmod: page.published_at })),
|
|
17
|
+
...posts.map((post) => ({ loc: `/blog/${post.slug}`, lastmod: post.published_at })),
|
|
18
|
+
];
|
|
19
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
20
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
21
|
+
${urls
|
|
22
|
+
.map(
|
|
23
|
+
({ loc, lastmod }) =>
|
|
24
|
+
` <url><loc>${SITE_URL}${loc}</loc>${lastmod ? `<lastmod>${lastmod.slice(0, 10)}</lastmod>` : ""}</url>`,
|
|
25
|
+
)
|
|
26
|
+
.join("\n")}
|
|
27
|
+
</urlset>`;
|
|
28
|
+
return new Response(body, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
|
|
29
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { error } from "@sveltejs/kit";
|
|
2
|
+
import { getContent, pageNumber } from "#lib/server/api.ts";
|
|
3
|
+
import type { PageServerLoad } from "./$types";
|
|
4
|
+
|
|
5
|
+
export const load: PageServerLoad = async ({ params, cookies, url }) => {
|
|
6
|
+
const content = getContent(cookies);
|
|
7
|
+
const tag = await content.getTag(params.slug);
|
|
8
|
+
if (!tag) error(404, "Page not found");
|
|
9
|
+
const result = await content.getPosts({ tag: params.slug, page: pageNumber(url), limit: 9 });
|
|
10
|
+
return { tag, result, seo: { title: `#${tag.name}` } };
|
|
11
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import PostList from "#lib/components/PostList.svelte";
|
|
3
|
+
|
|
4
|
+
let { data } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<section class="container">
|
|
8
|
+
<p class="muted">Tag</p>
|
|
9
|
+
<h1>#{data.tag.name}</h1>
|
|
10
|
+
<PostList result={data.result} base="/tag/{data.tag.slug}" />
|
|
11
|
+
</section>
|
|
@@ -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>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getCached, purgeAll, store } from "../src/lib/server/cache";
|
|
3
|
+
|
|
4
|
+
describe("page cache", () => {
|
|
5
|
+
it("stores, serves and purges pages", async () => {
|
|
6
|
+
purgeAll();
|
|
7
|
+
expect(getCached("/about")).toBeNull();
|
|
8
|
+
const response = await store(
|
|
9
|
+
"/about",
|
|
10
|
+
new Response("<h1>About</h1>", { headers: { "Content-Type": "text/html" } }),
|
|
11
|
+
);
|
|
12
|
+
expect(response.headers.get("X-Cache")).toBe("MISS");
|
|
13
|
+
expect(await response.text()).toBe("<h1>About</h1>");
|
|
14
|
+
|
|
15
|
+
const hit = getCached("/about");
|
|
16
|
+
expect(hit?.headers.get("X-Cache")).toBe("HIT");
|
|
17
|
+
expect(await hit?.text()).toBe("<h1>About</h1>");
|
|
18
|
+
|
|
19
|
+
expect(purgeAll()).toBe(1);
|
|
20
|
+
expect(getCached("/about")).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { rssFeed } from "../src/lib/server/rss";
|
|
3
|
+
|
|
4
|
+
describe("RSS feed", () => {
|
|
5
|
+
it("lists the posts with absolute links, escaped", () => {
|
|
6
|
+
const xml = rssFeed({
|
|
7
|
+
title: "News & notes",
|
|
8
|
+
description: "",
|
|
9
|
+
site: "https://example.com",
|
|
10
|
+
items: [{ title: "<Hello>", link: "/blog/hello", pubDate: new Date("2026-01-02T03:04:05Z") }],
|
|
11
|
+
});
|
|
12
|
+
expect(xml).toContain("<title>News & notes</title>");
|
|
13
|
+
expect(xml).toContain("<title><Hello></title>");
|
|
14
|
+
expect(xml).toContain("<link>https://example.com/blog/hello</link>");
|
|
15
|
+
expect(xml).toContain("<pubDate>Fri, 02 Jan 2026 03:04:05 GMT</pubDate>");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import adapter from "@sveltejs/adapter-node";
|
|
2
|
+
import { sveltekit } from "@sveltejs/kit/vite";
|
|
3
|
+
import { defineConfig } from "vitest/config";
|
|
4
|
+
|
|
5
|
+
// The Django backend: the frontend container reaches it on the Compose network.
|
|
6
|
+
const backend = process.env.BACKEND_INTERNAL_URL ?? "http://localhost:8000";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [
|
|
10
|
+
sveltekit({
|
|
11
|
+
// A Node server: pages render on request, so a publish shows within seconds.
|
|
12
|
+
adapter: adapter(),
|
|
13
|
+
// A Content-Security-Policy on every page: scripts and styles only from
|
|
14
|
+
// this site, with hashes for the ones SvelteKit inlines (hashes, not
|
|
15
|
+
// nonces, so cached pages stay valid). src/hooks.server.ts adds the
|
|
16
|
+
// origin of the media files.
|
|
17
|
+
csp: {
|
|
18
|
+
mode: "hash",
|
|
19
|
+
directives: {
|
|
20
|
+
"default-src": ["self"],
|
|
21
|
+
"script-src": ["self"],
|
|
22
|
+
"style-src": ["self"],
|
|
23
|
+
// Inline style attributes, such as the focal point of an image.
|
|
24
|
+
"style-src-attr": ["unsafe-inline"],
|
|
25
|
+
"base-uri": ["self"],
|
|
26
|
+
"form-action": ["self"],
|
|
27
|
+
"object-src": ["none"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
server: {
|
|
33
|
+
host: true,
|
|
34
|
+
port: 3000,
|
|
35
|
+
strictPort: true,
|
|
36
|
+
// Django calls /revalidate at http://frontend:3000 on the Compose network.
|
|
37
|
+
allowedHosts: ["frontend"],
|
|
38
|
+
// In development, serve Django's paths from the same origin as the site,
|
|
39
|
+
// like the reverse proxy does in production.
|
|
40
|
+
proxy: Object.fromEntries(
|
|
41
|
+
["/admin", "/api", "/media", "/static"].map((path) => [path, { target: backend, changeOrigin: false }]),
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
test: {
|
|
45
|
+
include: ["test/**/*.test.ts"],
|
|
46
|
+
},
|
|
47
|
+
});
|