@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 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 = buildIiifImageSrcset(heroService);
217
- const ogImage = heroService
218
- ? buildIiifImageUrlForDimensions(
219
- heroService,
220
- OG_IMAGE_WIDTH,
221
- OG_IMAGE_HEIGHT
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: heroPreferred || heroFallbackId || '',
226
- heroThumbnailWidth: heroWidth,
227
- heroThumbnailHeight: heroHeight,
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) touched = true;
991
- entry.ogImage = heroMedia.ogImage;
992
- entry.ogImageWidth = OG_IMAGE_WIDTH;
993
- entry.ogImageHeight = OG_IMAGE_HEIGHT;
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
- return await readJson(p);
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
- nextIndex.byId.push({
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
- entry.ogImageWidth = OG_IMAGE_WIDTH;
1240
- entry.ogImageHeight = OG_IMAGE_HEIGHT;
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 (entry.ogImageWidth !== OG_IMAGE_WIDTH) {
2010
- entry.ogImageWidth = OG_IMAGE_WIDTH;
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 (entry.ogImageHeight !== OG_IMAGE_HEIGHT) {
2014
- entry.ogImageHeight = OG_IMAGE_HEIGHT;
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 {
@@ -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 absolute = relativeUrl ? absoluteUrl(relativeUrl) : '';
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 (absolute) nodes.push(React.createElement('link', { key: 'canonical', rel: 'canonical', href: absolute }));
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 }));
@@ -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
  };
@@ -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 rendered = await mdx.compileMdxFile(searchLayoutPath, outPath, {});
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",