@canopy-iiif/app 0.8.2 → 0.8.4
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/AGENTS.md +1 -1
- package/lib/build/build.js +10 -0
- package/lib/build/dev.js +87 -40
- package/lib/build/iiif.js +124 -43
- package/lib/build/mdx.js +14 -4
- package/lib/build/pages.js +23 -9
- package/lib/build/runtimes.js +6 -6
- package/lib/build/search.js +7 -10
- package/lib/build/verify.js +9 -9
- package/lib/components/featured.js +17 -3
- package/lib/components/navigation.js +308 -0
- package/lib/page-context.js +14 -0
- package/lib/search/search-app.jsx +2 -2
- package/lib/search/{command-runtime.js → search-form-runtime.js} +9 -9
- package/lib/search/search.js +2 -2
- package/package.json +1 -1
- package/ui/dist/index.mjs +76 -63
- package/ui/dist/index.mjs.map +3 -3
- package/ui/dist/server.mjs +170 -67
- package/ui/dist/server.mjs.map +4 -4
- package/ui/styles/base/_common.scss +19 -6
- package/ui/styles/base/_heading.scss +17 -0
- package/ui/styles/base/index.scss +2 -1
- package/ui/styles/components/_sub-navigation.scss +76 -0
- package/ui/styles/components/header/_header.scss +13 -0
- package/ui/styles/components/header/_logo.scss +20 -0
- package/ui/styles/components/header/_navbar.scss +15 -0
- package/ui/styles/components/header/index.scss +3 -0
- package/ui/styles/components/index.scss +5 -4
- package/ui/styles/components/search/_filters.scss +265 -0
- package/ui/styles/components/search/_form.scss +171 -0
- package/ui/styles/components/search/_results.scss +158 -0
- package/ui/styles/components/search/index.scss +3 -0
- package/ui/styles/index.css +584 -71
- package/ui/styles/variables.scss +15 -5
- package/ui/styles/components/_command.scss +0 -164
- package/ui/styles/components/_header.scss +0 -0
package/lib/AGENTS.md
CHANGED
|
@@ -56,7 +56,7 @@ Logbook Template
|
|
|
56
56
|
Logbook
|
|
57
57
|
-------
|
|
58
58
|
- 2025-09-26 / chatgpt: Hardened runtime bundlers to throw when esbuild or source compilation fails and required `content/works/_layout.mdx`; build now aborts instead of silently writing placeholder assets.
|
|
59
|
-
- 2025-09-26 / chatgpt: Replaced the legacy command runtime stub with an esbuild-bundled runtime (`search/
|
|
59
|
+
- 2025-09-26 / chatgpt: Replaced the legacy command runtime stub with an esbuild-bundled runtime (`search/search-form-runtime.js`); `prepareSearchFormRuntime()` now builds `site/scripts/canopy-search-form.js` and fails if esbuild is missing.
|
|
60
60
|
- 2025-09-27 / chatgpt: Documented Tailwind token flow in `app/styles/tailwind.config.js`, compiled UI Sass variables during config load, and exposed `stylesheetHref`/`Stylesheet` helpers via `@canopy-iiif/app/head` so `_app.mdx` can reference the generated CSS directly.
|
|
61
61
|
- 2025-09-27 / chatgpt: Expanded search indexing to harvest MDX pages (respecting frontmatter/layout types), injected BASE_PATH hydration data into search.html, and reworked `mdx.extractTitle()` so generated records surface real headings instead of `Untitled`.
|
|
62
62
|
|
package/lib/build/build.js
CHANGED
|
@@ -20,6 +20,7 @@ const { ensureStyles } = require("./styles");
|
|
|
20
20
|
const { copyAssets } = require("./assets");
|
|
21
21
|
const { logLine } = require("./log");
|
|
22
22
|
const { verifyBuildOutput } = require("./verify");
|
|
23
|
+
const navigation = require("../components/navigation");
|
|
23
24
|
|
|
24
25
|
// hold records between builds if skipping IIIF
|
|
25
26
|
let iiifRecordsCache = [];
|
|
@@ -41,6 +42,7 @@ async function build(options = {}) {
|
|
|
41
42
|
});
|
|
42
43
|
logLine("• Reset MDX cache", "blue", { dim: true });
|
|
43
44
|
mdx?.resetMdxCaches();
|
|
45
|
+
navigation?.resetNavigationCache?.();
|
|
44
46
|
if (!skipIiif) {
|
|
45
47
|
await cleanDir(OUT_DIR);
|
|
46
48
|
logLine(`• Cleaned output directory`, "blue", { dim: true });
|
|
@@ -62,6 +64,14 @@ async function build(options = {}) {
|
|
|
62
64
|
if (!skipIiif) {
|
|
63
65
|
const results = await iiif.buildIiifCollectionPages(CONFIG);
|
|
64
66
|
iiifRecords = results?.iiifRecords;
|
|
67
|
+
iiifRecordsCache = Array.isArray(iiifRecords) ? iiifRecords : [];
|
|
68
|
+
} else {
|
|
69
|
+
iiifRecords = Array.isArray(iiifRecordsCache) ? iiifRecordsCache : [];
|
|
70
|
+
logLine(
|
|
71
|
+
`• Reusing cached IIIF search records (${iiifRecords.length})`,
|
|
72
|
+
"blue",
|
|
73
|
+
{ dim: true }
|
|
74
|
+
);
|
|
65
75
|
}
|
|
66
76
|
// Ensure any configured featured manifests are cached (and thumbnails computed)
|
|
67
77
|
// so SSR components like <Hero /> can resolve items even if they are not part of
|
package/lib/build/dev.js
CHANGED
|
@@ -617,29 +617,45 @@ function startServer() {
|
|
|
617
617
|
}
|
|
618
618
|
if (pathname === "/") pathname = "/index.html";
|
|
619
619
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
// 3) if a directory, use its index.html
|
|
624
|
-
let filePath = null;
|
|
625
|
-
const candidateA = path.join(OUT_DIR, pathname);
|
|
626
|
-
const candidateB = path.join(OUT_DIR, pathname + ".html");
|
|
627
|
-
if (fs.existsSync(candidateA)) {
|
|
628
|
-
filePath = candidateA;
|
|
629
|
-
} else if (fs.existsSync(candidateB)) {
|
|
630
|
-
filePath = candidateB;
|
|
620
|
+
function toSitePath(p) {
|
|
621
|
+
const rel = p.startsWith("/") ? `.${p}` : p;
|
|
622
|
+
return path.resolve(OUT_DIR, rel);
|
|
631
623
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
624
|
+
|
|
625
|
+
const ensureLeadingSlash = (p) => (p.startsWith("/") ? p : `/${p}`);
|
|
626
|
+
const basePath = ensureLeadingSlash(pathname);
|
|
627
|
+
const noTrailing = basePath !== "/" ? basePath.replace(/\/+$/, "") : basePath;
|
|
628
|
+
const attempts = [];
|
|
629
|
+
|
|
630
|
+
const primaryPath = noTrailing === "/" ? "/index.html" : noTrailing;
|
|
631
|
+
attempts.push(toSitePath(primaryPath));
|
|
632
|
+
|
|
633
|
+
if (!/\.html$/i.test(primaryPath)) {
|
|
634
|
+
attempts.push(toSitePath(`${primaryPath}.html`));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const withoutHtml = primaryPath.replace(/\.html$/i, "");
|
|
638
|
+
attempts.push(toSitePath(`${withoutHtml}/index.html`));
|
|
639
|
+
|
|
640
|
+
let filePath = null;
|
|
641
|
+
for (const candidate of attempts) {
|
|
642
|
+
if (!candidate) continue;
|
|
643
|
+
let stat;
|
|
644
|
+
try {
|
|
645
|
+
stat = fs.statSync(candidate);
|
|
646
|
+
} catch (_) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (stat.isFile()) {
|
|
650
|
+
filePath = candidate;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
if (stat.isDirectory()) {
|
|
654
|
+
const idx = path.join(candidate, "index.html");
|
|
655
|
+
if (fs.existsSync(idx)) {
|
|
656
|
+
filePath = idx;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
643
659
|
}
|
|
644
660
|
}
|
|
645
661
|
if (!filePath) {
|
|
@@ -969,17 +985,16 @@ async function dev() {
|
|
|
969
985
|
child = startTailwindWatcher();
|
|
970
986
|
|
|
971
987
|
const uiPlugin = path.join(
|
|
972
|
-
|
|
973
|
-
"../ui",
|
|
988
|
+
APP_UI_DIR,
|
|
974
989
|
"tailwind-canopy-iiif-plugin.js"
|
|
975
990
|
);
|
|
976
991
|
const uiPreset = path.join(
|
|
977
|
-
|
|
978
|
-
"../ui",
|
|
992
|
+
APP_UI_DIR,
|
|
979
993
|
"tailwind-canopy-iiif-preset.js"
|
|
980
994
|
);
|
|
981
|
-
const uiStylesDir = path.join(
|
|
982
|
-
const
|
|
995
|
+
const uiStylesDir = path.join(APP_UI_DIR, "styles");
|
|
996
|
+
const uiStylesCss = path.join(uiStylesDir, "index.css");
|
|
997
|
+
const pluginFiles = [uiPlugin, uiPreset].filter((p) => {
|
|
983
998
|
try {
|
|
984
999
|
return fs.existsSync(p);
|
|
985
1000
|
} catch (_) {
|
|
@@ -987,25 +1002,54 @@ async function dev() {
|
|
|
987
1002
|
}
|
|
988
1003
|
});
|
|
989
1004
|
let restartTimer = null;
|
|
990
|
-
|
|
1005
|
+
let uiCssWatcherAttached = false;
|
|
1006
|
+
const scheduleTailwindRestart = (message, compileLabel) => {
|
|
991
1007
|
clearTimeout(restartTimer);
|
|
992
1008
|
restartTimer = setTimeout(() => {
|
|
993
|
-
console.log(
|
|
994
|
-
"[tailwind] detected UI plugin/preset change — restarting Tailwind"
|
|
995
|
-
);
|
|
1009
|
+
if (message) console.log(message);
|
|
996
1010
|
try {
|
|
997
1011
|
if (child && !child.killed) child.kill();
|
|
998
1012
|
} catch (_) {}
|
|
999
|
-
safeCompile(
|
|
1013
|
+
safeCompile(compileLabel);
|
|
1000
1014
|
child = startTailwindWatcher();
|
|
1001
1015
|
try { onBuildStart(); } catch (_) {}
|
|
1002
|
-
},
|
|
1016
|
+
}, 120);
|
|
1003
1017
|
};
|
|
1004
|
-
for (const f of
|
|
1018
|
+
for (const f of pluginFiles) {
|
|
1005
1019
|
try {
|
|
1006
|
-
fs.watch(f, { persistent: false },
|
|
1020
|
+
fs.watch(f, { persistent: false }, () => {
|
|
1021
|
+
scheduleTailwindRestart(
|
|
1022
|
+
"[tailwind] detected UI plugin/preset change — restarting Tailwind",
|
|
1023
|
+
"[tailwind] compile after plugin change failed"
|
|
1024
|
+
);
|
|
1025
|
+
});
|
|
1007
1026
|
} catch (_) {}
|
|
1008
1027
|
}
|
|
1028
|
+
const attachCssWatcher = () => {
|
|
1029
|
+
if (uiCssWatcherAttached) {
|
|
1030
|
+
if (fs.existsSync(uiStylesCss)) return;
|
|
1031
|
+
uiCssWatcherAttached = false;
|
|
1032
|
+
}
|
|
1033
|
+
if (!fs.existsSync(uiStylesCss)) return;
|
|
1034
|
+
const handler = () =>
|
|
1035
|
+
scheduleTailwindRestart(
|
|
1036
|
+
"[tailwind] detected @canopy-iiif/app/ui styles change — restarting Tailwind",
|
|
1037
|
+
"[tailwind] compile after UI styles change failed"
|
|
1038
|
+
);
|
|
1039
|
+
try {
|
|
1040
|
+
const watcher = fs.watch(uiStylesCss, { persistent: false }, handler);
|
|
1041
|
+
uiCssWatcherAttached = true;
|
|
1042
|
+
watcher.on("close", () => {
|
|
1043
|
+
uiCssWatcherAttached = false;
|
|
1044
|
+
});
|
|
1045
|
+
} catch (_) {
|
|
1046
|
+
try {
|
|
1047
|
+
fs.watchFile(uiStylesCss, { interval: 250 }, handler);
|
|
1048
|
+
uiCssWatcherAttached = true;
|
|
1049
|
+
} catch (_) {}
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
attachCssWatcher();
|
|
1009
1053
|
if (fs.existsSync(uiStylesDir)) {
|
|
1010
1054
|
try {
|
|
1011
1055
|
fs.watch(
|
|
@@ -1013,7 +1057,7 @@ async function dev() {
|
|
|
1013
1057
|
{ persistent: false, recursive: true },
|
|
1014
1058
|
(evt, fn) => {
|
|
1015
1059
|
try {
|
|
1016
|
-
if (fn && /\.s[ac]ss$/i.test(String(fn)))
|
|
1060
|
+
if (fn && /\.s[ac]ss$/i.test(String(fn))) attachCssWatcher();
|
|
1017
1061
|
} catch (_) {}
|
|
1018
1062
|
}
|
|
1019
1063
|
);
|
|
@@ -1027,8 +1071,9 @@ async function dev() {
|
|
|
1027
1071
|
{ persistent: false },
|
|
1028
1072
|
(evt, fn) => {
|
|
1029
1073
|
try {
|
|
1030
|
-
if (fn && /\.s[ac]ss$/i.test(String(fn)))
|
|
1074
|
+
if (fn && /\.s[ac]ss$/i.test(String(fn))) attachCssWatcher();
|
|
1031
1075
|
} catch (_) {}
|
|
1076
|
+
scan(dir);
|
|
1032
1077
|
}
|
|
1033
1078
|
);
|
|
1034
1079
|
watchers.set(dir, w);
|
|
@@ -1055,8 +1100,10 @@ async function dev() {
|
|
|
1055
1100
|
if (fs.existsSync(configPath)) {
|
|
1056
1101
|
try {
|
|
1057
1102
|
fs.watch(configPath, { persistent: false }, () => {
|
|
1058
|
-
|
|
1059
|
-
|
|
1103
|
+
scheduleTailwindRestart(
|
|
1104
|
+
"[tailwind] tailwind.config change — restarting Tailwind",
|
|
1105
|
+
"[tailwind] compile after config change failed"
|
|
1106
|
+
);
|
|
1060
1107
|
});
|
|
1061
1108
|
} catch (_) {}
|
|
1062
1109
|
}
|
package/lib/build/iiif.js
CHANGED
|
@@ -32,6 +32,34 @@ const IIIF_CACHE_INDEX_MANIFESTS = path.join(
|
|
|
32
32
|
"manifest-index.json"
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
+
const DEFAULT_THUMBNAIL_SIZE = 400;
|
|
36
|
+
const DEFAULT_CHUNK_SIZE = 20;
|
|
37
|
+
const DEFAULT_FETCH_CONCURRENCY = 5;
|
|
38
|
+
|
|
39
|
+
function resolvePositiveInteger(value, fallback) {
|
|
40
|
+
const num = Number(value);
|
|
41
|
+
if (Number.isFinite(num) && num > 0) return Math.max(1, Math.floor(num));
|
|
42
|
+
return Math.max(1, Math.floor(fallback));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveBoolean(value) {
|
|
46
|
+
if (typeof value === "boolean") return value;
|
|
47
|
+
if (value === undefined || value === null) return false;
|
|
48
|
+
const normalized = String(value).trim().toLowerCase();
|
|
49
|
+
if (!normalized) return false;
|
|
50
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveThumbnailPreferences() {
|
|
54
|
+
return {
|
|
55
|
+
size: resolvePositiveInteger(
|
|
56
|
+
process.env.CANOPY_THUMBNAIL_SIZE,
|
|
57
|
+
DEFAULT_THUMBNAIL_SIZE
|
|
58
|
+
),
|
|
59
|
+
unsafe: resolveBoolean(process.env.CANOPY_THUMBNAILS_UNSAFE),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
function firstLabelString(label) {
|
|
36
64
|
if (!label) return "Untitled";
|
|
37
65
|
if (typeof label === "string") return label;
|
|
@@ -373,10 +401,9 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
373
401
|
const CONFIG = cfg || (await loadConfig());
|
|
374
402
|
const featured = Array.isArray(CONFIG && CONFIG.featured) ? CONFIG.featured : [];
|
|
375
403
|
if (!featured.length) return;
|
|
376
|
-
const { getThumbnail } = require("../iiif/thumbnail");
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const unsafeThumbs = !!(CONFIG && CONFIG.iiif && CONFIG.iiif.thumbnails && (CONFIG.iiif.thumbnails.unsafe === true || CONFIG.iiif.thumbnails.unsafe === 'true'));
|
|
404
|
+
const { getThumbnail, getRepresentativeImage } = require("../iiif/thumbnail");
|
|
405
|
+
const { size: thumbSize, unsafe: unsafeThumbs } = resolveThumbnailPreferences();
|
|
406
|
+
const HERO_THUMBNAIL_SIZE = 1200;
|
|
380
407
|
for (const rawId of featured) {
|
|
381
408
|
const id = normalizeIiifId(String(rawId || ''));
|
|
382
409
|
if (!id) continue;
|
|
@@ -392,18 +419,88 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
392
419
|
// Ensure thumbnail fields exist in index for this manifest (if computable)
|
|
393
420
|
try {
|
|
394
421
|
const t = await getThumbnail(manifest, thumbSize, unsafeThumbs);
|
|
422
|
+
const idx = await loadManifestIndex();
|
|
423
|
+
if (!Array.isArray(idx.byId)) continue;
|
|
424
|
+
const entry = idx.byId.find(
|
|
425
|
+
(e) =>
|
|
426
|
+
e &&
|
|
427
|
+
e.type === 'Manifest' &&
|
|
428
|
+
normalizeIiifId(String(e.id)) === normalizeIiifId(String(manifest.id))
|
|
429
|
+
);
|
|
430
|
+
if (!entry) continue;
|
|
431
|
+
|
|
432
|
+
let touched = false;
|
|
395
433
|
if (t && t.url) {
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
434
|
+
const nextUrl = String(t.url);
|
|
435
|
+
if (entry.thumbnail !== nextUrl) {
|
|
436
|
+
entry.thumbnail = nextUrl;
|
|
437
|
+
touched = true;
|
|
438
|
+
}
|
|
439
|
+
if (typeof t.width === 'number') {
|
|
440
|
+
if (entry.thumbnailWidth !== t.width) touched = true;
|
|
441
|
+
entry.thumbnailWidth = t.width;
|
|
442
|
+
}
|
|
443
|
+
if (typeof t.height === 'number') {
|
|
444
|
+
if (entry.thumbnailHeight !== t.height) touched = true;
|
|
445
|
+
entry.thumbnailHeight = t.height;
|
|
405
446
|
}
|
|
406
447
|
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const heroSource = (() => {
|
|
451
|
+
if (manifest && manifest.thumbnail) {
|
|
452
|
+
const clone = { ...manifest };
|
|
453
|
+
try {
|
|
454
|
+
delete clone.thumbnail;
|
|
455
|
+
} catch (_) {
|
|
456
|
+
clone.thumbnail = undefined;
|
|
457
|
+
}
|
|
458
|
+
return clone;
|
|
459
|
+
}
|
|
460
|
+
return manifest;
|
|
461
|
+
})();
|
|
462
|
+
const heroRep = await getRepresentativeImage(
|
|
463
|
+
heroSource || manifest,
|
|
464
|
+
HERO_THUMBNAIL_SIZE,
|
|
465
|
+
true
|
|
466
|
+
);
|
|
467
|
+
if (heroRep && heroRep.id) {
|
|
468
|
+
const nextHero = String(heroRep.id);
|
|
469
|
+
if (entry.heroThumbnail !== nextHero) {
|
|
470
|
+
entry.heroThumbnail = nextHero;
|
|
471
|
+
touched = true;
|
|
472
|
+
}
|
|
473
|
+
if (typeof heroRep.width === 'number') {
|
|
474
|
+
if (entry.heroThumbnailWidth !== heroRep.width) touched = true;
|
|
475
|
+
entry.heroThumbnailWidth = heroRep.width;
|
|
476
|
+
} else if (entry.heroThumbnailWidth !== undefined) {
|
|
477
|
+
delete entry.heroThumbnailWidth;
|
|
478
|
+
touched = true;
|
|
479
|
+
}
|
|
480
|
+
if (typeof heroRep.height === 'number') {
|
|
481
|
+
if (entry.heroThumbnailHeight !== heroRep.height) touched = true;
|
|
482
|
+
entry.heroThumbnailHeight = heroRep.height;
|
|
483
|
+
} else if (entry.heroThumbnailHeight !== undefined) {
|
|
484
|
+
delete entry.heroThumbnailHeight;
|
|
485
|
+
touched = true;
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
if (entry.heroThumbnail !== undefined) {
|
|
489
|
+
delete entry.heroThumbnail;
|
|
490
|
+
touched = true;
|
|
491
|
+
}
|
|
492
|
+
if (entry.heroThumbnailWidth !== undefined) {
|
|
493
|
+
delete entry.heroThumbnailWidth;
|
|
494
|
+
touched = true;
|
|
495
|
+
}
|
|
496
|
+
if (entry.heroThumbnailHeight !== undefined) {
|
|
497
|
+
delete entry.heroThumbnailHeight;
|
|
498
|
+
touched = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch (_) {}
|
|
502
|
+
|
|
503
|
+
if (touched) await saveManifestIndex(idx);
|
|
407
504
|
} catch (_) {}
|
|
408
505
|
}
|
|
409
506
|
} catch (err) {
|
|
@@ -613,11 +710,9 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
613
710
|
if (!tasks.length) return { searchRecords: [] };
|
|
614
711
|
|
|
615
712
|
// Split into chunks and process with limited concurrency
|
|
616
|
-
const chunkSize =
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
process.env.CANOPY_CHUNK_SIZE || (cfg.iiif && cfg.iiif.chunkSize) || 20
|
|
620
|
-
)
|
|
713
|
+
const chunkSize = resolvePositiveInteger(
|
|
714
|
+
process.env.CANOPY_CHUNK_SIZE,
|
|
715
|
+
DEFAULT_CHUNK_SIZE
|
|
621
716
|
);
|
|
622
717
|
const chunks = Math.ceil(tasks.length / chunkSize);
|
|
623
718
|
// Summary before processing chunks
|
|
@@ -630,18 +725,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
630
725
|
);
|
|
631
726
|
} catch (_) {}
|
|
632
727
|
const iiifRecords = [];
|
|
633
|
-
const unsafeThumbs =
|
|
634
|
-
|
|
635
|
-
cfg.iiif &&
|
|
636
|
-
cfg.iiif.thumbnails &&
|
|
637
|
-
cfg.iiif.thumbnails.unsafe === true
|
|
638
|
-
);
|
|
639
|
-
const thumbSize =
|
|
640
|
-
(cfg &&
|
|
641
|
-
cfg.iiif &&
|
|
642
|
-
cfg.iiif.thumbnails &&
|
|
643
|
-
cfg.iiif.thumbnails.preferredSize) ||
|
|
644
|
-
1200;
|
|
728
|
+
const { size: thumbSize, unsafe: unsafeThumbs } =
|
|
729
|
+
resolveThumbnailPreferences();
|
|
645
730
|
|
|
646
731
|
// Compile the works layout component once per run
|
|
647
732
|
const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
|
|
@@ -664,13 +749,9 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
664
749
|
const chunk = tasks.slice(ci * chunkSize, (ci + 1) * chunkSize);
|
|
665
750
|
logLine(`• Chunk ${ci + 1}/${chunks}`, "blue", { dim: true });
|
|
666
751
|
|
|
667
|
-
const concurrency =
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
process.env.CANOPY_FETCH_CONCURRENCY ||
|
|
671
|
-
(cfg.iiif && cfg.iiif.concurrency) ||
|
|
672
|
-
6
|
|
673
|
-
)
|
|
752
|
+
const concurrency = resolvePositiveInteger(
|
|
753
|
+
process.env.CANOPY_FETCH_CONCURRENCY,
|
|
754
|
+
DEFAULT_FETCH_CONCURRENCY
|
|
674
755
|
);
|
|
675
756
|
let next = 0;
|
|
676
757
|
const logs = new Array(chunk.length);
|
|
@@ -831,12 +912,12 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
831
912
|
const needsHydrateViewer = body.includes("data-canopy-viewer");
|
|
832
913
|
const needsRelated = body.includes("data-canopy-related-items");
|
|
833
914
|
const needsHero = body.includes("data-canopy-hero");
|
|
834
|
-
const
|
|
915
|
+
const needsSearchForm = body.includes("data-canopy-search-form");
|
|
835
916
|
const needsHydrate =
|
|
836
917
|
body.includes("data-canopy-hydrate") ||
|
|
837
918
|
needsHydrateViewer ||
|
|
838
919
|
needsRelated ||
|
|
839
|
-
|
|
920
|
+
needsSearchForm;
|
|
840
921
|
|
|
841
922
|
const viewerRel = needsHydrateViewer
|
|
842
923
|
? path
|
|
@@ -874,11 +955,11 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
874
955
|
.split(path.sep)
|
|
875
956
|
.join("/")
|
|
876
957
|
: null;
|
|
877
|
-
const
|
|
958
|
+
const searchFormRel = needsSearchForm
|
|
878
959
|
? path
|
|
879
960
|
.relative(
|
|
880
961
|
path.dirname(outPath),
|
|
881
|
-
path.join(OUT_DIR, "scripts", "canopy-
|
|
962
|
+
path.join(OUT_DIR, "scripts", "canopy-search-form.js")
|
|
882
963
|
)
|
|
883
964
|
.split(path.sep)
|
|
884
965
|
.join("/")
|
|
@@ -919,8 +1000,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
919
1000
|
extraScripts.push(`<script defer src="${viewerRel}"></script>`);
|
|
920
1001
|
if (sliderRel && jsRel !== sliderRel)
|
|
921
1002
|
extraScripts.push(`<script defer src="${sliderRel}"></script>`);
|
|
922
|
-
if (
|
|
923
|
-
extraScripts.push(`<script defer src="${
|
|
1003
|
+
if (searchFormRel && jsRel !== searchFormRel)
|
|
1004
|
+
extraScripts.push(`<script defer src="${searchFormRel}"></script>`);
|
|
924
1005
|
if (extraScripts.length)
|
|
925
1006
|
headExtra = extraScripts.join("") + headExtra;
|
|
926
1007
|
try {
|
package/lib/build/mdx.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
withBase,
|
|
13
13
|
} = require("../common");
|
|
14
14
|
const yaml = require("js-yaml");
|
|
15
|
+
const { getPageContext } = require("../page-context");
|
|
15
16
|
|
|
16
17
|
function parseFrontmatter(src) {
|
|
17
18
|
let input = String(src || "");
|
|
@@ -112,7 +113,7 @@ async function loadUiComponents() {
|
|
|
112
113
|
}
|
|
113
114
|
let comp = (mod && typeof mod === 'object') ? mod : {};
|
|
114
115
|
// Hard-require core exports; do not inject fallbacks
|
|
115
|
-
const required = ['SearchPanel', '
|
|
116
|
+
const required = ['SearchPanel', 'SearchFormModal', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems', 'Hero', 'FeaturedHero'];
|
|
116
117
|
const missing = required.filter((k) => !comp || !comp[k]);
|
|
117
118
|
if (missing.length) {
|
|
118
119
|
throw new Error('[canopy][mdx] Missing UI exports: ' + missing.join(', '));
|
|
@@ -123,7 +124,7 @@ async function loadUiComponents() {
|
|
|
123
124
|
mtime: currentMtime,
|
|
124
125
|
hasServerExport: !!mod,
|
|
125
126
|
hasWorkspace: typeof comp !== 'undefined',
|
|
126
|
-
|
|
127
|
+
SearchFormModal: !!comp.SearchFormModal,
|
|
127
128
|
Viewer: !!comp.Viewer,
|
|
128
129
|
Slider: !!comp.Slider,
|
|
129
130
|
}); } catch(_){}
|
|
@@ -280,10 +281,19 @@ async function compileMdxFile(filePath, outPath, Layout, extraProps = {}) {
|
|
|
280
281
|
const app = await loadAppWrapper();
|
|
281
282
|
const dirLayout = await getNearestDirLayout(filePath);
|
|
282
283
|
const contentNode = React.createElement(MDXContent, extraProps);
|
|
284
|
+
const layoutProps = dirLayout ? { ...extraProps } : null;
|
|
283
285
|
const withLayout = dirLayout
|
|
284
|
-
? React.createElement(dirLayout,
|
|
286
|
+
? React.createElement(dirLayout, layoutProps, contentNode)
|
|
285
287
|
: contentNode;
|
|
286
|
-
const
|
|
288
|
+
const PageContext = getPageContext();
|
|
289
|
+
const contextValue = {
|
|
290
|
+
navigation: extraProps && extraProps.navigation ? extraProps.navigation : null,
|
|
291
|
+
page: extraProps && extraProps.page ? extraProps.page : null,
|
|
292
|
+
};
|
|
293
|
+
const withContext = PageContext
|
|
294
|
+
? React.createElement(PageContext.Provider, { value: contextValue }, withLayout)
|
|
295
|
+
: withLayout;
|
|
296
|
+
const withApp = React.createElement(app.App, null, withContext);
|
|
287
297
|
const compMap = { ...components, a: Anchor };
|
|
288
298
|
const page = MDXProvider
|
|
289
299
|
? React.createElement(MDXProvider, { components: compMap }, withApp)
|
package/lib/build/pages.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { fs, fsp, path, CONTENT_DIR, OUT_DIR, ensureDirSync, htmlShell } = require('../common');
|
|
2
2
|
const { log } = require('./log');
|
|
3
3
|
const mdx = require('./mdx');
|
|
4
|
+
const navigation = require('../components/navigation');
|
|
4
5
|
|
|
5
6
|
// Cache: dir -> frontmatter data for _layout.mdx in that dir
|
|
6
7
|
const LAYOUT_META = new Map();
|
|
@@ -44,10 +45,23 @@ function mapContentPathToOutput(filePath) {
|
|
|
44
45
|
async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
45
46
|
const source = await fsp.readFile(filePath, 'utf8');
|
|
46
47
|
const title = mdx.extractTitle(source);
|
|
47
|
-
const
|
|
48
|
+
const relContentPath = path.relative(CONTENT_DIR, filePath);
|
|
49
|
+
const normalizedRel = navigation.normalizeRelativePath(relContentPath);
|
|
50
|
+
const pageInfo = navigation.getPageInfo(normalizedRel);
|
|
51
|
+
const navData = navigation.buildNavigationForFile(normalizedRel);
|
|
52
|
+
const mergedProps = { ...(extraProps || {}) };
|
|
53
|
+
if (pageInfo) {
|
|
54
|
+
mergedProps.page = mergedProps.page
|
|
55
|
+
? { ...pageInfo, ...mergedProps.page }
|
|
56
|
+
: pageInfo;
|
|
57
|
+
}
|
|
58
|
+
if (navData && !mergedProps.navigation) {
|
|
59
|
+
mergedProps.navigation = navData;
|
|
60
|
+
}
|
|
61
|
+
const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, mergedProps);
|
|
48
62
|
const needsHydrateViewer = body.includes('data-canopy-viewer');
|
|
49
63
|
const needsHydrateSlider = body.includes('data-canopy-slider');
|
|
50
|
-
const
|
|
64
|
+
const needsSearchForm = true; // search form runtime is global
|
|
51
65
|
const needsFacets = body.includes('data-canopy-related-items');
|
|
52
66
|
const viewerRel = needsHydrateViewer
|
|
53
67
|
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-viewer.js')).split(path.sep).join('/')
|
|
@@ -58,12 +72,12 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
58
72
|
const facetsRel = needsFacets
|
|
59
73
|
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-related-items.js')).split(path.sep).join('/')
|
|
60
74
|
: null;
|
|
61
|
-
let
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
let rel = path.relative(path.dirname(outPath),
|
|
65
|
-
try { const st = fs.statSync(
|
|
66
|
-
|
|
75
|
+
let searchFormRel = null;
|
|
76
|
+
if (needsSearchForm) {
|
|
77
|
+
const runtimeAbs = path.join(OUT_DIR, 'scripts', 'canopy-search-form.js');
|
|
78
|
+
let rel = path.relative(path.dirname(outPath), runtimeAbs).split(path.sep).join('/');
|
|
79
|
+
try { const st = fs.statSync(runtimeAbs); rel += `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
|
|
80
|
+
searchFormRel = rel;
|
|
67
81
|
}
|
|
68
82
|
let jsRel = null;
|
|
69
83
|
if (needsFacets && sliderRel) jsRel = sliderRel;
|
|
@@ -90,7 +104,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
90
104
|
if (facetsRel && jsRel !== facetsRel) extraScripts.push(`<script defer src="${facetsRel}"></script>`);
|
|
91
105
|
if (viewerRel && jsRel !== viewerRel) extraScripts.push(`<script defer src="${viewerRel}"></script>`);
|
|
92
106
|
if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
|
|
93
|
-
if (
|
|
107
|
+
if (searchFormRel && jsRel !== searchFormRel) extraScripts.push(`<script defer src="${searchFormRel}"></script>`);
|
|
94
108
|
if (extraScripts.length) headExtra = extraScripts.join('') + headExtra;
|
|
95
109
|
const html = htmlShell({ title, body, cssHref: null, scriptHref: jsRel, headExtra: vendorTag + headExtra });
|
|
96
110
|
const { applyBaseToHtml } = require('../common');
|
package/lib/build/runtimes.js
CHANGED
|
@@ -12,7 +12,7 @@ async function prepareAllRuntimes() {
|
|
|
12
12
|
} catch (_) {}
|
|
13
13
|
try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
|
|
14
14
|
try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
|
|
15
|
-
await
|
|
15
|
+
await prepareSearchFormRuntime();
|
|
16
16
|
try { logLine('✓ Prepared client hydration runtimes', 'cyan', { dim: true }); } catch (_) {}
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -24,14 +24,14 @@ async function resolveEsbuild() {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async function
|
|
27
|
+
async function prepareSearchFormRuntime() {
|
|
28
28
|
const esbuild = await resolveEsbuild();
|
|
29
|
-
if (!esbuild) throw new Error('
|
|
29
|
+
if (!esbuild) throw new Error('Search form runtime bundling requires esbuild. Install dependencies before building.');
|
|
30
30
|
ensureDirSync(OUT_DIR);
|
|
31
31
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
32
32
|
ensureDirSync(scriptsDir);
|
|
33
|
-
const entry = path.join(__dirname, '..', 'search', '
|
|
34
|
-
const outFile = path.join(scriptsDir, 'canopy-
|
|
33
|
+
const entry = path.join(__dirname, '..', 'search', 'search-form-runtime.js');
|
|
34
|
+
const outFile = path.join(scriptsDir, 'canopy-search-form.js');
|
|
35
35
|
await esbuild.build({
|
|
36
36
|
entryPoints: [entry],
|
|
37
37
|
outfile: outFile,
|
|
@@ -68,4 +68,4 @@ async function prepareSearchRuntime(timeoutMs = 10000, label = '') {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
module.exports = { prepareAllRuntimes,
|
|
71
|
+
module.exports = { prepareAllRuntimes, prepareSearchFormRuntime, prepareSearchRuntime };
|
package/lib/build/search.js
CHANGED
|
@@ -188,23 +188,20 @@ async function collectMdxPageRecords() {
|
|
|
188
188
|
let include = !underSearch;
|
|
189
189
|
let resolvedType = null;
|
|
190
190
|
const pageFm = fm && fm.data ? fm.data : null;
|
|
191
|
-
if (pageFm)
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
else include = false;
|
|
196
|
-
} else {
|
|
197
|
-
include = false; // frontmatter present w/o type => exclude per policy
|
|
198
|
-
}
|
|
191
|
+
if (pageFm && pageFm.search === false) include = false;
|
|
192
|
+
if (include && pageFm && Object.prototype.hasOwnProperty.call(pageFm, 'type')) {
|
|
193
|
+
if (pageFm.type) resolvedType = String(pageFm.type);
|
|
194
|
+
else include = false;
|
|
199
195
|
}
|
|
200
196
|
if (include && !resolvedType) {
|
|
201
197
|
const layoutMeta = await pagesHelpers.getNearestDirLayoutMeta(p);
|
|
202
198
|
if (layoutMeta && layoutMeta.type) resolvedType = String(layoutMeta.type);
|
|
203
199
|
}
|
|
204
200
|
if (include && !resolvedType) {
|
|
205
|
-
|
|
201
|
+
resolvedType = 'page';
|
|
206
202
|
}
|
|
207
|
-
|
|
203
|
+
const trimmedType = resolvedType && String(resolvedType).trim();
|
|
204
|
+
pages.push({ title, href, searchInclude: include && !!trimmedType, searchType: trimmedType || undefined });
|
|
208
205
|
}
|
|
209
206
|
}
|
|
210
207
|
}
|