@getquick/content 0.2.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 +114 -2
- 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 +7 -2
- package/dist/index.js +2 -2
- 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 +843 -55
- package/dist/{formatting-wOk2jz6J.js → site-language-cBP8SFrS.js} +302 -50
- package/dist/{version-DyyNhsuU.js → version-D_Ao31D3.js} +1 -1
- package/dist/{wordpress-CUFb6YZZ.d.ts → wordpress-D_HG2rEt.d.ts} +273 -51
- package/package.json +13 -1
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;
|
|
@@ -3737,7 +3737,8 @@ function publicationStore(db) {
|
|
|
3737
3737
|
RETURNING event_id`).bind(event.nodeId, event.id, event.occurredAt).first()))?.event_id ?? null;
|
|
3738
3738
|
},
|
|
3739
3739
|
async entryIndex() {
|
|
3740
|
-
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
|
|
3741
3742
|
FROM publications p LEFT JOIN refresh_attempts a ON a.key = p.key
|
|
3742
3743
|
WHERE substr(p.key, 1, 6) = 'entry:'
|
|
3743
3744
|
ORDER BY p.key`), db.prepare("SELECT node_id, withdrawn_at FROM withdrawals WHERE republished_at IS NULL")]));
|
|
@@ -3747,6 +3748,7 @@ function publicationStore(db) {
|
|
|
3747
3748
|
state: row.state,
|
|
3748
3749
|
nodeId: row.node_id,
|
|
3749
3750
|
modifiedAt: row.modified_at,
|
|
3751
|
+
readStartedAt: row.read_started_at,
|
|
3750
3752
|
attemptedAt: row.attempted_at
|
|
3751
3753
|
})),
|
|
3752
3754
|
withdrawals: new Map(withdrawals.results.map((row) => [row.node_id, row.withdrawn_at]))
|
|
@@ -3791,7 +3793,7 @@ function publicationStore(db) {
|
|
|
3791
3793
|
},
|
|
3792
3794
|
async status() {
|
|
3793
3795
|
const [shared, entries, withdrawals, failed] = await guard("be read", () => db.batch([
|
|
3794
|
-
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')"),
|
|
3795
3797
|
db.prepare(`SELECT CASE WHEN format = ?1 THEN state ELSE 'unusable' END AS state, count(*) AS count
|
|
3796
3798
|
FROM publications WHERE substr(key, 1, 6) = 'entry:' GROUP BY 1`).bind(1),
|
|
3797
3799
|
db.prepare("SELECT count(*) AS count FROM withdrawals WHERE republished_at IS NULL"),
|
|
@@ -3808,11 +3810,47 @@ function publicationStore(db) {
|
|
|
3808
3810
|
home: state("home"),
|
|
3809
3811
|
chrome: state("chrome"),
|
|
3810
3812
|
design: state("design"),
|
|
3813
|
+
...state("archives") === null ? {} : { archives: state("archives") },
|
|
3811
3814
|
entries: Object.fromEntries(entries.results.map((row) => [row.state, Number(row.count)])),
|
|
3812
3815
|
withdrawals: count(withdrawals),
|
|
3813
3816
|
failedEvents: count(failed)
|
|
3814
3817
|
};
|
|
3815
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
|
+
},
|
|
3816
3854
|
async startEvent(id) {
|
|
3817
3855
|
await guard("record an event", () => db.prepare("UPDATE publication_events SET attempts = attempts + 1 WHERE id = ?").bind(id).run());
|
|
3818
3856
|
},
|
|
@@ -3827,6 +3865,33 @@ function publicationStore(db) {
|
|
|
3827
3865
|
}
|
|
3828
3866
|
};
|
|
3829
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
|
+
}
|
|
3830
3895
|
function movedTo(body) {
|
|
3831
3896
|
try {
|
|
3832
3897
|
const uri = JSON.parse(body ?? "null")?.uri;
|
|
@@ -3837,7 +3902,7 @@ function movedTo(body) {
|
|
|
3837
3902
|
}
|
|
3838
3903
|
//#endregion
|
|
3839
3904
|
//#region frontend/src/reconciliation.ts
|
|
3840
|
-
function configuredReconciliation(languages, delivery, wordpress) {
|
|
3905
|
+
function configuredReconciliation(languages, delivery, wordpress, collections = false) {
|
|
3841
3906
|
const { eachLimited, reconcileShared, refreshEntries, sharedOutcomes } = delivery;
|
|
3842
3907
|
const { getPublishedEntries } = wordpress;
|
|
3843
3908
|
const isLanguageHome$4 = (route) => isLanguageHome(route, languages);
|
|
@@ -3848,10 +3913,24 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3848
3913
|
* is asked again without them. Chrome and design have no block recovery.
|
|
3849
3914
|
*/
|
|
3850
3915
|
const REQUESTS_PER_READ = 2;
|
|
3851
|
-
/**
|
|
3852
|
-
|
|
3853
|
-
|
|
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
|
+
*/
|
|
3854
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;
|
|
3855
3934
|
/** How long a run holds the lease; an interrupted run's lease is taken over after it. */
|
|
3856
3935
|
const LEASE_MS = 12e4;
|
|
3857
3936
|
const ENTRY = "entry:";
|
|
@@ -3904,7 +3983,7 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3904
3983
|
}
|
|
3905
3984
|
}
|
|
3906
3985
|
async function run(store, startedAt, siteOrigin, publish) {
|
|
3907
|
-
const [shared, listing] = await Promise.all([reconcileShared(store, siteOrigin), getPublishedEntries()]);
|
|
3986
|
+
const [{ archiveRoutes = [], ...shared }, listing] = await Promise.all([reconcileShared(store, siteOrigin), getPublishedEntries()]);
|
|
3908
3987
|
const failures = [];
|
|
3909
3988
|
let changed = 0;
|
|
3910
3989
|
for (const outcome of sharedOutcomes(shared)) {
|
|
@@ -3931,9 +4010,10 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3931
4010
|
}
|
|
3932
4011
|
const { entries: listed, requests } = listing.content;
|
|
3933
4012
|
const index = await store.entryIndex();
|
|
3934
|
-
const found = candidates(listed, index);
|
|
4013
|
+
const { found, settled } = candidates(listed, index);
|
|
3935
4014
|
let budget = CMS_REQUEST_BUDGET - SHARED_REQUESTS - requests;
|
|
3936
4015
|
const chosen = [];
|
|
4016
|
+
found.unshift(...archived(archiveRoutes, found));
|
|
3937
4017
|
let pending = 0;
|
|
3938
4018
|
for (const candidate of found) {
|
|
3939
4019
|
const cost = (candidate.previousRoute ? 2 : 1) * REQUESTS_PER_READ;
|
|
@@ -3944,7 +4024,7 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3944
4024
|
budget -= cost;
|
|
3945
4025
|
chosen.push(candidate);
|
|
3946
4026
|
}
|
|
3947
|
-
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)));
|
|
3948
4028
|
const entries = await refresh(store, chosen, publish);
|
|
3949
4029
|
for (const outcome of Object.values(entries)) {
|
|
3950
4030
|
if (outcome.outcome === "kept") failures.push(outcome.failure);
|
|
@@ -3967,7 +4047,8 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3967
4047
|
};
|
|
3968
4048
|
}
|
|
3969
4049
|
/**
|
|
3970
|
-
* 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).
|
|
3971
4052
|
* WordPress's front page (`/`) is the homepage's, reconciled with the shared
|
|
3972
4053
|
* rows, unless it is withdrawn and modified since.
|
|
3973
4054
|
*/
|
|
@@ -3979,6 +4060,7 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3979
4060
|
storedAt.set(row.nodeId, [...storedAt.get(row.nodeId) ?? [], row.key.slice(6)]);
|
|
3980
4061
|
}
|
|
3981
4062
|
const found = [];
|
|
4063
|
+
const settled = /* @__PURE__ */ new Set();
|
|
3982
4064
|
const listedRoutes = /* @__PURE__ */ new Set();
|
|
3983
4065
|
const left = /* @__PURE__ */ new Set();
|
|
3984
4066
|
for (const entry of listed) {
|
|
@@ -3999,6 +4081,10 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
3999
4081
|
if (isLanguageHome$4(route)) continue;
|
|
4000
4082
|
if (row?.state === "published" && entry.id !== null && row.nodeId === entry.id && entry.modifiedAt !== null && row.modifiedAt === entry.modifiedAt) continue;
|
|
4001
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
|
+
}
|
|
4002
4088
|
if (previousRoute) left.add(previousRoute);
|
|
4003
4089
|
found.push({
|
|
4004
4090
|
change: previousRoute ? "moved" : row ? "changed" : "new",
|
|
@@ -4017,12 +4103,33 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
4017
4103
|
attemptedAt: row.attemptedAt
|
|
4018
4104
|
});
|
|
4019
4105
|
}
|
|
4020
|
-
|
|
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
|
+
};
|
|
4111
|
+
}
|
|
4112
|
+
/** The addresses whose archive moved, unless already a change, as changes. */
|
|
4113
|
+
function archived(routes, found) {
|
|
4114
|
+
const changing = new Set(found.flatMap(({ route, previousRoute }) => [route, previousRoute]));
|
|
4115
|
+
return routes.filter((route) => !changing.has(route) && !isLanguageHome$4(route)).map((route) => ({
|
|
4116
|
+
change: "archive",
|
|
4117
|
+
route,
|
|
4118
|
+
attemptedAt: 0
|
|
4119
|
+
}));
|
|
4021
4120
|
}
|
|
4022
|
-
/**
|
|
4023
|
-
|
|
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) {
|
|
4024
4127
|
const changing = new Set(found.flatMap(({ route, previousRoute }) => [route, previousRoute]));
|
|
4025
|
-
return index.entries.filter((row) =>
|
|
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) => ({
|
|
4026
4133
|
change: "verified",
|
|
4027
4134
|
route: row.key.slice(6),
|
|
4028
4135
|
attemptedAt: row.attemptedAt
|
|
@@ -4053,11 +4160,11 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
4053
4160
|
};
|
|
4054
4161
|
try {
|
|
4055
4162
|
const { body } = await publish(store, publication);
|
|
4056
|
-
if (body.status === "refreshed") outcomes[candidate.route] = {
|
|
4163
|
+
if (body.status === "refreshed" && !body.duplicate) outcomes[candidate.route] = {
|
|
4057
4164
|
change: candidate.change,
|
|
4058
4165
|
outcome: "refreshed"
|
|
4059
4166
|
};
|
|
4060
|
-
else if (body.status === "superseded") superseded.push(candidate);
|
|
4167
|
+
else if (body.status === "refreshed" || body.status === "superseded") superseded.push(candidate);
|
|
4061
4168
|
else outcomes[candidate.route] = {
|
|
4062
4169
|
change: candidate.change,
|
|
4063
4170
|
outcome: "kept",
|
|
@@ -4134,7 +4241,7 @@ function configuredReconciliation(languages, delivery, wordpress) {
|
|
|
4134
4241
|
//#endregion
|
|
4135
4242
|
//#region frontend/src/events.ts
|
|
4136
4243
|
function configuredEvents(config, languages, delivery, wordpress) {
|
|
4137
|
-
const { reconcile } = configuredReconciliation(languages, delivery, wordpress);
|
|
4244
|
+
const { reconcile } = configuredReconciliation(languages, delivery, wordpress, Object.keys(config.collections ?? {}).length > 0);
|
|
4138
4245
|
const SITE = config.site;
|
|
4139
4246
|
const isLanguageHome$3 = (route) => isLanguageHome(route, languages);
|
|
4140
4247
|
const routeLanguage$3 = (route) => routeLanguage(route, languages);
|
|
@@ -4211,15 +4318,19 @@ function configuredEvents(config, languages, delivery, wordpress) {
|
|
|
4211
4318
|
* The shared settings an event can name, and what each change refreshes: the
|
|
4212
4319
|
* menus and the logo are in the chrome every page is served with; the site's
|
|
4213
4320
|
* identity is its title and tagline (the front page's) and its icon (the
|
|
4214
|
-
* 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.
|
|
4215
4324
|
*/
|
|
4216
4325
|
const SETTINGS = {
|
|
4217
4326
|
menus: ["chrome"],
|
|
4218
4327
|
logo: ["chrome"],
|
|
4219
4328
|
identity: ["home", "chrome"],
|
|
4220
4329
|
design: ["design"],
|
|
4330
|
+
patterns: ["chrome"],
|
|
4221
4331
|
legal: ["chrome"],
|
|
4222
|
-
analytics: ["chrome"]
|
|
4332
|
+
analytics: ["chrome"],
|
|
4333
|
+
archives: ["archives"]
|
|
4223
4334
|
};
|
|
4224
4335
|
/**
|
|
4225
4336
|
* What a change in one language refreshes, for the settings a language has
|
|
@@ -4565,7 +4676,7 @@ function configuredEvents(config, languages, delivery, wordpress) {
|
|
|
4565
4676
|
await store.startEvent(event.id);
|
|
4566
4677
|
const language = languages.find((each) => each.slug === event.language);
|
|
4567
4678
|
const { refreshed, ...parts } = await refreshShared(store, language ? [...LANGUAGE_SETTINGS[event.setting]] : [...SETTINGS[event.setting]], siteOrigin, language);
|
|
4568
|
-
const failure = sharedOutcomes(parts).find((outcome) => outcome.outcome === "kept");
|
|
4679
|
+
const failure = [...sharedOutcomes(parts), ...Object.values(parts.entries ?? {})].find((outcome) => outcome.outcome === "kept");
|
|
4569
4680
|
await store.finishEvent(recorded, failure?.outcome === "kept" ? {
|
|
4570
4681
|
status: "failed",
|
|
4571
4682
|
reason: failure.failure.reason,
|
|
@@ -4661,10 +4772,32 @@ function configuredRefresh(languages, delivery) {
|
|
|
4661
4772
|
function publishedHtml(languages, transform) {
|
|
4662
4773
|
const defaultLanguage = languages[0];
|
|
4663
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
|
+
}
|
|
4664
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
|
+
};
|
|
4665
4799
|
if (!transform || delivery.kind !== "found") return delivery;
|
|
4666
4800
|
const { home } = delivery.content;
|
|
4667
|
-
const served = languageOf(language.slug, defaultLanguage);
|
|
4668
4801
|
return {
|
|
4669
4802
|
...delivery,
|
|
4670
4803
|
content: {
|
|
@@ -4675,11 +4808,16 @@ function publishedHtml(languages, transform) {
|
|
|
4675
4808
|
...home.page,
|
|
4676
4809
|
content: transform(home.page.content, served)
|
|
4677
4810
|
}
|
|
4678
|
-
}
|
|
4811
|
+
},
|
|
4812
|
+
chrome: chrome(delivery.content.chrome, served)
|
|
4679
4813
|
}
|
|
4680
4814
|
};
|
|
4681
4815
|
}
|
|
4682
4816
|
function entry(delivery, routeLanguage) {
|
|
4817
|
+
if (transform && delivery.kind === "missing") return {
|
|
4818
|
+
...delivery,
|
|
4819
|
+
chrome: chrome(delivery.chrome, routeLanguage)
|
|
4820
|
+
};
|
|
4683
4821
|
if (!transform || delivery.kind !== "found") return delivery;
|
|
4684
4822
|
const { entry } = delivery.content;
|
|
4685
4823
|
const served = languageOf(entry.language, routeLanguage);
|
|
@@ -4690,24 +4828,28 @@ function publishedHtml(languages, transform) {
|
|
|
4690
4828
|
entry: {
|
|
4691
4829
|
...entry,
|
|
4692
4830
|
content: transform(entry.content, served)
|
|
4693
|
-
}
|
|
4831
|
+
},
|
|
4832
|
+
chrome: chrome(delivery.content.chrome, served)
|
|
4694
4833
|
}
|
|
4695
4834
|
};
|
|
4696
4835
|
}
|
|
4697
4836
|
return {
|
|
4698
4837
|
home,
|
|
4699
|
-
entry
|
|
4838
|
+
entry,
|
|
4839
|
+
chrome
|
|
4700
4840
|
};
|
|
4701
4841
|
}
|
|
4702
4842
|
//#endregion
|
|
4703
4843
|
//#region frontend/src/delivery.ts
|
|
4704
4844
|
function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
4705
4845
|
const defaultLanguage = languages[0];
|
|
4846
|
+
const collections = config.collections ?? {};
|
|
4847
|
+
const hasCollections = Object.keys(collections).length > 0;
|
|
4706
4848
|
const html = publishedHtml(languages, transformHtml);
|
|
4707
4849
|
const multilingual = languages.length > 1;
|
|
4708
4850
|
const routeLanguage$1 = (route) => routeLanguage(route, languages);
|
|
4709
4851
|
const isLanguageHome$1 = (route) => isLanguageHome(route, languages);
|
|
4710
|
-
const { getHomeContent, getEntryByUri, getSiteChrome, getDesignPresets, getPublishedRoutes } = wordpress;
|
|
4852
|
+
const { getHomeContent, getEntryByUri, getSiteChrome, getDesignPresets, getPublishedRoutes, getArchives } = wordpress;
|
|
4711
4853
|
const HOME = "home";
|
|
4712
4854
|
const CHROME = "chrome";
|
|
4713
4855
|
/**
|
|
@@ -4719,6 +4861,8 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4719
4861
|
const chromeKey = (language) => language.isDefault ? CHROME : `${CHROME}:${language.slug}`;
|
|
4720
4862
|
/** The design presets every page is served with, once a refresh has stored them. */
|
|
4721
4863
|
const DESIGN = "design";
|
|
4864
|
+
/** Where each collection's archive is, on a Site with collections. */
|
|
4865
|
+
const ARCHIVES = "archives";
|
|
4722
4866
|
/** Entries are stored at "entry:<route>". */
|
|
4723
4867
|
const ENTRY = "entry:";
|
|
4724
4868
|
const entryKey = (route) => `${ENTRY}${route}`;
|
|
@@ -4785,7 +4929,24 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4785
4929
|
uri: stringType(),
|
|
4786
4930
|
language: stringType()
|
|
4787
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
|
+
}));
|
|
4788
4948
|
const entryPublication = objectType({
|
|
4949
|
+
collection: membership.optional(),
|
|
4789
4950
|
language: stringType().optional(),
|
|
4790
4951
|
translations: arrayType(translation).optional(),
|
|
4791
4952
|
id: stringType(),
|
|
@@ -4816,10 +4977,22 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4816
4977
|
roles: arrayType(legalRole),
|
|
4817
4978
|
missingRequired: arrayType(stringType())
|
|
4818
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();
|
|
4819
4990
|
const siteChrome = objectType({
|
|
4820
4991
|
menuItems: arrayType(menuItem),
|
|
4821
4992
|
siteIcon: image,
|
|
4822
4993
|
siteLogo: image,
|
|
4994
|
+
header: designPattern.optional().transform((pattern) => pattern ?? null),
|
|
4995
|
+
footer: designPattern.optional().transform((pattern) => pattern ?? null),
|
|
4823
4996
|
legal: legal.optional(),
|
|
4824
4997
|
analytics: objectType({
|
|
4825
4998
|
enabled: booleanType(),
|
|
@@ -4830,6 +5003,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4830
5003
|
spacingSizes,
|
|
4831
5004
|
colors
|
|
4832
5005
|
});
|
|
5006
|
+
const archives = recordType(stringType());
|
|
4833
5007
|
const parseWith = (schema) => (body) => {
|
|
4834
5008
|
const parsed = schema.safeParse(body);
|
|
4835
5009
|
return parsed.success ? parsed.data : void 0;
|
|
@@ -4838,10 +5012,13 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
4838
5012
|
const parseChrome = parseWith(siteChrome);
|
|
4839
5013
|
const parseEntry = parseWith(entryPublication);
|
|
4840
5014
|
const parseDesign = parseWith(designPresets);
|
|
5015
|
+
const parseArchives = parseWith(archives);
|
|
4841
5016
|
const emptyChrome = {
|
|
4842
5017
|
menuItems: [],
|
|
4843
5018
|
siteIcon: null,
|
|
4844
|
-
siteLogo: null
|
|
5019
|
+
siteLogo: null,
|
|
5020
|
+
header: null,
|
|
5021
|
+
footer: null
|
|
4845
5022
|
};
|
|
4846
5023
|
async function boundStore() {
|
|
4847
5024
|
const { PUBLICATION_DB } = await config.bindings();
|
|
@@ -5185,16 +5362,18 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5185
5362
|
async function refreshSite(store, siteOrigin) {
|
|
5186
5363
|
const readStartedAt = Date.now();
|
|
5187
5364
|
const others = languages.filter((language) => !language.isDefault);
|
|
5188
|
-
const [home, chrome, design, listing, ...otherReads] = await Promise.all([
|
|
5365
|
+
const [home, chrome, design, archiveRead, listing, ...otherReads] = await Promise.all([
|
|
5189
5366
|
getHomeContent(),
|
|
5190
5367
|
getSiteChrome(siteOrigin),
|
|
5191
5368
|
getDesignPresets(),
|
|
5369
|
+
hasCollections ? getArchives() : void 0,
|
|
5192
5370
|
getPublishedRoutes(),
|
|
5193
5371
|
...others.map((language) => Promise.all([getHomeContent(language), getSiteChrome(siteOrigin, { language })]))
|
|
5194
5372
|
]);
|
|
5195
5373
|
const homeOutcome = await promoteHome(store, home, readStartedAt);
|
|
5196
5374
|
const chromeOutcome = await promoteChrome(store, chrome, readStartedAt);
|
|
5197
5375
|
const designOutcome = await promoteDesign(store, design, readStartedAt);
|
|
5376
|
+
const archivesOutcome = archiveRead ? await promoteArchives(store, archiveRead, readStartedAt) : void 0;
|
|
5198
5377
|
const languageOutcomes = {};
|
|
5199
5378
|
for (const [index, language] of others.entries()) {
|
|
5200
5379
|
const [languageHome, languageChrome] = otherReads[index];
|
|
@@ -5234,10 +5413,11 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5234
5413
|
}
|
|
5235
5414
|
return {
|
|
5236
5415
|
ready,
|
|
5237
|
-
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,
|
|
5238
5417
|
home: homeOutcome,
|
|
5239
5418
|
chrome: chromeOutcome,
|
|
5240
5419
|
design: designOutcome,
|
|
5420
|
+
...archivesOutcome ? { archives: archivesOutcome } : {},
|
|
5241
5421
|
routes,
|
|
5242
5422
|
entries: entries.entries,
|
|
5243
5423
|
moved: entries.moved,
|
|
@@ -5250,6 +5430,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5250
5430
|
rows.home,
|
|
5251
5431
|
rows.chrome,
|
|
5252
5432
|
rows.design,
|
|
5433
|
+
rows.archives,
|
|
5253
5434
|
...Object.values(rows.languages ?? {}).flatMap((language) => [language.home, language.chrome])
|
|
5254
5435
|
].filter((outcome) => outcome !== void 0);
|
|
5255
5436
|
}
|
|
@@ -5269,7 +5450,11 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5269
5450
|
const readStartedAt = Date.now();
|
|
5270
5451
|
const wanted = new Set(parts);
|
|
5271
5452
|
const scope = language ? [language] : languages;
|
|
5272
|
-
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
|
+
]);
|
|
5273
5458
|
const report = { refreshed: true };
|
|
5274
5459
|
for (const [index, each] of scope.entries()) {
|
|
5275
5460
|
const [home, chrome] = reads[index];
|
|
@@ -5278,7 +5463,15 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5278
5463
|
if (chrome) rows.chrome = await promoteChrome(store, chrome, readStartedAt, each);
|
|
5279
5464
|
}
|
|
5280
5465
|
if (design) report.design = await promoteDesign(store, design, readStartedAt);
|
|
5281
|
-
|
|
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");
|
|
5282
5475
|
return report;
|
|
5283
5476
|
}
|
|
5284
5477
|
/**
|
|
@@ -5305,7 +5498,11 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5305
5498
|
*/
|
|
5306
5499
|
async function reconcileShared(store, siteOrigin) {
|
|
5307
5500
|
const readStartedAt = Date.now();
|
|
5308
|
-
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
|
+
]);
|
|
5309
5506
|
const unchanged = async (key, parse, read) => {
|
|
5310
5507
|
if (!read) return false;
|
|
5311
5508
|
try {
|
|
@@ -5338,10 +5535,22 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5338
5535
|
chrome: await unchanged(chromeKey(language), parseChrome, sharedRead(chrome)) ? { outcome: "unchanged" } : await promoteChrome(store, chrome, readStartedAt, language)
|
|
5339
5536
|
});
|
|
5340
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
|
+
}
|
|
5341
5548
|
const [own, ...others] = outcomes;
|
|
5342
5549
|
return {
|
|
5343
5550
|
...own,
|
|
5344
5551
|
design: await unchanged(DESIGN, parseDesign, sharedRead(design)) ? { outcome: "unchanged" } : await promoteDesign(store, design, readStartedAt),
|
|
5552
|
+
...archives ? { archives } : {},
|
|
5553
|
+
...changedArchives.length > 0 ? { archiveRoutes: changedArchives } : {},
|
|
5345
5554
|
...multilingual ? { languages: Object.fromEntries(others.map((outcome, index) => [languages[index + 1].slug, outcome])) } : {}
|
|
5346
5555
|
};
|
|
5347
5556
|
}
|
|
@@ -5365,6 +5574,36 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5365
5574
|
function promoteDesign(store, read, readStartedAt) {
|
|
5366
5575
|
return promoteShared(store, DESIGN, "the shared design presets", parseDesign, read, readStartedAt);
|
|
5367
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
|
+
}
|
|
5368
5607
|
/**
|
|
5369
5608
|
* Refreshes a language's front page only (not its chrome or the entries): the
|
|
5370
5609
|
* front page itself was published or changed.
|
|
@@ -5465,7 +5704,7 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5465
5704
|
}
|
|
5466
5705
|
const outcome = await promote(canonical, () => ({
|
|
5467
5706
|
state: "published",
|
|
5468
|
-
content: entry,
|
|
5707
|
+
content: stored(entry),
|
|
5469
5708
|
nodeId: entry.id,
|
|
5470
5709
|
modifiedAt
|
|
5471
5710
|
}));
|
|
@@ -5490,6 +5729,18 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5490
5729
|
found
|
|
5491
5730
|
};
|
|
5492
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
|
+
}
|
|
5493
5744
|
/**
|
|
5494
5745
|
* Every other route still serving an entry that was just found at its own
|
|
5495
5746
|
* route is marked moved there, unless it was read since `readBefore`: a
|
|
@@ -5613,6 +5864,151 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5613
5864
|
withdrawn
|
|
5614
5865
|
};
|
|
5615
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
|
+
}
|
|
5616
6012
|
/** The refresh's credential when the Worker has one bound, and it is strong enough. */
|
|
5617
6013
|
async function refreshAuthority() {
|
|
5618
6014
|
const { PUBLICATION_DB, FRONTEND_REFRESH_TOKEN: token } = await config.bindings();
|
|
@@ -5646,13 +6042,25 @@ function configuredDelivery(config, languages, wordpress, transformHtml) {
|
|
|
5646
6042
|
isAuthorized,
|
|
5647
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")),
|
|
5648
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")),
|
|
5649
|
-
pageChrome: (siteOrigin) => safely(() => pageChrome(siteOrigin), () => null),
|
|
5650
|
-
designPresets: () => safely(() => pageDesign(), () => 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
|
+
}
|
|
5651
6053
|
};
|
|
5652
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
|
+
}
|
|
5653
6059
|
//#endregion
|
|
5654
6060
|
//#region frontend/src/wordpress.ts
|
|
5655
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";
|
|
5656
6064
|
function internalOrigins(config, siteOrigin) {
|
|
5657
6065
|
return [new URL(config.graphqlEndpoint).origin, ...siteOrigin ? [siteOrigin] : []];
|
|
5658
6066
|
}
|
|
@@ -5660,12 +6068,17 @@ function configuredWordPress(config, languages) {
|
|
|
5660
6068
|
const defaultLanguage = languages[0];
|
|
5661
6069
|
const multilingual = languages.length > 1;
|
|
5662
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();
|
|
5663
6076
|
/**
|
|
5664
6077
|
* The Site's extension fields on a kind of node, read with the blocks (left
|
|
5665
6078
|
* out with them when WordPress fails on them).
|
|
5666
6079
|
*/
|
|
5667
|
-
const extensionFields = (kind) => {
|
|
5668
|
-
const fields =
|
|
6080
|
+
const extensionFields = (kind, collection) => {
|
|
6081
|
+
const fields = extensionSelection(kind, collection);
|
|
5669
6082
|
return fields ? `\n... @include(if: $withBlocks) {\n${fields}\n}` : "";
|
|
5670
6083
|
};
|
|
5671
6084
|
const image = objectType({
|
|
@@ -5726,6 +6139,32 @@ function configuredWordPress(config, languages) {
|
|
|
5726
6139
|
pageBy: pageEntry.nullable(),
|
|
5727
6140
|
designTokens
|
|
5728
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
|
+
});
|
|
5729
6168
|
const translatedEntryData = objectType({
|
|
5730
6169
|
postBy: postEntry.extend(translated).nullable(),
|
|
5731
6170
|
pageBy: pageEntry.extend(translated).nullable(),
|
|
@@ -5754,13 +6193,21 @@ function configuredWordPress(config, languages) {
|
|
|
5754
6193
|
modifiedGmt: stringType().nullable().optional()
|
|
5755
6194
|
}))
|
|
5756
6195
|
}) });
|
|
5757
|
-
const
|
|
6196
|
+
const designPattern = objectType({
|
|
6197
|
+
title: stringType(),
|
|
6198
|
+
blocksJson: stringType()
|
|
6199
|
+
}).nullable();
|
|
6200
|
+
const chromeWithoutPatternsData = objectType({
|
|
5758
6201
|
generalSettings: objectType({
|
|
5759
6202
|
siteIcon: imageEdge,
|
|
5760
6203
|
siteLogo: imageEdge
|
|
5761
6204
|
}),
|
|
5762
6205
|
menuItems: objectType({ nodes: arrayType(menuItem) })
|
|
5763
6206
|
});
|
|
6207
|
+
const siteChromeData = chromeWithoutPatternsData.extend({
|
|
6208
|
+
header: designPattern,
|
|
6209
|
+
footer: designPattern
|
|
6210
|
+
});
|
|
5764
6211
|
const consentSettingsData = objectType({
|
|
5765
6212
|
getquickLegal: unknownType(),
|
|
5766
6213
|
googleAnalytics: objectType({
|
|
@@ -5850,9 +6297,13 @@ function configuredWordPress(config, languages) {
|
|
|
5850
6297
|
}
|
|
5851
6298
|
}
|
|
5852
6299
|
`;
|
|
5853
|
-
function siteChromeQueryText(inLanguage) {
|
|
6300
|
+
function siteChromeQueryText(inLanguage, withPatterns = true) {
|
|
5854
6301
|
return `
|
|
5855
|
-
query SiteChrome($
|
|
6302
|
+
query SiteChrome(${[
|
|
6303
|
+
"$location: MenuLocationEnum!",
|
|
6304
|
+
...withPatterns ? ["$template: String!"] : [],
|
|
6305
|
+
...inLanguage ? ["$language: LanguageCodeEnum!"] : []
|
|
6306
|
+
].join(", ")}) {
|
|
5856
6307
|
generalSettings {
|
|
5857
6308
|
siteIcon {
|
|
5858
6309
|
node {
|
|
@@ -5876,6 +6327,14 @@ function configuredWordPress(config, languages) {
|
|
|
5876
6327
|
target
|
|
5877
6328
|
}
|
|
5878
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
|
+
}` : ""}
|
|
5879
6338
|
}
|
|
5880
6339
|
`;
|
|
5881
6340
|
}
|
|
@@ -5945,6 +6404,73 @@ function configuredWordPress(config, languages) {
|
|
|
5945
6404
|
}
|
|
5946
6405
|
const entryQuery = entryQueryText(false);
|
|
5947
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);
|
|
5948
6474
|
const designQuery = `
|
|
5949
6475
|
query DesignPresets {
|
|
5950
6476
|
designTokens {
|
|
@@ -5959,10 +6485,19 @@ function configuredWordPress(config, languages) {
|
|
|
5959
6485
|
}
|
|
5960
6486
|
}
|
|
5961
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
|
+
];
|
|
5962
6497
|
function publishedRoutesQueryText(everyLanguage) {
|
|
5963
6498
|
return `
|
|
5964
6499
|
query PublishedRoutes($after: String) {
|
|
5965
|
-
contentNodes(first: 100, after: $after, where: { contentTypes: [
|
|
6500
|
+
contentNodes(first: 100, after: $after, where: { contentTypes: [${contentTypes.join(", ")}]${everyLanguage ? ", language: ALL" : ""} }) {
|
|
5966
6501
|
pageInfo {
|
|
5967
6502
|
hasNextPage
|
|
5968
6503
|
endCursor
|
|
@@ -6012,18 +6547,22 @@ function configuredWordPress(config, languages) {
|
|
|
6012
6547
|
* nothing: without an extension, fields for its kind, or a value, and when
|
|
6013
6548
|
* the blocks (and with them the fields) were left out.
|
|
6014
6549
|
*/
|
|
6015
|
-
function extended(kind, node, blocksOmitted) {
|
|
6016
|
-
if (!extension || !
|
|
6550
|
+
function extended(kind, node, blocksOmitted, collection) {
|
|
6551
|
+
if (!extension || !extensionSelection(kind, collection) || blocksOmitted) return {};
|
|
6017
6552
|
let value;
|
|
6018
6553
|
try {
|
|
6019
|
-
value = extension.read({
|
|
6554
|
+
value = extension.read(collection === void 0 ? {
|
|
6020
6555
|
kind,
|
|
6021
6556
|
fields: node
|
|
6557
|
+
} : {
|
|
6558
|
+
kind,
|
|
6559
|
+
collection,
|
|
6560
|
+
fields: node
|
|
6022
6561
|
});
|
|
6023
6562
|
} catch (error) {
|
|
6024
6563
|
throw new CmsFailureError({
|
|
6025
6564
|
reason: "schema",
|
|
6026
|
-
message: `WordPress's ${kind} doesn't match the Site's extension: ${error instanceof Error ? error.message : String(error)}`
|
|
6565
|
+
message: `WordPress's ${collection === void 0 ? kind : `${collection} item`} doesn't match the Site's extension: ${error instanceof Error ? error.message : String(error)}`
|
|
6027
6566
|
}, { cause: error });
|
|
6028
6567
|
}
|
|
6029
6568
|
return value === void 0 ? {} : { extension: value };
|
|
@@ -6203,6 +6742,7 @@ function configuredWordPress(config, languages) {
|
|
|
6203
6742
|
* what any visitor may read.
|
|
6204
6743
|
*/
|
|
6205
6744
|
async function getEntryByUri(uri) {
|
|
6745
|
+
if (collections.length > 0) return getNodeByUri(uri);
|
|
6206
6746
|
return deliver(`the entry ${uri}`, async () => {
|
|
6207
6747
|
const { data, blocksOmitted } = multilingual ? await queryWithBlockRecovery(translatedEntryData, translatedEntryQuery, { uri }) : await queryWithBlockRecovery(entryData, entryQuery, { uri });
|
|
6208
6748
|
const post = data.postBy ?? data.pageBy;
|
|
@@ -6226,6 +6766,59 @@ function configuredWordPress(config, languages) {
|
|
|
6226
6766
|
};
|
|
6227
6767
|
});
|
|
6228
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)
|
|
6811
|
+
}
|
|
6812
|
+
};
|
|
6813
|
+
});
|
|
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
|
+
}
|
|
6229
6822
|
/** An entry's language and published translations, as a multilingual Site stores them. */
|
|
6230
6823
|
function languageOf(post) {
|
|
6231
6824
|
return {
|
|
@@ -6287,6 +6880,93 @@ function configuredWordPress(config, languages) {
|
|
|
6287
6880
|
});
|
|
6288
6881
|
});
|
|
6289
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
|
+
}
|
|
6290
6970
|
/** The URI of every published page and post. */
|
|
6291
6971
|
async function getPublishedRoutes() {
|
|
6292
6972
|
const listed = await getPublishedEntries();
|
|
@@ -6338,17 +7018,68 @@ function configuredWordPress(config, languages) {
|
|
|
6338
7018
|
}
|
|
6339
7019
|
}
|
|
6340
7020
|
/**
|
|
6341
|
-
* The
|
|
6342
|
-
*
|
|
6343
|
-
*
|
|
6344
|
-
*
|
|
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.
|
|
6345
7079
|
*/
|
|
6346
7080
|
async function getSiteChrome(siteOrigin, { location = "PRIMARY", language = defaultLanguage } = {}) {
|
|
6347
7081
|
return deliver(languageSubject(language, "site chrome"), async () => {
|
|
6348
|
-
const [data, consent] = await Promise.all([language
|
|
6349
|
-
location,
|
|
6350
|
-
language: language.code
|
|
6351
|
-
}), config.consent?.cmsSettings ? consentSettings(language) : null]);
|
|
7082
|
+
const [data, consent] = await Promise.all([chromeData(language, location), config.consent?.cmsSettings ? consentSettings(language) : null]);
|
|
6352
7083
|
const origins = internalOrigins(config, siteOrigin);
|
|
6353
7084
|
return {
|
|
6354
7085
|
kind: "found",
|
|
@@ -6356,6 +7087,8 @@ function configuredWordPress(config, languages) {
|
|
|
6356
7087
|
menuItems: buildMenuTree(data.menuItems.nodes, origins),
|
|
6357
7088
|
siteIcon: normalizeImage(data.generalSettings.siteIcon),
|
|
6358
7089
|
siteLogo: normalizeImage(data.generalSettings.siteLogo),
|
|
7090
|
+
header: designPatternOf("header", data.header),
|
|
7091
|
+
footer: designPatternOf("footer", data.footer),
|
|
6359
7092
|
...consent ? {
|
|
6360
7093
|
legal: parseLegalLanguage(consent.getquickLegal),
|
|
6361
7094
|
analytics: consent.googleAnalytics ?? {
|
|
@@ -6373,6 +7106,7 @@ function configuredWordPress(config, languages) {
|
|
|
6373
7106
|
getDesignPresets,
|
|
6374
7107
|
getPublishedEntries,
|
|
6375
7108
|
getPublishedRoutes,
|
|
7109
|
+
getArchives,
|
|
6376
7110
|
getSiteChrome
|
|
6377
7111
|
};
|
|
6378
7112
|
}
|
|
@@ -6381,7 +7115,10 @@ function configuredWordPress(config, languages) {
|
|
|
6381
7115
|
function createContentRuntime(input) {
|
|
6382
7116
|
const site = input.site.trim();
|
|
6383
7117
|
if (!site) throw new Error("A nonempty Site identity is required");
|
|
6384
|
-
|
|
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 ?? {});
|
|
6385
7122
|
const config = {
|
|
6386
7123
|
...input,
|
|
6387
7124
|
site,
|
|
@@ -6436,8 +7173,37 @@ function createContentRuntime(input) {
|
|
|
6436
7173
|
handleRefresh: guarded("Refresh", "Refresh with POST.", refresh.handleRefresh),
|
|
6437
7174
|
publishedHome: delivery.publishedHome,
|
|
6438
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,
|
|
6439
7193
|
pageChrome: delivery.pageChrome,
|
|
6440
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
|
+
/**
|
|
6441
7207
|
* The design presets every page shares (GQ Design's spacing sizes and
|
|
6442
7208
|
* colors), for a page that isn't a publication: the stored ones when there
|
|
6443
7209
|
* is a store, else (in explicit development) a live read. Null when
|
|
@@ -6461,5 +7227,27 @@ function createContentRuntime(input) {
|
|
|
6461
7227
|
}) : null
|
|
6462
7228
|
};
|
|
6463
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
|
+
}
|
|
6464
7252
|
//#endregion
|
|
6465
7253
|
export { createContentRuntime };
|