@uniweb/build 0.15.6 → 0.15.8

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/build",
3
- "version": "0.15.6",
3
+ "version": "0.15.8",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,14 +59,14 @@
59
59
  "js-yaml": "^4.1.0",
60
60
  "sharp": "^0.33.2",
61
61
  "yaml": "^2.5.0",
62
- "@uniweb/projections": "0.1.1",
63
62
  "@uniweb/theming": "0.1.14",
64
- "@uniweb/content-writer": "0.2.8"
63
+ "@uniweb/projections": "0.1.2",
64
+ "@uniweb/content-writer": "0.2.9"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@uniweb/content-reader": "1.1.16",
68
- "@uniweb/runtime": "0.8.37",
69
- "@uniweb/schemas": "0.2.4"
67
+ "@uniweb/content-reader": "1.1.17",
68
+ "@uniweb/schemas": "0.2.4",
69
+ "@uniweb/runtime": "0.8.38"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -75,7 +75,7 @@
75
75
  "@tailwindcss/vite": "^4.0.0",
76
76
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
77
77
  "vite-plugin-svgr": "^4.0.0",
78
- "@uniweb/core": "0.7.30"
78
+ "@uniweb/core": "0.7.31"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "vite": {
@@ -527,6 +527,38 @@ function prettifySlug(slug) {
527
527
  .join(' ')
528
528
  }
529
529
 
530
+ /**
531
+ * Resolve a page's display title when the author declared none.
532
+ *
533
+ * Keys on whether the node has content of its own — the property the page
534
+ * payload already carries as `hasContent` — and deliberately NOT on which
535
+ * config file declared the folder. `folder.yml` vs `page.yml` decides how a
536
+ * directory's markdown is read; it says nothing about what the node IS, and
537
+ * keying on it made a content-less group node's name depend on how it happened
538
+ * to be declared. A `page.yml` folder with no markdown is the same group node
539
+ * as a `folder.yml` one and renders identically in nav.
540
+ *
541
+ * A node with no sections of its own (a group/container node, a redirect stub)
542
+ * has nothing to humanize from, so it shows the identifier the author actually
543
+ * wrote — `v1` stays `v1`. A node with content falls back to its opening H1,
544
+ * and only then to a prettified slug.
545
+ *
546
+ * The runtime has no fallback of its own: an empty title renders empty. The
547
+ * fill belongs here, at the producer, because `title` also feeds `<title>`,
548
+ * `og:title`, `llms.txt` and the search index — a fix applied at render would
549
+ * repair the nav row and leave the rest blank.
550
+ *
551
+ * @param {string|undefined} declaredTitle - Author-supplied title, if any
552
+ * @param {string} segment - Route segment (directory or file name)
553
+ * @param {Array|null} sections - The node's own sections, if any
554
+ * @returns {string}
555
+ */
556
+ function resolveDisplayTitle(declaredTitle, segment, sections) {
557
+ if (declaredTitle) return declaredTitle
558
+ if (!sections?.length) return segment
559
+ return extractH1(sections[0]?.content) || prettifySlug(segment)
560
+ }
561
+
530
562
  function parseNumericPrefix(filename) {
531
563
  const match = filename.match(/^(\d+(?:\.\d+)*)-?(.*)$/)
532
564
  if (match) {
@@ -1031,7 +1063,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
1031
1063
  sourcePath: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`,
1032
1064
  id: pageConfig.id || null,
1033
1065
  isIndex,
1034
- title: pageConfig.title || prettifySlug(pageName),
1066
+ title: resolveDisplayTitle(pageConfig.title, pageName, null),
1035
1067
  description: pageConfig.description || '',
1036
1068
  label: pageConfig.label || null,
1037
1069
  hidden: pageConfig.hidden || false,
@@ -1057,7 +1089,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
1057
1089
  sourcePath: parentRoute === '/' ? `/${pageName}` : `${parentRoute}/${pageName}`,
1058
1090
  id: pageConfig.id || null,
1059
1091
  isIndex,
1060
- title: pageConfig.title || prettifySlug(pageName),
1092
+ title: resolveDisplayTitle(pageConfig.title, pageName, null),
1061
1093
  description: pageConfig.description || '',
1062
1094
  label: pageConfig.label || null,
1063
1095
  hidden: pageConfig.hidden || false,
@@ -1318,7 +1350,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
1318
1350
  sourcePath, // Original folder-based path (for ancestor checking in navigation)
1319
1351
  id: pageConfig.id || null, // Stable page ID for page: links (survives reorganization)
1320
1352
  isIndex, // Marks this page as the index for its parent route
1321
- title: pageConfig.title || extractH1(hierarchicalSections[0]?.content) || prettifySlug(pageName),
1353
+ title: resolveDisplayTitle(pageConfig.title, pageName, hierarchicalSections),
1322
1354
  description: pageConfig.description || '',
1323
1355
  label: pageConfig.label || null, // Short label for navigation (defaults to title)
1324
1356
  // Localized URL slug overrides: { <locale>: <segment> }. Compiled into
@@ -1705,7 +1737,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
1705
1737
  sourcePath: isIndex ? (parentRoute === '/' ? `/${entry}` : `${parentRoute}/${entry}`) : null,
1706
1738
  id: dirConfig.id || null,
1707
1739
  isIndex,
1708
- title: dirConfig.title || entry,
1740
+ title: resolveDisplayTitle(dirConfig.title, entry, null),
1709
1741
  description: dirConfig.description || '',
1710
1742
  label: dirConfig.label || null,
1711
1743
  lastModified: null,
@@ -1801,7 +1833,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
1801
1833
  sourcePath: isIndex ? (parentRoute === '/' ? `/${entry}` : `${parentRoute}/${entry}`) : null,
1802
1834
  id: dirConfig.id || null,
1803
1835
  isIndex,
1804
- title: dirConfig.title || entry,
1836
+ title: resolveDisplayTitle(dirConfig.title, entry, null),
1805
1837
  description: dirConfig.description || '',
1806
1838
  label: dirConfig.label || null,
1807
1839
  lastModified: null,
@@ -56,7 +56,7 @@ function collectionOf(relPath) {
56
56
  * @returns {Promise<{ data: Object, search: Object }|null>} null when there is nothing
57
57
  * to deliver (no schema-less data AND no search index).
58
58
  */
59
- export async function assembleDataBall(distDir, schemalessNames = [], { projections = false } = {}) {
59
+ export async function assembleDataBall(distDir, schemalessNames = []) {
60
60
  const schemaless = new Set(schemalessNames)
61
61
  const allData = await readJsonTree(join(distDir, 'data'))
62
62
  const data = {}
@@ -65,47 +65,22 @@ export async function assembleDataBall(distDir, schemalessNames = [], { projecti
65
65
  }
66
66
  const search = await readJsonTree(join(distDir, '_search'))
67
67
 
68
- // Agent projections ride the same ball — same shape of thing (precomputed
69
- // at publish, opaque to the backend, served as-is), just text rather than
70
- // JSON. Off by default: what the artifacts are called and how they are
71
- // addressed on the serving side is the backend's to specify, and shipping
72
- // an unrecognized key into a payload the backend unwraps is not something
73
- // to decide unilaterally. Flip this on once that contract is agreed.
74
- const projectionFiles = projections ? await readTextTree(distDir) : {}
68
+ // Agent projections deliberately do NOT ride the ball.
69
+ //
70
+ // A backend that stores the site's content derives them itself at publish —
71
+ // one producer, so shipping ours would upload an artifact to the one host
72
+ // that does not need it, and invite two answers for one site. The projections
73
+ // this build emits into `dist/` are for hosts with no backend to derive them:
74
+ // static hosts and foreign backends. That is `@uniweb/projections`' scope and
75
+ // it is unchanged.
76
+ //
77
+ // (An opt-in `projections` bucket lived here while the delivery contract was
78
+ // open. It never shipped enabled, and the one-producer ruling closed the
79
+ // question — removed rather than left as a flag nobody may turn on.)
75
80
 
76
- const empty =
77
- Object.keys(data).length === 0 &&
78
- Object.keys(search).length === 0 &&
79
- Object.keys(projectionFiles).length === 0
80
- if (empty) return null
81
+ if (Object.keys(data).length === 0 && Object.keys(search).length === 0) return null
81
82
 
82
- return projections ? { data, search, projections: projectionFiles } : { data, search }
83
- }
84
-
85
- /**
86
- * Collect the agent projections from a built dist/ — `llms.txt` plus every
87
- * `.md` page projection, keyed by their served path.
88
- *
89
- * Deliberately excludes `node_modules`-shaped noise and anything under the
90
- * bundle's own asset directories; a site's `dist/` at this point holds only
91
- * what the link lane emitted.
92
- *
93
- * @param {string} distDir
94
- * @returns {Promise<Object<string, string>>}
95
- */
96
- async function readTextTree(distDir) {
97
- if (!existsSync(distDir)) return {}
98
- const out = {}
99
- const entries = await readdir(distDir, { withFileTypes: true, recursive: true })
100
- for (const entry of entries) {
101
- if (!entry.isFile()) continue
102
- const isProjection = entry.name === 'llms.txt' || entry.name.endsWith('.md')
103
- if (!isProjection) continue
104
- const full = join(entry.parentPath || entry.path, entry.name)
105
- const rel = relative(distDir, full).split(sep).join('/')
106
- out[rel] = await readFile(full, 'utf8')
107
- }
108
- return out
83
+ return { data, search }
109
84
  }
110
85
 
111
86
  // --- local media in the ball -------------------------------------------------