@canopy-iiif/app 1.5.15 → 1.5.16

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,8 @@ 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
+ const DEFAULT_SITE_TITLE = 'Site title';
16
19
 
17
20
  function resolveThemeAppearance() {
18
21
  if (cachedAppearance) return cachedAppearance;
@@ -47,17 +50,43 @@ function resolveThemeAccent() {
47
50
 
48
51
  function readYamlConfigBaseUrl() {
49
52
  try {
50
- const y = require('js-yaml');
51
53
  const p = resolveCanopyConfigPath();
52
54
  if (!fs.existsSync(p)) return '';
53
55
  const raw = fs.readFileSync(p, 'utf8');
54
- const data = y.load(raw) || {};
56
+ const data = yaml.load(raw) || {};
55
57
  const site = data && data.site;
56
58
  const url = site && site.baseUrl ? String(site.baseUrl) : '';
57
59
  return url;
58
60
  } catch (_) { return ''; }
59
61
  }
60
62
 
63
+ function readSiteMetadata() {
64
+ if (cachedSiteMetadata) return cachedSiteMetadata;
65
+ cachedSiteMetadata = { title: DEFAULT_SITE_TITLE };
66
+ try {
67
+ const cfgPath = resolveCanopyConfigPath();
68
+ if (!fs.existsSync(cfgPath)) return cachedSiteMetadata;
69
+ const raw = fs.readFileSync(cfgPath, 'utf8');
70
+ const data = yaml.load(raw) || {};
71
+ const directTitle = data && typeof data.title === 'string' ? data.title.trim() : '';
72
+ const nestedTitle =
73
+ data && data.site && typeof data.site.title === 'string'
74
+ ? data.site.title.trim()
75
+ : '';
76
+ const resolved = directTitle || nestedTitle || DEFAULT_SITE_TITLE;
77
+ cachedSiteMetadata = { title: resolved };
78
+ } catch (_) {}
79
+ return cachedSiteMetadata;
80
+ }
81
+
82
+ function getSiteTitle() {
83
+ const site = readSiteMetadata();
84
+ if (site && typeof site.title === 'string' && site.title.trim()) {
85
+ return site.title.trim();
86
+ }
87
+ return DEFAULT_SITE_TITLE;
88
+ }
89
+
61
90
  // Determine the absolute site origin (scheme + host[:port])
62
91
  // Priority:
63
92
  // 1) CANOPY_BASE_URL env
@@ -224,4 +253,7 @@ module.exports = {
224
253
  applyBaseToHtml,
225
254
  rootRelativeHref,
226
255
  canopyBodyClassForType,
256
+ readSiteMetadata,
257
+ getSiteTitle,
258
+ DEFAULT_SITE_TITLE,
227
259
  };
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
  }
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.16",
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",