@cogenta/starters 0.1.5

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 (200) hide show
  1. package/LICENSE +373 -0
  2. package/dist/blueprints/assets/photos/association/food-bank.jpg +0 -0
  3. package/dist/blueprints/assets/photos/association/fundraising-dinner.jpg +0 -0
  4. package/dist/blueprints/assets/photos/association/garden-planting.jpg +0 -0
  5. package/dist/blueprints/assets/photos/association/hero.jpg +0 -0
  6. package/dist/blueprints/assets/photos/association/homework-club.jpg +0 -0
  7. package/dist/blueprints/assets/photos/association/partner-ashworth-college.png +0 -0
  8. package/dist/blueprints/assets/photos/association/partner-ashworth-council.png +0 -0
  9. package/dist/blueprints/assets/photos/association/partner-brindle-and-sons.png +0 -0
  10. package/dist/blueprints/assets/photos/association/partner-kestrel-homes.png +0 -0
  11. package/dist/blueprints/assets/photos/association/partner-linden-trust.png +0 -0
  12. package/dist/blueprints/assets/photos/association/partner-marlow-street-pharmacy.png +0 -0
  13. package/dist/blueprints/assets/photos/association/volunteer-orientation.jpg +0 -0
  14. package/dist/blueprints/assets/photos/association/volunteer-portrait.jpg +0 -0
  15. package/dist/blueprints/assets/photos/association/winter-coat-collection.jpg +0 -0
  16. package/dist/blueprints/assets/photos/blog/desk-setup.jpg +0 -0
  17. package/dist/blueprints/assets/photos/blog/notebook-and-coffee.jpg +0 -0
  18. package/dist/blueprints/assets/photos/blog/notebooks.jpg +0 -0
  19. package/dist/blueprints/assets/photos/blog/platform.jpg +0 -0
  20. package/dist/blueprints/assets/photos/blog/pour-over.jpg +0 -0
  21. package/dist/blueprints/assets/photos/documentation/delivery-flow.png +0 -0
  22. package/dist/blueprints/assets/photos/magazine/archive-room.jpg +0 -0
  23. package/dist/blueprints/assets/photos/magazine/banquet-hall.jpg +0 -0
  24. package/dist/blueprints/assets/photos/magazine/city-hall.jpg +0 -0
  25. package/dist/blueprints/assets/photos/magazine/dining-room.jpg +0 -0
  26. package/dist/blueprints/assets/photos/magazine/downtown-towers.jpg +0 -0
  27. package/dist/blueprints/assets/photos/magazine/gallery-opening.jpg +0 -0
  28. package/dist/blueprints/assets/photos/magazine/garden-volunteers.jpg +0 -0
  29. package/dist/blueprints/assets/photos/magazine/station-platform.jpg +0 -0
  30. package/dist/blueprints/assets/photos/magazine/theater-stage.jpg +0 -0
  31. package/dist/blueprints/assets/photos/portfolio/fenmore-branch.jpg +0 -0
  32. package/dist/blueprints/assets/photos/portfolio/fenmore-cover.jpg +0 -0
  33. package/dist/blueprints/assets/photos/portfolio/fenmore-stationery.jpg +0 -0
  34. package/dist/blueprints/assets/photos/portfolio/gannet-cover.jpg +0 -0
  35. package/dist/blueprints/assets/photos/portfolio/gannet-sardines.jpg +0 -0
  36. package/dist/blueprints/assets/photos/portfolio/gannet-shelf.jpg +0 -0
  37. package/dist/blueprints/assets/photos/portfolio/hollis-crane-cover.jpg +0 -0
  38. package/dist/blueprints/assets/photos/portfolio/hollis-crane-grid.jpg +0 -0
  39. package/dist/blueprints/assets/photos/portfolio/hollis-crane-salt-year.jpg +0 -0
  40. package/dist/blueprints/assets/photos/portfolio/hollis-crane-spines.jpg +0 -0
  41. package/dist/blueprints/assets/photos/portfolio/rookery-hall-brochure.jpg +0 -0
  42. package/dist/blueprints/assets/photos/portfolio/rookery-hall-cover.jpg +0 -0
  43. package/dist/blueprints/assets/photos/portfolio/rookery-hall-poster-carnival.jpg +0 -0
  44. package/dist/blueprints/assets/photos/portfolio/rookery-hall-poster-sibelius.jpg +0 -0
  45. package/dist/blueprints/assets/photos/portfolio/rookery-hall-series.jpg +0 -0
  46. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-cover.jpg +0 -0
  47. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-gate.jpg +0 -0
  48. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-pictograms.jpg +0 -0
  49. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-totem.jpg +0 -0
  50. package/dist/blueprints/assets/photos/portfolio/tidewater-cover.jpg +0 -0
  51. package/dist/blueprints/assets/photos/portfolio/tidewater-data.jpg +0 -0
  52. package/dist/blueprints/assets/photos/portfolio/tidewater-report-cover.jpg +0 -0
  53. package/dist/blueprints/assets/photos/portfolio/warp-and-weft-cover.jpg +0 -0
  54. package/dist/blueprints/assets/photos/portfolio/warp-and-weft-room.jpg +0 -0
  55. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-components.jpg +0 -0
  56. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-cover.jpg +0 -0
  57. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-departures.jpg +0 -0
  58. package/dist/blueprints/assets/photos/restaurant/beetroot-goats-curd.jpg +0 -0
  59. package/dist/blueprints/assets/photos/restaurant/ceps-risotto.jpg +0 -0
  60. package/dist/blueprints/assets/photos/restaurant/chocolate-tart.jpg +0 -0
  61. package/dist/blueprints/assets/photos/restaurant/duck-leg-red-cabbage.jpg +0 -0
  62. package/dist/blueprints/assets/photos/restaurant/grilled-octopus.jpg +0 -0
  63. package/dist/blueprints/assets/photos/restaurant/hake-green-peppercorns.jpg +0 -0
  64. package/dist/blueprints/assets/photos/restaurant/hero.jpg +0 -0
  65. package/dist/blueprints/assets/photos/restaurant/poached-pear.jpg +0 -0
  66. package/dist/blueprints/assets/photos/restaurant/vegetable-broth.jpg +0 -0
  67. package/dist/blueprints/assets/photos/saas/adrian-tan.jpg +0 -0
  68. package/dist/blueprints/assets/photos/saas/api-webhooks.png +0 -0
  69. package/dist/blueprints/assets/photos/saas/approvals-queue.png +0 -0
  70. package/dist/blueprints/assets/photos/saas/audit-log.png +0 -0
  71. package/dist/blueprints/assets/photos/saas/erp-sync.png +0 -0
  72. package/dist/blueprints/assets/photos/saas/logo-brightwell-clinics.png +0 -0
  73. package/dist/blueprints/assets/photos/saas/logo-castlemere-foods.png +0 -0
  74. package/dist/blueprints/assets/photos/saas/logo-halvorsen-freight.png +0 -0
  75. package/dist/blueprints/assets/photos/saas/logo-northgate-homes.png +0 -0
  76. package/dist/blueprints/assets/photos/saas/logo-orrin-and-vale.png +0 -0
  77. package/dist/blueprints/assets/photos/saas/logo-tessaly-energy.png +0 -0
  78. package/dist/blueprints/assets/photos/saas/policy-editor.png +0 -0
  79. package/dist/blueprints/assets/photos/saas/spend-reporting.png +0 -0
  80. package/dist/blueprints/assets/photos/saas/sso-scim.png +0 -0
  81. package/dist/blueprints/assets/photos/store/brushed-cotton-overshirt.jpg +0 -0
  82. package/dist/blueprints/assets/photos/store/cable-knit-beanie.jpg +0 -0
  83. package/dist/blueprints/assets/photos/store/canvas-shoulder-bag.jpg +0 -0
  84. package/dist/blueprints/assets/photos/store/canvas-tote.jpg +0 -0
  85. package/dist/blueprints/assets/photos/store/cast-iron-skillet.jpg +0 -0
  86. package/dist/blueprints/assets/photos/store/enamel-mug.jpg +0 -0
  87. package/dist/blueprints/assets/photos/store/field-jacket.jpg +0 -0
  88. package/dist/blueprints/assets/photos/store/heavyweight-t-shirt.jpg +0 -0
  89. package/dist/blueprints/assets/photos/store/hero.jpg +0 -0
  90. package/dist/blueprints/assets/photos/store/leather-card-holder.jpg +0 -0
  91. package/dist/blueprints/assets/photos/store/linen-table-runner.jpg +0 -0
  92. package/dist/blueprints/assets/photos/store/porcelain-pour-over-set.jpg +0 -0
  93. package/dist/blueprints/assets/photos/store/striped-wool-blanket.jpg +0 -0
  94. package/dist/blueprints/assets/photos/vitrine/avatar-1.jpg +0 -0
  95. package/dist/blueprints/assets/photos/vitrine/avatar-2.jpg +0 -0
  96. package/dist/blueprints/assets/photos/vitrine/avatar-3.jpg +0 -0
  97. package/dist/blueprints/assets/photos/vitrine/case-council.jpg +0 -0
  98. package/dist/blueprints/assets/photos/vitrine/case-hotels.jpg +0 -0
  99. package/dist/blueprints/assets/photos/vitrine/case-rail.jpg +0 -0
  100. package/dist/blueprints/assets/photos/vitrine/exhibit-working-capital.png +0 -0
  101. package/dist/blueprints/assets/photos/vitrine/hero-review.jpg +0 -0
  102. package/dist/blueprints/assets/photos/vitrine/logo-aldermoor.png +0 -0
  103. package/dist/blueprints/assets/photos/vitrine/logo-brightwater.png +0 -0
  104. package/dist/blueprints/assets/photos/vitrine/logo-castell-vane.png +0 -0
  105. package/dist/blueprints/assets/photos/vitrine/logo-meridian-rail.png +0 -0
  106. package/dist/blueprints/assets/photos/vitrine/logo-nordvik.png +0 -0
  107. package/dist/blueprints/assets/photos/vitrine/logo-oakfield-health.png +0 -0
  108. package/dist/blueprints/association.d.ts +322 -0
  109. package/dist/blueprints/association.d.ts.map +1 -0
  110. package/dist/blueprints/association.js +1440 -0
  111. package/dist/blueprints/association.js.map +1 -0
  112. package/dist/blueprints/blog.d.ts +221 -0
  113. package/dist/blueprints/blog.d.ts.map +1 -0
  114. package/dist/blueprints/blog.js +1132 -0
  115. package/dist/blueprints/blog.js.map +1 -0
  116. package/dist/blueprints/content-pack.d.ts +148 -0
  117. package/dist/blueprints/content-pack.d.ts.map +1 -0
  118. package/dist/blueprints/content-pack.js +111 -0
  119. package/dist/blueprints/content-pack.js.map +1 -0
  120. package/dist/blueprints/content-packs.d.ts +12 -0
  121. package/dist/blueprints/content-packs.d.ts.map +1 -0
  122. package/dist/blueprints/content-packs.js +30 -0
  123. package/dist/blueprints/content-packs.js.map +1 -0
  124. package/dist/blueprints/demo-media.d.ts +39 -0
  125. package/dist/blueprints/demo-media.d.ts.map +1 -0
  126. package/dist/blueprints/demo-media.js +31 -0
  127. package/dist/blueprints/demo-media.js.map +1 -0
  128. package/dist/blueprints/documentation.d.ts +165 -0
  129. package/dist/blueprints/documentation.d.ts.map +1 -0
  130. package/dist/blueprints/documentation.js +1236 -0
  131. package/dist/blueprints/documentation.js.map +1 -0
  132. package/dist/blueprints/magazine.d.ts +207 -0
  133. package/dist/blueprints/magazine.d.ts.map +1 -0
  134. package/dist/blueprints/magazine.js +1076 -0
  135. package/dist/blueprints/magazine.js.map +1 -0
  136. package/dist/blueprints/menus.d.ts +25 -0
  137. package/dist/blueprints/menus.d.ts.map +1 -0
  138. package/dist/blueprints/menus.js +33 -0
  139. package/dist/blueprints/menus.js.map +1 -0
  140. package/dist/blueprints/photo-assets.d.ts +8 -0
  141. package/dist/blueprints/photo-assets.d.ts.map +1 -0
  142. package/dist/blueprints/photo-assets.js +41 -0
  143. package/dist/blueprints/photo-assets.js.map +1 -0
  144. package/dist/blueprints/portfolio.d.ts +207 -0
  145. package/dist/blueprints/portfolio.d.ts.map +1 -0
  146. package/dist/blueprints/portfolio.js +1032 -0
  147. package/dist/blueprints/portfolio.js.map +1 -0
  148. package/dist/blueprints/restaurant.d.ts +191 -0
  149. package/dist/blueprints/restaurant.d.ts.map +1 -0
  150. package/dist/blueprints/restaurant.js +1076 -0
  151. package/dist/blueprints/restaurant.js.map +1 -0
  152. package/dist/blueprints/saas.d.ts +268 -0
  153. package/dist/blueprints/saas.d.ts.map +1 -0
  154. package/dist/blueprints/saas.js +1472 -0
  155. package/dist/blueprints/saas.js.map +1 -0
  156. package/dist/blueprints/site-settings-seed.d.ts +17 -0
  157. package/dist/blueprints/site-settings-seed.d.ts.map +1 -0
  158. package/dist/blueprints/site-settings-seed.js +45 -0
  159. package/dist/blueprints/site-settings-seed.js.map +1 -0
  160. package/dist/blueprints/starting-skins.d.ts +19 -0
  161. package/dist/blueprints/starting-skins.d.ts.map +1 -0
  162. package/dist/blueprints/starting-skins.js +310 -0
  163. package/dist/blueprints/starting-skins.js.map +1 -0
  164. package/dist/blueprints/store.d.ts +290 -0
  165. package/dist/blueprints/store.d.ts.map +1 -0
  166. package/dist/blueprints/store.js +1031 -0
  167. package/dist/blueprints/store.js.map +1 -0
  168. package/dist/blueprints/vitrine.d.ts +266 -0
  169. package/dist/blueprints/vitrine.d.ts.map +1 -0
  170. package/dist/blueprints/vitrine.js +1056 -0
  171. package/dist/blueprints/vitrine.js.map +1 -0
  172. package/dist/blueprints/widgets.d.ts +26 -0
  173. package/dist/blueprints/widgets.d.ts.map +1 -0
  174. package/dist/blueprints/widgets.js +27 -0
  175. package/dist/blueprints/widgets.js.map +1 -0
  176. package/dist/demo-art/compositions.d.ts +48 -0
  177. package/dist/demo-art/compositions.d.ts.map +1 -0
  178. package/dist/demo-art/compositions.js +690 -0
  179. package/dist/demo-art/compositions.js.map +1 -0
  180. package/dist/demo-art/index.d.ts +8 -0
  181. package/dist/demo-art/index.d.ts.map +1 -0
  182. package/dist/demo-art/index.js +5 -0
  183. package/dist/demo-art/index.js.map +1 -0
  184. package/dist/demo-art/oklch.d.ts +39 -0
  185. package/dist/demo-art/oklch.d.ts.map +1 -0
  186. package/dist/demo-art/oklch.js +79 -0
  187. package/dist/demo-art/oklch.js.map +1 -0
  188. package/dist/demo-art/png.d.ts +24 -0
  189. package/dist/demo-art/png.d.ts.map +1 -0
  190. package/dist/demo-art/png.js +110 -0
  191. package/dist/demo-art/png.js.map +1 -0
  192. package/dist/demo-art/render.d.ts +228 -0
  193. package/dist/demo-art/render.d.ts.map +1 -0
  194. package/dist/demo-art/render.js +478 -0
  195. package/dist/demo-art/render.js.map +1 -0
  196. package/dist/index.d.ts +11 -0
  197. package/dist/index.d.ts.map +1 -0
  198. package/dist/index.js +7 -0
  199. package/dist/index.js.map +1 -0
  200. package/package.json +52 -0
