uniweb 1.19.1 → 1.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "1.19.1",
3
+ "version": "1.20.0",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,18 +37,18 @@
37
37
  "node": ">=22.12"
38
38
  },
39
39
  "dependencies": {
40
- "handlebars": "^4.7.8",
41
- "js-yaml": "^4.1.0",
40
+ "handlebars": "^4.7.10",
41
+ "js-yaml": "^4.3.2",
42
42
  "prompts": "^2.4.2",
43
- "tar": "^7.0.0",
44
- "@uniweb/content-writer": "~1.3.0",
43
+ "tar": "^7.5.21",
44
+ "@uniweb/content-writer": "~1.3.1",
45
+ "@uniweb/schemas": "~1.6.5",
45
46
  "@uniweb/core": "~1.4.6",
46
- "@uniweb/semantic-parser": "~1.13.0",
47
- "@uniweb/schemas": "~1.6.5"
47
+ "@uniweb/semantic-parser": "~1.13.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@uniweb/build": "~1.9.0",
51
- "@uniweb/content-reader": "~1.5.6",
50
+ "@uniweb/build": "~1.10.0",
51
+ "@uniweb/content-reader": "~1.5.7",
52
52
  "@uniweb/semantic-parser": "~1.13.0"
53
53
  },
54
54
  "peerDependenciesMeta": {
@@ -801,6 +801,8 @@ The point is the pair. An agent fetches the index, *reads* what each page is abo
801
801
 
802
802
  **Large sites also get per-branch indexes.** A branch with at least 5 pages gets its own — `/docs/llms.txt` lists just the docs, titled by that folder. The site index still lists everything; a branch index is an extra entry point, not a replacement, so an agent that starts at `/llms.txt` still reaches any page in two hops.
803
803
 
804
+ **Each language gets its own.** A site in several languages has `/fr/llms.txt` beside `/llms.txt`, listing the French pages, record pages included, and linking each at its French address (`/fr/a-propos.md`). Its search index is per language the same way (`/fr/search-index.json`).
805
+
804
806
  ```yaml
805
807
  # site.yml — all optional; these are the defaults
806
808
  agents:
@@ -1962,7 +1964,7 @@ fetch:
1962
1964
 
1963
1965
  **Lean lists.** A list carries each record's brief, so heavy parts belong in a section of their own — `@std/article`'s body is `body` — and never ride a list. A section that shows them declares its key `whole: true`; elsewhere components fetch the whole record on demand with `useWholeRecord(record, { query })`, which returns it as stored — `full.body.content`. Every file-based record gets its own file, `/data/<name>/<name of the record>.json`. An external query's whole record comes from its `record:` request, below. (`deferred:` is removed — the build stops on it.) The hook is safe to call on any query: when the query has no separate source for one record it returns the record you passed in.
1964
1966
 
1965
- **Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks — `useFetched`, `useCacheEntry`, `useWholeRecord`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
1967
+ **Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks — `useFetched`, `useCacheEntry`, `useWholeRecord`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically. On a pre-rendered page a hook's first render finds only what that page's own data holds — a declared key's answer, or its page's or the site's `fetch:`. Anything else is pre-rendered as the hook's loading state and fetched by the browser after it loads, so declare a key (`data:` in `meta.js`) for whatever a page must show without JavaScript.
1966
1968
 
1967
1969
  **Validate before shipping.** `uniweb validate` checks file-based data against your declared schemas — missing required fields, type/enum/format mismatches, nested fields. A violation fails it (`--lax` only reports); it checks every record file in `records/`, and `push` / `publish` refuse the same findings before sending anything. Distinct from `uniweb doctor` (project structure): `validate` checks your *data* against the schemas you *declared*. External queries (`url:`) are reported as deferred — their data isn't in the project; the value of a `ref`/`options` field is not checked against the entity it names.
1968
1970
 
@@ -12,8 +12,9 @@
12
12
  * --bundle (internal; called by `uniweb deploy --host` / `uniweb export`)
13
13
  * Full vite + post-vite pipeline. Produces a static-host JS bundle
14
14
  * (`dist/index.html`, `dist/entry.js`, `_importmap/*`, `_pages/*` for
15
- * split mode, sitemap/robots/search-index, prerendered HTML when
16
- * configured). The foundation is statically inlined when site.yml names a
15
+ * split mode, sitemap/robots, each language's search index, llms.txt and
16
+ * page markdown, prerendered HTML when configured). The foundation is
17
+ * statically inlined when site.yml names a
17
18
  * workspace-local one (the self-contained case), and loaded by URL when
18
19
  * it names a URL, or under VITE_FOUNDATION_MODE=runtime. A catalog ref
19
20
  * (`@org/name@version`) is refused (`detectFoundationType`).
@@ -461,19 +462,38 @@ async function loadI18nConfig(projectDir, siteConfig = null) {
461
462
  }
462
463
 
463
464
  /**
464
- * Build localized content for all configured locales
465
+ * Build every configured language but the default: its records, its content, and — on the bundle
466
+ * lane — its search index, llms.txt and page markdown, in that order (`buildLocalizedSite`).
467
+ *
468
+ * ⛔ Until 2026-10-25 each lane ran content, then records, here in two copies, and a language's
469
+ * search index was written in the content step, before its records were translated.
465
470
  */
466
- async function buildLocalizedContent(projectDir, i18nConfig) {
467
- const { buildLocalizedContent } = await import('@uniweb/build/i18n')
468
-
469
- const outputs = await buildLocalizedContent(projectDir, {
470
- localesDir: i18nConfig.localesDir,
471
+ async function buildLocalizedSite(projectDir, i18nConfig, { projections = false } = {}) {
472
+ const { buildLocalizedSite } = await import('@uniweb/build/i18n')
473
+ return buildLocalizedSite(projectDir, {
471
474
  locales: i18nConfig.locales,
475
+ localesDir: i18nConfig.localesDir,
472
476
  outputDir: join(projectDir, 'dist'),
473
- fallbackToSource: true
477
+ projections
474
478
  })
479
+ }
475
480
 
476
- return outputs
481
+ /** What `buildLocalizedSite` wrote, per language. */
482
+ function reportLocalizedSite(built) {
483
+ const locales = Object.keys(built.content)
484
+ success(`Generated ${locales.length} locale(s)`)
485
+ for (const locale of locales) {
486
+ log(` ${colors.dim}dist/${locale}/site-content.json${colors.reset}`)
487
+ const files = built.projections[locale] || []
488
+ for (const file of files) {
489
+ if (file.kind === 'search') log(` ${colors.dim}dist/${file.fileName} (${file.count} entries)${colors.reset}`)
490
+ if (file.kind === 'index') log(` ${colors.dim}dist/${file.fileName}${colors.reset}`)
491
+ }
492
+ const markdown = files.filter((file) => file.kind === 'markdown').length
493
+ if (markdown) log(` ${colors.dim}dist/${locale}/ — ${markdown} page markdown projections${colors.reset}`)
494
+ }
495
+ const translated = Object.values(built.records).filter((outputs) => Object.keys(outputs).length > 0).length
496
+ if (translated > 0) success(`Translated records for ${translated} locale(s)`)
477
497
  }
478
498
 
479
499
  /**
@@ -586,13 +606,16 @@ function resolveFoundationDir(projectDir, siteConfig) {
586
606
  *
587
607
  * Does NOT emit HTML, JS, CSS, source maps, _importmap chunks, processed media
588
608
  * (dist/assets/), or static-host extras (sitemap, robots, search-index,
589
- * _pages/*) — `publish` reads none of them. ⛔ *It processed media into
590
- * dist/assets/ — WebP copies, video posters, PDF thumbnails — until 2026-10-19.*
609
+ * llms.txt, page markdown, _pages/*), in any language — `publish` reads none
610
+ * of them. ⛔ *It processed media into dist/assets/ — WebP copies, video
611
+ * posters, PDF thumbnails — until 2026-10-19, and until 2026-10-25 wrote the
612
+ * default language's llms.txt and page markdown, and each other language's
613
+ * search index.*
591
614
  *
592
- * The `buildLocalizedContent` step is the same call bundle mode makes
593
- * post-vite, so multi-locale sites get identical per-locale outputs in
594
- * either mode. Record translation (`buildLocalizedRecords`) also runs here,
595
- * writing each locale's query files to dist/<lang>/data/.
615
+ * The `buildLocalizedSite` step is the call bundle mode makes post-vite,
616
+ * without the projections, so multi-locale sites get identical per-locale
617
+ * outputs in either mode: each language's query files (dist/<lang>/data/),
618
+ * then its content (dist/<lang>/site-content.json).
596
619
  *
597
620
  * History: when `deploy` uploaded `dist/site-content.json` to a backend, it
598
621
  * read the bundle pipeline's copy, which prerender rewrites into a lightweight
@@ -631,49 +654,16 @@ async function buildSiteLink(projectDir, options = {}) {
631
654
  })
632
655
  success(`Wrote ${join('dist', 'site-content.json')}`)
633
656
 
634
- // Per-locale variants — same call bundle mode makes post-vite. Both
635
- // modes produce identical `dist/<lang>/site-content.json` outputs, and
636
- // `publish` reads which languages were built from them.
657
+ // Per-locale variants — the call bundle mode makes post-vite, without the
658
+ // projections: a host derives its own. Both modes produce identical
659
+ // `dist/<lang>/site-content.json` outputs, and `publish` reads which
660
+ // languages were built from them.
637
661
  const i18nConfig = await loadI18nConfig(projectDir, siteConfig)
638
662
  if (i18nConfig && i18nConfig.locales.length > 0) {
639
663
  log('')
640
664
  info(`Building localized content for: ${i18nConfig.locales.join(', ')}`)
641
665
  try {
642
- const outputs = await buildLocalizedContent(projectDir, i18nConfig)
643
- success(`Generated ${Object.keys(outputs).length} locale(s)`)
644
- for (const [locale] of Object.entries(outputs)) {
645
- log(` ${colors.dim}dist/${locale}/site-content.json${colors.reset}`)
646
- }
647
-
648
- // Collection translations — optional; don't fail the build if
649
- // missing. Bundle mode does the same.
650
- try {
651
- const { buildLocalizedRecords } = await import('@uniweb/build/i18n')
652
- const recordOutputs = await buildLocalizedRecords(projectDir, {
653
- locales: i18nConfig.locales,
654
- outputDir: distDir,
655
- recordLocalesDir: join(
656
- projectDir,
657
- i18nConfig.localesDir,
658
- 'records'
659
- ),
660
- // Free-form record translations live under it — the default was `locales/`
661
- // whatever the site said.
662
- localesDir: join(projectDir, i18nConfig.localesDir)
663
- })
664
- const recordCount = Object.values(recordOutputs).reduce(
665
- (sum, localeOutputs) => sum + Object.keys(localeOutputs).length,
666
- 0
667
- )
668
- if (recordCount > 0) {
669
- success(
670
- `Translated records for ${Object.keys(recordOutputs).length} locale(s)`
671
- )
672
- }
673
- } catch (err) {
674
- if (process.env.UNIWEB_DEBUG)
675
- console.error('Record translation:', err.message)
676
- }
666
+ reportLocalizedSite(await buildLocalizedSite(projectDir, i18nConfig))
677
667
  } catch (err) {
678
668
  error(`i18n build failed: ${err.message}`)
679
669
  if (process.env.UNIWEB_DEBUG) console.error(err.stack)
@@ -719,52 +709,14 @@ async function buildSite(projectDir, options = {}) {
719
709
  info(`Building localized content for: ${i18nConfig.locales.join(', ')}`)
720
710
 
721
711
  try {
722
- // Generate locale-specific site-content.json
723
- const outputs = await buildLocalizedContent(projectDir, i18nConfig)
712
+ // Each language's records, then its content, then its search index, llms.txt and page
713
+ // markdown, made from both.
714
+ const built = await buildLocalizedSite(projectDir, i18nConfig, { projections: true })
724
715
 
725
716
  // Generate locale-specific index.html files
726
717
  await generateLocalizedHtml(projectDir, i18nConfig)
727
718
 
728
- success(`Generated ${Object.keys(outputs).length} locale(s)`)
729
-
730
- for (const [locale, path] of Object.entries(outputs)) {
731
- log(` ${colors.dim}dist/${locale}/site-content.json${colors.reset}`)
732
- }
733
-
734
- // Translate collections if they exist
735
- try {
736
- const { buildLocalizedRecords } = await import('@uniweb/build/i18n')
737
-
738
- const recordOutputs = await buildLocalizedRecords(projectDir, {
739
- locales: i18nConfig.locales,
740
- outputDir: join(projectDir, 'dist'),
741
- recordLocalesDir: join(
742
- projectDir,
743
- i18nConfig.localesDir,
744
- 'records'
745
- ),
746
- // Free-form record translations live under it — the default was `locales/`
747
- // whatever the site said.
748
- localesDir: join(projectDir, i18nConfig.localesDir)
749
- })
750
-
751
- // Count collections translated
752
- const recordCount = Object.values(recordOutputs).reduce(
753
- (sum, localeOutputs) => sum + Object.keys(localeOutputs).length,
754
- 0
755
- )
756
-
757
- if (recordCount > 0) {
758
- success(
759
- `Translated records for ${Object.keys(recordOutputs).length} locale(s)`
760
- )
761
- }
762
- } catch (err) {
763
- // Collection translation is optional, don't fail build
764
- if (process.env.UNIWEB_DEBUG) {
765
- console.error('Record translation:', err.message)
766
- }
767
- }
719
+ reportLocalizedSite(built)
768
720
  } catch (err) {
769
721
  error(`i18n build failed: ${err.message}`)
770
722
  if (process.env.UNIWEB_DEBUG) {
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-25T16:50:54.453Z",
3
+ "generatedAt": "2026-10-25T20:26:22.771Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "1.6.4"
7
7
  },
8
8
  "@uniweb/build": {
9
- "version": "1.9.0"
9
+ "version": "1.10.0"
10
10
  },
11
11
  "@uniweb/content-reader": {
12
- "version": "1.5.6"
12
+ "version": "1.5.7"
13
13
  },
14
14
  "@uniweb/content-writer": {
15
- "version": "1.3.0"
15
+ "version": "1.3.1"
16
16
  },
17
17
  "@uniweb/core": {
18
18
  "version": "1.4.6"
@@ -36,7 +36,7 @@
36
36
  "version": "1.2.0"
37
37
  },
38
38
  "@uniweb/runtime": {
39
- "version": "1.2.1"
39
+ "version": "1.2.2"
40
40
  },
41
41
  "@uniweb/scene": {
42
42
  "version": "1.0.1"
@@ -51,7 +51,7 @@
51
51
  "version": "1.13.0"
52
52
  },
53
53
  "@uniweb/snapshot": {
54
- "version": "1.1.0"
54
+ "version": "1.1.1"
55
55
  },
56
56
  "@uniweb/templates": {
57
57
  "version": "1.1.5"
@@ -60,7 +60,7 @@
60
60
  "version": "1.2.1"
61
61
  },
62
62
  "@uniweb/unipress": {
63
- "version": "1.1.15"
63
+ "version": "1.1.16"
64
64
  }
65
65
  },
66
66
  "templates": {