@canopy-iiif/app 1.5.5 → 1.5.7
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 +113 -3
- package/lib/build/pages.js +14 -0
- package/lib/head.js +9 -2
- package/lib/search/search.js +15 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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 : '';
|
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/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');
|