@uniweb/core 0.5.12 → 0.5.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,7 +30,7 @@
30
30
  "jest": "^29.7.0"
31
31
  },
32
32
  "dependencies": {
33
- "@uniweb/semantic-parser": "1.1.6",
33
+ "@uniweb/semantic-parser": "1.1.7",
34
34
  "@uniweb/theming": "0.1.2"
35
35
  },
36
36
  "scripts": {
package/src/page.js CHANGED
@@ -96,14 +96,15 @@ export default class Page {
96
96
  * @returns {Object} Head metadata
97
97
  */
98
98
  getHeadMeta() {
99
+ const resolvedTitle = this.getTitle()
99
100
  return {
100
- title: this.title,
101
+ title: resolvedTitle,
101
102
  description: this.description,
102
103
  keywords: this.keywords,
103
104
  canonical: this.seo.canonical,
104
105
  robots: this.seo.noindex ? 'noindex, nofollow' : null,
105
106
  og: {
106
- title: this.seo.ogTitle || this.title,
107
+ title: this.seo.ogTitle || resolvedTitle,
107
108
  description: this.seo.ogDescription || this.description,
108
109
  image: this.seo.image,
109
110
  url: this.route,
@@ -284,21 +285,41 @@ export default class Page {
284
285
  // ─────────────────────────────────────────────────────────────────
285
286
 
286
287
  /**
287
- * Get the navigation route (canonical route for links)
288
- * With the new routing model, route is already the canonical nav route.
289
- * Index pages have route set to parent route (e.g., '/' for homepage).
288
+ * Get the navigation route (canonical route for links).
289
+ * For index pages whose route ends in /index (e.g., /Articles/index),
290
+ * returns the parent folder route (/Articles) so nav comparisons and
291
+ * active-route highlighting work against the clean URL.
290
292
  * @returns {string}
291
293
  */
292
294
  getNavRoute() {
295
+ if (this.isIndex && this.route.endsWith('/index')) {
296
+ return this.route.slice(0, -'/index'.length) || '/'
297
+ }
293
298
  return this.route
294
299
  }
295
300
 
301
+ /**
302
+ * Get display title for the page.
303
+ * For index pages with no meaningful title (empty or the literal string "index"),
304
+ * falls back to the parent folder's title so /Articles/index shows "Articles".
305
+ * @returns {string}
306
+ */
307
+ getTitle() {
308
+ if (this.isIndex && this.route.endsWith('/index')) {
309
+ const own = this.title?.trim()
310
+ if (!own || own.toLowerCase() === 'index') {
311
+ return this.parent?.title || own || ''
312
+ }
313
+ }
314
+ return this.title
315
+ }
316
+
296
317
  /**
297
318
  * Get display label for navigation (short form of title)
298
319
  * @returns {string}
299
320
  */
300
321
  getLabel() {
301
- return this.label || this.title
322
+ return this.label || this.getTitle()
302
323
  }
303
324
 
304
325
  /**
@@ -361,7 +382,13 @@ export default class Page {
361
382
  */
362
383
  getNavigableRoute() {
363
384
  if (this.hasContent()) return this.route
364
- for (const child of this.children || []) {
385
+ const children = this.children || []
386
+ // Prefer the index child (designated landing page for this folder).
387
+ // Return this folder's own route so the URL stays clean (/Articles, not /Articles/index).
388
+ const indexChild = children.find((c) => c.isIndex)
389
+ if (indexChild) return this.route
390
+ // Fall back to first child with content
391
+ for (const child of children) {
365
392
  const route = child.getNavigableRoute()
366
393
  if (route) return route
367
394
  }
package/src/website.js CHANGED
@@ -289,7 +289,16 @@ export default class Website {
289
289
 
290
290
  // Priority 1: Exact match on actual route
291
291
  const exactMatch = this.pages.find((page) => page.route === normalizedRoute)
292
- if (exactMatch) return exactMatch
292
+ if (exactMatch) {
293
+ // Priority 1.5: folder page with an isIndex child — resolve to the index child.
294
+ // This makes /Articles resolve to the /Articles/index page for rendering,
295
+ // keeping the URL clean while serving real content.
296
+ if (!exactMatch.hasContent()) {
297
+ const indexChild = exactMatch.children.find((c) => c.isIndex)
298
+ if (indexChild) return indexChild
299
+ }
300
+ return exactMatch
301
+ }
293
302
 
294
303
  // Priority 2: Index page nav route match
295
304
  const indexMatch = this.pages.find((page) => page.isIndex && page.getNavRoute() === normalizedRoute)
@@ -878,6 +887,10 @@ export default class Website {
878
887
  // These are templates for generating pages, not actual navigable pages
879
888
  if (page.route.includes(':')) return false
880
889
 
890
+ // Exclude index pages (route ends in /index) from navigation — they are
891
+ // represented by their parent folder entry which links to them via navigableRoute
892
+ if (page.isIndex && page.route.endsWith('/index')) return false
893
+
881
894
  // Check visibility based on navigation type
882
895
  if (!includeHidden) {
883
896
  if (page.hidden) return false
@@ -885,9 +898,11 @@ export default class Website {
885
898
  if (navType === 'footer' && page.hideInFooter) return false
886
899
  }
887
900
 
888
- // Skip empty folders (no content) that have no visible children.
889
- // Folders with children still appear as dropdown parents.
890
- if (!page.hasContent() && !page.children?.some(isPageVisible)) return false
901
+ // Skip empty folders that have no visible children AND no index child.
902
+ // Folders with an isIndex child are navigable (they link to the index page)
903
+ // even after the index child itself is filtered out above.
904
+ const hasNavigableIndex = !page.hasContent() && page.children?.some((c) => c.isIndex)
905
+ if (!page.hasContent() && !hasNavigableIndex && !page.children?.some(isPageVisible)) return false
891
906
 
892
907
  // Apply custom filter if provided
893
908
  if (customFilter && !customFilter(page)) return false
@@ -900,7 +915,9 @@ export default class Website {
900
915
  // When nested, only include root-level pages at top level
901
916
  // (children will be nested inside their parents)
902
917
  if (nested) {
903
- filteredPages = filteredPages.filter(page => !page.parent)
918
+ // Exclude child pages from root list. Also exclude orphans whose parent
919
+ // was removed (e.g., hidden) — they have parentRoute but no resolved parent.
920
+ filteredPages = filteredPages.filter(page => !page.parent && !page.parentRoute)
904
921
  }
905
922
 
906
923
  // Apply custom sort or default to order
@@ -917,7 +934,7 @@ export default class Website {
917
934
  route: navRoute, // Use canonical nav route (e.g., '/' for index pages)
918
935
  navigableRoute: page.getNavigableRoute(), // First route with content (for links)
919
936
  translatedRoute: this.translateRoute(navRoute), // Locale-specific display route
920
- title: page.title,
937
+ title: page.getTitle(),
921
938
  label: page.getLabel(),
922
939
  description: page.description,
923
940
  hasContent: page.hasContent(),