create-shapio 0.2.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 (161) hide show
  1. package/LICENSE +201 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +311 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +37 -0
  6. package/templates/astro/.env.example +10 -0
  7. package/templates/astro/README.md +31 -0
  8. package/templates/astro/astro.config.mjs +50 -0
  9. package/templates/astro/gitignore +4 -0
  10. package/templates/astro/package.json +31 -0
  11. package/templates/astro/public/favicon.svg +9 -0
  12. package/templates/astro/public/robots.txt +3 -0
  13. package/templates/astro/scripts/incrementalBuild.ts +142 -0
  14. package/templates/astro/scripts/lib/admin.ts +76 -0
  15. package/templates/astro/scripts/lib/callback.ts +46 -0
  16. package/templates/astro/scripts/lib/content.ts +462 -0
  17. package/templates/astro/scripts/lib/png.ts +47 -0
  18. package/templates/astro/scripts/reportStatus.ts +40 -0
  19. package/templates/astro/scripts/seed.ts +396 -0
  20. package/templates/astro/scripts/smoke.ts +127 -0
  21. package/templates/astro/scripts/smokeHttp.ts +162 -0
  22. package/templates/astro/shapio/components/callToAction.json +85 -0
  23. package/templates/astro/shapio/components/feature.json +46 -0
  24. package/templates/astro/shapio/components/featureGrid.json +51 -0
  25. package/templates/astro/shapio/components/gallery.json +52 -0
  26. package/templates/astro/shapio/components/hero.json +108 -0
  27. package/templates/astro/shapio/models/article.json +150 -0
  28. package/templates/astro/shapio/models/author.json +73 -0
  29. package/templates/astro/shapio/models/page.json +93 -0
  30. package/templates/astro/shapio/models/siteSettings.json +87 -0
  31. package/templates/astro/src/layouts/Base.astro +52 -0
  32. package/templates/astro/src/lib/config.ts +42 -0
  33. package/templates/astro/src/lib/preview.ts +57 -0
  34. package/templates/astro/src/lib/render.ts +146 -0
  35. package/templates/astro/src/lib/shapio.ts +104 -0
  36. package/templates/astro/src/lib/site.ts +55 -0
  37. package/templates/astro/src/lib/types.ts +96 -0
  38. package/templates/astro/src/pages/404.astro +8 -0
  39. package/templates/astro/src/pages/[locale]/[...slug].astro +28 -0
  40. package/templates/astro/src/pages/[locale]/articles/[slug].astro +25 -0
  41. package/templates/astro/src/pages/[locale]/articles/index.astro +22 -0
  42. package/templates/astro/src/pages/[locale]/colophon.astro +19 -0
  43. package/templates/astro/src/pages/build.json.ts +9 -0
  44. package/templates/astro/src/pages/index.astro +18 -0
  45. package/templates/astro/src/pages/preview.astro +21 -0
  46. package/templates/astro/src/scripts/preview.ts +47 -0
  47. package/templates/astro/src/styles/site.css +199 -0
  48. package/templates/astro/tsconfig.json +17 -0
  49. package/templates/next/.env.example +15 -0
  50. package/templates/next/README.md +69 -0
  51. package/templates/next/gitignore +5 -0
  52. package/templates/next/next.config.ts +69 -0
  53. package/templates/next/package.json +34 -0
  54. package/templates/next/public/favicon.svg +9 -0
  55. package/templates/next/scripts/lib/admin.ts +76 -0
  56. package/templates/next/scripts/lib/content.ts +462 -0
  57. package/templates/next/scripts/lib/png.ts +47 -0
  58. package/templates/next/scripts/seed.ts +396 -0
  59. package/templates/next/scripts/smokeHttp.ts +162 -0
  60. package/templates/next/shapio/components/callToAction.json +85 -0
  61. package/templates/next/shapio/components/feature.json +46 -0
  62. package/templates/next/shapio/components/featureGrid.json +51 -0
  63. package/templates/next/shapio/components/gallery.json +52 -0
  64. package/templates/next/shapio/components/hero.json +108 -0
  65. package/templates/next/shapio/models/article.json +150 -0
  66. package/templates/next/shapio/models/author.json +73 -0
  67. package/templates/next/shapio/models/page.json +93 -0
  68. package/templates/next/shapio/models/siteSettings.json +87 -0
  69. package/templates/next/src/app/[locale]/[[...slug]]/page.tsx +40 -0
  70. package/templates/next/src/app/[locale]/articles/[slug]/page.tsx +37 -0
  71. package/templates/next/src/app/[locale]/articles/page.tsx +28 -0
  72. package/templates/next/src/app/[locale]/colophon/page.tsx +25 -0
  73. package/templates/next/src/app/[locale]/layout.tsx +77 -0
  74. package/templates/next/src/app/api/revalidate/route.ts +31 -0
  75. package/templates/next/src/app/preview/layout.tsx +20 -0
  76. package/templates/next/src/app/preview/page.tsx +6 -0
  77. package/templates/next/src/app/site.css +199 -0
  78. package/templates/next/src/components/ArticleCard/index.tsx +19 -0
  79. package/templates/next/src/components/ArticleView/index.tsx +40 -0
  80. package/templates/next/src/components/Byline/index.tsx +15 -0
  81. package/templates/next/src/components/LocaleSwitch/index.tsx +16 -0
  82. package/templates/next/src/components/Preview/hooks/usePreviewDraft.ts +53 -0
  83. package/templates/next/src/components/Preview/index.tsx +33 -0
  84. package/templates/next/src/components/ResponsiveImage/index.tsx +36 -0
  85. package/templates/next/src/components/RichText/index.tsx +13 -0
  86. package/templates/next/src/components/Sections/CallToAction/index.tsx +14 -0
  87. package/templates/next/src/components/Sections/FeatureGrid/index.tsx +17 -0
  88. package/templates/next/src/components/Sections/Gallery/index.tsx +20 -0
  89. package/templates/next/src/components/Sections/Hero/index.tsx +31 -0
  90. package/templates/next/src/components/Sections/SectionView/index.tsx +30 -0
  91. package/templates/next/src/components/Sections/index.tsx +19 -0
  92. package/templates/next/src/lib/articles.ts +5 -0
  93. package/templates/next/src/lib/config.ts +57 -0
  94. package/templates/next/src/lib/liveSnapshot.ts +74 -0
  95. package/templates/next/src/lib/log.ts +4 -0
  96. package/templates/next/src/lib/preview.ts +56 -0
  97. package/templates/next/src/lib/revalidation.ts +91 -0
  98. package/templates/next/src/lib/revalidationTargets.test.ts +67 -0
  99. package/templates/next/src/lib/revalidationTargets.ts +66 -0
  100. package/templates/next/src/lib/shapio.ts +75 -0
  101. package/templates/next/src/lib/site.ts +66 -0
  102. package/templates/next/src/lib/types.ts +96 -0
  103. package/templates/next/src/lib/webhookEvent.test.ts +61 -0
  104. package/templates/next/src/lib/webhookEvent.ts +47 -0
  105. package/templates/next/src/styles.d.ts +3 -0
  106. package/templates/next/tsconfig.json +23 -0
  107. package/templates/sveltekit/.env.example +10 -0
  108. package/templates/sveltekit/README.md +38 -0
  109. package/templates/sveltekit/gitignore +4 -0
  110. package/templates/sveltekit/package.json +38 -0
  111. package/templates/sveltekit/scripts/lib/admin.ts +76 -0
  112. package/templates/sveltekit/scripts/lib/content.ts +462 -0
  113. package/templates/sveltekit/scripts/lib/png.ts +47 -0
  114. package/templates/sveltekit/scripts/seed.ts +396 -0
  115. package/templates/sveltekit/scripts/smokeHttp.ts +162 -0
  116. package/templates/sveltekit/shapio/components/callToAction.json +85 -0
  117. package/templates/sveltekit/shapio/components/feature.json +46 -0
  118. package/templates/sveltekit/shapio/components/featureGrid.json +51 -0
  119. package/templates/sveltekit/shapio/components/gallery.json +52 -0
  120. package/templates/sveltekit/shapio/components/hero.json +108 -0
  121. package/templates/sveltekit/shapio/models/article.json +150 -0
  122. package/templates/sveltekit/shapio/models/author.json +73 -0
  123. package/templates/sveltekit/shapio/models/page.json +93 -0
  124. package/templates/sveltekit/shapio/models/siteSettings.json +87 -0
  125. package/templates/sveltekit/src/app.html +12 -0
  126. package/templates/sveltekit/src/env.ts +16 -0
  127. package/templates/sveltekit/src/hooks.server.ts +9 -0
  128. package/templates/sveltekit/src/lib/articles.ts +5 -0
  129. package/templates/sveltekit/src/lib/components/ArticleCard.svelte +21 -0
  130. package/templates/sveltekit/src/lib/components/ArticleView.svelte +43 -0
  131. package/templates/sveltekit/src/lib/components/Byline.svelte +14 -0
  132. package/templates/sveltekit/src/lib/components/CallToAction.svelte +15 -0
  133. package/templates/sveltekit/src/lib/components/FeatureGrid.svelte +22 -0
  134. package/templates/sveltekit/src/lib/components/Gallery.svelte +25 -0
  135. package/templates/sveltekit/src/lib/components/Hero.svelte +24 -0
  136. package/templates/sveltekit/src/lib/components/ResponsiveImage.svelte +30 -0
  137. package/templates/sveltekit/src/lib/components/RichText.svelte +13 -0
  138. package/templates/sveltekit/src/lib/components/Sections.svelte +30 -0
  139. package/templates/sveltekit/src/lib/preview.ts +56 -0
  140. package/templates/sveltekit/src/lib/server/config.ts +46 -0
  141. package/templates/sveltekit/src/lib/server/shapio.ts +93 -0
  142. package/templates/sveltekit/src/lib/site.css +199 -0
  143. package/templates/sveltekit/src/lib/site.ts +66 -0
  144. package/templates/sveltekit/src/lib/types.ts +96 -0
  145. package/templates/sveltekit/src/routes/+layout.ts +3 -0
  146. package/templates/sveltekit/src/routes/+page.ts +7 -0
  147. package/templates/sveltekit/src/routes/[locale]/+layout.server.ts +16 -0
  148. package/templates/sveltekit/src/routes/[locale]/+layout.svelte +32 -0
  149. package/templates/sveltekit/src/routes/[locale]/[...slug]/+page.server.ts +28 -0
  150. package/templates/sveltekit/src/routes/[locale]/[...slug]/+page.svelte +15 -0
  151. package/templates/sveltekit/src/routes/[locale]/articles/+page.server.ts +9 -0
  152. package/templates/sveltekit/src/routes/[locale]/articles/+page.svelte +17 -0
  153. package/templates/sveltekit/src/routes/[locale]/articles/[slug]/+page.server.ts +25 -0
  154. package/templates/sveltekit/src/routes/[locale]/articles/[slug]/+page.svelte +15 -0
  155. package/templates/sveltekit/src/routes/[locale]/colophon/+page.svelte +17 -0
  156. package/templates/sveltekit/src/routes/[locale]/colophon/+page.ts +5 -0
  157. package/templates/sveltekit/src/routes/preview/+page.server.ts +5 -0
  158. package/templates/sveltekit/src/routes/preview/+page.svelte +67 -0
  159. package/templates/sveltekit/static/favicon.svg +9 -0
  160. package/templates/sveltekit/tsconfig.json +14 -0
  161. package/templates/sveltekit/vite.config.ts +63 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Content as the Shapio delivery API returns it for the starter's models (shapio/). Delivery
