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 +9 -9
- package/partials/agents.md +3 -1
- package/src/commands/build.js +48 -96
- package/src/framework-index.json +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "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.
|
|
41
|
-
"js-yaml": "^4.
|
|
40
|
+
"handlebars": "^4.7.10",
|
|
41
|
+
"js-yaml": "^4.3.2",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
|
-
"tar": "^7.
|
|
44
|
-
"@uniweb/content-writer": "~1.3.
|
|
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.
|
|
51
|
-
"@uniweb/content-reader": "~1.5.
|
|
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": {
|
package/partials/agents.md
CHANGED
|
@@ -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
|
|
package/src/commands/build.js
CHANGED
|
@@ -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
|
|
16
|
-
* configured). The foundation is
|
|
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
|
|
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
|
|
467
|
-
const {
|
|
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
|
-
|
|
477
|
+
projections
|
|
474
478
|
})
|
|
479
|
+
}
|
|
475
480
|
|
|
476
|
-
|
|
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
|
|
590
|
-
* dist/assets/ — WebP copies, video
|
|
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 `
|
|
593
|
-
*
|
|
594
|
-
* either mode
|
|
595
|
-
*
|
|
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 —
|
|
635
|
-
//
|
|
636
|
-
// `publish` reads which
|
|
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
|
-
|
|
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
|
-
//
|
|
723
|
-
|
|
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
|
-
|
|
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) {
|
package/src/framework-index.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
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
|
+
"version": "1.10.0"
|
|
10
10
|
},
|
|
11
11
|
"@uniweb/content-reader": {
|
|
12
|
-
"version": "1.5.
|
|
12
|
+
"version": "1.5.7"
|
|
13
13
|
},
|
|
14
14
|
"@uniweb/content-writer": {
|
|
15
|
-
"version": "1.3.
|
|
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.
|
|
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.
|
|
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.
|
|
63
|
+
"version": "1.1.16"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"templates": {
|