@canopy-iiif/app 1.5.15 → 1.5.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/mdx.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  CACHE_DIR,
12
12
  ensureDirSync,
13
13
  withBase,
14
+ readSiteMetadata,
14
15
  } = require("../common");
15
16
  let remarkGfm = null;
16
17
  try {
@@ -1043,10 +1044,12 @@ async function compileMdxFile(filePath, outPath, Layout, extraProps = {}) {
1043
1044
  : contentNode;
1044
1045
  const withApp = React.createElement(app.App, null, withLayout);
1045
1046
  const PageContext = getPageContext();
1047
+ const siteMeta = readSiteMetadata();
1046
1048
  const contextValue = {
1047
1049
  navigation:
1048
1050
  extraProps && extraProps.navigation ? extraProps.navigation : null,
1049
1051
  page: extraProps && extraProps.page ? extraProps.page : null,
1052
+ site: siteMeta ? {...siteMeta} : null,
1050
1053
  };
1051
1054
  const withContext = PageContext
1052
1055
  ? React.createElement(PageContext.Provider, {value: contextValue}, withApp)
package/lib/common.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const fsp = fs.promises;
3
3
  const path = require('path');
4
+ const yaml = require('js-yaml');
4
5
  const { resolveCanopyConfigPath } = require('./config-path');
5
6
 
6
7
  const CONTENT_DIR = path.resolve('content');
@@ -13,6 +14,11 @@ const { readBasePath, withBasePath } = require('./base-path');
13
14
  const BASE_PATH = readBasePath();
14
15
  let cachedAppearance = null;
15
16
  let cachedAccent = null;
17
+ let cachedSiteMetadata = null;
18
+ let cachedSearchPageMetadata = null;
19
+ const DEFAULT_SITE_TITLE = 'Site title';
20
+ const DEFAULT_SEARCH_PAGE_TITLE = 'Search';
21
+ const DEFAULT_SEARCH_PAGE_DESCRIPTION = '';
16
22
 
17
23
  function resolveThemeAppearance() {
18
24
  if (cachedAppearance) return cachedAppearance;
@@ -47,17 +53,69 @@ function resolveThemeAccent() {
47
53
 
48
54
  function readYamlConfigBaseUrl() {
49
55
  try {
50
- const y = require('js-yaml');
51
56
  const p = resolveCanopyConfigPath();
52
57
  if (!fs.existsSync(p)) return '';
53
58
  const raw = fs.readFileSync(p, 'utf8');
54
- const data = y.load(raw) || {};
59
+ const data = yaml.load(raw) || {};
55
60
  const site = data && data.site;
56
61
  const url = site && site.baseUrl ? String(site.baseUrl) : '';
57
62
  return url;
58
63
  } catch (_) { return ''; }
59
64
  }
60
65
 
66
+ function readSiteMetadata() {
67
+ if (cachedSiteMetadata) return cachedSiteMetadata;
68
+ cachedSiteMetadata = { title: DEFAULT_SITE_TITLE };
69
+ try {
70
+ const cfgPath = resolveCanopyConfigPath();
71
+ if (!fs.existsSync(cfgPath)) return cachedSiteMetadata;
72
+ const raw = fs.readFileSync(cfgPath, 'utf8');
73
+ const data = yaml.load(raw) || {};
74
+ const directTitle = data && typeof data.title === 'string' ? data.title.trim() : '';
75
+ const nestedTitle =
76
+ data && data.site && typeof data.site.title === 'string'
77
+ ? data.site.title.trim()
78
+ : '';
79
+ const resolved = directTitle || nestedTitle || DEFAULT_SITE_TITLE;
80
+ cachedSiteMetadata = { title: resolved };
81
+ } catch (_) {}
82
+ return cachedSiteMetadata;
83
+ }
84
+
85
+ function getSiteTitle() {
86
+ const site = readSiteMetadata();
87
+ if (site && typeof site.title === 'string' && site.title.trim()) {
88
+ return site.title.trim();
89
+ }
90
+ return DEFAULT_SITE_TITLE;
91
+ }
92
+
93
+ function readSearchPageMetadata() {
94
+ if (cachedSearchPageMetadata) return cachedSearchPageMetadata;
95
+ cachedSearchPageMetadata = {
96
+ title: DEFAULT_SEARCH_PAGE_TITLE,
97
+ description: DEFAULT_SEARCH_PAGE_DESCRIPTION,
98
+ };
99
+ try {
100
+ const cfgPath = resolveCanopyConfigPath();
101
+ if (!fs.existsSync(cfgPath)) return cachedSearchPageMetadata;
102
+ const raw = fs.readFileSync(cfgPath, 'utf8');
103
+ const data = yaml.load(raw) || {};
104
+ const searchCfg = data && data.search ? data.search : null;
105
+ const pageCfg = searchCfg && searchCfg.page ? searchCfg.page : null;
106
+ const title = pageCfg && typeof pageCfg.title === 'string' ? pageCfg.title.trim() : '';
107
+ const description =
108
+ pageCfg && typeof pageCfg.description === 'string'
109
+ ? pageCfg.description.trim()
110
+ : '';
111
+ cachedSearchPageMetadata = {
112
+ title: title || DEFAULT_SEARCH_PAGE_TITLE,
113
+ description: description || DEFAULT_SEARCH_PAGE_DESCRIPTION,
114
+ };
115
+ } catch (_) {}
116
+ return cachedSearchPageMetadata;
117
+ }
118
+
61
119
  // Determine the absolute site origin (scheme + host[:port])
62
120
  // Priority:
63
121
  // 1) CANOPY_BASE_URL env
@@ -224,4 +282,10 @@ module.exports = {
224
282
  applyBaseToHtml,
225
283
  rootRelativeHref,
226
284
  canopyBodyClassForType,
285
+ readSiteMetadata,
286
+ getSiteTitle,
287
+ DEFAULT_SITE_TITLE,
288
+ readSearchPageMetadata,
289
+ DEFAULT_SEARCH_PAGE_TITLE,
290
+ DEFAULT_SEARCH_PAGE_DESCRIPTION,
227
291
  };
package/lib/head.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const React = require('react');
2
- const { withBase, rootRelativeHref, absoluteUrl } = require('./common');
2
+ const { withBase, rootRelativeHref, absoluteUrl, getSiteTitle } = require('./common');
3
3
  const { getPageContext } = require('./page-context');
4
4
 
5
5
  const DEFAULT_STYLESHEET_PATH = '/styles/styles.css';
@@ -56,10 +56,16 @@ function Meta(props = {}) {
56
56
  page.title ||
57
57
  fallbackTitle;
58
58
  const pageTitle = normalizeText(rawTitle);
59
- const siteTitle = normalizeText(props.siteTitle) || '';
60
- const defaultTitle = siteTitle || 'Canopy IIIF';
59
+ const fallbackSiteTitle = normalizeText(getSiteTitle()) || '';
60
+ const explicitSiteTitle = normalizeText(props.siteTitle) || '';
61
+ const siteTitle = explicitSiteTitle || fallbackSiteTitle;
62
+ const defaultTitle = siteTitle || fallbackSiteTitle || 'Site title';
61
63
  const title = pageTitle ? pageTitle : defaultTitle;
62
- const fullTitle = siteTitle ? (pageTitle ? `${pageTitle} | ${siteTitle}` : siteTitle) : title;
64
+ const fullTitle = siteTitle
65
+ ? pageTitle
66
+ ? `${pageTitle} | ${siteTitle}`
67
+ : siteTitle
68
+ : title;
63
69
  const rawDescription =
64
70
  props.description ||
65
71
  (metaFromPage && metaFromPage.description) ||
@@ -108,6 +114,35 @@ function Meta(props = {}) {
108
114
  if (description) nodes.push(React.createElement('meta', { key: 'twitter-description', name: 'twitter:description', content: description }));
109
115
  if (twitterImage) nodes.push(React.createElement('meta', { key: 'twitter-image', name: 'twitter:image', content: twitterImage }));
110
116
 
117
+ const slug = typeof page.slug === 'string' ? page.slug : '';
118
+ const relPath = typeof page.relativePath === 'string' ? page.relativePath : '';
119
+ const hrefRaw = page && typeof page.href === 'string' ? page.href : '';
120
+ const urlRaw = page && typeof page.url === 'string' ? page.url : '';
121
+ const normalizedHref = hrefRaw ? rootRelativeHref(hrefRaw) : '';
122
+ const normalizedUrl = urlRaw ? rootRelativeHref(urlRaw) : '';
123
+ const isHomepage = !!(
124
+ (slug === '' && page && page.isIndex) ||
125
+ relPath === 'index.mdx' ||
126
+ normalizedHref === '/' ||
127
+ normalizedUrl === '/'
128
+ );
129
+ const siteTitleForJsonLd = siteTitle || fallbackSiteTitle || 'Site title';
130
+ if (isHomepage && siteTitleForJsonLd) {
131
+ const ldPayload = {
132
+ '@context': 'https://schema.org',
133
+ '@type': 'WebSite',
134
+ name: siteTitleForJsonLd,
135
+ url: absoluteUrl('/'),
136
+ };
137
+ nodes.push(
138
+ React.createElement('script', {
139
+ key: 'canopy-website-json-ld',
140
+ type: 'application/ld+json',
141
+ dangerouslySetInnerHTML: { __html: JSON.stringify(ldPayload) },
142
+ })
143
+ );
144
+ }
145
+
111
146
  return React.createElement(React.Fragment, null, nodes);
112
147
  }
113
148
 
@@ -13,7 +13,7 @@ function getGlobalRoot() {
13
13
  function getPageContext() {
14
14
  const root = getGlobalRoot();
15
15
  if (root[CONTEXT_KEY]) return root[CONTEXT_KEY];
16
- const ctx = React.createContext({ navigation: null, page: null });
16
+ const ctx = React.createContext({ navigation: null, page: null, site: null });
17
17
  root[CONTEXT_KEY] = ctx;
18
18
  return ctx;
19
19
  }
@@ -11,6 +11,7 @@ const {
11
11
  OUT_DIR,
12
12
  htmlShell,
13
13
  canopyBodyClassForType,
14
+ readSearchPageMetadata,
14
15
  } = require('../common');
15
16
  const { resolveCanopyConfigPath } = require('../config-path');
16
17
 
@@ -259,14 +260,25 @@ async function buildSearchPage() {
259
260
  }
260
261
  const mdx = require('../build/mdx');
261
262
  const searchHref = rootRelativeHref('search.html');
263
+ const searchPageMeta = readSearchPageMetadata() || {};
264
+ const pageTitle =
265
+ typeof searchPageMeta.title === 'string' && searchPageMeta.title.trim()
266
+ ? searchPageMeta.title.trim()
267
+ : 'Search';
268
+ const pageDescription =
269
+ typeof searchPageMeta.description === 'string'
270
+ ? searchPageMeta.description
271
+ : '';
262
272
  const pageDetails = {
263
- title: 'Search',
273
+ title: pageTitle,
274
+ description: pageDescription,
264
275
  href: searchHref,
265
276
  url: searchHref,
266
277
  type: 'search',
267
278
  canonical: searchHref,
268
279
  meta: {
269
- title: 'Search',
280
+ title: pageTitle,
281
+ description: pageDescription,
270
282
  type: 'search',
271
283
  url: searchHref,
272
284
  canonical: searchHref,
@@ -312,7 +324,7 @@ async function buildSearchPage() {
312
324
  }
313
325
  } catch (_) {}
314
326
  const bodyClass = canopyBodyClassForType('search');
315
- let html = htmlShell({ title: 'Search', body, cssHref: null, scriptHref: jsRel, headExtra, bodyClass });
327
+ let html = htmlShell({ title: pageTitle, body, cssHref: null, scriptHref: jsRel, headExtra, bodyClass });
316
328
  try { html = require('../common').applyBaseToHtml(html); } catch (_) {}
317
329
  await fsp.writeFile(outPath, html, 'utf8');
318
330
  console.log('Search: Built', path.relative(process.cwd(), outPath));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.5.15",
3
+ "version": "1.5.17",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
package/ui/dist/index.mjs CHANGED
@@ -1211,7 +1211,7 @@ function getSharedRoot() {
1211
1211
  function getSafePageContext() {
1212
1212
  const root = getSharedRoot();
1213
1213
  if (root && root[CONTEXT_KEY]) return root[CONTEXT_KEY];
1214
- const ctx = React15.createContext({ navigation: null, page: null });
1214
+ const ctx = React15.createContext({ navigation: null, page: null, site: null });
1215
1215
  if (root) root[CONTEXT_KEY] = ctx;
1216
1216
  return ctx;
1217
1217
  }
@@ -1278,13 +1278,18 @@ function CanopyHeader(props = {}) {
1278
1278
  searchHotkey = "mod+k",
1279
1279
  searchPlaceholder = "Search\u2026",
1280
1280
  brandHref = "/",
1281
- title = "Canopy IIIF",
1281
+ title: titleProp,
1282
1282
  logo: SiteLogo
1283
1283
  } = props;
1284
1284
  const navLinks = ensureArray(navLinksProp);
1285
1285
  const PageContext = getSafePageContext();
1286
1286
  const context = React15.useContext(PageContext);
1287
1287
  const contextNavigation = context && context.navigation ? context.navigation : null;
1288
+ const contextSite = context && context.site ? context.site : null;
1289
+ const contextSiteTitle = contextSite && typeof contextSite.title === "string" ? contextSite.title.trim() : "";
1290
+ const defaultHeaderTitle = contextSiteTitle || "Site title";
1291
+ const normalizedTitleProp = typeof titleProp === "string" ? titleProp.trim() : "";
1292
+ const resolvedTitle = normalizedTitleProp || defaultHeaderTitle;
1288
1293
  const sectionNavigation = contextNavigation && contextNavigation.root ? contextNavigation : null;
1289
1294
  const navigationRoots = contextNavigation && contextNavigation.allRoots ? contextNavigation.allRoots : null;
1290
1295
  const sectionHeading = sectionNavigation && sectionNavigation.title || (sectionNavigation && sectionNavigation.root ? sectionNavigation.root.title : "");
@@ -1313,7 +1318,7 @@ function CanopyHeader(props = {}) {
1313
1318
  /* @__PURE__ */ React15.createElement("div", { className: "canopy-header__brand" }, /* @__PURE__ */ React15.createElement(
1314
1319
  CanopyBrand,
1315
1320
  {
1316
- label: title,
1321
+ label: resolvedTitle,
1317
1322
  href: brandHref,
1318
1323
  className: "canopy-header__brand-link",
1319
1324
  Logo: SiteLogo
@@ -1408,7 +1413,7 @@ function CanopyHeader(props = {}) {
1408
1413
  id: "canopy-modal-nav",
1409
1414
  variant: "nav",
1410
1415
  labelledBy: "canopy-modal-nav-label",
1411
- label: title,
1416
+ label: resolvedTitle,
1412
1417
  logo: SiteLogo,
1413
1418
  href: brandHref,
1414
1419
  closeLabel: "Close navigation",
@@ -1503,7 +1508,7 @@ function CanopyHeader(props = {}) {
1503
1508
  id: "canopy-modal-search",
1504
1509
  variant: "search",
1505
1510
  labelledBy: "canopy-modal-search-label",
1506
- label: title,
1511
+ label: resolvedTitle,
1507
1512
  logo: SiteLogo,
1508
1513
  href: brandHref,
1509
1514
  closeLabel: "Close search",