@canopy-iiif/app 1.8.14 → 1.9.0

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.
@@ -60,6 +60,45 @@ function rootBase() {
60
60
  return getBasePath();
61
61
  }
62
62
 
63
+ function documentLocale() {
64
+ try {
65
+ if (document && document.documentElement && document.documentElement.lang) {
66
+ const lang = String(document.documentElement.lang).trim();
67
+ if (lang) return lang;
68
+ }
69
+ } catch (_) {}
70
+ return '';
71
+ }
72
+
73
+ function resolveLocaleHref(record, fallbackHref) {
74
+ const routes =
75
+ record && record.routes && typeof record.routes === 'object'
76
+ ? record.routes
77
+ : null;
78
+ if (!routes) return fallbackHref;
79
+ const current = documentLocale();
80
+ const candidates = [];
81
+ if (current) {
82
+ candidates.push(current);
83
+ if (current.includes('-')) {
84
+ const base = current.split('-')[0];
85
+ if (base && base !== current) candidates.push(base);
86
+ }
87
+ }
88
+ if (record && typeof record.locale === 'string') {
89
+ const recLocale = record.locale.trim();
90
+ if (recLocale && !candidates.includes(recLocale)) candidates.push(recLocale);
91
+ }
92
+ for (const candidate of candidates) {
93
+ if (!candidate) continue;
94
+ if (routes[candidate]) return routes[candidate];
95
+ const lower = candidate.toLowerCase();
96
+ if (lower !== candidate && routes[lower]) return routes[lower];
97
+ }
98
+ const first = Object.values(routes).find((value) => value);
99
+ return first || fallbackHref;
100
+ }
101
+
63
102
  function isOnSearchPage() {
64
103
  try {
65
104
  const base = rootBase();
@@ -163,7 +202,13 @@ async function loadRecords() {
163
202
  const display = key ? displayMap.get(key) : null;
164
203
  const merged = { ...(display || {}), ...(rec || {}) };
165
204
  if (!merged.id && key) merged.id = key;
166
- if (!merged.href && display && display.href) merged.href = String(display.href);
205
+ const fallbackHref = merged.href || (display && display.href) || '';
206
+ const localeHref = resolveLocaleHref(display || merged, fallbackHref);
207
+ if (localeHref) {
208
+ merged.href = withBase(localeHref);
209
+ } else if (fallbackHref) {
210
+ merged.href = withBase(fallbackHref);
211
+ }
167
212
  if (!Array.isArray(merged.metadata)) {
168
213
  const meta = Array.isArray(rec && rec.metadata) ? rec.metadata : [];
169
214
  merged.metadata = meta;
@@ -12,6 +12,10 @@ const {
12
12
  htmlShell,
13
13
  canopyBodyClassForType,
14
14
  readSearchPageMetadata,
15
+ resolveLocaleFromHref,
16
+ getLocaleRouteEntries,
17
+ getDefaultRoute,
18
+ getDefaultLocaleCode,
15
19
  } = require('../common');
16
20
  const { resolveCanopyConfigPath } = require('../config-path');
17
21
 
@@ -91,6 +95,29 @@ function createSearchMdxPlugin() {
91
95
  };
92
96
  }
93
97
 
98
+ function getSearchRouteEntries() {
99
+ let entries = getLocaleRouteEntries('search');
100
+ if (!entries.length) {
101
+ entries = [
102
+ {
103
+ locale: getDefaultLocaleCode(),
104
+ route: getDefaultRoute('search'),
105
+ isDefault: true,
106
+ },
107
+ ];
108
+ }
109
+ return entries;
110
+ }
111
+
112
+ function resolveSearchOutputRelative(routeValue) {
113
+ const defaultRoute = getDefaultRoute('search') || 'search';
114
+ const trimmed = typeof routeValue === 'string' ? routeValue.trim().replace(/^\/+|\/+$/g, '') : '';
115
+ if (!trimmed || trimmed === defaultRoute) {
116
+ return `${trimmed || defaultRoute}.html`;
117
+ }
118
+ return path.join(trimmed, 'index.html');
119
+ }
120
+
94
121
  async function ensureSearchRuntime() {
95
122
  ensureDirSync(OUT_DIR);
96
123
  let esbuild = null;
@@ -248,18 +275,32 @@ async function ensureSearchRuntime() {
248
275
  }
249
276
 
250
277
  async function buildSearchPage() {
278
+ for (const entry of getSearchRouteEntries()) {
279
+ await buildSearchPageForEntry(entry);
280
+ }
281
+ }
282
+
283
+ async function buildSearchPageForEntry(routeEntry) {
251
284
  try {
252
- const outPath = path.join(OUT_DIR, 'search.html');
285
+ const defaultRoute = getDefaultRoute('search') || 'search';
286
+ const routeBase =
287
+ routeEntry && typeof routeEntry.route === 'string'
288
+ ? routeEntry.route
289
+ : defaultRoute;
290
+ const relativeOutput = resolveSearchOutputRelative(routeBase);
291
+ const outPath = path.join(OUT_DIR, relativeOutput);
253
292
  ensureDirSync(path.dirname(outPath));
254
- // Require author-provided content/search/_layout.mdx; do not fall back to a generated page.
255
293
  const searchLayoutPath = path.join(path.resolve('content'), 'search', '_layout.mdx');
256
- let body = '';
257
- let head = '';
258
294
  if (!require('../common').fs.existsSync(searchLayoutPath)) {
259
295
  throw new Error('Missing required file: content/search/_layout.mdx');
260
296
  }
261
297
  const mdx = require('../build/mdx');
262
- const searchHref = rootRelativeHref('search.html');
298
+ const normalizedRoute = routeBase ? routeBase.replace(/^\/+|\/+$/g, '') : '';
299
+ const fileHref = rootRelativeHref(relativeOutput.split(path.sep).join('/'));
300
+ const prettyHref =
301
+ normalizedRoute && normalizedRoute !== defaultRoute
302
+ ? rootRelativeHref(`${normalizedRoute}/`)
303
+ : fileHref;
263
304
  const searchPageMeta = readSearchPageMetadata() || {};
264
305
  const pageTitle =
265
306
  typeof searchPageMeta.title === 'string' && searchPageMeta.title.trim()
@@ -272,23 +313,30 @@ async function buildSearchPage() {
272
313
  const pageDetails = {
273
314
  title: pageTitle,
274
315
  description: pageDescription,
275
- href: searchHref,
276
- url: searchHref,
316
+ href: prettyHref,
317
+ url: prettyHref,
277
318
  type: 'search',
278
- canonical: searchHref,
319
+ canonical: prettyHref,
279
320
  meta: {
280
321
  title: pageTitle,
281
322
  description: pageDescription,
282
323
  type: 'search',
283
- url: searchHref,
284
- canonical: searchHref,
324
+ url: prettyHref,
325
+ canonical: prettyHref,
285
326
  },
286
327
  };
328
+ const fallbackLocale = resolveLocaleFromHref(prettyHref);
329
+ const pageLocale =
330
+ (routeEntry && routeEntry.locale) ||
331
+ fallbackLocale ||
332
+ getDefaultLocaleCode();
333
+ pageDetails.locale = pageLocale;
334
+ if (pageDetails.meta) pageDetails.meta.locale = pageLocale;
287
335
  const rendered = await mdx.compileMdxFile(searchLayoutPath, outPath, null, {
288
336
  page: pageDetails,
289
337
  });
290
- body = rendered && rendered.body ? rendered.body : '';
291
- head = rendered && rendered.head ? rendered.head : '';
338
+ const body = rendered && rendered.body ? rendered.body : '';
339
+ const head = rendered && rendered.head ? rendered.head : '';
292
340
  if (!body) throw new Error('Search: content/search/_layout.mdx produced empty output');
293
341
  const importMap = '';
294
342
  const jsAbs = path.join(OUT_DIR, 'scripts', 'search.js');
@@ -296,7 +344,6 @@ async function buildSearchPage() {
296
344
  let v = '';
297
345
  try { const st = require('fs').statSync(jsAbs); v = `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
298
346
  jsRel = jsRel + v;
299
- // Include react-globals vendor shim before search.js to provide window.React globals
300
347
  const vendorReactAbs = path.join(OUT_DIR, 'scripts', 'react-globals.js');
301
348
  const vendorFlexAbs = path.join(OUT_DIR, 'scripts', 'flexsearch-globals.js');
302
349
  const vendorSearchFormAbs = path.join(OUT_DIR, 'scripts', 'canopy-search-form.js');
@@ -326,7 +373,7 @@ async function buildSearchPage() {
326
373
  }
327
374
  } catch (_) {}
328
375
  const bodyClass = canopyBodyClassForType('search');
329
- let html = htmlShell({ title: pageTitle, body, cssHref: null, scriptHref: jsRel, headExtra, bodyClass });
376
+ let html = htmlShell({ title: pageTitle, body, cssHref: null, scriptHref: jsRel, headExtra, bodyClass, lang: pageLocale });
330
377
  try { html = require('../common').applyBaseToHtml(html); } catch (_) {}
331
378
  await fsp.writeFile(outPath, html, 'utf8');
332
379
  console.log('Search: Built', path.relative(process.cwd(), outPath));
@@ -395,6 +442,17 @@ function sanitizeRecordForDisplay(r) {
395
442
  const out = { ...base };
396
443
  if (out.metadata) delete out.metadata;
397
444
  if (out.summary) out.summary = toSafeString(out.summary, '');
445
+ const locale = toSafeString(r && r.locale, '').trim();
446
+ if (locale) out.locale = locale;
447
+ if (r && r.routes && typeof r.routes === 'object') {
448
+ const normalizedRoutes = {};
449
+ Object.keys(r.routes).forEach((key) => {
450
+ const routeHref = toSafeString(r.routes[key], '');
451
+ if (!routeHref) return;
452
+ normalizedRoutes[key] = rootRelativeHref(routeHref);
453
+ });
454
+ if (Object.keys(normalizedRoutes).length) out.routes = normalizedRoutes;
455
+ }
398
456
  const summaryMarkdown = toSafeString(
399
457
  (r && r.summaryMarkdown) ||
400
458
  (r && r.searchSummaryMarkdown) ||
@@ -405,7 +463,9 @@ function sanitizeRecordForDisplay(r) {
405
463
  out.summaryMarkdown = summaryMarkdown;
406
464
  }
407
465
  const hrefRaw = toSafeString(r && r.href, '');
408
- out.href = rootRelativeHref(hrefRaw);
466
+ if (hrefRaw) {
467
+ out.href = rootRelativeHref(hrefRaw);
468
+ }
409
469
  const thumbnail = toSafeString(r && r.thumbnail, '');
410
470
  if (thumbnail) out.thumbnail = thumbnail;
411
471
  // Preserve optional thumbnail dimensions for aspect ratio calculations in the UI
@@ -603,4 +663,10 @@ async function ensureResultTemplate() {
603
663
  } catch (_) {}
604
664
  }
605
665
 
606
- module.exports = { ensureSearchRuntime, ensureResultTemplate, buildSearchPage, writeSearchIndex };
666
+ module.exports = {
667
+ ensureSearchRuntime,
668
+ ensureResultTemplate,
669
+ buildSearchPage,
670
+ writeSearchIndex,
671
+ resolveSearchOutputRelative,
672
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.8.14",
3
+ "version": "1.9.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",