@bygghammar/cms-platform 0.90.0-m7.10 → 0.90.0-m7.11

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/CHANGELOG.md CHANGED
@@ -8,6 +8,23 @@ matches `package.json` exactly. See
8
8
  Versions 0.1.0 through 0.78.0 predate this repository and were reconstructed
9
9
  from the published npm tarballs.
10
10
 
11
+ ## 0.90.0-m7.11
12
+
13
+ - The page builder no longer invents a public URL for a locale with no
14
+ configured public domain. An empty `publicHost(locale)` means no public URL
15
+ exists, and is no longer read as a relative one.
16
+ - For an unmapped locale the page path is shown as plain, non-clickable
17
+ information, saying that no domain is configured for that language. The
18
+ public-link anchor and the Copy URL action are omitted rather than pointed
19
+ somewhere else: no relative href, no backoffice or request hostname, no other
20
+ locale's domain.
21
+ - Draft Preview keeps working for an unmapped locale. Previewing is a
22
+ backoffice operation and has never needed a public hostname.
23
+ - The `publicHost`, `publicPath` and `usePageUrl` host contracts document this,
24
+ so a host knows what the empty string means before it implements one.
25
+ - No migration and no database change. Nothing in this release concerns a
26
+ host's own integration code.
27
+
11
28
  ## 0.90.0-m7.10
12
29
 
13
30
  - Code & tracking drafts survive a refetch. A background reload replaced the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygghammar/cms-platform",
3
- "version": "0.90.0-m7.10",
3
+ "version": "0.90.0-m7.11",
4
4
  "description": "Shared CMS platform: blocks, block types, i18n core, UI primitives and design-token contract used by every Bygghammar/Voyara site.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -64,9 +64,23 @@ export type PageBuilderDeps<TBlock> = {
64
64
  locales: readonly string[];
65
65
  localeLabels: Record<string, string>;
66
66
  defaultLocale: string;
67
- /** Public path for this page in the given locale. */
67
+ /**
68
+ * Site-relative path for this page in the given locale.
69
+ *
70
+ * A path on its own is not a public address. It only becomes one when
71
+ * `publicHost` supplies the hostname it belongs to.
72
+ */
68
73
  publicPath: (locale: string) => string;
69
- /** Host name to build the absolute URL with, for the given locale. */
74
+ /**
75
+ * The public hostname for this locale, or `""` when there is none.
76
+ *
77
+ * Since m7.10 a deployment's locale-to-domain table is explicit: a locale
78
+ * that is not in it has no public site. `""` is that answer — NO PUBLIC URL
79
+ * EXISTS — and it must never be read as "use a relative URL" or "fall back
80
+ * to the current host". Doing so hands the browser a path it resolves
81
+ * against whatever hostname the backoffice happens to be served from,
82
+ * inventing a public address for a locale the deployment does not publish.
83
+ */
70
84
  publicHost: (locale: string) => string;
71
85
 
72
86
  versions: BuilderVersion[] | undefined;
