ferst-core 0.7.5 → 0.7.7

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 (36) hide show
  1. package/bin/ferst-modules.mjs +91 -0
  2. package/cms/config.template.yml +13 -0
  3. package/components/BlockRenderer.astro +2 -0
  4. package/components/ProseParts.astro +26 -0
  5. package/components/SiteFooter.astro +52 -1
  6. package/components/SiteHeader.astro +1246 -1243
  7. package/components/blocks/Prose.astro +12 -17
  8. package/components/recipes/Accordion.astro +3 -7
  9. package/components/recipes/Contacts.astro +3 -3
  10. package/components/recipes/Document.astro +7 -11
  11. package/components/recipes/Documents.astro +2 -2
  12. package/components/recipes/Form.astro +30 -0
  13. package/components/recipes/MediaText.astro +88 -92
  14. package/components/recipes/Steps.astro +121 -120
  15. package/components/recipes/Table.astro +3 -2
  16. package/components/recipes/Video.astro +1 -1
  17. package/content/blocks.ts +917 -887
  18. package/content/schemas.ts +5 -0
  19. package/fonts/OFL.txt +287 -0
  20. package/fonts/inter-latin-400-normal.woff2 +0 -0
  21. package/fonts/inter-latin-500-normal.woff2 +0 -0
  22. package/fonts/inter-latin-600-normal.woff2 +0 -0
  23. package/fonts/playfair-display-latin-700-normal.woff2 +0 -0
  24. package/fonts/poppins-latin-300-normal.woff2 +0 -0
  25. package/fonts/poppins-latin-400-normal.woff2 +0 -0
  26. package/fonts/poppins-latin-500-normal.woff2 +0 -0
  27. package/fonts/poppins-latin-600-normal.woff2 +0 -0
  28. package/fonts/poppins-latin-700-normal.woff2 +0 -0
  29. package/integration.mjs +41 -1
  30. package/layouts/Base.astro +1 -0
  31. package/lib/forms.ts +57 -0
  32. package/lib/icons.ts +10 -0
  33. package/lib/links.ts +24 -22
  34. package/lib/nav.ts +12 -0
  35. package/lib/siteSettings.ts +2 -1
  36. package/package.json +5 -3
