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

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,45 @@ 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.12
12
+
13
+ - The page SEO panel no longer prints a bare page path under the Canonical
14
+ heading. For a locale with no configured public domain there is no canonical
15
+ URL, and a path shown there reads as an address on whatever hostname the
16
+ backoffice happens to be open at.
17
+ - That case now says so in words — *No public URL — no domain configured for
18
+ this language* — instead of a relative path, the current or backoffice
19
+ hostname, another locale's domain, or a fabricated absolute URL.
20
+ - The Route row is unchanged and still shows the page path alongside the
21
+ current registry route, so nothing that was accurate is lost.
22
+ - A mapped locale is unaffected: its canonical preview stays absolute on that
23
+ locale's own domain. So is single-origin mode, where the one hostname serving
24
+ every language answers for each of them.
25
+ - `pageSeoPreviewCanonical` keeps its signature and its return value, path
26
+ fallback included, so existing callers are unaffected; what an empty origin
27
+ means is decided by `PageSeoPanelView`, which owns the display. The helper's
28
+ documentation now records that the fallback is not a URL.
29
+ - Regression coverage renders the panel and asserts the Canonical row itself
30
+ for the mapped, single-origin and unmapped cases.
31
+ - No migration and no database change.
32
+
33
+ ## 0.90.0-m7.11
34
+
35
+ - The page builder no longer invents a public URL for a locale with no
36
+ configured public domain. An empty `publicHost(locale)` means no public URL
37
+ exists, and is no longer read as a relative one.
38
+ - For an unmapped locale the page path is shown as plain, non-clickable
39
+ information, saying that no domain is configured for that language. The
40
+ public-link anchor and the Copy URL action are omitted rather than pointed
41
+ somewhere else: no relative href, no backoffice or request hostname, no other
42
+ locale's domain.
43
+ - Draft Preview keeps working for an unmapped locale. Previewing is a
44
+ backoffice operation and has never needed a public hostname.
45
+ - The `publicHost`, `publicPath` and `usePageUrl` host contracts document this,
46
+ so a host knows what the empty string means before it implements one.
47
+ - No migration and no database change. Nothing in this release concerns a
48
+ host's own integration code.
49
+
11
50
  ## 0.90.0-m7.10
12
51
 
13
52
  - 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.12",
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,
@@ -38,7 +38,21 @@ export type PageSeoPanelViewProps = {
38
38
  pageKey?: string | null;
39
39
  /** Current registry route for a locale, shown in the preview panel. */
40
40
  registryRoute: (locale: string) => string;
41
- /** Public origin used to build the canonical preview URL. */
41
+ /**
42
+ * The public hostname this locale is published on, or `""` when there is
43
+ * none.
44
+ *
45
+ * Since m7.10 a deployment's locale-to-domain table is explicit: a locale
46
+ * absent from it has no public site. `""` is that answer — NO PUBLIC URL
47
+ * EXISTS — and it must never be read as "use a relative URL", "use whatever
48
+ * hostname the backoffice is open at", or "borrow another locale's domain".
49
+ * Each of those states an address the page is not served at, under the one
50
+ * heading a search engine is asked to believe.
51
+ *
52
+ * A host in single-origin mode — no mappings at all, every language under a
53
+ * locale prefix on one hostname — answers with that hostname, so the
54
+ * canonical stays absolute there.
55
+ */
42
56
  originForLocale: (locale: string) => string;
43
57
  /** Images found on this page, for the "use hero / pick from page" buttons. */
44
58
  pageImages: readonly PageSeoImageCandidate[];
@@ -171,10 +185,13 @@ export function PageSeoPanelView({
171
185
  locale: activeLocale,
172
186
  defaultLocale,
173
187
  });
174
- const previewCanonical = pageSeoPreviewCanonical({
175
- path: previewPath,
176
- origin: originForLocale(activeLocale),
177
- });
188
+ const previewOrigin = originForLocale(activeLocale);
189
+ // No hostname, no canonical. The path alone is not a fallback: printed here
190
+ // it reads as a URL on the hostname this screen is being served from, and an
191
+ // unmapped locale has no public hostname at all.
192
+ const previewCanonical = previewOrigin
193
+ ? pageSeoPreviewCanonical({ path: previewPath, origin: previewOrigin })
194
+ : null;
178
195
 
179
196
  return (
180
197
  <div>
@@ -344,7 +361,15 @@ export function PageSeoPanelView({
344
361
  <div className="text-[0.65rem] uppercase tracking-[0.22em] text-ink/55">Preview</div>
345
362
  <dl className="mt-4 grid grid-cols-1 md:grid-cols-[140px_1fr] gap-x-6 gap-y-2 text-sm">
346
363
  <dt className="text-ink/50">Canonical</dt>
347
- <dd className="font-mono text-ink/80 break-all">{previewCanonical}</dd>
364
+ <dd className="text-ink/80 break-all">
365
+ {previewCanonical ? (
366
+ <span className="font-mono">{previewCanonical}</span>
367
+ ) : (
368
+ <span className="text-ink/55">
369
+ No public URL — no domain configured for this language
370
+ </span>
371
+ )}
372
+ </dd>
348
373
  <dt className="text-ink/50">Route</dt>
349
374
  <dd className="font-mono text-ink/80">
350
375
  {previewPath} (current registry: {registryRoute(activeLocale)})
@@ -210,7 +210,17 @@ export function pageSeoPreviewPath(args: {
210
210
  return slug === "" ? `/${args.locale}` : `/${args.locale}/${slug}`;
211
211
  }
212
212
 
213
- /** Absolute canonical preview URL; falls back to the path when no domain is known. */
213
+ /**
214
+ * Absolute canonical preview URL for a page path on a known public origin.
215
+ *
216
+ * With no origin there is no URL to build, and the bare path returned instead
217
+ * is not one either: shown under a heading like "Canonical" it reads as an
218
+ * address on whatever hostname is being browsed — which, for a locale with no
219
+ * configured domain, is the backoffice rather than a public site. Deciding what
220
+ * an empty origin means belongs to the caller; `PageSeoPanelView` asks first
221
+ * and says so in words rather than printing this. The fallback is kept for
222
+ * callers that already depend on it.
223
+ */
214
224
  export function pageSeoPreviewCanonical(args: { path: string; origin?: string | null }): string {
215
225
  return args.origin ? `https://${args.origin}${args.path}` : args.path;
216
226
  }