@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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
/**
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
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="
|
|
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
|
-
/**
|
|
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
|
}
|