@@ -90,6 +90,11 @@ export const navbarSettingsSchema = z.object({
90
90
  export const footerSettingsSchema = z.object({
91
91
  /** Footer legal/info nav. Empty by default — every client defines its own. */
92
92
  footer: z.array(navLinkSchema).default([]),
93
+ /** Headed footer columns for reference material (policies, safeguarding, documents,
94
+ * forms) — where it belongs, so the main menu stays short and clear. */
95
+ groups: z
96
+ .array(z.object({ heading: z.string(), links: z.array(navLinkSchema).min(1) }))
97
+ .default([]),
93
98
  });
94
99
 
95
100
  /**
package/fonts/OFL.txt ADDED
@@ -0,0 +1,287 @@
1
+ Fonts in this folder are distributed under the SIL Open Font License, Version 1.1.
2
+ Files taken unmodified from Fontsource (https://fontsource.org): @fontsource/inter, @fontsource/playfair-display and @fontsource/poppins 5.3.0.
3
+
4
+ ==================== inter ====================
5
+ Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
6
+
7
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
8
+ This license is copied below, and is also available with a FAQ at:
9
+ http://scripts.sil.org/OFL
10
+
11
+
12
+ -----------------------------------------------------------
13
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
14
+ -----------------------------------------------------------
15
+
16
+ PREAMBLE
17
+ The goals of the Open Font License (OFL) are to stimulate worldwide
18
+ development of collaborative font projects, to support the font creation
19
+ efforts of academic and linguistic communities, and to provide a free and
20
+ open framework in which fonts may be shared and improved in partnership
21
+ with others.
22
+
23
+ The OFL allows the licensed fonts to be used, studied, modified and
24
+ redistributed freely as long as they are not sold by themselves. The
25
+ fonts, including any derivative works, can be bundled, embedded,
26
+ redistributed and/or sold with any software provided that any reserved
27
+ names are not used by derivative works. The fonts and derivatives,
28
+ however, cannot be released under any other type of license. The
29
+ requirement for fonts to remain under this license does not apply
30
+ to any document created using the fonts or their derivatives.
31
+
32
+ DEFINITIONS
33
+ "Font Software" refers to the set of files released by the Copyright
34
+ Holder(s) under this license and clearly marked as such. This may
35
+ include source files, build scripts and documentation.
36
+
37
+ "Reserved Font Name" refers to any names specified as such after the
38
+ copyright statement(s).
39
+
40
+ "Original Version" refers to the collection of Font Software components as
41
+ distributed by the Copyright Holder(s).
42
+
43
+ "Modified Version" refers to any derivative made by adding to, deleting,
44
+ or substituting -- in part or in whole -- any of the components of the
45
+ Original Version, by changing formats or by porting the Font Software to a
46
+ new environment.
47
+
48
+ "Author" refers to any designer, engineer, programmer, technical
49
+ writer or other person who contributed to the Font Software.
50
+
51
+ PERMISSION & CONDITIONS
52
+ Permission is hereby granted, free of charge, to any person obtaining
53
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
54
+ redistribute, and sell modified and unmodified copies of the Font
55
+ Software, subject to the following conditions:
56
+
57
+ 1) Neither the Font Software nor any of its individual components,
58
+ in Original or Modified Versions, may be sold by itself.
59
+
60
+ 2) Original or Modified Versions of the Font Software may be bundled,
61
+ redistributed and/or sold with any software, provided that each copy
62
+ contains the above copyright notice and this license. These can be
63
+ included either as stand-alone text files, human-readable headers or
64
+ in the appropriate machine-readable metadata fields within text or
65
+ binary files as long as those fields can be easily viewed by the user.
66
+
67
+ 3) No Modified Version of the Font Software may use the Reserved Font
68
+ Name(s) unless explicit written permission is granted by the corresponding
69
+ Copyright Holder. This restriction only applies to the primary font name as
70
+ presented to the users.
71
+
72
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
73
+ Software shall not be used to promote, endorse or advertise any
74
+ Modified Version, except to acknowledge the contribution(s) of the
75
+ Copyright Holder(s) and the Author(s) or with their explicit written
76
+ permission.
77
+
78
+ 5) The Font Software, modified or unmodified, in part or in whole,
79
+ must be distributed entirely under this license, and must not be
80
+ distributed under any other license. The requirement for fonts to
81
+ remain under this license does not apply to any document created
82
+ using the Font Software.
83
+
84
+ TERMINATION
85
+ This license becomes null and void if any of the above conditions are
86
+ not met.
87
+
88
+ DISCLAIMER
89
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
90
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
91
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
92
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
93
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
94
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
95
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
96
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
97
+ OTHER DEALINGS IN THE FONT SOFTWARE.
98
+
99
+ ==================== playfair-display ====================
100
+ Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display". PlayfairDisplay-Italic[wght].ttf: Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display".
101
+
102
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
103
+ This license is copied below, and is also available with a FAQ at:
104
+ http://scripts.sil.org/OFL
105
+
106
+
107
+ -----------------------------------------------------------
108
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
109
+ -----------------------------------------------------------
110
+
111
+ PREAMBLE
112
+ The goals of the Open Font License (OFL) are to stimulate worldwide
113
+ development of collaborative font projects, to support the font creation
114
+ efforts of academic and linguistic communities, and to provide a free and
115
+ open framework in which fonts may be shared and improved in partnership
116
+ with others.
117
+
118
+ The OFL allows the licensed fonts to be used, studied, modified and
119
+ redistributed freely as long as they are not sold by themselves. The
120
+ fonts, including any derivative works, can be bundled, embedded,
121
+ redistributed and/or sold with any software provided that any reserved
122
+ names are not used by derivative works. The fonts and derivatives,
123
+ however, cannot be released under any other type of license. The
124
+ requirement for fonts to remain under this license does not apply
125
+ to any document created using the fonts or their derivatives.
126
+
127
+ DEFINITIONS
128
+ "Font Software" refers to the set of files released by the Copyright
129
+ Holder(s) under this license and clearly marked as such. This may
130
+ include source files, build scripts and documentation.
131
+
132
+ "Reserved Font Name" refers to any names specified as such after the
133
+ copyright statement(s).
134
+
135
+ "Original Version" refers to the collection of Font Software components as
136
+ distributed by the Copyright Holder(s).
137
+
138
+ "Modified Version" refers to any derivative made by adding to, deleting,
139
+ or substituting -- in part or in whole -- any of the components of the
140
+ Original Version, by changing formats or by porting the Font Software to a
141
+ new environment.
142
+
143
+ "Author" refers to any designer, engineer, programmer, technical
144
+ writer or other person who contributed to the Font Software.
145
+
146
+ PERMISSION & CONDITIONS
147
+ Permission is hereby granted, free of charge, to any person obtaining
148
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
149
+ redistribute, and sell modified and unmodified copies of the Font
150
+ Software, subject to the following conditions:
151
+
152
+ 1) Neither the Font Software nor any of its individual components,
153
+ in Original or Modified Versions, may be sold by itself.
154
+
155
+ 2) Original or Modified Versions of the Font Software may be bundled,
156
+ redistributed and/or sold with any software, provided that each copy
157
+ contains the above copyright notice and this license. These can be
158
+ included either as stand-alone text files, human-readable headers or
159
+ in the appropriate machine-readable metadata fields within text or
160
+ binary files as long as those fields can be easily viewed by the user.
161
+
162
+ 3) No Modified Version of the Font Software may use the Reserved Font
163
+ Name(s) unless explicit written permission is granted by the corresponding
164
+ Copyright Holder. This restriction only applies to the primary font name as
165
+ presented to the users.
166
+
167
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
168
+ Software shall not be used to promote, endorse or advertise any
169
+ Modified Version, except to acknowledge the contribution(s) of the
170
+ Copyright Holder(s) and the Author(s) or with their explicit written
171
+ permission.
172
+
173
+ 5) The Font Software, modified or unmodified, in part or in whole,
174
+ must be distributed entirely under this license, and must not be
175
+ distributed under any other license. The requirement for fonts to
176
+ remain under this license does not apply to any document created
177
+ using the Font Software.
178
+
179
+ TERMINATION
180
+ This license becomes null and void if any of the above conditions are
181
+ not met.
182
+
183
+ DISCLAIMER
184
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
185
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
186
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
187
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
188
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
189
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
190
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
191
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
192
+ OTHER DEALINGS IN THE FONT SOFTWARE.
193
+
194
+ ==================== poppins ====================
195
+ Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-ThinItalic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-ExtraLight.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-ExtraLightItalic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-Light.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-LightItalic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-Regular.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-Italic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-Medium.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-MediumItalic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-SemiBold.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-SemiBoldItalic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-Bold.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-BoldItalic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-ExtraBold.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-ExtraBoldItalic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-Black.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) Poppins-BlackItalic.ttf: Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
196
+
197
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
198
+ This license is copied below, and is also available with a FAQ at:
199
+ http://scripts.sil.org/OFL
200
+
201
+
202
+ -----------------------------------------------------------
203
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
204
+ -----------------------------------------------------------
205
+
206
+ PREAMBLE
207
+ The goals of the Open Font License (OFL) are to stimulate worldwide
208
+ development of collaborative font projects, to support the font creation
209
+ efforts of academic and linguistic communities, and to provide a free and
210
+ open framework in which fonts may be shared and improved in partnership
211
+ with others.
212
+
213
+ The OFL allows the licensed fonts to be used, studied, modified and
214
+ redistributed freely as long as they are not sold by themselves. The
215
+ fonts, including any derivative works, can be bundled, embedded,
216
+ redistributed and/or sold with any software provided that any reserved
217
+ names are not used by derivative works. The fonts and derivatives,
218
+ however, cannot be released under any other type of license. The
219
+ requirement for fonts to remain under this license does not apply
220
+ to any document created using the fonts or their derivatives.
221
+
222
+ DEFINITIONS
223
+ "Font Software" refers to the set of files released by the Copyright
224
+ Holder(s) under this license and clearly marked as such. This may
225
+ include source files, build scripts and documentation.
226
+
227
+ "Reserved Font Name" refers to any names specified as such after the
228
+ copyright statement(s).
229
+
230
+ "Original Version" refers to the collection of Font Software components as
231
+ distributed by the Copyright Holder(s).
232
+
233
+ "Modified Version" refers to any derivative made by adding to, deleting,
234
+ or substituting -- in part or in whole -- any of the components of the
235
+ Original Version, by changing formats or by porting the Font Software to a
236
+ new environment.
237
+
238
+ "Author" refers to any designer, engineer, programmer, technical
239
+ writer or other person who contributed to the Font Software.
240
+
241
+ PERMISSION & CONDITIONS
242
+ Permission is hereby granted, free of charge, to any person obtaining
243
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
244
+ redistribute, and sell modified and unmodified copies of the Font
245
+ Software, subject to the following conditions:
246
+
247
+ 1) Neither the Font Software nor any of its individual components,
248
+ in Original or Modified Versions, may be sold by itself.
249
+
250
+ 2) Original or Modified Versions of the Font Software may be bundled,
251
+ redistributed and/or sold with any software, provided that each copy
252
+ contains the above copyright notice and this license. These can be
253
+ included either as stand-alone text files, human-readable headers or
254
+ in the appropriate machine-readable metadata fields within text or
255
+ binary files as long as those fields can be easily viewed by the user.
256
+
257
+ 3) No Modified Version of the Font Software may use the Reserved Font
258
+ Name(s) unless explicit written permission is granted by the corresponding
259
+ Copyright Holder. This restriction only applies to the primary font name as
260
+ presented to the users.
261
+
262
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
263
+ Software shall not be used to promote, endorse or advertise any
264
+ Modified Version, except to acknowledge the contribution(s) of the
265
+ Copyright Holder(s) and the Author(s) or with their explicit written
266
+ permission.
267
+
268
+ 5) The Font Software, modified or unmodified, in part or in whole,
269
+ must be distributed entirely under this license, and must not be
270
+ distributed under any other license. The requirement for fonts to
271
+ remain under this license does not apply to any document created
272
+ using the Font Software.
273
+
274
+ TERMINATION
275
+ This license becomes null and void if any of the above conditions are
276
+ not met.
277
+
278
+ DISCLAIMER
279
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
280
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
281
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
282
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
283
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
284
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
285
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
286
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
287
+ OTHER DEALINGS IN THE FONT SOFTWARE.
package/integration.mjs CHANGED
@@ -27,7 +27,45 @@
27
27
  * the site has its own `src/pages/cookie-policy.astro`.
28
28
  * @returns {import('astro').AstroIntegration}
29
29
  */
30
- import { existsSync } from 'node:fs';
30
+ import { copyFileSync, createReadStream, existsSync, mkdirSync, readdirSync } from 'node:fs';
31
+
32
+ /** The self-hosted fonts the engine's CSS requests (/fonts/*.woff2), shipped in the package. */
33
+ const FONTS_DIR = new URL('./fonts/', import.meta.url);
34
+ const FONT_URL = /^\/fonts\/([a-z0-9-]+\.woff2)(?:\?.*)?$/;
35
+
36
+ /**
37
+ * ferstFonts() — serves the engine's self-hosted fonts in `astro dev` and copies them into
38
+ * every build's `/fonts/`. Included in ferst(); usable on its own (the showcase). A file
39
+ * the site already has in its own `public/fonts/` is never overwritten.
40
+ * @returns {import('astro').AstroIntegration}
41
+ */
42
+ export function ferstFonts() {
43
+ return {
44
+ name: 'ferst-fonts',
45
+ hooks: {
46
+ 'astro:server:setup': ({ server }) => {
47
+ server.middlewares.use((req, res, next) => {
48
+ const m = req.url && req.url.match(FONT_URL);
49
+ if (!m) return next();
50
+ const file = new URL(m[1], FONTS_DIR);
51
+ if (!existsSync(file)) return next();
52
+ res.setHeader('Content-Type', 'font/woff2');
53
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
54
+ createReadStream(file).pipe(res);
55
+ });
56
+ },
57
+ 'astro:build:done': async ({ dir }) => {
58
+ const out = new URL('fonts/', dir);
59
+ mkdirSync(out, { recursive: true });
60
+ for (const name of readdirSync(FONTS_DIR)) {
61
+ if (!name.endsWith('.woff2')) continue;
62
+ const dest = new URL(name, out);
63
+ if (!existsSync(dest)) copyFileSync(new URL(name, FONTS_DIR), dest);
64
+ }
65
+ },
66
+ },
67
+ };
68
+ }
31
69
 
32
70
  export default function ferst(options = {}) {
33
71
  const { calendar = true, blog = true, cookiePolicy = true } = options;
@@ -78,6 +116,8 @@ export default function ferst(options = {}) {
78
116
  });
79
117
  }
80
118
  },
