@canopy-iiif/app 1.5.4 → 1.5.6
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/lib/build/iiif.js +221 -32
- package/lib/build/pages.js +14 -0
- package/lib/head.js +9 -2
- package/lib/iiif/thumbnail.js +48 -0
- package/lib/search/search.js +15 -1
- package/package.json +1 -1
package/lib/build/iiif.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
buildIiifImageUrlForDimensions,
|
|
29
29
|
findPrimaryCanvasImage,
|
|
30
30
|
buildIiifImageSrcset,
|
|
31
|
+
isLevel0Service,
|
|
31
32
|
} = require("../iiif/thumbnail");
|
|
32
33
|
|
|
33
34
|
const IIIF_CACHE_DIR = path.resolve(".cache/iiif");
|
|
@@ -118,6 +119,67 @@ function normalizeSlugBase(value, fallback) {
|
|
|
118
119
|
return clampSlugLength(safeFallback, MAX_ENTRY_SLUG_LENGTH) || safeFallback;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
function manifestHrefFromSlug(slug) {
|
|
123
|
+
if (!slug) return "";
|
|
124
|
+
const rel = `works/${String(slug).trim()}.html`;
|
|
125
|
+
return rootRelativeHref(rel);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function extractHomepageId(resource) {
|
|
129
|
+
if (!resource) return "";
|
|
130
|
+
const homepageRaw = resource.homepage;
|
|
131
|
+
const list = Array.isArray(homepageRaw)
|
|
132
|
+
? homepageRaw
|
|
133
|
+
: homepageRaw
|
|
134
|
+
? [homepageRaw]
|
|
135
|
+
: [];
|
|
136
|
+
for (const entry of list) {
|
|
137
|
+
if (!entry) continue;
|
|
138
|
+
if (typeof entry === "string") {
|
|
139
|
+
const trimmed = entry.trim();
|
|
140
|
+
if (trimmed) return trimmed;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (typeof entry === "object") {
|
|
144
|
+
const id = entry.id || entry["@id"];
|
|
145
|
+
if (typeof id === "string" && id.trim()) return id.trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveManifestCanonical(manifest, slug) {
|
|
152
|
+
const homepageId = extractHomepageId(manifest);
|
|
153
|
+
if (homepageId) return homepageId;
|
|
154
|
+
return manifestHrefFromSlug(slug);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveCollectionCanonical(collection) {
|
|
158
|
+
const homepageId = extractHomepageId(collection);
|
|
159
|
+
if (homepageId) return homepageId;
|
|
160
|
+
const id = collection && (collection.id || collection["@id"]);
|
|
161
|
+
return typeof id === "string" ? id : "";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function assignEntryCanonical(entry, canonical) {
|
|
165
|
+
if (!entry || typeof entry !== "object") return "";
|
|
166
|
+
const value = canonical ? String(canonical) : "";
|
|
167
|
+
entry.canonical = value;
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function applyManifestEntryCanonical(entry, manifest, slug) {
|
|
172
|
+
if (!entry || entry.type !== "Manifest") return "";
|
|
173
|
+
const canonical = resolveManifestCanonical(manifest, slug);
|
|
174
|
+
return assignEntryCanonical(entry, canonical);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function applyCollectionEntryCanonical(entry, collection) {
|
|
178
|
+
if (!entry || entry.type !== "Collection") return "";
|
|
179
|
+
const canonical = resolveCollectionCanonical(collection);
|
|
180
|
+
return assignEntryCanonical(entry, canonical);
|
|
181
|
+
}
|
|
182
|
+
|
|
121
183
|
function buildSlugWithSuffix(base, fallback, counter) {
|
|
122
184
|
const suffix = `-${counter}`;
|
|
123
185
|
const baseLimit = Math.max(1, MAX_ENTRY_SLUG_LENGTH - suffix.length);
|
|
@@ -167,9 +229,29 @@ function ensureThumbnailValue(target, url, width, height) {
|
|
|
167
229
|
return true;
|
|
168
230
|
}
|
|
169
231
|
|
|
232
|
+
function extractResourceThumbnail(resource) {
|
|
233
|
+
try {
|
|
234
|
+
const rawThumb = resource && resource.thumbnail;
|
|
235
|
+
const first = Array.isArray(rawThumb) ? rawThumb[0] : rawThumb;
|
|
236
|
+
if (!first) return null;
|
|
237
|
+
if (typeof first === "string") {
|
|
238
|
+
const trimmed = first.trim();
|
|
239
|
+
return trimmed ? {url: trimmed} : null;
|
|
240
|
+
}
|
|
241
|
+
const id = first.id || first["@id"];
|
|
242
|
+
if (!id) return null;
|
|
243
|
+
const width = typeof first.width === "number" ? first.width : undefined;
|
|
244
|
+
const height = typeof first.height === "number" ? first.height : undefined;
|
|
245
|
+
return {url: String(id), width, height};
|
|
246
|
+
} catch (_) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
170
251
|
async function resolveHeroMedia(manifest) {
|
|
171
252
|
if (!manifest) return null;
|
|
172
253
|
try {
|
|
254
|
+
const manifestThumb = extractResourceThumbnail(manifest);
|
|
173
255
|
const heroSource = (() => {
|
|
174
256
|
if (manifest && manifest.thumbnail) {
|
|
175
257
|
const clone = { ...manifest };
|
|
@@ -191,15 +273,11 @@ async function resolveHeroMedia(manifest) {
|
|
|
191
273
|
const heroService =
|
|
192
274
|
(canvasImage && canvasImage.service) ||
|
|
193
275
|
(heroRep && heroRep.service);
|
|
276
|
+
const serviceIsLevel0 = isLevel0Service(heroService);
|
|
194
277
|
const heroPreferred = buildIiifImageUrlFromService(
|
|
195
|
-
heroService,
|
|
278
|
+
serviceIsLevel0 ? null : heroService,
|
|
196
279
|
HERO_THUMBNAIL_SIZE
|
|
197
280
|
);
|
|
198
|
-
const heroFallbackId = (() => {
|
|
199
|
-
if (canvasImage && canvasImage.id) return String(canvasImage.id);
|
|
200
|
-
if (heroRep && heroRep.id) return String(heroRep.id);
|
|
201
|
-
return '';
|
|
202
|
-
})();
|
|
203
281
|
const heroWidth = (() => {
|
|
204
282
|
if (canvasImage && typeof canvasImage.width === 'number')
|
|
205
283
|
return canvasImage.width;
|
|
@@ -213,21 +291,59 @@ async function resolveHeroMedia(manifest) {
|
|
|
213
291
|
return heroRep.height;
|
|
214
292
|
return undefined;
|
|
215
293
|
})();
|
|
216
|
-
const heroSrcset =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
294
|
+
const heroSrcset = serviceIsLevel0
|
|
295
|
+
? ''
|
|
296
|
+
: buildIiifImageSrcset(heroService);
|
|
297
|
+
const ogFromService =
|
|
298
|
+
!serviceIsLevel0 && heroService
|
|
299
|
+
? buildIiifImageUrlForDimensions(
|
|
300
|
+
heroService,
|
|
301
|
+
OG_IMAGE_WIDTH,
|
|
302
|
+
OG_IMAGE_HEIGHT
|
|
303
|
+
)
|
|
304
|
+
: '';
|
|
305
|
+
const annotationImageId =
|
|
306
|
+
canvasImage && canvasImage.isImageBody && canvasImage.id
|
|
307
|
+
? String(canvasImage.id)
|
|
308
|
+
: '';
|
|
309
|
+
let heroThumbnail = heroPreferred || '';
|
|
310
|
+
let heroThumbWidth = heroWidth;
|
|
311
|
+
let heroThumbHeight = heroHeight;
|
|
312
|
+
if (!heroThumbnail && manifestThumb && manifestThumb.url) {
|
|
313
|
+
heroThumbnail = manifestThumb.url;
|
|
314
|
+
if (typeof manifestThumb.width === 'number')
|
|
315
|
+
heroThumbWidth = manifestThumb.width;
|
|
316
|
+
if (typeof manifestThumb.height === 'number')
|
|
317
|
+
heroThumbHeight = manifestThumb.height;
|
|
318
|
+
}
|
|
319
|
+
if (!heroThumbnail) {
|
|
320
|
+
if (annotationImageId) {
|
|
321
|
+
heroThumbnail = annotationImageId;
|
|
322
|
+
} else if (!serviceIsLevel0 && heroRep && heroRep.id) {
|
|
323
|
+
heroThumbnail = String(heroRep.id);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
let ogImage = '';
|
|
327
|
+
let ogImageWidth;
|
|
328
|
+
let ogImageHeight;
|
|
329
|
+
if (ogFromService) {
|
|
330
|
+
ogImage = ogFromService;
|
|
331
|
+
ogImageWidth = OG_IMAGE_WIDTH;
|
|
332
|
+
ogImageHeight = OG_IMAGE_HEIGHT;
|
|
333
|
+
} else if (heroThumbnail) {
|
|
334
|
+
ogImage = heroThumbnail;
|
|
335
|
+
if (typeof heroThumbWidth === 'number') ogImageWidth = heroThumbWidth;
|
|
336
|
+
if (typeof heroThumbHeight === 'number') ogImageHeight = heroThumbHeight;
|
|
337
|
+
}
|
|
224
338
|
return {
|
|
225
|
-
heroThumbnail:
|
|
226
|
-
heroThumbnailWidth:
|
|
227
|
-
heroThumbnailHeight:
|
|
339
|
+
heroThumbnail: heroThumbnail || '',
|
|
340
|
+
heroThumbnailWidth: heroThumbWidth,
|
|
341
|
+
heroThumbnailHeight: heroThumbHeight,
|
|
228
342
|
heroThumbnailSrcset: heroSrcset || '',
|
|
229
343
|
heroThumbnailSizes: heroSrcset ? HERO_IMAGE_SIZES_ATTR : '',
|
|
230
344
|
ogImage: ogImage || '',
|
|
345
|
+
ogImageWidth,
|
|
346
|
+
ogImageHeight,
|
|
231
347
|
};
|
|
232
348
|
} catch (_) {
|
|
233
349
|
return null;
|
|
@@ -834,6 +950,7 @@ async function loadCachedManifestById(id) {
|
|
|
834
950
|
slug,
|
|
835
951
|
parent: "",
|
|
836
952
|
};
|
|
953
|
+
applyManifestEntryCanonical(entry, null, slug);
|
|
837
954
|
if (existingEntryIdx >= 0) index.byId[existingEntryIdx] = entry;
|
|
838
955
|
else index.byId.push(entry);
|
|
839
956
|
await saveManifestIndex(index);
|
|
@@ -851,6 +968,21 @@ async function loadCachedManifestById(id) {
|
|
|
851
968
|
await fsp.writeFile(p, JSON.stringify(normalized, null, 2), "utf8");
|
|
852
969
|
} catch (_) {}
|
|
853
970
|
}
|
|
971
|
+
try {
|
|
972
|
+
index.byId = Array.isArray(index.byId) ? index.byId : [];
|
|
973
|
+
const nid = normalizeIiifId(id);
|
|
974
|
+
const existingEntryIdx = index.byId.findIndex(
|
|
975
|
+
(e) => e && normalizeIiifId(e.id) === nid && e.type === "Manifest"
|
|
976
|
+
);
|
|
977
|
+
if (existingEntryIdx >= 0) {
|
|
978
|
+
const entry = index.byId[existingEntryIdx];
|
|
979
|
+
const prevCanonical = entry && entry.canonical ? String(entry.canonical) : "";
|
|
980
|
+
const nextCanonical = applyManifestEntryCanonical(entry, normalized, slug);
|
|
981
|
+
if (nextCanonical !== prevCanonical) {
|
|
982
|
+
await saveManifestIndex(index);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
} catch (_) {}
|
|
854
986
|
return normalized;
|
|
855
987
|
} catch (_) {
|
|
856
988
|
return null;
|
|
@@ -880,6 +1012,7 @@ async function saveCachedManifest(manifest, id, parentId) {
|
|
|
880
1012
|
slug,
|
|
881
1013
|
parent: parentId ? String(parentId) : "",
|
|
882
1014
|
};
|
|
1015
|
+
applyManifestEntryCanonical(entry, normalizedManifest, slug);
|
|
883
1016
|
if (existingEntryIdx >= 0) index.byId[existingEntryIdx] = entry;
|
|
884
1017
|
else index.byId.push(entry);
|
|
885
1018
|
await saveManifestIndex(index);
|
|
@@ -987,14 +1120,28 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
987
1120
|
}
|
|
988
1121
|
}
|
|
989
1122
|
if (heroMedia && heroMedia.ogImage) {
|
|
990
|
-
if (entry.ogImage !== heroMedia.ogImage)
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1123
|
+
if (entry.ogImage !== heroMedia.ogImage) {
|
|
1124
|
+
entry.ogImage = heroMedia.ogImage;
|
|
1125
|
+
touched = true;
|
|
1126
|
+
}
|
|
1127
|
+
if (typeof heroMedia.ogImageWidth === 'number') {
|
|
1128
|
+
if (entry.ogImageWidth !== heroMedia.ogImageWidth) touched = true;
|
|
1129
|
+
entry.ogImageWidth = heroMedia.ogImageWidth;
|
|
1130
|
+
} else if (entry.ogImageWidth !== undefined) {
|
|
1131
|
+
delete entry.ogImageWidth;
|
|
1132
|
+
touched = true;
|
|
1133
|
+
}
|
|
1134
|
+
if (typeof heroMedia.ogImageHeight === 'number') {
|
|
1135
|
+
if (entry.ogImageHeight !== heroMedia.ogImageHeight) touched = true;
|
|
1136
|
+
entry.ogImageHeight = heroMedia.ogImageHeight;
|
|
1137
|
+
} else if (entry.ogImageHeight !== undefined) {
|
|
1138
|
+
delete entry.ogImageHeight;
|
|
1139
|
+
touched = true;
|
|
1140
|
+
}
|
|
994
1141
|
} else if (entry.ogImage !== undefined) {
|
|
995
1142
|
delete entry.ogImage;
|
|
996
|
-
delete entry.ogImageWidth;
|
|
997
|
-
delete entry.ogImageHeight;
|
|
1143
|
+
if (entry.ogImageWidth !== undefined) delete entry.ogImageWidth;
|
|
1144
|
+
if (entry.ogImageHeight !== undefined) delete entry.ogImageHeight;
|
|
998
1145
|
touched = true;
|
|
999
1146
|
}
|
|
1000
1147
|
if (
|
|
@@ -1081,6 +1228,7 @@ async function loadCachedCollectionById(id) {
|
|
|
1081
1228
|
slug,
|
|
1082
1229
|
parent: "",
|
|
1083
1230
|
};
|
|
1231
|
+
applyCollectionEntryCanonical(entry, null);
|
|
1084
1232
|
if (existing >= 0) index.byId[existing] = entry;
|
|
1085
1233
|
else index.byId.push(entry);
|
|
1086
1234
|
await saveManifestIndex(index);
|
|
@@ -1095,7 +1243,23 @@ async function loadCachedCollectionById(id) {
|
|
|
1095
1243
|
if (!slug) return null;
|
|
1096
1244
|
const p = path.join(IIIF_CACHE_COLLECTIONS_DIR, slug + ".json");
|
|
1097
1245
|
if (!fs.existsSync(p)) return null;
|
|
1098
|
-
|
|
1246
|
+
const data = await readJson(p);
|
|
1247
|
+
try {
|
|
1248
|
+
index.byId = Array.isArray(index.byId) ? index.byId : [];
|
|
1249
|
+
const nid = normalizeIiifId(id);
|
|
1250
|
+
const existingEntryIdx = index.byId.findIndex(
|
|
1251
|
+
(e) => e && normalizeIiifId(e.id) === nid && e.type === "Collection"
|
|
1252
|
+
);
|
|
1253
|
+
if (existingEntryIdx >= 0) {
|
|
1254
|
+
const entry = index.byId[existingEntryIdx];
|
|
1255
|
+
const prevCanonical = entry && entry.canonical ? String(entry.canonical) : "";
|
|
1256
|
+
const nextCanonical = applyCollectionEntryCanonical(entry, data);
|
|
1257
|
+
if (nextCanonical !== prevCanonical) {
|
|
1258
|
+
await saveManifestIndex(index);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
} catch (_) {}
|
|
1262
|
+
return data;
|
|
1099
1263
|
} catch (_) {
|
|
1100
1264
|
return null;
|
|
1101
1265
|
}
|
|
@@ -1133,6 +1297,7 @@ async function saveCachedCollection(collection, id, parentId) {
|
|
|
1133
1297
|
slug,
|
|
1134
1298
|
parent: parentId ? String(parentId) : "",
|
|
1135
1299
|
};
|
|
1300
|
+
applyCollectionEntryCanonical(entry, normalizedCollection);
|
|
1136
1301
|
if (existingEntryIdx >= 0) index.byId[existingEntryIdx] = entry;
|
|
1137
1302
|
else index.byId.push(entry);
|
|
1138
1303
|
await saveManifestIndex(index);
|
|
@@ -1183,12 +1348,14 @@ async function rebuildManifestIndexFromCache() {
|
|
|
1183
1348
|
const key = `Collection:${nid}`;
|
|
1184
1349
|
const fallback = priorMap.get(key) || {};
|
|
1185
1350
|
const parent = resolveParentFromPartOf(data) || fallback.parent || "";
|
|
1186
|
-
|
|
1351
|
+
const entry = {
|
|
1187
1352
|
id: String(nid),
|
|
1188
1353
|
type: "Collection",
|
|
1189
1354
|
slug,
|
|
1190
1355
|
parent,
|
|
1191
|
-
}
|
|
1356
|
+
};
|
|
1357
|
+
applyCollectionEntryCanonical(entry, data);
|
|
1358
|
+
nextIndex.byId.push(entry);
|
|
1192
1359
|
}
|
|
1193
1360
|
|
|
1194
1361
|
for (const name of manifestFiles) {
|
|
@@ -1214,6 +1381,7 @@ async function rebuildManifestIndexFromCache() {
|
|
|
1214
1381
|
slug,
|
|
1215
1382
|
parent,
|
|
1216
1383
|
};
|
|
1384
|
+
applyManifestEntryCanonical(entry, manifest, slug);
|
|
1217
1385
|
try {
|
|
1218
1386
|
const thumb = await getThumbnail(manifest, thumbSize, unsafeThumbs);
|
|
1219
1387
|
if (thumb && thumb.url) {
|
|
@@ -1236,8 +1404,12 @@ async function rebuildManifestIndexFromCache() {
|
|
|
1236
1404
|
}
|
|
1237
1405
|
if (heroMedia.ogImage) {
|
|
1238
1406
|
entry.ogImage = heroMedia.ogImage;
|
|
1239
|
-
|
|
1240
|
-
|
|
1407
|
+
if (typeof heroMedia.ogImageWidth === 'number')
|
|
1408
|
+
entry.ogImageWidth = heroMedia.ogImageWidth;
|
|
1409
|
+
else delete entry.ogImageWidth;
|
|
1410
|
+
if (typeof heroMedia.ogImageHeight === 'number')
|
|
1411
|
+
entry.ogImageHeight = heroMedia.ogImageHeight;
|
|
1412
|
+
else delete entry.ogImageHeight;
|
|
1241
1413
|
}
|
|
1242
1414
|
ensureThumbnailValue(
|
|
1243
1415
|
entry,
|
|
@@ -1594,12 +1766,20 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1594
1766
|
slug,
|
|
1595
1767
|
parent: parentNorm,
|
|
1596
1768
|
};
|
|
1769
|
+
applyManifestEntryCanonical(newEntry, manifest, slug);
|
|
1597
1770
|
const existingIdx = idxMap.byId.findIndex(
|
|
1598
1771
|
(e) => e && e.type === "Manifest" && normalizeIiifId(e.id) === nid
|
|
1599
1772
|
);
|
|
1600
1773
|
if (existingIdx >= 0) idxMap.byId[existingIdx] = newEntry;
|
|
1601
1774
|
else idxMap.byId.push(newEntry);
|
|
1602
1775
|
await saveManifestIndex(idxMap);
|
|
1776
|
+
mEntry = newEntry;
|
|
1777
|
+
} else if (mEntry) {
|
|
1778
|
+
const prevCanonical = mEntry.canonical || "";
|
|
1779
|
+
const nextCanonical = applyManifestEntryCanonical(mEntry, manifest, slug);
|
|
1780
|
+
if (nextCanonical !== prevCanonical) {
|
|
1781
|
+
await saveManifestIndex(idxMap);
|
|
1782
|
+
}
|
|
1603
1783
|
}
|
|
1604
1784
|
const manifestId = manifest && manifest.id ? manifest.id : id;
|
|
1605
1785
|
const references = referenced.getReferencesForManifest(manifestId);
|
|
@@ -1644,6 +1824,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1644
1824
|
const normalizedHref = href.split(path.sep).join("/");
|
|
1645
1825
|
const pageHref = rootRelativeHref(normalizedHref);
|
|
1646
1826
|
const pageDescription = summaryForMeta || title;
|
|
1827
|
+
const canonical = resolveManifestCanonical(manifest, slug);
|
|
1647
1828
|
const pageDetails = {
|
|
1648
1829
|
title,
|
|
1649
1830
|
href: pageHref,
|
|
@@ -1653,11 +1834,13 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1653
1834
|
description: pageDescription,
|
|
1654
1835
|
manifestId,
|
|
1655
1836
|
referencedBy: references,
|
|
1837
|
+
canonical,
|
|
1656
1838
|
meta: {
|
|
1657
1839
|
title,
|
|
1658
1840
|
description: pageDescription,
|
|
1659
1841
|
type: "work",
|
|
1660
1842
|
url: pageHref,
|
|
1843
|
+
canonical,
|
|
1661
1844
|
},
|
|
1662
1845
|
};
|
|
1663
1846
|
const ogImageForPage = heroMedia && heroMedia.ogImage ? heroMedia.ogImage : '';
|
|
@@ -2006,12 +2189,18 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
2006
2189
|
entry.ogImage = heroMedia.ogImage;
|
|
2007
2190
|
touched = true;
|
|
2008
2191
|
}
|
|
2009
|
-
if (
|
|
2010
|
-
entry.ogImageWidth =
|
|
2192
|
+
if (typeof heroMedia.ogImageWidth === 'number') {
|
|
2193
|
+
if (entry.ogImageWidth !== heroMedia.ogImageWidth) touched = true;
|
|
2194
|
+
entry.ogImageWidth = heroMedia.ogImageWidth;
|
|
2195
|
+
} else if (entry.ogImageWidth !== undefined) {
|
|
2196
|
+
delete entry.ogImageWidth;
|
|
2011
2197
|
touched = true;
|
|
2012
2198
|
}
|
|
2013
|
-
if (
|
|
2014
|
-
entry.ogImageHeight =
|
|
2199
|
+
if (typeof heroMedia.ogImageHeight === 'number') {
|
|
2200
|
+
if (entry.ogImageHeight !== heroMedia.ogImageHeight) touched = true;
|
|
2201
|
+
entry.ogImageHeight = heroMedia.ogImageHeight;
|
|
2202
|
+
} else if (entry.ogImageHeight !== undefined) {
|
|
2203
|
+
delete entry.ogImageHeight;
|
|
2015
2204
|
touched = true;
|
|
2016
2205
|
}
|
|
2017
2206
|
} else {
|
package/lib/build/pages.js
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
ensureDirSync,
|
|
8
8
|
htmlShell,
|
|
9
9
|
canopyBodyClassForType,
|
|
10
|
+
rootRelativeHref,
|
|
10
11
|
} = require('../common');
|
|
11
12
|
const { log } = require('./log');
|
|
12
13
|
const mdx = require('./mdx');
|
|
@@ -82,6 +83,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}, source
|
|
|
82
83
|
const source = typeof sourceRaw === 'string' ? sourceRaw : String(sourceRaw || '');
|
|
83
84
|
const title = mdx.extractTitle(source);
|
|
84
85
|
const relContentPath = path.relative(CONTENT_DIR, filePath);
|
|
86
|
+
const relOutputPath = relContentPath.replace(/\.mdx$/i, '.html');
|
|
85
87
|
const normalizedRel = navigation.normalizeRelativePath(relContentPath);
|
|
86
88
|
const pageInfo = navigation.getPageInfo(normalizedRel);
|
|
87
89
|
const navData = navigation.buildNavigationForFile(normalizedRel);
|
|
@@ -127,6 +129,17 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}, source
|
|
|
127
129
|
if (resolvedType) basePage.type = resolvedType;
|
|
128
130
|
if (resolvedDescription) basePage.description = resolvedDescription;
|
|
129
131
|
if (basePage.href && !basePage.url) basePage.url = basePage.href;
|
|
132
|
+
const frontmatterCanonical = readFrontmatterString(frontmatterData, 'canonical');
|
|
133
|
+
const fallbackCanonical = rootRelativeHref(
|
|
134
|
+
relOutputPath.split(path.sep).join('/')
|
|
135
|
+
);
|
|
136
|
+
const canonicalValue =
|
|
137
|
+
frontmatterCanonical ||
|
|
138
|
+
(basePage && basePage.canonical) ||
|
|
139
|
+
basePage.url ||
|
|
140
|
+
basePage.href ||
|
|
141
|
+
fallbackCanonical;
|
|
142
|
+
if (canonicalValue) basePage.canonical = canonicalValue;
|
|
130
143
|
if (resolvedImage) {
|
|
131
144
|
basePage.image = resolvedImage;
|
|
132
145
|
basePage.ogImage = ogImageFrontmatter || resolvedImage;
|
|
@@ -137,6 +150,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}, source
|
|
|
137
150
|
if (resolvedDescription) pageMeta.description = resolvedDescription;
|
|
138
151
|
if (resolvedType) pageMeta.type = resolvedType;
|
|
139
152
|
if (basePage.url || basePage.href) pageMeta.url = basePage.url || basePage.href || pageMeta.url;
|
|
153
|
+
if (canonicalValue) pageMeta.canonical = canonicalValue;
|
|
140
154
|
if (resolvedImage) {
|
|
141
155
|
pageMeta.image = resolvedImage;
|
|
142
156
|
if (!pageMeta.ogImage) pageMeta.ogImage = resolvedImage;
|
package/lib/head.js
CHANGED
|
@@ -74,7 +74,14 @@ function Meta(props = {}) {
|
|
|
74
74
|
page.url ||
|
|
75
75
|
page.href ||
|
|
76
76
|
'';
|
|
77
|
-
const
|
|
77
|
+
const canonicalRaw =
|
|
78
|
+
props.canonical ||
|
|
79
|
+
(metaFromPage && metaFromPage.canonical) ||
|
|
80
|
+
page.canonical ||
|
|
81
|
+
'';
|
|
82
|
+
const canonicalSource = canonicalRaw || relativeUrl;
|
|
83
|
+
const canonicalAbsolute = canonicalSource ? absoluteUrl(canonicalSource) : '';
|
|
84
|
+
const absolute = canonicalAbsolute || (relativeUrl ? absoluteUrl(relativeUrl) : '');
|
|
78
85
|
const ogImageRaw =
|
|
79
86
|
props.image ||
|
|
80
87
|
props.ogImage ||
|
|
@@ -93,7 +100,7 @@ function Meta(props = {}) {
|
|
|
93
100
|
if (fullTitle) nodes.push(React.createElement('meta', { key: 'og-title', property: 'og:title', content: fullTitle }));
|
|
94
101
|
if (description) nodes.push(React.createElement('meta', { key: 'og-description', property: 'og:description', content: description }));
|
|
95
102
|
if (absolute) nodes.push(React.createElement('meta', { key: 'og-url', property: 'og:url', content: absolute }));
|
|
96
|
-
if (
|
|
103
|
+
if (canonicalAbsolute) nodes.push(React.createElement('link', { key: 'canonical', rel: 'canonical', href: canonicalAbsolute }));
|
|
97
104
|
if (ogType) nodes.push(React.createElement('meta', { key: 'og-type', property: 'og:type', content: ogType }));
|
|
98
105
|
if (image) nodes.push(React.createElement('meta', { key: 'og-image', property: 'og:image', content: image }));
|
|
99
106
|
if (twitterCard) nodes.push(React.createElement('meta', { key: 'twitter-card', name: 'twitter:card', content: twitterCard }));
|
package/lib/iiif/thumbnail.js
CHANGED
|
@@ -12,6 +12,30 @@ function arrayify(value) {
|
|
|
12
12
|
return Array.isArray(value) ? value : [value];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function normalizeBodyType(value) {
|
|
16
|
+
if (!value) return '';
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
for (const entry of value) {
|
|
23
|
+
const normalized = normalizeBodyType(entry);
|
|
24
|
+
if (normalized) return normalized;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isImageBodyType(value) {
|
|
31
|
+
if (!value) return false;
|
|
32
|
+
try {
|
|
33
|
+
return /image/i.test(String(value));
|
|
34
|
+
} catch (_) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
function normalizeImageServiceCandidate(candidate) {
|
|
16
40
|
if (!candidate || typeof candidate !== 'object') return null;
|
|
17
41
|
const id = candidate.id || candidate['@id'];
|
|
@@ -41,6 +65,23 @@ function normalizeImageServiceCandidate(candidate) {
|
|
|
41
65
|
};
|
|
42
66
|
}
|
|
43
67
|
|
|
68
|
+
function isLevel0Profile(profile) {
|
|
69
|
+
if (!profile) return false;
|
|
70
|
+
try {
|
|
71
|
+
return /level0/i.test(String(profile));
|
|
72
|
+
} catch (_) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isLevel0Service(candidate) {
|
|
78
|
+
if (!candidate || typeof candidate !== 'object') return false;
|
|
79
|
+
if (candidate.profile && isLevel0Profile(candidate.profile)) return true;
|
|
80
|
+
const normalized = normalizeImageServiceCandidate(candidate);
|
|
81
|
+
if (!normalized) return false;
|
|
82
|
+
return Boolean(normalized.profile && isLevel0Profile(normalized.profile));
|
|
83
|
+
}
|
|
84
|
+
|
|
44
85
|
function isIiifImageService(candidate) {
|
|
45
86
|
if (!candidate) return false;
|
|
46
87
|
const {type, profile} = candidate;
|
|
@@ -77,6 +118,7 @@ function extractImageService(value, seen = new Set()) {
|
|
|
77
118
|
function normalizeImagePayload(body, canvas) {
|
|
78
119
|
if (!body || typeof body !== 'object') return null;
|
|
79
120
|
const id = body.id || body['@id'];
|
|
121
|
+
const bodyType = normalizeBodyType(body.type || body['@type']);
|
|
80
122
|
const width =
|
|
81
123
|
typeof body.width === 'number'
|
|
82
124
|
? body.width
|
|
@@ -95,6 +137,8 @@ function normalizeImagePayload(body, canvas) {
|
|
|
95
137
|
width,
|
|
96
138
|
height,
|
|
97
139
|
service: service || undefined,
|
|
140
|
+
bodyType: bodyType || undefined,
|
|
141
|
+
isImageBody: Boolean(bodyType && isImageBodyType(bodyType)),
|
|
98
142
|
};
|
|
99
143
|
}
|
|
100
144
|
|
|
@@ -185,6 +229,7 @@ function selectServiceQuality(candidate) {
|
|
|
185
229
|
|
|
186
230
|
function buildIiifImageUrlFromNormalizedService(service, preferredSize = 800) {
|
|
187
231
|
if (!service || !isIiifImageService(service)) return '';
|
|
232
|
+
if (isLevel0Profile(service.profile)) return '';
|
|
188
233
|
const baseId = normalizeServiceBaseId(service.id);
|
|
189
234
|
if (!baseId) return '';
|
|
190
235
|
const size = preferredSize && preferredSize > 0 ? preferredSize : 800;
|
|
@@ -202,6 +247,7 @@ function buildIiifImageUrlFromService(service, preferredSize = 800) {
|
|
|
202
247
|
function buildIiifImageUrlForDimensions(service, width = 1200, height = 630) {
|
|
203
248
|
const normalized = normalizeImageServiceCandidate(service);
|
|
204
249
|
if (!normalized || !isIiifImageService(normalized)) return '';
|
|
250
|
+
if (isLevel0Profile(normalized.profile)) return '';
|
|
205
251
|
const baseId = normalizeServiceBaseId(normalized.id);
|
|
206
252
|
if (!baseId) return '';
|
|
207
253
|
const safeWidth = Math.max(1, Math.floor(Number(width) || 0));
|
|
@@ -214,6 +260,7 @@ function buildIiifImageUrlForDimensions(service, width = 1200, height = 630) {
|
|
|
214
260
|
function buildIiifImageSrcset(service, steps = [360, 640, 960, 1280, 1600]) {
|
|
215
261
|
const normalized = normalizeImageServiceCandidate(service);
|
|
216
262
|
if (!normalized || !isIiifImageService(normalized)) return '';
|
|
263
|
+
if (isLevel0Profile(normalized.profile)) return '';
|
|
217
264
|
const uniqueSteps = Array.from(
|
|
218
265
|
new Set(
|
|
219
266
|
(Array.isArray(steps) ? steps : [])
|
|
@@ -354,4 +401,5 @@ module.exports = {
|
|
|
354
401
|
buildIiifImageUrlForDimensions,
|
|
355
402
|
findPrimaryCanvasImage,
|
|
356
403
|
buildIiifImageSrcset,
|
|
404
|
+
isLevel0Service,
|
|
357
405
|
};
|
package/lib/search/search.js
CHANGED
|
@@ -258,7 +258,21 @@ async function buildSearchPage() {
|
|
|
258
258
|
throw new Error('Missing required file: content/search/_layout.mdx');
|
|
259
259
|
}
|
|
260
260
|
const mdx = require('../build/mdx');
|
|
261
|
-
const
|
|
261
|
+
const searchHref = rootRelativeHref('search.html');
|
|
262
|
+
const pageDetails = {
|
|
263
|
+
title: 'Search',
|
|
264
|
+
href: searchHref,
|
|
265
|
+
url: searchHref,
|
|
266
|
+
type: 'search',
|
|
267
|
+
canonical: searchHref,
|
|
268
|
+
meta: {
|
|
269
|
+
title: 'Search',
|
|
270
|
+
type: 'search',
|
|
271
|
+
url: searchHref,
|
|
272
|
+
canonical: searchHref,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const rendered = await mdx.compileMdxFile(searchLayoutPath, outPath, { page: pageDetails });
|
|
262
276
|
body = rendered && rendered.body ? rendered.body : '';
|
|
263
277
|
head = rendered && rendered.head ? rendered.head : '';
|
|
264
278
|
if (!body) throw new Error('Search: content/search/_layout.mdx produced empty output');
|