@glossarist/concept-browser 0.7.105 → 0.7.107
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/README.md +59 -1
- package/cli/index.mjs +133 -36
- package/cli/index.ts +137 -37
- package/package.json +3 -1
- package/scripts/generate-data.mjs +6 -0
- package/scripts/smoke.mjs +269 -0
- package/src/__tests__/cli/favicon-config.test.ts +61 -0
- package/src/__tests__/composables/use-concept-edges-defensive.test.ts +164 -0
- package/src/__tests__/concept-detail-hydration.test.ts +199 -0
- package/src/components/ConceptDetail.vue +45 -38
- package/src/composables/use-concept-edges.ts +59 -43
- package/src/config/types.ts +17 -1
- package/src/layouts/Default.astro +41 -16
- package/src/pages/[...path].astro +6 -0
package/README.md
CHANGED
|
@@ -109,7 +109,65 @@ branding:
|
|
|
109
109
|
|
|
110
110
|
If no logo is configured, the Glossarist logo is shown by default. The footer always shows the Glossarist logo ("Powered by Glossarist").
|
|
111
111
|
|
|
112
|
-
Favicons
|
|
112
|
+
### Favicons
|
|
113
|
+
|
|
114
|
+
`branding.favicon` accepts two shapes — a legacy **string** form and an **object** form that lets consumers provide their own canonical favicon set without writing a post-build script.
|
|
115
|
+
|
|
116
|
+
#### String form (legacy, still supported)
|
|
117
|
+
|
|
118
|
+
Path to a single source SVG/PNG. The CLI generates the full variant set (favicon.ico, apple-touch-icon-*.png, etc.) from it using the `favicons` package:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
branding:
|
|
122
|
+
favicon: assets/my-brand.svg
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Object form (recommended for branded deployments)
|
|
126
|
+
|
|
127
|
+
The object form lets you provide your own canonical favicon set (typically RealFaviconGenerator output) and have the CLI install it without a post-build script.
|
|
128
|
+
|
|
129
|
+
A canonical favicon set is **multiple files** — typically `favicon.svg`, `favicon.ico`, `favicon-96x96.png`, `apple-touch-icon.png`, `web-app-manifest-192x192.png`, `web-app-manifest-512x512.png`, and `site.webmanifest`. Put all of them in a directory (e.g. `assets/favicons/`) and reference it via `source_dir`:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
my-deployment/
|
|
133
|
+
├── site-config.yml
|
|
134
|
+
└── assets/
|
|
135
|
+
└── favicons/ ← branding.favicon.source_dir points here
|
|
136
|
+
├── favicon.svg
|
|
137
|
+
├── favicon.ico
|
|
138
|
+
├── favicon-96x96.png
|
|
139
|
+
├── apple-touch-icon.png
|
|
140
|
+
├── web-app-manifest-192x192.png
|
|
141
|
+
├── web-app-manifest-512x512.png
|
|
142
|
+
└── site.webmanifest ← optional; CLI regenerates with BASE_PATH
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then in `site-config.yml`:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
branding:
|
|
149
|
+
favicon:
|
|
150
|
+
base_path: / # URL prefix (BASE_PATH-aware; default '/')
|
|
151
|
+
skip_default_links: true # suppress the default <link> block
|
|
152
|
+
source_dir: assets/favicons # canonical files (all of them, in one directory)
|
|
153
|
+
links_html: |- # raw HTML emitted verbatim in <head>
|
|
154
|
+
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
|
155
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
156
|
+
<link rel="shortcut icon" href="/favicon.ico" />
|
|
157
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
158
|
+
<link rel="manifest" href="/site.webmanifest" />
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
| Field | Type | Effect |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `source_dir` | string | Path (relative to cwd) to a directory containing **all** canonical favicon files. The CLI copies every file in this directory into `public/`, overriding any defaults. Also removes the default-generated cruft (`apple-touch-icon-57x57.png`, `favicon-16x16.png`, etc.) so it doesn't linger. |
|
|
164
|
+
| `links_html` | string | Raw HTML (one or more `<link>` tags) emitted verbatim in `<head>`. Use this to declare which of the source_dir files the browser should load, with what `sizes`/`type`. |
|
|
165
|
+
| `skip_default_links` | boolean | When true, the CLI does NOT call the `favicons` package and does NOT emit the default `<link>` tags. Pair with `links_html` and `source_dir` for fully custom branding. |
|
|
166
|
+
| `base_path` | string | URL prefix prepended to every emitted link. Useful for BASE_PATH-scoped deployments (e.g. `/vocab/`). |
|
|
167
|
+
|
|
168
|
+
The object form exists so consumers with a RealFaviconGenerator favicon set (or any other canonical brand favicon bundle) can install it without a post-build script. Previously this required workarounds like `glossarist/cie-eilv/scripts/install-favicons.mjs` (149 lines) and `glossarist/iala-vocab/scripts/install-favicons.mjs` (175 lines) — both are now unnecessary.
|
|
169
|
+
|
|
170
|
+
If neither form is set, the Glossarist default favicon is used.
|
|
113
171
|
|
|
114
172
|
### About pages
|
|
115
173
|
|
package/cli/index.mjs
CHANGED
|
@@ -88,53 +88,150 @@ Environment:
|
|
|
88
88
|
fs.mkdirSync(publicDir, { recursive: true });
|
|
89
89
|
|
|
90
90
|
const branding = config?.branding || {};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
|
|
92
|
+
// Favicon resolution (issue #173).
|
|
93
|
+
//
|
|
94
|
+
// `branding.favicon` accepts two shapes:
|
|
95
|
+
//
|
|
96
|
+
// 1. String (legacy) — path to a single source SVG/PNG. The `favicons`
|
|
97
|
+
// package generates the full variant set from it.
|
|
98
|
+
//
|
|
99
|
+
// 2. Object — `{ source_dir?, links_html?, skip_default_links?, base_path? }`.
|
|
100
|
+
// - `links_html`: raw HTML (one or more <link> tags) emitted verbatim.
|
|
101
|
+
// Skips the `favicons` package entirely; consumer is responsible
|
|
102
|
+
// for providing the actual icon files in public/.
|
|
103
|
+
// - `source_dir`: directory of canonical favicon files copied as-is
|
|
104
|
+
// into public/ (overriding any defaults). Useful when the consumer
|
|
105
|
+
// already has a RealFaviconGenerator output set.
|
|
106
|
+
// - `skip_default_links`: don't emit the default <link> block at all.
|
|
107
|
+
// - `base_path`: URL prefix for the emitted links (BASE_PATH-aware).
|
|
108
|
+
const faviconCfg = branding.favicon;
|
|
109
|
+
const faviconCfgIsObject =
|
|
110
|
+
faviconCfg !== null && typeof faviconCfg === 'object' && !Array.isArray(faviconCfg);
|
|
95
111
|
|
|
96
112
|
let faviconHtml = '';
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
theme_color: branding.primaryColor || '#2563eb',
|
|
105
|
-
icons: {
|
|
106
|
-
android: false,
|
|
107
|
-
appleIcon: true,
|
|
108
|
-
appleStartup: false,
|
|
109
|
-
favicons: true,
|
|
110
|
-
windows: false,
|
|
111
|
-
yandex: false,
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
for (const img of response.images) {
|
|
116
|
-
fs.writeFileSync(resolve(publicDir, img.name), img.contents);
|
|
117
|
-
}
|
|
118
|
-
for (const file of response.files) {
|
|
119
|
-
fs.writeFileSync(resolve(publicDir, file.name), file.contents);
|
|
113
|
+
let skipDefaultGeneration = false;
|
|
114
|
+
|
|
115
|
+
if (faviconCfgIsObject) {
|
|
116
|
+
// Object form — honor each declared field.
|
|
117
|
+
if (faviconCfg.links_html) {
|
|
118
|
+
faviconHtml = String(faviconCfg.links_html);
|
|
119
|
+
skipDefaultGeneration = true;
|
|
120
120
|
}
|
|
121
|
+
if (faviconCfg.source_dir) {
|
|
122
|
+
const srcDir = resolve(process.cwd(), faviconCfg.source_dir);
|
|
123
|
+
if (fs.existsSync(srcDir) && fs.statSync(srcDir).isDirectory()) {
|
|
124
|
+
console.log(`\n=== FAVICONS (copy from ${faviconCfg.source_dir}) ===\n`);
|
|
125
|
+
// When the consumer provides a canonical favicon set, also delete
|
|
126
|
+
// the default-generated cruft (apple-touch-icon-*.png, etc.) so
|
|
127
|
+
// it doesn't linger in public/ and pollute the deployment.
|
|
128
|
+
// Mirrors glossarist/iala-vocab/scripts/install-favicons.mjs.
|
|
129
|
+
const cruftFiles = [
|
|
130
|
+
'apple-touch-icon-57x57.png',
|
|
131
|
+
'apple-touch-icon-60x60.png',
|
|
132
|
+
'apple-touch-icon-72x72.png',
|
|
133
|
+
'apple-touch-icon-76x76.png',
|
|
134
|
+
'apple-touch-icon-114x114.png',
|
|
135
|
+
'apple-touch-icon-120x120.png',
|
|
136
|
+
'apple-touch-icon-144x144.png',
|
|
137
|
+
'apple-touch-icon-152x152.png',
|
|
138
|
+
'apple-touch-icon-167x167.png',
|
|
139
|
+
'apple-touch-icon-180x180.png',
|
|
140
|
+
'apple-touch-icon-1024x1024.png',
|
|
141
|
+
'apple-touch-icon-precomposed.png',
|
|
142
|
+
'favicon-16x16.png',
|
|
143
|
+
'favicon-32x32.png',
|
|
144
|
+
'favicon-48x48.png',
|
|
145
|
+
'browserconfig.xml',
|
|
146
|
+
];
|
|
147
|
+
let removed = 0;
|
|
148
|
+
for (const cruft of cruftFiles) {
|
|
149
|
+
const cruftPath = resolve(publicDir, cruft);
|
|
150
|
+
if (fs.existsSync(cruftPath)) {
|
|
151
|
+
fs.unlinkSync(cruftPath);
|
|
152
|
+
removed++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (removed > 0) console.log(` Removed ${removed} default cruft file(s)`);
|
|
121
156
|
|
|
122
|
-
|
|
123
|
-
|
|
157
|
+
let copied = 0;
|
|
158
|
+
for (const entry of fs.readdirSync(srcDir)) {
|
|
159
|
+
const fromPath = resolve(srcDir, entry);
|
|
160
|
+
if (fs.statSync(fromPath).isFile()) {
|
|
161
|
+
fs.copyFileSync(fromPath, resolve(publicDir, entry));
|
|
162
|
+
copied++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
console.log(` Copied ${copied} canonical favicon file(s) from ${faviconCfg.source_dir}`);
|
|
166
|
+
} else {
|
|
167
|
+
console.warn(` Warning: branding.favicon.source_dir not found: ${srcDir}`);
|
|
168
|
+
}
|
|
124
169
|
}
|
|
170
|
+
if (faviconCfg.skip_default_links) {
|
|
171
|
+
skipDefaultGeneration = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!skipDefaultGeneration) {
|
|
176
|
+
// Legacy / default path — derive a single source file and let the
|
|
177
|
+
// `favicons` package generate the full variant set.
|
|
178
|
+
const faviconSrc =
|
|
179
|
+
(typeof faviconCfg === 'string' && faviconCfg && resolve(process.cwd(), faviconCfg)) ||
|
|
180
|
+
(branding.logo?.localPath && resolve(process.cwd(), branding.logo.localPath)) ||
|
|
181
|
+
resolve(pkgRoot, 'public', 'favicon.svg');
|
|
125
182
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
183
|
+
if (fs.existsSync(faviconSrc)) {
|
|
184
|
+
console.log(`\n=== FAVICONS ===\n`);
|
|
185
|
+
const favicons = (await import('favicons')).default;
|
|
186
|
+
const source = fs.readFileSync(faviconSrc);
|
|
187
|
+
const response = await favicons(source, {
|
|
188
|
+
appName: config?.title || 'Glossarist',
|
|
189
|
+
background: branding.primaryColor || '#2563eb',
|
|
190
|
+
theme_color: branding.primaryColor || '#2563eb',
|
|
191
|
+
icons: {
|
|
192
|
+
android: false,
|
|
193
|
+
appleIcon: true,
|
|
194
|
+
appleStartup: false,
|
|
195
|
+
favicons: true,
|
|
196
|
+
windows: false,
|
|
197
|
+
yandex: false,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
for (const img of response.images) {
|
|
202
|
+
fs.writeFileSync(resolve(publicDir, img.name), img.contents);
|
|
203
|
+
}
|
|
204
|
+
for (const file of response.files) {
|
|
205
|
+
fs.writeFileSync(resolve(publicDir, file.name), file.contents);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (faviconSrc.endsWith('.svg')) {
|
|
209
|
+
fs.copyFileSync(faviconSrc, resolve(publicDir, 'favicon.svg'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Only use the generated HTML if the consumer didn't supply their own.
|
|
213
|
+
if (!faviconHtml) {
|
|
214
|
+
faviconHtml = response.html.join('\n ');
|
|
215
|
+
if (faviconSrc.endsWith('.svg')) {
|
|
216
|
+
faviconHtml += '\n <link rel="icon" type="image/svg+xml" href="/favicon.svg">';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
console.log(` Generated ${response.images.length} favicon files`);
|
|
129
220
|
}
|
|
130
|
-
fs.writeFileSync(resolve(publicDir, 'favicon-links.html'), faviconHtml);
|
|
131
|
-
console.log(` Generated ${response.images.length} favicon files`);
|
|
132
221
|
}
|
|
133
222
|
|
|
134
223
|
if (faviconHtml) {
|
|
135
|
-
|
|
224
|
+
fs.writeFileSync(resolve(publicDir, 'favicon-links.html'), faviconHtml);
|
|
225
|
+
// Apply base_path prefix (issue #173 + BASE_PATH-aware deployments).
|
|
226
|
+
const basePath =
|
|
227
|
+
(faviconCfgIsObject && faviconCfg.base_path) ||
|
|
228
|
+
process.env.BASE_PATH?.replace(/\/+$/, '') ||
|
|
229
|
+
'';
|
|
136
230
|
if (basePath) {
|
|
137
|
-
faviconHtml = faviconHtml.replace(
|
|
231
|
+
faviconHtml = faviconHtml.replace(
|
|
232
|
+
/(href|content)="\/([^"]+)"/g,
|
|
233
|
+
`$1="${basePath}/$2`,
|
|
234
|
+
);
|
|
138
235
|
}
|
|
139
236
|
process.env.FAVICON_HTML = faviconHtml;
|
|
140
237
|
}
|
package/cli/index.ts
CHANGED
|
@@ -66,8 +66,26 @@ interface BrandingFonts {
|
|
|
66
66
|
weights?: number[];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
interface FaviconConfig {
|
|
70
|
+
/** Path prefix for all favicon URLs. Defaults to '/', set to a sub-path
|
|
71
|
+
* when deploying under BASE_PATH != '/'. */
|
|
72
|
+
base_path?: string;
|
|
73
|
+
/** Skip emitting the default <link rel="icon"> and <link rel="apple-touch-icon">
|
|
74
|
+
* tags. Use when the consumer provides their own RFG-compatible markup via
|
|
75
|
+
* `links_html`. Default: false (emit default tags). */
|
|
76
|
+
skip_default_links?: boolean;
|
|
77
|
+
/** Raw HTML string (one or more <link> tags) to emit in <head> instead of
|
|
78
|
+
* the default favicon block. RealFaviconGenerator output goes here. */
|
|
79
|
+
links_html?: string;
|
|
80
|
+
/** Consumer-side directory (relative to cwd) holding canonical favicon files.
|
|
81
|
+
* The CLI copies these into public/, overriding defaults. */
|
|
82
|
+
source_dir?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
interface BrandingConfig {
|
|
70
|
-
|
|
86
|
+
/** Either a string (legacy: path to a source SVG/PNG) or an object form
|
|
87
|
+
* with `source_dir`, `links_html`, `skip_default_links`, `base_path`. */
|
|
88
|
+
favicon?: string | FaviconConfig;
|
|
71
89
|
logo?: { localPath?: string };
|
|
72
90
|
primaryColor?: string;
|
|
73
91
|
fonts?: { header?: BrandingFonts; body?: BrandingFonts };
|
|
@@ -99,53 +117,135 @@ async function runBuildPipeline(config: SiteConfig | null): Promise<void> {
|
|
|
99
117
|
fs.mkdirSync(publicDir, { recursive: true });
|
|
100
118
|
|
|
101
119
|
const branding: BrandingConfig = config?.branding ?? {};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
|
|
121
|
+
// Favicon resolution (issue #173, #177).
|
|
122
|
+
// `branding.favicon` accepts two shapes:
|
|
123
|
+
// 1. String (legacy) — path to a single source SVG/PNG.
|
|
124
|
+
// 2. Object — `{ source_dir?, links_html?, skip_default_links?, base_path? }`.
|
|
125
|
+
const faviconCfg: string | FaviconConfig | undefined = branding.favicon;
|
|
126
|
+
const faviconCfgIsObject: boolean =
|
|
127
|
+
faviconCfg !== null && typeof faviconCfg === 'object' && !Array.isArray(faviconCfg);
|
|
128
|
+
const faviconCfgObject: FaviconConfig | undefined =
|
|
129
|
+
faviconCfgIsObject ? (faviconCfg as FaviconConfig) : undefined;
|
|
106
130
|
|
|
107
131
|
let faviconHtml = '';
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
theme_color: branding.primaryColor || '#2563eb',
|
|
116
|
-
icons: {
|
|
117
|
-
android: false,
|
|
118
|
-
appleIcon: true,
|
|
119
|
-
appleStartup: false,
|
|
120
|
-
favicons: true,
|
|
121
|
-
windows: false,
|
|
122
|
-
yandex: false,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
for (const img of response.images) {
|
|
127
|
-
fs.writeFileSync(resolve(publicDir, img.name), img.contents);
|
|
132
|
+
let skipDefaultGeneration = false;
|
|
133
|
+
|
|
134
|
+
if (faviconCfgObject) {
|
|
135
|
+
const cfg: FaviconConfig = faviconCfgObject;
|
|
136
|
+
if (cfg.links_html) {
|
|
137
|
+
faviconHtml = String(cfg.links_html);
|
|
138
|
+
skipDefaultGeneration = true;
|
|
128
139
|
}
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
if (cfg.source_dir) {
|
|
141
|
+
const srcDir: string = resolve(process.cwd(), cfg.source_dir);
|
|
142
|
+
if (fs.existsSync(srcDir) && fs.statSync(srcDir).isDirectory()) {
|
|
143
|
+
console.log(`\n=== FAVICONS (copy from ${cfg.source_dir}) ===\n`);
|
|
144
|
+
// When the consumer provides a canonical favicon set, also delete
|
|
145
|
+
// the default-generated cruft so it doesn't linger in public/.
|
|
146
|
+
const cruftFiles: readonly string[] = [
|
|
147
|
+
'apple-touch-icon-57x57.png',
|
|
148
|
+
'apple-touch-icon-60x60.png',
|
|
149
|
+
'apple-touch-icon-72x72.png',
|
|
150
|
+
'apple-touch-icon-76x76.png',
|
|
151
|
+
'apple-touch-icon-114x114.png',
|
|
152
|
+
'apple-touch-icon-120x120.png',
|
|
153
|
+
'apple-touch-icon-144x144.png',
|
|
154
|
+
'apple-touch-icon-152x152.png',
|
|
155
|
+
'apple-touch-icon-167x167.png',
|
|
156
|
+
'apple-touch-icon-180x180.png',
|
|
157
|
+
'apple-touch-icon-1024x1024.png',
|
|
158
|
+
'apple-touch-icon-precomposed.png',
|
|
159
|
+
'favicon-16x16.png',
|
|
160
|
+
'favicon-32x32.png',
|
|
161
|
+
'favicon-48x48.png',
|
|
162
|
+
'browserconfig.xml',
|
|
163
|
+
];
|
|
164
|
+
let removed = 0;
|
|
165
|
+
for (const cruft of cruftFiles) {
|
|
166
|
+
const cruftPath: string = resolve(publicDir, cruft);
|
|
167
|
+
if (fs.existsSync(cruftPath)) {
|
|
168
|
+
fs.unlinkSync(cruftPath);
|
|
169
|
+
removed++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (removed > 0) console.log(` Removed ${removed} default cruft file(s)`);
|
|
173
|
+
|
|
174
|
+
let copied = 0;
|
|
175
|
+
for (const entry of fs.readdirSync(srcDir)) {
|
|
176
|
+
const fromPath: string = resolve(srcDir, entry);
|
|
177
|
+
if (fs.statSync(fromPath).isFile()) {
|
|
178
|
+
fs.copyFileSync(fromPath, resolve(publicDir, entry));
|
|
179
|
+
copied++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
console.log(` Copied ${copied} canonical favicon file(s) from ${cfg.source_dir}`);
|
|
183
|
+
} else {
|
|
184
|
+
console.warn(` Warning: branding.favicon.source_dir not found: ${srcDir}`);
|
|
185
|
+
}
|
|
131
186
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
fs.copyFileSync(faviconSrc, resolve(publicDir, 'favicon.svg'));
|
|
187
|
+
if (cfg.skip_default_links) {
|
|
188
|
+
skipDefaultGeneration = true;
|
|
135
189
|
}
|
|
190
|
+
}
|
|
136
191
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
192
|
+
if (!skipDefaultGeneration) {
|
|
193
|
+
const faviconSrc: string =
|
|
194
|
+
(typeof faviconCfg === 'string' && faviconCfg && resolve(process.cwd(), faviconCfg)) ||
|
|
195
|
+
(branding.logo?.localPath && resolve(process.cwd(), branding.logo.localPath)) ||
|
|
196
|
+
resolve(pkgRoot, 'public', 'favicon.svg');
|
|
197
|
+
|
|
198
|
+
if (fs.existsSync(faviconSrc)) {
|
|
199
|
+
console.log(`\n=== FAVICONS ===\n`);
|
|
200
|
+
const favicons = (await import('favicons')).default;
|
|
201
|
+
const source: Buffer = fs.readFileSync(faviconSrc);
|
|
202
|
+
const response: FaviconResponse = await favicons(source, {
|
|
203
|
+
appName: config?.title || 'Glossarist',
|
|
204
|
+
background: branding.primaryColor || '#2563eb',
|
|
205
|
+
theme_color: branding.primaryColor || '#2563eb',
|
|
206
|
+
icons: {
|
|
207
|
+
android: false,
|
|
208
|
+
appleIcon: true,
|
|
209
|
+
appleStartup: false,
|
|
210
|
+
favicons: true,
|
|
211
|
+
windows: false,
|
|
212
|
+
yandex: false,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
for (const img of response.images) {
|
|
217
|
+
fs.writeFileSync(resolve(publicDir, img.name), img.contents);
|
|
218
|
+
}
|
|
219
|
+
for (const file of response.files) {
|
|
220
|
+
fs.writeFileSync(resolve(publicDir, file.name), file.contents);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (faviconSrc.endsWith('.svg')) {
|
|
224
|
+
fs.copyFileSync(faviconSrc, resolve(publicDir, 'favicon.svg'));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Only use the generated HTML if the consumer didn't supply their own.
|
|
228
|
+
if (!faviconHtml) {
|
|
229
|
+
faviconHtml = response.html.join('\n ');
|
|
230
|
+
if (faviconSrc.endsWith('.svg')) {
|
|
231
|
+
faviconHtml += '\n <link rel="icon" type="image/svg+xml" href="/favicon.svg">';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
console.log(` Generated ${response.images.length} favicon files`);
|
|
140
235
|
}
|
|
141
|
-
fs.writeFileSync(resolve(publicDir, 'favicon-links.html'), faviconHtml);
|
|
142
|
-
console.log(` Generated ${response.images.length} favicon files`);
|
|
143
236
|
}
|
|
144
237
|
|
|
145
238
|
if (faviconHtml) {
|
|
146
|
-
|
|
239
|
+
fs.writeFileSync(resolve(publicDir, 'favicon-links.html'), faviconHtml);
|
|
240
|
+
const basePath: string =
|
|
241
|
+
(faviconCfgObject?.base_path) ||
|
|
242
|
+
process.env.BASE_PATH?.replace(/\/+$/, '') ||
|
|
243
|
+
'';
|
|
147
244
|
if (basePath) {
|
|
148
|
-
faviconHtml = faviconHtml.replace(
|
|
245
|
+
faviconHtml = faviconHtml.replace(
|
|
246
|
+
/(href|content)="\/([^"]+)"/g,
|
|
247
|
+
`$1="${basePath}/$2`,
|
|
248
|
+
);
|
|
149
249
|
}
|
|
150
250
|
process.env.FAVICON_HTML = faviconHtml;
|
|
151
251
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.107",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"sync:model": "node scripts/sync-concept-model.mjs",
|
|
21
21
|
"build:full": "npm run generate-ontology && npm run fetch-datasets && npm run generate-data && node scripts/build-edges.js && npm run build",
|
|
22
22
|
"build:site": "concept-browser --site build",
|
|
23
|
+
"smoke": "node scripts/smoke.mjs",
|
|
23
24
|
"test": "vitest run",
|
|
24
25
|
"test:watch": "vitest",
|
|
25
26
|
"mutation:test": "stryker run stryker.config.mjs",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"fast-check": "^4.8.0",
|
|
59
60
|
"happy-dom": "^20.9.0",
|
|
60
61
|
"n3": "^1.26.0",
|
|
62
|
+
"playwright": "^1.62.1",
|
|
61
63
|
"rdf-validate-shacl": "^0.4.5",
|
|
62
64
|
"typescript": "~5.7.3",
|
|
63
65
|
"vitest": "^4.1.5",
|
|
@@ -1344,6 +1344,9 @@ async function processDataset(dir, register, opts) {
|
|
|
1344
1344
|
await writeDatasetRdf(register, manifest, concepts, refMaps, opts);
|
|
1345
1345
|
|
|
1346
1346
|
// Copy bibliography.yaml → bibliography.json
|
|
1347
|
+
// ALWAYS emit bibliography.json — empty stub `{ bibliography: [] }` when no
|
|
1348
|
+
// source exists — so the concept detail page's unconditional fetch doesn't
|
|
1349
|
+
// 404 (issue #174). The build never emits a request it can't satisfy.
|
|
1347
1350
|
const bibPath = path.join(sourceRoot, 'bibliography.yaml');
|
|
1348
1351
|
if (fs.existsSync(bibPath)) {
|
|
1349
1352
|
const rawBib = readYaml(bibPath);
|
|
@@ -1351,6 +1354,9 @@ async function processDataset(dir, register, opts) {
|
|
|
1351
1354
|
writeJson(path.join(DATA, register, 'bibliography.json'), bibData);
|
|
1352
1355
|
const bibCount = bibData.bibliography.length;
|
|
1353
1356
|
console.log(` Copied bibliography (${bibCount} entries)`);
|
|
1357
|
+
} else {
|
|
1358
|
+
writeJson(path.join(DATA, register, 'bibliography.json'), { bibliography: [] });
|
|
1359
|
+
console.log(` Emitted empty bibliography.json (no source bibliography.yaml)`);
|
|
1354
1360
|
}
|
|
1355
1361
|
|
|
1356
1362
|
// Copy images/ with magic-byte validation + manifest emission.
|