119
+ // The engine's self-hosted fonts: served in dev, copied into every build.
120
+ ...ferstFonts().hooks,
81
121
  },
82
122
  };
83
123
  }
@@ -215,6 +215,7 @@ const metaDescription = description ?? identity.description;
215
215
  <SiteFooter
216
216
  facebookUrl={contact.facebookUrl}
217
217
  footerLinks={navigation.footer}
218
+ groups={navigation.footerGroups}
218
219
  cta={navigation.cta}
219
220
  footerClass={footerClass}
220
221
  shortName={identity.shortName}
package/lib/forms.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A form placed by id (the `form` block) is read from the Ferst platform at build time, so
3
+ * a change made to it in the portal reaches the site on its next build — no pasted copy of
4
+ * its fields to keep in step. A missing form or an unreachable platform fails the build:
5
+ * better the last good deploy stays live than a form that can't be sent goes out.
6
+ */
7
+ export interface FormField {
8
+ name: string;
9
+ label: string;
10
+ type?: 'text' | 'email' | 'tel' | 'textarea';
11
+ required?: boolean;
12
+ placeholder?: string;
13
+ }
14
+
15
+ export interface FormDefinition {
16
+ name: string;
17
+ submitLabel: string;
18
+ fields: FormField[];
19
+ /** Where the browser posts the form. */
20
+ action: string;
21
+ }
22
+
23
+ type FetchFn = (input: string, init?: RequestInit) => Promise<Response>;
24
+
25
+ const cache = new Map<string, Promise<FormDefinition>>();
26
+
27
+ export function loadFormDefinition(id: string, opts: { base: string; fetch?: FetchFn }): Promise<FormDefinition> {
28
+ const key = `${opts.base}|${id}`;
29
+ let hit = cache.get(key);
30
+ if (!hit) {
31
+ hit = fetchDefinition(id, opts.base, opts.fetch ?? fetch);
32
+ cache.set(key, hit);
33
+ hit.catch(() => cache.delete(key)); // a failure is retried by the next caller
34
+ }
35
+ return hit;
36
+ }
37
+
38
+ async function fetchDefinition(id: string, base: string, fetchFn: FetchFn): Promise<FormDefinition> {
39
+ const url = `${base}/forms/${encodeURIComponent(id)}`;
40
+ let res: Response;
41
+ try {
42
+ res = await fetchFn(url, { headers: { Accept: 'application/json' } });
43
+ } catch (e) {
44
+ throw new Error(`Form block: couldn’t reach the Ferst platform for form "${id}" (${url}): ${e instanceof Error ? e.message : String(e)}`);
45
+ }
46
+ if (res.status === 404) {
47
+ throw new Error(`Form block: form "${id}" was not found — it may have been archived. Check the id in the portal (Forms), or run \`npx ferst-modules\` to list this site's forms.`);
48
+ }
49
+ if (!res.ok) throw new Error(`Form block: the Ferst platform answered ${res.status} for form "${id}" (${url}).`);
50
+ const def = (await res.json()) as { name: string; submit_label?: string; fields?: FormField[] };
51
+ return {
52
+ name: def.name,
53
+ submitLabel: def.submit_label || 'Send',
54
+ fields: def.fields ?? [],
55
+ action: `${base}/forms/${encodeURIComponent(id)}/submit`,
56
+ };
57
+ }
package/lib/icons.ts CHANGED
@@ -16,6 +16,16 @@
16
16
  * dropped in favour of the entries already here.
