@canopy-iiif/app 0.9.13 → 0.9.15

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.
@@ -76,6 +76,7 @@ async function build(options = {}) {
76
76
  // so SSR interstitials can resolve items even if they are not part of
77
77
  // the traversed collection or when IIIF build is skipped during incremental rebuilds.
78
78
  try { await iiif.ensureFeaturedInCache(CONFIG); } catch (_) {}
79
+ try { await iiif.rebuildManifestIndexFromCache(); } catch (_) {}
79
80
 
80
81
  /**
81
82
  * Build contextual MDX content from the content directory.
package/lib/build/iiif.js CHANGED
@@ -15,6 +15,13 @@ const {
15
15
  } = require("../common");
16
16
  const mdx = require("./mdx");
17
17
  const {log, logLine, logResponse} = require("./log");
18
+ const {
19
+ getThumbnail,
20
+ getRepresentativeImage,
21
+ buildIiifImageUrlFromService,
22
+ findPrimaryCanvasImage,
23
+ buildIiifImageSrcset,
24
+ } = require("../iiif/thumbnail");
18
25
 
19
26
  const IIIF_CACHE_DIR = path.resolve(".cache/iiif");
20
27
  const IIIF_CACHE_MANIFESTS_DIR = path.join(IIIF_CACHE_DIR, "manifests");
@@ -35,6 +42,8 @@ const IIIF_CACHE_INDEX_MANIFESTS = path.join(
35
42
  const DEFAULT_THUMBNAIL_SIZE = 400;
36
43
  const DEFAULT_CHUNK_SIZE = 20;
37
44
  const DEFAULT_FETCH_CONCURRENCY = 5;
45
+ const HERO_THUMBNAIL_SIZE = 800;
46
+ const HERO_IMAGE_SIZES_ATTR = "(min-width: 1024px) 1280px, 100vw";
38
47
 
39
48
  function resolvePositiveInteger(value, fallback) {
40
49
  const num = Number(value);
@@ -98,6 +107,20 @@ function normalizeMetadataLabel(label) {
98
107
  return trimmed.toLowerCase();
99
108
  }
100
109
 
110
+ function resolveParentFromPartOf(resource) {
111
+ try {
112
+ const partOf = resource && resource.partOf;
113
+ if (!partOf) return "";
114
+ const arr = Array.isArray(partOf) ? partOf : [partOf];
115
+ for (const entry of arr) {
116
+ if (!entry) continue;
117
+ const id = entry.id || entry["@id"];
118
+ if (id) return String(id);
119
+ }
120
+ } catch (_) {}
121
+ return "";
122
+ }
123
+
101
124
  function extractSummaryValues(manifest) {
102
125
  const values = [];
103
126
  try {
@@ -586,10 +609,8 @@ async function ensureFeaturedInCache(cfg) {
586
609
  ? CONFIG.featured
587
610
  : [];
588
611
  if (!featured.length) return;
589
- const {getThumbnail, getRepresentativeImage} = require("../iiif/thumbnail");
590
612
  const {size: thumbSize, unsafe: unsafeThumbs} =
591
613
  resolveThumbnailPreferences();
592
- const HERO_THUMBNAIL_SIZE = 1200;
593
614
  for (const rawId of featured) {
594
615
  const id = normalizeIiifId(String(rawId || ""));
595
616
  if (!id) continue;
@@ -651,22 +672,69 @@ async function ensureFeaturedInCache(cfg) {
651
672
  HERO_THUMBNAIL_SIZE,
652
673
  true
653
674
  );
654
- if (heroRep && heroRep.id) {
655
- const nextHero = String(heroRep.id);
675
+ const canvasImage = findPrimaryCanvasImage(manifest);
676
+ const heroService =
677
+ (canvasImage && canvasImage.service) ||
678
+ (heroRep && heroRep.service);
679
+ const preferredHeroThumbnail = buildIiifImageUrlFromService(
680
+ heroService,
681
+ HERO_THUMBNAIL_SIZE
682
+ );
683
+ const heroSrcset = buildIiifImageSrcset(heroService);
684
+ const heroFallbackId = (() => {
685
+ if (canvasImage && canvasImage.id) return String(canvasImage.id);
686
+ if (heroRep && heroRep.id) return String(heroRep.id);
687
+ return "";
688
+ })();
689
+ const heroWidth = (() => {
690
+ if (canvasImage && typeof canvasImage.width === "number") {
691
+ return canvasImage.width;
692
+ }
693
+ if (heroRep && typeof heroRep.width === "number") {
694
+ return heroRep.width;
695
+ }
696
+ return undefined;
697
+ })();
698
+ const heroHeight = (() => {
699
+ if (canvasImage && typeof canvasImage.height === "number") {
700
+ return canvasImage.height;
701
+ }
702
+ if (heroRep && typeof heroRep.height === "number") {
703
+ return heroRep.height;
704
+ }
705
+ return undefined;
706
+ })();
707
+ if (preferredHeroThumbnail || heroFallbackId) {
708
+ const nextHero = preferredHeroThumbnail || heroFallbackId;
656
709
  if (entry.heroThumbnail !== nextHero) {
657
710
  entry.heroThumbnail = nextHero;
658
711
  touched = true;
659
712
  }
660
- if (typeof heroRep.width === "number") {
661
- if (entry.heroThumbnailWidth !== heroRep.width) touched = true;
662
- entry.heroThumbnailWidth = heroRep.width;
713
+ if (heroSrcset) {
714
+ if (entry.heroThumbnailSrcset !== heroSrcset) touched = true;
715
+ entry.heroThumbnailSrcset = heroSrcset;
716
+ if (entry.heroThumbnailSizes !== HERO_IMAGE_SIZES_ATTR) touched = true;
717
+ entry.heroThumbnailSizes = HERO_IMAGE_SIZES_ATTR;
718
+ } else {
719
+ if (entry.heroThumbnailSrcset !== undefined) {
720
+ delete entry.heroThumbnailSrcset;
721
+ touched = true;
722
+ }
723
+ if (entry.heroThumbnailSizes !== undefined) {
724
+ delete entry.heroThumbnailSizes;
725
+ touched = true;
726
+ }
727
+ }
728
+ if (typeof heroWidth === "number") {
729
+ if (entry.heroThumbnailWidth !== heroWidth) touched = true;
730
+ entry.heroThumbnailWidth = heroWidth;
663
731
  } else if (entry.heroThumbnailWidth !== undefined) {
664
732
  delete entry.heroThumbnailWidth;
665
733
  touched = true;
666
734
  }
667
- if (typeof heroRep.height === "number") {
668
- if (entry.heroThumbnailHeight !== heroRep.height) touched = true;
669
- entry.heroThumbnailHeight = heroRep.height;
735
+ if (typeof heroHeight === "number") {
736
+ if (entry.heroThumbnailHeight !== heroHeight) touched = true;
737
+ entry.heroThumbnailHeight = heroHeight;
670
738
  } else if (entry.heroThumbnailHeight !== undefined) {
671
739
  delete entry.heroThumbnailHeight;
672
740
  touched = true;
@@ -810,6 +878,158 @@ async function saveCachedCollection(collection, id, parentId) {
810
878
  } catch (_) {}
811
879
  }
812
880
 
881
+ async function rebuildManifestIndexFromCache() {
882
+ try {
883
+ const previous = await loadManifestIndex();
884
+ const previousEntries = Array.isArray(previous.byId) ? previous.byId : [];
885
+ const priorMap = new Map();
886
+ for (const entry of previousEntries) {
887
+ if (!entry || !entry.id) continue;
888
+ const type = entry.type || "Manifest";
889
+ const key = `${type}:${normalizeIiifId(entry.id)}`;
890
+ priorMap.set(key, entry);
891
+ }
892
+ const nextIndex = {
893
+ byId: [],
894
+ collection: previous.collection || null,
895
+ };
896
+ const collectionFiles = fs.existsSync(IIIF_CACHE_COLLECTIONS_DIR)
897
+ ? (await fsp.readdir(IIIF_CACHE_COLLECTIONS_DIR))
898
+ .filter((name) => name && name.toLowerCase().endsWith(".json"))
899
+ .sort()
900
+ : [];
901
+ const manifestFiles = fs.existsSync(IIIF_CACHE_MANIFESTS_DIR)
902
+ ? (await fsp.readdir(IIIF_CACHE_MANIFESTS_DIR))
903
+ .filter((name) => name && name.toLowerCase().endsWith(".json"))
904
+ .sort()
905
+ : [];
906
+ const {size: thumbSize, unsafe: unsafeThumbs} =
907
+ resolveThumbnailPreferences();
908
+
909
+ for (const name of collectionFiles) {
910
+ const slug = name.replace(/\.json$/i, "");
911
+ const fp = path.join(IIIF_CACHE_COLLECTIONS_DIR, name);
912
+ let data = null;
913
+ try {
914
+ data = await readJson(fp);
915
+ } catch (_) {
916
+ data = null;
917
+ }
918
+ if (!data) continue;
919
+ const id = data.id || data["@id"];
920
+ if (!id) continue;
921
+ const nid = normalizeIiifId(String(id));
922
+ const key = `Collection:${nid}`;
923
+ const fallback = priorMap.get(key) || {};
924
+ const parent = resolveParentFromPartOf(data) || fallback.parent || "";
925
+ nextIndex.byId.push({
926
+ id: String(nid),
927
+ type: "Collection",
928
+ slug,
929
+ parent,
930
+ });
931
+ }
932
+
933
+ for (const name of manifestFiles) {
934
+ const slug = name.replace(/\.json$/i, "");
935
+ const fp = path.join(IIIF_CACHE_MANIFESTS_DIR, name);
936
+ let manifest = null;
937
+ try {
938
+ manifest = await readJson(fp);
939
+ } catch (_) {
940
+ manifest = null;
941
+ }
942
+ if (!manifest) continue;
943
+ const id = manifest.id || manifest["@id"];
944
+ if (!id) continue;
945
+ const nid = normalizeIiifId(String(id));
946
+ MEMO_ID_TO_SLUG.set(String(id), slug);
947
+ const key = `Manifest:${nid}`;
948
+ const fallback = priorMap.get(key) || {};
949
+ const parent = resolveParentFromPartOf(manifest) || fallback.parent || "";
950
+ const entry = {
951
+ id: String(nid),
952
+ type: "Manifest",
953
+ slug,
954
+ parent,
955
+ };
956
+ try {
957
+ const thumb = await getThumbnail(manifest, thumbSize, unsafeThumbs);
958
+ if (thumb && thumb.url) {
959
+ entry.thumbnail = String(thumb.url);
960
+ if (typeof thumb.width === "number") entry.thumbnailWidth = thumb.width;
961
+ if (typeof thumb.height === "number") entry.thumbnailHeight = thumb.height;
962
+ }
963
+ } catch (_) {}
964
+ try {
965
+ const heroSource = (() => {
966
+ if (manifest && manifest.thumbnail) {
967
+ const clone = {...manifest};
968
+ try {
969
+ delete clone.thumbnail;
970
+ } catch (_) {
971
+ clone.thumbnail = undefined;
972
+ }
973
+ return clone;
974
+ }
975
+ return manifest;
976
+ })();
977
+ const heroRep = await getRepresentativeImage(
978
+ heroSource || manifest,
979
+ HERO_THUMBNAIL_SIZE,
980
+ true
981
+ );
982
+ const canvasImage = findPrimaryCanvasImage(manifest);
983
+ const heroService =
984
+ (canvasImage && canvasImage.service) ||
985
+ (heroRep && heroRep.service);
986
+ const preferredHero = buildIiifImageUrlFromService(
987
+ heroService,
988
+ HERO_THUMBNAIL_SIZE
989
+ );
990
+ const heroFallbackId = (() => {
991
+ if (canvasImage && canvasImage.id) return String(canvasImage.id);
992
+ if (heroRep && heroRep.id) return String(heroRep.id);
993
+ return "";
994
+ })();
995
+ const heroWidth = (() => {
996
+ if (canvasImage && typeof canvasImage.width === "number")
997
+ return canvasImage.width;
998
+ if (heroRep && typeof heroRep.width === "number") return heroRep.width;
999
+ return undefined;
1000
+ })();
1001
+ const heroHeight = (() => {
1002
+ if (canvasImage && typeof canvasImage.height === "number")
1003
+ return canvasImage.height;
1004
+ if (heroRep && typeof heroRep.height === "number")
1005
+ return heroRep.height;
1006
+ return undefined;
1007
+ })();
1008
+ if (preferredHero || heroFallbackId) {
1009
+ entry.heroThumbnail = preferredHero || heroFallbackId;
1010
+ if (typeof heroWidth === "number") entry.heroThumbnailWidth = heroWidth;
1011
+ if (typeof heroHeight === "number") entry.heroThumbnailHeight = heroHeight;
1012
+ const heroSrcset = buildIiifImageSrcset(heroService);
1013
+ if (heroSrcset) {
1014
+ entry.heroThumbnailSrcset = heroSrcset;
1015
+ entry.heroThumbnailSizes = HERO_IMAGE_SIZES_ATTR;
1016
+ }
1017
+ }
1018
+ } catch (_) {}
1019
+ nextIndex.byId.push(entry);
1020
+ }
1021
+
1022
+ await saveManifestIndex(nextIndex);
1023
+ try {
1024
+ logLine("✓ Rebuilt IIIF cache index", "cyan");
1025
+ } catch (_) {}
1026
+ } catch (err) {
1027
+ try {
1028
+ logLine("! Skipped IIIF index rebuild", "yellow");
1029
+ } catch (_) {}
1030
+ }
1031
+ }
1032
+
813
1033
  async function loadConfig() {
814
1034
  const cfgPath = path.resolve("canopy.yml");
815
1035
  if (!fs.existsSync(cfgPath)) return {};
@@ -1155,7 +1375,9 @@ async function buildIiifCollectionPages(CONFIG) {
1155
1375
  )
1156
1376
  : "";
1157
1377
  const needsHydrateViewer =
1158
- body.includes("data-canopy-viewer") || body.includes("data-canopy-scroll");
1378
+ body.includes("data-canopy-viewer") ||
1379
+ body.includes("data-canopy-scroll") ||
1380
+ body.includes("data-canopy-image");
1159
1381
  const needsRelated = body.includes("data-canopy-related-items");
1160
1382
  const needsHeroSlider = body.includes("data-canopy-hero-slider");
1161
1383
  const needsSearchForm = body.includes("data-canopy-search-form");
@@ -1281,7 +1503,6 @@ async function buildIiifCollectionPages(CONFIG) {
1281
1503
  let thumbWidth = undefined;
1282
1504
  let thumbHeight = undefined;
1283
1505
  try {
1284
- const {getThumbnail} = require("../iiif/thumbnail");
1285
1506
  const t = await getThumbnail(manifest, thumbSize, unsafeThumbs);
1286
1507
  if (t && t.url) {
1287
1508
  thumbUrl = String(t.url);
@@ -1382,6 +1603,7 @@ module.exports = {
1382
1603
  loadCachedManifestById,
1383
1604
  saveCachedManifest,
1384
1605
  ensureFeaturedInCache,
1606
+ rebuildManifestIndexFromCache,
1385
1607
  };
1386
1608
 
1387
1609
  // Debug: list collections cache after traversal
package/lib/build/mdx.js CHANGED
@@ -11,6 +11,14 @@ const {
11
11
  ensureDirSync,
12
12
  withBase,
13
13
  } = require("../common");
14
+ let remarkGfm = null;
15
+ try {
16
+ const mod = require("remark-gfm");
17
+ const plugin = mod && (typeof mod === "function" ? mod : mod.default);
18
+ remarkGfm = typeof plugin === "function" ? plugin : null;
19
+ } catch (_) {
20
+ remarkGfm = null;
21
+ }
14
22
 
15
23
  const EXTRA_REMARK_PLUGINS = (() => {
16
24
  try {
@@ -32,6 +40,9 @@ function buildCompileOptions(overrides = {}) {
32
40
  format: "mdx",
33
41
  };
34
42
  const remarkPlugins = [];
43
+ if (remarkGfm) {
44
+ remarkPlugins.push(remarkGfm);
45
+ }
35
46
  if (overrides && Array.isArray(overrides.remarkPlugins)) {
36
47
  remarkPlugins.push(...overrides.remarkPlugins);
37
48
  }
@@ -514,6 +525,7 @@ async function ensureClientRuntime() {
514
525
  const entry = `
515
526
  import CloverViewer from '@samvera/clover-iiif/viewer';
516
527
  import CloverScroll from '@samvera/clover-iiif/scroll';
528
+ import CloverImage from '@samvera/clover-iiif/image';
517
529
 
518
530
  function ready(fn) {
519
531
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true });
@@ -552,6 +564,7 @@ async function ensureClientRuntime() {
552
564
  ready(function() {
553
565
  mountAll('[data-canopy-viewer]', CloverViewer);
554
566
  mountAll('[data-canopy-scroll]', CloverScroll);
567
+ mountAll('[data-canopy-image]', CloverImage);
555
568
  });
556
569
  `;
557
570
  const reactShim = `
@@ -66,7 +66,9 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
66
66
  }
67
67
  const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, mergedProps);
68
68
  const needsHydrateViewer =
69
- body.includes('data-canopy-viewer') || body.includes('data-canopy-scroll');
69
+ body.includes('data-canopy-viewer') ||
70
+ body.includes('data-canopy-scroll') ||
71
+ body.includes('data-canopy-image');
70
72
  const needsHydrateSlider = body.includes('data-canopy-slider');
71
73
  const needsHeroSlider = body.includes('data-canopy-hero-slider');
72
74
  const needsSearchForm = true; // search form runtime is global
@@ -80,6 +82,9 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
80
82
  const heroRel = needsHeroSlider
81
83
  ? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-hero-slider.js')).split(path.sep).join('/')
82
84
  : null;
85
+ const heroCssRel = needsHeroSlider
86
+ ? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-hero-slider.css')).split(path.sep).join('/')
87
+ : null;
83
88
  const facetsRel = needsFacets
84
89
  ? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-related-items.js')).split(path.sep).join('/')
85
90
  : null;
@@ -118,6 +123,17 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
118
123
  if (viewerRel && jsRel !== viewerRel) extraScripts.push(`<script defer src="${viewerRel}"></script>`);
119
124
  if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
120
125
  if (searchFormRel && jsRel !== searchFormRel) extraScripts.push(`<script defer src="${searchFormRel}"></script>`);
126
+ const extraStyles = [];
127
+ if (heroCssRel) {
128
+ let rel = heroCssRel;
129
+ try {
130
+ const heroCssAbs = path.join(OUT_DIR, 'scripts', 'canopy-hero-slider.css');
131
+ const st = fs.statSync(heroCssAbs);
132
+ rel += `?v=${Math.floor(st.mtimeMs || Date.now())}`;
133
+ } catch (_) {}
134
+ extraStyles.push(`<link rel="stylesheet" href="${rel}">`);
135
+ }
136
+ if (extraStyles.length) headExtra = extraStyles.join('') + headExtra;
121
137
  if (extraScripts.length) headExtra = extraScripts.join('') + headExtra;
122
138
  const html = htmlShell({ title, body, cssHref: null, scriptHref: jsRel, headExtra: vendorTag + headExtra });
123
139
  const { applyBaseToHtml } = require('../common');
@@ -117,6 +117,12 @@ function readFeaturedFromCacheSync() {
117
117
  } else if (typeof entry.thumbnailHeight === 'number') {
118
118
  rec.thumbnailHeight = entry.thumbnailHeight;
119
119
  }
120
+ if (entry && entry.heroThumbnailSrcset) {
121
+ rec.srcset = String(entry.heroThumbnailSrcset);
122
+ }
123
+ if (entry && entry.heroThumbnailSizes) {
124
+ rec.sizes = String(entry.heroThumbnailSizes);
125
+ }
120
126
  } else {
121
127
  if (entry && entry.thumbnail) rec.thumbnail = String(entry.thumbnail);
122
128
  if (entry && typeof entry.thumbnailWidth === 'number') rec.thumbnailWidth = entry.thumbnailWidth;
@@ -1,8 +1,9 @@
1
1
  import Swiper from 'swiper';
2
- import { Navigation, Pagination, Autoplay } from 'swiper/modules';
2
+ import { Navigation, Pagination, Autoplay, EffectFade } from 'swiper/modules';
3
3
  import 'swiper/css';
4
4
  import 'swiper/css/navigation';
5
5
  import 'swiper/css/pagination';
6
+ import 'swiper/css/effect-fade';
6
7
 
7
8
  function ready(fn) {
8
9
  if (typeof document === 'undefined') return;
@@ -20,12 +21,18 @@ function initSlider(host) {
20
21
  const prev = host.querySelector('.canopy-interstitial__nav-btn--prev');
21
22
  const next = host.querySelector('.canopy-interstitial__nav-btn--next');
22
23
  const pagination = host.querySelector('.canopy-interstitial__pagination');
24
+ const transitionAttr = (host.getAttribute && host.getAttribute('data-transition')) || 'fade';
25
+ const transition = transitionAttr && transitionAttr.toLowerCase() === 'slide' ? 'slide' : 'fade';
23
26
 
24
27
  try {
28
+ const baseModules = [Navigation, Pagination, Autoplay];
29
+ if (transition === 'fade') baseModules.push(EffectFade);
25
30
  const swiperInstance = new Swiper(slider, {
26
- modules: [Navigation, Pagination, Autoplay],
31
+ modules: baseModules,
27
32
  loop: true,
28
33
  slidesPerView: 1,
34
+ effect: transition,
35
+ fadeEffect: transition === 'fade' ? { crossFade: true } : undefined,
29
36
  navigation: {
30
37
  prevEl: prev || undefined,
31
38
  nextEl: next || undefined,