@bygghammar/cms-platform 0.91.0 → 0.92.0
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 +151 -0
- package/README.md +28 -4
- package/docs/NEW-SITE.md +29 -14
- package/docs/PAGES.md +79 -21
- package/docs/PUBLICATION-REVIEW.md +298 -0
- package/migrations/20260905120000_da2db0b2-6048-4aa8-8e4f-13ee33e9cc24.sql +3298 -0
- package/package.json +8 -2
- package/scripts/cms-review.mjs +832 -0
- package/scripts/review-fingerprint.sql +43 -0
- package/scripts/review-scan.sql +56 -0
- package/src/backup/datasets.ts +203 -2
- package/src/backup/restore-core.ts +382 -11
- package/src/platform/contract.ts +82 -60
- package/src/platform/digest.ts +203 -0
- package/src/platform/migration-manifest.ts +310 -0
- package/src/platform/publication-review.ts +413 -0
- package/src/server/backup-core.ts +291 -18
- package/src/server/page-seo-core.ts +56 -10
- package/src/server/pages-core.ts +79 -46
- package/src/server/redirects-core.ts +51 -9
- package/src/server/schema-drift.ts +21 -65
- package/src/site/route-index.ts +641 -0
- package/src/site/routes.ts +2 -8
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { auditAs } from "./audit-core";
|
|
10
|
+
import { normalizeRoutePath, servedPath, type RoutePathPolicy } from "../site/route-index";
|
|
10
11
|
|
|
11
12
|
/** Structural subset of a Supabase-shaped client used by these cores. */
|
|
12
13
|
export type RedirectClient = {
|
|
@@ -22,7 +23,7 @@ export type RedirectRow = {
|
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
export type PathResolution =
|
|
25
|
-
| { kind: "page"; pageKey: string }
|
|
26
|
+
| { kind: "page"; pageKey: string; locale: string }
|
|
26
27
|
| { kind: "redirect"; to: string; status: number }
|
|
27
28
|
| { kind: "notfound" };
|
|
28
29
|
|
|
@@ -106,6 +107,23 @@ export type PageSlugRegistry = Record<string, { slug: Record<string, string | un
|
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
109
|
* Resolve a URL path to a CMS page key, a redirect, or nothing.
|
|
110
|
+
*
|
|
111
|
+
* `locale` is the locale the host worked out from the domain; a locale prefix
|
|
112
|
+
* in the path overrides it, because the address the visitor typed is the
|
|
113
|
+
* address they asked for. The answer carries the locale it resolved under, so
|
|
114
|
+
* the caller renders the page in the language the URL named rather than the one
|
|
115
|
+
* the host guessed.
|
|
116
|
+
*
|
|
117
|
+
* Slugs are compared as addresses, not as strings: both sides go through
|
|
118
|
+
* `servedPath` and `normalizeRoutePath`, the same pair the site builds its own
|
|
119
|
+
* links with. Before this, every locale's slugs went into one flat map and the
|
|
120
|
+
* first one entered won, so a Swedish and an English page sharing a slug were
|
|
121
|
+
* one page and the loser was unreachable — and `/Om-Oss` reached nothing at all.
|
|
122
|
+
*
|
|
123
|
+
* Within the resolved locale a slug answers both prefixed and bare, because a
|
|
124
|
+
* locale served from its own domain has no prefix to strip. Across locales
|
|
125
|
+
* nothing is shared, which is the whole point.
|
|
126
|
+
*
|
|
109
127
|
* Slug overrides from `page_seo` win over the static registry; builder-created
|
|
110
128
|
* pages resolve by bare key through the `lookupPageByKey` callback.
|
|
111
129
|
*/
|
|
@@ -115,15 +133,31 @@ export async function resolvePathCore(
|
|
|
115
133
|
client: RedirectClient;
|
|
116
134
|
pages: PageSlugRegistry;
|
|
117
135
|
locales: readonly string[];
|
|
136
|
+
locale: string;
|
|
137
|
+
defaultLocale: string;
|
|
118
138
|
lookupPageByKey: (key: string) => Promise<string | null>;
|
|
139
|
+
policy?: RoutePathPolicy;
|
|
119
140
|
},
|
|
120
141
|
): Promise<PathResolution> {
|
|
121
|
-
const
|
|
142
|
+
const policy: RoutePathPolicy = { locales: deps.locales, ...(deps.policy ?? {}) };
|
|
143
|
+
const target = normalizeRoutePath(path, policy);
|
|
144
|
+
|
|
145
|
+
// The prefix in the URL beats the locale the host derived from the domain.
|
|
146
|
+
const first = target.split("/")[1] ?? "";
|
|
147
|
+
const prefixed = deps.locales.find((l) => l.toLowerCase() === first.toLowerCase());
|
|
148
|
+
const locale =
|
|
149
|
+
prefixed ?? (deps.locales.includes(deps.locale) ? deps.locale : deps.defaultLocale);
|
|
150
|
+
|
|
122
151
|
const bySlug = new Map<string, string>();
|
|
123
152
|
const put = (slug: string | undefined, key: string) => {
|
|
124
153
|
if (slug === undefined || slug === null) return;
|
|
125
|
-
const
|
|
126
|
-
|
|
154
|
+
for (const candidate of [
|
|
155
|
+
servedPath(slug, locale, deps.defaultLocale),
|
|
156
|
+
servedPath(slug, deps.defaultLocale, deps.defaultLocale),
|
|
157
|
+
]) {
|
|
158
|
+
const p = normalizeRoutePath(candidate, policy);
|
|
159
|
+
if (!bySlug.has(p)) bySlug.set(p, key);
|
|
160
|
+
}
|
|
127
161
|
};
|
|
128
162
|
|
|
129
163
|
const { data: seoRows } = await deps.client.from("page_seo").select("page_key, slugs");
|
|
@@ -131,27 +165,35 @@ export async function resolvePathCore(
|
|
|
131
165
|
for (const row of (seoRows ?? []) as { page_key: string; slugs: unknown }[]) {
|
|
132
166
|
overrides.set(row.page_key, (row.slugs as Record<string, string>) ?? {});
|
|
133
167
|
}
|
|
168
|
+
// Only this locale's slugs are consulted. A page with none falls back to its
|
|
169
|
+
// default-locale slug, exactly as path building does, so a page nobody has
|
|
170
|
+
// translated stays reachable under the prefix.
|
|
134
171
|
for (const [key, slugs] of overrides) {
|
|
135
|
-
|
|
172
|
+
put(slugs[locale] ?? slugs[deps.defaultLocale], key);
|
|
136
173
|
}
|
|
137
174
|
for (const key of Object.keys(deps.pages)) {
|
|
138
175
|
if (overrides.has(key)) continue;
|
|
139
|
-
|
|
176
|
+
const slugs = deps.pages[key].slug;
|
|
177
|
+
put(slugs[locale] ?? slugs[deps.defaultLocale], key);
|
|
140
178
|
}
|
|
141
179
|
|
|
142
180
|
let pageKey = bySlug.get(target);
|
|
143
181
|
if (!pageKey) {
|
|
144
|
-
const
|
|
182
|
+
const rest = prefixed ? target.slice(`/${prefixed}`.length) || "/" : target;
|
|
183
|
+
const candidate = rest.replace(/^\//, "");
|
|
145
184
|
if (/^[a-z0-9_-]+$/.test(candidate)) {
|
|
146
185
|
pageKey = (await deps.lookupPageByKey(candidate)) ?? undefined;
|
|
147
186
|
}
|
|
148
187
|
}
|
|
149
|
-
if (pageKey) return { kind: "page", pageKey };
|
|
188
|
+
if (pageKey) return { kind: "page", pageKey, locale };
|
|
150
189
|
|
|
190
|
+
// Redirect rows are literal strings an editor typed, matched as they were
|
|
191
|
+
// stored. Only page addresses go through the route identity; re-spelling a
|
|
192
|
+
// stored row here would look it up at an address the table does not hold.
|
|
151
193
|
const { data: hit } = await deps.client
|
|
152
194
|
.from("redirects")
|
|
153
195
|
.select("to_path, status")
|
|
154
|
-
.eq("from_path",
|
|
196
|
+
.eq("from_path", normalizePath(path))
|
|
155
197
|
.maybeSingle();
|
|
156
198
|
if (hit) return { kind: "redirect", to: hit.to_path, status: hit.status ?? 301 };
|
|
157
199
|
|
|
@@ -2,73 +2,29 @@
|
|
|
2
2
|
* Canonical schema baseline shipped with this package version.
|
|
3
3
|
*
|
|
4
4
|
* `SCHEMA_MIGRATIONS` is the ordered list of migration files under
|
|
5
|
-
* `@bygghammar/cms-platform/migrations
|
|
6
|
-
* its database has actually applied so
|
|
7
|
-
* version screen instead of surfacing as a
|
|
5
|
+
* `@bygghammar/cms-platform/migrations`, read from the generated manifest. A
|
|
6
|
+
* site compares it to the migrations its database has actually applied so
|
|
7
|
+
* drift is visible in the backoffice version screen instead of surfacing as a
|
|
8
|
+
* runtime error months later.
|
|
9
|
+
*
|
|
10
|
+
* The comparison is by name, because that is what migration tooling records.
|
|
11
|
+
* The manifest also carries each file's digest, and `platform/contract` exposes
|
|
12
|
+
* it as `migrationDigest`, for the separate question of whether a file by the
|
|
13
|
+
* right name holds the right bytes.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"20260729055312_08862d7e-0279-4d4f-8709-a59b9f068c55.sql",
|
|
23
|
-
"20260729055348_418b7e36-4877-4304-831f-290ca73e56f3.sql",
|
|
24
|
-
"20260729055358_20b467e2-4efd-42c1-bd34-773ed6b77fc5.sql",
|
|
25
|
-
"20260729061039_306859eb-a38c-44ef-af1a-6c02a8fd84db.sql",
|
|
26
|
-
"20260730065600_54c25861-3355-4600-a72f-3d67b457ecb1.sql",
|
|
27
|
-
"20260730071745_d4904314-15ff-4c58-ac66-85e7371e04b5.sql",
|
|
28
|
-
"20260730071914_48163aa3-830e-4dcc-bdd4-c35be282ab87.sql",
|
|
29
|
-
"20260730085949_bcfc3270-ea80-4617-86a3-2555ed34a280.sql",
|
|
30
|
-
"20260730095256_c977c30e-b285-4041-8804-c0bd72b6d9b3.sql",
|
|
31
|
-
"20260730100057_e341b887-eb0e-4a74-850e-0ee5ffb70d7a.sql",
|
|
32
|
-
"20260730103703_d5c4e1b7-1e2c-4dd0-be4e-185c159ade69.sql",
|
|
33
|
-
"20260730105635_3557b7da-919e-48db-8c92-8221d68d3cf9.sql",
|
|
34
|
-
"20260730120308_12ff8cfb-43f9-4070-abde-61531ad3b75f.sql",
|
|
35
|
-
"20260730125024_1baf8926-12a2-48f4-8edc-7309941309e9.sql",
|
|
36
|
-
"20260730173510_feb974d1-510e-4f52-9e20-5fd0582d8952.sql",
|
|
37
|
-
"20260730175409_9f60743f-f5e1-418c-a700-c91fb3704db8.sql",
|
|
38
|
-
"20260730181224_1c52ff16-c7a2-4ef1-a0a8-e493c2c9106f.sql",
|
|
39
|
-
"20260730183129_933adb75-f985-4d82-ad5d-213b69fd42e5.sql",
|
|
40
|
-
"20260730183918_7bc7a827-2100-49f1-bc6e-24509249350b.sql",
|
|
41
|
-
"20260730202628_7ceade5e-644c-4bc2-91b8-594133329f31.sql",
|
|
42
|
-
"20260730211056_57f42049-00b4-473b-a2cd-65a13570b8d2.sql",
|
|
43
|
-
"20260730214340_98af2e55-9031-4c11-9659-c47a37a8d6b7.sql",
|
|
44
|
-
"20260731091630_8a6f28ce-5833-4901-ab17-27648cef32ec.sql",
|
|
45
|
-
"20260731094716_4c68a1df-83ed-4ba8-8019-fa2fe2b9681e.sql",
|
|
46
|
-
"20260731191655_fef97c3c-1397-47b4-8b56-afb020298e97.sql",
|
|
47
|
-
"20260731192212_95772a2f-19a1-4013-b463-2eb161e96737.sql",
|
|
48
|
-
"20260731192815_c490653e-1232-4ad9-a150-3c83d2b90f5e.sql",
|
|
49
|
-
"20260731193135_7966e740-5e86-4b14-8e1d-5119069666ba.sql",
|
|
50
|
-
"20260731193245_599790d0-ca5e-4b5d-a3e7-d69b2a79e1a3.sql",
|
|
51
|
-
"20260731212959_a748cb0d-4dc7-45dc-be90-f6bacbfab26c.sql",
|
|
52
|
-
"20260801003057_1195f4c1-fb54-42dc-9718-638a25e6a82e.sql",
|
|
53
|
-
"20260801003300_fc591960-cb41-41cf-837d-2eef23b29ca9.sql",
|
|
54
|
-
"20260802151445_a56b0023-eaf7-41e2-a7fb-d4beddfc587c.sql",
|
|
55
|
-
"20260802202621_d661af8a-8513-4b54-b375-f963ae824c8f.sql",
|
|
56
|
-
"20260802202702_c30cd837-744c-4641-85f1-edd3a7ebbcb6.sql",
|
|
57
|
-
"20260802202720_4418d18d-c851-42dc-9345-78fc330128ae.sql",
|
|
58
|
-
"20260803132514_78ba1f53-c85b-4457-96b1-7d49c5c8b494.sql",
|
|
59
|
-
"20260803181201_28c8aa09-baa8-431c-8338-90b1af9c8172.sql",
|
|
60
|
-
"20260803201518_6e8a6028-b70f-4a7d-8bc9-95c86304e7ae.sql",
|
|
61
|
-
"20260803201535_32a9a213-cb92-4035-b8cd-8211ca34af68.sql",
|
|
62
|
-
"20260811103505_29df753d-14fa-4a05-8fea-f4207785fba3.sql",
|
|
63
|
-
"20260811130318_7cb95e95-9baa-40af-8f3f-de1d4e09cec4.sql",
|
|
64
|
-
"20260818040722_1bdf1b8f-7fb0-4ae8-8ecf-73f1fadff3c2.sql",
|
|
65
|
-
"20260818040808_69a5e832-57e8-4aa7-aff8-7fd7777faa7c.sql",
|
|
66
|
-
"20260818040839_e6523163-bbf5-4a39-9233-91759059569a.sql",
|
|
67
|
-
"20260902090000_dc072b25-8467-46ae-aef3-0ccdf40aa7eb.sql",
|
|
68
|
-
"20260904090000_c54603f8-a155-40f8-a86c-54434aa5c0ae.sql",
|
|
69
|
-
"20260904140000_9f2a7c41-06d5-4d6b-8b4e-1c7a5d3e9b20.sql",
|
|
70
|
-
"20260904160000_0d565000-2839-4738-aacc-537d7090398f.sql",
|
|
71
|
-
];
|
|
16
|
+
import { MIGRATION_NAMES } from "../platform/migration-manifest";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Every canonical migration filename, oldest first.
|
|
20
|
+
*
|
|
21
|
+
* Derived from the generated catalog, which is written from the `migrations/`
|
|
22
|
+
* directory itself. This module and `platform/contract` used to hold separate
|
|
23
|
+
* copies of the same sixty names; a site that trusts one and displays the
|
|
24
|
+
* other must not be able to see two answers, and equality by construction is a
|
|
25
|
+
* stronger guarantee than a test noticing they had already diverged.
|
|
26
|
+
*/
|
|
27
|
+
export const SCHEMA_MIGRATIONS: readonly string[] = MIGRATION_NAMES;
|
|
72
28
|
|
|
73
29
|
/** Schema baseline version = the newest canonical migration. */
|
|
74
30
|
export const SCHEMA_BASELINE = SCHEMA_MIGRATIONS[SCHEMA_MIGRATIONS.length - 1] ?? "";
|