@canopy-iiif/app 0.7.14 → 0.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/build/iiif.js CHANGED
@@ -22,7 +22,7 @@ const IIIF_CACHE_COLLECTIONS_DIR = path.join(IIIF_CACHE_DIR, "collections");
22
22
  const IIIF_CACHE_COLLECTION = path.join(IIIF_CACHE_DIR, "collection.json");
23
23
  // Primary global index location
24
24
  const IIIF_CACHE_INDEX = path.join(IIIF_CACHE_DIR, "index.json");
25
- // Legacy locations kept for backward compatibility (read + optional write)
25
+ // Additional legacy locations kept for backward compatibility (read + optional write)
26
26
  const IIIF_CACHE_INDEX_LEGACY = path.join(
27
27
  IIIF_CACHE_DIR,
28
28
  "manifest-index.json"
@@ -150,7 +150,7 @@ async function loadManifestIndex() {
150
150
  return { byId, collection: idx.collection || null };
151
151
  }
152
152
  }
153
- // Fallback: legacy in .cache/iiif
153
+ // Legacy index location retained for backward compatibility
154
154
  if (fs.existsSync(IIIF_CACHE_INDEX_LEGACY)) {
155
155
  const idx = await readJson(IIIF_CACHE_INDEX_LEGACY);
156
156
  if (idx && typeof idx === "object") {
@@ -167,7 +167,7 @@ async function loadManifestIndex() {
167
167
  return { byId, collection: idx.collection || null };
168
168
  }
169
169
  }
170
- // Fallback: legacy in manifests subdir
170
+ // Legacy manifests index retained for backward compatibility
171
171
  if (fs.existsSync(IIIF_CACHE_INDEX_MANIFESTS)) {
172
172
  const idx = await readJson(IIIF_CACHE_INDEX_MANIFESTS);
173
173
  if (idx && typeof idx === "object") {
@@ -406,8 +406,9 @@ async function ensureFeaturedInCache(cfg) {
406
406
  }
407
407
  } catch (_) {}
408
408
  }
409
- } catch (_) {
410
- // ignore failures; fallback SSR will still render a minimal hero without content
409
+ } catch (err) {
410
+ const message = err && err.message ? err.message : err;
411
+ throw new Error(`[iiif] Failed to populate featured cache: ${message}`);
411
412
  }
412
413
  }
413
414
 
