@canopy-iiif/app 1.5.5 → 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
@@ -119,6 +119,67 @@ function normalizeSlugBase(value, fallback) {
119
119
  return clampSlugLength(safeFallback, MAX_ENTRY_SLUG_LENGTH) || safeFallback;
120
120
  }
121
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
+
122
183
  function buildSlugWithSuffix(base, fallback, counter) {
123
184
  const suffix = `-${counter}`;
124
185
  const baseLimit = Math.max(1, MAX_ENTRY_SLUG_LENGTH - suffix.length);
@@ -889,6 +950,7 @@ async function loadCachedManifestById(id) {
889
950
  slug,
890
951
  parent: "",
891
952
  };
953
+ applyManifestEntryCanonical(entry, null, slug);
892
954
  if (existingEntryIdx >= 0) index.byId[existingEntryIdx] = entry;
893
955
  else index.byId.push(entry);
894
956
  await saveManifestIndex(index);
@@ -906,6 +968,21 @@ async function loadCachedManifestById(id) {
906
968
  await fsp.writeFile(p, JSON.stringify(normalized, null, 2), "utf8");
907
969
  } catch (_) {}
908
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 (_) {}
909
986
  return normalized;
910
987
  } catch (_) {
911
988
  return null;
@@ -935,6 +1012,7 @@ async function saveCachedManifest(manifest, id, parentId) {
935
1012
  slug,
936
1013
  parent: parentId ? String(parentId) : "",
937
1014
  };
1015
+ applyManifestEntryCanonical(entry, normalizedManifest, slug);
938
1016
  if (existingEntryIdx >= 0) index.byId[existingEntryIdx] = entry;
939
1017
  else index.byId.push(entry);
940
1018
  await saveManifestIndex(index);
@@ -1150,6 +1228,7 @@ async function loadCachedCollectionById(id) {
1150
1228
  slug,
1151
1229
  parent: "",
1152
1230
  };
1231
+ applyCollectionEntryCanonical(entry, null);
1153
1232
  if (existing >= 0) index.byId[existing] = entry;
1154
1233
  else index.byId.push(entry);
1155
1234
  await saveManifestIndex(index);
@@ -1164,7 +1243,23 @@ async function loadCachedCollectionById(id) {
1164
1243
  if (!slug) return null;
1165
1244
  const p = path.join(IIIF_CACHE_COLLECTIONS_DIR, slug + ".json");
1166
1245
  if (!fs.existsSync(p)) return null;
1167
- 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;
1168
1263
  } catch (_) {
1169
1264
  return null;
1170
1265
  }
@@ -1202,6 +1297,7 @@ async function saveCachedCollection(collection, id, parentId) {
1202
1297
  slug,
1203
1298
  parent: parentId ? String(parentId) : "",
1204
1299
  };
1300
+ applyCollectionEntryCanonical(entry, normalizedCollection);
1205
1301
  if (existingEntryIdx >= 0) index.byId[existingEntryIdx] = entry;
1206
1302
  else index.byId.push(entry);
1207
1303
  await saveManifestIndex(index);
@@ -1252,12 +1348,14 @@ async function rebuildManifestIndexFromCache() {
1252
1348
  const key = `Collection:${nid}`;
1253
1349
  const fallback = priorMap.get(key) || {};
1254
1350
  const parent = resolveParentFromPartOf(data) || fallback.parent || "";
1255
- nextIndex.byId.push({
1351
+ const entry = {
1256
1352
  id: String(nid),
1257
1353
  type: "Collection",
1258
1354
  slug,
1259
1355
  parent,
1260
- });
1356
+ };
1357
+ applyCollectionEntryCanonical(entry, data);
1358
+ nextIndex.byId.push(entry);
1261
1359
  }
1262
1360
 
1263
1361
  for (const name of manifestFiles) {
@@ -1283,6 +1381,7 @@ async function rebuildManifestIndexFromCache() {
1283
1381
  slug,
1284
1382
  parent,
1285
1383
  };
1384
+ applyManifestEntryCanonical(entry, manifest, slug);
1286
1385
  try {
1287
1386
  const thumb = await getThumbnail(manifest, thumbSize, unsafeThumbs);
1288
1387
  if (thumb && thumb.url) {
@@ -1667,12 +1766,20 @@ async function buildIiifCollectionPages(CONFIG) {
1667
1766
  slug,
1668
1767
  parent: parentNorm,
1669
1768
  };
1769
+ applyManifestEntryCanonical(newEntry, manifest, slug);
1670
1770
  const existingIdx = idxMap.byId.findIndex(
1671
1771
  (e) => e && e.type === "Manifest" && normalizeIiifId(e.id) === nid
1672
1772
  );
1673
1773
  if (existingIdx >= 0) idxMap.byId[existingIdx] = newEntry;
1674
1774
  else idxMap.byId.push(newEntry);
1675
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
+ }
1676
1783
  }
1677
1784
  const manifestId = manifest && manifest.id ? manifest.id : id;
1678
1785
  const references = referenced.getReferencesForManifest(manifestId);
@@ -1717,6 +1824,7 @@ async function buildIiifCollectionPages(CONFIG) {
1717
1824
  const normalizedHref = href.split(path.sep).join("/");
1718
1825
  const pageHref = rootRelativeHref(normalizedHref);
1719
1826
  const pageDescription = summaryForMeta || title;
1827
+ const canonical = resolveManifestCanonical(manifest, slug);
1720
1828
  const pageDetails = {
1721
1829
  title,
1722
1830
  href: pageHref,
@@ -1726,11 +1834,13 @@ async function buildIiifCollectionPages(CONFIG) {
1726
1834
  description: pageDescription,
1727
1835
  manifestId,
1728
1836
  referencedBy: references,
1837
+ canonical,
1729
1838
  meta: {
1730
1839
  title,
1731
1840
  description: pageDescription,
1732
1841
  type: "work",
1733
1842
  url: pageHref,
1843
+ canonical,
1734
1844
  },
1735
1845
  };
1736
1846
  const ogImageForPage = heroMedia && heroMedia.ogImage ? heroMedia.ogImage : '';
@@ -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 }));
@@ -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.5",
3
+ "version": "1.5.6",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",