@@ -156,7 +170,11 @@ export function PageBuilderView<TBlock>({ deps }: { deps: PageBuilderDeps<TBlock
156
170
 
157
171
  const slugPath = deps.publicPath(locale);
158
172
  const host = deps.publicHost(locale);
159
- const absoluteUrl = host ? `https://${host}${slugPath}` : slugPath;
173
+ // No hostname, no public URL. A relative href here would be resolved against
174
+ // the backoffice hostname, which is not where this page is published — and
175
+ // for an unmapped locale it is not published anywhere.
176
+ const publicUrl = host ? `https://${host}${slugPath}` : null;
177
+ const localeName = localeLabels[locale] ?? locale;
160
178
 
161
179
  const togglePanel = (panel: "history" | "schedule") => {
162
180
  if (panel === "history") {
@@ -171,8 +189,9 @@ export function PageBuilderView<TBlock>({ deps }: { deps: PageBuilderDeps<TBlock
171
189
  };
172
190
 
173
191
  const onCopyUrl = async () => {
192
+ if (!publicUrl) return;
174
193
  try {
175
- await navigator.clipboard.writeText(absoluteUrl);
194
+ await navigator.clipboard.writeText(publicUrl);
176
195
  setCopied(true);
177
196
  setTimeout(() => setCopied(false), 1500);
178
197
  } catch {
@@ -329,24 +348,40 @@ export function PageBuilderView<TBlock>({ deps }: { deps: PageBuilderDeps<TBlock
329
348
  </div>
330
349
  )}
331
350
  <div className="mt-2 flex flex-wrap items-center gap-2">
332
- <a
333
- href={absoluteUrl}
334
- target="_blank"
335
- rel="noreferrer"
336
- title={`Open ${absoluteUrl}`}
337
- className="inline-flex items-center gap-1.5 border border-ink/15 bg-white px-2.5 py-1 font-mono text-xs text-ink hover:border-sea"
338
- >
339
- {slugPath}
340
- <ExternalLink className="size-3 text-ink/40" />
341
- </a>
342
- <button
343
- onClick={onCopyUrl}
344
- title="Copy full URL"
345
- className="inline-flex items-center gap-1.5 border border-ink/15 px-2 py-1 text-xs text-ink/60 hover:border-sea hover:text-ink"
346
- >
347
- {copied ? <Check className="size-3" /> : <Copy className="size-3" />}
348
- {copied ? "Copied" : "Copy URL"}
349
- </button>
351
+ {publicUrl ? (
352
+ <>
353
+ <a
354
+ href={publicUrl}
355
+ target="_blank"
356
+ rel="noreferrer"
357
+ title={`Open ${publicUrl}`}
358
+ className="inline-flex items-center gap-1.5 border border-ink/15 bg-white px-2.5 py-1 font-mono text-xs text-ink hover:border-sea"
359
+ >
360
+ {slugPath}
361
+ <ExternalLink className="size-3 text-ink/40" />
362
+ </a>
363
+ <button
364
+ onClick={onCopyUrl}
365
+ title="Copy full URL"
366
+ className="inline-flex items-center gap-1.5 border border-ink/15 px-2 py-1 text-xs text-ink/60 hover:border-sea hover:text-ink"
367
+ >
368
+ {copied ? <Check className="size-3" /> : <Copy className="size-3" />}
369
+ {copied ? "Copied" : "Copy URL"}
370
+ </button>
371
+ </>
372
+ ) : (
373
+ /* Not a link and not a copy target: there is no address to open
374
+ or to paste into a message. The path is still shown, because
375
+ it is the one true thing here — where this page would live if
376
+ the locale were given a domain. */
377
+ <span
378
+ className="inline-flex items-center gap-1.5 border border-dashed border-ink/20 px-2.5 py-1 font-mono text-xs text-ink/50"
379
+ title={`${localeName} has no public domain, so this page has no public address.`}
380
+ >
381
+ {slugPath}
382
+ <span className="font-sans">· No domain configured for {localeName}</span>
383
+ </span>
384
+ )}
350
385
  {locales.length > 1 && (
351
386
  <div className="flex items-center gap-1">
352
387
  {locales.map((loc) => (
@@ -239,6 +239,14 @@ export type BackofficeHost = {
239
239
  /**
240
240
  * React hook returning a resolver for a page's public URL. Implemented as a
241
241
  * hook so a site can read its slug overrides and locale domains.
242
+ *
243
+ * `path` is site-relative. `host` is the public hostname for that locale,
244
+ * or `""` when the locale has no public hostname — since m7.10 the
245
+ * locale-to-domain table is explicit, so a locale missing from it has no
246
+ * public site at all. An empty `host` therefore means NO PUBLIC URL
247
+ * EXISTS; it is not a signal to use `path` on its own, and a host must
248
+ * not answer it by borrowing the current hostname or another locale's
249
+ * domain. Screens are expected to offer no public link in that case.
242
250
  */
243
251
  usePageUrl: () => (
244
252
  pageKey: string,