@canopy-iiif/app 0.8.1 → 0.8.3

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 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/command-runtime.js`); `prepareCommandRuntime()` now builds `site/scripts/canopy-command.js` and fails if esbuild is missing.
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
 
@@ -62,6 +62,14 @@ async function build(options = {}) {
62
62
  if (!skipIiif) {
63
63
  const results = await iiif.buildIiifCollectionPages(CONFIG);
64
64
  iiifRecords = results?.iiifRecords;
65
+ iiifRecordsCache = Array.isArray(iiifRecords) ? iiifRecords : [];
66
+ } else {
67
+ iiifRecords = Array.isArray(iiifRecordsCache) ? iiifRecordsCache : [];
68
+ logLine(
69
+ `• Reusing cached IIIF search records (${iiifRecords.length})`,
70
+ "blue",
71
+ { dim: true }
72
+ );
65
73
  }
66
74
  // Ensure any configured featured manifests are cached (and thumbnails computed)
67
75
  // so SSR components like <Hero /> can resolve items even if they are not part of
package/lib/build/dev.js CHANGED
@@ -969,17 +969,16 @@ async function dev() {
969
969
  child = startTailwindWatcher();
970
970
 
971
971
  const uiPlugin = path.join(
972
- __dirname,
973
- "../ui",
972
+ APP_UI_DIR,
974
973
  "tailwind-canopy-iiif-plugin.js"
975
974
  );
976
975
  const uiPreset = path.join(
977
- __dirname,
978
- "../ui",
976
+ APP_UI_DIR,
979
977
  "tailwind-canopy-iiif-preset.js"
980
978
  );
981
- const uiStylesDir = path.join(__dirname, "../ui", "styles");
982
- const files = [uiPlugin, uiPreset].filter((p) => {
979
+ const uiStylesDir = path.join(APP_UI_DIR, "styles");
980
+ const uiStylesCss = path.join(uiStylesDir, "index.css");
981
+ const pluginFiles = [uiPlugin, uiPreset].filter((p) => {
983
982
  try {
984
983
  return fs.existsSync(p);
985
984
  } catch (_) {
@@ -987,25 +986,54 @@ async function dev() {
987
986
  }
988
987
  });
989
988
  let restartTimer = null;
990
- const restart = () => {
989
+ let uiCssWatcherAttached = false;
990
+ const scheduleTailwindRestart = (message, compileLabel) => {
991
991
  clearTimeout(restartTimer);
992
992
  restartTimer = setTimeout(() => {
993
- console.log(
994
- "[tailwind] detected UI plugin/preset change — restarting Tailwind"
995
- );
993
+ if (message) console.log(message);
996
994
  try {
997
995
  if (child && !child.killed) child.kill();
998
996
  } catch (_) {}
999
- safeCompile("[tailwind] compile after plugin change failed");
997
+ safeCompile(compileLabel);
1000
998
  child = startTailwindWatcher();
1001
999
  try { onBuildStart(); } catch (_) {}
1002
- }, 50);
1000
+ }, 120);
1003
1001
  };
1004
- for (const f of files) {
1002
+ for (const f of pluginFiles) {
1005
1003
  try {
1006
- fs.watch(f, { persistent: false }, restart);
1004
+ fs.watch(f, { persistent: false }, () => {
1005
+ scheduleTailwindRestart(
1006
+ "[tailwind] detected UI plugin/preset change — restarting Tailwind",
1007
+ "[tailwind] compile after plugin change failed"
1008
+ );
1009
+ });
1007
1010
  } catch (_) {}
1008
1011
  }
1012
+ const attachCssWatcher = () => {
1013
+ if (uiCssWatcherAttached) {
1014
+ if (fs.existsSync(uiStylesCss)) return;
1015
+ uiCssWatcherAttached = false;
1016
+ }
1017
+ if (!fs.existsSync(uiStylesCss)) return;
1018
+ const handler = () =>
1019
+ scheduleTailwindRestart(
1020
+ "[tailwind] detected @canopy-iiif/app/ui styles change — restarting Tailwind",
1021
+ "[tailwind] compile after UI styles change failed"
1022
+ );
1023
+ try {
1024
+ const watcher = fs.watch(uiStylesCss, { persistent: false }, handler);
1025
+ uiCssWatcherAttached = true;
1026
+ watcher.on("close", () => {
1027
+ uiCssWatcherAttached = false;
1028
+ });
1029
+ } catch (_) {
1030
+ try {
1031
+ fs.watchFile(uiStylesCss, { interval: 250 }, handler);
1032
+ uiCssWatcherAttached = true;
1033
+ } catch (_) {}
1034
+ }
1035
+ };
1036
+ attachCssWatcher();
1009
1037
  if (fs.existsSync(uiStylesDir)) {
1010
1038
  try {
1011
1039
  fs.watch(
@@ -1013,7 +1041,7 @@ async function dev() {
1013
1041
  { persistent: false, recursive: true },
1014
1042
  (evt, fn) => {
1015
1043
  try {
1016
- if (fn && /\.s[ac]ss$/i.test(String(fn))) restart();
1044
+ if (fn && /\.s[ac]ss$/i.test(String(fn))) attachCssWatcher();
1017
1045
  } catch (_) {}
1018
1046
  }
1019
1047
  );
@@ -1027,8 +1055,9 @@ async function dev() {
1027
1055
  { persistent: false },
1028
1056
  (evt, fn) => {
1029
1057
  try {
1030
- if (fn && /\.s[ac]ss$/i.test(String(fn))) restart();
1058
+ if (fn && /\.s[ac]ss$/i.test(String(fn))) attachCssWatcher();
1031
1059
  } catch (_) {}
1060
+ scan(dir);
1032
1061
  }
1033
1062
  );
1034
1063
  watchers.set(dir, w);
@@ -1055,8 +1084,10 @@ async function dev() {
1055
1084
  if (fs.existsSync(configPath)) {
1056
1085
  try {
1057
1086
  fs.watch(configPath, { persistent: false }, () => {
1058
- console.log("[tailwind] tailwind.config change — restarting Tailwind");
1059
- restart();
1087
+ scheduleTailwindRestart(
1088
+ "[tailwind] tailwind.config change — restarting Tailwind",
1089
+ "[tailwind] compile after config change failed"
1090
+ );
1060
1091
  });
1061
1092
  } catch (_) {}
1062
1093
  }
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
- // Thumbnail sizing config
378
- const thumbSize = CONFIG && CONFIG.iiif && CONFIG.iiif.thumbnails && typeof CONFIG.iiif.thumbnails.preferredSize === 'number' ? CONFIG.iiif.thumbnails.preferredSize : 400;
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 idx = await loadManifestIndex();
397
- if (Array.isArray(idx.byId)) {
398
- const entry = idx.byId.find((e) => e && e.type === 'Manifest' && normalizeIiifId(String(e.id)) === normalizeIiifId(String(manifest.id)));
399
- if (entry) {
400
- entry.thumbnail = String(t.url);
401
- if (typeof t.width === 'number') entry.thumbnailWidth = t.width;
402
- if (typeof t.height === 'number') entry.thumbnailHeight = t.height;
403
- await saveManifestIndex(idx);
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 = Math.max(
617
- 1,
618
- Number(
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
- cfg &&
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 = Math.max(
668
- 1,
669
- Number(
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 needsCommand = body.includes("data-canopy-command");
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
- needsCommand;
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 commandRel = needsCommand
958
+ const searchFormRel = needsSearchForm
878
959
  ? path
879
960
  .relative(
880
961
  path.dirname(outPath),
881
- path.join(OUT_DIR, "scripts", "canopy-command.js")
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 (commandRel && jsRel !== commandRel)
923
- extraScripts.push(`<script defer src="${commandRel}"></script>`);
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
@@ -112,7 +112,7 @@ async function loadUiComponents() {
112
112
  }
113
113
  let comp = (mod && typeof mod === 'object') ? mod : {};
114
114
  // Hard-require core exports; do not inject fallbacks
115
- const required = ['SearchPanel', 'CommandPalette', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems', 'Hero', 'FeaturedHero'];
115
+ const required = ['SearchPanel', 'SearchFormModal', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems', 'Hero', 'FeaturedHero'];
116
116
  const missing = required.filter((k) => !comp || !comp[k]);
117
117
  if (missing.length) {
118
118
  throw new Error('[canopy][mdx] Missing UI exports: ' + missing.join(', '));
@@ -123,7 +123,7 @@ async function loadUiComponents() {
123
123
  mtime: currentMtime,
124
124
  hasServerExport: !!mod,
125
125
  hasWorkspace: typeof comp !== 'undefined',
126
- CommandPalette: !!comp.CommandPalette,
126
+ SearchFormModal: !!comp.SearchFormModal,
127
127
  Viewer: !!comp.Viewer,
128
128
  Slider: !!comp.Slider,
129
129
  }); } catch(_){}
@@ -47,7 +47,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
47
47
  const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, extraProps);
48
48
  const needsHydrateViewer = body.includes('data-canopy-viewer');
49
49
  const needsHydrateSlider = body.includes('data-canopy-slider');
50
- const needsCommand = true; // command runtime is global
50
+ const needsSearchForm = true; // search form runtime is global
51
51
  const needsFacets = body.includes('data-canopy-related-items');
52
52
  const viewerRel = needsHydrateViewer
53
53
  ? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-viewer.js')).split(path.sep).join('/')
@@ -58,12 +58,12 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
58
58
  const facetsRel = needsFacets
59
59
  ? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-related-items.js')).split(path.sep).join('/')
60
60
  : null;
61
- let commandRel = null;
62
- if (needsCommand) {
63
- const cmdAbs = path.join(OUT_DIR, 'scripts', 'canopy-command.js');
64
- let rel = path.relative(path.dirname(outPath), cmdAbs).split(path.sep).join('/');
65
- try { const st = fs.statSync(cmdAbs); rel += `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
66
- commandRel = rel;
61
+ let searchFormRel = null;
62
+ if (needsSearchForm) {
63
+ const runtimeAbs = path.join(OUT_DIR, 'scripts', 'canopy-search-form.js');
64
+ let rel = path.relative(path.dirname(outPath), runtimeAbs).split(path.sep).join('/');
65
+ try { const st = fs.statSync(runtimeAbs); rel += `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
66
+ searchFormRel = rel;
67
67
  }
68
68
  let jsRel = null;
69
69
  if (needsFacets && sliderRel) jsRel = sliderRel;
@@ -90,7 +90,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
90
90
  if (facetsRel && jsRel !== facetsRel) extraScripts.push(`<script defer src="${facetsRel}"></script>`);
91
91
  if (viewerRel && jsRel !== viewerRel) extraScripts.push(`<script defer src="${viewerRel}"></script>`);
92
92
  if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
93
- if (commandRel && jsRel !== commandRel) extraScripts.push(`<script defer src="${commandRel}"></script>`);
93
+ if (searchFormRel && jsRel !== searchFormRel) extraScripts.push(`<script defer src="${searchFormRel}"></script>`);
94
94
  if (extraScripts.length) headExtra = extraScripts.join('') + headExtra;
95
95
  const html = htmlShell({ title, body, cssHref: null, scriptHref: jsRel, headExtra: vendorTag + headExtra });
96
96
  const { applyBaseToHtml } = require('../common');
@@ -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 prepareCommandRuntime();
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 prepareCommandRuntime() {
27
+ async function prepareSearchFormRuntime() {
28
28
  const esbuild = await resolveEsbuild();
29
- if (!esbuild) throw new Error('Command runtime bundling requires esbuild. Install dependencies before building.');
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', 'command-runtime.js');
34
- const outFile = path.join(scriptsDir, 'canopy-command.js');
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, prepareCommandRuntime, prepareSearchRuntime };
71
+ module.exports = { prepareAllRuntimes, prepareSearchFormRuntime, prepareSearchRuntime };
@@ -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
- if (pageFm.search === false) include = false;
193
- if (Object.prototype.hasOwnProperty.call(pageFm, 'type')) {
194
- if (pageFm.type) resolvedType = String(pageFm.type);
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
- if (!pageFm) resolvedType = 'page';
201
+ resolvedType = 'page';
206
202
  }
207
- pages.push({ title, href, searchInclude: include && !!resolvedType, searchType: resolvedType || undefined });
203
+ const trimmedType = resolvedType && String(resolvedType).trim();
204
+ pages.push({ title, href, searchInclude: include && !!trimmedType, searchType: trimmedType || undefined });
208
205
  }
209
206
  }
210
207
  }
@@ -21,10 +21,10 @@ function verifyHomepageElements(outDir) {
21
21
  const idx = path.join(outDir, 'index.html');
22
22
  const html = readFileSafe(idx);
23
23
  const okHero = /class=\"[^\"]*canopy-hero/.test(html) || /<div[^>]+canopy-hero/.test(html);
24
- const okCommand = /data-canopy-command=/.test(html);
25
- const okCommandTrigger = /data-canopy-command-trigger/.test(html);
26
- const okCommandScriptRef = /<script[^>]+canopy-command\.js/.test(html);
27
- return { okHero, okCommand, okCommandTrigger, okCommandScriptRef, htmlPath: idx };
24
+ const okSearchForm = /data-canopy-search-form=/.test(html);
25
+ const okSearchFormTrigger = /data-canopy-search-form-trigger/.test(html);
26
+ const okSearchFormScriptRef = /<script[^>]+canopy-search-form\.js/.test(html);
27
+ return { okHero, okSearchForm, okSearchFormTrigger, okSearchFormScriptRef, htmlPath: idx };
28
28
  }
29
29
 
30
30
  function verifyBuildOutput(options = {}) {
@@ -34,7 +34,7 @@ function verifyBuildOutput(options = {}) {
34
34
  const okAny = total > 0;
35
35
  const indexPath = path.join(outDir, 'index.html');
36
36
  const hasIndex = fs.existsSync(indexPath) && fs.statSync(indexPath).size > 0;
37
- const { okHero, okCommand, okCommandTrigger, okCommandScriptRef } = verifyHomepageElements(outDir);
37
+ const { okHero, okSearchForm, okSearchFormTrigger, okSearchFormScriptRef } = verifyHomepageElements(outDir);
38
38
 
39
39
  const ck = (label, ok, extra) => {
40
40
  const status = ok ? '✓' : '✗';
@@ -44,12 +44,12 @@ function verifyBuildOutput(options = {}) {
44
44
  ck('HTML pages exist', okAny, okAny ? `(${total})` : '');
45
45
  ck('homepage exists', hasIndex, hasIndex ? `(${indexPath})` : '');
46
46
  ck('homepage: Hero present', okHero);
47
- ck('homepage: Command present', okCommand);
48
- ck('homepage: Command trigger present', okCommandTrigger);
49
- ck('homepage: Command script referenced', okCommandScriptRef);
47
+ ck('homepage: Search form present', okSearchForm);
48
+ ck('homepage: Search form trigger present', okSearchFormTrigger);
49
+ ck('homepage: Search form script referenced', okSearchFormScriptRef);
50
50
 
51
51
  // Do not fail build on missing SSR trigger; the client runtime injects a default.
52
- const ok = okAny && hasIndex && okHero && okCommand && okCommandScriptRef;
52
+ const ok = okAny && hasIndex && okHero && okSearchForm && okSearchFormScriptRef;
53
53
  if (!ok) {
54
54
  const err = new Error('Build verification failed');
55
55
  err.outDir = outDir;
@@ -105,9 +105,23 @@ function readFeaturedFromCacheSync() {
105
105
  href: rootRelativeHref(path.join('works', slug + '.html').split(path.sep).join('/')),
106
106
  type: 'work',
107
107
  };
108
- if (entry && entry.thumbnail) rec.thumbnail = String(entry.thumbnail);
109
- if (entry && typeof entry.thumbnailWidth === 'number') rec.thumbnailWidth = entry.thumbnailWidth;
110
- if (entry && typeof entry.thumbnailHeight === 'number') rec.thumbnailHeight = entry.thumbnailHeight;
108
+ if (entry && entry.heroThumbnail) {
109
+ rec.thumbnail = String(entry.heroThumbnail);
110
+ if (typeof entry.heroThumbnailWidth === 'number') {
111
+ rec.thumbnailWidth = entry.heroThumbnailWidth;
112
+ } else if (typeof entry.thumbnailWidth === 'number') {
113
+ rec.thumbnailWidth = entry.thumbnailWidth;
114
+ }
115
+ if (typeof entry.heroThumbnailHeight === 'number') {
116
+ rec.thumbnailHeight = entry.heroThumbnailHeight;
117
+ } else if (typeof entry.thumbnailHeight === 'number') {
118
+ rec.thumbnailHeight = entry.thumbnailHeight;
119
+ }
120
+ } else {
121
+ if (entry && entry.thumbnail) rec.thumbnail = String(entry.thumbnail);
122
+ if (entry && typeof entry.thumbnailWidth === 'number') rec.thumbnailWidth = entry.thumbnailWidth;
123
+ if (entry && typeof entry.thumbnailHeight === 'number') rec.thumbnailHeight = entry.thumbnailHeight;
124
+ }
111
125
  if (!rec.thumbnail) {
112
126
  try {
113
127
  const t = m && m.thumbnail;
@@ -681,7 +681,7 @@ function parseProps(el) {
681
681
  function bindSearchInputToStore() {
682
682
  if (!store || typeof document === "undefined") return;
683
683
  try {
684
- const input = document.querySelector("[data-canopy-command-input]");
684
+ const input = document.querySelector("[data-canopy-search-form-input]");
685
685
  if (!input || input.dataset.canopySearchSync === "1") return;
686
686
  input.dataset.canopySearchSync = "1";
687
687
 
@@ -764,7 +764,7 @@ if (typeof document !== "undefined") {
764
764
  const q =
765
765
  ev && ev.detail && typeof ev.detail.query === "string"
766
766
  ? ev.detail.query
767
- : document.querySelector("[data-canopy-command-input]")?.value ||
767
+ : document.querySelector("[data-canopy-search-form-input]")?.value ||
768
768
  "";
769
769
  if (typeof q === "string") store.setQuery(q);
770
770
  } catch (_) {}
@@ -217,7 +217,7 @@ function bindKeyboardNavigation({ input, list, panel }) {
217
217
  });
218
218
  }
219
219
 
220
- async function attachCommand(host) {
220
+ async function attachSearchForm(host) {
221
221
  const config = parseProps(host) || {};
222
222
  const maxResults = Number(config.maxResults || 8) || 8;
223
223
  const groupOrder = Array.isArray(config.groupOrder) ? config.groupOrder : ['work', 'page'];
@@ -225,14 +225,14 @@ async function attachCommand(host) {
225
225
  const onSearchPage = isOnSearchPage();
226
226
 
227
227
  const panel = (() => {
228
- try { return host.querySelector('[data-canopy-command-panel]'); } catch (_) { return null; }
228
+ try { return host.querySelector('[data-canopy-search-form-panel]'); } catch (_) { return null; }
229
229
  })();
230
230
  if (!panel) return;
231
231
 
232
232
  if (!onSearchPage) {
233
233
  try {
234
234
  const wrapper = host.querySelector('.relative');
235
- if (wrapper) wrapper.setAttribute('data-canopy-panel-auto', '1');
235
+ if (wrapper) wrapper.setAttribute('data-canopy-search-form-auto', '1');
236
236
  } catch (_) {}
237
237
  }
238
238
 
@@ -244,7 +244,7 @@ async function attachCommand(host) {
244
244
  if (!list) return;
245
245
 
246
246
  const input = (() => {
247
- try { return host.querySelector('[data-canopy-command-input]'); } catch (_) { return null; }
247
+ try { return host.querySelector('[data-canopy-search-form-input]'); } catch (_) { return null; }
248
248
  })();
249
249
  if (!input) return;
250
250
 
@@ -345,9 +345,9 @@ async function attachCommand(host) {
345
345
  }
346
346
 
347
347
  host.addEventListener('click', (event) => {
348
- const trigger = event.target && event.target.closest && event.target.closest('[data-canopy-command-trigger]');
348
+ const trigger = event.target && event.target.closest && event.target.closest('[data-canopy-search-form-trigger]');
349
349
  if (!trigger) return;
350
- const mode = (trigger.dataset && trigger.dataset.canopyCommandTrigger) || '';
350
+ const mode = (trigger.dataset && trigger.dataset.canopySearchFormTrigger) || '';
351
351
  if (mode === 'submit' || mode === 'form') return;
352
352
  event.preventDefault();
353
353
  openPanel();
@@ -359,12 +359,12 @@ async function attachCommand(host) {
359
359
  }
360
360
 
361
361
  ready(() => {
362
- const hosts = Array.from(document.querySelectorAll('[data-canopy-command]'));
362
+ const hosts = Array.from(document.querySelectorAll('[data-canopy-search-form]'));
363
363
  if (!hosts.length) return;
364
364
  hosts.forEach((host) => {
365
- attachCommand(host).catch((err) => {
365
+ attachSearchForm(host).catch((err) => {
366
366
  try {
367
- console.warn('[canopy][command] failed to initialise', err && (err.message || err));
367
+ console.warn('[canopy][search-form] failed to initialise', err && (err.message || err));
368
368
  } catch (_) {}
369
369
  });
370
370
  });