@getquick/content 0.1.0 → 0.3.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/README.md +155 -6
- package/dist/consent-astro.js +1 -1
- package/dist/consent.js +1 -1
- package/dist/dist-D4-CMkrf.js +7047 -0
- package/dist/index.d.ts +20 -4
- package/dist/index.js +24 -5
- package/dist/metadata-content.d.ts +36 -0
- package/dist/metadata-content.js +51 -0
- package/dist/metadata-testing.d.ts +34 -0
- package/dist/metadata-testing.js +142 -0
- package/dist/metadata.d.ts +94 -0
- package/dist/metadata.js +218 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +943 -58
- package/dist/{formatting-wOk2jz6J.js → site-language-cBP8SFrS.js} +302 -50
- package/dist/{version-Dn6VLZJO.js → version-D_Ao31D3.js} +1 -1
- package/dist/wordpress-D_HG2rEt.d.ts +606 -0
- package/package.json +13 -1
- package/dist/wordpress-CPL40-vq.d.ts +0 -318
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as uriPath, a as siteLanguages, b as parseWordPressBlocks, c as parseDesignBlocks, d as decodeEntities, g as stripHtml, h as routeOf, i as routeLanguage, l as renderDesignPattern, n as isLanguageHome, p as menuItemHref, r as languageSubject, u as cmsTime, x as renderWordPressBlocks, y as hasVideoHero } from "./site-language-cBP8SFrS.js";
|
|
2
2
|
import { t as bearerMatches } from "./bearer-DMOCoSci.js";
|
|
3
3
|
import { _ as consentConfiguration } from "./state-BYWc5TUB.js";
|
|
4
4
|
import { i as rewriteEmbeds, l as parseLegalLanguage } from "./embeds-p9SyGNZI.js";
|
|
@@ -3536,7 +3536,7 @@ const unionType = ZodUnion.create;
|
|
|
3536
3536
|
ZodDiscriminatedUnion.create;
|
|
3537
3537
|
ZodIntersection.create;
|
|
3538
3538
|
ZodTuple.create;
|
|
3539
|
-
ZodRecord.create;
|
|
3539
|
+
const recordType = ZodRecord.create;
|
|
3540
3540
|
ZodMap.create;
|
|
3541
3541
|
ZodSet.create;
|
|
3542
3542
|
ZodFunction.create;
|
|
@@ -3550,6 +3550,7 @@ ZodOptional.create;
|
|
|
3550
3550
|
ZodNullable.create;
|
|
3551
3551
|
ZodEffects.createWithPreprocess;
|
|
3552
3552
|
ZodPipeline.create;
|
|
3553
|
+
const NEVER = INVALID;
|
|
3553
3554
|
/** The store couldn't be read or written. Says nothing about the content. */
|
|
3554
3555
|
var StoreFailure = class extends Error {};
|
|
3555
3556
|
const eventOf = (row) => ({
|
|
@@ -3736,7 +3737,8 @@ function publicationStore(db) {
|
|
|
3736
3737
|
RETURNING event_id`).bind(event.nodeId, event.id, event.occurredAt).first()))?.event_id ?? null;
|
|
3737
3738
|
},
|
|
3738
3739
|
async entryIndex() {
|
|
3739
|
-
const [entries, withdrawals] = await guard("be read", () => db.batch([db.prepare(`SELECT p.key, p.state, p.node_id, p.modified_at,
|
|
3740
|
+
const [entries, withdrawals] = await guard("be read", () => db.batch([db.prepare(`SELECT p.key, p.state, p.node_id, p.modified_at, p.read_started_at,
|
|
3741
|
+
coalesce(a.attempted_at, 0) AS attempted_at
|
|
3740
3742
|
FROM publications p LEFT JOIN refresh_attempts a ON a.key = p.key
|
|
3741
3743
|
WHERE substr(p.key, 1, 6) = 'entry:'
|
|
3742
3744
|
ORDER BY p.key`), db.prepare("SELECT node_id, withdrawn_at FROM withdrawals WHERE republished_at IS NULL")]));
|
|
@@ -3746,6 +3748,7 @@ function publicationStore(db) {
|
|
|
3746
3748
|
state: row.state,
|
|
3747
3749
|
nodeId: row.node_id,
|
|
3748
3750
|
modifiedAt: row.modified_at,
|
|
3751
|
+
readStartedAt: row.read_started_at,
|
|
3749
3752
|
attemptedAt: row.attempted_at
|
|
3750
3753
|
})),
|
|
3751
3754
|
withdrawals: new Map(withdrawals.results.map((row) => [row.node_id, row.withdrawn_at]))
|
|
@@ -3790,7 +3793,7 @@ function publicationStore(db) {
|
|
|
3790
3793
|
},
|
|
3791
3794
|
async status() {
|
|
3792
3795
|
const [shared, entries, withdrawals, failed] = await guard("be read", () => db.batch([
|
|
3793
|
-
db.prepare("SELECT key, state, format FROM publications WHERE key IN ('home', 'chrome', 'design')"),
|
|
3796
|
+
db.prepare("SELECT key, state, format FROM publications WHERE key IN ('home', 'chrome', 'design', 'archives')"),
|
|
3794
3797
|
db.prepare(`SELECT CASE WHEN format = ?1 THEN state ELSE 'unusable' END AS state, count(*) AS count
|
|
3795
3798
|
FROM publications WHERE substr(key, 1, 6) = 'entry:' GROUP BY 1`).bind(1),
|
|
3796
3799
|
db.prepare("SELECT count(*) AS count FROM withdrawals WHERE republished_at IS NULL"),
|
|
@@ -3807,11 +3810,47 @@ function publicationStore(db) {
|
|
|
3807
3810
|
home: state("home"),
|
|
3808
3811
|
chrome: state("chrome"),
|
|
3809
3812
|
design: state("design"),
|
|
3813
|
+
...state("archives") === null ? {} : { archives: state("archives") },
|
|
3810
3814
|
entries: Object.fromEntries(entries.results.map((row) => [row.state, Number(row.count)])),
|
|
3811
3815
|
withdrawals: count(withdrawals),
|
|
3812
3816
|
failedEvents: count(failed)
|
|
3813
3817
|
};
|
|
3814
3818
|
},
|
|
3819
|
+
async listItems(filter, limit, offset) {
|
|
3820
|
+
const { where, values } = itemsWhere(filter);
|
|
3821
|
+
const { results } = await guard("be read", () => db.prepare(`SELECT key, body, modified_at, count(*) OVER () AS total FROM publications p
|
|
3822
|
+
WHERE ${where}
|
|
3823
|
+
ORDER BY json_extract(p.body, '$.date') DESC, p.key
|
|
3824
|
+
LIMIT ? OFFSET ?`).bind(...values, limit, offset).all());
|
|
3825
|
+
let total = Number(results[0]?.total ?? 0);
|
|
3826
|
+
if (results.length === 0 && offset > 0) {
|
|
3827
|
+
const row = await guard("be read", () => db.prepare(`SELECT count(*) AS total FROM publications p WHERE ${where}`).bind(...values).first());
|
|
3828
|
+
total = Number(row?.total ?? 0);
|
|
3829
|
+
}
|
|
3830
|
+
return {
|
|
3831
|
+
items: results.map((row) => ({
|
|
3832
|
+
key: row.key,
|
|
3833
|
+
body: row.body,
|
|
3834
|
+
modifiedAt: row.modified_at
|
|
3835
|
+
})),
|
|
3836
|
+
total
|
|
3837
|
+
};
|
|
3838
|
+
},
|
|
3839
|
+
async itemTerms(filter, path) {
|
|
3840
|
+
const { where, values } = itemsWhere(filter);
|
|
3841
|
+
const { results } = await guard("be read", () => db.prepare(`SELECT json_extract(t.value, '$.slug') AS slug,
|
|
3842
|
+
max(json_extract(t.value, '$.name')) AS name,
|
|
3843
|
+
count(DISTINCT p.key) AS count
|
|
3844
|
+
FROM publications p, json_each(p.body, ?) AS t
|
|
3845
|
+
WHERE ${where} AND json_type(t.value, '$.slug') = 'text'
|
|
3846
|
+
GROUP BY 1
|
|
3847
|
+
ORDER BY 2 COLLATE NOCASE, 1`).bind(path, ...values).all());
|
|
3848
|
+
return results.map((row) => ({
|
|
3849
|
+
slug: row.slug,
|
|
3850
|
+
name: row.name ?? row.slug,
|
|
3851
|
+
count: Number(row.count)
|
|
3852
|
+
}));
|
|
3853
|
+
},
|
|
3815
3854
|
async startEvent(id) {
|
|
3816
3855
|
await guard("record an event", () => db.prepare("UPDATE publication_events SET attempts = attempts + 1 WHERE id = ?").bind(id).run());
|
|
3817
3856
|
},
|
|
@@ -3826,6 +3865,33 @@ function publicationStore(db) {
|
|
|
3826
3865
|
}
|
|
3827
3866
|
};
|
|
3828
3867
|
}
|
|
3868
|
+
/** The WHERE clause (on `publications p`) for a collection's items, and its values in order. */
|
|
3869
|
+
function itemsWhere(filter) {
|
|
3870
|
+
const clauses = [
|
|
3871
|
+
"substr(p.key, 1, 6) = 'entry:'",
|
|
3872
|
+
"p.state = 'published'",
|
|
3873
|
+
"p.format = ?",
|
|
3874
|
+
"json_extract(p.body, '$.collection.name') = ?"
|
|
3875
|
+
];
|
|
3876
|
+
const values = [1, filter.collection];
|
|
3877
|
+
if (filter.language !== null) {
|
|
3878
|
+
clauses.push("coalesce(json_extract(p.body, '$.language'), ?) = ?");
|
|
3879
|
+
values.push(filter.defaultLanguage, filter.language);
|
|
3880
|
+
}
|
|
3881
|
+
if (filter.search !== null) {
|
|
3882
|
+
clauses.push("instr(json_extract(p.body, '$.collection.search'), ?) > 0");
|
|
3883
|
+
values.push(filter.search);
|
|
3884
|
+
}
|
|
3885
|
+
for (const { path, slugs } of filter.terms) {
|
|
3886
|
+
clauses.push(`EXISTS (SELECT 1 FROM json_each(p.body, ?) AS term
|
|
3887
|
+
WHERE json_extract(term.value, '$.slug') IN (SELECT slug.value FROM json_each(?) AS slug))`);
|
|
3888
|
+
values.push(path, JSON.stringify(slugs));
|
|
3889
|
+
}
|
|
3890
|
+
return {
|
|
3891
|
+
where: clauses.join(" AND "),
|
|
3892
|
+
values
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3829
3895
|
function movedTo(body) {
|
|
3830
3896
|
try {
|
|
3831
3897
|
const uri = JSON.parse(body ?? "null")?.uri;
|
|
@@ -3836,7 +3902,7 @@ function movedTo(body) {
|
|
|
3836
3902
|
}
|
|
3837
3903
|
//#endregion
|
|
3838
3904
|
//#region frontend/src/reconciliation.ts
|
|
3839
|
-
function configuredReconciliation(languages, delivery, wordpress) {
|
|
3905
|
+
function configuredReconciliation(languages, delivery, wordpress, collections = false) {
|
|
3840
3906
|
const { eachLimited, reconcileShared, refreshEntries, sharedOutcomes } = delivery;
|
|
3841
3907
|
const { getPublishedEntries } = wordpress;
|
|
3842
3908
|
const isLanguageHome$4 = (route) => isLanguageHome(route, languages);
|
|
@@ -3847,10 +3913,24 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3847
3913
|
* is asked again without them. Chrome and design have no block recovery.
|
|
3848
3914
|
*/
|
|
3849
3915
|
const REQUESTS_PER_READ = 2;
|
|
3850
|
-
/**
|
|
3851
|
-
|
|
3852
|
-
|
|
3916
|
+
/**
|
|
3917
|
+
* Reserve design once (and the archives, on a Site with collections), and
|
|
3918
|
+
* each language's home (including recovery) and chrome.
|
|
3919
|
+
*/
|
|
3920
|
+
const SHARED_REQUESTS = 1 + (collections ? 2 : 0) + 3 * languages.length;
|
|
3921
|
+
/**
|
|
3922
|
+
* Stored entries re-read each run, when the budget allows, though WordPress
|
|
3923
|
+
* lists them unchanged: published ones, and listed routes settled missing.
|
|
3924
|
+
*/
|
|
3853
3925
|
const VERIFIED_PER_RUN = 2;
|
|
3926
|
+
/**
|
|
3927
|
+
* How much later than an entry's modification time a read must have started
|
|
3928
|
+
* for the missing row it stored to count as settled. The read's start is
|
|
3929
|
+
* the Frontend's clock (ms) and the modification time the CMS's (floored to
|
|
3930
|
+
* the second): the margin covers that rounding and any skew between the two
|
|
3931
|
+
* hosts, so a change made just after a read isn't taken as already read.
|
|
3932
|
+
*/
|
|
3933
|
+
const SETTLED_CLOCK_MARGIN_MS = 3e5;
|
|
3854
3934
|
/** How long a run holds the lease; an interrupted run's lease is taken over after it. */
|
|
3855
3935
|
const LEASE_MS = 12e4;
|
|
3856
3936
|
const ENTRY = "entry:";
|
|
@@ -3903,7 +3983,7 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3903
3983
|
}
|
|
3904
3984
|
}
|
|
3905
3985
|
async function run(store, startedAt, siteOrigin, publish) {
|
|
3906
|
-
const [shared, listing] = await Promise.all([reconcileShared(store, siteOrigin), getPublishedEntries()]);
|
|
3986
|
+
const [{ archiveRoutes = [], ...shared }, listing] = await Promise.all([reconcileShared(store, siteOrigin), getPublishedEntries()]);
|
|
3907
3987
|
const failures = [];
|
|
3908
3988
|
let changed = 0;
|
|
3909
3989
|
for (const outcome of sharedOutcomes(shared)) {
|
|
@@ -3930,9 +4010,10 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3930
4010
|
}
|
|
3931
4011
|
const { entries: listed, requests } = listing.content;
|
|
3932
4012
|
const index = await store.entryIndex();
|
|
3933
|
-
const found = candidates(listed, index);
|
|
4013
|
+
const { found, settled } = candidates(listed, index);
|
|
3934
4014
|
let budget = CMS_REQUEST_BUDGET - SHARED_REQUESTS - requests;
|
|
3935
4015
|
const chosen = [];
|
|
4016
|
+
found.unshift(...archived(archiveRoutes, found));
|
|
3936
4017
|
let pending = 0;
|
|
3937
4018
|
for (const candidate of found) {
|
|
3938
4019
|
const cost = (candidate.previousRoute ? 2 : 1) * REQUESTS_PER_READ;
|
|
@@ -3943,7 +4024,7 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3943
4024
|
budget -= cost;
|
|
3944
4025
|
chosen.push(candidate);
|
|
3945
4026
|
}
|
|
3946
|
-
if (pending === 0) chosen.push(...verified(index, found, Math.floor(budget / REQUESTS_PER_READ)));
|
|
4027
|
+
if (pending === 0) chosen.push(...verified(index, found, settled, Math.floor(budget / REQUESTS_PER_READ)));
|
|
3947
4028
|
const entries = await refresh(store, chosen, publish);
|
|
3948
4029
|
for (const outcome of Object.values(entries)) {
|
|
3949
4030
|
if (outcome.outcome === "kept") failures.push(outcome.failure);
|
|
@@ -3966,7 +4047,8 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3966
4047
|
};
|
|
3967
4048
|
}
|
|
3968
4049
|
/**
|
|
3969
|
-
* What differs between WordPress's list and the store,
|
|
4050
|
+
* What differs between WordPress's list and the store (found),
|
|
4051
|
+
* oldest-attempted first, and the listed routes settled missing (settled).
|
|
3970
4052
|
* WordPress's front page (`/`) is the homepage's, reconciled with the shared
|
|
3971
4053
|
* rows, unless it is withdrawn and modified since.
|
|
3972
4054
|
*/
|
|
@@ -3978,6 +4060,7 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3978
4060
|
storedAt.set(row.nodeId, [...storedAt.get(row.nodeId) ?? [], row.key.slice(6)]);
|
|
3979
4061
|
}
|
|
3980
4062
|
const found = [];
|
|
4063
|
+
const settled = /* @__PURE__ */ new Set();
|
|
3981
4064
|
const listedRoutes = /* @__PURE__ */ new Set();
|
|
3982
4065
|
const left = /* @__PURE__ */ new Set();
|
|
3983
4066
|
for (const entry of listed) {
|
|
@@ -3998,6 +4081,10 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3998
4081
|
if (isLanguageHome$4(route)) continue;
|
|
3999
4082
|
if (row?.state === "published" && entry.id !== null && row.nodeId === entry.id && entry.modifiedAt !== null && row.modifiedAt === entry.modifiedAt) continue;
|
|
4000
4083
|
const previousRoute = entry.id === null ? void 0 : storedAt.get(entry.id)?.find((stored) => stored !== route);
|
|
4084
|
+
if (row?.state === "missing" && !previousRoute && entry.modifiedAt !== null && row.readStartedAt - SETTLED_CLOCK_MARGIN_MS > entry.modifiedAt) {
|
|
4085
|
+
settled.add(route);
|
|
4086
|
+
continue;
|
|
4087
|
+
}
|
|
4001
4088
|
if (previousRoute) left.add(previousRoute);
|
|
4002
4089
|
found.push({
|
|
4003
4090
|
change: previousRoute ? "moved" : row ? "changed" : "new",
|
|
@@ -4016,12 +4103,33 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
4016
4103
|
attemptedAt: row.attemptedAt
|
|
4017
4104
|
});
|
|
4018
4105
|
}
|
|
4019
|
-
|
|
4106
|
+
found.sort((a, b) => a.attemptedAt - b.attemptedAt || (a.route < b.route ? -1 : a.route > b.route ? 1 : 0));
|
|
4107
|
+
return {
|
|
4108
|
+
found,
|
|
4109
|
+
settled
|
|
4110
|
+
};
|
|
4020
4111
|
}
|
|
4021
|
-
/**
|
|
4022
|
-
function
|
|
4112
|
+
/** The addresses whose archive moved, unless already a change, as changes. */
|
|
4113
|
+
function archived(routes, found) {
|
|
4023
4114
|
const changing = new Set(found.flatMap(({ route, previousRoute }) => [route, previousRoute]));
|
|
4024
|
-
return
|
|
4115
|
+
return routes.filter((route) => !changing.has(route) && !isLanguageHome$4(route)).map((route) => ({
|
|
4116
|
+
change: "archive",
|
|
4117
|
+
route,
|
|
4118
|
+
attemptedAt: 0
|
|
4119
|
+
}));
|
|
4120
|
+
}
|
|
4121
|
+
/**
|
|
4122
|
+
* Up to `limit` (and VERIFIED_PER_RUN) stored entries no change was found
|
|
4123
|
+
* in, published or settled missing, oldest-attempted first. Other missing
|
|
4124
|
+
* rows (a 404 looked up at an address WordPress doesn't list) aren't re-read.
|
|
4125
|
+
*/
|
|
4126
|
+
function verified(index, found, settled, limit) {
|
|
4127
|
+
const changing = new Set(found.flatMap(({ route, previousRoute }) => [route, previousRoute]));
|
|
4128
|
+
return index.entries.filter((row) => {
|
|
4129
|
+
const route = row.key.slice(6);
|
|
4130
|
+
if (changing.has(route)) return false;
|
|
4131
|
+
return row.state === "published" || row.state === "missing" && settled.has(route);
|
|
4132
|
+
}).sort((a, b) => a.attemptedAt - b.attemptedAt).slice(0, Math.max(0, Math.min(limit, VERIFIED_PER_RUN))).map((row) => ({
|
|
4025
4133
|
change: "verified",
|
|
4026
4134
|
route: row.key.slice(6),
|
|
4027
4135
|
attemptedAt: row.attemptedAt
|
|
@@ -4052,11 +4160,11 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
4052
4160
|
};
|
|
4053
4161
|
try {
|
|
4054
4162
|
const { body } = await publish(store, publication);
|
|
4055
|
-
if (body.status === "refreshed") outcomes[candidate.route] = {
|
|
4163
|
+
if (body.status === "refreshed" && !body.duplicate) outcomes[candidate.route] = {
|
|
4056
4164
|
change: candidate.change,
|
|
4057
4165
|
outcome: "refreshed"
|
|
4058
4166
|
};
|
|
4059
|
-
else if (body.status === "superseded") superseded.push(candidate);
|
|
4167
|
+
else if (body.status === "refreshed" || body.status === "superseded") superseded.push(candidate);
|
|
4060
4168
|
else outcomes[candidate.route] = {
|
|
4061
4169
|
change: candidate.change,
|
|
4062
4170
|
outcome: "kept",
|
|
@@ -4133,7 +4241,7 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
4133
4241
|
//#endregion
|
|
4134
4242
|
//#region frontend/src/events.ts
|
|
4135
4243
|
function configuredEvents(config, languages, delivery, wordpress) {
|
|
4136
|
-
const { reconcile } = configuredReconciliation(languages, delivery, wordpress);
|
|
4244
|
+
const { reconcile } = configuredReconciliation(languages, delivery, wordpress, Object.keys(config.collections ?? {}).length > 0);
|
|
4137
4245
|
const SITE = config.site;
|
|
4138
4246
|
const isLanguageHome$3 = (route) => isLanguageHome(route, languages);
|
|
4139
4247
|
const routeLanguage$3 = (route) => routeLanguage(route, languages);
|
|
@@ -4210,15 +4318,19 @@ function configuredEvents(config, languages, delivery, wordpress) {
|
|
|
4210
4318
|
* The shared settings an event can name, and what each change refreshes: the
|
|
4211
4319
|
* menus and the logo are in the chrome every page is served with; the site's
|
|
4212
4320
|
* identity is its title and tagline (the front page's) and its icon (the
|
|
4213
|
-
* chrome's); the design presets are the shared design every page uses
|
|
4321
|
+
* chrome's); the design presets are the shared design every page uses; the
|
|
4322
|
+
* patterns are GQ Design's synced patterns and Header & Footer selection,
|
|
4323
|
+
* the chrome's header and footer.
|
|
4214
4324
|
*/
|
|
4215
4325
|
const SETTINGS = {
|
|
4216
4326
|
menus: ["chrome"],
|
|
4217
4327
|
logo: ["chrome"],
|
|
4218
4328
|
identity: ["home", "chrome"],
|
|
4219
4329
|
design: ["design"],
|
|
4330
|
+
patterns: ["chrome"],
|
|
4220
4331
|
legal: ["chrome"],
|
|
4221
|
-
analytics: ["chrome"]
|
|
4332
|
+
analytics: ["chrome"],
|
|
4333
|
+
archives: ["archives"]
|
|
4222
4334
|
};
|
|
4223
4335
|
/**
|
|
4224
4336
|
* What a change in one language refreshes, for the settings a language has
|
|
@@ -4564,7 +4676,7 @@ function configuredEvents(config, languages, delivery, wordpress) {
|
|
|
4564
4676
|
await store.startEvent(event.id);
|
|
4565
4677
|
const language = languages.find((each) => each.slug === event.language);
|
|
4566
4678
|
const { refreshed, ...parts } = await refreshShared(store, language ? [...LANGUAGE_SETTINGS[event.setting]] : [...SETTINGS[event.setting]], siteOrigin, language);
|
|
4567
|
-
const failure = sharedOutcomes(parts).find((outcome) => outcome.outcome === "kept");
|
|
4679
|
+
const failure = [...sharedOutcomes(parts), ...Object.values(parts.entries ?? {})].find((outcome) => outcome.outcome === "kept");
|
|
4568
4680
|
await store.finishEvent(recorded, failure?.outcome === "kept" ? {
|
|
4569
4681
|
status: "failed",
|
|
4570
4682
|
reason: failure.failure.reason,
|
|
@@ -4660,10 +4772,32 @@ function configuredRefresh(languages, delivery) {
|
|
|
4660
4772
|
function publishedHtml(languages, transform) {
|
|
4661
4773
|
const defaultLanguage = languages[0];
|
|
4662
4774
|
const languageOf = (slug, fallback) => languages.find((language) => language.slug === slug) ?? fallback;
|
|
4775
|
+
const blocks = (list, language) => list.map((block) => ({
|
|
4776
|
+
...block,
|
|
4777
|
+
content: block.content === null ? null : transform(block.content, language),
|
|
4778
|
+
children: blocks(block.children, language)
|
|
4779
|
+
}));
|
|
4780
|
+
const pattern = (value, language) => value ? {
|
|
4781
|
+
...value,
|
|
4782
|
+
blocks: blocks(value.blocks, language)
|
|
4783
|
+
} : value;
|
|
4784
|
+
/** A language's chrome, its header and footer patterns' HTML transformed. */
|
|
4785
|
+
function chrome(value, language = defaultLanguage) {
|
|
4786
|
+
if (!transform || !value || !value.header && !value.footer) return value;
|
|
4787
|
+
return {
|
|
4788
|
+
...value,
|
|
4789
|
+
header: pattern(value.header, language) ?? null,
|
|
4790
|
+
footer: pattern(value.footer, language) ?? null
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4663
4793
|
function home(delivery, language = defaultLanguage) {
|
|
4794
|
+
const served = languageOf(language.slug, defaultLanguage);
|
|
4795
|
+
if (transform && delivery.kind === "missing") return {
|
|
4796
|
+
...delivery,
|
|
4797
|
+
chrome: chrome(delivery.chrome, served)
|
|
4798
|
+
};
|
|
4664
4799
|
if (!transform || delivery.kind !== "found") return delivery;
|
|
4665
4800
|
const { home } = delivery.content;
|
|
4666
|
-
const served = languageOf(language.slug, defaultLanguage);
|
|
4667
4801
|
return {
|
|
4668
4802
|
...delivery,
|
|
4669
4803
|
content: {
|
|
@@ -4674,11 +4808,16 @@ function publishedHtml(languages, transform) {
|
|
|
4674
4808
|
...home.page,
|
|
4675
4809
|
content: transform(home.page.content, served)
|
|
4676
4810
|
}
|
|
4677
|
-
}
|
|
4811
|
+
},
|
|
4812
|
+
chrome: chrome(delivery.content.chrome, served)
|
|
4678
4813
|
}
|
|
4679
4814
|
};
|
|
4680
4815
|
}
|
|
4681
4816
|
function entry(delivery, routeLanguage) {
|
|
4817
|
+
if (transform && delivery.kind === "missing") return {
|
|
4818
|
+
...delivery,
|
|
4819
|
+
chrome: chrome(delivery.chrome, routeLanguage)
|
|
4820
|
+
};
|
|
4682
4821
|
if (!transform || delivery.kind !== "found") return delivery;
|
|
4683
4822
|
const { entry } = delivery.content;
|
|
4684
4823
|
const served = languageOf(entry.language, routeLanguage);
|
|
@@ -4689,24 +4828,28 @@ function publishedHtml(languages, transform) {
|
|
|
4689
4828
|
entry: {
|
|
4690
4829
|
...entry,
|
|
4691
4830
|
content: transform(entry.content, served)
|
|
4692
|
-
}
|
|
4831
|
+
},
|
|
4832
|
+
chrome: chrome(delivery.content.chrome, served)
|
|
4693
4833
|
}
|
|
4694
4834
|
};
|
|
4695
4835
|
}
|
|
4696
4836
|
return {
|
|
4697
4837
|
home,
|
|
4698
|
-
entry
|
|
4838
|
+
entry,
|
|
4839
|
+
chrome
|
|
4699
4840
|
};
|
|
4700
4841
|
}
|
|
4701
4842
|
//#endregion
|
|
4702
4843
|
//#region frontend/src/delivery.ts
|
|
4703
4844
|
function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
4704
4845
|
const defaultLanguage = languages[0];
|
|
4846
|
+
const collections = config.collections ?? {};
|
|
4847
|
+
const hasCollections = Object.keys(collections).length > 0;
|
|
4705
4848
|
const html = publishedHtml(languages, transformHtml);
|
|
4706
4849
|
const multilingual = languages.length > 1;
|
|
4707
4850
|
const routeLanguage$1 = (route) => routeLanguage(route, languages);
|
|
4708
4851
|
const isLanguageHome$1 = (route) => isLanguageHome(route, languages);
|
|
4709
|
-
const { getHomeContent, getEntryByUri, getSiteChrome, getDesignPresets, getPublishedRoutes } = wordpress;
|
|
4852
|
+
const { getHomeContent, getEntryByUri, getSiteChrome, getDesignPresets, getPublishedRoutes, getArchives } = wordpress;
|
|
4710
4853
|
const HOME = "home";
|
|
4711
4854
|
const CHROME = "chrome";
|
|
4712
4855
|
/**
|
|
@@ -4718,6 +4861,8 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4718
4861
|
const chromeKey = (language) => language.isDefault ? CHROME : `${CHROME}:${language.slug}`;
|
|
4719
4862
|
/** The design presets every page is served with, once a refresh has stored them. */
|
|
4720
4863
|
const DESIGN = "design";
|
|
4864
|
+
/** Where each collection's archive is, on a Site with collections. */
|
|
4865
|
+
const ARCHIVES = "archives";
|
|
4721
4866
|
/** Entries are stored at "entry:<route>". */
|
|
4722
4867
|
const ENTRY = "entry:";
|
|
4723
4868
|
const entryKey = (route) => `${ENTRY}${route}`;
|
|
@@ -4743,6 +4888,29 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4743
4888
|
slug: stringType(),
|
|
4744
4889
|
color: stringType()
|
|
4745
4890
|
}));
|
|
4891
|
+
/**
|
|
4892
|
+
* The Site's extension of a page or post, as its `parse` reads it back. A
|
|
4893
|
+
* row stored without one (before the Site declared it) still serves; an
|
|
4894
|
+
* extension stored without the Site declaring one now is left out.
|
|
4895
|
+
*/
|
|
4896
|
+
const extension = config.extension;
|
|
4897
|
+
const extended = extension ? { extension: unknownType().optional().transform((value, context) => {
|
|
4898
|
+
if (value === void 0) return void 0;
|
|
4899
|
+
let parsed;
|
|
4900
|
+
try {
|
|
4901
|
+
parsed = extension.parse(value);
|
|
4902
|
+
} catch {
|
|
4903
|
+
parsed = void 0;
|
|
4904
|
+
}
|
|
4905
|
+
if (parsed === void 0) {
|
|
4906
|
+
context.addIssue({
|
|
4907
|
+
code: "custom",
|
|
4908
|
+
message: "the Site can't serve this extension"
|
|
4909
|
+
});
|
|
4910
|
+
return NEVER;
|
|
4911
|
+
}
|
|
4912
|
+
return parsed;
|
|
4913
|
+
}) } : {};
|
|
4746
4914
|
const homePublication = objectType({
|
|
4747
4915
|
settings: objectType({
|
|
4748
4916
|
title: stringType(),
|
|
@@ -4751,7 +4919,8 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4751
4919
|
page: objectType({
|
|
4752
4920
|
title: stringType(),
|
|
4753
4921
|
content: stringType(),
|
|
4754
|
-
hasVideoHero: booleanType()
|
|
4922
|
+
hasVideoHero: booleanType(),
|
|
4923
|
+
...extended
|
|
4755
4924
|
}),
|
|
4756
4925
|
spacingSizes,
|
|
4757
4926
|
colors
|
|
@@ -4760,7 +4929,24 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4760
4929
|
uri: stringType(),
|
|
4761
4930
|
language: stringType()
|
|
4762
4931
|
});
|
|
4932
|
+
const term = objectType({
|
|
4933
|
+
slug: stringType(),
|
|
4934
|
+
name: stringType()
|
|
4935
|
+
});
|
|
4936
|
+
/**
|
|
4937
|
+
* The collection an item is in, with its terms; stored with the text a
|
|
4938
|
+
* search matches (`search`), which isn't served.
|
|
4939
|
+
*/
|
|
4940
|
+
const membership = objectType({
|
|
4941
|
+
name: stringType(),
|
|
4942
|
+
terms: recordType(arrayType(term)),
|
|
4943
|
+
search: stringType().optional()
|
|
4944
|
+
}).transform(({ name, terms }) => ({
|
|
4945
|
+
name,
|
|
4946
|
+
terms
|
|
4947
|
+
}));
|
|
4763
4948
|
const entryPublication = objectType({
|
|
4949
|
+
collection: membership.optional(),
|
|
4764
4950
|
language: stringType().optional(),
|
|
4765
4951
|
translations: arrayType(translation).optional(),
|
|
4766
4952
|
id: stringType(),
|
|
@@ -4772,7 +4958,8 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4772
4958
|
featuredImage: image,
|
|
4773
4959
|
hasVideoHero: booleanType(),
|
|
4774
4960
|
spacingSizes,
|
|
4775
|
-
colors
|
|
4961
|
+
colors,
|
|
4962
|
+
...extended
|
|
4776
4963
|
});
|
|
4777
4964
|
const legalRole = objectType({
|
|
4778
4965
|
role: stringType(),
|
|
@@ -4790,10 +4977,22 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4790
4977
|
roles: arrayType(legalRole),
|
|
4791
4978
|
missingRequired: arrayType(stringType())
|
|
4792
4979
|
}).nullable();
|
|
4980
|
+
const designBlock = lazyType(() => objectType({
|
|
4981
|
+
name: stringType().min(1),
|
|
4982
|
+
attributes: recordType(stringType(), unknownType()),
|
|
4983
|
+
content: stringType().nullable(),
|
|
4984
|
+
children: arrayType(designBlock)
|
|
4985
|
+
}));
|
|
4986
|
+
const designPattern = objectType({
|
|
4987
|
+
title: stringType(),
|
|
4988
|
+
blocks: arrayType(designBlock).min(1)
|
|
4989
|
+
}).nullable();
|
|
4793
4990
|
const siteChrome = objectType({
|
|
4794
4991
|
menuItems: arrayType(menuItem),
|
|
4795
4992
|
siteIcon: image,
|
|
4796
4993
|
siteLogo: image,
|
|
4994
|
+
header: designPattern.optional().transform((pattern) => pattern ?? null),
|
|
4995
|
+
footer: designPattern.optional().transform((pattern) => pattern ?? null),
|
|
4797
4996
|
legal: legal.optional(),
|
|
4798
4997
|
analytics: objectType({
|
|
4799
4998
|
enabled: booleanType(),
|
|
@@ -4804,6 +5003,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4804
5003
|
spacingSizes,
|
|
4805
5004
|
colors
|
|
4806
5005
|
});
|
|
5006
|
+
const archives = recordType(stringType());
|
|
4807
5007
|
const parseWith = (schema) => (body) => {
|
|
4808
5008
|
const parsed = schema.safeParse(body);
|
|
4809
5009
|
return parsed.success ? parsed.data : void 0;
|
|
@@ -4812,10 +5012,13 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4812
5012
|
const parseChrome = parseWith(siteChrome);
|
|
4813
5013
|
const parseEntry = parseWith(entryPublication);
|
|
4814
5014
|
const parseDesign = parseWith(designPresets);
|
|
5015
|
+
const parseArchives = parseWith(archives);
|
|
4815
5016
|
const emptyChrome = {
|
|
4816
5017
|
menuItems: [],
|
|
4817
5018
|
siteIcon: null,
|
|
4818
|
-
siteLogo: null
|
|
5019
|
+
siteLogo: null,
|
|
5020
|
+
header: null,
|
|
5021
|
+
footer: null
|
|
4819
5022
|
};
|
|
4820
5023
|
async function boundStore() {
|
|
4821
5024
|
const { PUBLICATION_DB } = await config.bindings();
|
|
@@ -5126,6 +5329,27 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5126
5329
|
}
|
|
5127
5330
|
}
|
|
5128
5331
|
/**
|
|
5332
|
+
* The design presets for a page that isn't a publication: the stored ones
|
|
5333
|
+
* when there is a store, else a live read in explicit development. Null
|
|
5334
|
+
* when neither has them; the page is then served without them.
|
|
5335
|
+
*/
|
|
5336
|
+
async function pageDesign() {
|
|
5337
|
+
if (!config.isServer()) return null;
|
|
5338
|
+
const store = await boundStore();
|
|
5339
|
+
if (!store) {
|
|
5340
|
+
if (!config.development) return null;
|
|
5341
|
+
const design = await getDesignPresets();
|
|
5342
|
+
return design.kind === "found" ? design.content : null;
|
|
5343
|
+
}
|
|
5344
|
+
try {
|
|
5345
|
+
return sharedDesign(await store.read(DESIGN, parseDesign));
|
|
5346
|
+
} catch (error) {
|
|
5347
|
+
if (!(error instanceof StoreFailure)) throw error;
|
|
5348
|
+
console.error(`Delivery: the design presets can't be served (store): ${error.message}`);
|
|
5349
|
+
return null;
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
/**
|
|
5129
5353
|
* Refreshes the whole Site, the explicit preparation of its store: each
|
|
5130
5354
|
* language's front page and chrome, the shared design presets, and every
|
|
5131
5355
|
* entry WordPress lists as published or the store already holds (so one
|
|
@@ -5138,16 +5362,18 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5138
5362
|
async function refreshSite(store, siteOrigin) {
|
|
5139
5363
|
const readStartedAt = Date.now();
|
|
5140
5364
|
const others = languages.filter((language) => !language.isDefault);
|
|
5141
|
-
const [home, chrome, design, listing, ...otherReads] = await Promise.all([
|
|
5365
|
+
const [home, chrome, design, archiveRead, listing, ...otherReads] = await Promise.all([
|
|
5142
5366
|
getHomeContent(),
|
|
5143
5367
|
getSiteChrome(siteOrigin),
|
|
5144
5368
|
getDesignPresets(),
|
|
5369
|
+
hasCollections ? getArchives() : void 0,
|
|
5145
5370
|
getPublishedRoutes(),
|
|
5146
5371
|
...others.map((language) => Promise.all([getHomeContent(language), getSiteChrome(siteOrigin, { language })]))
|
|
5147
5372
|
]);
|
|
5148
5373
|
const homeOutcome = await promoteHome(store, home, readStartedAt);
|
|
5149
5374
|
const chromeOutcome = await promoteChrome(store, chrome, readStartedAt);
|
|
5150
5375
|
const designOutcome = await promoteDesign(store, design, readStartedAt);
|
|
5376
|
+
const archivesOutcome = archiveRead ? await promoteArchives(store, archiveRead, readStartedAt) : void 0;
|
|
5151
5377
|
const languageOutcomes = {};
|
|
5152
5378
|
for (const [index, language] of others.entries()) {
|
|
5153
5379
|
const [languageHome, languageChrome] = otherReads[index];
|
|
@@ -5187,10 +5413,11 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5187
5413
|
}
|
|
5188
5414
|
return {
|
|
5189
5415
|
ready,
|
|
5190
|
-
refreshed: homeOutcome.outcome !== "kept" && chromeOutcome.outcome !== "kept" && designOutcome.outcome !== "kept" && Object.values(languageOutcomes).every((outcomes) => outcomes.home.outcome !== "kept" && outcomes.chrome.outcome !== "kept") && routes.outcome === "listed" && entries.refreshed,
|
|
5416
|
+
refreshed: homeOutcome.outcome !== "kept" && chromeOutcome.outcome !== "kept" && designOutcome.outcome !== "kept" && archivesOutcome?.outcome !== "kept" && Object.values(languageOutcomes).every((outcomes) => outcomes.home.outcome !== "kept" && outcomes.chrome.outcome !== "kept") && routes.outcome === "listed" && entries.refreshed,
|
|
5191
5417
|
home: homeOutcome,
|
|
5192
5418
|
chrome: chromeOutcome,
|
|
5193
5419
|
design: designOutcome,
|
|
5420
|
+
...archivesOutcome ? { archives: archivesOutcome } : {},
|
|
5194
5421
|
routes,
|
|
5195
5422
|
entries: entries.entries,
|
|
5196
5423
|
moved: entries.moved,
|
|
@@ -5203,6 +5430,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5203
5430
|
rows.home,
|
|
5204
5431
|
rows.chrome,
|
|
5205
5432
|
rows.design,
|
|
5433
|
+
rows.archives,
|
|
5206
5434
|
...Object.values(rows.languages ?? {}).flatMap((language) => [language.home, language.chrome])
|
|
5207
5435
|
].filter((outcome) => outcome !== void 0);
|
|
5208
5436
|
}
|
|
@@ -5222,7 +5450,11 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5222
5450
|
const readStartedAt = Date.now();
|
|
5223
5451
|
const wanted = new Set(parts);
|
|
5224
5452
|
const scope = language ? [language] : languages;
|
|
5225
|
-
const [reads, design] = await Promise.all([
|
|
5453
|
+
const [reads, design, archiveRead] = await Promise.all([
|
|
5454
|
+
Promise.all(scope.map((each) => Promise.all([wanted.has("home") ? getHomeContent(each) : void 0, wanted.has("chrome") ? getSiteChrome(siteOrigin, { language: each }) : void 0]))),
|
|
5455
|
+
wanted.has("design") && !language ? getDesignPresets() : void 0,
|
|
5456
|
+
wanted.has("archives") && !language && hasCollections ? getArchives() : void 0
|
|
5457
|
+
]);
|
|
5226
5458
|
const report = { refreshed: true };
|
|
5227
5459
|
for (const [index, each] of scope.entries()) {
|
|
5228
5460
|
const [home, chrome] = reads[index];
|
|
@@ -5231,7 +5463,15 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5231
5463
|
if (chrome) rows.chrome = await promoteChrome(store, chrome, readStartedAt, each);
|
|
5232
5464
|
}
|
|
5233
5465
|
if (design) report.design = await promoteDesign(store, design, readStartedAt);
|
|
5234
|
-
|
|
5466
|
+
if (archiveRead) {
|
|
5467
|
+
const before = archiveRead.kind === "found" ? await storedArchives(store) : void 0;
|
|
5468
|
+
report.archives = await promoteArchives(store, archiveRead, readStartedAt);
|
|
5469
|
+
if (archiveRead.kind === "found" && report.archives.outcome !== "kept") {
|
|
5470
|
+
const routes = archiveRoutes(before, archiveRead.content, "all");
|
|
5471
|
+
if (routes.length > 0) report.entries = (await refreshEntries(store, routes)).entries;
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
report.refreshed = [...sharedOutcomes(report), ...Object.values(report.entries ?? {})].every((outcome) => outcome.outcome !== "kept");
|
|
5235
5475
|
return report;
|
|
5236
5476
|
}
|
|
5237
5477
|
/**
|
|
@@ -5258,7 +5498,11 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5258
5498
|
*/
|
|
5259
5499
|
async function reconcileShared(store, siteOrigin) {
|
|
5260
5500
|
const readStartedAt = Date.now();
|
|
5261
|
-
const [reads, design] = await Promise.all([
|
|
5501
|
+
const [reads, design, archiveRead] = await Promise.all([
|
|
5502
|
+
Promise.all(languages.map((language) => Promise.all([getHomeContent(language), getSiteChrome(siteOrigin, { language })]))),
|
|
5503
|
+
getDesignPresets(),
|
|
5504
|
+
hasCollections ? getArchives() : void 0
|
|
5505
|
+
]);
|
|
5262
5506
|
const unchanged = async (key, parse, read) => {
|
|
5263
5507
|
if (!read) return false;
|
|
5264
5508
|
try {
|
|
@@ -5291,10 +5535,22 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5291
5535
|
chrome: await unchanged(chromeKey(language), parseChrome, sharedRead(chrome)) ? { outcome: "unchanged" } : await promoteChrome(store, chrome, readStartedAt, language)
|
|
5292
5536
|
});
|
|
5293
5537
|
}
|
|
5538
|
+
let archives;
|
|
5539
|
+
let changedArchives = [];
|
|
5540
|
+
if (archiveRead) {
|
|
5541
|
+
const before = await storedArchives(store);
|
|
5542
|
+
if (await unchanged(ARCHIVES, parseArchives, sharedRead(archiveRead))) archives = { outcome: "unchanged" };
|
|
5543
|
+
else {
|
|
5544
|
+
archives = await promoteArchives(store, archiveRead, readStartedAt);
|
|
5545
|
+
if (archiveRead.kind === "found" && archives.outcome === "promoted") changedArchives = archiveRoutes(before, archiveRead.content, "changed");
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5294
5548
|
const [own, ...others] = outcomes;
|
|
5295
5549
|
return {
|
|
5296
5550
|
...own,
|
|
5297
5551
|
design: await unchanged(DESIGN, parseDesign, sharedRead(design)) ? { outcome: "unchanged" } : await promoteDesign(store, design, readStartedAt),
|
|
5552
|
+
...archives ? { archives } : {},
|
|
5553
|
+
...changedArchives.length > 0 ? { archiveRoutes: changedArchives } : {},
|
|
5298
5554
|
...multilingual ? { languages: Object.fromEntries(others.map((outcome, index) => [languages[index + 1].slug, outcome])) } : {}
|
|
5299
5555
|
};
|
|
5300
5556
|
}
|
|
@@ -5318,6 +5574,36 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5318
5574
|
function promoteDesign(store, read, readStartedAt) {
|
|
5319
5575
|
return promoteShared(store, DESIGN, "the shared design presets", parseDesign, read, readStartedAt);
|
|
5320
5576
|
}
|
|
5577
|
+
/** Where each collection's archive is, as stored; undefined when it can't be read. */
|
|
5578
|
+
async function storedArchives(store) {
|
|
5579
|
+
try {
|
|
5580
|
+
const stored = await store.read(ARCHIVES, parseArchives);
|
|
5581
|
+
return stored?.state === "published" ? stored.content : void 0;
|
|
5582
|
+
} catch (error) {
|
|
5583
|
+
if (!(error instanceof StoreFailure)) throw error;
|
|
5584
|
+
return;
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
/**
|
|
5588
|
+
* The archive addresses to read again after a read of where the archives
|
|
5589
|
+
* are: every address before or after it ("all"), or only the old and new
|
|
5590
|
+
* address of each post type whose archive moved ("changed"). A language's
|
|
5591
|
+
* front page is its home's, never an entry.
|
|
5592
|
+
*/
|
|
5593
|
+
function archiveRoutes(before, after, which) {
|
|
5594
|
+
const routes = /* @__PURE__ */ new Set();
|
|
5595
|
+
for (const type of /* @__PURE__ */ new Set([...Object.keys(before ?? {}), ...Object.keys(after)])) {
|
|
5596
|
+
const old = before?.[type];
|
|
5597
|
+
const now = after[type];
|
|
5598
|
+
if (which === "changed" && old === now) continue;
|
|
5599
|
+
for (const uri of [old, now]) if (uri) routes.add(routeOf(uri));
|
|
5600
|
+
}
|
|
5601
|
+
return [...routes].filter((route) => !isLanguageHome$1(route));
|
|
5602
|
+
}
|
|
5603
|
+
/** Promotes a read of where each collection's archive is. */
|
|
5604
|
+
function promoteArchives(store, read, readStartedAt) {
|
|
5605
|
+
return promoteShared(store, ARCHIVES, "the archives", parseArchives, read, readStartedAt);
|
|
5606
|
+
}
|
|
5321
5607
|
/**
|
|
5322
5608
|
* Refreshes a language's front page only (not its chrome or the entries): the
|
|
5323
5609
|
* front page itself was published or changed.
|
|
@@ -5418,7 +5704,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5418
5704
|
}
|
|
5419
5705
|
const outcome = await promote(canonical, () => ({
|
|
5420
5706
|
state: "published",
|
|
5421
|
-
content: entry,
|
|
5707
|
+
content: stored(entry),
|
|
5422
5708
|
nodeId: entry.id,
|
|
5423
5709
|
modifiedAt
|
|
5424
5710
|
}));
|
|
@@ -5443,6 +5729,18 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5443
5729
|
found
|
|
5444
5730
|
};
|
|
5445
5731
|
}
|
|
5732
|
+
/** An entry as stored: a collection's item with the text its search matches. */
|
|
5733
|
+
function stored(entry) {
|
|
5734
|
+
if (!entry.collection) return entry;
|
|
5735
|
+
const search = searchText(decodeEntities(`${entry.title} ${stripHtml(entry.excerpt)}`));
|
|
5736
|
+
return {
|
|
5737
|
+
...entry,
|
|
5738
|
+
collection: {
|
|
5739
|
+
...entry.collection,
|
|
5740
|
+
search
|
|
5741
|
+
}
|
|
5742
|
+
};
|
|
5743
|
+
}
|
|
5446
5744
|
/**
|
|
5447
5745
|
* Every other route still serving an entry that was just found at its own
|
|
5448
5746
|
* route is marked moved there, unless it was read since `readBefore`: a
|
|
@@ -5566,6 +5864,151 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5566
5864
|
withdrawn
|
|
5567
5865
|
};
|
|
5568
5866
|
}
|
|
5867
|
+
/** The collection's config, or an error: a Site only lists what it declares. */
|
|
5868
|
+
function collectionNamed(name) {
|
|
5869
|
+
if (!Object.hasOwn(collections, name)) throw new Error(`${name} isn't a collection`);
|
|
5870
|
+
return collections[name];
|
|
5871
|
+
}
|
|
5872
|
+
/** A taxonomy's terms in an item's stored body, as a JSON path. */
|
|
5873
|
+
const termsPath = (taxonomy) => `$.collection.terms."${taxonomy}"`;
|
|
5874
|
+
/** The language a listing is in: a multilingual Site's (by slug), else none. */
|
|
5875
|
+
function listingLanguage(slug) {
|
|
5876
|
+
if (!multilingual) return null;
|
|
5877
|
+
if (slug === void 0) return defaultLanguage.slug;
|
|
5878
|
+
if (!languages.some((language) => language.slug === slug)) throw new Error(`${slug} isn't one of the Site's languages`);
|
|
5879
|
+
return slug;
|
|
5880
|
+
}
|
|
5881
|
+
/**
|
|
5882
|
+
* A page of a collection's items, from the store alone: never WordPress,
|
|
5883
|
+
* so a listing is served through outages as its items are. Items whose
|
|
5884
|
+
* stored body this Frontend can't serve are left out.
|
|
5885
|
+
*/
|
|
5886
|
+
async function list(query) {
|
|
5887
|
+
const collection = collectionNamed(query.collection);
|
|
5888
|
+
const subject = `the ${query.collection} collection`;
|
|
5889
|
+
const page = Math.max(1, whole("page", query.page, 1));
|
|
5890
|
+
const perPage = Math.min(100, Math.max(1, whole("perPage", query.perPage, 12)));
|
|
5891
|
+
const taxonomies = collection.taxonomies ?? [];
|
|
5892
|
+
const terms = Object.entries(query.terms ?? {}).flatMap(([taxonomy, slugs]) => {
|
|
5893
|
+
if (!taxonomies.includes(taxonomy)) throw new Error(`${taxonomy} isn't one of the ${query.collection} collection's taxonomies`);
|
|
5894
|
+
return slugs.length > 0 ? [{
|
|
5895
|
+
path: termsPath(taxonomy),
|
|
5896
|
+
slugs: [...slugs]
|
|
5897
|
+
}] : [];
|
|
5898
|
+
});
|
|
5899
|
+
const search = searchText(query.search ?? "");
|
|
5900
|
+
const filter = {
|
|
5901
|
+
collection: query.collection,
|
|
5902
|
+
language: listingLanguage(query.language),
|
|
5903
|
+
defaultLanguage: defaultLanguage.slug,
|
|
5904
|
+
terms,
|
|
5905
|
+
search: search || null
|
|
5906
|
+
};
|
|
5907
|
+
const store = await listingStore(subject);
|
|
5908
|
+
if ("kind" in store) return store;
|
|
5909
|
+
let found;
|
|
5910
|
+
try {
|
|
5911
|
+
found = await store.listItems(filter, perPage, (page - 1) * perPage);
|
|
5912
|
+
} catch (error) {
|
|
5913
|
+
if (!(error instanceof StoreFailure)) throw error;
|
|
5914
|
+
return notServed(subject, "store", error.message);
|
|
5915
|
+
}
|
|
5916
|
+
return {
|
|
5917
|
+
kind: "found",
|
|
5918
|
+
items: found.items.flatMap((row) => {
|
|
5919
|
+
let entry;
|
|
5920
|
+
try {
|
|
5921
|
+
entry = parseEntry(JSON.parse(row.body));
|
|
5922
|
+
} catch {
|
|
5923
|
+
entry = void 0;
|
|
5924
|
+
}
|
|
5925
|
+
if (!entry) {
|
|
5926
|
+
console.error(`Delivery: ${row.key} is stored with a body this Frontend can't read`);
|
|
5927
|
+
return [];
|
|
5928
|
+
}
|
|
5929
|
+
return [{
|
|
5930
|
+
uri: entry.uri,
|
|
5931
|
+
title: entry.title,
|
|
5932
|
+
date: entry.date,
|
|
5933
|
+
modifiedAt: row.modifiedAt,
|
|
5934
|
+
excerpt: entry.excerpt,
|
|
5935
|
+
featuredImage: entry.featuredImage,
|
|
5936
|
+
terms: Object.fromEntries(taxonomies.map((taxonomy) => [taxonomy, entry.collection?.terms[taxonomy] ?? []])),
|
|
5937
|
+
...entry.extension !== void 0 ? { extension: entry.extension } : {},
|
|
5938
|
+
...entry.language !== void 0 ? { language: entry.language } : {}
|
|
5939
|
+
}];
|
|
5940
|
+
}),
|
|
5941
|
+
total: found.total,
|
|
5942
|
+
page,
|
|
5943
|
+
pages: Math.ceil(found.total / perPage)
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5946
|
+
/** A listing's whole-number option (as a number, or a string of one, from a URL), else its default. */
|
|
5947
|
+
function whole(option, value, fallback) {
|
|
5948
|
+
if (value === void 0 || value === null || value === "") return fallback;
|
|
5949
|
+
const number = Number(value);
|
|
5950
|
+
if (!Number.isFinite(number)) throw new Error(`${option} must be a number, not ${JSON.stringify(value)}`);
|
|
5951
|
+
return Math.floor(number);
|
|
5952
|
+
}
|
|
5953
|
+
/** The terms of a collection's taxonomy its stored items have, with how many have each. */
|
|
5954
|
+
async function terms(name, taxonomy, options = {}) {
|
|
5955
|
+
if (!(collectionNamed(name).taxonomies ?? []).includes(taxonomy)) throw new Error(`${taxonomy} isn't one of the ${name} collection's taxonomies`);
|
|
5956
|
+
const subject = `the ${name} collection's ${taxonomy}`;
|
|
5957
|
+
const filter = {
|
|
5958
|
+
collection: name,
|
|
5959
|
+
language: listingLanguage(options.language),
|
|
5960
|
+
defaultLanguage: defaultLanguage.slug,
|
|
5961
|
+
terms: [],
|
|
5962
|
+
search: null
|
|
5963
|
+
};
|
|
5964
|
+
const store = await listingStore(subject);
|
|
5965
|
+
if ("kind" in store) return store;
|
|
5966
|
+
try {
|
|
5967
|
+
return {
|
|
5968
|
+
kind: "found",
|
|
5969
|
+
terms: await store.itemTerms(filter, termsPath(taxonomy))
|
|
5970
|
+
};
|
|
5971
|
+
} catch (error) {
|
|
5972
|
+
if (!(error instanceof StoreFailure)) throw error;
|
|
5973
|
+
return notServed(subject, "store", error.message);
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
/** The store a listing reads, or why there is none: a listing is never read live. */
|
|
5977
|
+
async function listingStore(subject) {
|
|
5978
|
+
if (!config.isServer()) return notServed(subject, "not-ready", "Content delivery requires a server context");
|
|
5979
|
+
let store;
|
|
5980
|
+
try {
|
|
5981
|
+
store = await boundStore();
|
|
5982
|
+
} catch {
|
|
5983
|
+
return notServed(subject, "store", "The publication store couldn't be loaded");
|
|
5984
|
+
}
|
|
5985
|
+
if (!store) return notServed(subject, "not-ready", "this Frontend has no publication store bound (PUBLICATION_DB); collections are only listed from one");
|
|
5986
|
+
return store;
|
|
5987
|
+
}
|
|
5988
|
+
/**
|
|
5989
|
+
* Where a collection's archive is: the stored archives when there is a
|
|
5990
|
+
* store, else (in explicit development) a live read. Null when it has none,
|
|
5991
|
+
* or neither can be read.
|
|
5992
|
+
*/
|
|
5993
|
+
async function archiveUri(name) {
|
|
5994
|
+
const { postType } = collectionNamed(name);
|
|
5995
|
+
if (!config.isServer()) return null;
|
|
5996
|
+
const store = await boundStore();
|
|
5997
|
+
if (!store) {
|
|
5998
|
+
if (!config.development) return null;
|
|
5999
|
+
const read = await getArchives();
|
|
6000
|
+
return read.kind === "found" ? read.content[postType] ?? null : null;
|
|
6001
|
+
}
|
|
6002
|
+
try {
|
|
6003
|
+
const stored = await store.read(ARCHIVES, parseArchives);
|
|
6004
|
+
if (stored?.state === "unusable") console.error(`Delivery: the archives can't be served: ${stored.message}`);
|
|
6005
|
+
return stored?.state === "published" ? stored.content[postType] ?? null : null;
|
|
6006
|
+
} catch (error) {
|
|
6007
|
+
if (!(error instanceof StoreFailure)) throw error;
|
|
6008
|
+
console.error(`Delivery: the archives can't be served (store): ${error.message}`);
|
|
6009
|
+
return null;
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
5569
6012
|
/** The refresh's credential when the Worker has one bound, and it is strong enough. */
|
|
5570
6013
|
async function refreshAuthority() {
|
|
5571
6014
|
const { PUBLICATION_DB, FRONTEND_REFRESH_TOKEN: token } = await config.bindings();
|
|
@@ -5599,14 +6042,45 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5599
6042
|
isAuthorized,
|
|
5600
6043
|
publishedHome: (siteOrigin, language) => safely(async () => html.home(await publishedHome(siteOrigin, language), language), () => notServed("the front page", "store", "The publication store couldn't be loaded")),
|
|
5601
6044
|
publishedEntry: (path, siteOrigin) => safely(async () => html.entry(await publishedEntry(path, siteOrigin), routeLanguage$1(routeOf(path))), () => notServed("the entry", "store", "The publication store couldn't be loaded")),
|
|
5602
|
-
pageChrome: (siteOrigin) => safely(() => pageChrome(siteOrigin), () => null)
|
|
6045
|
+
pageChrome: (siteOrigin) => safely(async () => html.chrome(await pageChrome(siteOrigin)), () => null),
|
|
6046
|
+
designPresets: () => safely(() => pageDesign(), () => null),
|
|
6047
|
+
list,
|
|
6048
|
+
terms,
|
|
6049
|
+
archiveUri: (collection) => {
|
|
6050
|
+
collectionNamed(collection);
|
|
6051
|
+
return safely(() => archiveUri(collection), () => null);
|
|
6052
|
+
}
|
|
5603
6053
|
};
|
|
5604
6054
|
}
|
|
6055
|
+
/** Text as a search compares it: lowercase, without accents, its spaces collapsed. */
|
|
6056
|
+
function searchText(text) {
|
|
6057
|
+
return text.normalize("NFD").replace(/\p{M}/gu, "").replace(/[\u2018\u2019\u201A\u201B\u2032]/g, "'").replace(/[\u201C\u201D\u201E\u2033]/g, "\"").toLowerCase().replace(/\s+/g, " ").trim();
|
|
6058
|
+
}
|
|
5605
6059
|
//#endregion
|
|
5606
6060
|
//#region frontend/src/wordpress.ts
|
|
6061
|
+
/** The origins a link into this Site may use: WordPress's, and the Frontend's when given. */
|
|
6062
|
+
/** The first GQ Design whose `designPattern` serves the default `page` route. */
|
|
6063
|
+
const DESIGN_PATTERN_VERSION = "0.4.1";
|
|
6064
|
+
function internalOrigins(config, siteOrigin) {
|
|
6065
|
+
return [new URL(config.graphqlEndpoint).origin, ...siteOrigin ? [siteOrigin] : []];
|
|
6066
|
+
}
|
|
5607
6067
|
function configuredWordPress(config, languages) {
|
|
5608
6068
|
const defaultLanguage = languages[0];
|
|
5609
6069
|
const multilingual = languages.length > 1;
|
|
6070
|
+
const extension = config.extension;
|
|
6071
|
+
const collections = Object.entries(config.collections ?? {});
|
|
6072
|
+
/** The collection whose items are of a WPGraphQL type, if one is. */
|
|
6073
|
+
const collectionOf = (typename) => collections.find(([, collection]) => collection.graphqlType === typename);
|
|
6074
|
+
/** The Site's extension fields on a kind of node (an item's, its collection's). */
|
|
6075
|
+
const extensionSelection = (kind, collection) => (kind === "item" ? extension?.fields.collections?.[collection ?? ""] : extension?.fields[kind])?.trim();
|
|
6076
|
+
/**
|
|
6077
|
+
* The Site's extension fields on a kind of node, read with the blocks (left
|
|
6078
|
+
* out with them when WordPress fails on them).
|
|
6079
|
+
*/
|
|
6080
|
+
const extensionFields = (kind, collection) => {
|
|
6081
|
+
const fields = extensionSelection(kind, collection);
|
|
6082
|
+
return fields ? `\n... @include(if: $withBlocks) {\n${fields}\n}` : "";
|
|
6083
|
+
};
|
|
5610
6084
|
const image = objectType({
|
|
5611
6085
|
sourceUrl: stringType(),
|
|
5612
6086
|
altText: stringType().nullable()
|
|
@@ -5634,7 +6108,7 @@ function configuredWordPress(config, languages) {
|
|
|
5634
6108
|
title: stringType().nullable(),
|
|
5635
6109
|
content: stringType().nullable(),
|
|
5636
6110
|
blocks: unknownType().optional()
|
|
5637
|
-
}), objectType({ __typename: stringType().refine((typename) => typename !== "Page") })]).nullable(),
|
|
6111
|
+
}).passthrough(), objectType({ __typename: stringType().refine((typename) => typename !== "Page") })]).nullable(),
|
|
5638
6112
|
designTokens
|
|
5639
6113
|
});
|
|
5640
6114
|
const pageEntry = objectType({
|
|
@@ -5647,7 +6121,7 @@ function configuredWordPress(config, languages) {
|
|
|
5647
6121
|
modifiedGmt: stringType().nullable().optional(),
|
|
5648
6122
|
featuredImage: imageEdge,
|
|
5649
6123
|
blocks: unknownType().optional()
|
|
5650
|
-
});
|
|
6124
|
+
}).passthrough();
|
|
5651
6125
|
const postEntry = pageEntry.extend({
|
|
5652
6126
|
excerpt: stringType().nullable(),
|
|
5653
6127
|
date: stringType().nullable()
|
|
@@ -5665,6 +6139,32 @@ function configuredWordPress(config, languages) {
|
|
|
5665
6139
|
pageBy: pageEntry.nullable(),
|
|
5666
6140
|
designTokens
|
|
5667
6141
|
});
|
|
6142
|
+
const termNode = objectType({
|
|
6143
|
+
slug: stringType(),
|
|
6144
|
+
name: stringType().nullable(),
|
|
6145
|
+
taxonomyName: stringType().nullable()
|
|
6146
|
+
});
|
|
6147
|
+
const nodeEntry = objectType({
|
|
6148
|
+
__typename: stringType(),
|
|
6149
|
+
id: stringType(),
|
|
6150
|
+
uri: stringType().nullable(),
|
|
6151
|
+
status: stringType().nullable(),
|
|
6152
|
+
isRestricted: booleanType().nullable(),
|
|
6153
|
+
modifiedGmt: stringType().nullable().optional(),
|
|
6154
|
+
date: stringType().nullable().optional(),
|
|
6155
|
+
title: stringType().nullable().optional(),
|
|
6156
|
+
content: stringType().nullable().optional(),
|
|
6157
|
+
excerpt: stringType().nullable().optional(),
|
|
6158
|
+
featuredImage: imageEdge.optional(),
|
|
6159
|
+
blocks: unknownType().optional(),
|
|
6160
|
+
terms: objectType({ nodes: arrayType(termNode) }).nullable().optional(),
|
|
6161
|
+
language: languageRef.nullable().optional(),
|
|
6162
|
+
translations: translated.translations.optional()
|
|
6163
|
+
}).passthrough();
|
|
6164
|
+
const nodeEntryData = objectType({
|
|
6165
|
+
node: unionType([nodeEntry, objectType({ __typename: stringType() })]).nullable(),
|
|
6166
|
+
designTokens
|
|
6167
|
+
});
|
|
5668
6168
|
const translatedEntryData = objectType({
|
|
5669
6169
|
postBy: postEntry.extend(translated).nullable(),
|
|
5670
6170
|
pageBy: pageEntry.extend(translated).nullable(),
|
|
@@ -5693,13 +6193,21 @@ function configuredWordPress(config, languages) {
|
|
|
5693
6193
|
modifiedGmt: stringType().nullable().optional()
|
|
5694
6194
|
}))
|
|
5695
6195
|
}) });
|
|
5696
|
-
const
|
|
6196
|
+
const designPattern = objectType({
|
|
6197
|
+
title: stringType(),
|
|
6198
|
+
blocksJson: stringType()
|
|
6199
|
+
}).nullable();
|
|
6200
|
+
const chromeWithoutPatternsData = objectType({
|
|
5697
6201
|
generalSettings: objectType({
|
|
5698
6202
|
siteIcon: imageEdge,
|
|
5699
6203
|
siteLogo: imageEdge
|
|
5700
6204
|
}),
|
|
5701
6205
|
menuItems: objectType({ nodes: arrayType(menuItem) })
|
|
5702
6206
|
});
|
|
6207
|
+
const siteChromeData = chromeWithoutPatternsData.extend({
|
|
6208
|
+
header: designPattern,
|
|
6209
|
+
footer: designPattern
|
|
6210
|
+
});
|
|
5703
6211
|
const consentSettingsData = objectType({
|
|
5704
6212
|
getquickLegal: unknownType(),
|
|
5705
6213
|
googleAnalytics: objectType({
|
|
@@ -5745,7 +6253,7 @@ function configuredWordPress(config, languages) {
|
|
|
5745
6253
|
content
|
|
5746
6254
|
blocks(attributes: true, htmlContent: true, dynamicContent: true, postTemplate: false)
|
|
5747
6255
|
@include(if: $withBlocks)
|
|
5748
|
-
isFrontPage
|
|
6256
|
+
isFrontPage${extensionFields("page")}
|
|
5749
6257
|
}
|
|
5750
6258
|
}
|
|
5751
6259
|
designTokens {
|
|
@@ -5774,7 +6282,7 @@ function configuredWordPress(config, languages) {
|
|
|
5774
6282
|
content
|
|
5775
6283
|
blocks(attributes: true, htmlContent: true, dynamicContent: true, postTemplate: false)
|
|
5776
6284
|
@include(if: $withBlocks)
|
|
5777
|
-
isFrontPage
|
|
6285
|
+
isFrontPage${extensionFields("page")}
|
|
5778
6286
|
}
|
|
5779
6287
|
}
|
|
5780
6288
|
designTokens {
|
|
@@ -5789,9 +6297,13 @@ function configuredWordPress(config, languages) {
|
|
|
5789
6297
|
}
|
|
5790
6298
|
}
|
|
5791
6299
|
`;
|
|
5792
|
-
function siteChromeQueryText(inLanguage) {
|
|
6300
|
+
function siteChromeQueryText(inLanguage, withPatterns = true) {
|
|
5793
6301
|
return `
|
|
5794
|
-
query SiteChrome($
|
|
6302
|
+
query SiteChrome(${[
|
|
6303
|
+
"$location: MenuLocationEnum!",
|
|
6304
|
+
...withPatterns ? ["$template: String!"] : [],
|
|
6305
|
+
...inLanguage ? ["$language: LanguageCodeEnum!"] : []
|
|
6306
|
+
].join(", ")}) {
|
|
5795
6307
|
generalSettings {
|
|
5796
6308
|
siteIcon {
|
|
5797
6309
|
node {
|
|
@@ -5815,6 +6327,14 @@ function configuredWordPress(config, languages) {
|
|
|
5815
6327
|
target
|
|
5816
6328
|
}
|
|
5817
6329
|
}
|
|
6330
|
+
${withPatterns ? `header: designPattern(template: $template, slot: "header") {
|
|
6331
|
+
title
|
|
6332
|
+
blocksJson
|
|
6333
|
+
}
|
|
6334
|
+
footer: designPattern(template: $template, slot: "footer") {
|
|
6335
|
+
title
|
|
6336
|
+
blocksJson
|
|
6337
|
+
}` : ""}
|
|
5818
6338
|
}
|
|
5819
6339
|
`;
|
|
5820
6340
|
}
|
|
@@ -5860,7 +6380,7 @@ function configuredWordPress(config, languages) {
|
|
|
5860
6380
|
sourceUrl
|
|
5861
6381
|
altText
|
|
5862
6382
|
}
|
|
5863
|
-
}
|
|
6383
|
+
}${extensionFields("post")}
|
|
5864
6384
|
}
|
|
5865
6385
|
pageBy(uri: $uri) {
|
|
5866
6386
|
id
|
|
@@ -5877,13 +6397,80 @@ function configuredWordPress(config, languages) {
|
|
|
5877
6397
|
sourceUrl
|
|
5878
6398
|
altText
|
|
5879
6399
|
}
|
|
5880
|
-
}
|
|
6400
|
+
}${extensionFields("page")}
|
|
5881
6401
|
}
|
|
5882
6402
|
}
|
|
5883
6403
|
`;
|
|
5884
6404
|
}
|
|
5885
6405
|
const entryQuery = entryQueryText(false);
|
|
5886
6406
|
const translatedEntryQuery = entryQueryText(true);
|
|
6407
|
+
function nodeQueryText(translated) {
|
|
6408
|
+
const languageFields = translated ? `
|
|
6409
|
+
language {
|
|
6410
|
+
slug
|
|
6411
|
+
}
|
|
6412
|
+
translations {
|
|
6413
|
+
uri
|
|
6414
|
+
language {
|
|
6415
|
+
slug
|
|
6416
|
+
}
|
|
6417
|
+
}` : "";
|
|
6418
|
+
const typeFields = (typename, fields) => fields.trim() ? `\n ... on ${typename} {${fields}\n }` : "";
|
|
6419
|
+
const itemFields = collections.map(([name, collection]) => typeFields(collection.graphqlType, `${collection.taxonomies?.length ? `
|
|
6420
|
+
terms(first: 100) {
|
|
6421
|
+
nodes {
|
|
6422
|
+
slug
|
|
6423
|
+
name
|
|
6424
|
+
taxonomyName
|
|
6425
|
+
}
|
|
6426
|
+
}` : ""}${languageFields}${extensionFields("item", name)}`)).join("");
|
|
6427
|
+
return `
|
|
6428
|
+
query EntryByUri($uri: String!, $withBlocks: Boolean = true) {
|
|
6429
|
+
designTokens {
|
|
6430
|
+
colors {
|
|
6431
|
+
slug
|
|
6432
|
+
color
|
|
6433
|
+
}
|
|
6434
|
+
spacingSizes {
|
|
6435
|
+
slug
|
|
6436
|
+
size
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
node: nodeByUri(uri: $uri) {
|
|
6440
|
+
__typename
|
|
6441
|
+
... on ContentNode {
|
|
6442
|
+
id
|
|
6443
|
+
uri
|
|
6444
|
+
date
|
|
6445
|
+
status
|
|
6446
|
+
isRestricted
|
|
6447
|
+
modifiedGmt
|
|
6448
|
+
blocks(attributes: true, htmlContent: true, dynamicContent: true, postTemplate: false)
|
|
6449
|
+
@include(if: $withBlocks)
|
|
6450
|
+
}
|
|
6451
|
+
... on NodeWithTitle {
|
|
6452
|
+
title
|
|
6453
|
+
}
|
|
6454
|
+
... on NodeWithContentEditor {
|
|
6455
|
+
content
|
|
6456
|
+
}
|
|
6457
|
+
... on NodeWithExcerpt {
|
|
6458
|
+
excerpt
|
|
6459
|
+
}
|
|
6460
|
+
... on NodeWithFeaturedImage {
|
|
6461
|
+
featuredImage {
|
|
6462
|
+
node {
|
|
6463
|
+
sourceUrl
|
|
6464
|
+
altText
|
|
6465
|
+
}
|
|
6466
|
+
}
|
|
6467
|
+
}${typeFields("Post", `${languageFields}${extensionFields("post")}`)}${typeFields("Page", `${languageFields}${extensionFields("page")}`)}${itemFields}
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
`;
|
|
6471
|
+
}
|
|
6472
|
+
const nodeQuery = nodeQueryText(false);
|
|
6473
|
+
const translatedNodeQuery = nodeQueryText(true);
|
|
5887
6474
|
const designQuery = `
|
|
5888
6475
|
query DesignPresets {
|
|
5889
6476
|
designTokens {
|
|
@@ -5898,10 +6485,19 @@ function configuredWordPress(config, languages) {
|
|
|
5898
6485
|
}
|
|
5899
6486
|
}
|
|
5900
6487
|
`;
|
|
6488
|
+
/** WPGraphQL's ContentTypeEnum: pages, posts and each collection's post type. */
|
|
6489
|
+
const contentTypes = [
|
|
6490
|
+
"PAGE",
|
|
6491
|
+
"POST",
|
|
6492
|
+
...collections.map(([, { postType }]) => {
|
|
6493
|
+
const name = postType.replace(/[^A-Za-z0-9_]/g, "_").toUpperCase();
|
|
6494
|
+
return /^[A-Z_]/.test(name) ? name : `_${name}`;
|
|
6495
|
+
})
|
|
6496
|
+
];
|
|
5901
6497
|
function publishedRoutesQueryText(everyLanguage) {
|
|
5902
6498
|
return `
|
|
5903
6499
|
query PublishedRoutes($after: String) {
|
|
5904
|
-
contentNodes(first: 100, after: $after, where: { contentTypes: [
|
|
6500
|
+
contentNodes(first: 100, after: $after, where: { contentTypes: [${contentTypes.join(", ")}]${everyLanguage ? ", language: ALL" : ""} }) {
|
|
5905
6501
|
pageInfo {
|
|
5906
6502
|
hasNextPage
|
|
5907
6503
|
endCursor
|
|
@@ -5946,6 +6542,31 @@ function configuredWordPress(config, languages) {
|
|
|
5946
6542
|
function describeIssues(error) {
|
|
5947
6543
|
return error.issues.map((issue) => `${issue.path.join(".") || "data"}: ${issue.message}`).join("; ");
|
|
5948
6544
|
}
|
|
6545
|
+
/**
|
|
6546
|
+
* `{ extension }` with the Site's value for a node WordPress returned, or
|
|
6547
|
+
* nothing: without an extension, fields for its kind, or a value, and when
|
|
6548
|
+
* the blocks (and with them the fields) were left out.
|
|
6549
|
+
*/
|
|
6550
|
+
function extended(kind, node, blocksOmitted, collection) {
|
|
6551
|
+
if (!extension || !extensionSelection(kind, collection) || blocksOmitted) return {};
|
|
6552
|
+
let value;
|
|
6553
|
+
try {
|
|
6554
|
+
value = extension.read(collection === void 0 ? {
|
|
6555
|
+
kind,
|
|
6556
|
+
fields: node
|
|
6557
|
+
} : {
|
|
6558
|
+
kind,
|
|
6559
|
+
collection,
|
|
6560
|
+
fields: node
|
|
6561
|
+
});
|
|
6562
|
+
} catch (error) {
|
|
6563
|
+
throw new CmsFailureError({
|
|
6564
|
+
reason: "schema",
|
|
6565
|
+
message: `WordPress's ${collection === void 0 ? kind : `${collection} item`} doesn't match the Site's extension: ${error instanceof Error ? error.message : String(error)}`
|
|
6566
|
+
}, { cause: error });
|
|
6567
|
+
}
|
|
6568
|
+
return value === void 0 ? {} : { extension: value };
|
|
6569
|
+
}
|
|
5949
6570
|
async function query(schema, queryText, variables) {
|
|
5950
6571
|
let response;
|
|
5951
6572
|
let credentials = [];
|
|
@@ -6105,7 +6726,8 @@ function configuredWordPress(config, languages) {
|
|
|
6105
6726
|
page: {
|
|
6106
6727
|
title: node.title ?? "",
|
|
6107
6728
|
content: containsVideoHero ? renderWordPressBlocks(blocks) : node.content ?? "",
|
|
6108
|
-
hasVideoHero: containsVideoHero
|
|
6729
|
+
hasVideoHero: containsVideoHero,
|
|
6730
|
+
...extended("page", node, blocksOmitted)
|
|
6109
6731
|
},
|
|
6110
6732
|
blocksOmitted,
|
|
6111
6733
|
spacingSizes: data.designTokens.spacingSizes,
|
|
@@ -6120,6 +6742,7 @@ function configuredWordPress(config, languages) {
|
|
|
6120
6742
|
* what any visitor may read.
|
|
6121
6743
|
*/
|
|
6122
6744
|
async function getEntryByUri(uri) {
|
|
6745
|
+
if (collections.length > 0) return getNodeByUri(uri);
|
|
6123
6746
|
return deliver(`the entry ${uri}`, async () => {
|
|
6124
6747
|
const { data, blocksOmitted } = multilingual ? await queryWithBlockRecovery(translatedEntryData, translatedEntryQuery, { uri }) : await queryWithBlockRecovery(entryData, entryQuery, { uri });
|
|
6125
6748
|
const post = data.postBy ?? data.pageBy;
|
|
@@ -6137,11 +6760,65 @@ function configuredWordPress(config, languages) {
|
|
|
6137
6760
|
modifiedAt: cmsTime(post.modifiedGmt),
|
|
6138
6761
|
spacingSizes: data.designTokens.spacingSizes,
|
|
6139
6762
|
colors: data.designTokens.colors,
|
|
6140
|
-
...multilingual ? languageOf(post) : {}
|
|
6763
|
+
...multilingual ? languageOf(post) : {},
|
|
6764
|
+
...extended(data.postBy ? "post" : "page", post, blocksOmitted)
|
|
6765
|
+
}
|
|
6766
|
+
};
|
|
6767
|
+
});
|
|
6768
|
+
}
|
|
6769
|
+
/**
|
|
6770
|
+
* On a Site with collections, the published page, post or collection item
|
|
6771
|
+
* at a URI. Missing when the node there is none of them (an archive, a
|
|
6772
|
+
* term, an item of a type the Site doesn't store), or isn't public.
|
|
6773
|
+
*/
|
|
6774
|
+
async function getNodeByUri(uri) {
|
|
6775
|
+
return deliver(`the entry ${uri}`, async () => {
|
|
6776
|
+
const { data, blocksOmitted } = await queryWithBlockRecovery(nodeEntryData, multilingual ? translatedNodeQuery : nodeQuery, { uri });
|
|
6777
|
+
const node = data.node;
|
|
6778
|
+
if (!node || !("id" in node)) return { kind: "missing" };
|
|
6779
|
+
const item = collectionOf(node.__typename);
|
|
6780
|
+
const kind = node.__typename === "Page" ? "page" : node.__typename === "Post" ? "post" : null;
|
|
6781
|
+
if (!item && !kind) return { kind: "missing" };
|
|
6782
|
+
if (!node.uri || node.isRestricted || node.status !== "publish") return { kind: "missing" };
|
|
6783
|
+
const entry = {
|
|
6784
|
+
...node,
|
|
6785
|
+
uri: node.uri,
|
|
6786
|
+
title: node.title ?? null,
|
|
6787
|
+
content: node.content ?? null,
|
|
6788
|
+
excerpt: node.excerpt ?? null,
|
|
6789
|
+
date: node.date ?? null,
|
|
6790
|
+
featuredImage: node.featuredImage ?? null
|
|
6791
|
+
};
|
|
6792
|
+
const normalized = normalizePost(entry);
|
|
6793
|
+
const blocks = parseWordPressBlocks(node.blocks);
|
|
6794
|
+
const containsVideoHero = hasVideoHero(blocks);
|
|
6795
|
+
return {
|
|
6796
|
+
kind: "found",
|
|
6797
|
+
content: {
|
|
6798
|
+
...normalized,
|
|
6799
|
+
content: containsVideoHero ? renderWordPressBlocks(blocks) : normalized.content,
|
|
6800
|
+
hasVideoHero: containsVideoHero,
|
|
6801
|
+
blocksOmitted,
|
|
6802
|
+
modifiedAt: cmsTime(node.modifiedGmt),
|
|
6803
|
+
spacingSizes: data.designTokens.spacingSizes,
|
|
6804
|
+
colors: data.designTokens.colors,
|
|
6805
|
+
...multilingual ? languageOf(entry) : {},
|
|
6806
|
+
...item ? { collection: {
|
|
6807
|
+
name: item[0],
|
|
6808
|
+
terms: termsOf(item[1].taxonomies ?? [], node.terms?.nodes ?? [])
|
|
6809
|
+
} } : {},
|
|
6810
|
+
...item ? extended("item", node, blocksOmitted, item[0]) : extended(kind, node, blocksOmitted)
|
|
6141
6811
|
}
|
|
6142
6812
|
};
|
|
6143
6813
|
});
|
|
6144
6814
|
}
|
|
6815
|
+
/** An item's terms in each of its collection's taxonomies (every one, if it has none). */
|
|
6816
|
+
function termsOf(taxonomies, nodes) {
|
|
6817
|
+
return Object.fromEntries(taxonomies.map((taxonomy) => [taxonomy, nodes.filter((term) => term.taxonomyName === taxonomy).map((term) => ({
|
|
6818
|
+
slug: term.slug,
|
|
6819
|
+
name: term.name ?? term.slug
|
|
6820
|
+
}))]));
|
|
6821
|
+
}
|
|
6145
6822
|
/** An entry's language and published translations, as a multilingual Site stores them. */
|
|
6146
6823
|
function languageOf(post) {
|
|
6147
6824
|
return {
|
|
@@ -6203,6 +6880,93 @@ function configuredWordPress(config, languages) {
|
|
|
6203
6880
|
});
|
|
6204
6881
|
});
|
|
6205
6882
|
}
|
|
6883
|
+
const names = objectType({ nodes: arrayType(objectType({ name: stringType() })) }).nullable();
|
|
6884
|
+
const archivesData = objectType({
|
|
6885
|
+
getquickArchives: arrayType(objectType({
|
|
6886
|
+
postType: stringType(),
|
|
6887
|
+
uri: stringType().nullable()
|
|
6888
|
+
})).nullable().optional(),
|
|
6889
|
+
contentTypes: objectType({ nodes: arrayType(objectType({
|
|
6890
|
+
name: stringType(),
|
|
6891
|
+
graphqlSingleName: stringType().nullable().optional(),
|
|
6892
|
+
connectedTaxonomies: names.optional()
|
|
6893
|
+
})) }).nullable(),
|
|
6894
|
+
taxonomies: names
|
|
6895
|
+
});
|
|
6896
|
+
const archivesQueryText = (archives) => `
|
|
6897
|
+
query ContentArchives {${archives ? `
|
|
6898
|
+
getquickArchives {
|
|
6899
|
+
postType
|
|
6900
|
+
uri
|
|
6901
|
+
}` : ""}
|
|
6902
|
+
contentTypes(first: 100) {
|
|
6903
|
+
nodes {
|
|
6904
|
+
name
|
|
6905
|
+
graphqlSingleName
|
|
6906
|
+
connectedTaxonomies(first: 100) {
|
|
6907
|
+
nodes {
|
|
6908
|
+
name
|
|
6909
|
+
}
|
|
6910
|
+
}
|
|
6911
|
+
}
|
|
6912
|
+
}
|
|
6913
|
+
taxonomies(first: 100) {
|
|
6914
|
+
nodes {
|
|
6915
|
+
name
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
`;
|
|
6920
|
+
/**
|
|
6921
|
+
* Where each collection's archive is, by post type: the Page assigned to it
|
|
6922
|
+
* in GETQUICK → Archives, else the type's own archive. A CMS whose GETQUICK
|
|
6923
|
+
* Content doesn't have Archives yet gives none. A collection whose post
|
|
6924
|
+
* type or taxonomy WPGraphQL doesn't show fails the read (`schema`).
|
|
6925
|
+
*/
|
|
6926
|
+
async function getArchives() {
|
|
6927
|
+
return deliver("the archives", async () => {
|
|
6928
|
+
let data;
|
|
6929
|
+
try {
|
|
6930
|
+
data = await query(archivesData, archivesQueryText(true));
|
|
6931
|
+
} catch (error) {
|
|
6932
|
+
if (!(error instanceof CmsFailureError) || error.failure.reason !== "graphql" || !/Cannot query field "getquickArchives"/.test(error.failure.message)) throw error;
|
|
6933
|
+
console.warn(`WordPress: GETQUICK Content provides no archives (${error.failure.message}); collections have none`);
|
|
6934
|
+
data = await query(archivesData, archivesQueryText(false));
|
|
6935
|
+
}
|
|
6936
|
+
const shown = (list) => new Set((list?.nodes ?? []).map(({ name }) => name));
|
|
6937
|
+
const postTypes = new Map((data.contentTypes?.nodes ?? []).map((type) => [type.name, type]));
|
|
6938
|
+
const taxonomies = shown(data.taxonomies);
|
|
6939
|
+
const hidden = collections.flatMap(([name, collection]) => [...postTypes.has(collection.postType) ? [] : [`${name}'s post type ${collection.postType}`], ...(collection.taxonomies ?? []).filter((taxonomy) => !taxonomies.has(taxonomy)).map((taxonomy) => `${name}'s taxonomy ${taxonomy}`)]);
|
|
6940
|
+
if (hidden.length > 0) throw new CmsFailureError({
|
|
6941
|
+
reason: "schema",
|
|
6942
|
+
message: `WPGraphQL doesn't show ${hidden.join(", ")} (show_in_graphql)`
|
|
6943
|
+
});
|
|
6944
|
+
const mismatched = collections.flatMap(([name, collection]) => {
|
|
6945
|
+
const type = postTypes.get(collection.postType);
|
|
6946
|
+
const typeName = type.graphqlSingleName ? graphqlTypeName(type.graphqlSingleName) : null;
|
|
6947
|
+
const connected = shown(type.connectedTaxonomies);
|
|
6948
|
+
return [...typeName === null || typeName === collection.graphqlType ? [] : [`${name}'s graphqlType ${collection.graphqlType} isn't post type ${collection.postType}'s GraphQL type, ${typeName}`], ...type.connectedTaxonomies === void 0 ? [] : (collection.taxonomies ?? []).filter((taxonomy) => !connected.has(taxonomy)).map((taxonomy) => `${name}'s taxonomy ${taxonomy} isn't connected to post type ${collection.postType}`)];
|
|
6949
|
+
});
|
|
6950
|
+
if (mismatched.length > 0) throw new CmsFailureError({
|
|
6951
|
+
reason: "schema",
|
|
6952
|
+
message: mismatched.join("; ")
|
|
6953
|
+
});
|
|
6954
|
+
const stored = new Set(collections.map(([, { postType }]) => postType));
|
|
6955
|
+
return {
|
|
6956
|
+
kind: "found",
|
|
6957
|
+
content: Object.fromEntries((data.getquickArchives ?? []).flatMap(({ postType, uri }) => uri && stored.has(postType) ? [[postType, uriPath(uri)]] : []))
|
|
6958
|
+
};
|
|
6959
|
+
});
|
|
6960
|
+
}
|
|
6961
|
+
/**
|
|
6962
|
+
* The GraphQL type WPGraphQL names after a post type's graphql_single_name
|
|
6963
|
+
* (WPGraphQL's Utils::format_type_name): each word, split at `_`, `-` or a
|
|
6964
|
+
* space, capitalised and joined.
|
|
6965
|
+
*/
|
|
6966
|
+
function graphqlTypeName(singleName) {
|
|
6967
|
+
const capital = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
6968
|
+
return singleName.replace(/[^a-zA-Z0-9 -]/g, "_").split(/[_ -]/).map(capital).join("");
|
|
6969
|
+
}
|
|
6206
6970
|
/** The URI of every published page and post. */
|
|
6207
6971
|
async function getPublishedRoutes() {
|
|
6208
6972
|
const listed = await getPublishedEntries();
|
|
@@ -6254,24 +7018,77 @@ function configuredWordPress(config, languages) {
|
|
|
6254
7018
|
}
|
|
6255
7019
|
}
|
|
6256
7020
|
/**
|
|
6257
|
-
* The
|
|
6258
|
-
*
|
|
6259
|
-
*
|
|
6260
|
-
*
|
|
7021
|
+
* The chrome's data in a language. A CMS whose GQ Design predates
|
|
7022
|
+
* `designPattern` (before 0.2.17) has no header or footer patterns to
|
|
7023
|
+
* select: its schema rejects the field, and the chrome is read again without
|
|
7024
|
+
* them, with both null (the Site's own header and footer), as when nothing
|
|
7025
|
+
* is selected. The warning names the cause and the version the default
|
|
7026
|
+
* `page` route needs; menus, logos and icons keep refreshing, and new Sites
|
|
7027
|
+
* and languages are served. Any other failure fails the read, so what is
|
|
7028
|
+
* stored is kept.
|
|
7029
|
+
*/
|
|
7030
|
+
async function chromeData(language, location) {
|
|
7031
|
+
const template = config.designTemplate ?? "page";
|
|
7032
|
+
const variables = language.isDefault ? { location } : {
|
|
7033
|
+
location,
|
|
7034
|
+
language: language.code
|
|
7035
|
+
};
|
|
7036
|
+
try {
|
|
7037
|
+
return await query(siteChromeData, language.isDefault ? siteChromeQuery : languageChromeQuery, {
|
|
7038
|
+
...variables,
|
|
7039
|
+
template
|
|
7040
|
+
});
|
|
7041
|
+
} catch (error) {
|
|
7042
|
+
if (!(error instanceof CmsFailureError) || error.failure.reason !== "graphql" || !/Cannot query field "designPattern"/.test(error.failure.message)) throw error;
|
|
7043
|
+
console.warn(`WordPress: GQ Design has no designPattern field (${error.failure.message}); header and footer patterns need GQ Design ${DESIGN_PATTERN_VERSION} or newer, so the chrome is read without them and the Site's own header and footer are served`);
|
|
7044
|
+
return {
|
|
7045
|
+
...await query(chromeWithoutPatternsData, siteChromeQueryText(!language.isDefault, false), variables),
|
|
7046
|
+
header: null,
|
|
7047
|
+
footer: null
|
|
7048
|
+
};
|
|
7049
|
+
}
|
|
7050
|
+
}
|
|
7051
|
+
/**
|
|
7052
|
+
* A selected pattern's block tree. GQ Design answers a selected pattern it
|
|
7053
|
+
* couldn't resolve (invalid attributes, a cycle, too many blocks) with an
|
|
7054
|
+
* empty tree, not an error; that is a failed read, so the stored chrome is
|
|
7055
|
+
* kept rather than replaced by an empty region.
|
|
7056
|
+
*/
|
|
7057
|
+
function designPatternOf(region, pattern) {
|
|
7058
|
+
if (!pattern) return null;
|
|
7059
|
+
const blocks = parseDesignBlocks(pattern.blocksJson);
|
|
7060
|
+
if (!blocks) throw new CmsFailureError({
|
|
7061
|
+
reason: "schema",
|
|
7062
|
+
message: `The ${region} pattern's blocksJson isn't a JSON list of blocks`
|
|
7063
|
+
});
|
|
7064
|
+
if (blocks.length === 0) throw new CmsFailureError({
|
|
7065
|
+
reason: "schema",
|
|
7066
|
+
message: `The ${region} pattern "${pattern.title}" has no blocks GQ Design could resolve`
|
|
7067
|
+
});
|
|
7068
|
+
return {
|
|
7069
|
+
title: pattern.title,
|
|
7070
|
+
blocks
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
/**
|
|
7074
|
+
* The menu, logo, icon, header and footer every page in a language shares:
|
|
7075
|
+
* the logo and icon are the site's, the menu the one at the location in that
|
|
7076
|
+
* language, the header and footer the patterns selected for the configured
|
|
7077
|
+
* design route. A site without them is found with an empty menu, no images
|
|
7078
|
+
* and no patterns; a failed read is Unavailable, not that.
|
|
6261
7079
|
*/
|
|
6262
7080
|
async function getSiteChrome(siteOrigin, { location = "PRIMARY", language = defaultLanguage } = {}) {
|
|
6263
7081
|
return deliver(languageSubject(language, "site chrome"), async () => {
|
|
6264
|
-
const [data, consent] = await Promise.all([language
|
|
6265
|
-
|
|
6266
|
-
language: language.code
|
|
6267
|
-
}), config.consent?.cmsSettings ? consentSettings(language) : null]);
|
|
6268
|
-
const origins = [new URL(config.graphqlEndpoint).origin, ...siteOrigin ? [siteOrigin] : []];
|
|
7082
|
+
const [data, consent] = await Promise.all([chromeData(language, location), config.consent?.cmsSettings ? consentSettings(language) : null]);
|
|
7083
|
+
const origins = internalOrigins(config, siteOrigin);
|
|
6269
7084
|
return {
|
|
6270
7085
|
kind: "found",
|
|
6271
7086
|
content: {
|
|
6272
7087
|
menuItems: buildMenuTree(data.menuItems.nodes, origins),
|
|
6273
7088
|
siteIcon: normalizeImage(data.generalSettings.siteIcon),
|
|
6274
7089
|
siteLogo: normalizeImage(data.generalSettings.siteLogo),
|
|
7090
|
+
header: designPatternOf("header", data.header),
|
|
7091
|
+
footer: designPatternOf("footer", data.footer),
|
|
6275
7092
|
...consent ? {
|
|
6276
7093
|
legal: parseLegalLanguage(consent.getquickLegal),
|
|
6277
7094
|
analytics: consent.googleAnalytics ?? {
|
|
@@ -6289,6 +7106,7 @@ function configuredWordPress(config, languages) {
|
|
|
6289
7106
|
getDesignPresets,
|
|
6290
7107
|
getPublishedEntries,
|
|
6291
7108
|
getPublishedRoutes,
|
|
7109
|
+
getArchives,
|
|
6292
7110
|
getSiteChrome
|
|
6293
7111
|
};
|
|
6294
7112
|
}
|
|
@@ -6297,6 +7115,10 @@ function configuredWordPress(config, languages) {
|
|
|
6297
7115
|
function createContentRuntime(input) {
|
|
6298
7116
|
const site = input.site.trim();
|
|
6299
7117
|
if (!site) throw new Error("A nonempty Site identity is required");
|
|
7118
|
+
if (input.designTemplate !== void 0 && (!/^[a-z0-9_-]+$/.test(input.designTemplate) || /^[0-9]+$/.test(input.designTemplate))) throw new Error("designTemplate must be a GQ Design route key, such as page");
|
|
7119
|
+
const { collections: collectionFields, ...kindFields } = input.extension?.fields ?? {};
|
|
7120
|
+
for (const [kind, fields] of Object.entries(kindFields)) if (fields !== void 0 && !fields.trim()) throw new Error(`The extension's ${kind} fields are empty; leave them out instead`);
|
|
7121
|
+
validateCollections(input.collections ?? {}, collectionFields ?? {});
|
|
6300
7122
|
const config = {
|
|
6301
7123
|
...input,
|
|
6302
7124
|
site,
|
|
@@ -6351,8 +7173,49 @@ function createContentRuntime(input) {
|
|
|
6351
7173
|
handleRefresh: guarded("Refresh", "Refresh with POST.", refresh.handleRefresh),
|
|
6352
7174
|
publishedHome: delivery.publishedHome,
|
|
6353
7175
|
publishedEntry: delivery.publishedEntry,
|
|
7176
|
+
/**
|
|
7177
|
+
* A page of a collection's items, from the publication store alone (never
|
|
7178
|
+
* WordPress), newest first: those with the given terms and text, in the
|
|
7179
|
+
* language. Unavailable when the store can't be read, or there is none.
|
|
7180
|
+
*/
|
|
7181
|
+
list: delivery.list,
|
|
7182
|
+
/**
|
|
7183
|
+
* The terms of a collection's taxonomy that its stored items have, with how
|
|
7184
|
+
* many of them have each, by name: for filters.
|
|
7185
|
+
*/
|
|
7186
|
+
terms: delivery.terms,
|
|
7187
|
+
/**
|
|
7188
|
+
* Where a collection's archive is: the Page assigned to it in GETQUICK →
|
|
7189
|
+
* Archives, else its post type's own archive. Null when it has neither, or
|
|
7190
|
+
* the store can't be read.
|
|
7191
|
+
*/
|
|
7192
|
+
archiveUri: delivery.archiveUri,
|
|
6354
7193
|
pageChrome: delivery.pageChrome,
|
|
6355
7194
|
/**
|
|
7195
|
+
* A chrome pattern as HTML, by the Site's block renderers (see
|
|
7196
|
+
* `renderDesignPattern`), with links into the CMS or `siteOrigin` served
|
|
7197
|
+
* as this Site's paths.
|
|
7198
|
+
*/
|
|
7199
|
+
renderDesignPattern: (pattern, options = {}) => {
|
|
7200
|
+
const { siteOrigin, internalOrigins: extraOrigins = [], ...rest } = options;
|
|
7201
|
+
return renderDesignPattern(pattern, {
|
|
7202
|
+
...rest,
|
|
7203
|
+
internalOrigins: [...internalOrigins(config, siteOrigin), ...extraOrigins]
|
|
7204
|
+
});
|
|
7205
|
+
},
|
|
7206
|
+
/**
|
|
7207
|
+
* The design presets every page shares (GQ Design's spacing sizes and
|
|
7208
|
+
* colors), for a page that isn't a publication: the stored ones when there
|
|
7209
|
+
* is a store, else (in explicit development) a live read. Null when
|
|
7210
|
+
* neither has them.
|
|
7211
|
+
*/
|
|
7212
|
+
designPresets: delivery.designPresets,
|
|
7213
|
+
/**
|
|
7214
|
+
* The origins a link into this Site may use: WordPress's, and the
|
|
7215
|
+
* Frontend's when given. `menuItemHref` turns links at them into paths.
|
|
7216
|
+
*/
|
|
7217
|
+
internalOrigins: (siteOrigin) => internalOrigins(config, siteOrigin),
|
|
7218
|
+
/**
|
|
6356
7219
|
* The Site's consent configuration for a page served with this chrome:
|
|
6357
7220
|
* declared services, GA4 when gq-config's toggle is on, and the
|
|
6358
7221
|
* controller from the Legal Contract. Null when consent isn't configured.
|
|
@@ -6364,5 +7227,27 @@ function createContentRuntime(input) {
|
|
|
6364
7227
|
}) : null
|
|
6365
7228
|
};
|
|
6366
7229
|
}
|
|
7230
|
+
/** A collection's names are paths in the store's JSON, and the post type and taxonomies GraphQL names. */
|
|
7231
|
+
function validateCollections(collections, fields) {
|
|
7232
|
+
const name = /^[a-z0-9][a-z0-9_-]*$/;
|
|
7233
|
+
const postTypes = /* @__PURE__ */ new Set();
|
|
7234
|
+
for (const [collection, { postType, graphqlType, taxonomies = [] }] of Object.entries(collections)) {
|
|
7235
|
+
if (!name.test(collection)) throw new Error(`The collection ${JSON.stringify(collection)} needs a name such as biblioteca`);
|
|
7236
|
+
if (!/^[a-z0-9_-]{1,20}$/.test(postType)) throw new Error(`The collection ${collection}'s postType isn't a WordPress post type name`);
|
|
7237
|
+
if ([
|
|
7238
|
+
"page",
|
|
7239
|
+
"post",
|
|
7240
|
+
"attachment"
|
|
7241
|
+
].includes(postType)) throw new Error(`The collection ${collection} can't be WordPress's ${postType}s`);
|
|
7242
|
+
if (postTypes.has(postType)) throw new Error(`The collection ${collection}'s post type is another collection's`);
|
|
7243
|
+
postTypes.add(postType);
|
|
7244
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(graphqlType)) throw new Error(`The collection ${collection}'s graphqlType isn't a GraphQL type name`);
|
|
7245
|
+
for (const taxonomy of taxonomies) if (!/^[a-z0-9_-]{1,32}$/.test(taxonomy)) throw new Error(`The collection ${collection}'s taxonomy ${taxonomy} isn't a taxonomy name`);
|
|
7246
|
+
}
|
|
7247
|
+
for (const [collection, value] of Object.entries(fields)) {
|
|
7248
|
+
if (!Object.hasOwn(collections, collection)) throw new Error(`The extension has fields for ${collection}, which isn't a collection`);
|
|
7249
|
+
if (value !== void 0 && !value.trim()) throw new Error(`The extension's ${collection} fields are empty; leave them out instead`);
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
6367
7252
|
//#endregion
|
|
6368
7253
|
export { createContentRuntime };
|