uniweb 0.51.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.51.3",
3
+ "version": "0.53.0",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,15 +41,15 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/runtime": "^0.22.0",
45
- "@uniweb/core": "^0.26.2",
44
+ "@uniweb/kit": "^0.18.6",
46
45
  "@uniweb/semantic-parser": "^1.4.0",
47
- "@uniweb/kit": "^0.18.4"
46
+ "@uniweb/runtime": "^0.24.0",
47
+ "@uniweb/core": "^0.28.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@uniweb/semantic-parser": "^1.4.0",
51
50
  "@uniweb/content-reader": "^1.2.4",
52
- "@uniweb/build": "^0.47.2"
51
+ "@uniweb/semantic-parser": "^1.4.0",
52
+ "@uniweb/build": "^0.49.0"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@uniweb/build": {
@@ -876,6 +876,12 @@ You can keep the same declarations under `queries:` in `site.yml` instead, if yo
876
876
 
877
877
  **Show a query on a page** with `query:` in `page.yml` or a section's frontmatter (the whole result), or `fetch:` for anything more — a `limit`, a `where`. A list — `query: [team, articles]` — declares several, each arriving under its own `content.data` key. `query:` takes names only; `data:`, its old name, is now an error.
878
878
 
879
+ **Who receives it:** a section's own declaration reaches that section; a page's, every section on the page; a parent page's, the sections of the pages directly under it. **The site is the root page:** `query:` in `site.yml` reaches the layout areas (header, footer, …) and the sections of top-level pages — the homepage included — and no page further down. A section on `/docs/setup` that needs site-wide data names the query itself.
880
+
881
+ **A fetch always names a query.** A string in `fetch:` is a query name: `fetch: team` is `fetch: { query: team }`, and `fetch: [team, articles]` is a list of those. A fetch never names a file — `/data/<query>.json` is what the build generates from a query for a site with no backend, and writing that path in a `fetch:` stops the build. That is what keeps a site portable: name the query, debug locally against the generated data, publish, and the same page reads live records from the host with nothing changed.
882
+
883
+ **A `fetch:` narrows its query, never widens it:** its `where` must hold beside the query's, and its `sort` and `limit` replace the query's (a larger `limit` is fine). That is the whole list — `scope` belongs to the query, and the build stops on a `fetch:` that carries one; for another folder branch, declare another query. A `limit` is how many a list shows, never which records exist: every record the query selects compiles and gets its detail page. Two entries under one key at one level: the first is used, and the build warns.
884
+
879
885
  ```yaml
880
886
  # pages/blog/page.yml | # a section on the homepage
881
887
  title: Blog | ---
@@ -897,7 +903,7 @@ pages/blog/
897
903
 
898
904
  `entities/article/design-tips.md` becomes `/blog/design-tips`. The section inside `[slug]/` needs no special markdown — the matched record is delivered to it. Generated pages are excluded from navigation menus.
899
905
 
900
- **Which query the URL narrows — the page's route query:** the `[slug]` page's own `query:`, else its parent page's (the usual shape, above), else `site.yml`'s; if none declares one, the query its sections all declare. The first query of that level wins. Every section the route query reaches gets the one record; a section declaring a *different* query of its own gets that query as declared. The folder name says what the URL segment matches: `[slug]` the record's handle (`$name`, which compiled records carry — equal to their `slug`), `[uuid]` its `$uuid`, any other `[name]` the record's own field of that name — and when that field holds several values, **any member** matches (the record's own link is its first value). Routing by a field that is not unique picks one record and which one is not guaranteed; the build warns. A folder inside `[slug]/` (`[slug]/cv/` → `/blog/:slug/cv`) is a parametric page too, reading the record when `[slug]/page.yml` declares the query. `[dir]` and `[path]` are refused as folder names, and so is any folder inside `[...path]/`.
906
+ **Which query the URL narrows — the page's route query:** the `[slug]` page's own `query:`, else its parent page's (the usual shape, above), else `site.yml`'s (for a top-level `pages/[slug]/` only — the site's reaches no deeper page); if none declares one, the query its sections all declare. The first query of that level wins. Every section the route query reaches gets the one record; a section declaring a *different* query of its own gets that query as declared. The folder name says what the URL segment matches: `[slug]` the record's handle (`$name`, which compiled records carry — equal to their `slug`), `[uuid]` its `$uuid`, any other `[name]` the record's own field of that name — and when that field holds several values, **any member** matches (the record's own link is its first value). Routing by a field that is not unique picks one record and which one is not guaranteed; the build warns. A folder inside `[slug]/` (`[slug]/cv/` → `/blog/:slug/cv`) is a parametric page too, about the same record: its route query is the `[slug]` page's, wherever that is declared, and a query `cv/` declares itself arrives under its own key. **A section chooses how it uses the page's record with `current:`** on its own `fetch:` under the route query's key — `only` (the default: the record, as a list of one), `exclude` (the others: "related"), `include` (all of them: a pager) — e.g. `fetch: { query: recent, current: exclude, limit: 3 }`, where `limit` counts the others. `refine: true` / `detail: false` are retired and stop the build. `[dir]` and `[path]` are refused as folder names, and so is any folder inside `[...path]/`.
901
907
 
902
908
  > **The record arrives as a single-element array under the query key** — `content.data.recent[0]`, not `content.data.article`. The runtime never coerces it to an object and never synthesizes a singular key. See *Data* in Part 4.
903
909
 
@@ -905,7 +911,7 @@ pages/blog/
905
911
 
906
912
  **Two options for bigger sets:**
907
913
 
908
- `deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch on demand via `useEntityDetail`. For a remote source, add `detailUrl: /api/articles/{slug}` so the framework knows how to fetch one full record; file-based records emit per-record files at `/data/<name>/<slug>.json` and need no configuration.
914
+ `deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch on demand via `useEntityDetail`. File-based records emit per-record files at `/data/<name>/<slug>.json` and need no configuration; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4).
909
915
 
910
916
  `queryable:` declares which fields a reader may filter on, with enough metadata for the foundation to render controls:
911
917
 
@@ -1608,7 +1614,7 @@ export default function Grid({ block, params }) {
1608
1614
 
1609
1615
  Each child is a regular section with its own type, params, and content — and you're in the middle: wrap each child, filter by type, reorder, add container classes. The author decides *what* goes in the grid; your component decides *how* it renders. Tomorrow the author can swap a child for a different section type with no code change, and your components stay reusable wherever child sections are accepted.
1610
1616
 
1611
- **Data and child blocks:** page-level `query:` (or `fetch:`) is available to all blocks including children, and each child resolves data independently through the page → site hierarchy. If a child needs data no ancestor declares, give it its own in its frontmatter (`query: articles`, or `fetch:`). Its `meta.js` `data:` declares the shape it reads, never where the data comes from — it fetches nothing.
1617
+ **Data and child blocks:** page-level `query:` (or `fetch:`) is available to all blocks including children, and each child resolves data independently through the page → parent page → site hierarchy (the site's only on a top-level page). If a child needs data no ancestor declares, give it its own in its frontmatter (`query: articles`, or `fetch:`). Its `meta.js` `data:` declares the shape it reads, never where the data comes from — it fetches nothing.
1612
1618
 
1613
1619
  **SSG:** insets, `<ChildBlocks>`, and `<Visual>` all render correctly during prerender. Inset components using React hooks internally trigger prerender warnings — expected and harmless; the page renders correctly client-side.
1614
1620
 
@@ -1667,7 +1673,7 @@ Layouts are full components with their own `params` in `meta.js`, not just struc
1667
1673
 
1668
1674
  Two optional keys tune how areas behave across a navigation. `transitions` renames or opts regions out of per-area view transitions (`{ left: null }`, or `false` for the whole layout). `layers` sets which area paints on top — every area is stacked above the body by default, so a fixed header works without declaring anything, but areas are equal to each other, so a layout whose chrome overlaps says which wins: `layers: { header: 2, left: 1 }`. Both take an object to override per region, or `false` to opt out. **A `z-index` inside an area cannot lift it past another area** — each area is its own stacking context — so reach for `layers` rather than a bigger number, and for `<Overlay>` when a modal needs to escape the area entirely.
1669
1675
 
1670
- **Layout content** lives in `site/layout/` — `header.md`, `footer.md` for the default layout, or a named subdirectory (`site/layout/marketing/`) for named layouts. Named subdirectories are self-contained — no inheritance. Cascade: `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`.
1676
+ **Layout content** lives in `site/layout/` — `header.md`, `footer.md` for the default layout, or a named subdirectory (`site/layout/marketing/`) for named layouts. Named subdirectories are self-contained — no inheritance. Cascade: `page.yml` → `folder.yml` → `site.yml` → foundation `defaultLayout` → `"default"`. The structure alone decides what an entry is: every folder directly under `layout/` is a named layout (its name matched regardless of case and of a trailing `Layout` — `site/layout/docs/` serves the foundation's `DocsLayout`), and an area with several sections is a folder inside a layout's folder — `site/layout/default/header/1-topbar.md` for the default layout. An area has no `page.yml`; its sections render in filename order.
1671
1677
 
1672
1678
  Layout sections are regular section types — they support the full content shape, including tagged data blocks, lists, links, and items. The only difference is they render on every page. Each content category takes a different role:
1673
1679
 
@@ -1804,7 +1810,7 @@ export default {
1804
1810
 
1805
1811
  A foundation can route a scope to a plain folder of schema files instead of a package via an optional `schemas.config.js` at its root — `export default { '@acme': '../shared/acme-schemas' }`. A routed scope wins over the package convention; `@/` and `@uniweb` are never routable; a routed scope has no package fallback for a missing schema (it errors rather than silently loading a different definition). Per-schema keys override single entries (most-specific wins: file › directory › package). Worked examples: `development/schemas-in-practice.md`.
1806
1812
 
1807
- **Authoring queries.** Fetch declarations accept `where:` (a where-object predicate), `sort:` (ONE key, e.g. `date desc`), and `limit:`. The framework evaluates them in the browser over the records it fetched; a host that answers queries evaluates the same language at the source; a foundation transport decides for itself. The declaration is identical in every case.
1813
+ **Authoring queries.** Fetch declarations accept `where:` (a where-object predicate), `sort:` (ONE key, e.g. `date desc` — text sorts in the page's language, and a record with no value for the key sorts last either way), and `limit:`. The framework evaluates them in the browser over the records it fetched; a host that answers queries evaluates the same language at the source; a foundation transport decides for itself. The declaration is identical in every case.
1808
1814
 
1809
1815
  ```yaml
