@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,1076 @@
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 `magazine` blueprint: an independent news and culture magazine for a
8
+ * mid-sized city (L9 task 8; richened by L25; rewritten to studio level by
9
+ * L27).
10
+ *
11
+ * Three classification decisions shape the model, each one read by
12
+ * `@cogenta/theme-magazine`:
13
+ *
14
+ * - `section` is a real taxonomy (ADR-0022), so every section has a public
15
+ * front at `/section/<slug>` and an article page can name its section as
16
+ * a kicker linking there.
17
+ * - `authors` is a taxonomy too. A byline on a magazine is not a login
18
+ * account: a columnist or a freelance critic has no business holding a
19
+ * user, and a taxonomy term gives every writer a public archive at
20
+ * `/author/<slug>` and the article page a byline it can link.
21
+ * - `kicker` is the short label a list sets above a headline (a topic, a
22
+ * form such as "Art review", or a columnist's name). It is plain text on
23
+ * purpose: a listing reads raw entry fields, where a taxonomy field holds
24
+ * an id.
25
+ *
26
+ * `frontPage` is the editor's choice of what the front page carries, which
27
+ * lets the section rails further down list everything else without
28
+ * repeating a story.
29
+ *
30
+ * The demo journalism names the publication the person scaffolding it chose
31
+ * (`SeedContext.siteName`), falling back to a fictional title only outside a
32
+ * real scaffold. The city, its institutions and every person quoted are
33
+ * fictional.
34
+ */
35
+ export const DEFAULT_PUBLICATION_NAME = 'The Meridian';
36
+ const TAXONOMY_PERMISSIONS = {
37
+ read: ['public'],
38
+ create: ['editor', 'admin'],
39
+ update: ['editor', 'admin'],
40
+ delete: ['admin'],
41
+ };
42
+ export const section = defineTaxonomy({
43
+ name: 'section',
44
+ labels: {
45
+ singular: { en: 'Section', fr: 'Rubrique' },
46
+ plural: { en: 'Sections', fr: 'Rubriques' },
47
+ },
48
+ hierarchical: false,
49
+ permissions: TAXONOMY_PERMISSIONS,
50
+ });
51
+ export const author = defineTaxonomy({
52
+ name: 'author',
53
+ labels: {
54
+ singular: { en: 'Author', fr: 'Auteur' },
55
+ plural: { en: 'Authors', fr: 'Auteurs' },
56
+ },
57
+ hierarchical: false,
58
+ permissions: TAXONOMY_PERMISSIONS,
59
+ });
60
+ export const article = defineCollection({
61
+ name: 'article',
62
+ labels: { singular: 'Article', plural: 'Articles' },
63
+ routing: { pattern: '/articles/:slug' },
64
+ fields: {
65
+ title: f.text({ required: true, max: 200 }),
66
+ slug: f.slug({ from: 'title', unique: true }),
67
+ kicker: f.text({
68
+ max: 60,
69
+ admin: {
70
+ label: 'Kicker',
71
+ help: 'The short label set above the headline in lists: a topic, a form such as "Art review", or a columnist’s name.',
72
+ },
73
+ }),
74
+ excerpt: f.text({
75
+ max: 300,
76
+ multiline: true,
77
+ admin: { label: 'Standfirst', help: 'One or two sentences under the headline.' },
78
+ }),
79
+ section: f.taxonomy({ of: 'section', many: false }),
80
+ authors: f.taxonomy({ of: 'author', many: true }),
81
+ // Read by `entryImage` (`@cogenta/theme-kit`) for every listing. An
82
+ // article that opens its body with a figure of the same photograph shows
83
+ // that figure, with its caption and credit, as its lead image.
84
+ coverImage: f.media({ accept: ['image'] }),
85
+ frontPage: f.boolean({
86
+ default: false,
87
+ admin: { label: 'Front page', help: 'Lists this article on the front page.' },
88
+ }),
89
+ // Declared so the store's publish-time default fires, and so a seeded
90
+ // article carries the date it was really published.
91
+ publishedAt: f.datetime(),
92
+ body: f.blocks({ required: true }),
93
+ ...SEO_FIELDS,
94
+ },
95
+ indexes: [['slug'], ['section'], ['frontPage']],
96
+ permissions: {
97
+ read: ['public'],
98
+ create: ['editor', 'admin'],
99
+ update: ['editor', 'admin'],
100
+ delete: ['admin'],
101
+ publish: ['admin'],
102
+ },
103
+ });
104
+ export const page = definePageCollection('/:slug');
105
+ export const MAGAZINE_COLLECTIONS = [article, page];
106
+ export const MAGAZINE_TAXONOMIES = [section, author];
107
+ validateCollectionSet(MAGAZINE_COLLECTIONS);
108
+ validateTaxonomySet(MAGAZINE_TAXONOMIES, MAGAZINE_COLLECTIONS);
109
+ const BLOCK_VERSION = '1.0.0';
110
+ let nodeKey = 0;
111
+ function nextKey(prefix) {
112
+ nodeKey += 1;
113
+ return `${prefix}-${nodeKey}`;
114
+ }
115
+ /** A run of text where `*words*` is set in italic (the title of a work). Nothing else is interpreted. */
116
+ function spans(text, key) {
117
+ return text
118
+ .split(/(\*[^*]+\*)/)
119
+ .filter((part) => part !== '')
120
+ .map((part, index) => {
121
+ const italic = part.startsWith('*') && part.endsWith('*') && part.length > 2;
122
+ return {
123
+ _key: `${key}-s${index}`,
124
+ _type: 'span',
125
+ text: italic ? part.slice(1, -1) : part,
126
+ marks: italic ? ['em'] : [],
127
+ };
128
+ });
129
+ }
130
+ function textBlock(style, text) {
131
+ const key = nextKey(`mag-${style}`);
132
+ return { _key: key, _type: 'block', style, children: spans(text, key), markDefs: [] };
133
+ }
134
+ const p = (text) => textBlock('normal', text);
135
+ const h2 = (text) => textBlock('h2', text);
136
+ function prose(...body) {
137
+ return {
138
+ _key: nextKey('mag-prose'),
139
+ _type: 'prose',
140
+ _version: BLOCK_VERSION,
141
+ body: [...body],
142
+ };
143
+ }
144
+ function pullQuote(text, who, role) {
145
+ return {
146
+ _key: nextKey('mag-quote'),
147
+ _type: 'quote',
148
+ _version: BLOCK_VERSION,
149
+ text,
150
+ author: who,
151
+ role,
152
+ };
153
+ }
154
+ export const MAGAZINE_DEMO_SECTIONS = [
155
+ {
156
+ name: 'News',
157
+ slug: 'news',
158
+ description: 'City hall, schools, transit, housing and the courts.',
159
+ },
160
+ {
161
+ name: 'Business',
162
+ slug: 'business',
163
+ description: 'The companies, workers and property decisions that shape the local economy.',
164
+ },
165
+ {
166
+ name: 'Culture',
167
+ slug: 'culture',
168
+ description: 'Reviews and reporting on the city’s art, theater, music and restaurants.',
169
+ },
170
+ {
171
+ name: 'Opinion',
172
+ slug: 'opinion',
173
+ description: 'Columnists and readers who live here and argue in good faith.',
174
+ },
175
+ ];
176
+ export const MAGAZINE_DEMO_AUTHORS = [
177
+ { name: 'Ruth Adebayo', slug: 'ruth-adebayo' },
178
+ { name: 'Tomas Lindqvist', slug: 'tomas-lindqvist' },
179
+ { name: 'Sam Okonjo', slug: 'sam-okonjo' },
180
+ { name: 'Priya Raman', slug: 'priya-raman' },
181
+ { name: 'Owen Mercer', slug: 'owen-mercer' },
182
+ { name: 'Celia Marchetti', slug: 'celia-marchetti' },
183
+ { name: 'Jonah Feld', slug: 'jonah-feld' },
184
+ { name: 'Lucía Ferrer', slug: 'lucia-ferrer' },
185
+ { name: 'Nadia Haddad', slug: 'nadia-haddad' },
186
+ { name: 'Graham Oyelaran', slug: 'graham-oyelaran' },
187
+ { name: 'Esther Kowalczyk', slug: 'esther-kowalczyk' },
188
+ { name: 'Marcus Bell', slug: 'marcus-bell' },
189
+ ];
190
+ // ---------------------------------------------------------------------------
191
+ // The journalism, oldest first
192
+ // ---------------------------------------------------------------------------
193
+ export const MAGAZINE_DEMO_ARTICLES = [
194
+ {
195
+ title: 'What the new rental licensing rules mean for small landlords',
196
+ slug: 'rental-licensing-rules-small-landlords',
197
+ kicker: 'Housing',
198
+ excerpt: 'The ordinance that takes effect on November 1 was written with large landlords in mind. Its $40 registration fee will also reach about 1,200 people who rent out a basement or a garage apartment.',
199
+ section: 'business',
200
+ authors: ['owen-mercer'],
201
+ publishedAt: '2026-08-24T07:00:00.000Z',
202
+ frontPage: false,
203
+ body: ({ siteName }) => [
204
+ prose(p('Most of the debate over Port Calder’s rental licensing ordinance has been about its rules for large buildings: annual inspections, a public registry of owners, fines for unregistered units. The provision likely to affect the most people is shorter, and it sits in Section 4.'), p('Section 4 requires every landlord renting out more than one unit to register each unit and pay an annual $40 inspection fee, whatever the size of the property. The city’s housing office estimates that about 1,200 residents rent out a basement apartment, a garage apartment or the second half of a duplex they live in, and that most of them have never registered anything with the city.'), p(`A staff memo obtained by ${siteName} describes the overlap as “an acceptable tradeoff for closing the registration gap.” It does not estimate how many small landlords might stop renting rather than register.`), p('Housing advocates disagree on the answer. Tobias Wren of the Lakeshore Housing Coalition expects the effect to be small. “Forty dollars is less than one day’s rent on a basement apartment,” he said. Maria Castellanos, who runs a legal clinic for tenants in Canal Ward, is less sure. Registration also triggers an inspection, and many older basement units would need egress windows or new wiring to pass it.'), p('“Some of those units will be fixed, which is good,” Castellanos said. “Some will be taken off the market, and the people living in them will not find anything cheaper.”'), h2('What small landlords need to do'), p('Registration opens on October 1 on the city’s website and at the permit counter on the second floor of City Hall. Landlords who register before January 31 will not be charged for their first inspection. A unit that fails its inspection has 90 days to be repaired before a fine applies.'), p('The housing office hotline has taken more calls about the $40 fee in its first two weeks than about any other part of the 60-page ordinance, according to a staff member who answers it.')),
205
+ ],
206
+ },
207
+ {
208
+ title: 'Keep two libraries open until 9 p.m. for a year, and count who comes',
209
+ slug: 'keep-libraries-open-late-and-count',
210
+ kicker: 'Marcus Bell',
211
+ excerpt: 'The library board wants to close the Northgate and Eastbank branches to save money. A cheaper experiment would tell the city what those two buildings are worth.',
212
+ section: 'opinion',
213
+ authors: ['marcus-bell'],
214
+ publishedAt: '2026-08-25T07:00:00.000Z',
215
+ frontPage: false,
216
+ body: ({ siteName }) => [
217
+ prose(p('The Port Calder Public Library board is considering closing two branches, Northgate and Eastbank, to fill a $2.1 million hole in its budget. Before it votes in November, I would like it to try the opposite for one year.'), p('Keep both branches open until 9 p.m. on weekdays, count who comes in after six, and publish the numbers every month. The extra staff hours would cost about $380,000 a year, according to figures the library itself published for a similar proposal in 2023. That is a fraction of the savings the board is chasing, and a small price for evidence.'), p('I taught eighth grade at Northgate Middle School for twenty-six years. The branch library across the street was where my students went when home was loud, cold or empty. It now closes at 6 p.m., which is about when most of their parents finish work.'), p('The board’s own figures show 88,000 visits to Northgate last year, even with shortened hours. They cannot show who stayed away because the doors were already locked by the time they could get there.'), p('A mobile library that visits twice a week is a fine service for a neighborhood that already has a branch. It is a poor replacement for a warm building with free internet that a thirteen-year-old can walk to on a Wednesday.'), p('Evening hours would also answer a question the board has never asked: who uses a library when it is open at the times working people are free. Two branch systems elsewhere in the region stay open until 8 p.m., and both report that their busiest hour of the day is the one after dinner.'), p('Run the experiment. If nobody comes after six, the board will have its answer, and I will write a column saying so.'), p(`*Marcus Bell taught at Northgate Middle School from 1994 to 2020 and writes for ${siteName}.*`)),
218
+ ],
219
+ },
220
+ {
221
+ title: 'The community radio station that finally found an overnight host',
222
+ slug: 'community-radio-overnight-host',
223
+ kicker: 'Radio',
224
+ excerpt: 'For two years a night-shift nurse kept Harbor Community Radio on the air whenever its automated playlist failed. This season the station gave him the hours for real.',
225
+ section: 'culture',
226
+ authors: ['sam-okonjo'],
227
+ publishedAt: '2026-08-26T07:00:00.000Z',
228
+ frontPage: false,
229
+ body: () => [
230
+ prose(p('For two years, whenever the automated playlist at Harbor Community Radio crashed in the middle of the night, the station went silent for a few minutes and then came back with a person talking. That person was Marcus Webb, a night-shift nurse at Port Calder General Hospital who lives three blocks from the studio and has a key.'), p('This month the volunteer-run station, which broadcasts on 91.3 FM from a converted storage room above the Eastbank Community Center, gave Webb the 1 a.m. to 5 a.m. slot on Thursday nights. It is unpaid, like every other show on the schedule, and it is his.'), p('He plans to fill most of it with a collection of 3,000 records that a retired disc jockey donated in 2019 and that nobody has had time to catalog. “I’m going to play them in the order they come out of the boxes,” Webb said, “and find out what we have.” His first letter has already arrived, from a listener on the night shift at the port who asked for more jazz.'), p('The overnight show is one of four new programs this season. The others are a Spanish-language news roundup produced by two journalism students at Northgate Community College, an hour of recordings sent in by local bands, and a series of oral histories with residents who arrived in the city as refugees. Three long-running shows have ended, including a sports call-in program that station manager Claire Anand said had not received a call in eleven months.'), p('The station runs on about $40,000 a year, most of it raised in a single pledge drive each spring. Anand said the new schedule was built around one rule: no show goes on air unless a second volunteer has been trained to run it.'), p('“We lost two programs the hard way, when the only person who knew how to run them moved away,” she said. “Marcus has already trained his backup.”')),
231
+ ],
232
+ },
233
+ {
234
+ title: 'The projectionist who kept forty years of the city’s film history in his garage',
235
+ slug: 'projectionist-kept-alhambra-film-notes',
236
+ kicker: 'Archives',
237
+ excerpt: 'Every film shown at the Alhambra between 1963 and 2003 got a typed page of notes. Frank Delury saved all of them, and the University of Port Calder has agreed to take them in.',
238
+ section: 'culture',
239
+ authors: ['celia-marchetti'],
240
+ publishedAt: '2026-08-28T07:00:00.000Z',
241
+ frontPage: true,
242
+ photo: {
243
+ media: 'archive-room',
244
+ caption: 'The Alhambra papers, 61 boxes of projection notes, in temporary storage at the university library.',
245
+ photographer: 'Ines Moura',
246
+ },
247
+ body: () => [
248
+ prose(p('For forty years, every film that played at the Alhambra on Canal Street got a single typed page. The page gave the title, the dates of the run and the number of reels, followed by a few lines for whichever projectionist was on duty next: the reel where the sound drops, the changeover that comes early, the print with a scratch through its second half.'), p('Nobody asked anyone to keep them. When the Alhambra closed in 2003, its owner told the staff to take whatever they wanted before the building was cleared. Frank Delury, who had worked in the projection booth since 1971, took the filing cabinets.'), p('They stayed in his garage in Eastbank for twenty-one years. Last month the University of Port Calder library agreed to catalog and digitize the collection, which fills 61 boxes and holds a little under 9,000 pages.'), h2('A record of what failed'), p('The archivist Teodora Vukić spent three weekends this spring sorting the papers on Delury’s kitchen table before recommending that the library take them. What makes the collection valuable, she said, is its record of the films nobody remembers.'), p('“The hits are documented everywhere,” Vukić said. “What you almost never find is a record of the films that opened on a Friday, played to forty people and were gone by Tuesday. Frank wrote those down too, because somebody still had to thread the projector.”'), p('Among them are a run of Italian westerns in the summer of 1968, a war film that played two nights in 1974 to a total of 83 people, and dozens of prints from regional distributors that appear in no national catalog. Vukić found notes on 214 films she could not identify in any database.'), p('The pages also record how the Alhambra changed. Notes from the early 1960s describe double bills and a newsreel. By the late 1970s they mention a second screen built into the old balcony, and by 1990 almost every page carries a warning about the new sound system, which the staff seem to have disliked from the day it was installed.'), h2('The booth, in writing'), p('The notes are also a record of the work itself. Delury and his colleagues wrote down which reels arrived damaged, which studios sent prints too late for a test screening and, in 1977, a two-page account of a fire in the rewinding room that closed the cinema for a week. The handwriting in the margins changes as projectionists came and went, and some pages carry corrections from three different people.'), p('Delury started as an usher at sixteen and moved into the booth when the senior projectionist broke his wrist. He learned to splice film on the job, he said, from a man who kept a razor blade in his shirt pocket and a list of every film he had ever shown taped inside the booth door.'), p('Delury, who is 79, remembers most of them. Sitting in his garage in August, he picked up a page from November 1985 and read out the note at the bottom, in his own hand: reel four runs short, watch the changeover, the audience will not notice if you are quick.'), p('“That was the whole job,” he said. “Making sure nobody noticed you were there.”')),
249
+ pullQuote('What you almost never find is a record of the films that opened on a Friday, played to forty people and were gone by Tuesday.', 'Teodora Vukić', 'Archivist'),
250
+ prose(h2('One condition'), p('The library will scan the collection over the next eighteen months and open it to researchers, with a public online index of every film listed. Delury keeps the original pages for the rest of his life, after which they pass to the library.'), p('The work will cost about $48,000, most of it covered by a grant from the state humanities council. The library has also asked former staff and moviegoers to lend photographs of the cinema, of which it has so far found only eleven, all of them taken from the street.'), p('He set one condition, which the library accepted without discussion. The catalog must name every projectionist who wrote a note, alongside every film. Delury has identified nineteen of them so far. He is still looking for the twentieth, a woman who signed her notes “M.K.” in the winter of 1966 and who, he believes, was the Alhambra’s only female projectionist before 1990.'), p('Anyone who worked at the Alhambra, or knows who did, can write to the university library’s special collections department. Vukić has already heard from two former ushers and from the grandson of the cinema’s first manager.')),
251
+ ],
252
+ },
253
+ {
254
+ title: 'The morning trains are full again. Midday service is running nearly empty',
255
+ slug: 'morning-trains-full-midday-empty',
256
+ kicker: 'Transport',
257
+ excerpt: 'Ridership on the Northgate commuter line is back to 94 percent of its 2019 level at rush hour and 41 percent the rest of the day. The transit authority must now decide what to do about it.',
258
+ section: 'business',
259
+ authors: ['owen-mercer'],
260
+ publishedAt: '2026-08-29T07:00:00.000Z',
261
+ frontPage: false,
262
+ photo: {
263
+ media: 'station-platform',
264
+ caption: 'Commuters wait for the 7:52 to downtown on the platform at Northgate station.',
265
+ photographer: 'Elena Vasquez',
266
+ },
267
+ body: () => [
268
+ prose(p('At 7:52 on a weekday morning, the platform at Northgate station is as crowded as it was in 2019. By 10 a.m., the trains passing through it carry an average of 38 passengers in cars built for 300.'), p('Figures published by the Harbor Transit Authority this week show rush-hour ridership on the Northgate commuter line back to 94 percent of its prepandemic level. Between the morning and evening peaks, ridership stands at 41 percent, and on Fridays it has barely moved since 2022.'), p('The pattern follows the way office work has settled in the city. Most downtown employers now expect staff at their desks three days a week, usually Tuesday to Thursday, and the trains show it: Wednesday morning ridership is above its 2019 level, while Friday mornings run at about 60 percent.'), h2('Two options on the table'), p('The authority is weighing two proposals for the timetable that takes effect in January. The first would cut midday service from every 20 minutes to every 40 and spend the savings, about $3.2 million a year, on two more trains in each rush hour. The second would keep the midday frequency and add weekend service, on the bet that off-peak riders return when trains come often enough to use without checking a timetable.'), p('Riders’ groups favor the second. “A train every 40 minutes is a train nobody takes unless they have to,” said Kofi Mensah of the Northgate Riders Association, who takes the 7:52 himself. “The midday rider is a nurse, a student, a retiree going to an appointment. Cut their train and they buy a car.”'), p('The authority’s planners say the budget does not allow both. Its chief planning officer, Ingrid Sorensen, said a cost comparison of the two options would be published in October, followed by a public hearing before the board votes in November.'), p('Whichever option wins, the authority will run four-car trains on Tuesday, Wednesday and Thursday mornings from October, after months of complaints from passengers standing all the way from Northgate to downtown.')),
269
+ ],
270
+ },
271
+ {
272
+ title: 'An empty storefront is sometimes just a retirement',
273
+ slug: 'empty-storefront-sometimes-a-retirement',
274
+ kicker: 'Esther Kowalczyk',
275
+ excerpt: 'Every closed shop on Vine Street gets the same eulogy about rents and decline. Some of them deserve it. Treating them all alike makes the real losses harder to see.',
276
+ section: 'opinion',
277
+ authors: ['esther-kowalczyk'],
278
+ publishedAt: '2026-08-31T07:00:00.000Z',
279
+ frontPage: false,
280
+ body: ({ siteName }) => [
281
+ prose(p('When a storefront goes dark in this city, the obituary writes itself: rising rents, a changing neighborhood, the end of something that cannot be replaced. Sometimes all of that is true. Sometimes the owner simply retired.'), p('The corner bakery at 9th and Vine closed in March, and the neighborhood association’s newsletter gave a full page to “another casualty of the rent crisis.” When I called the owner, she laughed. She is 68, her children have no interest in running a bakery, and she had tried to sell the business for three years before deciding to lock the door and enjoy her mornings.'), p('The newsletter did her a small disservice as well. Three neighbors called after it ran to ask whether she needed money to reopen, and one offered to organize a fundraiser. She had to explain, three times, that she was fine and would be at the farmers market on Saturday.'), p('Rents are rising, and they are closing businesses. I know four owners on Vine Street who lost their shops this year when their leases came up for renewal, and all four agreed to be named in this magazine’s coverage. They are angry, and they are right to be.'), p('The trouble with the reflex is that it erases the difference between a landlord’s decision and a person’s plans. A neighborhood that treats every vacancy as proof of decline stops asking the useful questions: who owns the building, what the renewal asked for, and whether anyone tried to buy the business and could not get a loan.'), p('Those answers point to different remedies. A retiring owner needs a buyer, and the city’s small business office could help match one. A tenant facing a doubled rent needs lease protections, which the council has discussed for two years without a vote.'), p('Grief for a corner bakery is real, and I share it. It should come with a phone call to find out what actually happened.'), p(`*Esther Kowalczyk has lived on Vine Street since 1994 and writes about neighborhoods for ${siteName}.*`)),
282
+ ],
283
+ },
284
+ {
285
+ title: 'Three small theaters, one unexpected season',
286
+ slug: 'three-small-theaters-one-season',
287
+ kicker: 'Theater',
288
+ excerpt: 'The Riverside Players, the Loft and the Blackbox Collective all came close to closing in 2024. All three are now selling out, for three different reasons.',
289
+ section: 'culture',
290
+ authors: ['jonah-feld'],
291
+ publishedAt: '2026-09-01T07:00:00.000Z',
292
+ frontPage: false,
293
+ photo: {
294
+ media: 'theater-stage',
295
+ caption: 'An open rehearsal at the Loft on Mill Street, which has added a Thursday performance this season.',
296
+ photographer: 'Ines Moura',
297
+ },
298
+ body: () => [
299
+ prose(p('Eighteen months ago, two of the three small theaters now selling out their runs in Port Calder were one bad season away from giving up their leases. The third had already given notice.'), p('The three artistic directors do not agree on what changed, and their explanations are different enough that they may all be right.'), p('The Riverside Players moved their main season from Friday-to-Sunday runs to Wednesday-to-Saturday. Artistic director Hal Brennan said the change followed a survey of the company’s mailing list, which found that a large share of its audience worked weekend shifts at the hospital and the port. Midweek attendance rose by 70 percent in the first season.'), p('At the Loft on Mill Street, a 40-second rehearsal video posted by a cast member brought in an audience about twenty years younger than the company’s subscribers. “We did nothing,” said artistic director Femi Adeyemi. “The video did it, and then we had to be good enough that they came back.” About half of those new ticket buyers returned for a second production, according to the Loft’s box office.'), p('The Blackbox Collective raised its ticket prices from $18 to $32, the first increase in six years, expecting to lose part of its audience. It lost almost none, and used the extra money to pay its actors for rehearsals as well as performances.'), p('“People had been telling us for years that the tickets were too cheap to take seriously,” said managing director Wren Adisa. “We thought they were being polite.”'), p('What the three share is their size. None seats more than 140 people, none owns its building, and all three depend on volunteers to run the front of house. A bad season could still undo any of them. For now, the Loft has added a Thursday performance, and the Blackbox Collective has a waiting list for its December production for the first time in its history.')),
300
+ ],
301
+ },
302
+ {
303
+ title: 'Juniper Hill gardeners win a ten-year lease on city land',
304
+ slug: 'juniper-hill-garden-ten-year-lease',
305
+ kicker: 'Neighborhoods',
306
+ excerpt: 'After five years on a month-to-month permit, the Juniper Hill Community Garden has a lease that lets its volunteers plant trees, build a shed and plan past next spring.',
307
+ section: 'news',
308
+ authors: ['sam-okonjo'],
309
+ publishedAt: '2026-09-02T07:00:00.000Z',
310
+ frontPage: true,
311
+ photo: {
312
+ media: 'garden-volunteers',
313
+ caption: 'Volunteers gather before a Saturday planting session at the Juniper Hill Community Garden.',
314
+ photographer: 'Ines Moura',
315
+ },
316
+ body: () => [
317
+ prose(p('For five years the Juniper Hill Community Garden has operated on a permit the city could cancel with thirty days’ notice. Its volunteers grew vegetables and flowers, and nothing that took longer than a season.'), p('On Thursday the parks committee approved a ten-year lease on the 1.3-acre lot at the corner of Alder Street and Wren Avenue, at a rent of one dollar a year. The full council is expected to confirm it on October 2.'), p('“The first thing we’re planting is a row of pear trees,” said Marcus Oyelowo, who coordinates the garden’s 140 volunteers. “You don’t plant pear trees on a month-to-month permit.”'), p('The city took ownership of the lot after a warehouse fire in 2014, and neighbors began clearing it in 2021. The garden now has 62 raised beds allotted by lottery, a composting area that takes food scraps from three nearby restaurants, and a waiting list of 90 households.'), p('Volunteers grew about 2,300 pounds of vegetables last season, according to the garden’s own records, and gave a third of it to the food pantry at St. Brendan’s Church on Harbor Road. The lease also allows a weekly produce stand, which the garden plans to open next summer.'), p('The lease requires the garden to stay open to the public in daylight, to carry liability insurance and to let the city reclaim the site with two years’ notice if it is needed for housing. That clause drew the only objection at Thursday’s meeting, from a resident who argued the lot should be built on now.'), p('Parks director Simone Achebe said the city’s housing office assessed the site in 2023 and ranked it low for development because of soil contamination beneath the old warehouse floor. The raised beds, filled with imported soil, were built partly for that reason.'), p('The garden’s next project is a tool shed with a rainwater tank, paid for by a $14,000 neighborhood grant. The volunteers hope to finish it before the first frost.')),
318
+ ],
319
+ },
320
+ {
321
+ title: 'Two downtown office towers will become apartments. Other owners are watching the numbers',
322
+ slug: 'downtown-office-towers-become-apartments',
323
+ kicker: 'Property',
324
+ excerpt: 'With a fifth of downtown office space empty, the owners of the Lakeview Building and 200 Canal Street have filed plans for 410 apartments. The rest of the market is waiting for their construction costs.',
325
+ section: 'business',
326
+ authors: ['priya-raman'],
327
+ publishedAt: '2026-09-03T07:00:00.000Z',
328
+ frontPage: false,
329
+ photo: {
330
+ media: 'downtown-towers',
331
+ caption: 'Office towers along Canal Street, where a fifth of the floor space now stands empty.',
332
+ photographer: 'Ines Moura',
333
+ },
334
+ body: () => [
335
+ prose(p('Downtown Port Calder has 6.8 million square feet of office space. On an average weekday this summer, 21 percent of it stood empty, according to the commercial property firm Halvorsen Grant, up from 9 percent in 2019.'), p('Two owners have decided to stop waiting for tenants. Plans filed with the city this month would convert the 14-story Lakeview Building and the 11-story tower at 200 Canal Street into a combined 410 apartments, the first office-to-residential conversions downtown since the 1990s.'), h2('Why most towers cannot be converted'), p('Conversions are harder than they sound. Office floors built after about 1970 tend to be deep, which leaves the interior rooms of an apartment without windows, and every unit needs its own plumbing. The architect Hye-jin Park, who studied the downtown stock for the city’s planning department, estimates that only about a quarter of downtown towers could be converted for less than the cost of new construction.'), p('Both buildings in the current plans are older and narrower. The Lakeview Building was completed in 1929 and measures 62 feet from its windows to its central corridor, a depth Park described as close to ideal.'), p('“Everybody asks why we cannot turn every empty tower into housing,” she said. “The answer is mostly geometry.”'), p('The developers are relying on a tax abatement the council approved in 2025, which reduces property taxes on converted buildings for twelve years provided 15 percent of the units are rented below market rates. The two projects would include 62 such apartments.'), h2('Who is watching'), p('Other landlords are treating the two buildings as a test. Paul Ferreira, who directs Halvorsen Grant’s downtown office, said at least five owners had commissioned feasibility studies since the abatement passed, and that none would commit before seeing real construction costs from the first projects.'), p('For the businesses at street level, the arithmetic is simpler. Ana Beltrán, who runs a lunch counter in the lobby of 200 Canal Street, said her takings have fallen by almost half since 2019. “Office workers eat lunch,” she said. “People who live here eat breakfast and dinner. I can open earlier.”'), p('The planning commission will review both plans on October 15. If they are approved, construction could begin next spring, with the first residents moving in during 2028.')),
336
+ ],
337
+ },
338
+ {
339
+ title: 'The small stories are the ones this city remembers',
340
+ slug: 'small-stories-this-city-remembers',
341
+ kicker: 'Graham Oyelaran',
342
+ excerpt: 'The article that drew the most letters last year ran on page eleven. It belonged on page one, and the reasons it did not say something about how newsrooms decide what matters.',
343
+ section: 'opinion',
344
+ authors: ['graham-oyelaran'],
345
+ publishedAt: '2026-09-04T07:00:00.000Z',
346
+ frontPage: false,
347
+ body: ({ siteName }) => [
348
+ prose(p('Every autumn this magazine publishes dozens of stories about the city budget, the rail line and the stadium proposal. They are read, shared and argued over, as they should be. They are also rarely the stories readers bring up when they write to us.'), p(`The article that drew the most letters to ${siteName} last year was 400 words long. It described Doris Quaye, who helped children cross the corner of Alder Street and 9th Avenue every school morning for thirty-one years and retired without so much as a card from the school district. We ran it on page eleven. It belonged on page one.`), p('Newsrooms bury stories like hers for a practical reason. A council vote arrives with an agenda, a press release and a deadline. A retiring crossing guard arrives with nothing until a reporter happens to notice her, and noticing takes time a small staff does not always have.'), p('We are going to make that time. From this month, one reporter will spend two days a week on stories nobody has sent us: the people who keep a neighborhood running, and the institutions that close without a hearing. The Juniper Hill garden lease and the fight over the Northgate library are the first results.'), p('The reporter’s first assignment was a single block of Canal Ward, walked door to door over a week. It produced three stories, a correction to a city map that had listed a closed clinic as open for two years, and a list of names we will go back to.'), p('This work will sit beside our coverage of city hall, which is the reason many of you subscribe. A city is also its corners, and a newspaper that only covers the council chamber misses most of what holds a place together.'), p('If you know a story like that, write to me at the address at the foot of this page. I read every letter, and so does the reporter.'), p(`*Graham Oyelaran is the editor of ${siteName}.*`)),
349
+ ],
350
+ },
351
+ {
352
+ title: 'Larkin’s finally has a kitchen as good as its dining room',
353
+ slug: 'larkins-review-kitchen-matches-dining-room',
354
+ kicker: 'Restaurant review',
355
+ excerpt: 'The 1924 room on Harbor Road has always been the reason to book. Under chef Noor Salame, the food is a reason too.',
356
+ section: 'culture',
357
+ authors: ['lucia-ferrer'],
358
+ publishedAt: '2026-09-05T07:00:00.000Z',
359
+ frontPage: false,
360
+ photo: {
361
+ media: 'dining-room',
362
+ caption: 'The dining room at Larkin’s, which has kept its 1924 skylight and paneling through three owners.',
363
+ photographer: 'Elena Vasquez',
364
+ },
365
+ body: () => [
366
+ prose(p('For as long as anyone can remember, people have gone to Larkin’s for the room. The dining room on Harbor Road has a coffered ceiling, a glass skylight installed in 1924, dark paneling that has outlasted three owners and a flood, and candlelight that flatters everyone at every table. The food, for most of those years, was fine.'), p('Noor Salame took over the kitchen in March. Over three visits in the past month, her cooking has been considerably better than fine.'), p('Start with the smoked trout, served warm with pickled beets and a sharp horseradish cream, or with the charred leeks with hazelnuts and a soft egg, which is the best vegetable dish I have eaten in the city this year. The bread is baked in the kitchen, comes with cultured butter and is worth its $6.'), p('Salame spent eight years in hotel kitchens on the West Coast before moving to Port Calder, and it shows in the discipline of the main courses. A duck leg, cooked slowly and then crisped, arrives with lentils and a bitter-orange sauce that keeps its richness in check. Halibut with mussels and a saffron broth is generous and precisely timed. The one disappointment was a mushroom risotto that tasted mostly of cream.'), p('Desserts are simple and good: a dark chocolate tart with salted caramel, and a poached pear with mascarpone that is exactly as light as it should be after the duck.'), p('Salame has cut the menu to eleven dishes, down from the laminated list of thirty the previous owners served, and changes about half of them each month. By the second visit the trout had been replaced by cured mackerel with rhubarb, which was just as good.'), p('Service is warm, and occasionally slow on a full Saturday. The wine list is short, sensibly priced and strongest in French and Oregon reds, with eleven bottles under $50.'), p('Main courses cost $26 to $38. Larkin’s, 318 Harbor Road, serves dinner Tuesday to Saturday. Book a week ahead for a weekend table, and ask for one under the skylight.')),
367
+ ],
368
+ },
369
+ {
370
+ title: 'Banquet halls are booked through June. The caterers are still short of staff',
371
+ slug: 'banquet-halls-booked-caterers-short-staffed',
372
+ kicker: 'Hospitality',
373
+ excerpt: 'Weddings, retirement dinners and fundraisers have returned to Port Calder’s banquet halls in numbers not seen since 2019. The people who cook and serve at them have been slower to come back.',
374
+ section: 'business',
375
+ authors: ['priya-raman'],
376
+ publishedAt: '2026-09-07T07:00:00.000Z',
377
+ frontPage: true,
378
+ photo: {
379
+ media: 'banquet-hall',
380
+ caption: 'A fundraising dinner for the Eastbank Community Center at St. Brendan’s Hall.',
381
+ photographer: 'Elena Vasquez',
382
+ },
383
+ body: ({ siteName }) => [
384
+ prose(p('St. Brendan’s Hall on Harbor Road has 16 Saturdays left this year, and all of them are booked. So are most Fridays through next June, and a growing number of Thursdays, which the hall’s manager, Theresa Quigley, never used to bother advertising.'), p('“In 2021 I was calling people to ask whether they wanted their deposits back,” Quigley said. “Now I have couples asking whether we do Tuesdays.”'), p('Across the city’s eleven licensed banquet halls, bookings for events of more than 150 guests rose 38 percent in the first half of 2026 compared with the same period in 2019, according to figures the Port Calder Hospitality Association collected from its members. Spending per guest rose by a little more than a third, roughly in line with food prices.'), h2('Full rooms, short kitchens'), p(`The kitchens have not recovered at the same pace. Four caterers told ${siteName} they turn down between one and three events a month because they cannot staff them, and pay overtime on the events they accept.`), p('Harbourside Catering, the largest independent caterer in the city, had 94 people on its payroll in 2019. It has 61 now, most of them part time. Its owner, Farid Haddad, raised the starting wage for servers to $21 an hour last year and still loses staff to hotel kitchens that can promise regular weekly hours.'), p('“A wedding is eleven hours on your feet on a Saturday,” Haddad said. “A hotel offers the same money on a Tuesday morning, with a pension. I would take the hotel.”'), p('Some caterers have simplified their menus to cut the number of people needed on the night. A served three-course dinner needs roughly one server for every twelve guests; family-style platters and buffets need about half as many. Quigley said about a third of her autumn bookings had switched formats, sometimes at the caterer’s suggestion and sometimes at the couple’s.'), p('The kitchens have changed too. Harbourside now prepares most sauces and desserts two days before a large event and chills them, a practice Haddad resisted for years because he believed guests could taste the difference. “Nobody has complained,” he said. “The cooks have noticed, because they go home before one in the morning.”'), h2('A training kitchen in Eastbank'), p('The shortage has pushed the trade into an unusual partnership. Since February, Northgate Community College has run a twelve-week banquet service course in a borrowed kitchen at the Eastbank Community Center, with trainees paid a wage by a group of seven caterers and three halls. Of the first 24 students, 19 now work in the trade.'), p('The course coordinator, Lena Moreau, said it deliberately teaches the unglamorous parts of the job: carrying four plates at once, setting a room for 300 in ninety minutes and clearing it again by midnight. “Nobody grows up wanting to be a banquet server,” she said. “People find out they are good at it, and that it pays.”'), p('Students spend the last three weeks of the course working paid shifts at real events, each one paired with an experienced server from a sponsoring caterer. Moreau said the placements matter more than the classroom: most of the students hired so far were offered jobs by the caterer they shadowed.'), p('A second group of students started this month. The college has applied for state workforce funding to double the size of the course next year, and expects a decision in December.')),
385
+ pullQuote('A wedding is eleven hours on your feet on a Saturday. A hotel offers the same money on a Tuesday morning, with a pension.', 'Farid Haddad', 'Owner, Harbourside Catering'),
386
+ prose(h2('What it costs the customer'), p('For families planning an event, the practical result is less choice and earlier deposits. Quigley now asks for 30 percent of the room fee at booking, up from 10 percent before the pandemic, because a cancelled Saturday is almost impossible to fill at short notice. Haddad asks for final guest numbers three weeks ahead instead of one.'), p('Some families have moved smaller celebrations out of the halls altogether. The Eastbank Community Center rents its main room for $400 on a Saturday and allows outside food, and its bookings for family events have doubled since 2023, according to its director, Maureen Oduya.'), p('Neither expects the pressure to ease soon. Couples who postponed weddings during the pandemic have mostly married, Quigley said, while retirement parties for a large generation of hospital and city workers are only beginning. “I have three nursing retirement dinners in November alone,” she said. “Those are the nights when nobody leaves before midnight.”')),
387
+ ],
388
+ },
389
+ {
390
+ title: 'At the Kell, a crowded and generous survey of forty Port Calder painters',
391
+ slug: 'kell-gallery-local-color-review',
392
+ kicker: 'Art review',
393
+ excerpt: '“Local Color” hangs 112 works by artists who have lived in the city since 1950. It is too big, and it is the best exhibition the Kell Gallery has mounted in a decade.',
394
+ section: 'culture',
395
+ authors: ['celia-marchetti'],
396
+ publishedAt: '2026-09-08T07:00:00.000Z',
397
+ frontPage: true,
398
+ photo: {
399
+ media: 'gallery-opening',
400
+ caption: 'Visitors on the opening night of “Local Color” at the Kell Gallery.',
401
+ photographer: 'Elena Vasquez',
402
+ },
403
+ body: () => [
404
+ prose(p('*Local Color*, which opened at the Kell Gallery on Friday and runs until January 11, is the kind of exhibition curators are trained to avoid. It hangs 112 paintings by 40 artists across five rooms, frame touching frame in places, with no argument holding it together beyond the fact that everyone in it lived and worked in Port Calder.'), p('It is also the most enjoyable show the Kell has put on in a decade, and the crowd on opening night suggested the city agrees.'), p('The curator, Amara Whitfield, has arranged the rooms by decade rather than by style, which produces some happy collisions. In the first room, a harbor scene by Walter Brisbane from 1954, all fog and gray freighters, hangs beside Ruth Okafor’s *Canal Street, Noon* from the same year, a bright, flattened street painted as if from a passing streetcar. They could be two different cities, and in a sense they are: Brisbane lived on the water, Okafor above a laundry downtown.'), p('The middle rooms are the weakest. The 1970s and 1980s produced a great deal of large abstract painting here, much of it bought by the banks on Canal Street for their lobbies, and too much of it has come back for this show. A wall of five nearly identical color-field canvases could have been one.'), p('The exhibition recovers in its last two rooms. Delia Marsh’s portraits of her neighbors in Canal Ward, painted in the 1990s on the backs of old shop signs, are funny and tender and ought to be far better known. The final room, given to artists working now, includes Samuel Adu’s enormous painting of the Northgate rail yard at night, which the Kell bought for its permanent collection before the show opened.'), p('Whitfield has written the wall labels in plain language and signed each one, a small decision that makes the building feel less like an institution. Several labels quote the artists. One quotes Okafor’s landlord, who complained in 1956 about the smell of turpentine.'), p('Admission is free on Thursdays after 5 p.m. Go on a Thursday, give yourself two hours, and walk quickly through the middle rooms.')),
405
+ ],
406
+ },
407
+ {
408
+ title: 'Northgate residents fill a school gym to oppose closing two branch libraries',
409
+ slug: 'northgate-residents-oppose-library-closures',
410
+ kicker: 'Libraries',
411
+ excerpt: 'More than 300 people came to the first hearing on a plan to close the Northgate and Eastbank branches and replace them with a mobile library.',
412
+ section: 'news',
413
+ authors: ['sam-okonjo'],
414
+ publishedAt: '2026-09-09T07:00:00.000Z',
415
+ frontPage: true,
416
+ body: () => [
417
+ prose(p('The Port Calder Public Library expected about sixty people at Monday’s hearing and booked a classroom. Twenty minutes before it began, with the line outside reaching the parking lot, staff moved the meeting to the gymnasium of Northgate Middle School.'), p('The library board is considering closing the Northgate and Eastbank branches next July to close a $2.1 million gap in its budget. The two branches would be replaced by a mobile library, a converted bus that would visit each neighborhood two days a week.'), p('Of the 41 people who spoke, none supported the plan. Several described the branches as the only public place in their neighborhood that stays warm, quiet and free after school.'), p('“My son does his homework there because we have one laptop and three people who need it,” said Denise Alvarado, who lives four blocks from the Northgate branch. “A bus that comes on Tuesdays does nothing for him on a Wednesday.”'), p('The library director, Martin Osei, told the audience that no decision had been made and that usage figures for every branch would be published before the board votes in November. Northgate recorded 88,000 visits last year, the third highest of the city’s eleven branches. Eastbank recorded 41,000.'), p('Several speakers questioned the savings. Frank Okoro, a retired accountant who volunteers at the Eastbank branch, said the board’s estimate counted the full salaries of staff who would be moved to other branches instead of laid off. “The savings are smaller than the slide says,” he said. Students from Northgate Middle School presented a petition with 1,140 signatures collected over two weeks.'), p('Board member Helen Achterberg said she would propose keeping both branches open with shorter weekday hours and asking the council to restore $900,000 cut from the library budget in 2024. A second hearing is scheduled for October 6 at the Eastbank Community Center.')),
418
+ ],
419
+ },
420
+ {
421
+ title: 'Port Calder keeps planning for the commute it had in 2019',
422
+ slug: 'planning-for-the-commute-of-2019',
423
+ kicker: 'Nadia Haddad',
424
+ excerpt: 'The trains, the parking garages and the downtown lunch trade are all built around five days a week in the office. That week no longer exists.',
425
+ section: 'opinion',
426
+ authors: ['nadia-haddad'],
427
+ publishedAt: '2026-09-10T07:00:00.000Z',
428
+ frontPage: false,
429
+ body: ({ siteName }) => [
430
+ prose(p('Nearly every transportation decision this city has taken in the past two years assumes a commute that has disappeared.'), p('The parking garage approved for Canal Street last spring was sized using traffic counts from 2019. The timetable the Harbor Transit Authority is now revising still treats the midday train as a leftover between two rush hours. Even the case for the Harbor Line, which I support, leans on ridership forecasts built from five days a week downtown.'), p('That week is gone. The authority’s own figures show Wednesday morning trains fuller than in 2019 and Friday mornings at 60 percent. Downtown office occupancy averages 79 percent and falls closer to half on Mondays and Fridays. None of this is a crisis. It is a different city, and it moves differently.'), p('The people riding at midday are the ones planners call off-peak: hospital staff on rotating shifts, students, parents with small children, retirees on their way to appointments. They are the riders most likely to have no other way to travel, and the first any timetable cut will reach.'), p('There is a cheaper way to plan for the city we actually have. Count trips every month instead of every five years, and publish the counts. Design the garage so it can be converted later. Set the timetable so that frequency follows the riders instead of the office calendar.'), p('None of this needs new money. The authority already records every tap of a transit card, to the minute. It publishes the totals once a year, in a report that treats everything between the two rush hours as a single number.'), p('The council will spend this autumn arguing about stations for a line that opens in 2030. It should spend at least one evening asking what a Tuesday in Port Calder looks like now.'), p(`*Nadia Haddad is an urban planner, a member of the Northgate Riders Association and a columnist for ${siteName}.*`)),
431
+ ],
432
+ },
433
+ {
434
+ title: 'A Vine Street bakery turned down three buyout offers. Its owner explains why',
435
+ slug: 'vine-street-bakery-turned-down-buyout-offers',
436
+ kicker: 'Small business',
437
+ excerpt: 'A regional chain offered more than $600,000 for Okonkwo’s Bakery. Delphine Okonkwo-Reyes said no three times, and has started telling her customers about it at the counter.',
438
+ section: 'business',
439
+ authors: ['owen-mercer'],
440
+ publishedAt: '2026-09-11T07:00:00.000Z',
441
+ frontPage: true,
442
+ body: ({ siteName }) => [
443
+ prose(p('The first offer came in a letter. The second came with a lawyer. By the third, in June, Delphine Okonkwo-Reyes had stopped opening the envelopes and started telling the story to customers at the counter of Okonkwo’s Bakery on Vine Street.'), p(`The buyer was Continental Baking Group, a company based two hours north that has bought four independent bakeries in the Port Calder area since 2024. In each case it kept the bakery’s name on the sign and, within six months, moved most of the bread production to a central commissary, according to two former owners who spoke to ${siteName}.`), p('The last offer, confirmed by two people who saw it, was a little over $600,000, more than four times what Okonkwo-Reyes paid when she bought the business and its building from her mother in 2009.'), p('“They explained the number to me every time, as if I had not heard it the first time,” she said. “I heard it. I want my name on the door, and I want the bread made in the back.”'), p('The bakery employs nine people and bakes about 700 loaves a day, starting at 3:30 in the morning. Okonkwo-Reyes, who is 58, has begun training her head baker, Joel Amankwah, to take over within five years, and is working with a lawyer on an arrangement that would let him buy the business gradually out of its profits.'), p('Customers have noticed. A handwritten card taped to the register last week read, in full, “Please don’t sell.” She has left it there.'), p('Independent bakeries have become scarcer in the city. Of the fourteen that operated in Port Calder in 2019, nine are still run by the people who owned them then, according to the county’s business license records.'), p('Continental Baking Group did not respond to three requests for comment. Of the eleven storefronts on the block, the bakery is now one of two that have not changed hands since 2020.')),
444
+ ],
445
+ },
446
+ {
447
+ title: 'The quiet shift in who is leaving Port Calder',
448
+ slug: 'quiet-shift-in-who-is-leaving',
449
+ kicker: 'Housing',
450
+ excerpt: 'New census estimates show that renters over 60 were the largest group of households to leave the city core last year, and the city’s own permit records suggest why.',
451
+ section: 'news',
452
+ authors: ['tomas-lindqvist'],
453
+ publishedAt: '2026-09-12T07:00:00.000Z',
454
+ frontPage: true,
455
+ body: ({ siteName }) => [
456
+ prose(p('For a decade, the explanation for the shrinking population of Port Calder’s downtown has been the same: young families moving to the suburbs for space and schools. The latest census estimates, released on Thursday, point somewhere else.'), p('Of the roughly 2,400 households that left the city core between 2024 and 2025, 31 percent were headed by someone aged 60 or older, almost double that group’s share of the core’s population. Households with children under 18 made up 19 percent of departures, in line with their share of residents.'), p(`Grace Tanaka, a demographer at the Lakeshore Regional Planning Council, checked the estimates against utility disconnection records at ${siteName}’s request, a more reliable guide to real moves than changes of address. The two sources agreed within three percentage points.`), p('“The families story is true in some cities,” Tanaka said. “Here, the older renter is the person who moves when a building is sold and the new owner renovates. They are leaving because the rent doubled.”'), p('The pattern matches the city’s permit records. In 2025 the city issued renovation permits for 37 apartment buildings of twenty units or more in the core, the highest number since 2008. In at least 22 of them, rents for renovated units rose by more than 30 percent, according to listings compiled by the tenants’ group Canal Ward Renters.'), p('The group says the estimates match what its volunteers see at housing court. They counted 64 households headed by someone over 60 at eviction hearings in the first half of this year, most of them facing a lease that ended after their building was sold.'), h2('A bill waiting in committee'), p('Two council members have sponsored an ordinance that would require landlords to offer existing tenants a renovated unit at no more than a 10 percent increase. It was introduced in March and has not been scheduled for a hearing. Council member Adaeze Nwosu, one of its sponsors, said she would ask for a hearing in October now that the estimates are public.')),
457
+ ],
458
+ },
459
+ {
460
+ title: 'Council approves the Harbor Line after a decade of false starts',
461
+ slug: 'council-approves-harbor-line',
462
+ kicker: 'Transit',
463
+ excerpt: 'An 8-3 vote commits Port Calder to a 9.4-mile light rail line from Northgate to the Old Harbor. Utility work starts in April, and the first passengers are due to ride in 2030.',
464
+ section: 'news',
465
+ authors: ['ruth-adebayo', 'tomas-lindqvist'],
466
+ publishedAt: '2026-09-13T07:00:00.000Z',
467
+ frontPage: true,
468
+ photo: {
469
+ media: 'city-hall',
470
+ caption: 'City Hall on the morning after the vote. The council chamber was full until a little after 11 p.m.',
471
+ photographer: 'Elena Vasquez',
472
+ },
473
+ body: ({ siteName }) => [
474
+ prose(p('The Harbor Line has been drawn, costed, cancelled and redrawn four times since 2016. On Tuesday night, a little after eleven, the Port Calder City Council voted 8-3 to build it.'), p('The decision commits the city to a 9.4-mile light rail line from the Northgate park-and-ride to the Old Harbor ferry terminal, with fourteen stops, two of them underground beneath Canal Street. The approved budget is $1.62 billion, of which $710 million is expected to come from a federal transit grant the city applied for in March and has not yet received.'), p('The first phase of construction, moving water mains and power lines along Harbor Road, is scheduled to start in April. The Harbor Transit Authority says passengers will ride the full line in the autumn of 2030, and projects 41,000 weekday trips by 2032. Opponents call that figure optimistic: the busiest bus route in the corridor, the 12, carried 9,800 riders a day last year.'), h2('What changed this time'), p(`The 2019 version of the plan died in committee over the cost of its tunnel. The 2022 version lost its federal match when the authority missed a filing deadline by nine days. What carried this one, according to three council members who spoke to ${siteName} before the vote, is a contract structure the city has never used on a project of this size. A single consortium, Lakeshore Rail Partners, will design and build the line for a fixed price and operate it for fifteen years.`), p('“We have spent a decade paying consultants to tell us what it would cost,” said council member Adaeze Nwosu, who represents the Flats and chairs the transportation committee. “The fixed price is the first number anyone has been willing to sign.”'), p('The three votes against came from the members who blocked the 2022 plan. Council member Leonard Pryce, whose Eastbank ward the line does not reach, argued that a fifteen-year operating contract gives a private company too much say over fares and service.'), p('“If ridership disappoints, the city pays the difference,” Pryce said. “If it succeeds, the consortium keeps the gain. I have read the summary twice and I still cannot find the clause that protects the rider.”'), p('City staff dispute that reading. The contract caps the city’s annual operating payment at $48 million, adjusted for inflation, and sets penalties for late or cancelled trains, according to a summary released by the city attorney’s office on Monday. The full agreement runs to 412 pages and has not been published. The authority says it will post it once the federal grant is confirmed.'), h2('The businesses on the route'), p('Along Harbor Road, where two years of utility work will close one lane in each direction, the reaction was relief mixed with arithmetic. Rosa Delgado has run Delgado Hardware at Harbor and Vine for nineteen years, and still keeps the 2016 public notice for the line folded in a drawer behind the counter.'), p('“I believe it now that there’s a date,” she said on Wednesday morning, measuring out chain for a customer. “What I want to know is where my deliveries park for two years.”'), p('The authority has promised a loading plan for every block before work begins, and a $6 million fund for businesses that can show lost revenue during construction. Merchants on Canal Street, who lived through eighteen months of sewer replacement in 2021, have asked for the fund to pay out monthly instead of at the end of the project.')),
475
+ pullQuote('We have spent a decade paying consultants to tell us what it would cost. The fixed price is the first number anyone has been willing to sign.', 'Adaeze Nwosu', 'Chair of the council’s transportation committee'),
476
+ prose(h2('What happens next'), p('The federal grant decision is expected in January. Without it, the city would have to borrow another $710 million or shorten the line, most likely by ending it at Canal Street and leaving the last three stops to a later phase. Mayor Joanna Kessler’s office said the city would not break ground on the underground section without the grant in hand.'), p('Residents can comment on the station designs at four public sessions this autumn, the first on September 24 at the Northgate branch library. The authority has also agreed to publish ridership forecasts for each individual stop, a document opponents requested in 2022 and never received.'), p('For Pryce, the vote settles whether the line will be built and opens the question of how it will be run. “I lost tonight,” he said, on his way out of the chamber. “I intend to be the most persistent person at every one of those sessions.”')),
477
+ ],
478
+ },
479
+ ];
480
+ /** An article's whole body: its lead photograph as a captioned figure, when it has one and the scaffold ingested it, then its text. */
481
+ export function magazineArticleBlocks(demo, copy, media) {
482
+ const photoId = demo.photo === undefined ? undefined : media[demo.photo.media];
483
+ const lead = demo.photo === undefined || photoId === undefined
484
+ ? []
485
+ : [
486
+ {
487
+ _key: `${demo.slug}-lead`,
488
+ _type: 'mediaFigure',
489
+ _version: BLOCK_VERSION,
490
+ media: photoId,
491
+ caption: demo.photo.caption,
492
+ credit: `Photograph: ${demo.photo.photographer} for ${copy.siteName}`,
493
+ ratio: '3:2',
494
+ align: 'wide',
495
+ },
496
+ ];
497
+ return [...lead, ...demo.body(copy)];
498
+ }
499
+ function answer(key, text) {
500
+ return [
501
+ {
502
+ _key: key,
503
+ _type: 'block',
504
+ style: 'normal',
505
+ children: [{ _key: `${key}-span`, _type: 'span', text, marks: [] }],
506
+ markDefs: [],
507
+ },
508
+ ];
509
+ }
510
+ /**
511
+ * `home`, `about`, `subscribe` and `standards`: a function of the site's name
512
+ * and of the section term ids assigned at seed time. None of these pages
513
+ * carries a photograph, so unlike `blog.ts`'s `buildBlogDemoPages` it takes
514
+ * no media map.
515
+ *
516
+ * The home page lists no story twice outside "Most read": the front page
517
+ * carries the articles flagged for it, and every rail below filters those
518
+ * out.
519
+ */
520
+ export function buildMagazineDemoPages(options = {}) {
521
+ const siteName = options.siteName ?? DEFAULT_PUBLICATION_NAME;
522
+ const sectionFilter = (slug) => {
523
+ const id = options.sectionIds?.get(slug);
524
+ return id === undefined ? {} : { section: id };
525
+ };
526
+ return [
527
+ {
528
+ title: 'Front page',
529
+ slug: 'home',
530
+ blocks: [
531
+ {
532
+ _key: 'demo-home-front',
533
+ _type: 'collectionList',
534
+ _version: BLOCK_VERSION,
535
+ collection: 'article',
536
+ filter: { frontPage: true },
537
+ sort: { field: 'createdAt', direction: 'desc' },
538
+ limit: 8,
539
+ layout: 'grid',
540
+ },
541
+ {
542
+ _key: 'demo-home-opinion',
543
+ _type: 'collectionList',
544
+ _version: BLOCK_VERSION,
545
+ title: 'Opinion',
546
+ collection: 'article',
547
+ filter: sectionFilter('opinion'),
548
+ sort: { field: 'createdAt', direction: 'desc' },
549
+ limit: 4,
550
+ layout: 'carousel',
551
+ },
552
+ {
553
+ _key: 'demo-home-culture',
554
+ _type: 'collectionList',
555
+ _version: BLOCK_VERSION,
556
+ title: 'Culture',
557
+ collection: 'article',
558
+ filter: { ...sectionFilter('culture'), frontPage: false },
559
+ sort: { field: 'createdAt', direction: 'desc' },
560
+ limit: 3,
561
+ layout: 'grid',
562
+ },
563
+ {
564
+ _key: 'demo-home-most-read',
565
+ _type: 'collectionList',
566
+ _version: BLOCK_VERSION,
567
+ title: 'Most read',
568
+ collection: 'article',
569
+ sort: { field: 'createdAt', direction: 'asc' },
570
+ limit: 5,
571
+ layout: 'list',
572
+ },
573
+ {
574
+ _key: 'demo-home-business',
575
+ _type: 'collectionList',
576
+ _version: BLOCK_VERSION,
577
+ title: 'Business',
578
+ collection: 'article',
579
+ filter: { ...sectionFilter('business'), frontPage: false },
580
+ sort: { field: 'createdAt', direction: 'desc' },
581
+ limit: 3,
582
+ layout: 'grid',
583
+ },
584
+ {
585
+ _key: 'demo-home-subscribe',
586
+ _type: 'cta',
587
+ _version: BLOCK_VERSION,
588
+ title: 'Journalism for Port Calder, paid for by its readers',
589
+ text: `${siteName} keeps its coverage of city hall free for everyone to read. Subscriptions start at $8 a month and pay for sixteen journalists.`,
590
+ actions: [
591
+ { label: 'Subscribe', target: { href: '/subscribe' }, emphasis: 'primary' },
592
+ { label: 'Read our standards', target: { href: '/standards' } },
593
+ ],
594
+ },
595
+ ],
596
+ },
597
+ {
598
+ title: 'About',
599
+ slug: 'about',
600
+ blocks: [
601
+ {
602
+ _key: 'demo-about-prose',
603
+ _type: 'prose',
604
+ _version: BLOCK_VERSION,
605
+ body: [
606
+ p(`${siteName} is an independent news and culture magazine for Port Calder. It publishes online every weekday and in print every Thursday, and it has covered the city since its first issue went to press in the back room of a print shop on Mill Street in 2011.`),
607
+ p('It is owned by the Harbor Press Cooperative, whose members are its subscribers and its staff. No owner outside the city can buy it, and no advertiser sits on its board.'),
608
+ h2('What we cover'),
609
+ p('News reports on city hall, schools, transit, housing and the courts. Business follows the companies, workers and property decisions that shape the local economy. Culture reviews the city’s art, theater, music and restaurants. Opinion publishes columnists and readers who live here and argue in good faith.'),
610
+ h2('How we are paid for'),
611
+ p('Subscriptions provide 94 percent of our revenue. The rest comes from a small number of local advertisers, whose advertisements are labeled and never placed beside coverage of their own businesses. Our reporting on city government is free to read for everyone.'),
612
+ h2('When we get it wrong'),
613
+ p('We correct the article, add a dated note at its foot and say what changed. Our full policy on corrections, sources and conflicts of interest is published on our standards page.'),
614
+ ],
615
+ },
616
+ {
617
+ _key: 'demo-about-figures',
618
+ _type: 'stats',
619
+ _version: BLOCK_VERSION,
620
+ title: 'The newsroom in figures',
621
+ items: [
622
+ { _key: 'demo-about-founded', value: '2011', label: 'First printed issue' },
623
+ {
624
+ _key: 'demo-about-staff',
625
+ value: '16',
626
+ label: 'Reporters, editors and photographers',
627
+ },
628
+ { _key: 'demo-about-subscribers', value: '31,200', label: 'Paying subscribers' },
629
+ {
630
+ _key: 'demo-about-revenue',
631
+ value: '94',
632
+ unit: '%',
633
+ label: 'Of revenue from readers',
634
+ },
635
+ ],
636
+ },
637
+ {
638
+ _key: 'demo-about-sections',
639
+ _type: 'featureGrid',
640
+ _version: BLOCK_VERSION,
641
+ title: 'Four sections',
642
+ items: MAGAZINE_DEMO_SECTIONS.map((demo) => ({
643
+ _key: `demo-about-section-${demo.slug}`,
644
+ title: demo.name,
645
+ text: demo.description,
646
+ link: { href: `/section/${demo.slug}` },
647
+ })),
648
+ },
649
+ {
650
+ _key: 'demo-about-quote',
651
+ _type: 'quote',
652
+ _version: BLOCK_VERSION,
653
+ text: 'We print the council agenda on the Thursday before every meeting, because that is when a reader can still change what happens at it.',
654
+ author: 'Graham Oyelaran',
655
+ role: 'Editor since 2019',
656
+ },
657
+ ],
658
+ },
659
+ {
660
+ title: 'Subscribe',
661
+ slug: 'subscribe',
662
+ blocks: [
663
+ {
664
+ _key: 'demo-subscribe-prose',
665
+ _type: 'prose',
666
+ _version: BLOCK_VERSION,
667
+ body: [
668
+ p(`A subscription pays for ${siteName}’s reporting. It funds sixteen journalists, a weekly print edition and an archive that goes back to 2011, and it keeps our coverage of city hall free for everyone.`),
669
+ p('Every subscription includes unlimited access to the website and the archive, the morning newsletter, and an invitation to the quarterly editors’ meeting, where subscribers question the newsroom about its coverage.'),
670
+ ],
671
+ },
672
+ {
673
+ _key: 'demo-subscribe-plans',
674
+ _type: 'pricingTable',
675
+ _version: BLOCK_VERSION,
676
+ title: 'Choose a subscription',
677
+ tiers: [
678
+ {
679
+ _key: 'demo-plan-digital',
680
+ name: 'Digital',
681
+ price: '$8',
682
+ interval: 'a month',
683
+ features: [
684
+ 'Every article and the full archive',
685
+ 'The morning newsletter',
686
+ 'Cancel online at any time',
687
+ ],
688
+ action: { label: 'Choose Digital', target: { href: '/subscribe#digital' } },
689
+ },
690
+ {
691
+ _key: 'demo-plan-print',
692
+ name: 'Print and digital',
693
+ price: '$19',
694
+ interval: 'a month',
695
+ features: [
696
+ 'The Thursday print edition, delivered',
697
+ 'Everything in Digital',
698
+ 'Five articles a month to share with anyone',
699
+ ],
700
+ action: {
701
+ label: 'Choose Print and digital',
702
+ target: { href: '/subscribe#print' },
703
+ emphasis: 'primary',
704
+ },
705
+ highlighted: true,
706
+ },
707
+ {
708
+ _key: 'demo-plan-student',
709
+ name: 'Student',
710
+ price: '$3',
711
+ interval: 'a month',
712
+ features: [
713
+ 'Everything in Digital',
714
+ 'A student email address or card is enough',
715
+ 'Renews each academic year',
716
+ ],
717
+ action: { label: 'Choose Student', target: { href: '/subscribe#student' } },
718
+ },
719
+ {
720
+ _key: 'demo-plan-supporter',
721
+ name: 'Supporter',
722
+ price: '$150',
723
+ interval: 'a year',
724
+ features: [
725
+ 'Everything in Print and digital',
726
+ 'A vote at the cooperative’s annual meeting',
727
+ 'Your name in the annual report, if you wish',
728
+ ],
729
+ action: { label: 'Become a supporter', target: { href: '/subscribe#supporter' } },
730
+ },
731
+ ],
732
+ },
733
+ {
734
+ _key: 'demo-subscribe-letter',
735
+ _type: 'testimonial',
736
+ _version: BLOCK_VERSION,
737
+ quote: answer('demo-subscribe-letter-text', 'I cancelled two national subscriptions to keep this one. It was the only paper that told me the library near my house might close, three months before anyone asked my opinion.'),
738
+ attribution: { name: 'Denise Alvarado', role: 'Subscriber in Northgate since 2019' },
739
+ },
740
+ {
741
+ _key: 'demo-subscribe-questions',
742
+ _type: 'faq',
743
+ _version: BLOCK_VERSION,
744
+ title: 'Questions about subscriptions',
745
+ items: [
746
+ {
747
+ _key: 'demo-subscribe-cancel',
748
+ question: 'Can I cancel online?',
749
+ answer: answer('demo-subscribe-cancel-answer', 'Yes. Choose Cancel subscription on your account page. You keep access until the end of the period you have paid for.'),
750
+ },
751
+ {
752
+ _key: 'demo-subscribe-delivery',
753
+ question: 'Where is the print edition delivered?',
754
+ answer: answer('demo-subscribe-delivery-answer', 'Anywhere in Port Calder and the neighboring towns of Alder Falls and Wenham, on Thursday morning. Outside that area it is mailed and usually arrives on Saturday.'),
755
+ },
756
+ {
757
+ _key: 'demo-subscribe-gift',
758
+ question: 'Can I give a subscription as a gift?',
759
+ answer: answer('demo-subscribe-gift-answer', 'Yes, for three, six or twelve months. You choose the start date, and we post a card to the person receiving it.'),
760
+ },
761
+ {
762
+ _key: 'demo-subscribe-low-income',
763
+ question: 'Is there a reduced rate for people on a low income?',
764
+ answer: answer('demo-subscribe-low-income-answer', 'Yes. Anyone receiving public assistance can subscribe to Digital for $1 a month. Write to the circulation desk and we will set it up without asking for documents.'),
765
+ },
766
+ ],
767
+ },
768
+ {
769
+ _key: 'demo-subscribe-newsletter',
770
+ _type: 'cta',
771
+ _version: BLOCK_VERSION,
772
+ title: 'Start with the morning newsletter',
773
+ text: 'Every weekday at 6 a.m., the day’s city news in five minutes. It is free, and it is how most of our subscribers first found us.',
774
+ actions: [
775
+ {
776
+ label: 'Get the newsletter',
777
+ target: { href: '/subscribe#newsletter' },
778
+ emphasis: 'primary',
779
+ },
780
+ ],
781
+ },
782
+ ],
783
+ },
784
+ {
785
+ title: 'Standards and corrections',
786
+ slug: 'standards',
787
+ blocks: [
788
+ {
789
+ _key: 'demo-standards-prose',
790
+ _type: 'prose',
791
+ _version: BLOCK_VERSION,
792
+ body: [
793
+ p(`These are the standards ${siteName}’s journalists work to. We publish them so that readers can hold us to them.`),
794
+ h2('Accuracy and corrections'),
795
+ p('We correct errors of fact as soon as we confirm them. A correction is added to the foot of the article, dated, and says what was wrong. We never quietly edit a published article to remove a mistake.'),
796
+ h2('Sources'),
797
+ p('We name our sources whenever we can. We grant anonymity only when a source faces a real risk of harm or of losing their job, when the information matters, and when we cannot get it on the record. An editor must know the identity of every anonymous source.'),
798
+ h2('Independence'),
799
+ p('Our journalists accept no gifts, free travel or paid speaking engagements from the people and organizations they cover. Restaurant and theater critics pay for their meals and tickets, and the magazine reimburses them.'),
800
+ h2('Photographs'),
801
+ p('We never alter the content of a news photograph. Photographs are cropped and adjusted for exposure and color, and nothing more.'),
802
+ ],
803
+ },
804
+ {
805
+ _key: 'demo-standards-contact',
806
+ _type: 'accordion',
807
+ _version: BLOCK_VERSION,
808
+ title: 'Reaching the standards desk',
809
+ items: [
810
+ {
811
+ _key: 'demo-standards-error',
812
+ question: 'Report an error',
813
+ answer: answer('demo-standards-error-answer', 'Write to the standards desk at 212 Harbor Road, Port Calder. Tell us which article, what is wrong and, if you can, how you know.'),
814
+ },
815
+ {
816
+ _key: 'demo-standards-complaint',
817
+ question: 'Complain about our coverage',
818
+ answer: answer('demo-standards-complaint-answer', 'Complaints go to the standards editor, Ama Boateng, who replies to every one within ten working days. If her answer does not satisfy you, the cooperative’s board will review it.'),
819
+ },
820
+ {
821
+ _key: 'demo-standards-removal',
822
+ question: 'Ask us to update an old article',
823
+ answer: answer('demo-standards-removal-answer', 'We rarely remove published journalism. We will consider updating or anonymizing an article about a minor offense more than seven years old. Write to the standards editor with the details.'),
824
+ },
825
+ ],
826
+ },
827
+ ],
828
+ },
829
+ ];
830
+ }
831
+ // ---------------------------------------------------------------------------
832
+ // Media, menus, settings
833
+ // ---------------------------------------------------------------------------
834
+ /**
835
+ * The blueprint's own starting skin (`starting-skins.js`), asserted present
836
+ * with a real check: `STARTING_SKINS` is keyed by blueprint id and TypeScript
837
+ * cannot see that this key is always populated.
838
+ */
839
+ function magazinePalette() {
840
+ const skin = STARTING_SKINS.magazine;
841
+ if (skin === undefined) {
842
+ throw new CogentaError({
843
+ code: 'BLUEPRINT_REGISTRY_CORRUPT',
844
+ message: 'STARTING_SKINS.magazine is missing.',
845
+ hint: 'The "magazine" entry must stay declared in starting-skins.ts for this blueprint to seed its media.',
846
+ });
847
+ }
848
+ return skin.color;
849
+ }
850
+ /**
851
+ * Nine photographs, all bundled under `assets/photos/magazine/`, each checked
852
+ * at full size for invented lettering and cropped where a sign, a label or a
853
+ * printed shirt carried any. Most stories have no picture, as in any
854
+ * newspaper. `spec` is the procedural fallback `seedDemoMedia` would use if a
855
+ * file were ever missing; with every file present it is never rendered.
856
+ */
857
+ export const MAGAZINE_MEDIA_SPECS = [
858
+ {
859
+ name: 'city-hall',
860
+ alt: 'The columned front of Port Calder City Hall on a clear morning, seen from the foot of its steps',
861
+ photo: 'magazine/city-hall.jpg',
862
+ },
863
+ {
864
+ name: 'gallery-opening',
865
+ alt: 'A crowd of visitors seen from behind in a gallery hung with framed paintings',
866
+ photo: 'magazine/gallery-opening.jpg',
867
+ },
868
+ {
869
+ name: 'theater-stage',
870
+ alt: 'A small audience watching an actor rehearse on a stage lit in front of a red curtain',
871
+ photo: 'magazine/theater-stage.jpg',
872
+ },
873
+ {
874
+ name: 'archive-room',
875
+ alt: 'Rows of dark metal filing cabinets and stacks of papers in a dim storage room',
876
+ photo: 'magazine/archive-room.jpg',
877
+ },
878
+ {
879
+ name: 'downtown-towers',
880
+ alt: 'A glass office tower beside older brick buildings on a downtown street',
881
+ photo: 'magazine/downtown-towers.jpg',
882
+ },
883
+ {
884
+ name: 'station-platform',
885
+ alt: 'Commuters with backpacks waiting on a station platform beside a train',
886
+ photo: 'magazine/station-platform.jpg',
887
+ },
888
+ {
889
+ name: 'garden-volunteers',
890
+ alt: 'Smiling volunteers in aprons and caps standing together outdoors among young plants',
891
+ photo: 'magazine/garden-volunteers.jpg',
892
+ },
893
+ {
894
+ name: 'banquet-hall',
895
+ alt: 'Guests talking at round dinner tables in a hall lit by chandeliers',
896
+ photo: 'magazine/banquet-hall.jpg',
897
+ },
898
+ {
899
+ name: 'dining-room',
900
+ alt: 'A candlelit restaurant dining room with wood paneling, white tablecloths and a skylight',
901
+ photo: 'magazine/dining-room.jpg',
902
+ },
903
+ ].map((item, index) => ({ ...item, spec: coverArt(magazinePalette(), index + 1) }));
904
+ /** Header, footer and the header's call to action. Every path is a page this blueprint seeds or a section front the server provides. */
905
+ export const MAGAZINE_MENUS = {
906
+ header: [
907
+ ...MAGAZINE_DEMO_SECTIONS.map((demo) => ({ label: demo.name, url: `/section/${demo.slug}` })),
908
+ { label: 'About', url: '/about' },
909
+ ],
910
+ footer: [
911
+ ...MAGAZINE_DEMO_SECTIONS.map((demo) => ({ label: demo.name, url: `/section/${demo.slug}` })),
912
+ { label: 'About', url: '/about' },
913
+ { label: 'Subscribe', url: '/subscribe' },
914
+ { label: 'Standards and corrections', url: '/standards' },
915
+ ],
916
+ headerAction: { label: 'Subscribe', url: '/subscribe' },
917
+ };
918
+ export const MAGAZINE_SITE_SETTINGS = {
919
+ 'general.tagline': 'Independent news and culture from Port Calder.',
920
+ 'general.socialLinks': [
921
+ { label: 'Bluesky', url: 'https://bsky.app/profile/example.bsky.social' },
922
+ { label: 'Instagram', url: 'https://instagram.com/example' },
923
+ { label: 'Facebook', url: 'https://facebook.com/example' },
924
+ ],
925
+ 'general.footerNote': 'Published by the Harbor Press Cooperative, 212 Harbor Road, Port Calder. Letters to the editor and corrections go to the standards desk at the same address.',
926
+ };
927
+ /**
928
+ * The rail a reader of a daily expects beside a story, an archive and search
929
+ * results: the search box, the latest stories, the sections with their
930
+ * counts, and the subscription pitch. Never on the front page, which already
931
+ * lists everything, nor on the About and Standards pages. Under the story,
932
+ * the related stories carry the reader on.
933
+ */
934
+ const MAGAZINE_READING_PAGES = {
935
+ pages: {
936
+ mode: 'only',
937
+ targets: [
938
+ { kind: 'collection', collection: 'article' },
939
+ { kind: 'taxonomy', taxonomy: 'section' },
940
+ { kind: 'taxonomy', taxonomy: 'author' },
941
+ { kind: 'dateArchive' },
942
+ { kind: 'search' },
943
+ ],
944
+ },
945
+ };
946
+ export const MAGAZINE_WIDGETS = [
947
+ {
948
+ area: 'sidebar',
949
+ type: 'search',
950
+ title: 'Search the archive',
951
+ settings: { placeholder: 'Stories, people, places' },
952
+ visibility: MAGAZINE_READING_PAGES,
953
+ },
954
+ {
955
+ area: 'sidebar',
956
+ type: 'recentEntries',
957
+ title: 'Latest',
958
+ settings: { collection: 'article', count: 5, showDate: true },
959
+ visibility: MAGAZINE_READING_PAGES,
960
+ },
961
+ {
962
+ area: 'sidebar',
963
+ type: 'terms',
964
+ title: 'Sections',
965
+ settings: { taxonomy: 'section', showCounts: true, hierarchical: false, hideEmpty: true },
966
+ visibility: MAGAZINE_READING_PAGES,
967
+ },
968
+ {
969
+ area: 'sidebar',
970
+ type: 'cta',
971
+ settings: {
972
+ heading: 'Local reporting, paid for by readers',
973
+ body: 'No paywall and no billionaire owner. Members keep the newsroom open for everyone.',
974
+ label: 'Become a member',
975
+ href: '/subscribe',
976
+ },
977
+ visibility: MAGAZINE_READING_PAGES,
978
+ },
979
+ {
980
+ area: 'content-after',
981
+ type: 'relatedEntries',
982
+ title: 'More stories',
983
+ settings: { count: 3, showDate: true, showImage: false },
984
+ visibility: {
985
+ pages: { mode: 'only', targets: [{ kind: 'collection', collection: 'article' }] },
986
+ },
987
+ },
988
+ ];
989
+ export const MAGAZINE_RECOMMENDED_AGENTS = [
990
+ {
991
+ name: 'contentAgent',
992
+ package: '@cogenta/agents-builtin',
993
+ reason: 'Flags terminology drift and topic gaps across sections written by different editors.',
994
+ },
995
+ {
996
+ name: 'seoAgent',
997
+ package: '@cogenta/agents-builtin',
998
+ reason: 'Audits published articles for on-page SEO issues and internal-linking gaps.',
999
+ },
1000
+ ];
1001
+ // ---------------------------------------------------------------------------
1002
+ // Seeding
1003
+ // ---------------------------------------------------------------------------
1004
+ /**
1005
+ * Inserts the demo content through the real `ContentStore` and taxonomy
1006
+ * stores, never mocked (house rule).
1007
+ *
1008
+ * Articles are created oldest first, each with the date it was published, so
1009
+ * the `createdAt` order every list on the home page sorts on agrees with the
1010
+ * dates a reader sees.
1011
+ */
1012
+ async function seedMagazineDemoContent(ctx) {
1013
+ const { db, defaultLocale, adminId, media } = ctx;
1014
+ const siteName = ctx.siteName?.trim() || DEFAULT_PUBLICATION_NAME;
1015
+ const sectionStore = createTaxonomyStore({ db, taxonomy: section });
1016
+ const authorStore = createTaxonomyStore({ db, taxonomy: author });
1017
+ const articleStore = createContentStore({ db, collection: article, defaultLocale });
1018
+ const pageStore = createContentStore({ db, collection: page, defaultLocale });
1019
+ const sectionIds = new Map();
1020
+ for (const demo of MAGAZINE_DEMO_SECTIONS) {
1021
+ const term = await sectionStore.create({
1022
+ slug: demo.slug,
1023
+ labels: { [defaultLocale]: demo.name },
1024
+ });
1025
+ sectionIds.set(demo.slug, term.id);
1026
+ }
1027
+ const authorIds = new Map();
1028
+ for (const demo of MAGAZINE_DEMO_AUTHORS) {
1029
+ const term = await authorStore.create({
1030
+ slug: demo.slug,
1031
+ labels: { [defaultLocale]: demo.name },
1032
+ });
1033
+ authorIds.set(demo.slug, term.id);
1034
+ }
1035
+ for (const demo of MAGAZINE_DEMO_ARTICLES) {
1036
+ const cover = demo.photo === undefined ? undefined : media[demo.photo.media];
1037
+ await articleStore.create({
1038
+ status: 'published',
1039
+ createdBy: adminId,
1040
+ values: {
1041
+ title: demo.title,
1042
+ slug: demo.slug,
1043
+ kicker: demo.kicker,
1044
+ excerpt: demo.excerpt,
1045
+ section: sectionIds.get(demo.section) ?? null,
1046
+ authors: demo.authors.map((slug) => authorIds.get(slug)).filter((id) => id !== undefined),
1047
+ frontPage: demo.frontPage,
1048
+ publishedAt: demo.publishedAt,
1049
+ ...(cover === undefined ? {} : { coverImage: cover }),
1050
+ },
1051
+ blocks: {
1052
+ body: magazineArticleBlocks(demo, { siteName }, media).map(toBlockZoneEntry),
1053
+ },
1054
+ });
1055
+ }
1056
+ for (const demo of buildMagazineDemoPages({ siteName, sectionIds })) {
1057
+ await pageStore.create({
1058
+ status: 'published',
1059
+ createdBy: adminId,
1060
+ values: { title: demo.title, slug: demo.slug },
1061
+ blocks: { blocks: demo.blocks.map(toBlockZoneEntry) },
1062
+ });
1063
+ }
1064
+ }
1065
+ export const magazineContentPack = {
1066
+ collections: MAGAZINE_COLLECTIONS,
1067
+ taxonomies: MAGAZINE_TAXONOMIES,
1068
+ recommendedAgents: MAGAZINE_RECOMMENDED_AGENTS,
1069
+ seedDemoContent: seedMagazineDemoContent,
1070
+ defaultTheme: '@cogenta/theme-magazine',
1071
+ menus: MAGAZINE_MENUS,
1072
+ widgets: MAGAZINE_WIDGETS,
1073
+ siteSettings: MAGAZINE_SITE_SETTINGS,
1074
+ mediaSpecs: MAGAZINE_MEDIA_SPECS,
1075
+ };
1076
+ //# sourceMappingURL=magazine.js.map