@unitninety/site-kit 0.0.0-stage → 1.6.2
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/README.md +30 -2
- package/package.json +79 -4
- package/src/bridge/DraftBlocks.tsx +70 -0
- package/src/bridge/DraftEntries.tsx +41 -0
- package/src/bridge/PreviewCommitBoundary.tsx +19 -0
- package/src/bridge/U90Bridge.tsx +54 -0
- package/src/bridge/caret.ts +317 -0
- package/src/bridge/client.tsx +66 -0
- package/src/bridge/createBridge.ts +1015 -0
- package/src/bridge/hooks.tsx +56 -0
- package/src/bridge/index.ts +9 -0
- package/src/bridge/lexical.ts +177 -0
- package/src/bridge/lexicalDom.ts +212 -0
- package/src/bridge/preserveBlockMoves.ts +88 -0
- package/src/bridge/preview.ts +179 -0
- package/src/bridge/protocol.ts +273 -0
- package/src/bridge/registry.ts +30 -0
- package/src/bridge/styles.ts +137 -0
- package/src/components/DateText.tsx +9 -0
- package/src/components/Entries.tsx +16 -0
- package/src/components/File.tsx +9 -0
- package/src/components/Image.tsx +30 -0
- package/src/components/Link.tsx +24 -0
- package/src/components/NumberText.tsx +9 -0
- package/src/components/RichText.tsx +47 -0
- package/src/components/Text.tsx +15 -0
- package/src/components/Video.tsx +12 -0
- package/src/components/VideoPlayback.tsx +19 -0
- package/src/components/draft.ts +9 -0
- package/src/components/imageProps.ts +26 -0
- package/src/components/index.ts +13 -0
- package/src/components/videoProps.ts +6 -0
- package/src/elements/containers/group.ts +33 -0
- package/src/elements/containers/list.ts +31 -0
- package/src/elements/containers/zone.ts +41 -0
- package/src/elements/declaration.ts +118 -0
- package/src/elements/define.ts +195 -0
- package/src/elements/ids.ts +50 -0
- package/src/elements/index.ts +35 -0
- package/src/elements/jsonSchema.ts +36 -0
- package/src/elements/leaf.ts +98 -0
- package/src/elements/leaves/boolean.ts +30 -0
- package/src/elements/leaves/choice.ts +53 -0
- package/src/elements/leaves/date.ts +39 -0
- package/src/elements/leaves/file.ts +56 -0
- package/src/elements/leaves/image.ts +160 -0
- package/src/elements/leaves/link.ts +149 -0
- package/src/elements/leaves/number.ts +52 -0
- package/src/elements/leaves/query.ts +101 -0
- package/src/elements/leaves/relation.ts +69 -0
- package/src/elements/leaves/rich-text.ts +155 -0
- package/src/elements/leaves/selection.ts +40 -0
- package/src/elements/leaves/single.ts +29 -0
- package/src/elements/leaves/text.ts +103 -0
- package/src/elements/leaves/video.ts +108 -0
- package/src/elements/ops/expand.ts +94 -0
- package/src/elements/ops/overlay.ts +59 -0
- package/src/elements/ops/refs.ts +45 -0
- package/src/elements/ops/seed.ts +185 -0
- package/src/elements/ops/text.ts +20 -0
- package/src/elements/ops/translatable.ts +19 -0
- package/src/elements/ops/upgrade.ts +43 -0
- package/src/elements/ops/validate.ts +56 -0
- package/src/elements/order.ts +10 -0
- package/src/elements/path.ts +43 -0
- package/src/elements/prefix.ts +2 -0
- package/src/elements/registry.ts +48 -0
- package/src/elements/tokens.ts +14 -0
- package/src/elements/walk.ts +42 -0
- package/src/features/client/AskAiProvider.tsx +551 -0
- package/src/features/client/AskAiWidget.tsx +222 -0
- package/src/features/client/ConsentRuntime.tsx +95 -0
- package/src/features/client/FeedbackProvider.tsx +231 -0
- package/src/features/client/FeedbackWidget.tsx +101 -0
- package/src/features/client/PageReporter.tsx +26 -0
- package/src/features/client/README.md +131 -0
- package/src/features/client/SiteFeatures.tsx +250 -0
- package/src/features/client/askAiRecovery.ts +92 -0
- package/src/features/client/askAiStream.ts +59 -0
- package/src/features/client/context.tsx +24 -0
- package/src/features/client/index.ts +24 -0
- package/src/features/client/overlay.tsx +129 -0
- package/src/features/client/styles.css +337 -0
- package/src/features/client/transport.ts +310 -0
- package/src/features/client/useAskAI.ts +45 -0
- package/src/features/client/useConsent.ts +15 -0
- package/src/features/client/useFeedback.ts +33 -0
- package/src/features/client/useSiteSearch.ts +50 -0
- package/src/features/client/verification.ts +141 -0
- package/src/features/consent/README.md +70 -0
- package/src/features/consent/cookieScript.ts +53 -0
- package/src/features/consent/custom.ts +89 -0
- package/src/features/consent/defaults.ts +40 -0
- package/src/features/consent/drivers.ts +233 -0
- package/src/features/consent/loader.ts +248 -0
- package/src/features/consent/registry.ts +398 -0
- package/src/features/contracts/askAi.ts +166 -0
- package/src/features/contracts/askAiSettings.ts +42 -0
- package/src/features/contracts/base.ts +107 -0
- package/src/features/contracts/consent.ts +150 -0
- package/src/features/contracts/feedback.ts +22 -0
- package/src/features/contracts/feedbackSettings.ts +49 -0
- package/src/features/contracts/index.ts +61 -0
- package/src/features/contracts/knowledge.ts +99 -0
- package/src/features/contracts/knowledgePolicy.ts +249 -0
- package/src/features/contracts/search.ts +24 -0
- package/src/features/contracts/verification.ts +52 -0
- package/src/features/contracts/verificationConfig.ts +9 -0
- package/src/features/server/ConsentDefaults.tsx +15 -0
- package/src/features/server/README.md +185 -0
- package/src/features/server/gateway.ts +318 -0
- package/src/features/server/http.ts +179 -0
- package/src/features/server/index.ts +6 -0
- package/src/features/server/session.ts +23 -0
- package/src/features/server/verification.ts +39 -0
- package/src/features/server/verifyVisitor.ts +69 -0
- package/src/languages/config.d.mts +5 -0
- package/src/languages/config.mjs +53 -0
- package/src/languages/decide.ts +72 -0
- package/src/languages/index.ts +4 -0
- package/src/languages/proxy.ts +108 -0
- package/src/languages/routing.ts +93 -0
- package/src/languages/wire.ts +19 -0
- package/src/legacy/bridge.tsx +1987 -0
- package/src/legacy/caret.ts +246 -0
- package/src/legacy/cli.mjs +189 -0
- package/src/legacy/content.ts +993 -0
- package/src/legacy/contract.ts +664 -0
- package/src/legacy/defineBlocks.ts +1602 -0
- package/src/legacy/defineSite.ts +623 -0
- package/src/legacy/defineSite.types.ts +270 -0
- package/src/legacy/draft.tsx +128 -0
- package/src/legacy/image.ts +433 -0
- package/src/legacy/index.ts +209 -0
- package/src/legacy/language-switcher.tsx +58 -0
- package/src/legacy/languageHints.ts +153 -0
- package/src/legacy/languages/fields.ts +131 -0
- package/src/legacy/languages/hash.ts +98 -0
- package/src/legacy/languages/health.ts +97 -0
- package/src/legacy/languages/identity.ts +247 -0
- package/src/legacy/languages/links.ts +81 -0
- package/src/legacy/languages/resolution.ts +56 -0
- package/src/legacy/languages/routing.ts +169 -0
- package/src/legacy/languages/runtime.ts +216 -0
- package/src/legacy/languages/seo.ts +95 -0
- package/src/legacy/languages/settings.ts +211 -0
- package/src/legacy/languages/types.ts +85 -0
- package/src/legacy/languages.ts +8 -0
- package/src/legacy/lexical.ts +170 -0
- package/src/legacy/lexicalDom.ts +166 -0
- package/src/legacy/media.ts +272 -0
- package/src/legacy/nav.ts +176 -0
- package/src/legacy/preview.ts +79 -0
- package/src/legacy/redirects.ts +474 -0
- package/src/legacy/richText.tsx +104 -0
- package/src/legacy/routes.ts +414 -0
- package/src/legacy/seo.ts +327 -0
- package/src/legacy/shape.ts +412 -0
- package/src/legacy/uploads.ts +233 -0
- package/src/legacy/verification.ts +42 -0
- package/src/preview-lock/contracts.ts +42 -0
- package/src/preview-lock/index.ts +3 -0
- package/src/preview-lock/screen.tsx +244 -0
- package/src/preview-lock/server.ts +416 -0
- package/src/redirects/config.d.mts +5 -0
- package/src/redirects/config.mjs +22 -0
- package/src/redirects/index.ts +3 -0
- package/src/redirects/location.ts +31 -0
- package/src/redirects/match.ts +199 -0
- package/src/redirects/proxy.ts +91 -0
- package/src/redirects/wire.ts +30 -0
- package/src/seo/jsonLd.ts +70 -0
- package/src/seo/metadata.ts +127 -0
- package/src/seo/robots.ts +14 -0
- package/src/seo/sitemap.ts +22 -0
- package/src/seo/title.ts +22 -0
- package/src/server/adapters.ts +73 -0
- package/src/server/createU90.ts +96 -0
- package/src/server/handlers.ts +164 -0
- package/src/server/index.ts +6 -0
- package/src/server/read.ts +207 -0
- package/src/server/render.tsx +46 -0
- package/src/server/routes.tsx +171 -0
- package/src/site/assemble.ts +109 -0
- package/src/site/defineSite.ts +132 -0
- package/src/site/documents.ts +18 -0
- package/src/site/fields.ts +59 -0
- package/src/site/index.ts +14 -0
- package/src/site/replies.ts +118 -0
- package/src/site/store.ts +45 -0
- package/src/site/types.ts +75 -0
- package/src/site/version.ts +1 -0
- package/src/site/wire.ts +125 -0
- package/src/switcher/LanguageSwitcher.tsx +42 -0
- package/src/test-preview/README.md +90 -0
- package/src/test-preview/binding.ts +54 -0
- package/src/test-preview/bridge.ts +16 -0
- package/src/test-preview/guard.ts +186 -0
- package/src/test-preview/media.ts +23 -0
- package/src/test-preview/protocol.d.mts +67 -0
- package/src/test-preview/protocol.mjs +222 -0
- package/src/test-preview/runtime.ts +168 -0
- package/src/test-preview/server.ts +10 -0
- package/src/testing/fake.ts +148 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/seed.ts +2 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between a UnitNinety site and the platform.
|
|
3
|
+
*
|
|
4
|
+
* These types are **canonical here**, and the platform imports them from this
|
|
5
|
+
* package rather than the other way round. That direction is the whole point: a site
|
|
6
|
+
* repo declaring its own content schema must not depend on platform internals, or
|
|
7
|
+
* every site is coupled to the dashboard's file layout.
|
|
8
|
+
*
|
|
9
|
+
* Changing anything in this file changes what every site repo has to emit, so it is a
|
|
10
|
+
* breaking change to the contract even when TypeScript doesn't say so. The platform
|
|
11
|
+
* re-validates everything at runtime (`src/manifest/validate.ts`), which is what
|
|
12
|
+
* actually enforces it — these types only make it pleasant to get right.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
declare const VALUE: unique symbol
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A field spec, carrying the type of the value it committed.
|
|
19
|
+
*
|
|
20
|
+
* Phantom — `VALUE` is never assigned and the property never exists at runtime. It is here
|
|
21
|
+
* so `defineSite` can infer `site.values` from the helpers a site wrote, which is what
|
|
22
|
+
* retires the hand-written `Brand` type.
|
|
23
|
+
*
|
|
24
|
+
* It lives in this file because `defineBlocks.ts` and `defineSite.ts` both need it and both
|
|
25
|
+
* already import from here; declaring it in either of them would make the two import each
|
|
26
|
+
* other.
|
|
27
|
+
*/
|
|
28
|
+
export type Spec<V> = { [VALUE]?: V }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The phantom key itself, so a reader can ask whether a type carries a value *at all*.
|
|
32
|
+
*
|
|
33
|
+
* `Spec`'s property is optional, which makes it a *weak type*: an object sharing none of its
|
|
34
|
+
* properties is refused, so `logo: { ratio: 241 / 105 }` does not match `Spec<infer V>` and
|
|
35
|
+
* needs no help from here.
|
|
36
|
+
*
|
|
37
|
+
* **An object with an index signature does.** Weak-type detection is relaxed for one, and a
|
|
38
|
+
* string index signature says nothing about a symbol key, so `Record<string, number>` matches
|
|
39
|
+
* `Spec<infer V>` with `V` inferred as `unknown` — a bare entry read from a lookup table
|
|
40
|
+
* would arrive in `site.values` as `unknown`, which is the widening a hand-written `Brand`
|
|
41
|
+
* type existed to avoid. `SpecKey extends keyof S` is the test that tells "carries nothing"
|
|
42
|
+
* apart from "carries `unknown`", and that case is the one that needs it.
|
|
43
|
+
*/
|
|
44
|
+
export type SpecKey = typeof VALUE
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The runtime tag that tells a `block()` entry from any other value.
|
|
48
|
+
*
|
|
49
|
+
* Here for the same reason `Spec` is: `defineSite` stamps it and `defineBlocks` reads it —
|
|
50
|
+
* `blocksField()` takes the blocks of a dynamic zone, and those are written with `block()` and
|
|
51
|
+
* held in a plain const, so it must be able to recognise one. Declared in either module, the
|
|
52
|
+
* two would have to import each other.
|
|
53
|
+
*
|
|
54
|
+
* A symbol rather than a property name, so a site's own data cannot be mistaken for an entry
|
|
55
|
+
* and `JSON.stringify` never carries it anywhere. `Symbol.for` rather than `Symbol()`, so two
|
|
56
|
+
* instances of this package in one bundle — a vendored tarball beside a workspace link — still
|
|
57
|
+
* recognise each other's entries; the registry is keyed by this string.
|
|
58
|
+
*/
|
|
59
|
+
export const BLOCK_ENTRY: unique symbol = Symbol.for('u90.block')
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A page section as a site writes it, before `defineSite` knows what it is called.
|
|
63
|
+
*
|
|
64
|
+
* The slug is optional because the document key normally supplies it — `hero: block({…})` is
|
|
65
|
+
* stored as `hero`. A zone's blocks have no document key, so those name their own; see
|
|
66
|
+
* `block()` for when the two must diverge.
|
|
67
|
+
*
|
|
68
|
+
* `F` is left unconstrained here, because `FieldSpec` is declared in `defineBlocks.ts` and
|
|
69
|
+
* this file is upstream of both modules. Every caller restates the constraint it needs.
|
|
70
|
+
*/
|
|
71
|
+
export type BlockEntry<F = Record<string, unknown>> = {
|
|
72
|
+
[BLOCK_ENTRY]: true
|
|
73
|
+
fields: F
|
|
74
|
+
label?: string
|
|
75
|
+
/** This block belongs to a layout, not to a page. See `EditorBlock.layoutOnly`. */
|
|
76
|
+
layoutOnly?: boolean
|
|
77
|
+
/** Collections this block draws, by slug. See `EditorBlock.renders`. */
|
|
78
|
+
renders?: string[]
|
|
79
|
+
seed: boolean
|
|
80
|
+
/** Stored slug, when it must differ from the document key. See `block()`. */
|
|
81
|
+
slug?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Field types the dashboard's editors can render.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately a closed set. A site declaring something outside it gets its whole
|
|
88
|
+
* manifest rejected rather than a field that silently can't be edited — the platform
|
|
89
|
+
* would otherwise fall through to a `default` branch, render nothing, and leave a
|
|
90
|
+
* client unable to change something they can see on their own page.
|
|
91
|
+
*/
|
|
92
|
+
export type EditorFieldType =
|
|
93
|
+
| 'array'
|
|
94
|
+
| 'blocks'
|
|
95
|
+
| 'checkbox'
|
|
96
|
+
/**
|
|
97
|
+
* A script, an embed snippet, or anything else that is code rather than prose.
|
|
98
|
+
*
|
|
99
|
+
* Stored as a string exactly like `textarea`, and separate from it for the editor's sake
|
|
100
|
+
* alone: a code field gets a monospace control with spellcheck, autocorrect and
|
|
101
|
+
* autocapitalise off, and the rail badges it so a client can tell the one field that
|
|
102
|
+
* configures a third-party widget from the fields that hold their own words.
|
|
103
|
+
*
|
|
104
|
+
* **Badged `E`, for embed, and not `C`.** `C` is the collection badge, and this file's
|
|
105
|
+
* neighbour in the rail already carries the scar from reusing a letter: `B` for a button
|
|
106
|
+
* exists because a button once shared `L` with a list, which made the two rows a client is
|
|
107
|
+
* most likely to be hunting for the only two that looked alike.
|
|
108
|
+
*
|
|
109
|
+
* It changes nothing about rendering. A site decides what to do with the string, the same
|
|
110
|
+
* as it always did — `pbir` passes its reviews script straight to `next/script`.
|
|
111
|
+
*/
|
|
112
|
+
| 'code'
|
|
113
|
+
/**
|
|
114
|
+
* A calendar day, stored as `YYYY-MM-DD`.
|
|
115
|
+
*
|
|
116
|
+
* Date-only rather than a timestamp, and that is a decision rather than a simplification:
|
|
117
|
+
* the field exists for a post's published date, which has no timezone, and date-only ISO
|
|
118
|
+
* sorts lexicographically so a site can order by it without parsing. Adding a time
|
|
119
|
+
* component later is additive; taking one away is not.
|
|
120
|
+
*/
|
|
121
|
+
| 'date'
|
|
122
|
+
| 'email'
|
|
123
|
+
| 'group'
|
|
124
|
+
| 'number'
|
|
125
|
+
| 'relationship'
|
|
126
|
+
/**
|
|
127
|
+
* A field whose value lives once, in the client's Singles, and is pointed at from here
|
|
128
|
+
* rather than stored on this block. See `reference()` in `defineBlocks.ts`.
|
|
129
|
+
*
|
|
130
|
+
* Not folded into `relationship`: a relationship's `relationTo` names a *collection*, and
|
|
131
|
+
* the dashboard reads it to load picker options. A reference names a **row key inside one
|
|
132
|
+
* reserved collection**, `_singles`, and there is nothing to pick — the client keeps one
|
|
133
|
+
* value per key, not a list of rows to choose among. Sharing the type would put `_singles`
|
|
134
|
+
* into the relationship machinery, offering every client's Single as a relationship
|
|
135
|
+
* target and storing an id where a key belongs.
|
|
136
|
+
*/
|
|
137
|
+
| 'reference'
|
|
138
|
+
| 'richText'
|
|
139
|
+
| 'select'
|
|
140
|
+
| 'text'
|
|
141
|
+
| 'textarea'
|
|
142
|
+
| 'upload'
|
|
143
|
+
|
|
144
|
+
export type EditorField = {
|
|
145
|
+
blocks?: EditorBlock[]
|
|
146
|
+
/**
|
|
147
|
+
* The block slugs a zone's children may be, when the site named them instead of embedding
|
|
148
|
+
* them — resolved against the site's declared blocks when read.
|
|
149
|
+
*
|
|
150
|
+
* The alternative to `blocks` above, and the reason both exist: a block whose children may
|
|
151
|
+
* include its own type cannot embed their definitions, because the manifest is serialised and
|
|
152
|
+
* the expansion never terminates. Naming defers the lookup to read time, which is what lets
|
|
153
|
+
* one element contain itself. A slug naming no declared block is carried through the manifest
|
|
154
|
+
* unrebuilt, not dropped — it resolves to nothing once read, where the full block list is
|
|
155
|
+
* known. That keeps the manifest non-lossy: a block declared later, or renamed back, comes
|
|
156
|
+
* good on its own the next time the zone is read, with nothing to republish. In the meantime,
|
|
157
|
+
* a zone offering one fewer choice still beats an editor that will not open.
|
|
158
|
+
*/
|
|
159
|
+
childBlocks?: string[]
|
|
160
|
+
/**
|
|
161
|
+
* What a `group` **is**, when its fields are one thing the client edits rather than
|
|
162
|
+
* several — the site *saying so*, rather than the platform inferring it from a name.
|
|
163
|
+
*
|
|
164
|
+
* A group carrying this gets one row in the rail and one panel in the inspector, in
|
|
165
|
+
* place of a row per child. Two of them exist:
|
|
166
|
+
*
|
|
167
|
+
* - `'button'` — its words, where it goes, and whether it opens a new tab. Stamped by
|
|
168
|
+
* `link()`.
|
|
169
|
+
* - `'sentence'` — a line of copy with one link inside it, stamped by `sentence()`.
|
|
170
|
+
* The sentence carries a `{link}` marker saying where the link falls, so the words
|
|
171
|
+
* either side of it are one editable string rather than two.
|
|
172
|
+
*
|
|
173
|
+
* Which groups got the button panel used to be decided by a rule written in the
|
|
174
|
+
* platform — *a group whose name is literally `link`* — and a site could only opt in by
|
|
175
|
+
* choosing that name. That rule spends the name, so a block got **one** button: pbir's
|
|
176
|
+
* reviews band has two, a Submit call to action and a link out to Google, and under the
|
|
177
|
+
* naming rule the second could only be loose fields with nothing saying they were one
|
|
178
|
+
* control.
|
|
179
|
+
*
|
|
180
|
+
* `'sentence'` is what that generalisation bought. pbir's privacy line was three rows —
|
|
181
|
+
* `Before`, `Link`, `After` — because the link falls in the middle of it, and the split
|
|
182
|
+
* was worse than untidy: `text()` in `shape.ts` trims what it is given, so `'See our '`
|
|
183
|
+
* lost the space that separated it from the link and the page rendered
|
|
184
|
+
* `See ourPrivacy Policyfor how we handle your details.` A sentence whose parts are
|
|
185
|
+
* separate fields cannot hold the whitespace between them.
|
|
186
|
+
*
|
|
187
|
+
* **A manifest published before this existed carries no flag**, and the platform's
|
|
188
|
+
* validator is where the old convention still lives: a group named `link` arriving
|
|
189
|
+
* without one is marked a button on the way in. That keeps the compatibility in the one
|
|
190
|
+
* place a stale manifest is already being read, and leaves everything downstream with a
|
|
191
|
+
* single rule — this property, and not a name.
|
|
192
|
+
*/
|
|
193
|
+
element?: 'button' | 'sentence' | 'image' | 'video' | 'file'
|
|
194
|
+
poster?: {
|
|
195
|
+
label: string
|
|
196
|
+
constraints?: import('./image').ImageConstraints
|
|
197
|
+
variants?: Record<string, import('./image').ImageVariantDeclaration>
|
|
198
|
+
}
|
|
199
|
+
contractVersion?: 1
|
|
200
|
+
constraints?: import('./image').ImageConstraints
|
|
201
|
+
variants?: Record<string, import('./image').ImageVariantDeclaration>
|
|
202
|
+
/** Shown under the control in the inspector. */
|
|
203
|
+
description?: string
|
|
204
|
+
/**
|
|
205
|
+
* What the empty choice on a single-value `relationship` control is called.
|
|
206
|
+
*
|
|
207
|
+
* Optional, and the platform's own wording — "Nothing selected" — stands when it is
|
|
208
|
+
* absent, because that option is shared by every relationship field and renaming it
|
|
209
|
+
* globally would mislabel all of them. This exists for the fields where empty *means*
|
|
210
|
+
* something: a page's Layout is `null` for "inherit from the page above", so an option
|
|
211
|
+
* reading "Nothing selected" says the opposite of what choosing it does, and the cascade
|
|
212
|
+
* becomes something a client discovers by accident rather than something they were told.
|
|
213
|
+
*/
|
|
214
|
+
emptyLabel?: string
|
|
215
|
+
/** Nested fields, for `group` and `array`. */
|
|
216
|
+
fields?: EditorField[]
|
|
217
|
+
hasMany?: boolean
|
|
218
|
+
/**
|
|
219
|
+
* Present when the **target** collection should show this edge from its own side.
|
|
220
|
+
*
|
|
221
|
+
* The edge is stored once, on the side that declares it. This only asks the dashboard
|
|
222
|
+
* to derive a panel over there — "Sector › Capabilities" is a filter over rows the
|
|
223
|
+
* dashboard has already loaded, never a second copy of the reference. Two copies of one
|
|
224
|
+
* fact is the bug that reports itself as "I removed it and it's still showing".
|
|
225
|
+
*
|
|
226
|
+
* `name` may not collide with a real field on the target collection, or the derived
|
|
227
|
+
* panel and a stored value would fight over one key.
|
|
228
|
+
*/
|
|
229
|
+
inverse?: { label: string; name: string }
|
|
230
|
+
label: string
|
|
231
|
+
/** Eligible website copy is localized by default; false opts this field out. */
|
|
232
|
+
localized?: boolean
|
|
233
|
+
/**
|
|
234
|
+
* Becomes part of a dotted path (`0.slides.1.title`), so it must be an identifier:
|
|
235
|
+
* letters, digits and underscores, starting with a letter. A name containing a `.`
|
|
236
|
+
* would address a different field than it claims to.
|
|
237
|
+
*/
|
|
238
|
+
name: string
|
|
239
|
+
options?: { label: string; value: string }[]
|
|
240
|
+
/**
|
|
241
|
+
* Renders the control read-only. Never *grants* anything — permissions come from
|
|
242
|
+
* the platform, and omitting this cannot widen them.
|
|
243
|
+
*/
|
|
244
|
+
readOnly?: boolean
|
|
245
|
+
relationTo?: string
|
|
246
|
+
/**
|
|
247
|
+
* The key of the Single this field points at, present only on a `type: 'reference'` field.
|
|
248
|
+
*
|
|
249
|
+
* **Not `relationTo`.** `relationTo` names a collection the dashboard's relationship
|
|
250
|
+
* control loads options from; this names a row inside `_singles`, which is not a
|
|
251
|
+
* collection a client picks from — it is the one place a scalar like a phone number or
|
|
252
|
+
* an ABN is kept, reused wherever a block points at it. Reusing `relationTo` here would
|
|
253
|
+
* make every client's Singles selectable as a relationship target, and would store an id
|
|
254
|
+
* where this needs a stable key instead.
|
|
255
|
+
*
|
|
256
|
+
* The site names the **default** key; nothing here resolves it to a value yet — that is
|
|
257
|
+
* later work, on egress. See `reference()` in `defineBlocks.ts`.
|
|
258
|
+
*/
|
|
259
|
+
referenceKey?: string
|
|
260
|
+
required?: boolean
|
|
261
|
+
type: EditorFieldType
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type EditorBlock = {
|
|
265
|
+
fields: EditorField[]
|
|
266
|
+
label: string
|
|
267
|
+
/**
|
|
268
|
+
* This block belongs to a layout, not to a page.
|
|
269
|
+
*
|
|
270
|
+
* `seed: false` keeps a block out of the seed; it does not keep it out of the picker, so a
|
|
271
|
+
* client could add their own header as a page section and get two of them. The platform
|
|
272
|
+
* still learns nothing about what this block *is* — only that the site says it is not a
|
|
273
|
+
* page section, which is the site's call in exactly the way every other declaration is.
|
|
274
|
+
*/
|
|
275
|
+
layoutOnly?: boolean
|
|
276
|
+
/**
|
|
277
|
+
* Collections this block draws, by slug.
|
|
278
|
+
*
|
|
279
|
+
* The block is the thing rendering, so it is the thing that declares it — and a list,
|
|
280
|
+
* because one block legitimately draws more than one: a "What we do" section whose columns
|
|
281
|
+
* are `sectors` and whose lists inside them are `capabilities`.
|
|
282
|
+
*
|
|
283
|
+
* What it buys is the editor knowing where rows belong. Without it the dashboard appends
|
|
284
|
+
* every declared collection to the end of the rail at the same level as the page's own
|
|
285
|
+
* sections, which says nothing true: those rows are drawn *by* a section, and a client
|
|
286
|
+
* clicking the section that draws them found none of them there.
|
|
287
|
+
*
|
|
288
|
+
* Absent rather than empty when a block draws nothing, so a site that never declares one
|
|
289
|
+
* publishes exactly the manifest it published before.
|
|
290
|
+
*/
|
|
291
|
+
renders?: string[]
|
|
292
|
+
/** Stored on every section as `blockType`. Must be an identifier. */
|
|
293
|
+
slug: string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A repeatable set of rows the client manages as records rather than as page copy —
|
|
298
|
+
* projects, team members, testimonials.
|
|
299
|
+
*
|
|
300
|
+
* The distinction from a block's `array` field is *ownership*, not shape. An array
|
|
301
|
+
* belongs to the section it sits in: reordering the page moves it, and deleting the
|
|
302
|
+
* section deletes it. A collection's rows are their own records, listed in the CMS,
|
|
303
|
+
* addressable individually, and rendered by whichever sections ask for them.
|
|
304
|
+
*
|
|
305
|
+
* Rows are stored one per database row, all collections of all tenants in the same
|
|
306
|
+
* table, keyed by this `slug` — so declaring one here costs no migration on the
|
|
307
|
+
* platform, exactly like declaring a block.
|
|
308
|
+
*/
|
|
309
|
+
/**
|
|
310
|
+
* Where a collection's rows are rendered on the site.
|
|
311
|
+
*
|
|
312
|
+
* **The platform cannot work this out.** A row is a record in a jsonb column; whether
|
|
313
|
+
* it also has a page of its own, and at what address, is the site's own routing and
|
|
314
|
+
* lives nowhere else. Without this the dashboard can offer "edit this project" and have
|
|
315
|
+
* no idea which screen to open — which is the whole difference between a CMS list and
|
|
316
|
+
* editing a thing where it actually appears.
|
|
317
|
+
*
|
|
318
|
+
* Same bargain as `pages` and the block schema one level up: the site declares it, the
|
|
319
|
+
* platform renders from the declaration, and adding a detail route is a site deploy
|
|
320
|
+
* rather than a platform change.
|
|
321
|
+
*/
|
|
322
|
+
export type CollectionRoutes = {
|
|
323
|
+
/**
|
|
324
|
+
* Routes that render rows of this collection alongside other content — a home page
|
|
325
|
+
* carousel, an index. Rows appear here without having a page to themselves.
|
|
326
|
+
*/
|
|
327
|
+
alsoOn?: string[]
|
|
328
|
+
/**
|
|
329
|
+
* The address of a single row's own page, with `:slug` standing in for the value of
|
|
330
|
+
* `slugField` — e.g. `/projects/:slug`. Omitted when rows have no page of their own.
|
|
331
|
+
*/
|
|
332
|
+
detailPath?: string
|
|
333
|
+
/**
|
|
334
|
+
* Which of `fields` fills `:slug`. Required if `detailPath` uses one.
|
|
335
|
+
*
|
|
336
|
+
* **A row with nothing in that field falls back to its `titleField`, slugified** —
|
|
337
|
+
* lowercased, non-alphanumerics collapsed to hyphens. That is declared behaviour rather
|
|
338
|
+
* than a platform guess, and it is load-bearing: a collection's seed is captured only on
|
|
339
|
+
* a tenant's first pair, so rows predating a later-added slug field hold nothing in it
|
|
340
|
+
* and would otherwise all resolve to `/projects/`. A site whose addresses come from
|
|
341
|
+
* anything else should populate the slug field.
|
|
342
|
+
*/
|
|
343
|
+
slugField?: string
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export type ManifestCollection = {
|
|
347
|
+
/** Where these rows show up on the site. See `CollectionRoutes`. */
|
|
348
|
+
appearsOn?: CollectionRoutes
|
|
349
|
+
/** Shown under the row list in the CMS. */
|
|
350
|
+
description?: string
|
|
351
|
+
/**
|
|
352
|
+
* Second column in the CMS list — the field that best distinguishes one row from
|
|
353
|
+
* another at a glance. Omitted, the column is blank.
|
|
354
|
+
*/
|
|
355
|
+
detailField?: string
|
|
356
|
+
fields: EditorField[]
|
|
357
|
+
/** Singular noun for buttons and empty states, e.g. "project". */
|
|
358
|
+
itemNoun?: string
|
|
359
|
+
label: string
|
|
360
|
+
/** Stored on every row as its `collection`. Must be an identifier. */
|
|
361
|
+
slug: string
|
|
362
|
+
/**
|
|
363
|
+
* The site's committed rows, for seeding a tenant on its first pair — the same
|
|
364
|
+
* bargain as a block's `default`, one level up. Each entry is one row's values.
|
|
365
|
+
*/
|
|
366
|
+
seed?: Record<string, unknown>[]
|
|
367
|
+
/**
|
|
368
|
+
* Which field titles a row in the CMS list. Must name one of `fields`; a row whose
|
|
369
|
+
* value there is empty falls back to a positional label.
|
|
370
|
+
*/
|
|
371
|
+
titleField: string
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The types a form field may declare. A closed set, on the same reasoning as
|
|
376
|
+
* `EditorFieldType`: the platform's submission validator and the dashboard's renderer both
|
|
377
|
+
* switch on it, and a site inventing a tenth type gets that field silently dropped rather
|
|
378
|
+
* than mis-rendered.
|
|
379
|
+
*/
|
|
380
|
+
export type FormFieldType =
|
|
381
|
+
'checkbox' | 'date' | 'email' | 'number' | 'select' | 'tel' | 'text' | 'textarea'
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* One field of a form, as the platform stores and validates against it.
|
|
385
|
+
*
|
|
386
|
+
* `role` promotes a field into the column of that name on every submission — `email`,
|
|
387
|
+
* `name` or `phone` — so the inbox can sort and search leads without unpacking every row's
|
|
388
|
+
* free-form data. A form free to declare none of these falls back to a field literally
|
|
389
|
+
* named `email`/`name`/`phone`; see `promote()` on the platform.
|
|
390
|
+
*/
|
|
391
|
+
export type FormFieldDeclaration = {
|
|
392
|
+
label: string
|
|
393
|
+
name: string
|
|
394
|
+
options?: string[]
|
|
395
|
+
required?: boolean
|
|
396
|
+
role?: 'email' | 'name' | 'phone'
|
|
397
|
+
type: FormFieldType
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* What a site declares about one form it collects — a contact form, a careers application.
|
|
402
|
+
*
|
|
403
|
+
* Mirrors `ManifestCollection`'s bargain one level along: the site says what it collects and
|
|
404
|
+
* who should hear about it, the platform stores submissions against the declaration and
|
|
405
|
+
* validates every write to `POST /api/u90/forms/[slug]` against this `fields` snapshot.
|
|
406
|
+
*/
|
|
407
|
+
export type FormDeclaration = {
|
|
408
|
+
fields: FormFieldDeclaration[]
|
|
409
|
+
label: string
|
|
410
|
+
/**
|
|
411
|
+
* Seeds the handling half — who is emailed — on first sight only, and never again. A
|
|
412
|
+
* client who repoints the recipients in the dashboard keeps their choice across every
|
|
413
|
+
* later redeploy; this is only ever read once, the moment the form is first synced.
|
|
414
|
+
*/
|
|
415
|
+
recipients?: string[]
|
|
416
|
+
/** Stored on every submission as its `form`. Must be an identifier. */
|
|
417
|
+
slug: string
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* One route this site serves, and where it sits in the site's own structure.
|
|
422
|
+
*
|
|
423
|
+
* The platform draws its sitemap from these, so a client can see their whole site as a
|
|
424
|
+
* tree and add a page inside any branch of it. Which means the *shape* has to come from
|
|
425
|
+
* here: a path is not a hierarchy. JDSi's `/infrastructure` is a top-level route that
|
|
426
|
+
* belongs under "What We Do" in the menu, and `/about-us/our-team` is nested in both — no
|
|
427
|
+
* rule over paths gets both right, because the answer is the site's information
|
|
428
|
+
* architecture and that lives in the site's own nav.
|
|
429
|
+
*
|
|
430
|
+
* Same bargain as the block schema: the site declares, the platform renders what it is
|
|
431
|
+
* given, and changing the structure is a site deploy rather than a platform change. Use
|
|
432
|
+
* `pagesFromNav` rather than writing these by hand — the nav is already the tree.
|
|
433
|
+
*/
|
|
434
|
+
export type PageDeclaration = {
|
|
435
|
+
label: string
|
|
436
|
+
/**
|
|
437
|
+
* What this page starts with, if the site commits content for it.
|
|
438
|
+
*
|
|
439
|
+
* The top-level `seed` on the manifest is the *home* page's, and for a long time it was the
|
|
440
|
+
* only one a site could send — every other declared route arrived at a new tenant empty,
|
|
441
|
+
* however much committed copy the repo held for it. That is right for a route the client is
|
|
442
|
+
* meant to write, and wrong for one the site already renders in full: a legal page whose
|
|
443
|
+
* words live in the repo seeded as a blank, so pairing produced a page that said nothing
|
|
444
|
+
* where the site said plenty.
|
|
445
|
+
*
|
|
446
|
+
* Optional and additive. A site that sends none behaves exactly as before, and the home
|
|
447
|
+
* page keeps arriving through the top-level `seed` — this does not replace it, and a page
|
|
448
|
+
* carrying both is seeded from this one, because the specific beats the general.
|
|
449
|
+
*/
|
|
450
|
+
seed?: (Record<string, unknown> & { blockType: string })[]
|
|
451
|
+
/**
|
|
452
|
+
* The slug of the page this one sits under.
|
|
453
|
+
*
|
|
454
|
+
* Must be another declared page's `slug`. Omitted means top level, which the sitemap
|
|
455
|
+
* hangs off the landing page. A parent naming a page that isn't declared, or a cycle,
|
|
456
|
+
* rejects the whole manifest — a tree the platform can't lay out is not a tree.
|
|
457
|
+
*/
|
|
458
|
+
parent?: string
|
|
459
|
+
/**
|
|
460
|
+
* Route path with no leading slash — `about-us`, `about-us/our-team`. The landing page
|
|
461
|
+
* is `home`, matching the slug the platform's own page documents use.
|
|
462
|
+
*/
|
|
463
|
+
slug: string
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The postMessage protocol version between a site and the dashboard.
|
|
468
|
+
*
|
|
469
|
+
* The dashboard refuses to render its editor when a site reports a version it doesn't
|
|
470
|
+
* speak, because the alternative is edits appearing to save and never showing up. Bump
|
|
471
|
+
* this only alongside a change to the message shapes in `bridge.tsx`, and expect every
|
|
472
|
+
* site to need a `site-kit` update — that is exactly what it is for.
|
|
473
|
+
*
|
|
474
|
+
* **2 — `u90:content` carries `entries`.** Collection rows are editable on Canvas, so the
|
|
475
|
+
* dashboard pushes the rows alongside the page's sections and `useLiveContent` returns
|
|
476
|
+
* them. A site on version 1 renders its rows from the server fetch and ignores the push:
|
|
477
|
+
* a client's keystrokes into a project would do nothing while the page around them
|
|
478
|
+
* updated live, which is exactly what this version number exists to refuse loudly.
|
|
479
|
+
*
|
|
480
|
+
* **3 — `u90:content` carries `layout`.** The chrome a page is drawn inside — its header,
|
|
481
|
+
* its footer — is its own document, and the dashboard now pushes its blocks beside the
|
|
482
|
+
* page's sections so a tagline in a header edits live like anything else on the screen.
|
|
483
|
+
*
|
|
484
|
+
* That much is additive, and on its own would not have moved this number: a site on 2
|
|
485
|
+
* ignores a key it does not read. It bumps because of the other half of the feature, which
|
|
486
|
+
* ships *here* — a site marks its chrome by calling `layoutField`, so a site on 2 marks no
|
|
487
|
+
* layout field at all and its Canvas would show a header the client can see and cannot
|
|
488
|
+
* touch, with nothing saying why. Refusing that site and naming both versions says what
|
|
489
|
+
* has to happen, which is that this package needs re-vendoring. The refusal is a choice to
|
|
490
|
+
* keep the two halves in step, not a wire format they truly disagree about.
|
|
491
|
+
*/
|
|
492
|
+
export const BRIDGE_VERSION = 3
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The browsing-context name Canvas gives every site iframe.
|
|
496
|
+
*
|
|
497
|
+
* `window.parent !== window` is the ordinary framed check, but Vercel Sandbox previews run in a
|
|
498
|
+
* sandboxed cross-origin iframe whose child can report itself as its own parent. The name is an
|
|
499
|
+
* explicit identity that survives the preview route's redirects without leaking a secret into
|
|
500
|
+
* the URL. It only enables the bridge; every inbound message is still restricted to the site's
|
|
501
|
+
* configured platform origin.
|
|
502
|
+
*/
|
|
503
|
+
export const CANVAS_FRAME_NAME = 'u90-canvas'
|
|
504
|
+
|
|
505
|
+
/** A named arrangement of blocks with one content slot. Absent on a site that declares none. */
|
|
506
|
+
export type LayoutDeclaration = {
|
|
507
|
+
default?: boolean
|
|
508
|
+
entries: (Record<string, unknown> & { blockType: string })[]
|
|
509
|
+
label: string
|
|
510
|
+
/** The declaration key. Stable across re-seeds; the label is not. */
|
|
511
|
+
name: string
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* One shared scalar a site says its client starts with.
|
|
516
|
+
*
|
|
517
|
+
* Not a category the platform invented and not a shape it understands: the label is the
|
|
518
|
+
* client's word for the fact, the value is whatever the site shipped with, and the platform
|
|
519
|
+
* learns nothing about what any of them mean beyond whether it is a piece of text or a URL.
|
|
520
|
+
* That is the whole difference from `business()`, which named the facts itself and so could
|
|
521
|
+
* only ever describe the sites it had been written for.
|
|
522
|
+
*
|
|
523
|
+
* The key is the identifier a block's `referenceKey` points at, so it obeys the rule field
|
|
524
|
+
* names obey — see `singles()` in `defineBlocks.ts`.
|
|
525
|
+
*/
|
|
526
|
+
export type SingleDeclaration = {
|
|
527
|
+
/** The identifier a `reference()` names. Stable across re-seeds; the label is not. */
|
|
528
|
+
key: string
|
|
529
|
+
/** What the client recognises the value by in their Singles tab. Theirs to change. */
|
|
530
|
+
label: string
|
|
531
|
+
type: 'link' | 'text'
|
|
532
|
+
/** What the site ships with, and what the seeded row starts as. */
|
|
533
|
+
value: string
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* What a site publishes at `GET /api/u90/manifest`.
|
|
538
|
+
*
|
|
539
|
+
* The platform fetches this, validates it, and renders its editor from it. It is never
|
|
540
|
+
* accepted over any other route: one authenticated way in is what makes validation
|
|
541
|
+
* worth having.
|
|
542
|
+
*/
|
|
543
|
+
export type SiteManifest = {
|
|
544
|
+
blocks: EditorBlock[]
|
|
545
|
+
bridgeVersion: number
|
|
546
|
+
/**
|
|
547
|
+
* Repeatable record types this site renders from. Optional: a site with none is
|
|
548
|
+
* exactly the site this contract described before they existed.
|
|
549
|
+
*/
|
|
550
|
+
collections?: ManifestCollection[]
|
|
551
|
+
/** ISO 8601. Only shown to an operator, to judge how fresh a schema is. */
|
|
552
|
+
generatedAt: string
|
|
553
|
+
/**
|
|
554
|
+
* The sections a page starts with when a client adds one in the dashboard.
|
|
555
|
+
*
|
|
556
|
+
* Distinct from `seed`, which is the copy this site already serves and is captured once on
|
|
557
|
+
* a tenant's first pair. This is applied on **every** create, forever, and lands as a
|
|
558
|
+
* draft. The platform cannot invent it: it has no way to know which of a site's blocks
|
|
559
|
+
* belong on a page it did not design — a client-added page should not open with a hero
|
|
560
|
+
* carousel — so the site says, and a site that says nothing gets a blank page.
|
|
561
|
+
*
|
|
562
|
+
* **Position is the address.** An editable element is `${sectionIndex}.${fieldPath}`, so
|
|
563
|
+
* this array's order is the identity of every field on the new page. Reordering it changes
|
|
564
|
+
* what existing pages' stored content means.
|
|
565
|
+
*
|
|
566
|
+
* Give the leaves literal placeholder words rather than leaving them empty. Everywhere else
|
|
567
|
+
* in this contract a blank field means "keep the copy the component ships with", but a
|
|
568
|
+
* brand-new page has no committed copy underneath it — so blank means nothing renders, and
|
|
569
|
+
* an element with no text is one the editing bridge cannot make clickable.
|
|
570
|
+
*/
|
|
571
|
+
newPage?: SeedSection[]
|
|
572
|
+
/**
|
|
573
|
+
* What one fresh instance of each block contains, keyed by block slug.
|
|
574
|
+
*
|
|
575
|
+
* Read when a client adds a section in the dashboard. The platform stores exactly this,
|
|
576
|
+
* then lets them edit it.
|
|
577
|
+
*
|
|
578
|
+
* **The platform cannot invent it, and an empty section is worse than none.** A section
|
|
579
|
+
* whose leaves are blank renders nothing, and an element with no text is one the editing
|
|
580
|
+
* bridge cannot make clickable — so appending a hollow block would add an invisible
|
|
581
|
+
* section a client can neither see nor select. The same reasoning `newPage` carries, per
|
|
582
|
+
* block rather than per page.
|
|
583
|
+
*
|
|
584
|
+
* **Here rather than on `EditorBlock`, and that is not filing.** `EditorBlock` is schema,
|
|
585
|
+
* and schema is carrier-independent: a site may declare a default through `default` or
|
|
586
|
+
* through the positional `value` form, and `defineSite` and `defineBlocks` must publish
|
|
587
|
+
* the same fields either way — `verify:define-site` asserts exactly that. Content hung off
|
|
588
|
+
* a block would make its schema depend on which carrier the site happened to use. So this
|
|
589
|
+
* sits beside `seed` and `newPage`, with the other content, and is validated like them.
|
|
590
|
+
*
|
|
591
|
+
* A block absent from this map declares no defaults. The dashboard still offers it and
|
|
592
|
+
* says it will arrive empty, rather than hiding a block the site can render.
|
|
593
|
+
*/
|
|
594
|
+
starters?: Record<string, Record<string, unknown>>
|
|
595
|
+
/**
|
|
596
|
+
* Routes the site serves, offered to clients as link targets and drawn as the sitemap.
|
|
597
|
+
*
|
|
598
|
+
* `parent` is what makes it a tree rather than a list; see `PageDeclaration`.
|
|
599
|
+
*/
|
|
600
|
+
pages: PageDeclaration[]
|
|
601
|
+
/**
|
|
602
|
+
* True when the site serves a page at **every** slug the platform stores — a catch-all
|
|
603
|
+
* route rather than one hand-built route per address.
|
|
604
|
+
*
|
|
605
|
+
* The dashboard's page tree marks a page it holds that this manifest does not declare as
|
|
606
|
+
* "Not routed", which is the honest thing to say when each address needs its own file and
|
|
607
|
+
* a deploy. With a catch-all it is false, and telling a client the page they just added is
|
|
608
|
+
* unreachable — when it is live — is worse than saying nothing at all.
|
|
609
|
+
*
|
|
610
|
+
* It does not replace `pages`: those are still what the nav lists, what a link picker
|
|
611
|
+
* offers, and what the tree's shape comes from. This only changes what an *undeclared*
|
|
612
|
+
* page is called.
|
|
613
|
+
*/
|
|
614
|
+
routesAllPages?: boolean
|
|
615
|
+
/**
|
|
616
|
+
* The site's own committed content, for seeding a tenant on its first pair.
|
|
617
|
+
*
|
|
618
|
+
* Optional, and only ever read once: a tenant that already has a page keeps it.
|
|
619
|
+
* The point is that a client's first session shows their real copy rather than a
|
|
620
|
+
* grid of empty fields — the site looks finished, so the editor should too.
|
|
621
|
+
*
|
|
622
|
+
* It has to come from the site because nothing else can know it. The manifest
|
|
623
|
+
* describes field *names and types*; the values live in the site's own config, and
|
|
624
|
+
* the rendered page cannot be scraped for them either — the editable-field markers
|
|
625
|
+
* only appear once content exists, so a fresh site emits none.
|
|
626
|
+
*/
|
|
627
|
+
seed?: SeedSection[]
|
|
628
|
+
/**
|
|
629
|
+
* The site's starting layouts. Seeded once per tenant, client-owned after that — so this
|
|
630
|
+
* is a *starting set*, not a description of what the tenant currently has. See the
|
|
631
|
+
* layouts design on why re-seeding is additive and keyed on `name`.
|
|
632
|
+
*/
|
|
633
|
+
layouts?: LayoutDeclaration[]
|
|
634
|
+
/**
|
|
635
|
+
* The shared values this site starts with. Seeded once per tenant, client-owned after
|
|
636
|
+
* that — so this is a *starting set*, like `layouts`, and never a description of what the
|
|
637
|
+
* tenant currently holds.
|
|
638
|
+
*
|
|
639
|
+
* A site that points at a Single with `reference()` and declares nothing here is legal and
|
|
640
|
+
* renders correctly off its committed fallbacks; what it costs the client is discovery,
|
|
641
|
+
* which is the whole reason this key exists. The reverse is legal too: a value declared
|
|
642
|
+
* here that no block references yet is seeded and left alone.
|
|
643
|
+
*/
|
|
644
|
+
singles?: SingleDeclaration[]
|
|
645
|
+
/**
|
|
646
|
+
* The forms this site collects, declared once and maintained afterwards by the platform's
|
|
647
|
+
* own sync — a redeploy replaces `fields` on the stored row, and leaves the label, the
|
|
648
|
+
* recipients and whether the client switched it off alone. Optional, on the same terms as
|
|
649
|
+
* `collections`: a site with none is exactly the site this contract described before forms
|
|
650
|
+
* existed.
|
|
651
|
+
*/
|
|
652
|
+
forms?: FormDeclaration[]
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** One seeded section: a block slug plus values matching that block's fields. */
|
|
656
|
+
export type SeedSection = Record<string, unknown> & { blockType: string }
|
|
657
|
+
|
|
658
|
+
/** The three values a site needs, as returned by `POST /api/u90/pair`. */
|
|
659
|
+
export type PairedCredentials = {
|
|
660
|
+
apiKey: string
|
|
661
|
+
platformUrl: string
|
|
662
|
+
revalidateSecret: string
|
|
663
|
+
tenantSlug: string
|
|
664
|
+
}
|