1810
1816
  # pages/blog/page.yml
@@ -1815,17 +1821,30 @@ fetch:
1815
1821
  limit: 3
1816
1822
  ```
1817
1823
 
1818
- **Lean lists with `deferred:`.** A query over records with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]`. The cascade payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json` (file-based records) or fetched from an author-declared `detailUrl:` (API-backed). On dynamic-route pages the focused record's full data is delivered automatically; elsewhere components fetch on demand via `useEntityDetail`. The hook is safe to call on any query: when there is no separate detail source it returns the record you passed in, because nothing was stripped from it.
1824
+ **Lean lists with `deferred:`.** A query over records with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]`. The cascade payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json`. (An external query declares no `deferred:` its whole record comes from `record:`, below.) On dynamic-route pages the focused record's full data is delivered automatically; elsewhere components fetch on demand via `useEntityDetail`. The hook is safe to call on any query: when there is no separate detail source it returns the record you passed in, because nothing was stripped from it.
1819
1825
 
1820
1826
  **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`, `useEntityDetail`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
1821
1827
 
1822
- **Validate before shipping.** `uniweb validate` checks file-based data against your declared schemas — missing required fields, type/enum/format mismatches, nested fields. Warns by default; `--strict` for a non-zero CI exit. Distinct from `uniweb doctor` (project structure): `validate` checks your *data* against the schemas you *declared*. Remote (`url:`), `ref`/`options`, and rich `sections`-form inputs are reported deferred.
1828
+ **Validate before shipping.** `uniweb validate` checks file-based data against your declared schemas — missing required fields, type/enum/format mismatches, nested fields. Warns by default; `--strict` for a non-zero CI exit. Distinct from `uniweb doctor` (project structure): `validate` checks your *data* against the schemas you *declared*. External queries (`url:`), `ref`/`options`, and rich `sections`-form inputs are reported deferred.
1823
1829
 
