ferst-core 0.7.4 → 0.7.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.
@@ -161,6 +161,23 @@ export function replacePackageCoreDep(raw, next) {
161
161
  return raw.replace(DEP, `$1${next}$2`);
162
162
  }
163
163
 
164
+ /** The `templateVersion` a manifest's text declares, or null (absent / not JSON). */
165
+ export function readTemplateVersion(raw) {
166
+ try {
167
+ const v = JSON.parse(raw).templateVersion;
168
+ return typeof v === 'string' && v ? v : null;
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /** Rewrite the manifest's `templateVersion` in place (formatting kept); null if it has none. */
175
+ export function replaceTemplateVersion(raw, next) {
176
+ const RE = /("templateVersion"\s*:\s*")[^"]*(")/;
177
+ if (!RE.test(raw)) return null;
178
+ return raw.replace(RE, `$1${next}$2`);
179
+ }
180
+
164
181
  /** package.json fields that describe the CLIENT's site, not the template's scaffold. */
165
182
  export const CLIENT_PACKAGE_FIELDS = ['name', 'description', 'version', 'private', 'author', 'homepage', 'repository'];
166
183
 
@@ -205,10 +222,11 @@ export function tail(text, n = 40) {
205
222
  * @param {{ upstream?: string, commit?: string|null, coreVersion?: string|null, now?: Date|string }} p
206
223
  * @returns {{ templateUpstream: string|null, templateCommit: string|null, coreVersion: string|null, at: string }}
207
224
  */
208
- export function buildProvenance({ upstream, commit, coreVersion, now = new Date() } = {}) {
225
+ export function buildProvenance({ upstream, commit, coreVersion, templateVersion, now = new Date() } = {}) {
209
226
  return {
210
227
  templateUpstream: upstream ?? null,
211
228
  templateCommit: commit ?? null,
229
+ templateVersion: templateVersion ?? null,
212
230
  coreVersion: coreVersion ?? null,
213
231
  at: typeof now === 'string' ? now : now.toISOString(),
214
232
  };
@@ -324,6 +342,7 @@ function main() {
324
342
  // The exact template commit this run syncs from (for the provenance stamp / #18 ledger).
325
343
  const templateCommit = (tryGit(`rev-parse upstream/${branch}`, cwd) || '').trim() || null;
326
344
 
345
+ let templateVersion = null; // the template version this sync brought (for the manifest + stamp)
327
346
  let scaffoldCount = 0; // files the scaffold sync touched (counted BEFORE the npm churn)
328
347
  if (dryRun) {
329
348
  const spec = ownedPaths.map(q).join(' ');
@@ -352,6 +371,18 @@ function main() {
352
371
  }
353
372
  } catch { /* package.json not synced — nothing to restore */ }
354
373
  }
374
+ // The manifest isn't a synced path (it holds the client's own settings), so carry the
375
+ // template's version across explicitly; `raw` is what the coreRange step builds on.
376
+ const upstreamVersion = readTemplateVersion(tryGit(`show upstream/${branch}:${MANIFEST}`, cwd) || '');
377
+ if (upstreamVersion) {
378
+ const bumped = replaceTemplateVersion(raw, upstreamVersion);
379
+ if (bumped && bumped !== raw) {
380
+ raw = bumped;
381
+ writeFileSync(manifestPath, raw);
382
+ console.log(`ferst-update: templateVersion → ${upstreamVersion}`);
383
+ }
384
+ }
385
+ templateVersion = upstreamVersion ?? readTemplateVersion(raw);
355
386
  const status = (tryGit('status --short', cwd) || '').trim();
356
387
  scaffoldCount = status ? status.split(/\r?\n/).filter(Boolean).length : 0;
357
388
  console.log('ferst-update: scaffold synced. git status --short:');
@@ -436,7 +467,7 @@ function main() {
436
467
  // drift ledger (#18) always know its state. MERGE: preserve gitBase/productionBranch
437
468
  // and any other existing ferst-site.json fields.
438
469
  const coreVersion = coreBump?.after ?? readInstalledCoreVersion(cwd);
439
- const provenance = buildProvenance({ upstream: upstream.url, commit: templateCommit, coreVersion });
470
+ const provenance = buildProvenance({ upstream: upstream.url, commit: templateCommit, coreVersion, templateVersion: templateVersion ?? readTemplateVersion(raw) });
440
471
  writeFileSync(
441
472
  resolve(cwd, SITE_META),
442
473
  `${JSON.stringify(mergeSiteMeta(readSiteMeta(cwd), provenance), null, 2)}\n`,
@@ -1,206 +1,214 @@
1
- # Sveltia CMS — the content editor for a Ferst site. SINGLE SOURCE: this file lives
2
- # in ferst-core and is written to dist/admin/config.yml at build by `ferst-cms-config`,
3
- # so every client's collections are identical and always match the engine. The client
4
- # repo commits NO config.yml — it is a build product, so it can never drift by hand.
5
- #
6
- # The backend repo (owner/name), git-proxy origin, its /git api_root and this
7
- # deployment's branch are injected at build by ferst-cms-config.
8
- backend:
9
- name: github
10
- repo: __REPO__
11
- branch: __BRANCH__
12
- base_url: __BASE_URL__
13
- auth_endpoint: auth
14
- api_root: __API_ROOT__
15
-
16
- media_folder: public/uploads
17
- public_folder: /uploads
18
-
19
- collections:
20
- - name: settings
21
- label: Site settings
22
- files:
23
- - name: theme
24
- label: Design tokens
25
- file: src/content/themeSettings/index.json
26
- icon: palette
27
- description: "Edit your brand in the Theme Studio, where you preview changes live before publishing — open portal.ferst.co.uk, choose this site, then the Design tab. The fields here still work as a fallback."
28
- fields:
29
- - { name: brand, label: Brand colour, widget: color, default: '#0E4D4A', hint: Buttons, links, icons, highlights. }
30
- - { name: ink, label: Ink colour, widget: color, default: '#0C2123', hint: Headings and body text. }
31
- - { name: surface, label: Surface colour, widget: color, default: '#FFFFFF', hint: Page background. }
32
- - name: fonts
33
- label: Typography
34
- widget: object
35
- collapsed: false
36
- fields:
37
- - name: heading
38
- label: Heading font
39
- widget: select
40
- required: false
41
- default: ''
42
- options: &fonts
43
- - { label: 'Default (theme)', value: '' }
44
- - { label: 'Poppins', value: 'Poppins, system-ui, sans-serif' }
45
- - { label: 'Inter', value: 'Inter, system-ui, sans-serif' }
46
- - { label: 'Playfair Display', value: "'Playfair Display', Georgia, serif" }
47
- - { label: 'Georgia', value: "Georgia, 'Times New Roman', serif" }
48
- - { label: 'System', value: 'system-ui, sans-serif' }
49
- - { name: body, label: Body font, widget: select, required: false, default: '', options: *fonts }
50
- - { name: display, label: Display font, widget: select, required: false, default: '', options: *fonts }
51
- - { name: corners, label: Corners, widget: select, default: rounded, options: [{ label: Sharp, value: sharp }, { label: Rounded, value: rounded }, { label: Soft, value: soft }] }
52
- - { name: elevation, label: Elevation, widget: select, default: raised, options: [{ label: Raised, value: raised }, { label: Flat, value: flat }] }
53
- - { name: stroke, label: Card stroke, widget: select, default: 'on', options: [{ label: 'On', value: 'on' }, { label: 'Off', value: 'off' }] }
54
- - { name: fields, label: Input fields, widget: select, default: boxed, options: [{ label: Boxed, value: boxed }, { label: Filled, value: filled }, { label: Underline, value: underline }] }
55
- - { name: darkDepth, label: Dark depth, widget: number, value_type: float, min: 0.14, max: 0.34, step: 0.01, default: 0.21 }
56
- - { name: darkChroma, label: Dark colour intensity, widget: number, value_type: float, min: 0, max: 0.15, step: 0.01, default: 0.05 }
57
- - name: darkOverrides
58
- label: Dark colour overrides (optional)
59
- widget: object
60
- collapsed: true
61
- fields:
62
- - { name: ground, label: Ground, widget: string, required: false }
63
- - { name: text, label: Text, widget: string, required: false }
64
- - { name: accent, label: Accent, widget: string, required: false }
65
-
66
- - name: identity
67
- label: Identity & contact
68
- file: src/content/siteSettings/index.json
69
- icon: info
70
- fields:
71
- - name: identity
72
- label: Identity
73
- widget: object
74
- fields:
75
- - { name: siteTitle, label: Site title, widget: string }
76
- - { name: shortName, label: Short name, widget: string }
77
- - { name: description, label: Description, widget: text }
78
- - { name: location, label: Location, widget: string, required: false }
79
- - name: contact
80
- label: Contact
81
- widget: object
82
- fields:
83
- - { name: phoneDisplay, label: Phone (display), widget: string, required: false }
84
- - { name: phoneTel, label: 'Phone (tel: digits)', widget: string, required: false }
85
- - { name: email, label: Email, widget: string, required: false }
86
- - { name: facebookUrl, label: Facebook URL, widget: string, required: false }
87
-
88
- - name: logos
89
- label: Logos
90
- file: src/content/logoSettings/index.json
91
- icon: image
92
- fields:
93
- - { name: primaryLogoLight, label: Header logo (light), widget: image, required: false }
94
- - { name: primaryLogoDark, label: Header logo (dark), widget: image, required: false }
95
- - { name: secondaryLogoLight, label: Footer logo (light), widget: image, required: false }
96
- - { name: secondaryLogoDark, label: Footer logo (dark), widget: image, required: false }
97
-
98
- - name: navbar
99
- label: Navigation
100
- file: src/content/navbarSettings/index.json
101
- icon: menu
102
- fields:
103
- - name: primary
104
- label: Primary links
105
- widget: list
106
- fields:
107
- - { name: label, label: Label, widget: string }
108
- - { name: href, label: Link, widget: string }
109
-
110
- - name: footer
111
- label: Footer
112
- file: src/content/footerSettings/index.json
113
- icon: table_rows
114
- fields:
115
- - name: footer
116
- label: Footer links
117
- widget: list
118
- fields:
119
- - { name: label, label: Label, widget: string }
120
- - { name: href, label: Link, widget: string }
121
-
122
- - name: calendar-settings
123
- label: Calendar
124
- file: src/content/calendarSettings/index.json
125
- icon: calendar_today
126
- fields:
127
- - { name: title, label: Heading, widget: string, default: Calendar }
128
- - { name: intro, label: Intro, widget: text, required: false }
129
- - { name: embedUrl, label: Embed URL, widget: string, required: false }
130
- - { name: publicUrl, label: Public link, widget: string, required: false }
131
- - { name: height, label: Height (px), widget: number, default: 700, value_type: int, min: 200, max: 2000 }
132
- - { name: provider, label: Provider name, widget: string, default: Google Calendar }
133
- - { name: weekStart, label: Week starts on, widget: select, default: monday, options: [{ label: "Monday", value: monday }, { label: "Sunday", value: sunday }] }
134
-
135
- - name: posts
136
- label: Posts
137
- label_singular: Post
138
- folder: src/content/posts
139
- create: true
140
- slug: "{{slug}}"
141
- extension: md
142
- format: frontmatter
143
- icon: article
144
- summary: "{{title}} — {{date}}"
145
- sortable_fields: [date, title]
146
- fields:
147
- - { name: title, label: Title, widget: string }
148
- - { name: date, label: Date, widget: datetime, picker_utc: true }
149
- - { name: summary, label: Summary, widget: text, required: false }
150
- - name: tags
151
- label: Tags
152
- widget: relation
153
- required: false
154
- collection: tags
155
- value_field: "{{slug}}"
156
- display_fields: [label]
157
- search_fields: [label]
158
- multiple: true
159
- - { name: heroImage, label: Hero image, widget: image, required: false }
160
- - { name: attachment, label: Attachment (e.g. PDF), widget: file, required: false }
161
- - { name: draft, label: Draft, widget: boolean, default: false, required: false }
162
- - { name: body, label: Body, widget: markdown, required: false }
163
-
164
- - name: tags
165
- label: Tags
166
- label_singular: Tag
167
- folder: src/content/tags
168
- create: true
169
- slug: "{{slug}}"
170
- extension: json
171
- format: json
172
- icon: label
173
- identifier_field: label
174
- fields:
175
- - { name: label, label: Label, widget: string }
176
- - { name: description, label: Description, widget: text, required: false }
177
- - { name: hidden, label: Hidden, widget: boolean, default: false, required: false }
178
-
179
- - name: pages
180
- label: Pages
181
- label_singular: Page
182
- description: "See each page and request a change — our team makes it and sends it back for you to check. You don't edit the page layout here."
183
- folder: src/content/pages
184
- extension: json
185
- format: json
186
- icon: description
187
- nested: { depth: 3, summary: "{{title}}" }
188
- summary: "{{title}}"
189
- identifier_field: title
190
- fields:
191
- - { name: title, label: Page title, widget: string }
192
- - name: requests
193
- label: Requested changes
194
- label_singular: Request
195
- widget: list
196
- required: false
197
- summary: "{{fields.text}}"
198
- fields:
199
- - { name: text, label: "What would you like changed?", widget: text }
200
- - { name: status, label: Status, widget: select, default: open, options: [{ label: Open, value: open }, { label: Done, value: done }] }
201
- - { name: description, label: SEO description, widget: hidden, required: false }
202
- - { name: heroImage, widget: hidden, required: false }
203
- - { name: align, widget: hidden, required: false }
204
- - { name: templateId, widget: hidden, required: false }
205
- - { name: draft, widget: hidden, required: false }
206
- - { name: blocks, widget: hidden }
1
+ # Sveltia CMS — the content editor for a Ferst site. SINGLE SOURCE: this file lives
2
+ # in ferst-core and is written to dist/admin/config.yml at build by `ferst-cms-config`,
3
+ # so every client's collections are identical and always match the engine. The client
4
+ # repo commits NO config.yml — it is a build product, so it can never drift by hand.
5
+ #
6
+ # The backend repo (owner/name), git-proxy origin, its /git api_root and this
7
+ # deployment's branch are injected at build by ferst-cms-config.
8
+ backend:
9
+ name: github
10
+ repo: __REPO__
11
+ branch: __BRANCH__
12
+ base_url: __BASE_URL__
13
+ auth_endpoint: auth
14
+ api_root: __API_ROOT__
15
+
16
+ media_folder: public/uploads
17
+ public_folder: /uploads
18
+
19
+ collections:
20
+ - name: settings
21
+ label: Site settings
22
+ files:
23
+ - name: theme
24
+ label: Design tokens
25
+ file: src/content/themeSettings/index.json
26
+ icon: palette
27
+ description: "Edit your brand in the Theme Studio, where you preview changes live before publishing — open portal.ferst.co.uk, choose this site, then the Design tab. The fields here still work as a fallback."
28
+ fields:
29
+ - { name: brand, label: Brand colour, widget: color, default: '#0E4D4A', hint: Buttons, links, icons, highlights. }
30
+ - { name: ink, label: Ink colour, widget: color, default: '#0C2123', hint: Headings and body text. }
31
+ - { name: surface, label: Surface colour, widget: color, default: '#FFFFFF', hint: Page background. }
32
+ - name: fonts
33
+ label: Typography
34
+ widget: object
35
+ collapsed: false
36
+ fields:
37
+ - name: heading
38
+ label: Heading font
39
+ widget: select
40
+ required: false
41
+ default: ''
42
+ options: &fonts
43
+ - { label: 'Default (theme)', value: '' }
44
+ - { label: 'Poppins', value: 'Poppins, system-ui, sans-serif' }
45
+ - { label: 'Inter', value: 'Inter, system-ui, sans-serif' }
46
+ - { label: 'Playfair Display', value: "'Playfair Display', Georgia, serif" }
47
+ - { label: 'Georgia', value: "Georgia, 'Times New Roman', serif" }
48
+ - { label: 'System', value: 'system-ui, sans-serif' }
49
+ - { name: body, label: Body font, widget: select, required: false, default: '', options: *fonts }
50
+ - { name: display, label: Display font, widget: select, required: false, default: '', options: *fonts }
51
+ - { name: corners, label: Corners, widget: select, default: rounded, options: [{ label: Sharp, value: sharp }, { label: Rounded, value: rounded }, { label: Soft, value: soft }] }
52
+ - { name: elevation, label: Elevation, widget: select, default: raised, options: [{ label: Raised, value: raised }, { label: Flat, value: flat }] }
53
+ - { name: stroke, label: Card stroke, widget: select, default: 'on', options: [{ label: 'On', value: 'on' }, { label: 'Off', value: 'off' }] }
54
+ - { name: fields, label: Input fields, widget: select, default: boxed, options: [{ label: Boxed, value: boxed }, { label: Filled, value: filled }, { label: Underline, value: underline }] }
55
+ - { name: darkDepth, label: Dark depth, widget: number, value_type: float, min: 0.14, max: 0.34, step: 0.01, default: 0.21 }
56
+ - { name: darkChroma, label: Dark colour intensity, widget: number, value_type: float, min: 0, max: 0.15, step: 0.01, default: 0.05 }
57
+ - name: darkOverrides
58
+ label: Dark colour overrides (optional)
59
+ widget: object
60
+ collapsed: true
61
+ fields:
62
+ - { name: ground, label: Ground, widget: string, required: false }
63
+ - { name: text, label: Text, widget: string, required: false }
64
+ - { name: accent, label: Accent, widget: string, required: false }
65
+
66
+ - name: identity
67
+ label: Identity & contact
68
+ file: src/content/siteSettings/index.json
69
+ icon: info
70
+ fields:
71
+ - name: identity
72
+ label: Identity
73
+ widget: object
74
+ fields:
75
+ - { name: siteTitle, label: Site title, widget: string }
76
+ - { name: shortName, label: Short name, widget: string }
77
+ - { name: description, label: Description, widget: text }
78
+ - { name: location, label: Location, widget: string, required: false }
79
+ - name: contact
80
+ label: Contact
81
+ widget: object
82
+ fields:
83
+ - { name: phoneDisplay, label: Phone (display), widget: string, required: false }
84
+ - { name: phoneTel, label: 'Phone (tel: digits)', widget: string, required: false }
85
+ - { name: email, label: Email, widget: string, required: false }
86
+ - { name: facebookUrl, label: Facebook URL, widget: string, required: false }
87
+
88
+ - name: logos
89
+ label: Logos
90
+ file: src/content/logoSettings/index.json
91
+ icon: image
92
+ fields:
93
+ - { name: primaryLogoLight, label: Header logo (light), widget: image, required: false }
94
+ - { name: primaryLogoDark, label: Header logo (dark), widget: image, required: false }
95
+ - { name: secondaryLogoLight, label: Footer logo (light), widget: image, required: false }
96
+ - { name: secondaryLogoDark, label: Footer logo (dark), widget: image, required: false }
97
+
98
+ - name: navbar
99
+ label: Navigation
100
+ file: src/content/navbarSettings/index.json
101
+ icon: menu
102
+ fields:
103
+ - name: primary
104
+ label: Primary links
105
+ widget: list
106
+ fields:
107
+ - { name: label, label: Label, widget: string }
108
+ - { name: href, label: Link, widget: string }
109
+ - name: cta
110
+ label: Header button
111
+ widget: object
112
+ required: false
113
+ hint: The button at the right of the header (and in the footer). Leave as Contact → /contact/ unless your contact page lives elsewhere.
114
+ fields:
115
+ - { name: label, label: Label, widget: string, default: Contact }
116
+ - { name: href, label: Link, widget: string, default: /contact/ }
117
+
118
+ - name: footer
119
+ label: Footer
120
+ file: src/content/footerSettings/index.json
121
+ icon: table_rows
122
+ fields:
123
+ - name: footer
124
+ label: Footer links
125
+ widget: list
126
+ fields:
127
+ - { name: label, label: Label, widget: string }
128
+ - { name: href, label: Link, widget: string }
129
+
130
+ - name: calendar-settings
131
+ label: Calendar
132
+ file: src/content/calendarSettings/index.json
133
+ icon: calendar_today
134
+ fields:
135
+ - { name: title, label: Heading, widget: string, default: Calendar }
136
+ - { name: intro, label: Intro, widget: text, required: false }
137
+ - { name: embedUrl, label: Embed URL, widget: string, required: false }
138
+ - { name: publicUrl, label: Public link, widget: string, required: false }
139
+ - { name: height, label: Height (px), widget: number, default: 700, value_type: int, min: 200, max: 2000 }
140
+ - { name: provider, label: Provider name, widget: string, default: Google Calendar }
141
+ - { name: weekStart, label: Week starts on, widget: select, default: monday, options: [{ label: "Monday", value: monday }, { label: "Sunday", value: sunday }] }
142
+
143
+ - name: posts
144
+ label: Posts
145
+ label_singular: Post
146
+ folder: src/content/posts
147
+ create: true
148
+ slug: "{{slug}}"
149
+ extension: md
150
+ format: frontmatter
151
+ icon: article
152
+ summary: "{{title}} — {{date}}"
153
+ sortable_fields: [date, title]
154
+ fields:
155
+ - { name: title, label: Title, widget: string }
156
+ - { name: date, label: Date, widget: datetime, picker_utc: true }
157
+ - { name: summary, label: Summary, widget: text, required: false }
158
+ - name: tags
159
+ label: Tags
160
+ widget: relation
161
+ required: false
162
+ collection: tags
163
+ value_field: "{{slug}}"
164
+ display_fields: [label]
165
+ search_fields: [label]
166
+ multiple: true
167
+ - { name: heroImage, label: Hero image, widget: image, required: false }
168
+ - { name: attachment, label: Attachment (e.g. PDF), widget: file, required: false }
169
+ - { name: draft, label: Draft, widget: boolean, default: false, required: false }
170
+ - { name: body, label: Body, widget: markdown, required: false }
171
+
172
+ - name: tags
173
+ label: Tags
174
+ label_singular: Tag
175
+ folder: src/content/tags
176
+ create: true
177
+ slug: "{{slug}}"
178
+ extension: json
179
+ format: json
180
+ icon: label
181
+ identifier_field: label
182
+ fields:
183
+ - { name: label, label: Label, widget: string }
184
+ - { name: description, label: Description, widget: text, required: false }
185
+ - { name: hidden, label: Hidden, widget: boolean, default: false, required: false }
186
+
187
+ - name: pages
188
+ label: Pages
189
+ label_singular: Page
190
+ description: "See each page and request a change — our team makes it and sends it back for you to check. You don't edit the page layout here."
191
+ folder: src/content/pages
192
+ extension: json
193
+ format: json
194
+ icon: description
195
+ nested: { depth: 3, summary: "{{title}}" }
196
+ summary: "{{title}}"
197
+ identifier_field: title
198
+ fields:
199
+ - { name: title, label: Page title, widget: string }
200
+ - name: requests
201
+ label: Requested changes
202
+ label_singular: Request
203
+ widget: list
204
+ required: false
205
+ summary: "{{fields.text}}"
206
+ fields:
207
+ - { name: text, label: "What would you like changed?", widget: text }
208
+ - { name: status, label: Status, widget: select, default: open, options: [{ label: Open, value: open }, { label: Done, value: done }] }
209
+ - { name: description, label: SEO description, widget: hidden, required: false }
210
+ - { name: heroImage, widget: hidden, required: false }
211
+ - { name: align, widget: hidden, required: false }
212
+ - { name: templateId, widget: hidden, required: false }
213
+ - { name: draft, widget: hidden, required: false }
214
+ - { name: blocks, widget: hidden }
@@ -49,6 +49,8 @@ import Documents from './recipes/Documents.astro';
49
49
  import LinkRow from './recipes/LinkRow.astro';
50
50
  import DocumentRecipe from './recipes/Document.astro';
51
51
  import Timetable from './recipes/Timetable.astro';
52
+ import Contacts from './recipes/Contacts.astro';
53
+ import MediaText from './recipes/MediaText.astro';
52
54
 
53
55
  interface Props {
54
56
  blocks: Block[];
@@ -100,6 +102,8 @@ const registry: Record<BlockType, any> = {
100
102
  linkRow: LinkRow,
101
103
  document: DocumentRecipe,
102
104
  timetable: Timetable,
105
+ contacts: Contacts,
106
+ mediaText: MediaText,
103
107
  };
104
108
  ---
105
109