@kenjura/ursa 0.51.0 → 0.53.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # 0.53.0
2
+ 2025-01-01
3
+
4
+ ### Menu Size Optimization
5
+ - **External Menu JSON**: Menu data is now stored in `/public/menu-data.json` instead of being embedded in every HTML file. This dramatically reduces HTML file sizes for sites with large folder structures (e.g., from 2-3MB per file down to ~50KB).
6
+ - **Async Menu Loading**: Menu data is fetched asynchronously after page render, showing a "Loading menu..." indicator until ready.
7
+ - **Debug Fields Removed**: Menu JSON no longer includes debug/inactive fields, reducing JSON size further.
8
+ - **Gzip Compression**: Development server now uses gzip compression for all responses, significantly reducing transfer size for JSON and HTML files.
9
+
10
+ # 0.52.0
11
+ 2025-12-21
12
+
13
+ - Fixed search results not displaying correctly when no matches are found
14
+
1
15
  # 0.51.0
2
16
  2025-12-21
3
17
 
@@ -6,19 +6,6 @@
6
6
  <title>${title}</title>
7
7
  <link rel="stylesheet" href="/public/default.css" />
8
8
  ${styleLink}
9
- <script>
10
- // Search index loaded asynchronously from separate file to reduce page size
11
- window.SEARCH_INDEX = ${searchIndex};
12
- // Lazy load full search index if placeholder is empty
13
- if (!window.SEARCH_INDEX || window.SEARCH_INDEX.length === 0) {
14
- fetch('/public/search-index.json')
15
- .then(r => r.json())
16
- .then(data => { window.SEARCH_INDEX = data; })
17
- .catch(() => { window.SEARCH_INDEX = []; });
18
- }
19
- </script>
20
- <script src="/public/search.js"></script>
21
-
22
9
  </head>
23
10
 
24
11
  <body data-template-id="default">
@@ -47,6 +34,7 @@
47
34
  <script src="/public/toc.js"></script>
48
35
  <script src="/public/toc-generator.js"></script>
49
36
  <script src="/public/menu.js"></script>
37
+ <script src="/public/search.js"></script>
50
38
  <script src="/public/sectionify.js"></script>
51
39
  <script src="/public/sticky.js"></script>
52
40
  </body>
package/meta/default.css CHANGED
@@ -142,6 +142,14 @@ nav#nav-global {
142
142
  opacity: 0.7;
143
143
  }
144
144
 
145
+ .search-result-message {
146
+ padding: 16px;
147
+ text-align: center;
148
+ color: var(--text-color);
149
+ opacity: 0.7;
150
+ font-style: italic;
151
+ }
152
+
145
153
  nav#nav-main {
146
154
  position: fixed;
147
155
  top: calc(var(--global-nav-height) + 0.5rem);