1824
1830
  ### Fetching from other sources (`fetcher:`)
1825
1831
 
1826
- A site isn't limited to file-based records. The default fetcher also reads a plain JSON `url:`GET, or `method: POST` with a `body:`, an optional `transform:` dot-path, the `detail:` forms for a record and evaluates `where` / `sort` / `limit` in the browser over what arrived. A site published to a Uniweb host reads the host's records with no configuration at all.
1832
+ A site isn't limited to its own records. A public, keyless JSON API is an **external query**a query with `url:`, declared beside the others and named by pages the same way (`query: posts`):
1833
+
1834
+ ```yaml
1835
+ # queries.yml
1836
+ posts:
1837
+ url: https://api.example.com/posts
1838
+ transform: data.items # dot-path to the records in the response
1839
+ record: # one whole post, for a [id] page
1840
+ url: https://api.example.com/posts/{id}
1841
+ ```
1842
+
1843
+ It also takes `method: POST` with a `body`, `where` / `sort` / `limit` (evaluated over what arrived, after `transform`) and `queryable`. `record:` takes `url`, `method`, `body` and `transform`: `url` and `method` default to the query's, `body` and `transform` never carry over, and a placeholder named by the page's folder (`{id}` for `[id]`, `{slug}` for `[slug]`) is its URL segment. `schema`, `scope`, `deferred`, `excerpt`, `route` and `path` are refused beside `url:` — they describe the site's own records. The visitor's browser fetches an external query (a `fetch:` with `prerender: true` makes the build fetch it instead); it is never asked of a host's records service and compiles no `/data` file. On a `[id]` page the list still decides which records exist — `record:` fetches the one it found, whole. A site published to a Uniweb host reads the host's records with no configuration at all.
1844
+
1845
+ ⛔ A `fetch:` never carries a source: `path`, `url`, `method`, `body`, `transform`, `detail` and `scope` on a binding stop the build. The `detail:` forms (`rest`, `query`, a URL pattern) are now an external query's `record:`, and `detailUrl:` is `record.url`.
1827
1846
 
