@cogenta/starters 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blueprints/assets/photos/vitrine/about-instrument.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/about-servers.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/careers-open-office.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/careers-studio.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/case-grid-pylons.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/case-offshore-array.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/case-rail-bridge.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/case-water-pipes.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/contact-loft.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/hero-pylon-dusk.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-ardenne.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-castelane.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-helion.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-marea.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-railvia.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-tessane.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/news-blades.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/news-drive-internals.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/news-hydro-aerial.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/news-transformer.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/sector-energy-offshore.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/sector-industry-turbines.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/sector-rail-viaduct.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/sector-water-plant.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/solution-clean-room.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/solution-control-room.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/solution-field-fibre.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/solution-sensor-node.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/vigie-detail-en.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/vigie-detail-fr.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/vigie-overview-en.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/vigie-overview-fr.png +0 -0
- package/dist/blueprints/content-pack.d.ts +6 -0
- package/dist/blueprints/content-pack.d.ts.map +1 -1
- package/dist/blueprints/content-pack.js.map +1 -1
- package/dist/blueprints/content-packs.d.ts +7 -0
- package/dist/blueprints/content-packs.d.ts.map +1 -1
- package/dist/blueprints/content-packs.js +12 -1
- package/dist/blueprints/content-packs.js.map +1 -1
- package/dist/blueprints/starting-skins.d.ts.map +1 -1
- package/dist/blueprints/starting-skins.js +17 -19
- package/dist/blueprints/starting-skins.js.map +1 -1
- package/dist/blueprints/vitrine-copy-en.d.ts +4 -0
- package/dist/blueprints/vitrine-copy-en.d.ts.map +1 -0
- package/dist/blueprints/vitrine-copy-en.js +1085 -0
- package/dist/blueprints/vitrine-copy-en.js.map +1 -0
- package/dist/blueprints/vitrine-copy-fr.d.ts +4 -0
- package/dist/blueprints/vitrine-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/vitrine-copy-fr.js +1085 -0
- package/dist/blueprints/vitrine-copy-fr.js.map +1 -0
- package/dist/blueprints/vitrine-copy.d.ts +273 -0
- package/dist/blueprints/vitrine-copy.d.ts.map +1 -0
- package/dist/blueprints/vitrine-copy.js +15 -0
- package/dist/blueprints/vitrine-copy.js.map +1 -0
- package/dist/blueprints/vitrine-credits.d.ts +24 -0
- package/dist/blueprints/vitrine-credits.d.ts.map +1 -0
- package/dist/blueprints/vitrine-credits.js +190 -0
- package/dist/blueprints/vitrine-credits.js.map +1 -0
- package/dist/blueprints/vitrine.d.ts +59 -246
- package/dist/blueprints/vitrine.d.ts.map +1 -1
- package/dist/blueprints/vitrine.js +657 -989
- package/dist/blueprints/vitrine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/dist/blueprints/assets/photos/vitrine/avatar-1.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/avatar-2.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/avatar-3.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/case-council.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/case-hotels.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/case-rail.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/exhibit-working-capital.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/hero-review.jpg +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-aldermoor.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-brightwater.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-castell-vane.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-meridian-rail.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-nordvik.png +0 -0
- package/dist/blueprints/assets/photos/vitrine/logo-oakfield-health.png +0 -0
|
@@ -1,804 +1,549 @@
|
|
|
1
1
|
import { CogentaError } from '@cogenta/core';
|
|
2
2
|
import { createContentStore, createTaxonomyStore, defineCollection, defineTaxonomy, f, validateCollectionSet, validateTaxonomySet, } from '@cogenta/schema';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { ICON_NAMES } from '@cogenta/theme-kit';
|
|
4
|
+
import { coverArt, heroArt, logoArt } from '../demo-art/compositions.js';
|
|
5
|
+
import { definePageCollection, SEO_FIELDS, toBlockZoneEntry, } from './content-pack.js';
|
|
5
6
|
import { STARTING_SKINS } from './starting-skins.js';
|
|
7
|
+
import { VITRINE_COPY_EN } from './vitrine-copy-en.js';
|
|
8
|
+
import { VITRINE_COPY_FR } from './vitrine-copy-fr.js';
|
|
9
|
+
import { VITRINE_PHOTO_CREDITS } from './vitrine-credits.js';
|
|
6
10
|
/**
|
|
7
|
-
* The `vitrine` blueprint: the website of
|
|
8
|
-
*
|
|
11
|
+
* The `vitrine` blueprint: the website of an engineering company that designs
|
|
12
|
+
* sensors, software and field services for critical infrastructure (L36;
|
|
13
|
+
* previously a management consultancy, L9 to L27).
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* Written in French and in English, not translated at render time: the
|
|
16
|
+
* installer picks the copy for the site's default locale, and with it the
|
|
17
|
+
* addresses (`/references/…` in French, `/case-studies/…` in English), the
|
|
18
|
+
* collection names the administration shows and every word of demo content.
|
|
19
|
+
*
|
|
20
|
+
* Five editable collections sit behind the pages: solutions, case studies
|
|
21
|
+
* (filed by sector), testimonials, job openings and news articles. Every
|
|
22
|
+
* photograph is a real one under an open licence, credited on its own page;
|
|
23
|
+
* the client logos and the product screenshots were drawn for this blueprint.
|
|
15
24
|
*/
|
|
16
|
-
export const DEFAULT_FIRM_NAME = '
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
25
|
+
export const DEFAULT_FIRM_NAME = 'Norvane';
|
|
26
|
+
/** The copy for a site's default locale: French for any `fr*` locale, English otherwise. */
|
|
27
|
+
export function vitrineCopyFor(locale) {
|
|
28
|
+
return locale.toLowerCase().startsWith('fr') ? VITRINE_COPY_FR : VITRINE_COPY_EN;
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Content model
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const EDITORIAL = {
|
|
34
|
+
read: ['public'],
|
|
35
|
+
create: ['editor', 'admin'],
|
|
36
|
+
update: ['editor', 'admin'],
|
|
37
|
+
delete: ['admin'],
|
|
38
|
+
};
|
|
39
|
+
/** The content model, named and routed in the copy's language. */
|
|
40
|
+
export function vitrineSchema(copy) {
|
|
41
|
+
const { schema } = copy;
|
|
42
|
+
const sector = defineTaxonomy({
|
|
43
|
+
name: schema.taxonomy.name,
|
|
44
|
+
labels: {
|
|
45
|
+
singular: { en: 'Sector', fr: 'Secteur' },
|
|
46
|
+
plural: { en: 'Sectors', fr: 'Secteurs' },
|
|
47
|
+
},
|
|
48
|
+
hierarchical: false,
|
|
49
|
+
permissions: EDITORIAL,
|
|
50
|
+
});
|
|
51
|
+
const solution = defineCollection({
|
|
52
|
+
name: 'solution',
|
|
53
|
+
labels: schema.labels.solution,
|
|
54
|
+
routing: { pattern: schema.routes.solution },
|
|
55
|
+
fields: {
|
|
56
|
+
name: f.text({ required: true, max: 120 }),
|
|
57
|
+
slug: f.slug({ from: 'name', unique: true }),
|
|
58
|
+
description: f.text({ max: 400, multiline: true }),
|
|
59
|
+
body: f.richText(),
|
|
60
|
+
// A choice among the symbols every theme draws, not a name to remember (L36 audit).
|
|
61
|
+
icon: f.select({ options: [...ICON_NAMES], admin: { label: schema.fields.icon } }),
|
|
62
|
+
coverImage: f.media({ accept: ['image'] }),
|
|
63
|
+
...SEO_FIELDS,
|
|
64
|
+
},
|
|
65
|
+
indexes: [['slug']],
|
|
66
|
+
permissions: EDITORIAL,
|
|
67
|
+
});
|
|
68
|
+
const caseStudy = defineCollection({
|
|
69
|
+
name: 'case_study',
|
|
70
|
+
labels: schema.labels.caseStudy,
|
|
71
|
+
routing: { pattern: schema.routes.caseStudy },
|
|
72
|
+
fields: {
|
|
73
|
+
title: f.text({ required: true, max: 200 }),
|
|
74
|
+
slug: f.slug({ from: 'title', unique: true }),
|
|
75
|
+
client: f.text({ max: 120, admin: { label: schema.fields.client } }),
|
|
76
|
+
location: f.text({ max: 120, admin: { label: schema.fields.location } }),
|
|
77
|
+
keyFigure: f.text({ max: 40, admin: { label: schema.fields.keyFigure } }),
|
|
78
|
+
keyFigureLabel: f.text({ max: 120, admin: { label: schema.fields.keyFigureLabel } }),
|
|
79
|
+
summary: f.text({ max: 400, multiline: true }),
|
|
80
|
+
body: f.richText(),
|
|
81
|
+
coverImage: f.media({ accept: ['image'] }),
|
|
82
|
+
sector: f.taxonomy({ of: schema.taxonomy.name, many: false }),
|
|
83
|
+
...SEO_FIELDS,
|
|
84
|
+
},
|
|
85
|
+
indexes: [['slug']],
|
|
86
|
+
permissions: EDITORIAL,
|
|
87
|
+
});
|
|
88
|
+
const testimonial = defineCollection({
|
|
89
|
+
name: 'testimonial',
|
|
90
|
+
labels: schema.labels.testimonial,
|
|
91
|
+
fields: {
|
|
92
|
+
authorName: f.text({ required: true, max: 120 }),
|
|
93
|
+
authorRole: f.text({ max: 160 }),
|
|
94
|
+
quote: f.text({ required: true, max: 600, multiline: true }),
|
|
95
|
+
avatar: f.media({ accept: ['image'] }),
|
|
96
|
+
},
|
|
97
|
+
permissions: EDITORIAL,
|
|
98
|
+
});
|
|
99
|
+
const job = defineCollection({
|
|
100
|
+
name: 'job',
|
|
101
|
+
labels: schema.labels.job,
|
|
102
|
+
routing: { pattern: schema.routes.job },
|
|
103
|
+
fields: {
|
|
104
|
+
title: f.text({ required: true, max: 160 }),
|
|
105
|
+
slug: f.slug({ from: 'title', unique: true }),
|
|
106
|
+
team: f.text({ max: 80, admin: { label: schema.fields.team } }),
|
|
107
|
+
location: f.text({ max: 80, admin: { label: schema.fields.location } }),
|
|
108
|
+
contract: f.text({ max: 80, admin: { label: schema.fields.contract } }),
|
|
109
|
+
summary: f.text({ max: 400, multiline: true }),
|
|
110
|
+
body: f.richText(),
|
|
111
|
+
...SEO_FIELDS,
|
|
112
|
+
},
|
|
113
|
+
indexes: [['slug']],
|
|
114
|
+
permissions: EDITORIAL,
|
|
115
|
+
});
|
|
116
|
+
const post = defineCollection({
|
|
117
|
+
name: 'post',
|
|
118
|
+
labels: schema.labels.post,
|
|
119
|
+
routing: { pattern: schema.routes.post },
|
|
120
|
+
fields: {
|
|
121
|
+
title: f.text({ required: true, max: 200 }),
|
|
122
|
+
slug: f.slug({ from: 'title', unique: true }),
|
|
123
|
+
summary: f.text({ max: 400, multiline: true }),
|
|
124
|
+
author: f.text({ max: 120, admin: { label: schema.fields.author } }),
|
|
125
|
+
// Declared so an article can be scheduled, and appears in the
|
|
126
|
+
// editorial calendar (L35).
|
|
127
|
+
publishedAt: f.datetime(),
|
|
128
|
+
coverImage: f.media({ accept: ['image'] }),
|
|
129
|
+
body: f.richText(),
|
|
130
|
+
sector: f.taxonomy({ of: schema.taxonomy.name, many: false }),
|
|
131
|
+
...SEO_FIELDS,
|
|
132
|
+
},
|
|
133
|
+
indexes: [['slug']],
|
|
134
|
+
permissions: EDITORIAL,
|
|
135
|
+
});
|
|
136
|
+
const page = definePageCollection('/:slug');
|
|
137
|
+
const collections = [solution, caseStudy, testimonial, job, post, page];
|
|
138
|
+
const taxonomies = [sector];
|
|
139
|
+
validateCollectionSet(collections);
|
|
140
|
+
validateTaxonomySet(taxonomies, collections);
|
|
141
|
+
return { sector, solution, caseStudy, testimonial, job, post, page, collections, taxonomies };
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Rich text, written as data
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
function spans(key, inline) {
|
|
147
|
+
const parts = typeof inline === 'string' ? [inline] : inline;
|
|
148
|
+
const markDefs = [];
|
|
149
|
+
const children = parts.map((part, index) => {
|
|
150
|
+
const spanKey = `${key}-s${index}`;
|
|
151
|
+
if (typeof part === 'string')
|
|
152
|
+
return { _key: spanKey, _type: 'span', text: part, marks: [] };
|
|
153
|
+
const markKey = `${key}-l${index}`;
|
|
154
|
+
markDefs.push({ _key: markKey, _type: 'link', href: part.href });
|
|
155
|
+
return { _key: spanKey, _type: 'span', text: part.text, marks: [markKey] };
|
|
156
|
+
});
|
|
157
|
+
return { children, markDefs };
|
|
110
158
|
}
|
|
111
159
|
/** Builds a contract-A rich-text document from plain parts (never HTML, R3). */
|
|
112
|
-
function richText(key, parts) {
|
|
160
|
+
export function richText(key, parts) {
|
|
113
161
|
return parts.flatMap((part, index) => {
|
|
114
162
|
const partKey = `${key}-${index}`;
|
|
115
|
-
if ('p' in part) {
|
|
116
|
-
return [
|
|
117
|
-
{
|
|
118
|
-
_key: partKey,
|
|
119
|
-
_type: 'block',
|
|
120
|
-
style: 'normal',
|
|
121
|
-
children: [span(partKey, part.p)],
|
|
122
|
-
markDefs: [],
|
|
123
|
-
},
|
|
124
|
-
];
|
|
125
|
-
}
|
|
126
163
|
if ('h2' in part) {
|
|
127
|
-
return [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
style: 'h2',
|
|
132
|
-
children: [span(partKey, part.h2)],
|
|
133
|
-
markDefs: [],
|
|
134
|
-
},
|
|
135
|
-
];
|
|
164
|
+
return [{ _key: partKey, _type: 'block', style: 'h2', ...spans(partKey, part.h2) }];
|
|
165
|
+
}
|
|
166
|
+
if ('p' in part) {
|
|
167
|
+
return [{ _key: partKey, _type: 'block', style: 'normal', ...spans(partKey, part.p) }];
|
|
136
168
|
}
|
|
137
|
-
return part.bullets.map((
|
|
169
|
+
return part.bullets.map((item, bulletIndex) => ({
|
|
138
170
|
_key: `${partKey}-${bulletIndex}`,
|
|
139
171
|
_type: 'block',
|
|
140
172
|
style: 'normal',
|
|
141
173
|
listItem: 'bullet',
|
|
142
174
|
level: 1,
|
|
143
|
-
|
|
144
|
-
markDefs: [],
|
|
175
|
+
...spans(`${partKey}-${bulletIndex}`, item),
|
|
145
176
|
}));
|
|
146
177
|
});
|
|
147
178
|
}
|
|
148
|
-
export const VITRINE_DEMO_SERVICES = [
|
|
149
|
-
{
|
|
150
|
-
name: 'Strategy',
|
|
151
|
-
slug: 'strategy',
|
|
152
|
-
icon: 'globe',
|
|
153
|
-
description: 'Where to compete, what to stop doing and what the plan is worth. We build the three-year plan with the executive team and test every assumption against the figures the board will see.',
|
|
154
|
-
body: (firm) => [
|
|
155
|
-
{
|
|
156
|
-
p: `Strategy work at ${firm} starts from the operating results. Before anyone discusses ambition, we rebuild the last three years of profit by customer, product and channel, so the executive team and the board argue from one set of facts.`,
|
|
157
|
-
},
|
|
158
|
-
{ h2: 'Questions clients bring' },
|
|
159
|
-
{
|
|
160
|
-
bullets: [
|
|
161
|
-
'Which of our markets can we lead within five years, and which should we leave?',
|
|
162
|
-
'Should we grow by acquisition, and what can we afford to pay?',
|
|
163
|
-
'What does the business need to look like before a sale in three years?',
|
|
164
|
-
],
|
|
165
|
-
},
|
|
166
|
-
{ h2: 'What the work produces' },
|
|
167
|
-
{
|
|
168
|
-
p: 'A three-year plan with targets by business unit, the investment case behind each major decision, and a quarterly scorecard the board can run without us. Strategy engagements usually take ten to fourteen weeks.',
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
name: 'Operations',
|
|
174
|
-
slug: 'operations',
|
|
175
|
-
icon: 'settings',
|
|
176
|
-
description: 'Cost, service and working capital across plants, supply chains and shared services. Our programmes typically release 2 to 4% of revenue within eighteen months.',
|
|
177
|
-
body: (firm) => [
|
|
178
|
-
{
|
|
179
|
-
p: `${firm} consultants start on the floor: in the depot, the warehouse, the contact centre. The diagnostic takes six weeks and ends with a costed list of changes ranked by cash released and by risk to service.`,
|
|
180
|
-
},
|
|
181
|
-
{ h2: 'Where the value usually is' },
|
|
182
|
-
{
|
|
183
|
-
bullets: [
|
|
184
|
-
'Inventory held against forecasts nobody uses',
|
|
185
|
-
'Maintenance and rostering plans built for last year’s demand',
|
|
186
|
-
'Procurement spread across too many suppliers on short contracts',
|
|
187
|
-
],
|
|
188
|
-
},
|
|
189
|
-
{ h2: 'How results are measured' },
|
|
190
|
-
{
|
|
191
|
-
p: 'Every target is agreed with your finance director before implementation starts and tracked in your own management accounts. On most operations programmes part of our fee depends on those numbers.',
|
|
192
|
-
},
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
name: 'Finance and transactions',
|
|
197
|
-
slug: 'finance-transactions',
|
|
198
|
-
icon: 'chart',
|
|
199
|
-
description: 'Commercial due diligence, value-creation plans and post-merger integration for owners, lenders and management teams, from the first model to the hundredth day.',
|
|
200
|
-
body: () => [
|
|
201
|
-
{
|
|
202
|
-
p: 'We work for buyers, sellers and the management teams in between. A diligence report tells an investment committee what it needs to know in forty pages; the value-creation plan that follows is written to be executed by the company, with owners for every line.',
|
|
203
|
-
},
|
|
204
|
-
{ h2: 'Typical mandates' },
|
|
205
|
-
{
|
|
206
|
-
bullets: [
|
|
207
|
-
'Buy-side commercial diligence in four to six weeks',
|
|
208
|
-
'Vendor preparation twelve months before a sale process',
|
|
209
|
-
'Integration planning and the first hundred days after completion',
|
|
210
|
-
],
|
|
211
|
-
},
|
|
212
|
-
{ h2: 'Independence' },
|
|
213
|
-
{
|
|
214
|
-
p: 'We do not take success fees on transactions, and we will not advise both sides of the same deal.',
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
name: 'Organisation',
|
|
220
|
-
slug: 'organisation',
|
|
221
|
-
icon: 'users',
|
|
222
|
-
description: 'Operating models, spans and layers, and succession for the top hundred roles. We design structures a company can run without its advisers in the room.',
|
|
223
|
-
body: () => [
|
|
224
|
-
{
|
|
225
|
-
p: 'Most reorganisations fail in the second year, once the new chart meets the old habits. We design the structure together with the decision rights, the committee calendar and the performance measures, and we test it on real decisions before it is announced.',
|
|
226
|
-
},
|
|
227
|
-
{ h2: 'What we look at' },
|
|
228
|
-
{
|
|
229
|
-
bullets: [
|
|
230
|
-
'How many layers sit between the chief executive and the customer',
|
|
231
|
-
'Which decisions need three signatures, and why',
|
|
232
|
-
'Who is ready to take the twenty most critical roles in the next two years',
|
|
233
|
-
],
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
name: 'Technology and data',
|
|
239
|
-
slug: 'technology-data',
|
|
240
|
-
icon: 'layers',
|
|
241
|
-
description: 'Architecture reviews, vendor selection and delivery assurance for ERP, pricing and planning systems, judged against the business case they were bought for.',
|
|
242
|
-
body: () => [
|
|
243
|
-
{
|
|
244
|
-
p: 'We are independent of software vendors and integrators, and we are paid by the client alone. That lets us tell a board whether a stalled programme should be rescued, reduced or stopped.',
|
|
245
|
-
},
|
|
246
|
-
{ h2: 'Typical work' },
|
|
247
|
-
{
|
|
248
|
-
bullets: [
|
|
249
|
-
'Independent reviews of programmes that are late or over budget',
|
|
250
|
-
'Selection of ERP, pricing and planning software',
|
|
251
|
-
'Quarterly assurance reports for audit committees',
|
|
252
|
-
],
|
|
253
|
-
},
|
|
254
|
-
],
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
name: 'Risk and resilience',
|
|
258
|
-
slug: 'risk-resilience',
|
|
259
|
-
icon: 'shield',
|
|
260
|
-
description: 'Operational resilience, regulatory remediation and continuity planning, written to the standard a supervisor or an insurer will actually test.',
|
|
261
|
-
body: () => [
|
|
262
|
-
{
|
|
263
|
-
p: 'Resilience plans are usually written for the audit and discovered to be unworkable on the day they are needed. We run the scenarios with the people who would have to act on them, at night and without the usual systems.',
|
|
264
|
-
},
|
|
265
|
-
{ h2: 'Typical work' },
|
|
266
|
-
{
|
|
267
|
-
bullets: [
|
|
268
|
-
'Important business services and impact tolerances for regulated firms',
|
|
269
|
-
'Remediation programmes after a supervisory review',
|
|
270
|
-
'Crisis exercises for executive teams and boards',
|
|
271
|
-
],
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
},
|
|
275
|
-
];
|
|
276
|
-
export const VITRINE_DEMO_SECTORS = [
|
|
277
|
-
{ slug: 'transport', name: 'Transport' },
|
|
278
|
-
{ slug: 'hospitality', name: 'Hospitality' },
|
|
279
|
-
{ slug: 'public-sector', name: 'Public sector' },
|
|
280
|
-
];
|
|
281
|
-
export const VITRINE_DEMO_CASE_STUDIES = [
|
|
282
|
-
{
|
|
283
|
-
title: 'Meridian Rail raises punctuality from 88% to 94% in fourteen months',
|
|
284
|
-
slug: 'meridian-rail-punctuality',
|
|
285
|
-
client: 'Meridian Rail',
|
|
286
|
-
sectorSlug: 'transport',
|
|
287
|
-
summary: 'A commuter operator carrying 190,000 passengers a day rebuilt its crew rosters and depot maintenance plan. Cancellations caused by missing crew fell by two thirds.',
|
|
288
|
-
body: (firm) => [
|
|
289
|
-
{ h2: 'The situation' },
|
|
290
|
-
{
|
|
291
|
-
p: 'Meridian Rail runs 1,140 services a day on four lines into the city. Punctuality had fallen for three consecutive years, and the regulator had opened a formal review. Internal analysis blamed ageing trains; the operator’s own data showed that most delays began before a train left the depot.',
|
|
292
|
-
},
|
|
293
|
-
{ h2: 'What we did' },
|
|
294
|
-
{
|
|
295
|
-
p: `A team of five from ${firm} spent the first three weeks on night shifts in the two main depots and in the control room. Rosters were being built for a timetable that had changed twice since they were drawn up, and heavy maintenance was scheduled in the hours when spare trains were most needed.`,
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
bullets: [
|
|
299
|
-
'Rebuilt crew rosters around the current timetable, with a reserve pool at each depot',
|
|
300
|
-
'Moved heavy maintenance to a rolling overnight plan agreed with the unions',
|
|
301
|
-
'Set up a daily performance meeting using one shared delay log',
|
|
302
|
-
],
|
|
303
|
-
},
|
|
304
|
-
{ h2: 'Results' },
|
|
305
|
-
{
|
|
306
|
-
p: 'Trains arriving within five minutes of schedule rose from 88% to 94% over fourteen months. Cancellations caused by missing crew fell by 67%, and the regulator closed its review without penalty. The operator’s planning team now maintains the rosters itself.',
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
title: 'Castell & Vane Hotels adds £11.2 million of revenue with one pricing desk',
|
|
312
|
-
slug: 'castell-vane-pricing',
|
|
313
|
-
client: 'Castell & Vane Hotels',
|
|
314
|
-
sectorSlug: 'hospitality',
|
|
315
|
-
summary: 'Room rates at fourteen properties had been set by each general manager from a spreadsheet. A central pricing desk and a weekly forecast changed that within a season.',
|
|
316
|
-
body: (firm) => [
|
|
317
|
-
{ h2: 'The situation' },
|
|
318
|
-
{
|
|
319
|
-
p: 'Castell & Vane owns fourteen hotels in six cities, most of them historic buildings with restaurants that account for a third of revenue. Each general manager set room rates independently, and weekend prices were often lower than weekday prices in the same city.',
|
|
320
|
-
},
|
|
321
|
-
{ h2: 'What we did' },
|
|
322
|
-
{
|
|
323
|
-
p: `${firm} built a demand forecast from four years of bookings, local events and competitor rates, then designed a central pricing desk of three analysts who publish rates every Monday. General managers kept the right to override a price, with a reason recorded against it.`,
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
bullets: [
|
|
327
|
-
'Weekly forecast by property, room type and channel',
|
|
328
|
-
'Rate rules for group bookings, long stays and restaurant packages',
|
|
329
|
-
'Training for fourteen general managers and their revenue leads',
|
|
330
|
-
],
|
|
331
|
-
},
|
|
332
|
-
{ h2: 'Results' },
|
|
333
|
-
{
|
|
334
|
-
p: 'Revenue per available room rose by 9% in the first full year, worth £11.2 million, with occupancy unchanged. The pricing desk paid for the engagement within eleven weeks.',
|
|
335
|
-
},
|
|
336
|
-
],
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
title: 'Kellmoor City Council closes a £38 million budget gap and keeps every library open',
|
|
340
|
-
slug: 'kellmoor-council-budget',
|
|
341
|
-
client: 'Kellmoor City Council',
|
|
342
|
-
sectorSlug: 'public-sector',
|
|
343
|
-
summary: 'Facing a statutory deficit, the council merged six back-office functions into one shared service and renegotiated 41 contracts. Front-line spending was protected in full.',
|
|
344
|
-
body: (firm) => [
|
|
345
|
-
{ h2: 'The situation' },
|
|
346
|
-
{
|
|
347
|
-
p: 'Kellmoor City Council serves 310,000 residents. Rising care costs had opened a £38 million gap in a £420 million budget, and the council had eighteen months to close it before the government could intervene.',
|
|
348
|
-
},
|
|
349
|
-
{ h2: 'What we did' },
|
|
350
|
-
{
|
|
351
|
-
p: `${firm} began with the council’s statutory duties, then its cost base. Each proposed saving was checked against those duties by the council’s own legal team before it went to elected members.`,
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
bullets: [
|
|
355
|
-
'Merged finance, payroll, procurement, IT, legal and property support into one shared service',
|
|
356
|
-
'Renegotiated 41 contracts worth £96 million a year',
|
|
357
|
-
'Sold or let eleven under-used buildings',
|
|
358
|
-
],
|
|
359
|
-
},
|
|
360
|
-
{ h2: 'Results' },
|
|
361
|
-
{
|
|
362
|
-
p: 'The gap was closed four months ahead of the deadline. Libraries, children’s centres and road maintenance kept their budgets in full, and none of the savings has had to be reversed.',
|
|
363
|
-
},
|
|
364
|
-
],
|
|
365
|
-
},
|
|
366
|
-
];
|
|
367
|
-
export const VITRINE_DEMO_TESTIMONIALS = [
|
|
368
|
-
{
|
|
369
|
-
authorName: 'Helen Achterberg',
|
|
370
|
-
authorRole: 'Chief Operating Officer, Meridian Rail',
|
|
371
|
-
quote: () => 'They spent three weeks in our depots and on night shifts before they showed us a single slide. The rosters we run today are theirs, and so are the eleven people on my team who now maintain them.',
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
authorName: 'Tomás Aguirre',
|
|
375
|
-
authorRole: 'Group Finance Director, Castell & Vane Hotels',
|
|
376
|
-
quote: () => 'The pricing desk paid for the whole engagement in its first eleven weeks. What I value more is that my general managers now argue about the forecast using the same numbers.',
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
authorName: 'Daniel Whitcombe',
|
|
380
|
-
authorRole: 'Chief Executive, Kellmoor City Council',
|
|
381
|
-
quote: (firm) => `${firm} was the only adviser that asked to see our statutory duties before our cost base. Every saving in the plan was checked against them, and none has had to be reversed.`,
|
|
382
|
-
},
|
|
383
|
-
];
|
|
384
179
|
// ---------------------------------------------------------------------------
|
|
385
180
|
// Pages
|
|
386
181
|
// ---------------------------------------------------------------------------
|
|
387
182
|
const BLOCK_VERSION = '1.0.0';
|
|
183
|
+
function block(key, type, data) {
|
|
184
|
+
return { _key: key, _type: type, _version: BLOCK_VERSION, ...data };
|
|
185
|
+
}
|
|
388
186
|
function prose(key, parts) {
|
|
389
|
-
return {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
187
|
+
return block(key, 'prose', { body: richText(`${key}-body`, parts) });
|
|
188
|
+
}
|
|
189
|
+
function questions(key, items) {
|
|
190
|
+
return items.map(([question, answer], index) => ({
|
|
191
|
+
_key: `${key}-${index}`,
|
|
192
|
+
question,
|
|
193
|
+
answer: richText(`${key}-${index}-a`, [{ p: answer }]),
|
|
194
|
+
}));
|
|
395
195
|
}
|
|
396
|
-
const LOGO_KEYS = [
|
|
397
|
-
'logo-meridian-rail',
|
|
398
|
-
'logo-castell-vane',
|
|
399
|
-
'logo-aldermoor',
|
|
400
|
-
'logo-brightwater',
|
|
401
|
-
'logo-nordvik',
|
|
402
|
-
'logo-oakfield-health',
|
|
403
|
-
];
|
|
404
|
-
const LOGO_NAMES = {
|
|
405
|
-
'logo-meridian-rail': 'Meridian Rail',
|
|
406
|
-
'logo-castell-vane': 'Castell & Vane Hotels',
|
|
407
|
-
'logo-aldermoor': 'Aldermoor Bank',
|
|
408
|
-
'logo-brightwater': 'Brightwater Foods',
|
|
409
|
-
'logo-nordvik': 'Nordvik Industries',
|
|
410
|
-
'logo-oakfield-health': 'Oakfield Health',
|
|
411
|
-
};
|
|
412
196
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* A function of `media` (`SeedContext.media`), of the real practice ids
|
|
418
|
-
* `seedVitrineDemoContent` assigns (`serviceIdBySlug`) and of the firm's name.
|
|
419
|
-
* All three default, so `buildVitrineDemoPages({})` still returns complete,
|
|
420
|
-
* contract-valid pages: a block whose required media is absent is omitted
|
|
421
|
-
* rather than emitted invalid.
|
|
197
|
+
* Every page of the site, in the copy's language: home, solutions, case
|
|
198
|
+
* studies, company, careers, news, contact, legal notice, privacy and photo
|
|
199
|
+
* credits. A block whose media is absent is omitted rather than emitted
|
|
200
|
+
* invalid, so `buildVitrineDemoPages(copy)` alone returns contract-valid pages.
|
|
422
201
|
*/
|
|
423
|
-
export function buildVitrineDemoPages(
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
202
|
+
export function buildVitrineDemoPages(copy, context = {}) {
|
|
203
|
+
const media = context.media ?? {};
|
|
204
|
+
const firm = context.firm ?? DEFAULT_FIRM_NAME;
|
|
205
|
+
const words = copy.pages(firm);
|
|
206
|
+
const slug = copy.pageSlugs;
|
|
207
|
+
const path = (key) => `/${slug[key]}`;
|
|
208
|
+
const taxonomy = copy.schema.taxonomy.name;
|
|
209
|
+
const [ops, water, offshore] = copy.testimonials;
|
|
210
|
+
const figure = (key, name, caption, align = 'wide') => media[name] === undefined
|
|
211
|
+
? []
|
|
212
|
+
: [block(key, 'mediaFigure', { media: media[name], caption, ratio: 'original', align })];
|
|
213
|
+
const testimonialBlock = (key, entry) => block(key, 'testimonial', {
|
|
214
|
+
quote: richText(`${key}-quote`, [{ p: entry.quote(firm) }]),
|
|
215
|
+
attribution: { name: entry.authorName, role: entry.authorRole },
|
|
216
|
+
});
|
|
217
|
+
const list = (key, collection, layout, limit, title, sort = 'asc') => block(key, 'collectionList', {
|
|
218
|
+
...(title === undefined ? {} : { title }),
|
|
219
|
+
collection,
|
|
220
|
+
sort: { field: 'createdAt', direction: sort },
|
|
221
|
+
limit,
|
|
222
|
+
layout,
|
|
223
|
+
});
|
|
224
|
+
const logos = copy.clients.filter((client) => media[client.key] !== undefined);
|
|
225
|
+
const heroBlock = (key, eyebrow, title, subtitle, mediaName, actions) => block(key, 'hero', {
|
|
226
|
+
eyebrow,
|
|
432
227
|
title,
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
...(link === undefined ? {} : { link }),
|
|
441
|
-
};
|
|
442
|
-
}),
|
|
228
|
+
subtitle,
|
|
229
|
+
...(media[mediaName] === undefined ? {} : { media: media[mediaName] }),
|
|
230
|
+
actions: actions.map((action) => ({
|
|
231
|
+
label: action.label,
|
|
232
|
+
target: { href: action.href },
|
|
233
|
+
...(action.primary === true ? { emphasis: 'primary' } : {}),
|
|
234
|
+
})),
|
|
443
235
|
});
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
236
|
+
const cta = (key, title, text, actions) => block(key, 'cta', {
|
|
237
|
+
title,
|
|
238
|
+
text,
|
|
239
|
+
actions: actions.map((action) => ({
|
|
240
|
+
label: action.label,
|
|
241
|
+
target: { href: action.href },
|
|
242
|
+
...(action.primary === true ? { emphasis: 'primary' } : {}),
|
|
243
|
+
})),
|
|
452
244
|
});
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
};
|
|
458
|
-
const home =
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
],
|
|
474
|
-
},
|
|
475
|
-
...(logoIds.length === 0
|
|
476
|
-
? []
|
|
477
|
-
: [
|
|
478
|
-
{
|
|
479
|
-
_key: 'demo-home-clients',
|
|
480
|
-
_type: 'logoStrip',
|
|
481
|
-
_version: BLOCK_VERSION,
|
|
482
|
-
logos: logoIds.map((logo, index) => ({
|
|
483
|
-
_key: `demo-home-client-${index}`,
|
|
484
|
-
media: logo.id,
|
|
485
|
-
})),
|
|
486
|
-
caption: 'Selected clients',
|
|
487
|
-
},
|
|
488
|
-
]),
|
|
489
|
-
practices('demo-home-practices', 'Practices'),
|
|
490
|
-
{
|
|
491
|
-
_key: 'demo-home-figures',
|
|
492
|
-
_type: 'stats',
|
|
493
|
-
_version: BLOCK_VERSION,
|
|
494
|
-
title: 'The firm in figures',
|
|
495
|
-
items: [
|
|
496
|
-
{ _key: 'demo-home-figure-1', value: '340', label: 'engagements completed since 2011' },
|
|
497
|
-
{
|
|
498
|
-
_key: 'demo-home-figure-2',
|
|
499
|
-
value: '72',
|
|
500
|
-
unit: '%',
|
|
501
|
-
label: 'of fees from clients who have worked with us before',
|
|
502
|
-
},
|
|
503
|
-
{
|
|
504
|
-
_key: 'demo-home-figure-3',
|
|
505
|
-
value: '58',
|
|
506
|
-
label: 'consultants, a third of them former operating executives',
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
_key: 'demo-home-figure-4',
|
|
510
|
-
value: '3',
|
|
511
|
-
label: 'offices, in London, New York and Singapore',
|
|
512
|
-
},
|
|
513
|
-
],
|
|
514
|
-
},
|
|
515
|
-
{
|
|
516
|
-
_key: 'demo-home-work',
|
|
517
|
-
_type: 'collectionList',
|
|
518
|
-
_version: BLOCK_VERSION,
|
|
519
|
-
title: 'Selected work',
|
|
520
|
-
collection: 'case_study',
|
|
521
|
-
sort: { field: 'createdAt', direction: 'asc' },
|
|
522
|
-
limit: 3,
|
|
523
|
-
layout: 'list',
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
_key: 'demo-home-testimonial',
|
|
527
|
-
_type: 'testimonial',
|
|
528
|
-
_version: BLOCK_VERSION,
|
|
529
|
-
quote: richTextParagraph('demo-home-testimonial-quote', rail.quote(firm)),
|
|
530
|
-
attribution: { name: rail.authorName, role: rail.authorRole, ...avatar(0) },
|
|
531
|
-
},
|
|
532
|
-
prose('demo-home-research', [
|
|
533
|
-
{ h2: 'Research' },
|
|
534
|
-
{
|
|
535
|
-
p: 'Each year we survey the finance and operations leaders of mid-sized companies. The 2026 edition covers 412 companies in four countries, and its central finding is simple: the number of separate planning cycles a company runs predicts how much cash is tied up in the business better than its sector or its size.',
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
p: 'Companies that had merged three or more of those cycles released a median 3.8% of revenue in working capital within a year. The full report, with the method and the questionnaire, is available from any of our offices.',
|
|
539
|
-
},
|
|
245
|
+
const page = (key, pageSlug, blocks) => ({
|
|
246
|
+
title: words.titles[key],
|
|
247
|
+
slug: pageSlug,
|
|
248
|
+
blocks,
|
|
249
|
+
});
|
|
250
|
+
const home = page('home', 'home', [
|
|
251
|
+
heroBlock('home-hero', words.home.heroEyebrow, words.home.heroTitle, words.home.heroSubtitle, 'hero', [
|
|
252
|
+
{ label: words.home.heroPrimary, href: path('contact'), primary: true },
|
|
253
|
+
{ label: words.home.heroSecondary, href: path('caseStudies') },
|
|
254
|
+
]),
|
|
255
|
+
...(logos.length === 0
|
|
256
|
+
? []
|
|
257
|
+
: [
|
|
258
|
+
block('home-clients', 'logoStrip', {
|
|
259
|
+
logos: logos.map((client, index) => ({
|
|
260
|
+
_key: `home-client-${index}`,
|
|
261
|
+
media: media[client.key],
|
|
262
|
+
})),
|
|
263
|
+
caption: words.home.clientsCaption,
|
|
264
|
+
}),
|
|
540
265
|
]),
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
266
|
+
list('home-solutions', 'solution', 'grid', 6, words.home.solutionsTitle),
|
|
267
|
+
block('home-figures', 'stats', {
|
|
268
|
+
title: words.home.figuresTitle,
|
|
269
|
+
items: words.home.figures.map((item, index) => ({ _key: `home-figure-${index}`, ...item })),
|
|
270
|
+
}),
|
|
271
|
+
prose('home-platform', words.home.platformIntro),
|
|
272
|
+
...figure('home-platform-figure', 'platform-overview', words.home.platformCaption, 'wide'),
|
|
273
|
+
block('home-sectors', 'featureGrid', {
|
|
274
|
+
title: words.home.sectorsTitle,
|
|
275
|
+
items: copy.sectors.map((sector, index) => ({
|
|
276
|
+
_key: `home-sector-${index}`,
|
|
277
|
+
icon: sector.icon,
|
|
278
|
+
title: sector.name,
|
|
279
|
+
text: sector.text,
|
|
280
|
+
link: { href: `/${taxonomy}/${sector.slug}` },
|
|
281
|
+
})),
|
|
282
|
+
}),
|
|
283
|
+
list('home-work', 'case_study', 'list', 3, words.home.workTitle),
|
|
284
|
+
testimonialBlock('home-testimonial', ops),
|
|
285
|
+
list('home-news', 'post', 'grid', 3, words.home.newsTitle, 'desc'),
|
|
286
|
+
block('home-faq', 'faq', {
|
|
287
|
+
title: words.home.faqTitle,
|
|
288
|
+
items: questions('home-faq', words.home.faq),
|
|
289
|
+
}),
|
|
290
|
+
cta('home-cta', words.home.ctaTitle, words.home.ctaText, [
|
|
291
|
+
{ label: words.home.ctaPrimary, href: path('contact'), primary: true },
|
|
292
|
+
{ label: words.home.ctaSecondary, href: path('solutions') },
|
|
293
|
+
]),
|
|
294
|
+
]);
|
|
295
|
+
const solutions = page('solutions', slug.solutions, [
|
|
296
|
+
prose('solutions-intro', words.solutions.intro),
|
|
297
|
+
list('solutions-list', 'solution', 'grid', 12, words.solutions.listTitle),
|
|
298
|
+
...figure('solutions-detail', 'platform-detail', words.solutions.detailCaption, 'wide'),
|
|
299
|
+
block('solutions-method', 'accordion', {
|
|
300
|
+
title: words.solutions.methodTitle,
|
|
301
|
+
items: questions('solutions-method', words.solutions.method),
|
|
302
|
+
}),
|
|
303
|
+
cta('solutions-cta', words.home.ctaTitle, words.home.ctaText, [
|
|
304
|
+
{ label: words.home.ctaPrimary, href: path('contact'), primary: true },
|
|
305
|
+
]),
|
|
306
|
+
]);
|
|
307
|
+
const caseStudies = page('caseStudies', slug.caseStudies, [
|
|
308
|
+
prose('work-intro', words.caseStudies.intro),
|
|
309
|
+
list('work-all', 'case_study', 'grid', 12),
|
|
310
|
+
block('work-figures', 'stats', {
|
|
311
|
+
title: words.caseStudies.figuresTitle,
|
|
312
|
+
items: words.caseStudies.figures.map((item, index) => ({
|
|
313
|
+
_key: `work-figure-${index}`,
|
|
314
|
+
...item,
|
|
315
|
+
})),
|
|
316
|
+
}),
|
|
317
|
+
...(logos.length === 0
|
|
318
|
+
? []
|
|
319
|
+
: [
|
|
320
|
+
block('work-clients', 'logos', {
|
|
321
|
+
title: words.caseStudies.logosTitle,
|
|
322
|
+
items: logos.map((client, index) => ({
|
|
323
|
+
_key: `work-client-${index}`,
|
|
324
|
+
media: media[client.key],
|
|
325
|
+
name: client.name,
|
|
326
|
+
})),
|
|
327
|
+
}),
|
|
567
328
|
]),
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
329
|
+
testimonialBlock('work-testimonial', water),
|
|
330
|
+
cta('work-cta', words.home.ctaTitle, words.home.ctaText, [
|
|
331
|
+
{ label: words.home.ctaPrimary, href: path('contact'), primary: true },
|
|
332
|
+
]),
|
|
333
|
+
]);
|
|
334
|
+
const fieldPhotos = ['field-rail', 'field-water', 'field-energy', 'solution-predictive']
|
|
335
|
+
.map((name) => media[name])
|
|
336
|
+
.filter((id) => id !== undefined);
|
|
337
|
+
const company = page('company', slug.company, [
|
|
338
|
+
heroBlock('company-hero', words.company.heroEyebrow, words.company.heroTitle, words.company.heroSubtitle, 'company-studio', []),
|
|
339
|
+
prose('company-story', words.company.story),
|
|
340
|
+
block('company-figures', 'statCounter', {
|
|
341
|
+
title: words.company.figuresTitle,
|
|
342
|
+
stats: words.company.figures.map((item, index) => ({
|
|
343
|
+
_key: `company-figure-${index}`,
|
|
344
|
+
...item,
|
|
345
|
+
})),
|
|
346
|
+
}),
|
|
347
|
+
block('company-principles', 'featureGrid', {
|
|
348
|
+
title: words.company.principlesTitle,
|
|
349
|
+
items: words.company.principles.map((item, index) => ({
|
|
350
|
+
_key: `company-principle-${index}`,
|
|
351
|
+
...item,
|
|
352
|
+
})),
|
|
353
|
+
}),
|
|
354
|
+
...figure('company-lab', 'company-lab', words.company.labCaption, 'wide'),
|
|
355
|
+
block('company-team', 'featureGrid', {
|
|
356
|
+
title: words.company.teamTitle,
|
|
357
|
+
items: copy.team.map((member, index) => ({
|
|
358
|
+
_key: `company-team-${index}`,
|
|
359
|
+
icon: 'user',
|
|
360
|
+
title: member.name,
|
|
361
|
+
text: `${member.role}. ${member.description}`,
|
|
362
|
+
})),
|
|
363
|
+
}),
|
|
364
|
+
...(fieldPhotos.length === 0
|
|
365
|
+
? []
|
|
366
|
+
: [
|
|
367
|
+
prose('company-field-title', [{ h2: words.company.fieldTitle }]),
|
|
368
|
+
block('company-field', 'gallery', {
|
|
369
|
+
items: fieldPhotos.map((id, index) => ({ _key: `company-field-${index}`, media: id })),
|
|
370
|
+
layout: 'grid',
|
|
371
|
+
}),
|
|
608
372
|
]),
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
373
|
+
testimonialBlock('company-testimonial', offshore),
|
|
374
|
+
cta('company-cta', words.careers.heroTitle, words.careers.heroSubtitle, [
|
|
375
|
+
{ label: words.careers.heroAction, href: path('careers'), primary: true },
|
|
376
|
+
]),
|
|
377
|
+
]);
|
|
378
|
+
const careers = page('careers', slug.careers, [
|
|
379
|
+
heroBlock('careers-hero', words.careers.heroEyebrow, words.careers.heroTitle, words.careers.heroSubtitle, 'careers-hero', [{ label: words.careers.ctaAction, href: 'mailto:jobs@example.com', primary: true }]),
|
|
380
|
+
prose('careers-intro', words.careers.intro),
|
|
381
|
+
list('careers-roles', 'job', 'list', 20, words.careers.rolesTitle),
|
|
382
|
+
block('careers-benefits', 'featureGrid', {
|
|
383
|
+
title: words.careers.benefitsTitle,
|
|
384
|
+
items: words.careers.benefits.map((item, index) => ({
|
|
385
|
+
_key: `careers-benefit-${index}`,
|
|
386
|
+
...item,
|
|
387
|
+
})),
|
|
388
|
+
}),
|
|
389
|
+
block('careers-process', 'accordion', {
|
|
390
|
+
title: words.careers.processTitle,
|
|
391
|
+
items: questions('careers-process', words.careers.process),
|
|
392
|
+
}),
|
|
393
|
+
cta('careers-cta', words.careers.ctaTitle, words.careers.ctaText, [
|
|
394
|
+
{ label: words.careers.ctaAction, href: 'mailto:jobs@example.com', primary: true },
|
|
395
|
+
]),
|
|
396
|
+
]);
|
|
397
|
+
const news = page('news', slug.news, [
|
|
398
|
+
prose('news-intro', words.news.intro),
|
|
399
|
+
list('news-all', 'post', 'grid', 24, undefined, 'desc'),
|
|
400
|
+
]);
|
|
401
|
+
const contact = page('contact', slug.contact, [
|
|
402
|
+
prose('contact-offices', words.contact.intro),
|
|
403
|
+
...figure('contact-office', 'contact-office', words.contact.officeCaption, 'wide'),
|
|
404
|
+
cta('contact-cta', words.contact.ctaTitle, words.contact.ctaText, [
|
|
405
|
+
{ label: words.contact.ctaAction, href: 'mailto:contact@example.com', primary: true },
|
|
406
|
+
]),
|
|
407
|
+
]);
|
|
408
|
+
const legal = page('legal', slug.legal, [prose('legal-body', words.legal)]);
|
|
409
|
+
const privacy = page('privacy', slug.privacy, [prose('privacy-body', words.privacy)]);
|
|
410
|
+
const credits = page('credits', slug.credits, [
|
|
411
|
+
prose('credits-body', [
|
|
412
|
+
...words.credits.intro,
|
|
413
|
+
{
|
|
414
|
+
bullets: VITRINE_PHOTO_CREDITS.map((credit) => [
|
|
415
|
+
`${credit.title} · ${credit.author} · `,
|
|
416
|
+
credit.licenceUrl === undefined
|
|
417
|
+
? credit.licence
|
|
418
|
+
: { text: credit.licence, href: credit.licenceUrl },
|
|
419
|
+
' · ',
|
|
420
|
+
{ text: words.credits.sourceWord, href: credit.source },
|
|
632
421
|
]),
|
|
633
|
-
callToAction('demo-work-cta'),
|
|
634
|
-
],
|
|
635
|
-
};
|
|
636
|
-
const about = {
|
|
637
|
-
title: 'About',
|
|
638
|
-
slug: 'about',
|
|
639
|
-
blocks: [
|
|
640
|
-
prose('demo-about-story', [
|
|
641
|
-
{
|
|
642
|
-
p: `${firm} was founded in London in 2011 by four partners who had spent their careers running operations inside mid-sized companies before advising them.`,
|
|
643
|
-
},
|
|
644
|
-
{ h2: 'Why we started' },
|
|
645
|
-
{
|
|
646
|
-
p: 'Large consultancies were built for the largest companies, and their methods assume a head office with hundreds of analysts. Companies with a few thousand employees need the same quality of thinking from a team that fits in their building and understands a business where the chief executive signs the larger purchase orders.',
|
|
647
|
-
},
|
|
648
|
-
{ h2: 'How we work' },
|
|
649
|
-
{
|
|
650
|
-
p: 'A partner leads every engagement from the first meeting to the final review. Teams are small and stay with a client from diagnosis to implementation, and we publish our research so clients can judge our thinking before they hire us.',
|
|
651
|
-
},
|
|
652
|
-
{
|
|
653
|
-
p: 'We opened in New York in 2016 and in Singapore in 2021, each time to follow clients who were expanding there.',
|
|
654
|
-
},
|
|
655
|
-
{ h2: 'Ownership' },
|
|
656
|
-
{
|
|
657
|
-
p: 'The firm is a limited liability partnership owned by its 14 partners. We have no outside shareholders and no commercial relationships with software vendors.',
|
|
658
|
-
},
|
|
659
|
-
]),
|
|
660
|
-
{
|
|
661
|
-
_key: 'demo-about-figures',
|
|
662
|
-
_type: 'statCounter',
|
|
663
|
-
_version: BLOCK_VERSION,
|
|
664
|
-
title: 'Since 2011',
|
|
665
|
-
stats: [
|
|
666
|
-
{ _key: 'demo-about-figure-1', value: '340', label: 'engagements completed' },
|
|
667
|
-
{
|
|
668
|
-
_key: 'demo-about-figure-2',
|
|
669
|
-
value: '14',
|
|
670
|
-
label: 'partners, each with operating experience',
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
_key: 'demo-about-figure-3',
|
|
674
|
-
value: '21',
|
|
675
|
-
label: 'countries where clients have operations',
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
_key: 'demo-about-figure-4',
|
|
679
|
-
value: '£1.9bn',
|
|
680
|
-
label: 'of cash and profit our clients have reported',
|
|
681
|
-
},
|
|
682
|
-
],
|
|
683
|
-
},
|
|
684
|
-
{
|
|
685
|
-
_key: 'demo-about-quote-hotels',
|
|
686
|
-
_type: 'quote',
|
|
687
|
-
_version: BLOCK_VERSION,
|
|
688
|
-
text: hotels.quote(firm),
|
|
689
|
-
author: hotels.authorName,
|
|
690
|
-
role: hotels.authorRole,
|
|
691
|
-
...avatar(1),
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
_key: 'demo-about-quote-council',
|
|
695
|
-
_type: 'quote',
|
|
696
|
-
_version: BLOCK_VERSION,
|
|
697
|
-
text: council.quote(firm),
|
|
698
|
-
author: council.authorName,
|
|
699
|
-
role: council.authorRole,
|
|
700
|
-
...avatar(2),
|
|
701
|
-
},
|
|
702
|
-
callToAction('demo-about-cta'),
|
|
703
|
-
],
|
|
704
|
-
};
|
|
705
|
-
const contact = {
|
|
706
|
-
title: 'Contact',
|
|
707
|
-
slug: 'contact',
|
|
708
|
-
blocks: [
|
|
709
|
-
prose('demo-contact-offices', [
|
|
710
|
-
{
|
|
711
|
-
p: 'Write to the office nearest to you, or to any partner you already know. We reply within two working days.',
|
|
712
|
-
},
|
|
713
|
-
{ h2: 'London' },
|
|
714
|
-
{
|
|
715
|
-
p: '12 Hanover Square, London W1S 1JB, United Kingdom. Telephone +44 20 7946 0321. london@example.com',
|
|
716
|
-
},
|
|
717
|
-
{ h2: 'New York' },
|
|
718
|
-
{
|
|
719
|
-
p: '230 Park Avenue, Floor 10, New York, NY 10169, United States. Telephone +1 212 555 0148. newyork@example.com',
|
|
720
|
-
},
|
|
721
|
-
{ h2: 'Singapore' },
|
|
722
|
-
{
|
|
723
|
-
p: '8 Marina View, #32-01, Singapore 018960. Telephone +65 6555 0172. singapore@example.com',
|
|
724
|
-
},
|
|
725
|
-
{ h2: 'Careers' },
|
|
726
|
-
{
|
|
727
|
-
p: 'We recruit experienced hires throughout the year and graduates each autumn. Send a CV and a short note on the kind of work you want to do to careers@example.com.',
|
|
728
|
-
},
|
|
729
|
-
]),
|
|
730
|
-
{
|
|
731
|
-
_key: 'demo-contact-cta',
|
|
732
|
-
_type: 'cta',
|
|
733
|
-
_version: BLOCK_VERSION,
|
|
734
|
-
title: 'Discuss a mandate',
|
|
735
|
-
text: 'Describe the decision, its timing and who is involved. A partner will reply within two working days.',
|
|
736
|
-
actions: [
|
|
737
|
-
{
|
|
738
|
-
label: 'Write to us',
|
|
739
|
-
target: { href: 'mailto:hello@example.com' },
|
|
740
|
-
emphasis: 'primary',
|
|
741
|
-
},
|
|
742
|
-
],
|
|
743
422
|
},
|
|
744
|
-
],
|
|
423
|
+
]),
|
|
424
|
+
]);
|
|
425
|
+
return [home, solutions, caseStudies, company, careers, news, contact, legal, privacy, credits];
|
|
426
|
+
}
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Menus, settings, widgets
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
export function vitrineMenus(copy) {
|
|
431
|
+
const url = (page) => `/${copy.pageSlugs[page]}`;
|
|
432
|
+
return {
|
|
433
|
+
header: copy.menus.header.map((item) => ({ label: item.label, url: url(item.page) })),
|
|
434
|
+
footer: copy.menus.footer.map((item) => ({ label: item.label, url: url(item.page) })),
|
|
435
|
+
headerAction: { label: copy.menus.action, url: url('contact') },
|
|
745
436
|
};
|
|
746
|
-
return [home, practicesPage, caseStudiesPage, about, contact];
|
|
747
437
|
}
|
|
748
|
-
function
|
|
749
|
-
const
|
|
750
|
-
[
|
|
751
|
-
'What size of company do you work with?',
|
|
752
|
-
'Most of our clients have annual revenue between £50 million and £2 billion and are owned by founders, families or private equity. We also advise public bodies and a small number of listed companies.',
|
|
753
|
-
],
|
|
754
|
-
[
|
|
755
|
-
'How is an engagement priced?',
|
|
756
|
-
'We agree a fixed fee for a defined scope before work starts. On operations programmes, up to a third of the fee can depend on results measured by your own finance team.',
|
|
757
|
-
],
|
|
758
|
-
[
|
|
759
|
-
'Who will we work with day to day?',
|
|
760
|
-
'A partner leads every engagement and attends each steering meeting. Teams are small, usually three to six consultants, and the people who scope the work are the people who do it.',
|
|
761
|
-
],
|
|
762
|
-
[
|
|
763
|
-
'How long does a typical engagement last?',
|
|
764
|
-
'Diagnostic work takes six to ten weeks. Implementation runs from six to eighteen months, with a formal review at each stage, so you can stop or change course.',
|
|
765
|
-
],
|
|
766
|
-
[
|
|
767
|
-
'Do you work alongside other advisers?',
|
|
768
|
-
'Often. We work with auditors, lawyers and systems integrators under a shared plan, and we will say early if another firm is better placed for part of the work.',
|
|
769
|
-
],
|
|
770
|
-
];
|
|
438
|
+
export function vitrineSiteSettings(copy, firm = DEFAULT_FIRM_NAME) {
|
|
439
|
+
const settings = copy.settings(firm);
|
|
771
440
|
return {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
answer: richTextParagraph(`demo-home-faq-${index}-a`, answer),
|
|
780
|
-
})),
|
|
441
|
+
'general.tagline': settings.tagline,
|
|
442
|
+
'general.socialLinks': [
|
|
443
|
+
{ label: 'LinkedIn', url: 'https://linkedin.com/company/example' },
|
|
444
|
+
{ label: 'GitHub', url: 'https://github.com/example' },
|
|
445
|
+
{ label: 'YouTube', url: 'https://youtube.com/@example' },
|
|
446
|
+
],
|
|
447
|
+
'general.footerNote': settings.footerNote,
|
|
781
448
|
};
|
|
782
449
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
{
|
|
787
|
-
name: 'seoAgent',
|
|
788
|
-
package: '@cogenta/agents-builtin',
|
|
789
|
-
reason: 'Audits practice and case-study pages for on-page SEO issues before they go live.',
|
|
790
|
-
},
|
|
791
|
-
{
|
|
792
|
-
name: 'performanceAgent',
|
|
793
|
-
package: '@cogenta/agents-builtin',
|
|
794
|
-
reason: 'Catches oversized images and third-party scripts that would slow the site down.',
|
|
795
|
-
},
|
|
796
|
-
];
|
|
450
|
+
function onlyOn(...targets) {
|
|
451
|
+
return { pages: { mode: 'only', targets } };
|
|
452
|
+
}
|
|
797
453
|
/**
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
454
|
+
* The side column beside an entry: on a case study, the sectors, other case
|
|
455
|
+
* studies and one call to action; on a solution, the other solutions and an
|
|
456
|
+
* engineer to call, with related case studies under the text; on an article,
|
|
457
|
+
* recent articles; on a job, the other open roles. Never on a page, which
|
|
458
|
+
* already carries its own lists and calls to action.
|
|
801
459
|
*/
|
|
460
|
+
export function vitrineWidgets(copy) {
|
|
461
|
+
const w = copy.widgets;
|
|
462
|
+
const caseStudy = { kind: 'collection', collection: 'case_study' };
|
|
463
|
+
const solution = { kind: 'collection', collection: 'solution' };
|
|
464
|
+
const post = { kind: 'collection', collection: 'post' };
|
|
465
|
+
const job = { kind: 'collection', collection: 'job' };
|
|
466
|
+
const sector = { kind: 'taxonomy', taxonomy: copy.schema.taxonomy.name };
|
|
467
|
+
const search = { kind: 'search' };
|
|
468
|
+
const contactHref = `/${copy.pageSlugs.contact}`;
|
|
469
|
+
return [
|
|
470
|
+
{
|
|
471
|
+
area: 'sidebar',
|
|
472
|
+
type: 'search',
|
|
473
|
+
title: w.searchTitle,
|
|
474
|
+
settings: { placeholder: w.searchPlaceholder },
|
|
475
|
+
visibility: onlyOn(sector),
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
area: 'sidebar',
|
|
479
|
+
type: 'terms',
|
|
480
|
+
title: w.sectors,
|
|
481
|
+
settings: {
|
|
482
|
+
taxonomy: copy.schema.taxonomy.name,
|
|
483
|
+
showCounts: true,
|
|
484
|
+
hierarchical: false,
|
|
485
|
+
hideEmpty: true,
|
|
486
|
+
},
|
|
487
|
+
visibility: onlyOn(caseStudy, sector, search),
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
area: 'sidebar',
|
|
491
|
+
type: 'recentEntries',
|
|
492
|
+
title: w.moreCaseStudies,
|
|
493
|
+
settings: { collection: 'case_study', count: 3, showDate: false },
|
|
494
|
+
visibility: onlyOn(caseStudy),
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
area: 'sidebar',
|
|
498
|
+
type: 'recentEntries',
|
|
499
|
+
title: w.otherSolutions,
|
|
500
|
+
settings: { collection: 'solution', count: 6, showDate: false },
|
|
501
|
+
visibility: onlyOn(solution),
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
area: 'sidebar',
|
|
505
|
+
type: 'contact',
|
|
506
|
+
title: w.contactTitle,
|
|
507
|
+
settings: {
|
|
508
|
+
address: '18 rue des Ateliers, 75011 Paris',
|
|
509
|
+
phone: '+33 1 99 00 42 17',
|
|
510
|
+
email: 'contact@example.com',
|
|
511
|
+
hours: [{ label: w.contactHours, value: w.contactHoursValue }],
|
|
512
|
+
},
|
|
513
|
+
visibility: onlyOn(solution),
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
area: 'sidebar',
|
|
517
|
+
type: 'recentEntries',
|
|
518
|
+
title: w.recentPosts,
|
|
519
|
+
settings: { collection: 'post', count: 4, showDate: true },
|
|
520
|
+
visibility: onlyOn(post),
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
area: 'sidebar',
|
|
524
|
+
type: 'recentEntries',
|
|
525
|
+
title: w.openRoles,
|
|
526
|
+
settings: { collection: 'job', count: 6, showDate: false },
|
|
527
|
+
visibility: onlyOn(job),
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
area: 'sidebar',
|
|
531
|
+
type: 'cta',
|
|
532
|
+
settings: { heading: w.ctaHeading, body: w.ctaBody, label: w.ctaLabel, href: contactHref },
|
|
533
|
+
visibility: onlyOn(caseStudy, sector, search, post),
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
area: 'content-after',
|
|
537
|
+
type: 'recentEntries',
|
|
538
|
+
title: w.relatedWork,
|
|
539
|
+
settings: { collection: 'case_study', count: 3, showDate: false, showImage: true },
|
|
540
|
+
visibility: onlyOn(solution),
|
|
541
|
+
},
|
|
542
|
+
];
|
|
543
|
+
}
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// Media
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
802
547
|
function vitrinePalette() {
|
|
803
548
|
const skin = STARTING_SKINS.vitrine;
|
|
804
549
|
if (skin === undefined) {
|
|
@@ -811,246 +556,169 @@ function vitrinePalette() {
|
|
|
811
556
|
return skin.color;
|
|
812
557
|
}
|
|
813
558
|
/**
|
|
814
|
-
*
|
|
815
|
-
* `
|
|
816
|
-
*
|
|
817
|
-
* wordmarks drawn once with OFL typefaces. The procedural `spec` is only the
|
|
818
|
-
* fallback `seedDemoMedia` uses if a file is ever missing.
|
|
559
|
+
* Every slot names a bundled file under `assets/photos/vitrine/`. The
|
|
560
|
+
* procedural `spec` is only the fallback `seedDemoMedia` uses if a file is
|
|
561
|
+
* ever missing.
|
|
819
562
|
*/
|
|
820
|
-
export
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
alt: LOGO_NAMES[key],
|
|
837
|
-
photo: `vitrine/${key}.png`,
|
|
838
|
-
})),
|
|
839
|
-
...VITRINE_DEMO_TESTIMONIALS.map((demo, index) => ({
|
|
840
|
-
name: `avatar-${index}`,
|
|
841
|
-
spec: avatarArt(vitrinePalette(), 80 + index),
|
|
842
|
-
alt: `Portrait of ${demo.authorName}`,
|
|
843
|
-
photo: `vitrine/avatar-${index + 1}.jpg`,
|
|
844
|
-
})),
|
|
845
|
-
{
|
|
846
|
-
name: 'case-meridian-rail-punctuality',
|
|
847
|
-
spec: coverArt(vitrinePalette(), 90),
|
|
848
|
-
alt: 'Passengers walking along a platform beside a commuter train',
|
|
849
|
-
photo: 'vitrine/case-rail.jpg',
|
|
850
|
-
},
|
|
851
|
-
{
|
|
852
|
-
name: 'case-castell-vane-pricing',
|
|
853
|
-
spec: coverArt(vitrinePalette(), 91),
|
|
854
|
-
alt: 'The dining room of a hotel at dusk, tables laid under a skylight',
|
|
855
|
-
photo: 'vitrine/case-hotels.jpg',
|
|
856
|
-
},
|
|
857
|
-
{
|
|
858
|
-
name: 'case-kellmoor-council-budget',
|
|
859
|
-
spec: coverArt(vitrinePalette(), 92),
|
|
860
|
-
alt: 'The colonnaded front of a city hall in afternoon sun',
|
|
861
|
-
photo: 'vitrine/case-council.jpg',
|
|
862
|
-
},
|
|
863
|
-
];
|
|
864
|
-
/** Header/footer navigation and the header call to action (L25, D4). */
|
|
865
|
-
export const VITRINE_MENUS = {
|
|
866
|
-
header: [
|
|
867
|
-
{ label: 'Practices', url: '/practices' },
|
|
868
|
-
{ label: 'Case studies', url: '/case-studies' },
|
|
869
|
-
{ label: 'About', url: '/about' },
|
|
870
|
-
{ label: 'Contact', url: '/contact' },
|
|
871
|
-
],
|
|
872
|
-
footer: [
|
|
873
|
-
{ label: 'Practices', url: '/practices' },
|
|
874
|
-
{ label: 'Case studies', url: '/case-studies' },
|
|
875
|
-
{ label: 'About', url: '/about' },
|
|
876
|
-
{ label: 'Contact', url: '/contact' },
|
|
877
|
-
],
|
|
878
|
-
headerAction: { label: 'Discuss a mandate', url: '/contact' },
|
|
879
|
-
};
|
|
880
|
-
export const VITRINE_SITE_SETTINGS = {
|
|
881
|
-
'general.tagline': 'Management consultancy for mid-sized companies, their owners and their boards.',
|
|
882
|
-
'general.socialLinks': [
|
|
883
|
-
{ label: 'LinkedIn', url: 'https://linkedin.com/company/example' },
|
|
884
|
-
{ label: 'X', url: 'https://x.com/example' },
|
|
885
|
-
{ label: 'YouTube', url: 'https://youtube.com/@example' },
|
|
886
|
-
],
|
|
887
|
-
'general.footerNote': [
|
|
888
|
-
'London\n12 Hanover Square\nLondon W1S 1JB',
|
|
889
|
-
'New York\n230 Park Avenue, Floor 10\nNew York, NY 10169',
|
|
890
|
-
'Singapore\n8 Marina View, #32-01\nSingapore 018960',
|
|
891
|
-
].join('\n\n'),
|
|
892
|
-
};
|
|
893
|
-
/**
|
|
894
|
-
* The side column a consultancy's reader expects beside the work: on a case
|
|
895
|
-
* study, the sectors with their counts, the other published engagements and
|
|
896
|
-
* the one call to discuss a mandate; on a practice, the other practices and
|
|
897
|
-
* the partners' switchboard, with selected work under the text; on a sector
|
|
898
|
-
* archive and on search results, the sectors and the same call, plus a
|
|
899
|
-
* search box on the archive (a results page already opens on its own). Never
|
|
900
|
-
* on the home page or on the Practices, Case studies, About and Contact
|
|
901
|
-
* pages, which already carry their own lists and calls to action.
|
|
902
|
-
*/
|
|
903
|
-
const VITRINE_CASE_STUDY = { kind: 'collection', collection: 'case_study' };
|
|
904
|
-
const VITRINE_SERVICE = { kind: 'collection', collection: 'service' };
|
|
905
|
-
const VITRINE_SECTOR = { kind: 'taxonomy', taxonomy: 'sector' };
|
|
906
|
-
const VITRINE_SEARCH = { kind: 'search' };
|
|
907
|
-
function onlyOn(...targets) {
|
|
908
|
-
return { pages: { mode: 'only', targets } };
|
|
563
|
+
export function vitrineMediaSpecs(copy) {
|
|
564
|
+
const palette = vitrinePalette();
|
|
565
|
+
return [
|
|
566
|
+
...copy.media.map((item, index) => ({
|
|
567
|
+
name: item.name,
|
|
568
|
+
spec: item.name === 'hero' ? heroArt(palette, 'sun', 61) : coverArt(palette, 90 + index),
|
|
569
|
+
alt: item.alt,
|
|
570
|
+
photo: `vitrine/${item.file}`,
|
|
571
|
+
})),
|
|
572
|
+
...copy.clients.map((client, index) => ({
|
|
573
|
+
name: client.key,
|
|
574
|
+
spec: logoArt(70 + index),
|
|
575
|
+
alt: client.name,
|
|
576
|
+
photo: `vitrine/${client.key}.png`,
|
|
577
|
+
})),
|
|
578
|
+
];
|
|
909
579
|
}
|
|
910
|
-
export const
|
|
911
|
-
{
|
|
912
|
-
area: 'sidebar',
|
|
913
|
-
type: 'search',
|
|
914
|
-
title: 'Search the site',
|
|
915
|
-
settings: { placeholder: 'A client or a sector' },
|
|
916
|
-
visibility: onlyOn(VITRINE_SECTOR),
|
|
917
|
-
},
|
|
918
|
-
{
|
|
919
|
-
area: 'sidebar',
|
|
920
|
-
type: 'terms',
|
|
921
|
-
title: 'Sectors',
|
|
922
|
-
settings: { taxonomy: 'sector', showCounts: true, hierarchical: false, hideEmpty: true },
|
|
923
|
-
visibility: onlyOn(VITRINE_CASE_STUDY, VITRINE_SECTOR, VITRINE_SEARCH),
|
|
924
|
-
},
|
|
925
|
-
{
|
|
926
|
-
area: 'sidebar',
|
|
927
|
-
type: 'recentEntries',
|
|
928
|
-
title: 'More case studies',
|
|
929
|
-
settings: { collection: 'case_study', count: 3, showDate: false },
|
|
930
|
-
visibility: onlyOn(VITRINE_CASE_STUDY),
|
|
931
|
-
},
|
|
932
|
-
{
|
|
933
|
-
area: 'sidebar',
|
|
934
|
-
type: 'recentEntries',
|
|
935
|
-
title: 'Other practices',
|
|
936
|
-
settings: { collection: 'service', count: 6, showDate: false },
|
|
937
|
-
visibility: onlyOn(VITRINE_SERVICE),
|
|
938
|
-
},
|
|
939
|
-
{
|
|
940
|
-
area: 'sidebar',
|
|
941
|
-
type: 'contact',
|
|
942
|
-
title: 'Speak to a partner',
|
|
943
|
-
settings: {
|
|
944
|
-
address: '12 Hanover Square, London W1S 1JB',
|
|
945
|
-
phone: '+44 20 7946 0321',
|
|
946
|
-
email: 'hello@example.com',
|
|
947
|
-
hours: [{ label: 'Monday to Friday', value: '8:30 to 18:30' }],
|
|
948
|
-
},
|
|
949
|
-
visibility: onlyOn(VITRINE_SERVICE),
|
|
950
|
-
},
|
|
580
|
+
export const VITRINE_RECOMMENDED_AGENTS = [
|
|
951
581
|
{
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
heading: 'Facing a similar decision?',
|
|
956
|
-
body: 'Describe it in a few lines. A partner will reply within two working days, at no charge.',
|
|
957
|
-
label: 'Discuss a mandate',
|
|
958
|
-
href: '/contact',
|
|
959
|
-
},
|
|
960
|
-
visibility: onlyOn(VITRINE_CASE_STUDY, VITRINE_SECTOR, VITRINE_SEARCH),
|
|
582
|
+
name: 'seoAgent',
|
|
583
|
+
package: '@cogenta/agents-builtin',
|
|
584
|
+
reason: 'Audits solution, case study and article pages for on-page SEO issues before they go live.',
|
|
961
585
|
},
|
|
962
586
|
{
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
settings: { collection: 'case_study', count: 3, showDate: false, showImage: true },
|
|
967
|
-
visibility: onlyOn(VITRINE_SERVICE),
|
|
587
|
+
name: 'performanceAgent',
|
|
588
|
+
package: '@cogenta/agents-builtin',
|
|
589
|
+
reason: 'Catches oversized images and third-party scripts that would slow the site down.',
|
|
968
590
|
},
|
|
969
591
|
];
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
// Seeding
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
970
595
|
/**
|
|
971
596
|
* Inserts the demo content through the real `ContentStore` and taxonomy store
|
|
972
|
-
* (never mocked, house rule)
|
|
973
|
-
*
|
|
974
|
-
* since a theme lists only published entries.
|
|
597
|
+
* (never mocked, house rule): sectors first, then every collection, then the
|
|
598
|
+
* pages. Everything is published, since a theme lists only published entries.
|
|
975
599
|
*/
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
600
|
+
function seedVitrineDemoContent(copy) {
|
|
601
|
+
return async (ctx) => {
|
|
602
|
+
const { db, defaultLocale, adminId, media } = ctx;
|
|
603
|
+
const firm = ctx.siteName?.trim() || DEFAULT_FIRM_NAME;
|
|
604
|
+
const model = vitrineSchema(copy);
|
|
605
|
+
const store = (collection) => createContentStore({ db, collection, defaultLocale });
|
|
606
|
+
const sectorStore = createTaxonomyStore({ db, taxonomy: model.sector });
|
|
607
|
+
const sectorIdBySlug = new Map();
|
|
608
|
+
for (const sector of copy.sectors) {
|
|
609
|
+
const term = await sectorStore.create({
|
|
610
|
+
slug: sector.slug,
|
|
611
|
+
labels: { [defaultLocale]: sector.name },
|
|
612
|
+
});
|
|
613
|
+
sectorIdBySlug.set(sector.slug, term.id);
|
|
614
|
+
}
|
|
615
|
+
const cover = (name) => (media[name] === undefined ? {} : { coverImage: media[name] });
|
|
616
|
+
const published = { status: 'published', createdBy: adminId };
|
|
617
|
+
const solutions = store(model.solution);
|
|
618
|
+
for (const item of copy.solutions) {
|
|
619
|
+
await solutions.create({
|
|
620
|
+
...published,
|
|
621
|
+
values: {
|
|
622
|
+
name: item.name,
|
|
623
|
+
slug: item.slug,
|
|
624
|
+
description: item.description,
|
|
625
|
+
body: richText(`solution-${item.slug}`, item.body(firm)),
|
|
626
|
+
icon: item.icon,
|
|
627
|
+
...cover(item.media),
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
const caseStudies = store(model.caseStudy);
|
|
632
|
+
for (const item of copy.caseStudies) {
|
|
633
|
+
await caseStudies.create({
|
|
634
|
+
...published,
|
|
635
|
+
values: {
|
|
636
|
+
title: item.title,
|
|
637
|
+
slug: item.slug,
|
|
638
|
+
client: item.client,
|
|
639
|
+
location: item.location,
|
|
640
|
+
keyFigure: item.keyFigure,
|
|
641
|
+
keyFigureLabel: item.keyFigureLabel,
|
|
642
|
+
summary: item.summary,
|
|
643
|
+
body: richText(`case-${item.slug}`, item.body(firm)),
|
|
644
|
+
sector: sectorIdBySlug.get(item.sector) ?? null,
|
|
645
|
+
...cover(item.media),
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
const testimonials = store(model.testimonial);
|
|
650
|
+
for (const item of copy.testimonials) {
|
|
651
|
+
await testimonials.create({
|
|
652
|
+
...published,
|
|
653
|
+
values: {
|
|
654
|
+
authorName: item.authorName,
|
|
655
|
+
authorRole: item.authorRole,
|
|
656
|
+
quote: item.quote(firm),
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const jobs = store(model.job);
|
|
661
|
+
for (const item of copy.jobs) {
|
|
662
|
+
await jobs.create({
|
|
663
|
+
...published,
|
|
664
|
+
values: {
|
|
665
|
+
title: item.title,
|
|
666
|
+
slug: item.slug,
|
|
667
|
+
team: item.team,
|
|
668
|
+
location: item.location,
|
|
669
|
+
contract: item.contract,
|
|
670
|
+
summary: item.summary,
|
|
671
|
+
body: richText(`job-${item.slug}`, item.body(firm)),
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
// Oldest first, so a list sorted by creation date newest first reads in
|
|
676
|
+
// publication order.
|
|
677
|
+
const posts = store(model.post);
|
|
678
|
+
for (const item of [...copy.posts].sort((a, b) => a.publishedAt.localeCompare(b.publishedAt))) {
|
|
679
|
+
await posts.create({
|
|
680
|
+
...published,
|
|
681
|
+
values: {
|
|
682
|
+
title: item.title,
|
|
683
|
+
slug: item.slug,
|
|
684
|
+
summary: item.summary,
|
|
685
|
+
author: item.author,
|
|
686
|
+
publishedAt: item.publishedAt,
|
|
687
|
+
body: richText(`post-${item.slug}`, item.body(firm)),
|
|
688
|
+
sector: sectorIdBySlug.get(item.sector) ?? null,
|
|
689
|
+
...cover(item.media),
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const pages = store(model.page);
|
|
694
|
+
for (const demo of buildVitrineDemoPages(copy, { media, firm })) {
|
|
695
|
+
await pages.create({
|
|
696
|
+
...published,
|
|
697
|
+
values: { title: demo.title, slug: demo.slug },
|
|
698
|
+
blocks: { blocks: demo.blocks.map(toBlockZoneEntry) },
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
};
|
|
1044
702
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
703
|
+
/** The whole blueprint, in one language. */
|
|
704
|
+
export function createVitrineContentPack(locale) {
|
|
705
|
+
const copy = vitrineCopyFor(locale);
|
|
706
|
+
const model = vitrineSchema(copy);
|
|
707
|
+
return {
|
|
708
|
+
collections: model.collections,
|
|
709
|
+
taxonomies: model.taxonomies,
|
|
710
|
+
recommendedAgents: VITRINE_RECOMMENDED_AGENTS,
|
|
711
|
+
seedDemoContent: seedVitrineDemoContent(copy),
|
|
712
|
+
defaultTheme: '@cogenta/theme-entreprise',
|
|
713
|
+
menus: vitrineMenus(copy),
|
|
714
|
+
widgets: vitrineWidgets(copy),
|
|
715
|
+
siteSettings: vitrineSiteSettings(copy),
|
|
716
|
+
mediaSpecs: vitrineMediaSpecs(copy),
|
|
717
|
+
// A company site: no comment thread under a solution, a case study, a
|
|
718
|
+
// job opening or a press article.
|
|
719
|
+
commentsDisabledOn: ['solution', 'case_study', 'job', 'post'],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
/** The English pack, which `BLUEPRINT_CONTENT_PACKS` lists; `contentPackFor` picks by locale. */
|
|
723
|
+
export const vitrineContentPack = createVitrineContentPack('en');
|
|
1056
724
|
//# sourceMappingURL=vitrine.js.map
|