package/meta/menu.js CHANGED
@@ -2,19 +2,12 @@ document.addEventListener('DOMContentLoaded', () => {
2
2
  const navMain = document.querySelector('nav#nav-main');
3
3
  if (!navMain) return;
4
4
 
5
- // Load menu data from embedded JSON
6
- const menuDataScript = document.getElementById('menu-data');
7
- if (!menuDataScript) return;
5
+ // State - menu data will be loaded asynchronously
6
+ let menuData = null;
7
+ let menuDataLoaded = false;
8
+ let menuDataLoading = false;
8
9
 
9
- let menuData;
10
- try {
11
- menuData = JSON.parse(menuDataScript.textContent);
12
- } catch (e) {
13
- console.error('Failed to parse menu data:', e);
14
- return;
15
- }
16
-
17
- // Load menu config from embedded JSON (contains openMenuItems)
10
+ // Load menu config from embedded JSON (contains openMenuItems) - this is small, so it's embedded
18
11
  const menuConfigScript = document.getElementById('menu-config');
19
12
  let menuConfig = { openMenuItems: [] };
20
13
  if (menuConfigScript) {
@@ -39,9 +32,39 @@ document.addEventListener('DOMContentLoaded', () => {
39
32
 
40
33
  // Helper to check if we're on mobile
41
34
  const isMobile = () => window.matchMedia('(max-width: 800px)').matches;
35
+
36
+ /**
37
+ * Load menu data from external JSON file
38
+ * This is done asynchronously to avoid blocking page render
39
+ */
40
+ async function loadMenuData() {
41
+ if (menuDataLoaded || menuDataLoading) return;
42
+ menuDataLoading = true;
43
+
44
+ try {
45
+ const response = await fetch('/public/menu-data.json');
46
+ if (!response.ok) {
47
+ throw new Error(`HTTP ${response.status}`);
48
+ }
49
+ menuData = await response.json();
50
+ menuDataLoaded = true;
51
+
52
+ // Re-render menu now that we have full data
53
+ initializeFromCurrentPage();
54
+ } catch (error) {
55
+ console.error('Failed to load menu data:', error);
56
+ menuDataLoaded = true; // Mark as loaded to prevent retries
57
+ } finally {
58
+ menuDataLoading = false;
59
+ }
60
+ }
61
+
62
+ // Start loading menu data immediately
63
+ loadMenuData();
42
64
 
43
65
  // Get items at a specific path
44
66
  function getItemsAtPath(path) {
67
+ if (!menuData) return [];
45
68
  let items = menuData;
46
69
  for (const segment of path) {
47
70
  const folder = items.find(item => item.path === (path.slice(0, path.indexOf(segment) + 1).join('/') || segment));
@@ -56,6 +79,7 @@ document.addEventListener('DOMContentLoaded', () => {
56
79
 
57
80
  // Find item by path
58
81
  function findItemByPath(pathString) {
82
+ if (!menuData) return null;
59
83
  const segments = pathString.split('/').filter(Boolean);
60
84
  let items = menuData;
61
85
  let item = null;
@@ -98,6 +122,12 @@ document.addEventListener('DOMContentLoaded', () => {
98
122
 
99
123
  // Render menu at current path
100
124
  function renderMenu() {
125
+ // Wait for menu data to load
126
+ if (!menuData) {
127
+ menuContainer.innerHTML = '<li class="menu-loading">Loading menu...</li>';
128
+ return;
129
+ }
130
+
101
131
  // Get items for current level (level 1)
102
132
  let level1Items;
103
133
  if (currentPath.length === 0) {
@@ -421,5 +451,6 @@ document.addEventListener('DOMContentLoaded', () => {
421
451
  renderMenu();
422
452
  }
423
453
 
424
- initializeFromCurrentPage();
454
+ // Initial render shows loading state, then loadMenuData() will call initializeFromCurrentPage() when ready
455
+ renderMenu();
425
456
  });
package/meta/search.js CHANGED
@@ -1,10 +1,16 @@
1
1
  // Global search functionality with typeahead
2
+ // Supports lazy-loading of search index for large datasets
3
+
2
4
  class GlobalSearch {
3
5
  constructor() {
4
6
  this.searchInput = document.getElementById('global-search');
5
7
  this.searchResults = null;
6
- this.searchIndex = window.SEARCH_INDEX || [];
8
+ this.searchIndex = null; // Will be loaded asynchronously
9
+ this.indexLoading = false;
10
+ this.indexLoaded = false;
7
11
  this.currentSelection = -1;
12
+ this._lastResults = null;
13
+ this.MIN_QUERY_LENGTH = 3;
8
14
 
9
15
  if (!this.searchInput) return;
10
16
 
@@ -14,6 +20,8 @@ class GlobalSearch {
14
20
  init() {
15
21
  this.createResultsContainer();
16
22
  this.bindEvents();
23
+ // Start loading the index immediately (but don't block)
24
+ this.loadSearchIndex();
17
25
  }
18
26
 
19
27
  createResultsContainer() {
@@ -23,6 +31,43 @@ class GlobalSearch {
23
31
  this.searchInput.parentNode.appendChild(this.searchResults);
24
32
  }
25
33
 
34
+ /**
35
+ * Load search index from external JSON file
36
+ * This is done asynchronously to avoid blocking page render
37
+ */
38
+ async loadSearchIndex() {
39
+ // Check if index was already embedded in page (legacy support)
40
+ if (window.SEARCH_INDEX && Array.isArray(window.SEARCH_INDEX) && window.SEARCH_INDEX.length > 0) {
41
+ this.searchIndex = window.SEARCH_INDEX;
42
+ this.indexLoaded = true;
43
+ return;
44
+ }
45
+
46
+ this.indexLoading = true;
47
+
48
+ try {
49
+ const response = await fetch('/public/search-index.json');
50
+ if (!response.ok) {
51
+ throw new Error(`HTTP ${response.status}`);
52
+ }
53
+ const data = await response.json();
54
+ this.searchIndex = data;
55
+ this.indexLoaded = true;
56
+ window.SEARCH_INDEX = data; // Cache globally for potential reuse
57
+
58
+ // If user was waiting, trigger search now
59
+ if (this.searchInput.value.length >= this.MIN_QUERY_LENGTH) {
60
+ this.handleSearch(this.searchInput.value);
61
+ }
62
+ } catch (error) {
63
+ console.error('Failed to load search index:', error);
64
+ this.searchIndex = [];
65
+ this.indexLoaded = true; // Mark as loaded (with empty) to stop loading indicator
66
+ } finally {
67
+ this.indexLoading = false;
68
+ }
69
+ }
70
+
26
71
  bindEvents() {
27
72
  // Input events
28
73
  this.searchInput.addEventListener('input', (e) => {
@@ -57,55 +102,103 @@ class GlobalSearch {
57
102
  }
58
103
 
59
104
  handleSearch(query) {
60
- if (!query || query.length < 2) {
105
+ const trimmedQuery = (query || '').trim();
106
+
107
+ // Clear results for empty query
108
+ if (!trimmedQuery) {
61
109
  this.hideResults();
62
110
  return;
63
111
  }
64
112
 
65
- const results = this.search(query);
66
- this.displayResults(results);
113
+ // Show minimum character message
114
+ if (trimmedQuery.length < this.MIN_QUERY_LENGTH) {
115
+ this.showMessage(`Type at least ${this.MIN_QUERY_LENGTH} characters to search`);
116
+ return;
117
+ }
118
+
119
+ // Show loading indicator if index isn't ready
120
+ if (this.indexLoading || !this.indexLoaded) {
121
+ this.showMessage('Loading search index...');
122
+ return;
123
+ }
124
+
125
+ const results = this.search(trimmedQuery);
126
+ this._lastResults = results;
127
+ this.displayResults(results, trimmedQuery);
128
+ }
129
+
130
+ /**
131
+ * Show a message in the results dropdown (for loading, errors, etc.)
132
+ */
133
+ showMessage(message) {
134
+ this.searchResults.innerHTML = '';
135
+ this.currentSelection = -1;
136
+
137
+ const item = document.createElement('div');
138
+ item.className = 'search-result-message';
139
+ item.textContent = message;
140
+ this.searchResults.appendChild(item);
141
+
142
+ this.showResults();
67
143
  }
68
144
 
69
145
  search(query) {
146
+ if (!this.searchIndex || !Array.isArray(this.searchIndex)) {
147
+ return [];
148
+ }
149
+
70
150
  const normalizedQuery = query.toLowerCase().trim();
151
+ const queryWords = normalizedQuery.split(/\s+/);
71
152
  const results = [];
72
153
 
73
154
  // Search through the index
74
155
  this.searchIndex.forEach(item => {
75
- const titleMatch = item.title.toLowerCase().includes(normalizedQuery);
76
- const pathMatch = item.path.toLowerCase().includes(normalizedQuery);
77
- const contentMatch = item.content && item.content.toLowerCase().includes(normalizedQuery);
156
+ const titleLower = (item.title || '').toLowerCase();
157
+ const pathLower = (item.path || '').toLowerCase();
158
+ const contentLower = (item.content || '').toLowerCase();
78
159
 
79
- if (titleMatch || pathMatch || contentMatch) {
80
- let score = 0;
81
-
82
- // Boost exact title matches
83
- if (item.title.toLowerCase() === normalizedQuery) score += 100;
84
- else if (item.title.toLowerCase().startsWith(normalizedQuery)) score += 50;
85
- else if (titleMatch) score += 25;
86
-
87
- // Boost path matches
88
- if (pathMatch) score += 10;
89
-
90
- // Content matches get lower score
91
- if (contentMatch) score += 5;
92
-
93
- results.push({ ...item, score });
94
- }
160
+ // Check if all query words match somewhere
161
+ const allWordsMatch = queryWords.every(word =>
162
+ titleLower.includes(word) ||
163
+ pathLower.includes(word) ||
164
+ contentLower.includes(word)
165
+ );
166
+
167
+ if (!allWordsMatch) return;
168
+
169
+ let score = 0;
170
+
171
+ // Boost exact title matches
172
+ if (titleLower === normalizedQuery) score += 100;
173
+ else if (titleLower.startsWith(normalizedQuery)) score += 50;
174
+ else if (titleLower.includes(normalizedQuery)) score += 25;
175
+
176
+ // Boost path matches
177
+ if (pathLower.includes(normalizedQuery)) score += 10;
178
+
179
+ // Content matches get lower score
180
+ if (contentLower.includes(normalizedQuery)) score += 5;
181
+
182
+ // Bonus for each word match in title
183
+ queryWords.forEach(word => {
184
+ if (titleLower.includes(word)) score += 3;
185
+ });
186
+
187
+ results.push({ ...item, score });
95
188
  });
96
189
 
97
190
  // Sort by score, then by title
98
191
  return results
99
192
  .sort((a, b) => {
100
193
  if (b.score !== a.score) return b.score - a.score;
101
- return a.title.localeCompare(b.title);
194
+ return (a.title || '').localeCompare(b.title || '');
102
195
  })
103
196
  .slice(0, 10); // Limit to 10 results
104
197
  }
105
198
 
106
- displayResults(results) {
199
+ displayResults(results, query) {
107
200
  if (results.length === 0) {
108
- this.hideResults();
201
+ this.showMessage(`No results for "${query}"`);
109
202
  return;
110
203
  }
111
204
 
@@ -119,11 +212,11 @@ class GlobalSearch {
119
212
 
120
213
  const title = document.createElement('div');
121
214
  title.className = 'search-result-title';
122
- title.textContent = result.title;
215
+ title.textContent = result.title || 'Untitled';
123
216
 
124
217
  const path = document.createElement('div');
125
218
  path.className = 'search-result-path';
126
- path.textContent = result.path;
219
+ path.textContent = result.path || result.url || '';
127
220
 
128
221
  item.appendChild(title);
129
222
  item.appendChild(path);
@@ -133,6 +226,12 @@ class GlobalSearch {
133
226
  this.navigateToResult(result);
134
227
  });
135
228
 
229
+ // Mouse hover selection
230
+ item.addEventListener('mouseenter', () => {
231
+ this.currentSelection = index;
232
+ this.updateSelection();
233
+ });
234
+
136
235
  this.searchResults.appendChild(item);
137
236
  });
138
237
 
@@ -154,24 +253,24 @@ class GlobalSearch {
154
253
  switch (e.key) {
155
254
  case 'ArrowDown':
156
255
  e.preventDefault();
157
- this.currentSelection = Math.min(this.currentSelection + 1, items.length - 1);
158
- this.updateSelection();
256
+ if (items.length > 0) {
257
+ this.currentSelection = Math.min(this.currentSelection + 1, items.length - 1);
258
+ this.updateSelection();
259
+ }
159
260
  break;
160
261
 
161
262
  case 'ArrowUp':
162
263
  e.preventDefault();
163
- this.currentSelection = Math.max(this.currentSelection - 1, -1);
164
- this.updateSelection();
264
+ if (items.length > 0) {
265
+ this.currentSelection = Math.max(this.currentSelection - 1, 0);
266
+ this.updateSelection();
267
+ }
165
268
  break;
166
269
 
167
270
  case 'Enter':
168
271
  e.preventDefault();
169
- if (this.currentSelection >= 0 && items[this.currentSelection]) {
170
- const index = items[this.currentSelection].dataset.index;
171
- const results = this.getLastSearchResults();
172
- if (results && results[index]) {
173
- this.navigateToResult(results[index]);
174
- }
272
+ if (this.currentSelection >= 0 && this._lastResults && this._lastResults[this.currentSelection]) {
273
+ this.navigateToResult(this._lastResults[this.currentSelection]);
175
274
  }
176
275
  break;
177
276
 
@@ -187,22 +286,23 @@ class GlobalSearch {
187
286
  items.forEach((item, index) => {
188
287
  item.classList.toggle('selected', index === this.currentSelection);
189
288
  });
190
- }
191
-
192
- getLastSearchResults() {
193
- // Store last search results for keyboard navigation
194
- if (!this._lastResults) {
195
- this._lastResults = this.search(this.searchInput.value);
289
+
290
+ // Scroll selected item into view
291
+ if (this.currentSelection >= 0 && items[this.currentSelection]) {
292
+ items[this.currentSelection].scrollIntoView({ block: 'nearest' });
196
293
  }
197
- return this._lastResults;
198
294
  }
199
295
 
200
296
  navigateToResult(result) {
201
- window.location.href = result.url;
297
+ if (result.url) {
298
+ window.location.href = result.url;
299
+ } else if (result.path) {
300
+ window.location.href = result.path;
301
+ }
202
302
  }
203
303
  }
204
304
 
205
305
  // Initialize when DOM is loaded
206
306
  document.addEventListener('DOMContentLoaded', () => {
207
- new GlobalSearch();
307
+ window.globalSearch = new GlobalSearch();
208
308
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@kenjura/ursa",
3
3
  "author": "Andrew London <andrew@kenjura.com>",
4
4
  "type": "module",
5
- "version": "0.51.0",
5
+ "version": "0.53.0",
6
6
  "description": "static site generator from MD/wikitext/YML",
7
7
  "main": "lib/index.js",
8
8
  "bin": {
@@ -24,6 +24,7 @@
24
24
  "url": "git+https://github.com/kenjura/ursa.git"
25
25
  },
26
26
  "dependencies": {
27
+ "compression": "^1.7.4",
27
28
  "directory-tree": "^3.3.2",
28
29
  "express": "^4.18.2",
29
30
  "fs-extra": "^10.1.0",
@@ -132,7 +132,8 @@ function resolveHref(rawHref, validPaths) {
132
132
  }
133
133
 
134
134
  // Build a flat tree structure with path info for JS navigation
135
- function buildMenuData(tree, source, validPaths, parentPath = '') {
135
+ // Set includeDebug=false to exclude debug fields and reduce JSON size
136
+ function buildMenuData(tree, source, validPaths, parentPath = '', includeDebug = true) {
136
137
  const items = [];
137
138
 
138
139
  // Files to hide from menu by default
@@ -192,14 +193,21 @@ function buildMenuData(tree, source, validPaths, parentPath = '') {
192
193
  label,
193
194
  path: folderPath,
194
195
  href,
195
- inactive,
196
- debug,
197
196
  hasChildren,
198
197
  icon,
199
198
  };
200
199
 
200
+ // Only include debug and inactive fields if requested (for smaller JSON)
201
+ if (includeDebug) {
202
+ menuItem.inactive = inactive;
203
+ menuItem.debug = debug;
204
+ } else if (inactive) {
205
+ // Only include inactive if true (to save space)
206
+ menuItem.inactive = true;
207
+ }
208
+
201
209
  if (hasChildren) {
202
- menuItem.children = buildMenuData(item, source, validPaths, folderPath);
210
+ menuItem.children = buildMenuData(item, source, validPaths, folderPath, includeDebug);
203
211
  }
204
212
 
205
213
  items.push(menuItem);
@@ -218,7 +226,9 @@ export async function getAutomenu(source, validPaths) {
218
226
  const tree = dirTree(source, {
219
227
  exclude: /[\/\\]\.|node_modules/, // Exclude hidden folders (starting with .) and node_modules
220
228
  });
221
- const menuData = buildMenuData(tree, source, validPaths);
229
+
230
+ // Build menu data WITHOUT debug fields for smaller JSON
231
+ const menuData = buildMenuData(tree, source, validPaths, '', false);
222
232
 
223
233
  // Get root config for openMenuItems setting
224
234
  const rootConfig = getRootConfig(source);
@@ -227,14 +237,11 @@ export async function getAutomenu(source, validPaths) {
227
237
  // Add home item with resolved href
228
238
  const homeResolved = resolveHref('/', validPaths);
229
239
  const fullMenuData = [
230
- { label: 'Home', path: '', href: homeResolved.href, inactive: homeResolved.inactive, debug: homeResolved.debug, hasChildren: false, icon: `<span class="menu-icon">${HOME_ICON}</span>` },
240
+ { label: 'Home', path: '', href: homeResolved.href, hasChildren: false, icon: `<span class="menu-icon">${HOME_ICON}</span>` },
231
241
  ...menuData
232
242
  ];
233
243
 
234
- // Embed the menu data as JSON for JavaScript to use
235
- const menuDataScript = `<script type="application/json" id="menu-data">${JSON.stringify(fullMenuData)}</script>`;
236
-
237
- // Embed the openMenuItems config as separate JSON
244
+ // Embed the openMenuItems config as JSON (small, safe to embed)
238
245
  const menuConfigScript = `<script type="application/json" id="menu-config">${JSON.stringify({ openMenuItems })}</script>`;
239
246
 
240
247
  // Render the breadcrumb header (hidden by default, shown when navigating)
@@ -245,10 +252,14 @@ export async function getAutomenu(source, validPaths) {
245
252
  <span class="menu-current-path"></span>
246
253
  </div>`;
247
254
 
248
- // Render the initial menu (root level)
255
+ // Render the initial menu (root level only - children loaded from external JSON)
249
256
  const menuHtml = renderMenuLevel(fullMenuData, 0);
250
257
 
251
- return `${menuDataScript}${menuConfigScript}${breadcrumbHtml}<ul class="menu-level" data-level="0">${menuHtml}</ul>`;
258
+ // Return both the HTML for embedding and the full menu data for the static JSON file
259
+ return {
260
+ html: `${menuConfigScript}${breadcrumbHtml}<ul class="menu-level" data-level="0">${menuHtml}</ul>`,
261
+ menuData: fullMenuData
262
+ };
252
263
  }
253
264
 
254
265
  function renderMenuLevel(items, level) {
@@ -323,7 +323,9 @@ export async function generate({
323
323
  const validPaths = buildValidPaths(allSourceFilenamesThatAreArticles, source, allSourceFilenamesThatAreDirectories);
324
324
  progress.log(`Built ${validPaths.size} valid paths for link validation`);
325
325
 
326
- const menu = await getMenu(allSourceFilenames, source, validPaths);
326
+ const menuResult = await getMenu(allSourceFilenames, source, validPaths);
327
+ const menu = menuResult.html;
328
+ const menuData = menuResult.menuData;
327
329
 
328
330
  // Get and increment build ID from .ursa.json
329
331
  const buildId = getAndIncrementBuildId(resolve(_source));
@@ -549,6 +551,13 @@ export async function generate({
549
551
  progress.log(`Writing search index with ${searchIndex.length} entries`);
550
552
  await outputFile(searchIndexPath, JSON.stringify(searchIndex));
551
553
 
554
+ // Write menu data as a separate JSON file (not embedded in each page)
555
+ // This dramatically reduces HTML file sizes for large sites
556
+ const menuDataPath = join(output, 'public', 'menu-data.json');
557
+ const menuDataJson = JSON.stringify(menuData);
558
+ progress.log(`Writing menu data (${(menuDataJson.length / 1024).toFixed(1)} KB)`);
559
+ await outputFile(menuDataPath, menuDataJson);
560
+
552
561
  // Process directory indices with batched concurrency
553
562
  const totalDirs = allSourceFilenamesThatAreDirectories.length;
554
563
  let processedDirs = 0;
@@ -1084,23 +1093,12 @@ async function getTemplates(meta) {
1084
1093
  async function getMenu(allSourceFilenames, source, validPaths) {
1085
1094
  // todo: handle various incarnations of menu filename
1086
1095
 
1087
- const rawMenu = await getAutomenu(source, validPaths);
1088
- const menuBody = renderFile({ fileContents: rawMenu, type: ".md" });
1089
- return menuBody;
1090
-
1091
- // const allMenus = allSourceFilenames.filter((filename) =>
1092
- // filename.match(/_?menu\.(html|yml|md|txt)/)
1093
- // );
1094
- // console.log({ allMenus });
1095
- // if (allMenus.length === 0) return "";
1096
-
1097
- // // pick best menu...TODO: actually apply logic here
1098
- // const bestMenu = allMenus[0];
1099
- // const rawBody = await readFile(bestMenu, "utf8");
1100
- // const type = parse(bestMenu).ext;
1101
- // const menuBody = renderFile({ fileContents: rawBody, type });
1102
-
1103
- // return menuBody;
1096
+ const menuResult = await getAutomenu(source, validPaths);
1097
+ const menuBody = renderFile({ fileContents: menuResult.html, type: ".md" });
1098
+ return {
1099
+ html: menuBody,
1100
+ menuData: menuResult.menuData
1101
+ };
1104
1102
  }
1105
1103
 
1106
1104
  async function getTransformedMetadata(dirname, metadata) {
package/src/serve.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import express from "express";
2
+ import compression from "compression";
2
3
  import watch from "node-watch";
3
4
  import { generate, regenerateSingleFile, clearWatchCache } from "./jobs/generate.js";
4
5
  import { join, resolve, dirname } from "path";
@@ -252,6 +253,15 @@ export async function serve({
252
253
  function serveFiles(outputDir, port = 8080) {
253
254
  const app = express();
254
255
 
256
+ // Enable gzip compression for all responses
257
+ // This significantly reduces transfer size for JSON and HTML files
258
+ app.use(compression({
259
+ // Compress everything over 1KB
260
+ threshold: 1024,
261
+ // Use default compression level (good balance of speed vs size)
262
+ level: 6
263
+ }));
264
+
255
265
  app.use(
256
266
  express.static(outputDir, { extensions: ["html"], index: "index.html" })
257
267
  );