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,462 @@
1
+ import type { Rgb } from './png.js';
2
+
3
+ /**
4
+ * What the seed creates: placeholder images, one author, the site settings, two pages and three articles in
5
+ * English and French. Two articles are published; the third stays a draft (for trying preview). Media and
6
+ * author IDs are only known at run time, so the entries are functions of them.
7
+ */
8
+ export type ImageSpec = { filename: string; alt: string; width: number; height: number; from: Rgb; to: Rgb };
9
+
10
+ export const IMAGES = {
11
+ hero: {
12
+ filename: 'hero.png',
13
+ alt: 'An abstract blue gradient',
14
+ width: 1600,
15
+ height: 900,
16
+ from: [37, 99, 235],
17
+ to: [165, 180, 252],
18
+ },
19
+ galleryOne: {
20
+ filename: 'gallery-1.png',
21
+ alt: 'Studio desk at dawn',
22
+ width: 1200,
23
+ height: 900,
24
+ from: [15, 23, 42],
25
+ to: [37, 99, 235],
26
+ },
27
+ galleryTwo: {
28
+ filename: 'gallery-2.png',
29
+ alt: 'Workshop wall with sketches',
30
+ width: 1200,
31
+ height: 900,
32
+ from: [165, 180, 252],
33
+ to: [250, 249, 246],
34
+ },
35
+ galleryThree: {
36
+ filename: 'gallery-3.png',
37
+ alt: 'Evening light over the city',
38
+ width: 1200,
39
+ height: 900,
40
+ from: [124, 58, 237],
41
+ to: [37, 99, 235],
42
+ },
43
+ avatar: {
44
+ filename: 'avatar.png',
45
+ alt: 'Portrait of Ada Moreau',
46
+ width: 400,
47
+ height: 400,
48
+ from: [250, 204, 21],
49
+ to: [234, 88, 12],
50
+ },
51
+ coverOne: {
52
+ filename: 'cover-modelling.png',
53
+ alt: 'Blocks arranged into a grid',
54
+ width: 1600,
55
+ height: 900,
56
+ from: [16, 185, 129],
57
+ to: [37, 99, 235],
58
+ },
59
+ coverTwo: {
60
+ filename: 'cover-snapshots.png',
61
+ alt: 'A camera shutter in blue',
62
+ width: 1600,
63
+ height: 900,
64
+ from: [37, 99, 235],
65
+ to: [15, 23, 42],
66
+ },
67
+ } as const satisfies Record<string, ImageSpec>;
68
+
69
+ export type MediaIds = Record<keyof typeof IMAGES, string>;
70
+
71
+ type Localized<T> = { en: T; fr: T };
72
+
73
+ const text = (value: string) => ({ type: 'text', text: value });
74
+ const paragraph = (...content: unknown[]) => ({ type: 'paragraph', content });
75
+ const heading = (level: number, value: string) => ({
76
+ type: 'heading',
77
+ attrs: { level },
78
+ content: [text(value)],
79
+ });
80
+ const cell = (type: 'tableHeader' | 'tableCell', value: string) => ({
81
+ type,
82
+ content: [paragraph(text(value))],
83
+ });
84
+ const richText = (...content: unknown[]) => ({
85
+ format: 'shapio-richtext',
86
+ version: 1,
87
+ doc: { type: 'doc', content },
88
+ });
89
+
90
+ export const AUTHOR = {
91
+ name: 'Ada Moreau',
92
+ bio: 'Editor at Northwind Studio. Writes about content models, publishing and the web.',
93
+ };
94
+
95
+ /** The `siteSettings` singleton: the name, tagline and footer every page shows, and the colophon page. */
96
+ export const SITE_SETTINGS: Localized<Record<string, unknown>> = {
97
+ en: {
98
+ siteName: 'Northwind Studio',
99
+ tagline: 'Fast, well-made websites from structured content.',
100
+ footer: 'Built with Shapio',
101
+ colophon: richText(
102
+ paragraph(
103
+ text('This site is built from Shapio content, pinned to one publication snapshot per build.'),
104
+ ),
105
+ paragraph(text('Its name, tagline and footer come from a singleton model: edit them once, rebuild.')),
106
+ ),
107
+ },
108
+ fr: {
109
+ siteName: 'Studio Northwind',
110
+ tagline: 'Des sites rapides et soignés, construits à partir de contenu structuré.',
111
+ footer: 'Construit avec Shapio',
112
+ colophon: richText(
113
+ paragraph(
114
+ text(
115
+ 'Ce site est construit à partir du contenu de Shapio, figé sur un instantané de publication par build.',
116
+ ),
117
+ ),
118
+ paragraph(text('Son nom, son slogan et son pied de page viennent d’un modèle singleton.')),
119
+ ),
120
+ },
121
+ };
122
+
123
+ const home = (media: MediaIds): Localized<Record<string, unknown>> => ({
124
+ en: {
125
+ title: 'Home',
126
+ slug: 'home',
127
+ description: 'Northwind Studio builds fast, well-made websites from structured content.',
128
+ sections: [
129
+ {
130
+ __component: 'hero',
131
+ heading: 'Content that changes as fast as your product',
132
+ subheading: 'Model it live, edit it in two languages, publish it through to a static site.',
133
+ image: media.hero,
134
+ ctaLabel: 'Read the journal',
135
+ ctaUrl: '/en/articles/',
136
+ },
137
+ {
138
+ __component: 'featureGrid',
139
+ heading: 'What we do',
140
+ features: [
141
+ {
142
+ title: 'Content models',
143
+ description: 'Pages, articles and reusable sections, changed without a deploy.',
144
+ },
145
+ {
146
+ title: 'Two languages',
147
+ description: 'Every page in English and French, published per language.',
148
+ },
149
+ {
150
+ title: 'Static delivery',
151
+ description: 'Each build reads one pinned snapshot, then ships plain HTML.',
152
+ },
153
+ ],
154
+ },
155
+ {
156
+ __component: 'gallery',
157
+ heading: 'From the studio',
158
+ images: [media.galleryOne, media.galleryTwo, media.galleryThree],
159
+ },
160
+ {
161
+ __component: 'callToAction',
162
+ heading: 'Start a project with us',
163
+ text: 'Tell us what you are building.',
164
+ buttonLabel: 'Get in touch',
165
+ buttonUrl: 'mailto:hello@example.com',
166
+ },
167
+ ],
168
+ },
169
+ fr: {
170
+ title: 'Accueil',
171
+ description: 'Le Studio Northwind construit des sites rapides et soignés à partir de contenu structuré.',
172
+ sections: [
173
+ {
174
+ __component: 'hero',
175
+ heading: 'Un contenu qui évolue aussi vite que votre produit',
176
+ subheading: 'Modélisez-le en direct, rédigez-le en deux langues, publiez-le jusqu’au site statique.',
177
+ image: media.hero,
178
+ ctaLabel: 'Lire le journal',
179
+ ctaUrl: '/fr/articles/',
180
+ },
181
+ {
182
+ __component: 'featureGrid',
183
+ heading: 'Ce que nous faisons',
184
+ features: [
185
+ {
186
+ title: 'Modèles de contenu',
187
+ description: 'Pages, articles et sections réutilisables, modifiés sans déploiement.',
188
+ },
189
+ {
190
+ title: 'Deux langues',
191
+ description: 'Chaque page en anglais et en français, publiée langue par langue.',
192
+ },
193
+ {
194
+ title: 'Diffusion statique',
195
+ description: 'Chaque build lit un instantané figé, puis livre du HTML simple.',
196
+ },
197
+ ],
198
+ },
199
+ {
200
+ __component: 'gallery',
201
+ heading: 'Au studio',
202
+ images: [media.galleryOne, media.galleryTwo, media.galleryThree],
203
+ },
204
+ {
205
+ __component: 'callToAction',
206
+ heading: 'Lancez un projet avec nous',
207
+ text: 'Dites-nous ce que vous construisez.',
208
+ buttonLabel: 'Nous écrire',
209
+ buttonUrl: 'mailto:hello@example.com',
210
+ },
211
+ ],
212
+ },
213
+ });
214
+
215
+ const about = (): Localized<Record<string, unknown>> => ({
216
+ en: {
217
+ title: 'About the studio',
218
+ slug: 'about',
219
+ description: 'Who we are and how we work.',
220
+ sections: [
221
+ {
222
+ __component: 'featureGrid',
223
+ heading: 'How we work',
224
+ features: [
225
+ { title: 'Small team', description: 'Three people, one shared content model.' },
226
+ { title: 'Open tools', description: 'Self-hosted, open-source, no vendor lock-in.' },
227
+ ],
228
+ },
229
+ {
230
+ __component: 'callToAction',
231
+ heading: 'Read our journal',
232
+ text: 'Notes on modelling, publishing and building.',
233
+ buttonLabel: 'Open the journal',
234
+ buttonUrl: '/en/articles/',
235
+ },
236
+ ],
237
+ },
238
+ fr: {
239
+ title: 'À propos du studio',
240
+ description: 'Qui nous sommes et comment nous travaillons.',
241
+ sections: [
242
+ {
243
+ __component: 'featureGrid',
244
+ heading: 'Notre façon de travailler',
245
+ features: [
246
+ { title: 'Petite équipe', description: 'Trois personnes, un seul modèle de contenu partagé.' },
247
+ {
248
+ title: 'Outils ouverts',
249
+ description: 'Auto-hébergé, open source, sans dépendance à un fournisseur.',
250
+ },
251
+ ],
252
+ },
253
+ {
254
+ __component: 'callToAction',
255
+ heading: 'Lisez notre journal',
256
+ text: 'Des notes sur la modélisation, la publication et la construction.',
257
+ buttonLabel: 'Ouvrir le journal',
258
+ buttonUrl: '/fr/articles/',
259
+ },
260
+ ],
261
+ },
262
+ });
263
+
264
+ const modelling = (media: MediaIds, authorId: string): Localized<Record<string, unknown>> => ({
265
+ en: {
266
+ title: 'Modelling content without a deploy',
267
+ slug: 'modelling-without-a-deploy',
268
+ excerpt: 'We added a field to our article model on a Tuesday afternoon. Nothing restarted.',
269
+ publishedOn: '2026-09-01',
270
+ cover: media.coverOne,
271
+ author: authorId,
272
+ body: richText(
273
+ paragraph(text('Our article model needed a new field. In Shapio that is a form, not a migration.')),
274
+ heading(2, 'What changed'),
275
+ {
276
+ type: 'bulletList',
277
+ content: [
278
+ {
279
+ type: 'listItem',
280
+ content: [
281
+ paragraph(
282
+ text('The model got an optional '),
283
+ { type: 'text', text: 'excerpt', marks: [{ type: 'code' }] },
284
+ text(' field.'),
285
+ ),
286
+ ],
287
+ },
288
+ { type: 'listItem', content: [paragraph(text('The API served it at once, in REST and GraphQL.'))] },
289
+ ],
290
+ },
291
+ { type: 'image', attrs: { mediaId: media.galleryOne, alt: 'Studio desk at dawn' } },
292
+ heading(2, 'Before and after'),
293
+ {
294
+ type: 'table',
295
+ content: [
296
+ {
297
+ type: 'tableRow',
298
+ content: [
299
+ cell('tableHeader', 'Step'),
300
+ cell('tableHeader', 'Before'),
301
+ cell('tableHeader', 'With Shapio'),
302
+ ],
303
+ },
304
+ {
305
+ type: 'tableRow',
306
+ content: [
307
+ cell('tableCell', 'Add a field'),
308
+ cell('tableCell', 'Pull request, deploy'),
309
+ cell('tableCell', 'Save the model'),
310
+ ],
311
+ },
312
+ {
313
+ type: 'tableRow',
314
+ content: [cell('tableCell', 'Restart'), cell('tableCell', 'Yes'), cell('tableCell', 'No')],
315
+ },
316
+ ],
317
+ },
318
+ paragraph(
319
+ text('Read more in the '),
320
+ {
321
+ type: 'text',
322
+ text: 'Shapio docs',
323
+ marks: [{ type: 'link', attrs: { href: 'https://github.com/mybrokengnome/shapio' } }],
324
+ },
325
+ text('.'),
326
+ ),
327
+ ),
328
+ },
329
+ fr: {
330
+ title: 'Modéliser du contenu sans déploiement',
331
+ excerpt: 'Nous avons ajouté un champ à notre modèle d’article un mardi après-midi. Rien n’a redémarré.',
332
+ body: richText(
333
+ paragraph(
334
+ text(
335
+ 'Notre modèle d’article avait besoin d’un nouveau champ. Dans Shapio, c’est un formulaire, pas une migration.',
336
+ ),
337
+ ),
338
+ heading(2, 'Ce qui a changé'),
339
+ {
340
+ type: 'bulletList',
341
+ content: [
342
+ {
343
+ type: 'listItem',
344
+ content: [
345
+ paragraph(
346
+ text('Le modèle a reçu un champ facultatif '),
347
+ { type: 'text', text: 'excerpt', marks: [{ type: 'code' }] },
348
+ text('.'),
349
+ ),
350
+ ],
351
+ },
352
+ {
353
+ type: 'listItem',
354
+ content: [paragraph(text('L’API l’a servi aussitôt, en REST et en GraphQL.'))],
355
+ },
356
+ ],
357
+ },
358
+ { type: 'image', attrs: { mediaId: media.galleryOne, alt: 'Un bureau à l’aube' } },
359
+ heading(2, 'Avant et après'),
360
+ {
361
+ type: 'table',
362
+ content: [
363
+ {
364
+ type: 'tableRow',
365
+ content: [
366
+ cell('tableHeader', 'Étape'),
367
+ cell('tableHeader', 'Avant'),
368
+ cell('tableHeader', 'Avec Shapio'),
369
+ ],
370
+ },
371
+ {
372
+ type: 'tableRow',
373
+ content: [
374
+ cell('tableCell', 'Ajouter un champ'),
375
+ cell('tableCell', 'Pull request, déploiement'),
376
+ cell('tableCell', 'Enregistrer le modèle'),
377
+ ],
378
+ },
379
+ {
380
+ type: 'tableRow',
381
+ content: [cell('tableCell', 'Redémarrer'), cell('tableCell', 'Oui'), cell('tableCell', 'Non')],
382
+ },
383
+ ],
384
+ },
385
+ ),
386
+ },
387
+ });
388
+
389
+ const snapshots = (media: MediaIds, authorId: string): Localized<Record<string, unknown>> => ({
390
+ en: {
391
+ title: 'Why our builds pin a snapshot',
392
+ slug: 'why-builds-pin-a-snapshot',
393
+ excerpt: 'A build that reads content for two minutes must not show two different moments.',
394
+ publishedOn: '2026-09-15',
395
+ cover: media.coverTwo,
396
+ author: authorId,
397
+ body: richText(
398
+ paragraph(
399
+ text(
400
+ 'Every build of this site asks Shapio for the current publication snapshot once, then reads every page at that snapshot.',
401
+ ),
402
+ ),
403
+ {
404
+ type: 'blockquote',
405
+ content: [paragraph(text('A publish during the build waits for the next build.'))],
406
+ },
407
+ ),
408
+ },
409
+ fr: {
410
+ title: 'Pourquoi nos builds figent un instantané',
411
+ excerpt: 'Un build qui lit du contenu pendant deux minutes ne doit pas montrer deux moments différents.',
412
+ body: richText(
413
+ paragraph(
414
+ text(
415
+ 'Chaque build de ce site demande une fois l’instantané de publication courant à Shapio, puis lit chaque page à cet instantané.',
416
+ ),
417
+ ),
418
+ {
419
+ type: 'blockquote',
420
+ content: [paragraph(text('Une publication pendant le build attend le build suivant.'))],
421
+ },
422
+ ),
423
+ },
424
+ });
425
+
426
+ /** Never published: open it in the admin and use Preview. */
427
+ const draft = (media: MediaIds, authorId: string): Localized<Record<string, unknown>> => ({
428
+ en: {
429
+ title: 'Coming soon: our winter projects',
430
+ slug: 'winter-projects',
431
+ excerpt: 'A draft that only preview can show.',
432
+ publishedOn: '2026-12-01',
433
+ cover: media.coverOne,
434
+ author: authorId,
435
+ body: richText(paragraph(text('This article is still a draft.'))),
436
+ },
437
+ fr: {
438
+ title: 'Bientôt : nos projets d’hiver',
439
+ excerpt: 'Un brouillon que seul l’aperçu peut montrer.',
440
+ body: richText(paragraph(text('Cet article est encore un brouillon.'))),
441
+ },
442
+ });
443
+
444
+ export type EntrySpec = {
445
+ model: 'page' | 'article';
446
+ slug: string;
447
+ content: Localized<Record<string, unknown>>;
448
+ publish: boolean;
449
+ };
450
+
451
+ export const entries = (media: MediaIds, authorId: string): EntrySpec[] => [
452
+ { model: 'page', slug: 'home', content: home(media), publish: true },
453
+ { model: 'page', slug: 'about', content: about(), publish: true },
454
+ {
455
+ model: 'article',
456
+ slug: 'modelling-without-a-deploy',
457
+ content: modelling(media, authorId),
458
+ publish: true,
459
+ },
460
+ { model: 'article', slug: 'why-builds-pin-a-snapshot', content: snapshots(media, authorId), publish: true },
461
+ { model: 'article', slug: 'winter-projects', content: draft(media, authorId), publish: false },
462
+ ];
@@ -0,0 +1,47 @@
1
+ import { crc32, deflateSync } from 'node:zlib';
2
+
3
+ /**
4
+ * A dependency-free PNG encoder for the seed's placeholder images: an RGB diagonal gradient between two
5
+ * colours with a soft circle, so variants and galleries have something to show.
6
+ */
7
+ export type Rgb = readonly [number, number, number];
8
+
9
+ const chunk = (type: string, data: Buffer) => {
10
+ const length = Buffer.alloc(4);
11
+ length.writeUInt32BE(data.length);
12
+ const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
13
+ const crc = Buffer.alloc(4);
14
+ crc.writeUInt32BE(crc32(body));
15
+ return Buffer.concat([length, body, crc]);
16
+ };
17
+
18
+ const mix = (from: number, to: number, t: number) => Math.round(from + (to - from) * t);
19
+
20
+ export const createPng = (width: number, height: number, from: Rgb, to: Rgb): Buffer => {
21
+ const header = Buffer.alloc(13);
22
+ header.writeUInt32BE(width, 0);
23
+ header.writeUInt32BE(height, 4);
24
+ header[8] = 8; // bit depth
25
+ header[9] = 2; // colour type: RGB
26
+ const stride = width * 3 + 1;
27
+ const pixels = Buffer.alloc(stride * height);
28
+ const radius = Math.min(width, height) / 4;
29
+ for (let y = 0; y < height; y += 1) {
30
+ pixels[y * stride] = 0; // filter: none
31
+ for (let x = 0; x < width; x += 1) {
32
+ const t = (x / width + y / height) / 2;
33
+ const inCircle = (x - width * 0.68) ** 2 + (y - height * 0.42) ** 2 < radius ** 2;
34
+ const offset = y * stride + 1 + x * 3;
35
+ for (let channel = 0; channel < 3; channel += 1) {
36
+ const value = mix(from[channel] ?? 0, to[channel] ?? 0, t);
37
+ pixels[offset + channel] = inCircle ? Math.min(255, value + 40) : value;
38
+ }
39
+ }
40
+ }
41
+ return Buffer.concat([
42
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
43
+ chunk('IHDR', header),
44
+ chunk('IDAT', deflateSync(pixels, { level: 9 })),
45
+ chunk('IEND', Buffer.alloc(0)),
46
+ ]);
47
+ };