3
+ * entries are flat: system attributes next to the fields, keyed by API key. Media fields are asset views,
4
+ * rich text carries the JSON document plus a sanitised `html` rendering, populated relations are entries.
5
+ */
6
+ export type MediaVariant = {
7
+ name: string;
8
+ width: number | null;
9
+ height: number | null;
10
+ format: string;
11
+ mimeType: string;
12
+ url: string;
13
+ };
14
+
15
+ export type Media = {
16
+ id: string;
17
+ filename: string;
18
+ mimeType: string;
19
+ width: number | null;
20
+ height: number | null;
21
+ alt: string;
22
+ caption: string;
23
+ url: string;
24
+ variants: MediaVariant[];
25
+ };
26
+
27
+ export type RichText = { format: 'shapio-richtext'; version: number; html: string };
28
+
29
+ type EntryBase = {
30
+ id: string;
31
+ /** The locale that served the entry (differs from the requested one on fallback). */
32
+ locale: string;
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ publishedAt?: string;
36
+ };
37
+
38
+ export type Feature = { title: string; description: string | null };
39
+
40
+ export type HeroSection = {
41
+ __component: 'hero';
42
+ heading: string;
43
+ subheading: string | null;
44
+ image: Media | null;
45
+ ctaLabel: string | null;
46
+ ctaUrl: string | null;
47
+ };
48
+ export type FeatureGridSection = { __component: 'featureGrid'; heading: string | null; features: Feature[] };
49
+ export type GallerySection = { __component: 'gallery'; heading: string | null; images: Media[] };
50
+ export type CallToActionSection = {
51
+ __component: 'callToAction';
52
+ heading: string;
53
+ text: string | null;
54
+ buttonLabel: string;
55
+ buttonUrl: string;
56
+ };
57
+ export type Section = HeroSection | FeatureGridSection | GallerySection | CallToActionSection;
58
+
59
+ export type Page = EntryBase & {
60
+ title: string;
61
+ slug: string;
62
+ description: string | null;
63
+ sections: Section[];
64
+ };
65
+
66
+ export type Author = EntryBase & { name: string; bio: string | null; avatar: Media | null };
67
+
68
+ export type Article = EntryBase & {
69
+ title: string;
70
+ slug: string;
71
+ excerpt: string | null;
72
+ body: RichText | null;
73
+ cover: Media | null;
74
+ /** Populated (`populate=author`) or just the ID; null when the author is not published. */
75
+ author: Author | string | null;
76
+ publishedOn: string;
77
+ };
78
+
79
+ /** The `siteSettings` singleton: one entry per site, read by its API ID. */
80
+ export type SiteSettings = EntryBase & {
81
+ siteName: string;
82
+ tagline: string | null;
83
+ footer: string | null;
84
+ colophon: RichText | null;
85
+ };
86
+
87
+ export type DeliveryList<T> = {
88
+ data: T[];
89
+ meta: {
90
+ locale: string;
91
+ snapshot: number;
92
+ pagination: { page: number; pageSize: number; total: number; pageCount: number };
93
+ };
94
+ };
95
+
96
+ export type DeliveryItem<T> = { data: T; meta: { locale: string; snapshot: number } };
@@ -0,0 +1,61 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { readWebhookEvent } from './webhookEvent';
4
+
5
+ const SECRET = 'test-secret';
6
+ const NOW = new Date('2026-10-03T12:00:00Z');
7
+ const BODY = JSON.stringify({
8
+ id: 'evt-1',
9
+ type: 'entry.published',
10
+ createdAt: NOW.toISOString(),
11
+ site: null,
12
+ data: {},
13
+ });
14
+
15
+ const signed = (body: string, { secret = SECRET, at = NOW } = {}) => {
16
+ const timestamp = String(Math.floor(at.getTime() / 1000));
17
+ const signature = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
18
+ return new Request('http://localhost/api/revalidate', {
19
+ method: 'POST',
20
+ headers: { 'x-shapio-timestamp': timestamp, 'x-shapio-signature': `v1=${signature}` },
21
+ body,
22
+ });
23
+ };
24
+
25
+ describe('readWebhookEvent', () => {
26
+ it('accepts a correctly signed event', async () => {
27
+ const read = await readWebhookEvent(signed(BODY), SECRET, NOW);
28
+ expect(read).toMatchObject({ ok: true, event: { id: 'evt-1', type: 'entry.published' } });
29
+ });
30
+
31
+ it('refuses another secret, a stale timestamp and a missing signature', async () => {
32
+ expect(await readWebhookEvent(signed(BODY, { secret: 'other' }), SECRET, NOW)).toMatchObject({
33
+ ok: false,
34
+ status: 401,
35
+ reason: 'signature mismatch',
36
+ });
37
+ const stale = new Date(NOW.getTime() - 10 * 60 * 1000);
38
+ expect(await readWebhookEvent(signed(BODY, { at: stale }), SECRET, NOW)).toMatchObject({
39
+ ok: false,
40
+ status: 401,
41
+ });
42
+ const unsigned = new Request('http://localhost/api/revalidate', { method: 'POST', body: BODY });
43
+ expect(await readWebhookEvent(unsigned, SECRET, NOW)).toMatchObject({ ok: false, status: 401 });
44
+ });
45
+
46
+ it('refuses a body changed after signing', async () => {
47
+ const request = signed(BODY);
48
+ const tampered = new Request(request.url, {
49
+ method: 'POST',
50
+ headers: request.headers,
51
+ body: BODY.replace('evt-1', 'evt-2'),
52
+ });
53
+ expect(await readWebhookEvent(tampered, SECRET, NOW)).toMatchObject({ ok: false, status: 401 });
54
+ });
55
+
56
+ it('answers 503 without a configured secret and 400 for a signed non-event', async () => {
57
+ expect(await readWebhookEvent(signed(BODY), undefined, NOW)).toMatchObject({ ok: false, status: 503 });
58
+ expect(await readWebhookEvent(signed('[]'), SECRET, NOW)).toMatchObject({ ok: false, status: 400 });
59
+ expect(await readWebhookEvent(signed('not json'), SECRET, NOW)).toMatchObject({ ok: false, status: 400 });
60
+ });
61
+ });
@@ -0,0 +1,47 @@
1
+ import {
2
+ verifyWebhookSignature,
3
+ WEBHOOK_SIGNATURE_HEADER,
4
+ WEBHOOK_TIMESTAMP_HEADER,
5
+ type WebhookEventBody,
6
+ } from '@shapio/client';
7
+
8
+ export type WebhookEventRead =
9
+ { ok: true; event: WebhookEventBody } | { ok: false; status: 400 | 401 | 503; reason: string };
10
+
11
+ const isEventBody = (value: unknown): value is WebhookEventBody =>
12
+ typeof value === 'object' &&
13
+ value !== null &&
14
+ typeof (value as { id?: unknown }).id === 'string' &&
15
+ typeof (value as { type?: unknown }).type === 'string';
16
+
17
+ /**
18
+ * Reads a Shapio webhook delivery: checks `X-Shapio-Signature` over the raw body with the webhook's secret
19
+ * (and the timestamp's freshness), then parses the event. Nothing in the body is trusted before that.
20
+ */
21
+ export const readWebhookEvent = async (
22
+ request: Request,
23
+ secret: string | undefined,
24
+ now = new Date(),
25
+ ): Promise<WebhookEventRead> => {
26
+ if (!secret) {
27
+ return { ok: false, status: 503, reason: 'SHAPIO_WEBHOOK_SECRET is not set' };
28
+ }
29
+ const body = await request.text();
30
+ const check = await verifyWebhookSignature({
31
+ secret,
32
+ signature: request.headers.get(WEBHOOK_SIGNATURE_HEADER),
33
+ timestamp: request.headers.get(WEBHOOK_TIMESTAMP_HEADER),
34
+ body,
35
+ now,
36
+ });
37
+ if (!check.ok) {
38
+ return { ok: false, status: 401, reason: check.reason };
39
+ }
40
+ let event: unknown;
41
+ try {
42
+ event = JSON.parse(body);
43
+ } catch {
44
+ return { ok: false, status: 400, reason: 'the body is not JSON' };
45
+ }
46
+ return isEventBody(event) ? { ok: true, event } : { ok: false, status: 400, reason: 'not a Shapio event' };
47
+ };
@@ -0,0 +1,3 @@
1
+ // Global stylesheets are imported for their side effect (Next's own declarations arrive in next-env.d.ts,
2
+ // which `next build` writes; this keeps `npm run typecheck` working before the first build).
3
+ declare module '*.css';
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "lib": ["dom", "dom.iterable", "es2024"],
5
+ "allowJs": false,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "bundler",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "verbatimModuleSyntax": true,
16
+ "jsx": "react-jsx",
17
+ "incremental": true,
18
+ "types": ["node"],
19
+ "plugins": [{ "name": "next" }]
20
+ },
21
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
22
+ "exclude": ["node_modules"]
23
+ }
@@ -0,0 +1,10 @@
1
+ # Copy to .env, or let `npm run seed` write SHAPIO_URL and SHAPIO_DELIVERY_TOKEN for you.
2
+ SHAPIO_URL=http://localhost:4300
3
+ # A read-only delivery token (Shapio admin: Settings → API tokens, a delivery role).
4
+ SHAPIO_DELIVERY_TOKEN=
5
+ # Optional: build this publication snapshot instead of the latest one.
6
+ # SHAPIO_SNAPSHOT=42
7
+ # Optional: the site key on a multi-site Shapio (default: the token's site, else the primary site).
8
+ # SHAPIO_SITE=marketing
9
+ # Optional: the Shapio URL the browser calls for draft previews, when it differs from SHAPIO_URL.
10
+ # PUBLIC_SHAPIO_URL=https://cms.example.com
@@ -0,0 +1,38 @@
1
+ # Shapio starter: SvelteKit
2
+
3
+ A [SvelteKit 3](https://svelte.dev) site built from [Shapio](https://github.com/mybrokengnome/shapio) content:
4
+ pages made of sections, a journal of articles with an author, and the `siteSettings` singleton, in English and
5
+ French. Every page is prerendered to static HTML (adapter-static) from one pinned publication snapshot.
6
+
7
+ You need a running Shapio (>= 0.1) with an owner account.
8
+
9
+ ```sh
10
+ npm install
11
+ SHAPIO_URL=http://localhost:4300 SHAPIO_ADMIN_EMAIL=you@example.com SHAPIO_ADMIN_PASSWORD='…' npm run seed
12
+ npm run build # → build/
13
+ npm run preview # http://localhost:4173/ (redirects to /en/)
14
+ npm run smoke # in another shell: checks the served pages over HTTP
15
+ ```
16
+
17
+ `npm run seed` applies the models in `shapio/` to Shapio live, creates the content and writes `SHAPIO_URL` and
18
+ `SHAPIO_DELIVERY_TOKEN` to `.env`. `SHAPIO_SNAPSHOT` pins a build to an exact snapshot (by default the latest
19
+ one, read once when prerendering starts); `SHAPIO_SITE` picks the site on a multi-site instance.
20
+
21
+ | File | Role |
22
+ | ---------------------------- | ------------------------------------------------------------------- |
23
+ | `src/env.ts` | the settings the build reads (`SHAPIO_*`, from the environment) |
24
+ | `src/lib/server/shapio.ts` | reads the delivery API with `@shapio/client` at the pinned snapshot |
25
+ | `src/routes/[locale]/…` | the layout (site settings), pages, articles and the colophon |
26
+ | `src/lib/components/…` | sections, articles and responsive images |
27
+ | `src/routes/preview/` | draft preview with visual editing (`@shapio/visual`) |
28
+ | `vite.config.ts` | `frame-ancestors` for the servers and `build/_headers` |
29
+ | `shapio/`, `scripts/seed.ts` | the models and the seed |
30
+
31
+ Preview: the seed creates a deployment connection named **Preview** that opens drafts at
32
+ `http://localhost:5173/preview/` (`npm run dev`; set `SITE_URL` when seeding for another address). Add that
33
+ origin to Shapio's `CORS_ORIGINS`. In Shapio's preview pane, clicking the title, body or cover focuses that
34
+ field and saves re-render the draft ([visual editing](https://github.com/mybrokengnome/shapio/blob/main/documentation/visual-editing.md)). `PUBLIC_SHAPIO_URL` sets the Shapio URL the
35
+ browser calls, when it differs from `SHAPIO_URL`.
36
+
37
+ Guide: [site starters](https://github.com/mybrokengnome/shapio/blob/main/documentation/starters.md). Inside the
38
+ Shapio repository, run the scripts with `pnpm --filter example-sveltekit <script>` after `pnpm build`.
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ .svelte-kit/
3
+ build/
4
+ .env
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "example-sveltekit",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "SvelteKit starter: pages and articles from Shapio, prerendered to static HTML at one pinned snapshot, EN + FR.",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=24"
9
+ },
10
+ "imports": {
11
+ "#lib": "./src/lib",
12
+ "#lib/*": "./src/lib/*"
13
+ },
14
+ "scripts": {
15
+ "dev": "vite dev",
16
+ "build": "vite build",
17
+ "preview": "vite preview",
18
+ "prepare": "svelte-kit sync || echo ''",
19
+ "seed": "node --env-file-if-exists=.env --import tsx scripts/seed.ts --site-url http://localhost:5173",
20
+ "smoke": "node --import tsx scripts/smokeHttp.ts --url http://localhost:4173",
21
+ "typecheck": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
22
+ },
23
+ "dependencies": {
24
+ "@shapio/client": "^0.2.0",
25
+ "@shapio/visual": "^0.2.0"
26
+ },
27
+ "devDependencies": {
28
+ "@sveltejs/adapter-static": "4.0.0",
29
+ "@sveltejs/kit": "3.0.0",
30
+ "@sveltejs/vite-plugin-svelte": "7.3.1",
31
+ "@types/node": "24.19.1",
32
+ "svelte": "5.57.1",
33
+ "svelte-check": "4.7.6",
34
+ "tsx": "4.23.15",
35
+ "typescript": "6.0.3",
36
+ "vite": "8.3.2"
37
+ }
38
+ }
@@ -0,0 +1,76 @@
1
+ import { createClient, type ShapioClient } from '@shapio/client';
2
+
3
+ /**
4
+ * How the seed talks to Shapio's admin API: with SHAPIO_TOKEN (an admin API token), or with
5
+ * SHAPIO_ADMIN_EMAIL + SHAPIO_ADMIN_PASSWORD, in which case it signs in, creates a one-hour admin API token
6
+ * for itself (the schema CLI needs a bearer token) and revokes it when done.
7
+ */
8
+ export type AdminConnection = {
9
+ url: string;
10
+ token: string;
11
+ client: ShapioClient;
12
+ /** A signed-in admin session (email and password only): preview tokens are issued to people, not API tokens. */
13
+ session: ShapioClient | undefined;
14
+ close: () => Promise<void>;
15
+ };
16
+
17
+ const SESSION_COOKIE = 'shapio_session';
18
+ const SEED_TOKEN_TTL_MS = 60 * 60 * 1000;
19
+
20
+ const signIn = async (url: string, email: string, password: string) => {
21
+ const response = await fetch(`${url.replace(/\/+$/, '')}/api/admin/auth/login`, {
22
+ method: 'POST',
23
+ headers: { 'content-type': 'application/json' },
24
+ body: JSON.stringify({ email, password }),
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`Signing in as ${email} failed: HTTP ${response.status} ${await response.text()}`);
28
+ }
29
+ const cookie = response.headers.getSetCookie().find((value) => value.startsWith(`${SESSION_COOKIE}=`));
30
+ const { csrfToken } = (await response.json()) as { csrfToken: string };
31
+ if (!cookie) {
32
+ throw new Error('Signing in returned no session cookie');
33
+ }
34
+ return createClient({
35
+ baseUrl: url,
36
+ headers: () => ({ cookie: cookie.split(';')[0] ?? '', 'x-csrf-token': csrfToken }),
37
+ });
38
+ };
39
+
40
+ export const connectAdmin = async (env: NodeJS.ProcessEnv): Promise<AdminConnection> => {
41
+ const url = env.SHAPIO_URL ?? 'http://localhost:4300';
42
+ if (env.SHAPIO_TOKEN) {
43
+ return {
44
+ url,
45
+ token: env.SHAPIO_TOKEN,
46
+ client: createClient({ baseUrl: url, token: env.SHAPIO_TOKEN }),
47
+ session: undefined,
48
+ close: async () => undefined,
49
+ };
50
+ }
51
+ const email = env.SHAPIO_ADMIN_EMAIL;
52
+ const password = env.SHAPIO_ADMIN_PASSWORD;
53
+ if (!email || !password) {
54
+ throw new Error('Set SHAPIO_TOKEN (an admin API token), or SHAPIO_ADMIN_EMAIL and SHAPIO_ADMIN_PASSWORD');
55
+ }
56
+ const session = await signIn(url, email, password);
57
+ const roles = await session.admin.roles.list();
58
+ const admin = roles.find((role) => role.key === 'admin');
59
+ if (!admin) {
60
+ throw new Error('The built-in admin role is missing');
61
+ }
62
+ const created = await session.admin.tokens.create({
63
+ name: 'starter seed (temporary)',
64
+ roleId: admin.id,
65
+ // A network token: the seed applies models and adds locales, which are network actions.
66
+ network: true,
67
+ expiresAt: new Date(Date.now() + SEED_TOKEN_TTL_MS).toISOString(),
68
+ });
69
+ return {
70
+ url,
71
+ token: created.token,
72
+ client: createClient({ baseUrl: url, token: created.token }),
73
+ session,
74
+ close: () => session.admin.tokens.revoke(created.apiToken.id),
75
+ };
76
+ };