@uniweb/core 0.24.3 → 0.24.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.24.3",
3
+ "version": "0.24.4",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
package/src/services.js CHANGED
@@ -12,10 +12,14 @@ import { applyBasePath } from './base-path.js'
12
12
  * address comes from configuration, and there are exactly two places it can
13
13
  * come from:
14
14
  *
15
- * 1. **The site**, authored — `search:`, `submit:`, `assistant:`, `tracking:`
16
- * in site.yml. The operator's own declaration, and it wins.
17
- * 2. **The host**, served — `config.services.<name>` in the payload. What the
18
- * deployment offers, which the site never had to know about.
15
+ * 1. **The host**, served — `config.services.<name>` in the payload. What the
16
+ * deployment offers, which the site never had to know about. When a host
17
+ * offers a service, that is the answer: it is the authority on what a
18
+ * hosted site is given.
19
+ * 2. **The site**, authored — `search:`, `submit:`, `assistant:`, `tracking:`
20
+ * in site.yml. The operator's own declaration — used for anything the host
21
+ * does not provide, and on a static site, where no host speaks, the whole
22
+ * answer.
19
23
  *
20
24
  * Absent from both means the site has no such service, and the caller acts on
21
25
  * that rather than guessing an address. That is the same rule for every service,
@@ -117,7 +121,37 @@ const ABSOLUTE_URL_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i
117
121
  * @param {*} declaration
118
122
  * @returns {string} the endpoint, or '' when there is none
119
123
  */