@@ -0,0 +1,1032 @@
1
+ import { CogentaError } from '@cogenta/core';
2
+ import { createContentStore, createTaxonomyStore, defineCollection, defineTaxonomy, f, validateCollectionSet, validateTaxonomySet, } from '@cogenta/schema';
3
+ import { coverArt } from '../demo-art/compositions.js';
4
+ import { definePageCollection, SEO_FIELDS, toBlockZoneEntry, } from './content-pack.js';
5
+ import { STARTING_SKINS } from './starting-skins.js';
6
+ /**
7
+ * The `portfolio` blueprint: an independent design studio in London (L9 task
8
+ * 8; richened by L25; rewritten to studio level by L27).
9
+ *
10
+ * The model is shaped by what `@cogenta/theme-portfolio` shows:
11
+ *
12
+ * - `client`, `discipline` and `publishedAt` are plain fields a listing
13
+ * reads straight off the entry, for the caption under every cover
14
+ * (client, discipline, year). A listing sees raw fields, where a taxonomy
15
+ * field holds an id.
16
+ * - `client`, `disciplines` and `team` are also real taxonomies (ADR-0022).
17
+ * They give every client, discipline and member of the studio a public
18
+ * archive at `/<taxonomy>/<slug>`, and they are what a project page lists in
19
+ * its fact sheet, labelled with each taxonomy's own name. The year in the
20
+ * fact sheet is the project's `publishedAt`, the date the work was
21
+ * finished.
22
+ *
23
+ * The visuals of every project are real design work drawn for this blueprint
24
+ * (posters, book covers, signs, an identity sheet, report spreads, app
25
+ * screens, tins, exhibition graphics), rendered once with open-licence fonts
26
+ * and bundled under `assets/photos/portfolio/`. Nothing is generated at
27
+ * scaffold time.
28
+ *
29
+ * The copy names the studio the person scaffolding it chose
30
+ * (`SeedContext.siteName`), falling back to a fictional name only outside a
31
+ * real scaffold. Every client, person, place and figure is fictional.
32
+ */
33
+ export const DEFAULT_STUDIO_NAME = 'Studio Hale';
34
+ const TAXONOMY_PERMISSIONS = {
35
+ read: ['public'],
36
+ create: ['editor', 'admin'],
37
+ update: ['editor', 'admin'],
38
+ delete: ['admin'],
39
+ };
40
+ export const client = defineTaxonomy({
41
+ name: 'client',
42
+ labels: {
43
+ singular: { en: 'Client', fr: 'Client' },
44
+ plural: { en: 'Clients', fr: 'Clients' },
45
+ },
46
+ hierarchical: false,
47
+ permissions: TAXONOMY_PERMISSIONS,
48
+ });
49
+ export const disciplines = defineTaxonomy({
50
+ name: 'disciplines',
51
+ labels: {
52
+ singular: { en: 'Discipline', fr: 'Discipline' },
53
+ plural: { en: 'Disciplines', fr: 'Disciplines' },
54
+ },
55
+ hierarchical: false,
56
+ permissions: TAXONOMY_PERMISSIONS,
57
+ });
58
+ export const team = defineTaxonomy({
59
+ name: 'team',
60
+ labels: {
61
+ singular: { en: 'Team member', fr: 'Membre de l’équipe' },
62
+ plural: { en: 'Team', fr: 'Équipe' },
63
+ },
64
+ hierarchical: false,
65
+ permissions: TAXONOMY_PERMISSIONS,
66
+ });
67
+ export const project = defineCollection({
68
+ name: 'project',
69
+ labels: { singular: 'Project', plural: 'Projects' },
70
+ routing: { pattern: '/work/:slug' },
71
+ fields: {
72
+ title: f.text({ required: true, max: 200 }),
73
+ slug: f.slug({ from: 'title', unique: true }),
74
+ summary: f.text({
75
+ max: 300,
76
+ multiline: true,
77
+ admin: {
78
+ label: 'Statement',
79
+ help: 'One or two sentences under the title of the project page.',
80
+ },
81
+ }),
82
+ client: f.text({
83
+ max: 120,
84
+ admin: { label: 'Client', help: 'Set under the cover in lists of work.' },
85
+ }),
86
+ discipline: f.text({
87
+ max: 60,
88
+ admin: {
89
+ label: 'Discipline',
90
+ help: 'The one discipline set under the cover in lists, such as "Identity".',
91
+ },
92
+ }),
93
+ // Read by `entryImage` (`@cogenta/theme-kit`) for every listing and as the
94
+ // lead visual of the project page. Drawn at 3:2, subject in the middle.
95
+ coverImage: f.media({ accept: ['image'] }),
96
+ // The date the work was finished: the year in the caption and the fact
97
+ // sheet, and the order a reader expects the index in.
98
+ publishedAt: f.datetime(),
99
+ clientArchive: f.taxonomy({
100
+ of: 'client',
101
+ many: false,
102
+ admin: {
103
+ label: 'Client page',
104
+ help: 'Files the project under its client, which gives the client a page listing all its work with the studio.',
105
+ },
106
+ }),
107
+ disciplines: f.taxonomy({ of: 'disciplines', many: true }),
108
+ team: f.taxonomy({ of: 'team', many: true }),
109
+ blocks: f.blocks({ required: true }),
110
+ ...SEO_FIELDS,
111
+ },
112
+ indexes: [['slug']],
113
+ permissions: {
114
+ read: ['public'],
115
+ create: ['editor', 'admin'],
116
+ update: ['editor', 'admin'],
117
+ delete: ['admin'],
118
+ publish: ['admin'],
119
+ },
120
+ });
121
+ export const page = definePageCollection('/:slug');
122
+ export const PORTFOLIO_COLLECTIONS = [project, page];
123
+ export const PORTFOLIO_TAXONOMIES = [client, disciplines, team];
124
+ validateCollectionSet(PORTFOLIO_COLLECTIONS);
125
+ validateTaxonomySet(PORTFOLIO_TAXONOMIES, PORTFOLIO_COLLECTIONS);
126
+ const BLOCK_VERSION = '1.0.0';
127
+ let nodeKey = 0;
128
+ function nextKey(prefix) {
129
+ nodeKey += 1;
130
+ return `${prefix}-${nodeKey}`;
131
+ }
132
+ /** A run of text where `*words*` is set in italic (the title of a work). Nothing else is interpreted. */
133
+ function spans(text, key) {
134
+ return text
135
+ .split(/(\*[^*]+\*)/)
136
+ .filter((part) => part !== '')
137
+ .map((part, index) => {
138
+ const italic = part.startsWith('*') && part.endsWith('*') && part.length > 2;
139
+ return {
140
+ _key: `${key}-s${index}`,
141
+ _type: 'span',
142
+ text: italic ? part.slice(1, -1) : part,
143
+ marks: italic ? ['em'] : [],
144
+ };
145
+ });
146
+ }
147
+ function textBlock(style, text) {
148
+ const key = nextKey(`studio-${style}`);
149
+ return { _key: key, _type: 'block', style, children: spans(text, key), markDefs: [] };
150
+ }
151
+ const p = (text) => textBlock('normal', text);
152
+ const h2 = (text) => textBlock('h2', text);
153
+ function prose(...body) {
154
+ return {
155
+ _key: nextKey('studio-prose'),
156
+ _type: 'prose',
157
+ _version: BLOCK_VERSION,
158
+ body: [...body],
159
+ };
160
+ }
161
+ function figure(media, name, caption, align) {
162
+ const id = media[name];
163
+ if (id === undefined)
164
+ return [];
165
+ return [
166
+ {
167
+ _key: nextKey('studio-figure'),
168
+ _type: 'mediaFigure',
169
+ _version: BLOCK_VERSION,
170
+ media: id,
171
+ caption,
172
+ align,
173
+ },
174
+ ];
175
+ }
176
+ function gallery(media, names, layout) {
177
+ const items = names
178
+ .map((name) => media[name])
179
+ .filter((id) => id !== undefined)
180
+ .map((id) => ({ _key: nextKey('studio-picture'), media: id }));
181
+ if (items.length === 0)
182
+ return [];
183
+ return [
184
+ {
185
+ _key: nextKey('studio-gallery'),
186
+ _type: 'gallery',
187
+ _version: BLOCK_VERSION,
188
+ layout,
189
+ items,
190
+ },
191
+ ];
192
+ }
193
+ function quote(text, author, role) {
194
+ return {
195
+ _key: nextKey('studio-quote'),
196
+ _type: 'quote',
197
+ _version: BLOCK_VERSION,
198
+ text,
199
+ author,
200
+ role,
201
+ };
202
+ }
203
+ function testimonial(text, name, role) {
204
+ const key = nextKey('studio-testimonial');
205
+ return {
206
+ _key: key,
207
+ _type: 'testimonial',
208
+ _version: BLOCK_VERSION,
209
+ quote: [
210
+ {
211
+ _key: `${key}-p`,
212
+ _type: 'block',
213
+ style: 'normal',
214
+ children: [{ _key: `${key}-s`, _type: 'span', text, marks: [] }],
215
+ markDefs: [],
216
+ },
217
+ ],
218
+ attribution: { name, role },
219
+ };
220
+ }
221
+ export const PORTFOLIO_DEMO_DISCIPLINES = [
222
+ {
223
+ name: 'Identity',
224
+ slug: 'identity',
225
+ description: 'Names, marks, typefaces and the rules that let other people apply them well for years after we have gone.',
226
+ },
227
+ {
228
+ name: 'Print and editorial',
229
+ slug: 'editorial',
230
+ description: 'Books, annual reports, magazines and programmes, typeset by the people who designed them.',
231
+ },
232
+ {
233
+ name: 'Wayfinding',
234
+ slug: 'wayfinding',
235
+ description: 'Signs for stations, hospitals, campuses and terminals, tested on site with the people who use them.',
236
+ },
237
+ {
238
+ name: 'Digital products',
239
+ slug: 'digital',
240
+ description: 'Apps and services where the interface is the brand: tickets, bookings and the screens behind a counter.',
241
+ },
242
+ {
243
+ name: 'Packaging',
244
+ slug: 'packaging',
245
+ description: 'Labels, tins, cartons and shelf systems, proofed on the production line before they reach a shop.',
246
+ },
247
+ {
248
+ name: 'Exhibitions',
249
+ slug: 'exhibitions',
250
+ description: 'Title walls, room panels and object labels, written and set to be read standing up.',
251
+ },
252
+ ];
253
+ export const PORTFOLIO_DEMO_TEAM = [
254
+ { name: 'Mara Lindgren', slug: 'mara-lindgren' },
255
+ { name: 'Joel Okafor', slug: 'joel-okafor' },
256
+ { name: 'Ines Carvalho', slug: 'ines-carvalho' },
257
+ { name: 'Tom Reyes', slug: 'tom-reyes' },
258
+ { name: 'Priya Nair', slug: 'priya-nair' },
259
+ { name: 'Sam Whitford', slug: 'sam-whitford' },
260
+ { name: 'Ada Brennan', slug: 'ada-brennan' },
261
+ ];
262
+ /** `hello@studiohale.com` for "Studio Hale": the studio's own name, as a mailbox. */
263
+ export function studioEmail(siteName, mailbox = 'hello') {
264
+ const domain = siteName
265
+ .normalize('NFKD')
266
+ .replace(/[̀-ͯ]/g, '')
267
+ .toLowerCase()
268
+ .replace(/[^a-z0-9]+/g, '');
269
+ return `${mailbox}@${domain === '' ? 'studio' : domain}.com`;
270
+ }
271
+ function copyFor(siteName) {
272
+ return { siteName, email: studioEmail(siteName) };
273
+ }
274
+ export const PORTFOLIO_DEMO_PROJECTS = [
275
+ {
276
+ title: 'Warp and Weft',
277
+ slug: 'warp-and-weft',
278
+ client: { name: 'Bradwell Mill Museum', slug: 'bradwell-mill-museum' },
279
+ discipline: 'Exhibitions',
280
+ disciplines: ['exhibitions', 'identity'],
281
+ team: ['mara-lindgren', 'tom-reyes', 'priya-nair'],
282
+ publishedAt: '2022-04-12T09:00:00.000Z',
283
+ summary: 'Graphics for an exhibition about two centuries of cotton weaving, in a museum that occupies the mill itself: a woven title wall, room panels and three hundred object labels.',
284
+ cover: 'warp-and-weft-cover',
285
+ body: (_copy, media) => [
286
+ prose(h2('The brief'), p('Bradwell Mill Museum occupies the old spinning and weaving sheds of a mill that closed in 1981. For its fortieth year the museum planned the largest exhibition in its history: 340 objects in four galleries, from pattern books and loom parts to recordings of the people who worked there.'), p('The museum asked us for all of the graphics: the title wall, the room panels, the object labels and the posters that would bring the town in. The galleries are the mill’s own sheds, with iron columns, uneven light and walls that could not be drilled.')),
287
+ prose(h2('Woven, then written'), p('The identity is a plain weave drawn in the dyes the mill used: indigo, madder and weld yellow, over undyed cotton. At the entrance the weave is the size of a wall. On the posters, the room panels and the labels it shrinks to a single woven strip along the top edge.'), p('Most of the work was writing. With the curators we set a limit of 120 words for a room panel and 40 for a label, and every label begins with what the object was for before it says what it is made of. The type is Barlow Condensed, set large enough to be read without stepping over the rope, and every panel was proofed at full size on the gallery wall before it went to print.')),
288
+ ...figure(media, 'warp-and-weft-room', 'Room 2, the weaving shed: panel and object label.', 'end'),
289
+ prose(h2('Result'), p('The exhibition ran from April to November 2022 and drew 61,000 visitors, the museum’s highest attendance in a single year. The room panels are now part of the permanent display, and the museum’s own staff write new labels to the same limits.')),
290
+ ],
291
+ },
292
+ {
293
+ title: 'Four tins for a family cannery',
294
+ slug: 'gannet-and-sons',
295
+ client: { name: 'Gannet & Sons', slug: 'gannet-and-sons' },
296
+ discipline: 'Packaging',
297
+ disciplines: ['packaging', 'identity'],
298
+ team: ['joel-okafor', 'ines-carvalho'],
299
+ publishedAt: '2023-04-20T09:00:00.000Z',
300
+ summary: 'Packaging for a hundred-year-old cannery selling four of its tins under its own name for the first time: a fish for each variety, drawn from three shapes, and a label that says where the fish came from.',
301
+ cover: 'gannet-cover',
302
+ body: (_copy, media) => [
303
+ prose(h2('The brief'), p('Gannet & Sons has packed fish in Aldermouth since 1923. For most of that time its tins were sold under supermarket labels, and the family’s name appeared only in small print on the back. When the fourth generation decided to sell under their own name, in delicatessens and food halls, they had four products, a small budget and a shelf of competitors with beautiful tins.')),
304
+ prose(h2('Four fish'), p('Each variety has a colour and a fish of its own, drawn from the same three shapes: a lens for the body, a forked triangle for the tail and a dot for the eye. The sardine is cream on blue with a row of red spots, the mackerel carries its stripes, the anchovy is long and thin, and the sprats come in threes. The family name runs across the top of every tin in spaced capitals, with the town and the year the cannery opened.'), p('The band around the tin carries what the lid has no room for: where and when the fish was caught, how soon after landing it was packed, and what to do with a tin once it is open. Ines Carvalho wrote it with the family, in their words.')),
305
+ ...gallery(media, ['gannet-sardines', 'gannet-shelf'], 'grid'),
306
+ prose(h2('Result'), p('The tins were printed in runs of 20,000 per variety. Within a year they were stocked by 140 shops, from Edinburgh to Penzance, and the cannery added a fifth variety, smoked mackerel, which the family drew themselves from the same three shapes.')),
307
+ ],
308
+ },
309
+ {
310
+ title: 'A ticketing app for regional rail',
311
+ slug: 'westmoor-rail-app',
312
+ client: { name: 'Westmoor Rail', slug: 'westmoor-rail' },
313
+ discipline: 'Digital products',
314
+ disciplines: ['digital'],
315
+ team: ['ada-brennan', 'tom-reyes'],
316
+ publishedAt: '2023-11-06T09:00:00.000Z',
317
+ summary: 'An app for a regional rail operator that lets a passenger plan, buy and show a ticket in under a minute, built around the departures board passengers already trust.',
318
+ cover: 'westmoor-rail-cover',
319
+ body: (_copy, media) => [
320
+ prose(h2('The brief'), p('Westmoor Rail runs 410 trains a day between Westmoor Central, Aldermouth and Kirkby Fell. In 2022 a third of its tickets were still bought from machines at the station, many by passengers who had downloaded the operator’s previous app and given up at the payment screen.'), p('The operator wanted a new app, and a shorter queue at the Westmoor Central machines between seven and nine in the morning.')),
321
+ ...figure(media, 'westmoor-rail-departures', 'Live departures at Westmoor Central.', 'end'),
322
+ prose(h2('Start from the board'), p('We watched passengers read the departures boards and saw that they read them in the same order every time: the time, the destination, the platform. The main screen of the app repeats that order exactly, with the platform number in the same yellow square the stations use. Calling points and the service status sit under the destination in a smaller size, where a passenger who needs them looks.'), p('Buying a ticket takes three screens: where from and to, which ticket, and payment. The app keeps the last three journeys on its first screen, because that is how most commuters travel, and a season ticket renews from its own reminder.'), p('Ada Brennan led the design with four engineers at the operator, in two-week cycles, and every version was tried by passengers on the platform at Carrick Bridge before it shipped.')),
323
+ ...figure(media, 'westmoor-rail-components', 'Components: buttons, platform numbers, service status and the departure row.', 'wide'),
324
+ prose(h2('Result'), p('The app launched in November 2023. A year later it sold 41 percent of the operator’s tickets, a purchase took 52 seconds on average, and the morning queue at Westmoor Central had shortened enough for two of the eight machines to be moved to smaller stations.')),
325
+ ],
326
+ },
327
+ {
328
+ title: 'A cover system for paperback fiction',
329
+ slug: 'hollis-and-crane-paperbacks',
330
+ client: { name: 'Hollis & Crane', slug: 'hollis-and-crane' },
331
+ discipline: 'Book design',
332
+ disciplines: ['editorial'],
333
+ team: ['joel-okafor', 'priya-nair', 'sam-whitford'],
334
+ publishedAt: '2024-03-14T09:00:00.000Z',
335
+ summary: 'A cover system for the paperback fiction list of an independent publisher in Leith: one typographic rule, a cut-paper image for every book, and a shelf that reads as one list.',
336
+ cover: 'hollis-crane-cover',
337
+ body: ({ siteName }, media) => [
338
+ prose(h2('The brief'), p('Hollis & Crane publishes around twenty novels and story collections a year from a converted print works in Leith. Its hardbacks had a following. Its paperbacks, where most readers actually meet the list, were designed title by title and sold on the strength of the author alone. Booksellers told the sales team that they could not find the imprint on a table of new fiction.'), p('Ruth Crane, the publisher, wanted the paperbacks to be recognisable from three metres away without making twenty books look alike. The covers also had to be produced in a week by a production team of two, for print runs that are often no more than three thousand copies.')),
339
+ ...figure(media, 'hollis-crane-salt-year', '*The Salt Year* by Nora Pell, the first cover in the series.', 'start'),
340
+ prose(h2('One rule, many images'), p('The system has one fixed rule and a great deal of freedom. The author’s name sits at the top in small capitals. The title sits below it in EB Garamond, ranged left and never longer than three lines. At the foot there is a thin line and the name of the imprint in italic. Those positions never move, whatever the book.'), p('Between them is the image, and here every book gets its own. Each one is cut from paper in two colours at most, from a single idea in the book: a sun sinking into the sea for a novel set on a salt marsh, one lit window among dark ones for a story about neighbours, a crescent for a book written at night. The images are commissioned from Ida Moss, an illustrator in Glasgow, and scanned flat. Two colours keep the covers calm on a table crowded with photography, and they keep the printing cheap.'), p('The spines follow the same logic turned on its side: the title in Garamond, the author in capitals, the imprint’s mark at the base. Shelved together, a year’s titles read as a list, which is what a publisher’s backlist should look like in a good bookshop.')),
341
+ ...figure(media, 'hollis-crane-spines', 'Twelve spines from the 2024 list.', 'wide'),
342
+ ...figure(media, 'hollis-crane-grid', 'The cover grid, as given to the production team.', 'end'),
343
+ prose(h2('In the shops'), p('The first six paperbacks were published in March 2024. By the end of the year two national chains and eleven independent shops had given the list a table of its own, and the publisher’s paperback sales had risen by 22 percent in a year when fiction sales fell across the country.'), p(`The production team now briefs Ida Moss directly and sends ${siteName} each finished cover for a last look. It takes about ten minutes, and most weeks there is nothing to add.`)),
344
+ testimonial('We used to argue about every cover. Now we argue about the image, which is the right thing to argue about, and the rest of the cover looks after itself.', 'Ruth Crane', 'Publisher, Hollis & Crane'),
345
+ ],
346
+ },
347
+ {
348
+ title: 'Saltmarsh Quay ferry terminal',
349
+ slug: 'saltmarsh-quay',
350
+ client: { name: 'Aldermouth Harbour Board', slug: 'aldermouth-harbour-board' },
351
+ discipline: 'Wayfinding',
352
+ disciplines: ['wayfinding'],
353
+ team: ['tom-reyes', 'mara-lindgren', 'sam-whitford'],
354
+ publishedAt: '2024-09-02T09:00:00.000Z',
355
+ summary: 'Wayfinding for a rebuilt ferry terminal serving three islands: a family of signs, twelve pictograms and a number for every gate, tested with passengers before a single sign was made.',
356
+ cover: 'saltmarsh-quay-cover',
357
+ body: (_copy, media) => [
358
+ prose(h2('The brief'), p('Saltmarsh Quay carries 1.4 million passengers a year on ferries to Brenn, Holm and Sulvay, and a freight service that runs through the night. When the Aldermouth Harbour Board rebuilt the terminal in 2023, the new building doubled the waiting space and moved every gate. It also inherited forty years of signs, added one at a time, in four typefaces and three shades of blue.'), p('The board asked for a complete wayfinding system for the terminal, its car park and the walk to the bus station, to be installed in the eight weeks between the end of the winter timetable and the first summer sailing.')),
359
+ ...figure(media, 'saltmarsh-quay-gate', 'Gate sign in the departures hall.', 'wide'),
360
+ prose(h2('Who reads a sign'), p('Before drawing anything we spent four days in the old terminal, following passengers from the car park to the boats. Two things stood out. Most passengers were in no hurry, but they were anxious, carrying bags and children and trying to read a sign while walking. And about one in five on a summer weekend were visiting the islands for the first time and knew the island they were going to, never the name of the ferry.'), p('So the system is organised around the islands. Every gate has a number, set in a yellow square that can be seen from the entrance, and the name of its island in Atkinson Hyperlegible, a typeface designed for readers with low vision. The number and the name are repeated on every sign between the door and the gate, in the same order, so that a passenger never has to translate one sign into the next.')),
361
+ ...gallery(media, ['saltmarsh-quay-totem', 'saltmarsh-quay-pictograms'], 'grid'),
362
+ prose(h2('Pictograms and testing'), p('We drew twelve pictograms on a grid of one hundred units and tested each on paper with passengers waiting for the 11.15 to Sulvay. The first ferry was read as a factory. The first waiting room, a figure standing behind a bench, was read as a desk. Both were redrawn until nine people in ten named them correctly without a word beside them.'), p('Full-size cardboard versions of the overhead signs hung in the new building for a week before fabrication, and Tom Reyes walked the route with members of the Aldermouth Sight Loss Council. Two signs moved. One was taken out altogether, because it hid the gate numbers behind it.')),
363
+ prose(h2('Result'), p('All 214 signs were installed on time. In the first summer season the information desk recorded 38 percent fewer questions about where to go, and the harbour board has since adopted the system at its two smaller piers.')),
364
+ quote('Passengers used to queue at the desk to ask for gate three. Now the desk gets asked about the weather on Sulvay.', 'Callum Rennie', 'Terminal manager, Aldermouth Harbour Board'),
365
+ ],
366
+ },
367
+ {
368
+ title: 'Annual report 2024',
369
+ slug: 'tidewater-trust-annual-report',
370
+ client: { name: 'Tidewater Trust', slug: 'tidewater-trust' },
371
+ discipline: 'Print and editorial',
372
+ disciplines: ['editorial'],
373
+ team: ['priya-nair', 'sam-whitford'],
374
+ publishedAt: '2025-04-24T09:00:00.000Z',
375
+ summary: 'The annual report of a coastal conservation charity, designed and edited: forty-eight pages that open with what changed on the coast and keep the accounts complete and readable.',
376
+ cover: 'tidewater-cover',
377
+ body: (_copy, media) => [
378
+ prose(h2('The brief'), p('Tidewater Trust looks after 3,100 hectares of saltmarsh, mudflat and dune on the north-east coast. Its annual report has three readers: 6,400 members, the grant-makers who fund most of its restoration work, and the charity regulator. The earlier reports were written for the last of the three, and opened with eleven pages of governance before a single bird was mentioned.')),
379
+ ...figure(media, 'tidewater-report-cover', 'The cover, printed on uncoated recycled stock.', 'start'),
380
+ prose(h2('Coast first'), p('We reversed the order. The report now opens on the year’s work, one spread per site, each led by a single figure a member would care about: 412 hectares of saltmarsh returned to the tide, 140 wintering redshank at Hollow Marsh. The governance and the accounts follow in full, in the same typefaces and on the same grid, so that they read as part of the year.'), p('Libre Caslon sets the headlines and the large figures, and Work Sans sets the text and every table. The colours come from the estuary at low tide and code every chart the same way: dark blue for restoration, marsh green for the reserves, pale blue for learning.'), p('Priya Nair edited the text with the trust’s director over six weeks, taking it from 31,000 words to 14,000 without losing a number the auditors needed.')),
381
+ ...figure(media, 'tidewater-data', 'Finance and volunteering, pages 28 and 29.', 'wide'),
382
+ prose(h2('Result'), p('The report was printed in an edition of 7,000 and published online as a set of pages rather than a download. Two new funders quoted it in their award letters, and the trust has asked us to design the next three.')),
383
+ ],
384
+ },
385
+ {
386
+ title: 'Fenmore Building Society',
387
+ slug: 'fenmore-building-society',
388
+ client: { name: 'Fenmore Building Society', slug: 'fenmore-building-society' },
389
+ discipline: 'Identity',
390
+ disciplines: ['identity', 'wayfinding'],
391
+ team: ['ines-carvalho', 'joel-okafor'],
392
+ publishedAt: '2025-06-16T09:00:00.000Z',
393
+ summary: 'A new identity for a building society with 38 branches across the fens: a warm serif, a mark of five reeds and letters that sound like the person behind the counter.',
394
+ cover: 'fenmore-cover',
395
+ body: (_copy, media) => [
396
+ prose(h2('The brief'), p('Fenmore was founded in Ely in 1872 by farm workers who wanted somewhere safe to keep their wages. It now holds the savings of 190,000 members and the mortgages of 26,000 households, nearly all of them within forty miles of its head office. Its identity, a green shield with a sheaf of wheat, dated from a merger in 1994 and made the society look like a bank from somewhere larger.'), p('The board wanted an identity that looked local without looking old, and that its own staff could apply to everything from a branch fascia to a letter about a change of rate.')),
397
+ ...figure(media, 'fenmore-stationery', 'Letterhead and business cards.', 'start'),
398
+ prose(h2('Reeds and a serif'), p('The mark is five reeds of different heights standing on a line of water, drawn with rounded ends so that it survives being embroidered on a uniform or engraved on a cash machine. The name is set in Young Serif, heavy and warm, whose curves sit well beside the reeds. Everything else is set in Schibsted Grotesk, chosen for its clear figures on a statement.'), p('Four colours cover every use: Fen green, Reed, Chalk and Slate. Branch windows carry three words, savings, mortgages and advice, large enough to read from the other side of a market square.'), p('With Ines Carvalho the society also rewrote its forty-three standard letters, so that each one begins with what has happened to the member’s money and ends with the name of a person to ask.')),
399
+ ...figure(media, 'fenmore-branch', 'Branch front, Littleport.', 'wide'),
400
+ prose(h2('Rollout'), p('The identity went live on the website and on statements in June 2025, and branch by branch over the following nine months, each fascia replaced when the old one was due for repainting. Complaints about unclear letters halved in the first quarter.')),
401
+ testimonial('They listened to our branch staff before they showed us a single drawing, and the staff recognise their own suggestions in what we use every day.', 'Claire Denholm', 'Head of marketing, Fenmore Building Society'),
402
+ ],
403
+ },
404
+ {
405
+ title: 'The 2025/26 concert season',
406
+ slug: 'rookery-hall-season',
407
+ client: { name: 'Rookery Hall', slug: 'rookery-hall' },
408
+ discipline: 'Identity',
409
+ disciplines: ['identity', 'editorial'],
410
+ team: ['mara-lindgren', 'ines-carvalho', 'sam-whitford'],
411
+ publishedAt: '2025-09-01T09:00:00.000Z',
412
+ summary: 'A season identity for a concert hall in Manchester: four series, four colour pairs and one poster grid that the hall’s own team now fills in for sixty-two concerts a year.',
413
+ cover: 'rookery-hall-cover',
414
+ body: ({ siteName }, media) => [
415
+ prose(h2('The brief'), p('Rookery Hall presents sixty-two concerts a year, from the resident orchestra’s symphony nights to Saturday mornings for families. Until 2024 every concert was designed on its own, by whichever freelance designer was free that month, and the season brochure ran to forty pages that audiences told the box office they never finished.'), p('Helen Marsh, the hall’s director of programming, asked for something more modest than a rebrand. The name, the building and the orchestra were never the problem. What the hall lacked was a way of making sixty-two concerts look like one season, which its marketing team of three could keep up without calling a designer every week.')),
416
+ ...figure(media, 'rookery-hall-series', 'Four series, four colour pairs, four figures.', 'end'),
417
+ prose(h2('The system'), p('We started by sorting the season before drawing it. Every concert already belonged to one of four series, Symphonic, Chamber, New Music and Family, but the series were named only in small print. We made them the organising idea. Each series has a pair of colours and a single geometric figure: rings for the orchestra, five raised voices for chamber music, a climbing stair for new work and a scatter of circles for families.'), p('The type does the rest. Big Shoulders Display, a condensed face first drawn for the signs of Chicago, sets the composer’s name as large as the poster allows, so that a name of eight letters and a name of five both fill the width. Everything else sits on a strip of three columns at the foot of the poster: when, who and how much. The grid is fixed. A poster for a Tuesday lunchtime recital and a poster for the opening night differ only in their words, their series and their figure.'), p('We built the templates in the software the hall already used and wrote a twelve-page guide together with the marketing team. It covers the decisions they actually face: what to do with a title that is too long, how to credit a soloist, which photographs of a conductor may appear, and where.')),
418
+ ...gallery(media, ['rookery-hall-poster-sibelius', 'rookery-hall-poster-carnival'], 'grid'),
419
+ prose(h2('The brochure'), p('The season brochure went from forty pages to twenty-four. The first half introduces the season series by series; the second half is a calendar with one line per concert, coded by series so that a reader can find every chamber concert with one glance down the page. The hall printed 30,000 copies and handed them out through libraries, cafés and its members instead of posting them.')),
420
+ ...figure(media, 'rookery-hall-brochure', 'The season brochure, calendar spread.', 'wide'),
421
+ quote('The team can make a poster on a Tuesday afternoon and it looks like the season. That was the whole point.', 'Helen Marsh', 'Director of programming, Rookery Hall'),
422
+ prose(h2('The outcome'), p('The identity launched with the 2025/26 season in May. In the first four months the hall’s team produced 118 posters, banners and programme covers without outside help. Advance sales for the season were 14 percent higher than the year before, and sales for the New Music series, which the hall had considered cutting, rose by a third.'), p(`${siteName} still designs one poster a season, for the opening night. Everything else belongs to the hall.`)),
423
+ ],
424
+ },
425
+ ];
426
+ /** The blocks of one project page, in order. Media that is absent (a caller that never seeded any) is left out, never replaced. */
427
+ export function portfolioProjectBlocks(demo, copy, media) {
428
+ return demo.body(copyFor(copy.siteName), media);
429
+ }
430
+ const CLIENT_NAMES = [
431
+ 'Aldermouth Harbour Board',
432
+ 'Aldermouth City Council',
433
+ 'Bradwell Mill Museum',
434
+ 'Fell & Tarn',
435
+ 'Fenmore Building Society',
436
+ 'Gannet & Sons',
437
+ 'Hollis & Crane',
438
+ 'Kestrel Opera Company',
439
+ 'Lindley Public Library',
440
+ 'Marrick Architects',
441
+ 'Oakwright Joinery',
442
+ 'Rookery Hall',
443
+ 'Tidewater Trust',
444
+ 'Westmoor Rail',
445
+ ];
446
+ function disciplineRows(key) {
447
+ return {
448
+ _key: key,
449
+ _type: 'featureGrid',
450
+ _version: BLOCK_VERSION,
451
+ title: 'Disciplines',
452
+ items: PORTFOLIO_DEMO_DISCIPLINES.map((demo) => ({
453
+ _key: `${key}-${demo.slug}`,
454
+ title: demo.name,
455
+ text: demo.description,
456
+ link: { href: `/disciplines/${demo.slug}` },
457
+ })),
458
+ };
459
+ }
460
+ export function buildPortfolioHomeBlocks(options = {}) {
461
+ const { siteName, email } = copyFor(options.siteName?.trim() || DEFAULT_STUDIO_NAME);
462
+ return [
463
+ {
464
+ _key: 'home-statement',
465
+ _type: 'hero',
466
+ _version: BLOCK_VERSION,
467
+ title: `${siteName} designs identities, books, signs and exhibitions.`,
468
+ subtitle: 'An independent studio of sixteen people in Clerkenwell, London, working since 2011 for cultural institutions, public bodies and companies that make things.',
469
+ actions: [{ label: 'All work', target: { href: '/work' } }],
470
+ },
471
+ {
472
+ _key: 'home-work',
473
+ _type: 'collectionList',
474
+ _version: BLOCK_VERSION,
475
+ title: 'Selected work',
476
+ collection: 'project',
477
+ sort: { field: 'createdAt', direction: 'desc' },
478
+ limit: 6,
479
+ layout: 'grid',
480
+ },
481
+ disciplineRows('home-disciplines'),
482
+ {
483
+ _key: 'home-figures',
484
+ _type: 'stats',
485
+ _version: BLOCK_VERSION,
486
+ title: 'Since 2011',
487
+ items: [
488
+ { _key: 'home-figures-people', value: '16', label: 'people in the studio' },
489
+ { _key: 'home-figures-projects', value: '212', label: 'projects completed' },
490
+ {
491
+ _key: 'home-figures-returning',
492
+ value: '31',
493
+ label: 'clients who came back for a second project',
494
+ },
495
+ { _key: 'home-figures-signs', value: '1,900', label: 'signs installed and still standing' },
496
+ ],
497
+ },
498
+ quote('They took our season brochure from forty pages to twenty-four, and it sold more tickets than the forty ever did.', 'Helen Marsh', 'Director of programming, Rookery Hall'),
499
+ {
500
+ _key: 'home-clients',
501
+ _type: 'featureGrid',
502
+ _version: BLOCK_VERSION,
503
+ title: 'Clients',
504
+ items: CLIENT_NAMES.map((name, index) => ({ _key: `home-clients-${index}`, title: name })),
505
+ },
506
+ {
507
+ _key: 'home-index',
508
+ _type: 'collectionList',
509
+ _version: BLOCK_VERSION,
510
+ title: 'Index',
511
+ collection: 'project',
512
+ sort: { field: 'createdAt', direction: 'desc' },
513
+ limit: 12,
514
+ layout: 'list',
515
+ },
516
+ {
517
+ _key: 'home-contact',
518
+ _type: 'cta',
519
+ _version: BLOCK_VERSION,
520
+ title: 'New work',
521
+ text: 'Tell us what you are making, when it has to open and who it is for. Mara Lindgren replies to every enquiry within two working days.',
522
+ actions: [{ label: email, target: { href: `mailto:${email}` }, emphasis: 'primary' }],
523
+ },
524
+ ];
525
+ }
526
+ export function buildPortfolioDemoPages(options = {}) {
527
+ const media = options.media ?? {};
528
+ const siteName = options.siteName?.trim() || DEFAULT_STUDIO_NAME;
529
+ const { email } = copyFor(siteName);
530
+ const jobs = studioEmail(siteName, 'jobs');
531
+ const press = studioEmail(siteName, 'press');
532
+ return [
533
+ { title: 'Home', slug: 'home', blocks: buildPortfolioHomeBlocks({ siteName }) },
534
+ {
535
+ title: 'Work',
536
+ slug: 'work',
537
+ blocks: [
538
+ {
539
+ _key: 'work-grid',
540
+ _type: 'collectionList',
541
+ _version: BLOCK_VERSION,
542
+ collection: 'project',
543
+ sort: { field: 'createdAt', direction: 'desc' },
544
+ limit: 24,
545
+ layout: 'grid',
546
+ },
547
+ {
548
+ _key: 'work-disciplines',
549
+ _type: 'featureGrid',
550
+ _version: BLOCK_VERSION,
551
+ title: 'By discipline',
552
+ items: PORTFOLIO_DEMO_DISCIPLINES.map((demo) => ({
553
+ _key: `work-disciplines-${demo.slug}`,
554
+ title: demo.name,
555
+ link: { href: `/disciplines/${demo.slug}` },
556
+ })),
557
+ },
558
+ ],
559
+ },
560
+ {
561
+ title: 'Studio',
562
+ slug: 'studio',
563
+ blocks: [
564
+ prose(h2('About'), p(`${siteName} is a design studio of sixteen people in Clerkenwell, London. Mara Lindgren founded it in 2011, after ten years of designing signs and books for other studios, and it has been run by three partners since 2017.`), p('We work for institutions and companies that expect to be around for a long time: concert halls, publishers, museums, charities, transport operators and a few family businesses. Most of our projects are identities, printed matter, signs and exhibitions, and many of them are all four at once.'), p('The studio stays small on purpose. The partner who meets a client stays on the project until it is finished, and every designer here sets type, writes, and presents their own work.')),
565
+ ...gallery(media, [
566
+ 'rookery-hall-poster-sibelius',
567
+ 'hollis-crane-salt-year',
568
+ 'saltmarsh-quay-totem',
569
+ 'tidewater-report-cover',
570
+ 'westmoor-rail-departures',
571
+ 'warp-and-weft-room',
572
+ ], 'carousel'),
573
+ prose(h2('How we work'), p('A project starts with reading. Before we draw anything we read what the client already has, the old brochures and signs and letters, and we spend time where the work will be used: a box office on a Saturday morning, a ferry terminal in the rain.'), p('We present few directions, usually one, with the reasons written down beside it. We would rather spend the time making one idea work on everything it has to carry than polishing three for a meeting.'), p('Then we hand over the tools. Every identity leaves the studio with templates in the software the client’s team already uses and a guide written with that team, and a year later we visit to see what has been made.')),
574
+ {
575
+ _key: 'studio-principles',
576
+ _type: 'featureGrid',
577
+ _version: BLOCK_VERSION,
578
+ title: 'Principles',
579
+ items: [
580
+ {
581
+ _key: 'studio-principles-brief',
582
+ title: 'Read the brief twice',
583
+ text: 'Once for what it asks, and once for what it leaves out. The second reading usually changes the project.',
584
+ },
585
+ {
586
+ _key: 'studio-principles-year',
587
+ title: 'Design for the second year',
588
+ text: 'A launch is the easy part. We design for the tenth poster, the fortieth letter and the sign added after we have left.',
589
+ },
590
+ {
591
+ _key: 'studio-principles-proof',
592
+ title: 'Proof on the real thing',
593
+ text: 'Signs go on the wall, labels on the tin and type on the paper it will be printed on. A screen flatters everything.',
594
+ },
595
+ {
596
+ _key: 'studio-principles-write',
597
+ title: 'Write it down',
598
+ text: 'Every decision comes with a sentence that explains it, so a client can defend it in a meeting we are not in.',
599
+ },
600
+ ],
601
+ },
602
+ {
603
+ _key: 'studio-numbers',
604
+ _type: 'statCounter',
605
+ _version: BLOCK_VERSION,
606
+ title: 'The studio in numbers',
607
+ stats: [
608
+ { _key: 'studio-numbers-founded', value: '2011', label: 'Founded' },
609
+ { _key: 'studio-numbers-people', value: '16', label: 'People' },
610
+ { _key: 'studio-numbers-partners', value: '3', label: 'Partners' },
611
+ { _key: 'studio-numbers-projects', value: '212', label: 'Projects completed' },
612
+ {
613
+ _key: 'studio-numbers-languages',
614
+ value: '9',
615
+ label: 'Languages our work has been set in',
616
+ },
617
+ ],
618
+ },
619
+ {
620
+ _key: 'studio-partners',
621
+ _type: 'featureGrid',
622
+ _version: BLOCK_VERSION,
623
+ title: 'Partners',
624
+ items: [
625
+ {
626
+ _key: 'studio-partners-mara',
627
+ title: 'Mara Lindgren',
628
+ text: 'Founding partner. Signs and books. Before starting the studio she designed wayfinding for hospitals in Stockholm and London.',
629
+ link: { href: '/team/mara-lindgren' },
630
+ },
631
+ {
632
+ _key: 'studio-partners-joel',
633
+ title: 'Joel Okafor',
634
+ text: 'Partner since 2017. Identity and packaging. He teaches typography one day a week at an art school in London.',
635
+ link: { href: '/team/joel-okafor' },
636
+ },
637
+ {
638
+ _key: 'studio-partners-ines',
639
+ title: 'Ines Carvalho',
640
+ text: 'Partner since 2017. Identity and writing. She was art director at a publisher in Lisbon for eight years before joining.',
641
+ link: { href: '/team/ines-carvalho' },
642
+ },
643
+ ],
644
+ },
645
+ testimonial(`${siteName} listened to our branch staff before showing us a single drawing. The identity works because the people who use it every day recognise their own suggestions in it.`, 'Claire Denholm', 'Head of marketing, Fenmore Building Society'),
646
+ {
647
+ _key: 'studio-faq',
648
+ _type: 'faq',
649
+ _version: BLOCK_VERSION,
650
+ title: 'Working with us',
651
+ items: [
652
+ {
653
+ _key: 'studio-faq-start',
654
+ question: 'How does a project start?',
655
+ answer: [
656
+ p('With a conversation and a written brief, usually after a visit. We then send a proposal with a fixed fee for the first stage, research and one direction, so that a client can stop there if the fit is wrong.'),
657
+ ],
658
+ },
659
+ {
660
+ _key: 'studio-faq-cost',
661
+ question: 'What does a project cost?',
662
+ answer: [
663
+ p('Identities start at around £45,000 and wayfinding systems at around £60,000 before fabrication. Books, reports and exhibitions are quoted project by project. We give these ranges because a first conversation goes better when both sides know them.'),
664
+ ],
665
+ },
666
+ {
667
+ _key: 'studio-faq-abroad',
668
+ question: 'Do you work outside the UK?',
669
+ answer: [
670
+ p('About a quarter of our work is for clients elsewhere in Europe. We travel for research and installation and work from London in between.'),
671
+ ],
672
+ },
673
+ {
674
+ _key: 'studio-faq-placements',
675
+ question: 'Do you offer placements?',
676
+ answer: [
677
+ p('Two paid placements a year, starting in January and July. We reply to every application.'),
678
+ ],
679
+ },
680
+ ],
681
+ },
682
+ ],
683
+ },
684
+ {
685
+ title: 'Contact',
686
+ slug: 'contact',
687
+ blocks: [
688
+ {
689
+ _key: 'contact-new-work',
690
+ _type: 'cta',
691
+ _version: BLOCK_VERSION,
692
+ title: 'New projects',
693
+ text: 'Write to Mara Lindgren with what you are making, when it has to be ready and who it is for. She replies to every enquiry within two working days.',
694
+ actions: [
695
+ { label: email, target: { href: `mailto:${email}` }, emphasis: 'primary' },
696
+ { label: 'Telephone 020 7946 0184', target: { href: 'tel:+442079460184' } },
697
+ ],
698
+ },
699
+ prose(h2('Visit'), p('Second floor, 41 Hatherley Mews, Clerkenwell, London EC1R.'), p('The studio is a ten-minute walk from Farringdon station. We are glad to show work in progress by appointment, Monday to Thursday.')),
700
+ prose(h2('Jobs'), p(`We hire one or two designers most years, usually in the spring, and look for people who set type well and can explain their work in writing. Send a short letter and a portfolio of no more than twenty pages to ${jobs}.`)),
701
+ prose(h2('Press'), p(`For images of our work and permission to reproduce them, write to Ines Carvalho at ${press}. Please tell us where the images will appear.`)),
702
+ ],
703
+ },
704
+ {
705
+ title: 'Privacy',
706
+ slug: 'privacy',
707
+ blocks: [
708
+ prose(h2('Privacy'), p('This site sets no advertising or tracking cookies. It counts visits without storing anything that identifies you.'), p(`If you write to us, we keep your message and your address for as long as we need them to reply and to keep a record of any work we do together, and we never pass them to anyone else. To see or delete what we hold about you, write to ${email}.`), p(`${siteName} Ltd is registered in England and Wales, company number 07642318. Registered office: second floor, 41 Hatherley Mews, London EC1R.`)),
709
+ ],
710
+ },
711
+ ];
712
+ }
713
+ // ---------------------------------------------------------------------------
714
+ // Media, menus, settings
715
+ // ---------------------------------------------------------------------------
716
+ /**
717
+ * `portfolio`'s own starting skin (`starting-skins.js`), asserted present
718
+ * with a real check, not a `!`, since `STARTING_SKINS` is keyed by blueprint
719
+ * id and TypeScript cannot see that this particular key is always populated.
720
+ */
721
+ function portfolioPalette() {
722
+ const skin = STARTING_SKINS.portfolio;
723
+ if (skin === undefined) {
724
+ throw new CogentaError({
725
+ code: 'BLUEPRINT_REGISTRY_CORRUPT',
726
+ message: 'STARTING_SKINS.portfolio is missing.',
727
+ hint: 'The "portfolio" entry must stay declared in starting-skins.ts for this blueprint to seed its media.',
728
+ });
729
+ }
730
+ return skin.color;
731
+ }
732
+ /**
733
+ * The work itself: every file under `assets/photos/portfolio/`, one entry per
734
+ * image. Covers are 3:2 with their subject in the middle three quarters of the
735
+ * height, because the work grid shows its full-width places at 2:1; details
736
+ * keep the shape of the thing they show.
737
+ */
738
+ export const PORTFOLIO_MEDIA_SPECS = [
739
+ {
740
+ name: 'rookery-hall-cover',
741
+ alt: 'Three concert posters side by side for Rookery Hall’s 2025/26 season, in red, night blue and black',
742
+ },
743
+ {
744
+ name: 'rookery-hall-poster-sibelius',
745
+ alt: 'A red poster for Sibelius’s Fifth Symphony, with cream concentric half rings above the composer’s name',
746
+ },
747
+ {
748
+ name: 'rookery-hall-poster-carnival',
749
+ alt: 'A green family concert poster for The Carnival of the Animals, with orange circles above the title',
750
+ },
751
+ {
752
+ name: 'rookery-hall-series',
753
+ alt: 'Four colour tiles showing the Symphonic, Chamber, New Music and Family series, each with its own figure',
754
+ },
755
+ {
756
+ name: 'rookery-hall-brochure',
757
+ alt: 'An open season brochure: a red page reading 25/26 beside a calendar of concerts from October to March',
758
+ },
759
+ {
760
+ name: 'hollis-crane-cover',
761
+ alt: 'Four Hollis & Crane paperback covers in a row, each a flat colour with a cut-paper image under the title',
762
+ },
763
+ {
764
+ name: 'hollis-crane-salt-year',
765
+ alt: 'The cover of The Salt Year by Nora Pell: a pale sun sinking into a band of dark sea on sand-coloured paper',
766
+ },
767
+ {
768
+ name: 'hollis-crane-spines',
769
+ alt: 'Twelve paperback spines standing side by side, each with its title in a serif and the H&C mark at the base',
770
+ },
771
+ {
772
+ name: 'hollis-crane-grid',
773
+ alt: 'A cover with red construction lines beside notes on the typefaces, image field and margins of the series',
774
+ },
775
+ {
776
+ name: 'saltmarsh-quay-cover',
777
+ alt: 'Two navy overhead signs with white pictograms directing passengers to tickets, the café, gates, ferries and buses',
778
+ },
779
+ {
780
+ name: 'saltmarsh-quay-totem',
781
+ alt: 'A tall navy sign listing gates 1 to 4 in yellow squares with island names, and services with arrows below',
782
+ },
783
+ {
784
+ name: 'saltmarsh-quay-pictograms',
785
+ alt: 'Twelve white pictograms on navy squares, from ferries and buses to left luggage and the waiting room',
786
+ },
787
+ {
788
+ name: 'saltmarsh-quay-gate',
789
+ alt: 'A gate sign with a large yellow number 3 beside the destination Sulvay and the day’s sailing times',
790
+ },
791
+ {
792
+ name: 'fenmore-cover',
793
+ alt: 'The Fenmore Building Society identity: the reed mark and name on green, colour swatches and type specimens',
794
+ },
795
+ {
796
+ name: 'fenmore-stationery',
797
+ alt: 'A Fenmore letter confirming a savings account, with a green business card and a cream card beside it',
798
+ },
799
+ {
800
+ name: 'fenmore-branch',
801
+ alt: 'A branch front with the Fenmore name on a green fascia and the words savings, mortgages and advice in the window',
802
+ },
803
+ {
804
+ name: 'tidewater-cover',
805
+ alt: 'An annual report spread: the figure 412 on a blue page beside an article and a bar chart of redshank counts',
806
+ },
807
+ {
808
+ name: 'tidewater-report-cover',
809
+ alt: 'The Tidewater Trust Annual Report 2024 cover, sand-coloured above horizontal bands of blue and green',
810
+ },
811
+ {
812
+ name: 'tidewater-data',
813
+ alt: 'A report spread with a spending breakdown on the left and a chart of volunteers by year on the right',
814
+ },
815
+ {
816
+ name: 'westmoor-rail-cover',
817
+ alt: 'Three phone screens from the Westmoor Rail app: journey planner, a ticket with its code, and live departures',
818
+ },
819
+ {
820
+ name: 'westmoor-rail-departures',
821
+ alt: 'The live departures screen for Westmoor Central, listing five trains with their status and platform numbers',
822
+ },
823
+ {
824
+ name: 'westmoor-rail-components',
825
+ alt: 'A sheet of app components: buttons, yellow platform numbers, service status labels and a departure row',
826
+ },
827
+ {
828
+ name: 'gannet-cover',
829
+ alt: 'Four Gannet & Sons tins seen from above: sardines on blue, mackerel on yellow, anchovies on red, sprats on green',
830
+ },
831
+ {
832
+ name: 'gannet-sardines',
833
+ alt: 'The sardine tin with its cream fish on blue, above the unrolled paper band describing the catch',
834
+ },
835
+ {
836
+ name: 'gannet-shelf',
837
+ alt: 'Three shop shelves stocked with Gannet & Sons tins in four colours, priced at £3.40 a tin',
838
+ },
839
+ {
840
+ name: 'warp-and-weft-cover',
841
+ alt: 'The exhibition title wall: a woven pattern of indigo, red and yellow bands beside the words Warp and Weft',
842
+ },
843
+ {
844
+ name: 'warp-and-weft-room',
845
+ alt: 'A museum panel headed The weaving shed, with an object label for Jacquard loom cards beneath it',
846
+ },
847
+ ].map((item, index) => ({
848
+ ...item,
849
+ photo: `portfolio/${item.name}.jpg`,
850
+ spec: coverArt(portfolioPalette(), index + 1),
851
+ }));
852
+ /** Header and footer navigation. Every path is a page this blueprint seeds. */
853
+ export const PORTFOLIO_MENUS = {
854
+ header: [
855
+ { label: 'Work', url: '/work' },
856
+ { label: 'Studio', url: '/studio' },
857
+ { label: 'Contact', url: '/contact' },
858
+ ],
859
+ footer: [
860
+ { label: 'Work', url: '/work' },
861
+ { label: 'Studio', url: '/studio' },
862
+ { label: 'Contact', url: '/contact' },
863
+ { label: 'Privacy', url: '/privacy' },
864
+ ],
865
+ };
866
+ const PROJECT_PAGE = { kind: 'collection', collection: 'project' };
867
+ const SEARCH_RESULTS = { kind: 'search' };
868
+ const HOME = { kind: 'home' };
869
+ const archiveOf = (taxonomy) => ({
870
+ kind: 'taxonomy',
871
+ taxonomy,
872
+ });
873
+ const pagePath = (path) => ({ kind: 'path', path });
874
+ function shownOn(mode, ...targets) {
875
+ return { pages: { mode, targets } };
876
+ }
877
+ export const PORTFOLIO_WIDGETS = [
878
+ {
879
+ area: 'sidebar',
880
+ type: 'terms',
881
+ title: 'Disciplines',
882
+ settings: { taxonomy: 'disciplines', showCounts: true, hierarchical: false },
883
+ visibility: shownOn('only', archiveOf('disciplines'), SEARCH_RESULTS),
884
+ },
885
+ {
886
+ area: 'sidebar',
887
+ type: 'terms',
888
+ title: 'Clients',
889
+ settings: { taxonomy: 'client', showCounts: false, hierarchical: false },
890
+ visibility: shownOn('only', archiveOf('client')),
891
+ },
892
+ {
893
+ area: 'sidebar',
894
+ type: 'terms',
895
+ title: 'The studio',
896
+ settings: { taxonomy: 'team', showCounts: true, hierarchical: false },
897
+ visibility: shownOn('only', archiveOf('team')),
898
+ },
899
+ {
900
+ area: 'content-after',
901
+ type: 'relatedEntries',
902
+ title: 'More work',
903
+ settings: { count: 3, showImage: true, showDate: false },
904
+ visibility: shownOn('only', PROJECT_PAGE),
905
+ },
906
+ {
907
+ area: 'footer-1',
908
+ type: 'cta',
909
+ title: 'New work',
910
+ settings: {
911
+ heading: 'Tell us what you are making.',
912
+ body: 'When it has to be ready and who it is for: Mara Lindgren replies to every enquiry within two working days.',
913
+ label: 'Start a project',
914
+ href: '/contact',
915
+ },
916
+ visibility: shownOn('except', HOME, pagePath('/contact')),
917
+ },
918
+ ];
919
+ export const PORTFOLIO_SITE_SETTINGS = {
920
+ 'general.tagline': 'Identity, print, wayfinding and exhibitions.',
921
+ 'general.socialLinks': [
922
+ { label: 'Instagram', url: 'https://instagram.com/example' },
923
+ { label: 'LinkedIn', url: 'https://linkedin.com/company/example' },
924
+ { label: 'Bluesky', url: 'https://bsky.app/profile/example.bsky.social' },
925
+ ],
926
+ 'general.footerNote': 'Second floor, 41 Hatherley Mews, Clerkenwell, London EC1R. Studio visits by appointment, Monday to Thursday.',
927
+ // A studio's case studies are not a discussion: no comment form under the
928
+ // work. An editor can open comments again per collection or per entry.
929
+ 'discussion.enabled': false,
930
+ };
931
+ export const PORTFOLIO_RECOMMENDED_AGENTS = [
932
+ {
933
+ name: 'performanceAgent',
934
+ package: '@cogenta/agents-builtin',
935
+ reason: 'Catches oversized project images before they slow the work grid down.',
936
+ },
937
+ {
938
+ name: 'seoAgent',
939
+ package: '@cogenta/agents-builtin',
940
+ reason: 'Audits project pages so each one can be found on its own, as well as from the grid.',
941
+ },
942
+ ];
943
+ // ---------------------------------------------------------------------------
944
+ // Seeding
945
+ // ---------------------------------------------------------------------------
946
+ /**
947
+ * Inserts the demo content through the real `ContentStore` and taxonomy
948
+ * stores, never mocked (house rule). Everything is published, since a theme
949
+ * lists only published entries.
950
+ *
951
+ * Projects are created oldest first, each with the date the work was
952
+ * finished, so the `createdAt` order every list sorts on agrees with the
953
+ * years a visitor reads.
954
+ */
955
+ async function seedPortfolioDemoContent(ctx) {
956
+ const { db, defaultLocale, adminId, media } = ctx;
957
+ const siteName = ctx.siteName?.trim() || DEFAULT_STUDIO_NAME;
958
+ const clientStore = createTaxonomyStore({ db, taxonomy: client });
959
+ const disciplineStore = createTaxonomyStore({ db, taxonomy: disciplines });
960
+ const teamStore = createTaxonomyStore({ db, taxonomy: team });
961
+ const projectStore = createContentStore({ db, collection: project, defaultLocale });
962
+ const pageStore = createContentStore({ db, collection: page, defaultLocale });
963
+ const disciplineIds = new Map();
964
+ for (const demo of PORTFOLIO_DEMO_DISCIPLINES) {
965
+ const term = await disciplineStore.create({
966
+ slug: demo.slug,
967
+ labels: { [defaultLocale]: demo.name },
968
+ });
969
+ disciplineIds.set(demo.slug, term.id);
970
+ }
971
+ const teamIds = new Map();
972
+ for (const demo of PORTFOLIO_DEMO_TEAM) {
973
+ const term = await teamStore.create({ slug: demo.slug, labels: { [defaultLocale]: demo.name } });
974
+ teamIds.set(demo.slug, term.id);
975
+ }
976
+ const clientIds = new Map();
977
+ for (const demo of PORTFOLIO_DEMO_PROJECTS) {
978
+ if (clientIds.has(demo.client.slug))
979
+ continue;
980
+ const term = await clientStore.create({
981
+ slug: demo.client.slug,
982
+ labels: { [defaultLocale]: demo.client.name },
983
+ });
984
+ clientIds.set(demo.client.slug, term.id);
985
+ }
986
+ for (const demo of PORTFOLIO_DEMO_PROJECTS) {
987
+ const cover = media[demo.cover];
988
+ await projectStore.create({
989
+ status: 'published',
990
+ createdBy: adminId,
991
+ values: {
992
+ title: demo.title,
993
+ slug: demo.slug,
994
+ summary: demo.summary,
995
+ client: demo.client.name,
996
+ discipline: demo.discipline,
997
+ publishedAt: demo.publishedAt,
998
+ clientArchive: clientIds.get(demo.client.slug) ?? null,
999
+ disciplines: demo.disciplines
1000
+ .map((slug) => disciplineIds.get(slug))
1001
+ .filter((id) => id !== undefined),
1002
+ team: demo.team
1003
+ .map((slug) => teamIds.get(slug))
1004
+ .filter((id) => id !== undefined),
1005
+ ...(cover === undefined ? {} : { coverImage: cover }),
1006
+ },
1007
+ blocks: {
1008
+ blocks: portfolioProjectBlocks(demo, { siteName }, media).map(toBlockZoneEntry),
1009
+ },
1010
+ });
1011
+ }
1012
+ for (const demo of buildPortfolioDemoPages({ media, siteName })) {
1013
+ await pageStore.create({
1014
+ status: 'published',
1015
+ createdBy: adminId,
1016
+ values: { title: demo.title, slug: demo.slug },
1017
+ blocks: { blocks: demo.blocks.map(toBlockZoneEntry) },
1018
+ });
1019
+ }
1020
+ }
1021
+ export const portfolioContentPack = {
1022
+ collections: PORTFOLIO_COLLECTIONS,
1023
+ taxonomies: PORTFOLIO_TAXONOMIES,
1024
+ recommendedAgents: PORTFOLIO_RECOMMENDED_AGENTS,
1025
+ seedDemoContent: seedPortfolioDemoContent,
1026
+ defaultTheme: '@cogenta/theme-portfolio',
1027
+ menus: PORTFOLIO_MENUS,
1028
+ widgets: PORTFOLIO_WIDGETS,
1029
+ siteSettings: PORTFOLIO_SITE_SETTINGS,
1030
+ mediaSpecs: PORTFOLIO_MEDIA_SPECS,
1031
+ };
1032
+ //# sourceMappingURL=portfolio.js.map