@@ -643,33 +644,20 @@ async function buildIiifCollectionPages(CONFIG) {
643
644
  1200;
644
645
 
645
646
  // Compile the works layout component once per run
647
+ const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
648
+ if (!fs.existsSync(worksLayoutPath)) {
649
+ throw new Error(
650
+ "IIIF build requires content/works/_layout.mdx. Create the layout instead of relying on generated output."
651
+ );
652
+ }
646
653
  let WorksLayoutComp = null;
647
654
  try {
648
- const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
649
655
  WorksLayoutComp = await mdx.compileMdxToComponent(worksLayoutPath);
650
- } catch (_) {
651
- // Minimal fallback layout if missing or fails to compile
652
- WorksLayoutComp = function FallbackWorksLayout({ manifest }) {
653
- const title = firstLabelString(manifest && manifest.label);
654
- return React.createElement(
655
- "div",
656
- { className: "content" },
657
- React.createElement("h1", null, title || "Untitled"),
658
- // Render viewer placeholder for hydration
659
- React.createElement(
660
- "div",
661
- { "data-canopy-viewer": "1" },
662
- React.createElement("script", {
663
- type: "application/json",
664
- dangerouslySetInnerHTML: {
665
- __html: JSON.stringify({
666
- iiifContent: manifest && (manifest.id || ""),
667
- }),
668
- },
669
- })
670
- )
671
- );
672
- };
656
+ } catch (err) {
657
+ const message = err && err.message ? err.message : err;
658
+ throw new Error(
659
+ `Failed to compile content/works/_layout.mdx: ${message}`
660
+ );
673
661
  }
674
662
 
675
663
  for (let ci = 0; ci < chunks; ci++) {
@@ -808,20 +796,8 @@ async function buildIiifCollectionPages(CONFIG) {
808
796
  href = withBase(href);
809
797
  return React.createElement("a", { href, ...rest }, props.children);
810
798
  };
811
- // Map exported UI components into MDX, with sensible aliases/fallbacks
799
+ // Map exported UI components into MDX and add anchor helper
812
800
  const compMap = { ...components, a: Anchor };
813
- if (!compMap.SearchPanel && compMap.CommandPalette) {
814
- compMap.SearchPanel = compMap.CommandPalette;
815
- }
816
- if (!components.HelloWorld) {
817
- components.HelloWorld = components.Fallback
818
- ? (props) =>
819
- React.createElement(components.Fallback, {
820
- name: "HelloWorld",
821
- ...props,
822
- })
823
- : () => null;
824
- }
825
801
  let MDXProvider = null;
826
802
  try {
827
803
  const mod = await import("@mdx-js/react");
@@ -852,13 +828,6 @@ async function buildIiifCollectionPages(CONFIG) {
852
828
  React.createElement(app.Head)
853
829
  )
854
830
  : "";
855
- const cssRel = path
856
- .relative(
857
- path.dirname(outPath),
858
- path.join(OUT_DIR, "styles", "styles.css")
859
- )
860
- .split(path.sep)
861
- .join("/");
862
831
  const needsHydrateViewer = body.includes("data-canopy-viewer");
863
832
  const needsRelated = body.includes("data-canopy-related-items");
864
833
  const needsHero = body.includes("data-canopy-hero");
@@ -966,7 +935,7 @@ async function buildIiifCollectionPages(CONFIG) {
966
935
  let html = htmlShell({
967
936
  title,
968
937
  body: pageBody,
969
- cssHref: cssRel || "styles/styles.css",
938
+ cssHref: null,
970
939
  scriptHref: jsRel,
971
940
  headExtra: vendorTag + headExtra,
972
941
  });
package/lib/build/mdx.js CHANGED
@@ -101,7 +101,7 @@ async function loadUiComponents() {
101
101
  }
102
102
  }
103
103
  if (!mod) {
104
- // Try package subpath as a secondary resolution path (not a UI component fallback)
104
+ // Try package subpath as a secondary resolution path to avoid export-map issues
105
105
  try {
106
106
  mod = await import('@canopy-iiif/app/ui/server');
107
107
  } catch (e2) {
@@ -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'];
115
+ const required = ['SearchPanel', 'CommandPalette', '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(', '));
@@ -128,41 +128,6 @@ async function loadUiComponents() {
128
128
  Slider: !!comp.Slider,
129
129
  }); } catch(_){}
130
130
  }
131
- // No stub injection beyond this point; UI package must supply these.
132
- // Ensure a minimal SSR Hero exists
133
- if (!comp.Hero) {
134
- comp.Hero = function SimpleHero({ height = 360, item, className = '', style = {}, ...rest }){
135
- const h = typeof height === 'number' ? `${height}px` : String(height || '').trim() || '360px';
136
- const base = { position: 'relative', width: '100%', height: h, overflow: 'hidden', backgroundColor: 'var(--color-gray-muted)', ...style };
137
- const title = (item && item.title) || '';
138
- const href = (item && item.href) || '#';
139
- const thumbnail = (item && item.thumbnail) || '';
140
- return React.createElement('div', { className: ['canopy-hero', className].filter(Boolean).join(' '), style: base, ...rest },
141
- thumbnail ? React.createElement('img', { src: thumbnail, alt: '', 'aria-hidden': 'true', style: { position:'absolute', inset:0, width:'100%', height:'100%', objectFit:'cover', objectPosition:'center', filter:'none' } }) : null,
142
- React.createElement('div', { className:'canopy-hero-overlay', style: { position:'absolute', left:0, right:0, bottom:0, padding:'1rem', background:'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 55%, rgba(0,0,0,0.65) 100%)', color:'white' } },
143
- React.createElement('h3', { style: { margin:0, fontSize:'1.5rem', fontWeight:600, lineHeight:1.2, textShadow:'0 1px 3px rgba(0,0,0,0.6)' } },
144
- React.createElement('a', { href, style:{ color:'inherit', textDecoration:'none' }, className:'canopy-hero-link' }, title)
145
- )
146
- )
147
- );
148
- };
149
- }
150
- // Provide a minimal SSR FeaturedHero fallback if missing
151
- if (!comp.FeaturedHero) {
152
- try {
153
- const helpers = require('../components/featured');
154
- comp.FeaturedHero = function FeaturedHero(props) {
155
- try {
156
- const list = helpers && helpers.readFeaturedFromCacheSync ? helpers.readFeaturedFromCacheSync() : [];
157
- if (!Array.isArray(list) || list.length === 0) return null;
158
- const index = (props && typeof props.index === 'number') ? Math.max(0, Math.min(list.length - 1, Math.floor(props.index))) : null;
159
- const pick = (index != null) ? index : ((props && (props.random === true || props.random === 'true')) ? Math.floor(Math.random() * list.length) : 0);
160
- const item = list[pick] || list[0];
161
- return React.createElement(comp.Hero, { ...props, item });
162
- } catch (_) { return null; }
163
- };
164
- } catch (_) { /* ignore */ }
165
- }
166
131
  UI_COMPONENTS = comp;
167
132
  UI_COMPONENTS_PATH = currentPath;
168
133
  UI_COMPONENTS_MTIME = currentMtime;
@@ -174,8 +139,11 @@ async function loadUiComponents() {
174
139
  }
175
140
 
176
141
  function extractTitle(mdxSource) {
177
- const { content } = parseFrontmatter(String(mdxSource || ""));
178
- const m = content.match(/^\s*#\s+(.+)\s*$/m);
142
+ const { data, content } = parseFrontmatter(String(mdxSource || ""));
143
+ if (data && typeof data.title === "string" && data.title.trim()) {
144
+ return data.title.trim();
145
+ }
146
+ const m = content.match(/^\s*#{1,6}\s+(.+?)\s*$/m);
179
147
  return m ? m[1].trim() : "Untitled";
180
148
  }
181
149
 
@@ -265,26 +233,9 @@ async function loadAppWrapper() {
265
233
  ok = false;
266
234
  }
267
235
  if (!ok) {
268
- // If default export swallowed children, try to recover using __MDXLayout
269
- if (!App && mod.__MDXLayout) {
270
- App = mod.__MDXLayout;
271
- }
272
- // Fallback to pass-through wrapper to avoid blocking builds
273
- if (!App) {
274
- App = function PassThrough(props) {
275
- return React.createElement(React.Fragment, null, props.children);
276
- };
277
- }
278
- try {
279
- require("./log").log(
280
- "! Warning: content/_app.mdx did not clearly render {children}; proceeding with best-effort wrapper\n",
281
- "yellow"
282
- );
283
- } catch (_) {
284
- console.warn(
285
- "Warning: content/_app.mdx did not clearly render {children}; proceeding."
286
- );
287
- }
236
+ throw new Error(
237
+ "content/_app.mdx must render {children}. Update the layout so downstream pages receive their content."
238
+ );
288
239
  }
289
240
  APP_WRAPPER = { App, Head };
290
241
  return APP_WRAPPER;
@@ -390,7 +341,7 @@ async function ensureClientRuntime() {
390
341
  esbuild = require("esbuild");
391
342
  } catch (_) {}
392
343
  }
393
- if (!esbuild) return;
344
+ if (!esbuild) throw new Error('Viewer runtime bundling requires esbuild. Install dependencies before building.');
394
345
  ensureDirSync(OUT_DIR);
395
346
  const scriptsDir = path.join(OUT_DIR, 'scripts');
396
347
  ensureDirSync(scriptsDir);
@@ -509,6 +460,9 @@ async function ensureClientRuntime() {
509
460
  async function ensureFacetsRuntime() {
510
461
  let esbuild = null;
511
462
  try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
463
+ if (!esbuild) {
464
+ throw new Error('RelatedItems runtime bundling requires esbuild. Install dependencies before building.');
465
+ }
512
466
  ensureDirSync(OUT_DIR);
513
467
  const scriptsDir = path.join(OUT_DIR, 'scripts');
514
468
  ensureDirSync(scriptsDir);
@@ -589,18 +543,30 @@ async function ensureFacetsRuntime() {
589
543
  });
590
544
  `;
591
545
  const shim = { name: 'facets-vanilla', setup(){} };
592
- if (esbuild) {
593
- try {
594
- await esbuild.build({ stdin: { contents: entry, resolveDir: process.cwd(), sourcefile: 'canopy-facets-entry.js', loader: 'js' }, outfile: outFile, platform: 'browser', format: 'iife', bundle: true, sourcemap: false, target: ['es2018'], logLevel: 'silent', minify: true, plugins: [shim] });
595
- } catch(e){ try{ console.error('RelatedItems: bundle error:', e && e.message ? e.message : e); }catch(_){ }
596
- // Fallback: write the entry script directly so the file exists
597
- try { fs.writeFileSync(outFile, entry, 'utf8'); } catch(_){}
598
- return; }
599
- try { const { logLine } = require('./log'); let size=0; try{ const st = fs.statSync(outFile); size = st && st.size || 0; }catch(_){} const kb = size ? ` (${(size/1024).toFixed(1)} KB)` : ''; const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/'); logLine(`✓ Wrote ${rel}${kb}`, 'cyan'); } catch(_){}
600
- } else {
601
- // No esbuild: write a non-bundled version (no imports used)
602
- try { fs.writeFileSync(outFile, entry, 'utf8'); } catch(_){}
546
+ try {
547
+ await esbuild.build({
548
+ stdin: { contents: entry, resolveDir: process.cwd(), sourcefile: 'canopy-facets-entry.js', loader: 'js' },
549
+ outfile: outFile,
550
+ platform: 'browser',
551
+ format: 'iife',
552
+ bundle: true,
553
+ sourcemap: false,
554
+ target: ['es2018'],
555
+ logLevel: 'silent',
556
+ minify: true,
557
+ plugins: [shim]
558
+ });
559
+ } catch (e) {
560
+ const message = e && e.message ? e.message : e;
561
+ throw new Error(`RelatedItems runtime build failed: ${message}`);
603
562
  }
563
+ try {
564
+ const { logLine } = require('./log');
565
+ let size = 0; try { const st = fs.statSync(outFile); size = st && st.size || 0; } catch (_) {}
566
+ const kb = size ? ` (${(size/1024).toFixed(1)} KB)` : '';
567
+ const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
568
+ logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
569
+ } catch (_) {}
604
570
  }
605
571
 
606
572
  // Bundle a separate client runtime for the Clover Slider to keep payloads split.
@@ -611,7 +577,7 @@ async function ensureSliderRuntime() {
611
577
  } catch (_) {
612
578
  try { esbuild = require("esbuild"); } catch (_) {}
613
579
  }
614
- if (!esbuild) return;
580
+ if (!esbuild) throw new Error('Slider runtime bundling requires esbuild. Install dependencies before building.');
615
581
  ensureDirSync(OUT_DIR);
616
582
  const scriptsDir = path.join(OUT_DIR, 'scripts');
617
583
  ensureDirSync(scriptsDir);
@@ -735,8 +701,8 @@ async function ensureSliderRuntime() {
735
701
  plugins: [plugin],
736
702
  });
737
703
  } catch (e) {
738
- try { console.error('Slider: bundle error:', e && e.message ? e.message : e); } catch (_) {}
739
- return;
704
+ const message = e && e.message ? e.message : e;
705
+ throw new Error(`Slider runtime build failed: ${message}`);
740
706
  }
741
707
  try {
742
708
  const { logLine } = require('./log');
@@ -757,7 +723,7 @@ async function ensureReactGlobals() {
757
723
  esbuild = require("esbuild");
758
724
  } catch (_) {}
759
725
  }
760
- if (!esbuild) return;
726
+ if (!esbuild) throw new Error('React globals bundling requires esbuild. Install dependencies before building.');
761
727
  const { path } = require("../common");
762
728
  ensureDirSync(OUT_DIR);
763
729
  const scriptsDir = path.join(OUT_DIR, "scripts");
@@ -792,7 +758,7 @@ async function ensureReactGlobals() {
792
758
  async function ensureHeroRuntime() {
793
759
  let esbuild = null;
794
760
  try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
795
- if (!esbuild) return;
761
+ if (!esbuild) throw new Error('Hero runtime bundling requires esbuild. Install dependencies before building.');
796
762
  const { path } = require("../common");
797
763
  ensureDirSync(OUT_DIR);
798
764
  const scriptsDir = path.join(OUT_DIR, 'scripts');
@@ -45,10 +45,6 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
45
45
  const source = await fsp.readFile(filePath, 'utf8');
46
46
  const title = mdx.extractTitle(source);
47
47
  const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, extraProps);
48
- const cssRel = path
49
- .relative(path.dirname(outPath), path.join(OUT_DIR, 'styles', 'styles.css'))
50
- .split(path.sep)
51
- .join('/');
52
48
  const needsHydrateViewer = body.includes('data-canopy-viewer');
53
49
  const needsHydrateSlider = body.includes('data-canopy-slider');
54
50
  const needsCommand = true; // command runtime is global
@@ -96,7 +92,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
96
92
  if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
97
93
  if (commandRel && jsRel !== commandRel) extraScripts.push(`<script defer src="${commandRel}"></script>`);
98
94
  if (extraScripts.length) headExtra = extraScripts.join('') + headExtra;
99
- const html = htmlShell({ title, body, cssHref: cssRel || 'styles.css', scriptHref: jsRel, headExtra: vendorTag + headExtra });
95
+ const html = htmlShell({ title, body, cssHref: null, scriptHref: jsRel, headExtra: vendorTag + headExtra });
100
96
  const { applyBaseToHtml } = require('../common');
101
97
  return applyBaseToHtml(html);
102
98
  }
@@ -1,12 +1,10 @@
1
-
2
1
  const { logLine } = require('./log');
3
- const { fs, fsp, path, OUT_DIR, ensureDirSync } = require('../common');
2
+ const { fs, path, OUT_DIR, ensureDirSync } = require('../common');
4
3
 
5
4
  async function prepareAllRuntimes() {
6
5
  const mdx = require('./mdx');
7
6
  try { await mdx.ensureClientRuntime(); } catch (_) {}
8
7
  try { if (typeof mdx.ensureSliderRuntime === 'function') await mdx.ensureSliderRuntime(); } catch (_) {}
9
- // Optional: Hero runtime is SSR-only by default; enable explicitly to avoid bundling Node deps in browser
10
8
  try {
11
9
  if (process.env.CANOPY_ENABLE_HERO_RUNTIME === '1' || process.env.CANOPY_ENABLE_HERO_RUNTIME === 'true') {
12
10
  if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime();
@@ -14,121 +12,60 @@ async function prepareAllRuntimes() {
14
12
  } catch (_) {}
15
13
  try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
16
14
  try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
17
- try { await ensureCommandFallback(); } catch (_) {}
15
+ await prepareCommandRuntime();
18
16
  try { logLine('✓ Prepared client hydration runtimes', 'cyan', { dim: true }); } catch (_) {}
19
17
  }
20
18
 
21
- module.exports = { prepareAllRuntimes };
19
+ async function resolveEsbuild() {
20
+ try { return require('../../ui/node_modules/esbuild'); } catch (_) {
21
+ try { return require('esbuild'); } catch (_) {
22
+ return null;
23
+ }
24
+ }
25
+ }
22
26
 
23
- async function ensureCommandFallback() {
24
- const cmdOut = path.join(OUT_DIR, 'scripts', 'canopy-command.js');
25
- ensureDirSync(path.dirname(cmdOut));
26
- const fallback = `
27
- (function(){
28
- function ready(fn){ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn(); }
29
- function parseProps(el){ try{ const s = el.querySelector('script[type="application/json"]'); if(s) return JSON.parse(s.textContent||'{}'); }catch(_){ } return {}; }
30
- function norm(s){ try{ return String(s||'').toLowerCase(); }catch(_){ return ''; } }
31
- function withBase(href){
32
- try{
33
- var raw = href == null ? '' : String(href);
34
- raw = raw.trim();
35
- if(!raw) return raw;
36
- var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : '';
37
- if(!bp){
38
- if(/^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.indexOf('//') === 0 || raw.charAt(0)==='#' || raw.charAt(0)==='?') return raw;
39
- var cleaned = raw.replace(/^\\/+/, '');
40
- while(cleaned.indexOf('./') === 0) cleaned = cleaned.slice(2);
41
- while(cleaned.indexOf('../') === 0) cleaned = cleaned.slice(3);
42
- if(!cleaned) return '/';
43
- return '/' + cleaned;
44
- }
45
- if(/^https?:/i.test(raw)) return raw;
46
- var clean = raw.replace(/^\\/+/, '');
47
- return (bp.endsWith('/') ? bp.slice(0,-1) : bp) + '/' + clean;
48
- } catch(_){ return href; }
49
- }
50
- function rootBase(){ try { var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : ''; return bp && bp.endsWith('/') ? bp.slice(0,-1) : bp; } catch(_) { return ''; } }
51
- function isOnSearchPage(){ try{ var base=rootBase(); var p=String(location.pathname||''); if(base && p.startsWith(base)) p=p.slice(base.length); if(p.endsWith('/')) p=p.slice(0,-1); return p==='/search'; }catch(_){ return false; } }
52
- async function loadRecords(){ try{ var v=''; try{ var m = await fetch(rootBase() + '/api/index.json').then(function(r){return r&&r.ok?r.json():null;}).catch(function(){return null;}); v=(m&&m.version)||''; }catch(_){} var res = await fetch(rootBase() + '/api/search-index.json' + (v?('?v='+encodeURIComponent(v)):'')).catch(function(){return null;}); var j = res && res.ok ? await res.json().catch(function(){return[];}) : []; return Array.isArray(j) ? j : (j && j.records) || []; } catch(_){ return []; } }
53
- ready(async function(){ var host=document.querySelector('[data-canopy-command]'); if(!host) return; var cfg=parseProps(host)||{}; var maxResults = Number(cfg.maxResults||8)||8; var groupOrder = Array.isArray(cfg.groupOrder)?cfg.groupOrder:['work','page']; var onSearchPage = isOnSearchPage();
54
- var panel = (function(){ try{ return host.querySelector('[data-canopy-command-panel]') || null; }catch(_){ return null; } })();
55
- if(!panel) return; // no SSR panel present; do nothing
56
- try{ var rel=host.querySelector('.relative'); if(rel && !onSearchPage) rel.setAttribute('data-canopy-panel-auto','1'); }catch(_){}
57
- if(onSearchPage){ panel.style.display='none'; }
58
- var list=panel.querySelector('#cplist');
59
- var input = (function(){ try{ return host.querySelector('[data-canopy-command-input]') || null; }catch(_){ return null; } })();
60
- if(!input) return; // require SSR input; no dynamic creation
61
- // Populate from ?q= URL param if present
62
- try {
63
- var sp = new URLSearchParams(location.search || '');
64
- var qp = sp.get('q');
65
- if (qp) input.value = qp;
66
- } catch(_) {}
67
- // Do not inject legacy trigger buttons or inputs
68
- var records = await loadRecords(); function render(items){ list.innerHTML=''; if(!items.length){ panel.style.display='none'; return; } var groups=new Map(); items.forEach(function(r){ var t=String(r.type||'page'); if(!groups.has(t)) groups.set(t, []); groups.get(t).push(r); }); function gl(t){ if(t==='work') return 'Works'; if(t==='page') return 'Pages'; return t.charAt(0).toUpperCase()+t.slice(1);} var ordered=[].concat(groupOrder.filter(function(t){return groups.has(t);})).concat(Array.from(groups.keys()).filter(function(t){return groupOrder.indexOf(t)===-1;})); ordered.forEach(function(t){ var hdr=document.createElement('div'); hdr.textContent=gl(t); hdr.style.cssText='padding:6px 12px;font-weight:600;color:#374151'; list.appendChild(hdr); groups.get(t).forEach(function(r){ var it=document.createElement('div'); it.setAttribute('data-canopy-item',''); it.tabIndex=0; it.style.cssText='display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;outline:none;'; var thumb=(String(r.type||'')==='work' && r.thumbnail)?r.thumbnail:''; if(thumb){ var img=document.createElement('img'); img.src=thumb; img.alt=''; img.style.cssText='width:40px;height:40px;object-fit:cover;border-radius:4px'; it.appendChild(img);} var span=document.createElement('span'); span.textContent=r.title||r.href; it.appendChild(span); it.onmouseenter=function(){ it.style.background='#f8fafc'; }; it.onmouseleave=function(){ it.style.background='transparent'; }; it.onfocus=function(){ it.style.background='#eef2ff'; try{ it.scrollIntoView({ block: 'nearest' }); }catch(_){} }; it.onblur=function(){ it.style.background='transparent'; }; it.onclick=function(){ try{ window.location.href = withBase(String(r.href||'')); }catch(_){} panel.style.display='none'; }; list.appendChild(it); }); }); }
69
- function getItems(){ try{ return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]')); }catch(_){ return []; } }
70
- function filterAndShow(q){ try{ var qq=norm(q); if(!qq){ try{ panel.style.display='block'; list.innerHTML=''; }catch(_){} return; } var out=[]; for(var i=0;i<records.length;i++){ var r=records[i]; var title=String((r&&r.title)||''); if(!title) continue; if(norm(title).indexOf(qq)!==-1) out.push(r); if(out.length>=maxResults) break; } render(out); }catch(_){} }
71
- input.addEventListener('input', function(){ if(onSearchPage){ try{ var ev = new CustomEvent('canopy:search:setQuery', { detail: { query: (input.value||'') } }); window.dispatchEvent(ev); }catch(_){ } return; } filterAndShow(input.value||''); });
72
- // Keyboard navigation: ArrowDown/ArrowUp to move through items; Enter to select
73
- input.addEventListener('keydown', function(e){
74
- if(e.key==='ArrowDown'){ e.preventDefault(); try{ var items=getItems(); if(items.length){ panel.style.display='block'; items[0].focus(); } }catch(_){} }
75
- else if(e.key==='ArrowUp'){ e.preventDefault(); try{ var items2=getItems(); if(items2.length){ panel.style.display='block'; items2[items2.length-1].focus(); } }catch(_){} }
76
- });
77
- list.addEventListener('keydown', function(e){
78
- var cur = e.target && e.target.closest && e.target.closest('[data-canopy-item]');
79
- if(!cur) return;
80
- if(e.key==='ArrowDown'){
81
- e.preventDefault();
82
- try{ var arr=getItems(); var i=arr.indexOf(cur); var nxt=arr[Math.min(arr.length-1, i+1)]||cur; nxt.focus(); }catch(_){}
83
- } else if(e.key==='ArrowUp'){
84
- e.preventDefault();
85
- try{ var arr2=getItems(); var i2=arr2.indexOf(cur); if(i2<=0){ input && input.focus && input.focus(); } else { var prv=arr2[i2-1]; (prv||cur).focus(); } }catch(_){}
86
- } else if(e.key==='Enter'){
87
- e.preventDefault(); try{ cur.click(); }catch(_){}
88
- } else if(e.key==='Escape'){
89
- panel.style.display='none'; try{ input && input.focus && input.focus(); }catch(_){}
90
- }
91
- });
92
- document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ panel.style.display='none'; }});
93
- document.addEventListener('mousedown', function(e){ try{ if(!panel.contains(e.target) && !host.contains(e.target)){ panel.style.display='none'; } }catch(_){} });
94
- // Hotkey support (e.g., mod+k)
95
- document.addEventListener('keydown', function(e){
96
- try {
97
- var want = String((cfg && cfg.hotkey) || '').toLowerCase();
98
- if (!want) return;
99
- var isMod = e.metaKey || e.ctrlKey;
100
- if ((want === 'mod+k' || want === 'cmd+k' || want === 'ctrl+k') && isMod && (e.key === 'k' || e.key === 'K')) {
101
- e.preventDefault();
102
- if(onSearchPage){ try{ var ev2 = new CustomEvent('canopy:search:setQuery', { detail: { hotkey: true } }); window.dispatchEvent(ev2); }catch(_){ } return; }
103
- panel.style.display='block';
104
- (input && input.focus && input.focus());
105
- filterAndShow(input && input.value || '');
106
- }
107
- } catch(_) { }
108
- });
109
- function openPanel(){ if(onSearchPage){ try{ var ev3 = new CustomEvent('canopy:search:setQuery', { detail: {} }); window.dispatchEvent(ev3); }catch(_){ } return; } panel.style.display='block'; (input && input.focus && input.focus()); filterAndShow(input && input.value || ''); }
110
- host.addEventListener('click', function(e){ var trg=e.target && e.target.closest && e.target.closest('[data-canopy-command-trigger]'); if(trg){ e.preventDefault(); openPanel(); }});
111
- try{ var teaser2 = host.querySelector('[data-canopy-command-input]'); if(teaser2){ teaser2.addEventListener('focus', function(){ openPanel(); }); } }catch(_){}
112
- });
113
- })();
114
- `;
115
- await fsp.writeFile(cmdOut, fallback, 'utf8');
116
- try { logLine(`✓ Wrote ${path.relative(process.cwd(), cmdOut)} (fallback)`, 'cyan'); } catch (_) {}
27
+ async function prepareCommandRuntime() {
28
+ const esbuild = await resolveEsbuild();
29
+ if (!esbuild) throw new Error('Command runtime bundling requires esbuild. Install dependencies before building.');
30
+ ensureDirSync(OUT_DIR);
31
+ const scriptsDir = path.join(OUT_DIR, 'scripts');
32
+ ensureDirSync(scriptsDir);
33
+ const entry = path.join(__dirname, '..', 'search', 'command-runtime.js');
34
+ const outFile = path.join(scriptsDir, 'canopy-command.js');
35
+ await esbuild.build({
36
+ entryPoints: [entry],
37
+ outfile: outFile,
38
+ platform: 'browser',
39
+ format: 'iife',
40
+ bundle: true,
41
+ sourcemap: false,
42
+ target: ['es2018'],
43
+ logLevel: 'silent',
44
+ minify: true,
45
+ });
46
+ try {
47
+ let size = 0;
48
+ try { const st = fs.statSync(outFile); size = st.size || 0; } catch (_) {}
49
+ const kb = size ? ` (${(size / 1024).toFixed(1)} KB)` : '';
50
+ const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
51
+ logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
52
+ } catch (_) {}
117
53
  }
118
54
 
119
55
  async function prepareSearchRuntime(timeoutMs = 10000, label = '') {
120
56
  const search = require('../search/search');
121
- try { logLine(`• Writing search runtime${label ? ' ('+label+')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
57
+ try { logLine(`• Writing search runtime${label ? ' (' + label + ')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
58
+
122
59
  let timedOut = false;
123
60
  await Promise.race([
124
61
  search.ensureSearchRuntime(),
125
62
  new Promise((_, reject) => setTimeout(() => { timedOut = true; reject(new Error('timeout')); }, Number(timeoutMs)))
126
63
  ]).catch(() => {
127
- try { console.warn(`Search: Bundling runtime timed out${label ? ' ('+label+')' : ''}, skipping`); } catch (_) {}
64
+ try { console.warn(`Search: Bundling runtime timed out${label ? ' (' + label + ')' : ''}, skipping`); } catch (_) {}
128
65
  });
129
66
  if (timedOut) {
130
- try { logLine(`! Search runtime not bundled${label ? ' ('+label+')' : ''}\n`, 'yellow'); } catch (_) {}
67
+ try { logLine(`! Search runtime not bundled${label ? ' (' + label + ')' : ''}\n`, 'yellow'); } catch (_) {}
131
68
  }
132
69
  }
133
70
 
134
- module.exports = { prepareAllRuntimes, ensureCommandFallback, prepareSearchRuntime };
71
+ module.exports = { prepareAllRuntimes, prepareCommandRuntime, prepareSearchRuntime };
@@ -165,10 +165,6 @@ async function writeFacetsSearchApi() {
165
165
  await fsp.writeFile(dest, JSON.stringify(data, null, 2), 'utf8');
166
166
  }
167
167
 
168
- module.exports = { buildFacetsForWorks, writeFacetCollections, writeFacetsSearchApi };
169
-
170
-
171
-
172
168
  async function collectMdxPageRecords() {
173
169
  const { fs, fsp, path, CONTENT_DIR, rootRelativeHref } = require('../common');
174
170
  const mdx = require('./mdx');
@@ -183,7 +179,8 @@ async function collectMdxPageRecords() {
183
179
  const base = path.basename(p).toLowerCase();
184
180
  const src = await fsp.readFile(p, 'utf8');
185
181
  const fm = mdx.parseFrontmatter(src);
186
- const title = mdx.extractTitle(src);
182
+ const titleRaw = mdx.extractTitle(src);
183
+ const title = typeof titleRaw === 'string' ? titleRaw.trim() : '';
187
184
  const rel = path.relative(CONTENT_DIR, p).replace(/\.mdx$/i, '.html');
188
185
  if (base !== 'sitemap.mdx') {
189
186
  const href = rootRelativeHref(rel.split(path.sep).join('/'));
@@ -70,21 +70,15 @@ async function ensureStyles() {
70
70
  }
71
71
 
72
72
  function resolveTailwindCli() {
73
- try {
74
- const cliJs = require.resolve("tailwindcss/lib/cli.js");
75
- return { cmd: process.execPath, args: [cliJs] };
76
- } catch (_) {}
77
- try {
78
- const localBin = path.join(
79
- process.cwd(),
80
- "node_modules",
81
- ".bin",
82
- process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
83
- );
84
- if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
85
- } catch (_) {}
86
- return null;
87
- }
73
+ const localBin = path.join(
74
+ process.cwd(),
75
+ "node_modules",
76
+ ".bin",
77
+ process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
78
+ );
79
+ if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
80
+ return { cmd: 'tailwindcss', args: [] };
81
+ }
88
82
  function buildTailwindCli({ input, output, config, minify = true }) {
89
83
  try {
90
84
  const cli = resolveTailwindCli();
package/lib/common.js CHANGED
@@ -26,13 +26,13 @@ function readYamlConfigBaseUrl() {
26
26
  // Priority:
27
27
  // 1) CANOPY_BASE_URL env
28
28
  // 2) canopy.yml → site.baseUrl
29
- // 3) dev server default http://localhost:PORT (PORT env or 3000)
29
+ // 3) dev server default http://localhost:PORT (PORT env or 5001)
30
30
  const BASE_ORIGIN = (() => {
31
31
  const env = String(process.env.CANOPY_BASE_URL || '').trim();
32
32
  if (env) return env.replace(/\/$/, '');
33
33
  const cfg = readYamlConfigBaseUrl();
34
34
  if (cfg) return cfg.replace(/\/$/, '');
35
- const port = Number(process.env.PORT || 3000);
35
+ const port = Number(process.env.PORT || 5001);
36
36
  return `http://localhost:${port}`;
37
37
  })();
38
38
 
@@ -50,7 +50,8 @@ async function cleanDir(dir) {
50
50
  function htmlShell({ title, body, cssHref, scriptHref, headExtra }) {
51
51
  const scriptTag = scriptHref ? `<script defer src="${scriptHref}"></script>` : '';
52
52
  const extra = headExtra ? String(headExtra) : '';
53
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>${title}</title>${extra}<link rel="stylesheet" href="${cssHref}">${scriptTag}</head><body>${body}</body></html>`;
53
+ const cssTag = cssHref ? `<link rel="stylesheet" href="${cssHref}">` : '';
54
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>${title}</title>${extra}${cssTag}${scriptTag}</head><body>${body}</body></html>`;
54
55
  }
55
56
 
56
57
  function withBase(href) {
@@ -114,11 +115,18 @@ function absoluteUrl(p) {
114
115
  function applyBaseToHtml(html) {
115
116
  if (!BASE_PATH) return html;
116
117
  try {
117
- let out = String(html || '');
118
- // Avoid protocol-relative (//example.com) by using a negative lookahead
119
- out = out.replace(/(href|src)=(\")\/(?!\/)/g, `$1=$2${BASE_PATH}/`);
120
- out = out.replace(/(href|src)=(\')\/(?!\/)/g, `$1=$2${BASE_PATH}/`);
121
- return out;
118
+ const out = String(html || '');
119
+ const normalizedBase = BASE_PATH.startsWith('/')
120
+ ? BASE_PATH.replace(/\/$/, '')
121
+ : `/${BASE_PATH.replace(/\/$/, '')}`;
122
+ if (!normalizedBase || normalizedBase === '/') return out;
123
+ const pattern = /(href|src)=(['"])(\/(?!\/)[^'"\s]*)\2/g;
124
+ return out.replace(pattern, (match, attr, quote, path) => {
125
+ if (path === normalizedBase || path.startsWith(`${normalizedBase}/`)) {
126
+ return match;
127
+ }
128
+ return `${attr}=${quote}${normalizedBase}${path}${quote}`;
129
+ });
122
130
  } catch (_) {
123
131
  return html;
124
132
  }