124
+ /**
125
+ * Has this declaration switched the service OFF?
126
+ *
127
+ * ⭐ `false`, or an object with `enabled: false` — on either tier, for every
128
+ * name. It is the spelling `search` has always honoured
129
+ * (`Website.isSearchEnabled`), now read for every service.
130
+ *
131
+ * ⛔ **Until 2026-09-10 this module read it for NO service** — so the same
132
+ * spelling behaved two ways: `search: false` turned search off, while
133
+ * `submit: false` or `submit: { endpoint, enabled: false }` drew the form
134
+ * anyway. On a static site the authored key is the only place a service can be
135
+ * declared, so there the switch worked for search alone.
136
+ *
137
+ * ⚖️ **A site's refusal does not override a host's offer** — step 3 of
138
+ * `resolveService` says why.
139
+ *
140
+ * Module-private on purpose: it is a rule of the two readers below, not a
141
+ * question a caller should ask. `resolveService` is the question.
142
+ */
143
+ function isServiceRefused(declaration) {
144
+ return (
145
+ declaration === false ||
146
+ (declaration !== null && typeof declaration === 'object' && declaration.enabled === false)
147
+ )
148
+ }
149
+
120
150
  export function readEndpoint(declaration) {
151
+ // A refused service offers no address, whatever else its block carries — so
152
+ // a host row with `enabled: false` reads as a decline, and the records lane
153
+ // (`records-service.js`) withholds its address the same way.
154
+ if (isServiceRefused(declaration)) return ''
121
155
  if (typeof declaration === 'string') return declaration.trim()
122
156
  if (typeof declaration?.endpoint === 'string') return declaration.endpoint.trim()
123
157
  return ''
@@ -168,28 +202,42 @@ export function resolveServiceUrl(endpoint, basePath = '') {
168
202
  * @param {string} name - service name, e.g. 'submit' · 'search' · 'tracking'
169
203
  * @returns {{ url: string|null, source: 'site'|'host'|null }}
170
204
  * `url` is the whole answer for acting. `source` says which declaration
171
- * answered — a diagnostic, and the thing to check when a host's value appears
172
- * not to be taking effect. `'host'` with a null `url` means the host answered
205
+ * answered — a diagnostic, and the thing to check when a value you set
206
+ * appears not to be taking effect. `'host'` with a null `url` means the host answered
173
207
  * and offered no address; `null` means nothing declared the service at all.
174
208
  */
175
209
  export function resolveService(website, name) {
176
210
  const config = website?.config
177
211
  const basePath = website?.basePath
178
212
 
179
- // 1 — the site's own declaration wins. An operator who named an endpoint
180
- // means it, including on a host that offers one.
181
- const authored = readEndpoint(config?.[name])
182
- if (authored) {
183
- return { url: resolveServiceUrl(authored, basePath), source: 'site' }
184
- }
185
-
186
- // 2 — what the host says it offers.
213
+ // 1 — what the host OFFERS. A host that returns an address is the authority
214
+ // on what a hosted site is given: nothing the site declares overrides it —
215
+ // not its own address (step 2), not its off switch (step 3).
187
216
  const hostDeclaration = config?.services?.[name]
188
217
  const hostEndpoint = readEndpoint(hostDeclaration)
189
218
  if (hostEndpoint) {
190
219
  return { url: resolveServiceUrl(hostEndpoint, basePath), source: 'host' }
191
220
  }
192
221
 
222
+ // 2 — the site's own ADDRESS, for a service the host does not provide: an
223
+ // operator bringing their own provider — a form service, self-hosted search
224
+ // on a host that does not sell it — keeps working. On a static site no host
225
+ // speaks, so this is the whole answer. (Until 2026-09-10 it came first and
226
+ // outranked a host's offer too.)
227
+ const authored = readEndpoint(config?.[name])
228
+ if (authored) {
229
+ return { url: resolveServiceUrl(authored, basePath), source: 'site' }
230
+ }
231
+
232
+ // 3 — ⛔ A SITE THAT SWITCHED THE SERVICE OFF — but only where no host offers
233
+ // it. A host that returns an address is the authority on what a hosted site
234
+ // is given: the site asked for the service where the host manages it, and a
235
+ // hosted service is turned off there — not with a second switch here that
236
+ // would hide a service the host is still providing. On a static site no host
237
+ // speaks, so this is the only switch there is, and it decides. (It briefly
238
+ // ran first, on 2026-09-10, and beat a host's offer — the wrong way round.)
239
+ if (isServiceRefused(config?.[name])) return { url: null, source: 'site' }
240
+
193
241
  // A host may declare the name while offering no address — a decline. It is
194
242
  // still the host answering, which is all a caller can use: any *wording* for
195
243
  // that state would be ours to invent, in one language, for a visitor who has
package/src/website.js CHANGED
@@ -1140,13 +1140,15 @@ export default class Website {
1140
1140
  * @returns {boolean}
1141
1141
  */
1142
1142
  isSearchEnabled() {
1143
- const search = this.config?.search
1144
- if (typeof search === 'boolean') {
1145
- if (!search) return false
1146
- } else if (search?.enabled === false) {
1147
- return false
1148
- }
1149
-
1143
+ // ⭐ THE ORDER IS resolveService's, the same for every service: the host's
1144
+ // offer, then the site's own endpoint, then the site's `false` /
1145
+ // `{ enabled: false }` — which decides only where no host offers search. So
1146
+ // an author's `search: false` turns the local index off on a static site
1147
+ // and does not hide search a host provides; a hosted service is turned off
1148
+ // where the host provides it. Until 2026-09-10 this method read the
1149
+ // author's switch FIRST, so `search: false` beat a host's offer — the one
1150
+ // service where it did, and the opposite of every other.
1151
+ //
1150
1152
  // ⭐ THE HOST GETS A SAY, and this is the half that reaches an already-
1151
1153
  // published foundation. A foundation bundles its own frozen copy of
1152
1154
  // `@uniweb/kit`, so a fix made in kit never reaches one built before it.
@@ -1164,14 +1166,18 @@ export default class Website {
1164
1166
  // error, and any text a visitor reads is site content — authored and
1165
1167
  // localized — never a string a service layer invents.
1166
1168
  //
1167
- // A site's OWN `search.endpoint` still wins: `resolveService` answers from
1168
- // the site tier first, so self-hosted search on a host that does not sell
1169
- // it is untouched.
1169
+ // A site's own `search.endpoint` still works where the host does not offer
1170
+ // search, so self-hosted search on a host that does not sell it is
1171
+ // untouched. Where the host offers search, its offer wins.
1170
1172
  const { url, source } = resolveService(this, 'search')
1171
- if (source === 'host' && !url) return false
1172
-
1173
- // Enabled by default — absent means enabled, and a host that publishes no
1174
- // services block at all has declined nothing (the static-host path).
1173
+ // An address from anyone — the site's own endpoint, or a host's offer.
1174
+ if (url) return true
1175
+ // 'site': switched off, with no host offering it. 'host': the host declines.
1176
+ if (source) return false
1177
+
1178
+ // Nobody spoke: enabled by default — absent means enabled, and a host that
1179
+ // publishes no services block at all has declined nothing (the static-host
1180
+ // path, where the build emits the local index).
1175
1181
  return true
1176
1182
  }
1177
1183