1828
- A backend with its own base URL, headers, wire or query language is a **transport**: a named `{ resolve, cacheKey? }` exported by the foundation (or an extension), which the site selects per schema in `fetcher:` — the only thing that block is for:
1847
+ A backend that needs a key, headers, paging, its own wire or query language is a **transport**: a named `{ resolve, cacheKey? }` exported by the foundation (or an extension), which the site selects per data key in `fetcher:` — the only thing that block is for:
1829
1848
 
1830
1849
  ```yaml
1831
1850
  # site.yml
@@ -1841,11 +1860,11 @@ fetcher:
1841
1860
 
1842
1861
  ⛔ `fetcher.baseUrl`, `headers`, `envelope`, `supports` and `request.*` are **retired**: a third party's conventions belong in a transport, not in the runtime every site loads. The build warns once and ignores them.
1843
1862
 
1844
- > **Never put secrets in `site.yml`** — every value in it is public to the browser. Sites needing private credentials proxy through the same origin at the deployment layer, so the site fetches `/api/…` and the proxy attaches the credential server-side.
1863
+ > **Never put secrets in `site.yml` or `queries.yml`** — every value in them is public to the browser. Sites needing private credentials proxy through the same origin at the deployment layer, so the site fetches `/api/…` and the proxy attaches the credential server-side.
1845
1864
 
1846
1865
  **Failures are visible, not empty:** a fetch that failed leaves its key ABSENT from `content.data` and names the message on `block.dataError[key]`; it is never delivered as `[]`, which means "no records". The page still renders — a section reads `dataError` to tell the two apart.
1847
1866
 
1848
- When a plain `url:` is enough and when a transport is the answer: `development/data-sources.md`.
1867
+ When an external query is enough and when a transport is the answer: `development/data-sources.md`.
1849
1868
 
1850
1869
  Full model: `reference/data-fetching.md`. Where-object format with examples: `authoring/predicates.md`.
1851
1870
 
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-09-12T23:21:26.057Z",
3
+ "generatedAt": "2026-09-14T03:15:19.388Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
- "version": "0.3.3",
6
+ "version": "0.3.5",
7
7
  "path": "framework/api",
8
8
  "deps": [
9
9
  "@uniweb/core"
10
10
  ]
11
11
  },
12
12
  "@uniweb/build": {
13
- "version": "0.47.2",
13
+ "version": "0.49.0",
14
14
  "path": "framework/build",
15
15
  "deps": [
16
16
  "@uniweb/content-reader",
@@ -34,7 +34,7 @@
34
34
  "deps": []
35
35
  },
36
36
  "@uniweb/core": {
37
- "version": "0.26.2",
37
+ "version": "0.28.0",
38
38
  "path": "framework/core",
39
39
  "deps": [
40
40
  "@uniweb/semantic-parser",
@@ -47,14 +47,14 @@
47
47
  "deps": []
48
48
  },
49
49
  "@uniweb/icons": {
50
- "version": "0.4.17",
50
+ "version": "0.4.19",
51
51
  "path": "framework/icons",
52
52
  "deps": [
53
53
  "@uniweb/core"
54
54
  ]
55
55
  },
56
56
  "@uniweb/kit": {
57
- "version": "0.18.4",
57
+ "version": "0.18.6",
58
58
  "path": "framework/kit",
59
59
  "deps": [
60
60
  "@uniweb/core",
@@ -74,7 +74,7 @@
74
74
  "deps": []
75
75
  },
76
76
  "@uniweb/projections": {
77
- "version": "0.6.2",
77
+ "version": "0.7.1",
78
78
  "path": "framework/projections",
79
79
  "deps": [
80
80
  "@uniweb/content-writer",
@@ -82,7 +82,7 @@
82
82
  ]
83
83
  },
84
84
  "@uniweb/runtime": {
85
- "version": "0.22.0",
85
+ "version": "0.24.0",
86
86
  "path": "framework/runtime",
87
87
  "deps": [
88
88
  "@uniweb/core",
@@ -110,7 +110,7 @@
110
110
  "deps": []
111
111
  },
112
112
  "@uniweb/templates": {
113
- "version": "0.13.1",
113
+ "version": "0.14.0",
114
114
  "path": "framework/templates",
115
115
  "deps": []
116
116
  },
@@ -120,7 +120,7 @@
120
120
  "deps": []
121
121
  },
122
122
  "@uniweb/unipress": {
123
- "version": "0.9.16",
123
+ "version": "0.9.18",
124
124
  "path": "framework/unipress",
125
125
  "deps": [
126
126
  "@uniweb/build",