17
17
  */
18
18
  export const ICONS: Record<string, string> = {
19
+ // ── Shapes added in 0.7.6 (named for what they depict, not for a sector) ──
20
+ steeple: '<path d="M12 2v3M10.5 3.5h3"></path><path d="M12 5 8 10v11h8V10z"></path><path d="M8 13l-4 2.5V21M16 13l4 2.5V21M3 21h18"></path><path d="M11 21v-3.5a1 1 0 0 1 2 0V21"></path>',
21
+ cross: '<path d="M12 3v18M7 8h10"></path>',
22
+ chalice: '<path d="M7 3h10c0 4.2-2.2 7-5 7S7 7.2 7 3z"></path><path d="M12 10v7M8.5 21h7M9.5 17h5"></path>',
23
+ shell: '<path d="M12 20 4.2 9.5a8.5 8.5 0 0 1 15.6 0z"></path><path d="M12 20 7.4 6.6M12 20V4.6M12 20l4.6-13.4"></path>',
24
+ jar: '<path d="M9.5 3h5M10.5 3v2.5L8 8.5v10a2.5 2.5 0 0 0 2.5 2.5h3a2.5 2.5 0 0 0 2.5-2.5v-10l-2.5-3V3"></path><path d="M8 13h8"></path>',
25
+ rings: '<circle cx="9" cy="15" r="5"></circle><circle cx="15" cy="15" r="5"></circle><path d="m15 4.5 1.6 1.6L15 7.7l-1.6-1.6z"></path><path d="M15 7.7V10"></path>',
26
+ stole: '<path d="M8.5 3c.4 6 .2 12-1.5 18M15.5 3c-.4 6-.2 12 1.5 18"></path><path d="M8.5 3c1.8 1.6 5.2 1.6 7 0"></path><path d="M7.4 17.5h2.4M14.2 17.5h2.4"></path>',
27
+ candle: '<path d="M12 3c1.1 1.4 1.6 2.4 1.6 3.2a1.6 1.6 0 0 1-3.2 0c0-.8.5-1.8 1.6-3.2z"></path><rect x="9" y="10" width="6" height="11" rx="1"></rect>',
28
+
19
29
  landmark: '<path d="M12 3v4M9 7h6M6 21V10l6-4 6 4v11M6 21h12M10 21v-5h4v5"></path>',
20
30
  canopy: '<polygon points="12 3 3 9 21 9"></polygon><line x1="4" y1="9" x2="4" y2="19"></line><line x1="9" y1="9" x2="9" y2="19"></line><line x1="15" y1="9" x2="15" y2="19"></line><line x1="20" y1="9" x2="20" y2="19"></line><line x1="2" y1="22" x2="22" y2="22"></line>',
21
31
  drop: '<path d="M12 3c3 4 6 8.5 6 12a6 6 0 1 1-12 0c0-3.5 3-8 6-12z"></path>',
package/lib/links.ts CHANGED
@@ -1,22 +1,24 @@
1
- /**
2
- * The icon a link deserves, read from the link itself — so an editor writes a phone
3
- * number, an email or a web address and the row shows the right symbol without choosing.
4
- */
5
- import type { IconKey } from './icons';
6
-
7
- export function linkIcon(href: string): IconKey {
8
- if (/^tel:/i.test(href)) return 'phone';
9
- if (/^mailto:/i.test(href)) return 'email';
10
- if (/^https?:\/\//i.test(href)) return 'globe';
11
- return 'arrow';
12
- }
13
-
14
- /** A tel: link from a number as an editor types it (spaces, brackets, a "(0)" dropped). */
15
- export function telHref(phone: string): string {
16
- return 'tel:' + phone.replace(/\(0\)/g, '').replace(/[^\d+]/g, '');
17
- }
18
-
19
- /** A mailto: link, with an optional pre-filled subject. */
20
- export function mailtoHref(email: string, subject?: string): string {
21
- return `mailto:${email}${subject ? `?subject=${encodeURIComponent(subject)}` : ''}`;
22
- }
1
+ /**
2
+ * The icon a link deserves, read from the link itself — so an editor writes a phone
3
+ * number, an email or a web address and the row shows the right symbol without choosing.
4
+ */
5
+ import type { IconKey } from './icons';
6
+
7
+ export function linkIcon(href: string): IconKey {
8
+ if (/^tel:/i.test(href)) return 'phone';
9
+ if (/^mailto:/i.test(href)) return 'email';
10
+ if (/^https?:\/\//i.test(href)) return 'globe';
11
+ return 'arrow';
12
+ }
13
+
14
+ /** A tel: link from a number as an editor types it (spaces, brackets, a "(0)" dropped). */
15
+ export function telHref(phone: string): string {
16
+ // An extension ("ext. 204", "x204", "(extension 204)") is shown but never dialled.
17
+ const number = phone.replace(/\(?\s*(?:ext\.?|extension|x)\s*\d+\s*\)?\s*$/i, '');
18
+ return 'tel:' + number.replace(/\(0\)/g, '').replace(/[^\d+]/g, '');
19
+ }
20
+
21
+ /** A mailto: link, with an optional pre-filled subject. */
22
+ export function mailtoHref(email: string, subject?: string): string {
23
+ return `mailto:${email}${subject ? `?subject=${encodeURIComponent(subject)}` : ''}`;
24
+ }
package/lib/nav.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The header's call-to-action button (navbarSettings.cta) wins over a plain menu link to
3
+ * the same place, so a site never shows Contact twice (once as a link, once as the button).
4
+ * Migrated sites' old menus almost always carry a Contact link. Pure.
5
+ */
6
+ const norm = (href: string) => href.replace(/\/+$/, '') || '/';
7
+
8
+ export function navWithoutCta<T extends { href: string }>(links: T[], cta: { href: string } | null | undefined): T[] {
9
+ if (!cta) return links;
10
+ const target = norm(cta.href);
11
+ return links.filter((l) => norm(l.href) !== target);
12
+ }
@@ -27,7 +27,7 @@ import {
27
27
 
28
28
  export type LoadedSiteSettings = SiteSettings &
29
29
  LogoSettings & {
30
- navigation: { primary: NavLink[]; footer: NavLink[]; dropdown?: NavGroup; cta: { label: string; href: string } | null };
30
+ navigation: { primary: NavLink[]; footer: NavLink[]; footerGroups: Array<{ heading: string; links: NavLink[] }>; dropdown?: NavGroup; cta: { label: string; href: string } | null };
31
31
  /** Client-provided design-token overrides (empty objects if none). */
32
32
  theme: ThemeSettings;
33
33
  };
@@ -51,6 +51,7 @@ export async function loadSiteSettings(): Promise<LoadedSiteSettings> {
51
51
  navigation: {
52
52
  primary: navbarSettingsSchema.parse(navbarEntry?.data ?? {}).primary,
53
53
  footer: footerSettingsSchema.parse(footerEntry?.data ?? {}).footer,
54
+ footerGroups: footerSettingsSchema.parse(footerEntry?.data ?? {}).groups,
54
55
  dropdown: navbarSettingsSchema.parse(navbarEntry?.data ?? {}).dropdown,
55
56
  cta: navbarSettingsSchema.parse(navbarEntry?.data ?? {}).cta,
56
57
  },
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ferst-core",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "type": "module",
5
- "description": "Ferst Core — the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
5
+ "description": "Ferst Core \u2014 the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
6
6
  "license": "BUSL-1.1",
7
7
  "author": "MyAI4 Ltd (https://ferst.co.uk)",
8
8
  "homepage": "https://ferst.co.uk",
@@ -42,6 +42,7 @@
42
42
  "routes",
43
43
  "utils",
44
44
  "styles",
45
+ "fonts",
45
46
  "README.md",
46
47
  "NOTICE",
47
48
  "LICENSE",
@@ -51,7 +52,8 @@
51
52
  "ferst-check-thin": "bin/check-thin.mjs",
52
53
  "ferst-set-cms-branch": "bin/set-cms-branch.mjs",
53
54
  "ferst-cms-config": "bin/cms-config.mjs",
54
- "ferst-update": "bin/ferst-update.mjs"
55
+ "ferst-update": "bin/ferst-update.mjs",
56
+ "ferst-modules": "bin/ferst-modules.mjs"
55
57
  },
56
58
  "dependencies": {
57
59
  "zod": "^3.25.0"