uniweb 0.33.0 → 0.34.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 +8 -8
- package/partials/agents.md +73 -38
- package/src/backend/site-sync.js +144 -22
- package/src/commands/build.js +17 -17
- package/src/commands/clone.js +7 -7
- package/src/commands/content.js +2 -3
- package/src/commands/doctor.js +34 -10
- package/src/commands/i18n.js +53 -39
- package/src/commands/publish.js +28 -6
- package/src/commands/pull.js +57 -22
- package/src/commands/push.js +25 -11
- package/src/commands/validate.js +1 -1
- package/src/framework-index.json +8 -8
- package/src/utils/flag-guard.js +2 -2
- package/src/utils/git.js +8 -2
- package/src/utils/records-guard.js +80 -0
- package/src/utils/schemaless-report.js +4 -4
- package/src/utils/site-data-upload.js +2 -2
- package/templates/site/site.yml.hbs +24 -6
package/src/commands/push.js
CHANGED
|
@@ -69,7 +69,7 @@ import { updateAssetMap, ASSET_MAP_FILE } from '@uniweb/build/uwx'
|
|
|
69
69
|
import { BackendClient } from '../backend/client.js'
|
|
70
70
|
import { resolveSiteDir, resolveSiteBackend } from './deploy.js'
|
|
71
71
|
import { warnIfContentDoesNotConform } from '../utils/conformance.js'
|
|
72
|
-
import {
|
|
72
|
+
import { reportSchemalessQueries } from '../utils/schemaless-report.js'
|
|
73
73
|
import { readOrgFlag } from '../utils/args.js'
|
|
74
74
|
import { checkFlags } from '../utils/flag-guard.js'
|
|
75
75
|
import {
|
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
readSiteIdentity
|
|
78
78
|
} from '../utils/site-identity.js'
|
|
79
79
|
import { confirm } from '../utils/interactive.js'
|
|
80
|
+
import { guardEmptyRecords } from '../utils/records-guard.js'
|
|
80
81
|
import { bringFoundationAlong } from '../backend/foundation-bring-along.js'
|
|
81
82
|
import {
|
|
82
83
|
makeModelResolver,
|
|
@@ -85,6 +86,7 @@ import {
|
|
|
85
86
|
readItemBaseVersions,
|
|
86
87
|
readItemUuids,
|
|
87
88
|
readFolderItemUuids,
|
|
89
|
+
readQueryUuids,
|
|
88
90
|
ensureItemUuids,
|
|
89
91
|
ensureSiteExists,
|
|
90
92
|
clearRemoteSyncStateIfUnbound,
|
|
@@ -368,6 +370,15 @@ export async function push(args = [], deps = {}) {
|
|
|
368
370
|
// It also has to be this emit that carries `assetRewrite` below: the push cache
|
|
369
371
|
// stores hashes of the REWRITTEN content, so the emit compared against it must
|
|
370
372
|
// rewrite too, or every entity reads as changed forever.
|
|
373
|
+
// ⛔ AN EMPTY `records.yml` REMOVES. It is the one path where an ordinary act is
|
|
374
|
+
// destructive — a placeholder file, created meaning to fill it in — so the count
|
|
375
|
+
// is reported and confirmed before anything is sent. The format stays honest;
|
|
376
|
+
// the asking happens here.
|
|
377
|
+
if (!dryRun) {
|
|
378
|
+
const guard = await guardEmptyRecords({ siteDir, args, warn, note })
|
|
379
|
+
if (!guard.ok) return { exitCode: 1 }
|
|
380
|
+
}
|
|
381
|
+
|
|
371
382
|
let assetRewrite = null
|
|
372
383
|
let assetIds = null
|
|
373
384
|
if (!output && !dryRun) {
|
|
@@ -474,6 +485,9 @@ export async function push(args = [], deps = {}) {
|
|
|
474
485
|
pkg = await emitSyncPackages(siteDir, {
|
|
475
486
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
476
487
|
folderItemUuids: readFolderItemUuids(siteDir),
|
|
488
|
+
// Identity for the `queries` section — see readQueryUuids. Keyed by
|
|
489
|
+
// name, because a declaration has no file for a path-keyed map to hold.
|
|
490
|
+
queryUuids: readQueryUuids(siteDir),
|
|
477
491
|
// Resolves a foundation-relative `@/x` model ref into `@org/x`.
|
|
478
492
|
...(asOrg ? { org: asOrg } : {}),
|
|
479
493
|
...(foundationDir ? { foundationDir } : {}),
|
|
@@ -505,14 +519,14 @@ export async function push(args = [], deps = {}) {
|
|
|
505
519
|
error(`Could not build the sync package: ${err.message}`)
|
|
506
520
|
return { exitCode: 2 }
|
|
507
521
|
}
|
|
508
|
-
const { siteContent,
|
|
522
|
+
const { siteContent, records, siteContentUuid, warnings, skipped } = pkg
|
|
509
523
|
log('')
|
|
510
524
|
for (const w of warnings) note(`! ${w}`)
|
|
511
525
|
// Warn level, not dim: this is the author choosing entities vs static files.
|
|
512
|
-
|
|
526
|
+
reportSchemalessQueries(pkg.schemaless, { warn, dim: note })
|
|
513
527
|
|
|
514
528
|
const totalEntities =
|
|
515
|
-
(siteContent?.entityCount || 0) + (
|
|
529
|
+
(siteContent?.entityCount || 0) + (records?.entityCount || 0)
|
|
516
530
|
|
|
517
531
|
// Nothing changed since the last push — the backend is already up to date.
|
|
518
532
|
if (totalEntities === 0) {
|
|
@@ -525,10 +539,10 @@ export async function push(args = [], deps = {}) {
|
|
|
525
539
|
info(
|
|
526
540
|
`${colors.bright}site-content${colors.reset} → ${siteContent.models.join(', ')}`
|
|
527
541
|
)
|
|
528
|
-
if (
|
|
529
|
-
const n =
|
|
542
|
+
if (records) {
|
|
543
|
+
const n = records.entityCount
|
|
530
544
|
info(
|
|
531
|
-
`${colors.bright}
|
|
545
|
+
`${colors.bright}records${colors.reset} (${n} entit${n === 1 ? 'y' : 'ies'}) → ${records.models.join(', ')}`
|
|
532
546
|
)
|
|
533
547
|
}
|
|
534
548
|
if (skipped) note(`${skipped} unchanged, skipped`)
|
|
@@ -538,11 +552,11 @@ export async function push(args = [], deps = {}) {
|
|
|
538
552
|
const base = output.replace(/\.uwx$/, '')
|
|
539
553
|
if (siteContent)
|
|
540
554
|
writeFileSync(resolve(`${base}.site-content.uwx`), siteContent.buffer)
|
|
541
|
-
if (
|
|
542
|
-
writeFileSync(resolve(`${base}.
|
|
555
|
+
if (records)
|
|
556
|
+
writeFileSync(resolve(`${base}.records.uwx`), records.buffer)
|
|
543
557
|
const lanes = [
|
|
544
558
|
siteContent && 'site-content',
|
|
545
|
-
|
|
559
|
+
records && 'records'
|
|
546
560
|
].filter(Boolean)
|
|
547
561
|
success(`Wrote ${lanes.join(' + ')} .uwx — not submitted`)
|
|
548
562
|
return { exitCode: 0 }
|
|
@@ -554,7 +568,7 @@ export async function push(args = [], deps = {}) {
|
|
|
554
568
|
`Dry run — would ${verb} content at ${colors.dim}${client.origin}${colors.reset}`
|
|
555
569
|
)
|
|
556
570
|
}
|
|
557
|
-
if (
|
|
571
|
+
if (records) {
|
|
558
572
|
info(
|
|
559
573
|
`Dry run — would push the folder at ${colors.dim}${client.origin}${colors.reset}`
|
|
560
574
|
)
|
package/src/commands/validate.js
CHANGED
|
@@ -269,7 +269,7 @@ export async function validate(args = []) {
|
|
|
269
269
|
return { exitCode: 0 }
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
// The data pipeline (collectSiteContent /
|
|
272
|
+
// The data pipeline (collectSiteContent / processQueries) prints progress
|
|
273
273
|
// via console.log. Route that to stderr while the engine runs so stdout stays
|
|
274
274
|
// clean — pure JSON for `--json`, just the report otherwise. `log` captured
|
|
275
275
|
// the original stdout writer at module load, so our own output is unaffected.
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-29T19:59:22.061Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.30.0",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"deps": []
|
|
28
28
|
},
|
|
29
29
|
"@uniweb/core": {
|
|
30
|
-
"version": "0.
|
|
30
|
+
"version": "0.14.0",
|
|
31
31
|
"path": "framework/core",
|
|
32
32
|
"deps": [
|
|
33
33
|
"@uniweb/semantic-parser",
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"deps": []
|
|
41
41
|
},
|
|
42
42
|
"@uniweb/icons": {
|
|
43
|
-
"version": "0.4.
|
|
43
|
+
"version": "0.4.5",
|
|
44
44
|
"path": "framework/icons",
|
|
45
45
|
"deps": [
|
|
46
46
|
"@uniweb/core"
|
|
47
47
|
]
|
|
48
48
|
},
|
|
49
49
|
"@uniweb/kit": {
|
|
50
|
-
"version": "0.
|
|
50
|
+
"version": "0.15.0",
|
|
51
51
|
"path": "framework/kit",
|
|
52
52
|
"deps": [
|
|
53
53
|
"@uniweb/core",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"deps": []
|
|
67
67
|
},
|
|
68
68
|
"@uniweb/projections": {
|
|
69
|
-
"version": "0.5.
|
|
69
|
+
"version": "0.5.2",
|
|
70
70
|
"path": "framework/projections",
|
|
71
71
|
"deps": [
|
|
72
72
|
"@uniweb/content-writer",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
]
|
|
75
75
|
},
|
|
76
76
|
"@uniweb/runtime": {
|
|
77
|
-
"version": "0.13.
|
|
77
|
+
"version": "0.13.2",
|
|
78
78
|
"path": "framework/runtime",
|
|
79
79
|
"deps": [
|
|
80
80
|
"@uniweb/core",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"deps": []
|
|
113
113
|
},
|
|
114
114
|
"@uniweb/unipress": {
|
|
115
|
-
"version": "0.8.
|
|
115
|
+
"version": "0.8.16",
|
|
116
116
|
"path": "framework/unipress",
|
|
117
117
|
"deps": [
|
|
118
118
|
"@uniweb/build",
|
package/src/utils/flag-guard.js
CHANGED
|
@@ -77,12 +77,12 @@ const VERBS = {
|
|
|
77
77
|
pull: [
|
|
78
78
|
'--backend', '--content-only', '--dry-run', '--force', '--merge',
|
|
79
79
|
'--no-assets',
|
|
80
|
-
'--no-
|
|
80
|
+
'--no-records', '--no-delete', '--no-prune', '--registry', '--token',
|
|
81
81
|
// via backend/site-sync.js (the owner resolver) and utils/conformance.js
|
|
82
82
|
'--yes', '--org', '--as-org', '--no-validate', ...VIA_DEPLOY
|
|
83
83
|
],
|
|
84
84
|
clone: [
|
|
85
|
-
'--backend', '--content-only', '--no-assets', '--no-
|
|
85
|
+
'--backend', '--content-only', '--no-assets', '--no-records', '--path',
|
|
86
86
|
'--project', '--registry', '--token', '--org', '--as-org'
|
|
87
87
|
],
|
|
88
88
|
register: [
|
package/src/utils/git.js
CHANGED
|
@@ -32,11 +32,17 @@ import yaml from 'js-yaml'
|
|
|
32
32
|
* rather than assuming the defaults.
|
|
33
33
|
*/
|
|
34
34
|
export function siteContentRoots(siteDir) {
|
|
35
|
+
// ⚠️ `queries.yml` IS AT THE SITE ROOT, and that is why it must be named here.
|
|
36
|
+
// Its predecessor lived at `collections/collections.yml`, so the `collections`
|
|
37
|
+
// root added below already covered it and the bare `'collections.yml'` entry
|
|
38
|
+
// that used to sit in this list resolved to a path no site ever had. A
|
|
39
|
+
// root-level file has no directory entry standing in for it.
|
|
35
40
|
const roots = new Set([
|
|
36
41
|
'site.yml',
|
|
37
42
|
'theme.yml',
|
|
38
43
|
'head.html',
|
|
39
|
-
'
|
|
44
|
+
'queries.yml',
|
|
45
|
+
'records.yml',
|
|
40
46
|
'locales'
|
|
41
47
|
])
|
|
42
48
|
let paths = {}
|
|
@@ -48,7 +54,7 @@ export function siteContentRoots(siteDir) {
|
|
|
48
54
|
}
|
|
49
55
|
roots.add(paths.pages || 'pages')
|
|
50
56
|
roots.add(paths.layout || 'layout')
|
|
51
|
-
roots.add(paths.
|
|
57
|
+
roots.add(paths.entities || 'entities')
|
|
52
58
|
return [...roots]
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// ⛔ THE ONE PLACE AN ORDINARY ACT IS DESTRUCTIVE.
|
|
2
|
+
//
|
|
3
|
+
// `records.yml` is the sync control, and `missing` and `empty` deliberately mean
|
|
4
|
+
// different things: missing leaves the server's folder untouched, empty says the
|
|
5
|
+
// folder holds nothing and the backend removes what is there. The asymmetry is
|
|
6
|
+
// well-shaped — the safe state is the ABSENCE of a file, so a live folder cannot
|
|
7
|
+
// be wiped by deleting one, and the destructive act requires affirmatively
|
|
8
|
+
// creating one.
|
|
9
|
+
//
|
|
10
|
+
// ⚠️ WHICH LEAVES EXACTLY ONE SHARP EDGE: a PLACEHOLDER. Someone creates an empty
|
|
11
|
+
// `records.yml` intending to fill it in, pushes, and the live folder empties.
|
|
12
|
+
// That is plausible and it is the only path where a normal act destroys content.
|
|
13
|
+
//
|
|
14
|
+
// ⭐ THE FORMAT STAYS HONEST AND THE CLI DOES THE ASKING. Never make "empty" mean
|
|
15
|
+
// "missing" to dodge this: that would delete a capability to avoid writing a
|
|
16
|
+
// prompt.
|
|
17
|
+
//
|
|
18
|
+
// The count comes from the placement identity a previous push banked — what WE
|
|
19
|
+
// last saw the folder hold. It needs no network call, and it is the right source:
|
|
20
|
+
// a site that has never pushed has nothing to lose and is never asked.
|
|
21
|
+
|
|
22
|
+
import { readRecordsConfig, FOLDER_EMPTY } from '@uniweb/build/uwx'
|
|
23
|
+
import { readFolderItemUuids } from '../backend/site-sync.js'
|
|
24
|
+
import { confirm, isNonInteractive, getCliPrefix } from './interactive.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Leaf placements in a banked path→uuid map.
|
|
28
|
+
*
|
|
29
|
+
* A branch's path is a prefix of every path beneath it, so anything that is a
|
|
30
|
+
* prefix of another key is a folder rather than a record. Counting raw keys would
|
|
31
|
+
* report a two-record site inside one folder as three things to lose.
|
|
32
|
+
*/
|
|
33
|
+
export function countPlacedRecords(pathToUuid) {
|
|
34
|
+
const paths = Object.keys(pathToUuid || {})
|
|
35
|
+
return paths.filter((p) => !paths.some((q) => q !== p && q.startsWith(`${p}/`))).length
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stop an empty `records.yml` from silently emptying a live folder.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} params
|
|
42
|
+
* @param {string} params.siteDir
|
|
43
|
+
* @param {string[]} params.args - the verb's argv, for --yes / non-interactive
|
|
44
|
+
* @param {(m: string) => void} params.warn - the CALLER's reporter. Each verb owns
|
|
45
|
+
* its own output style; a second copy here would drift from all of them.
|
|
46
|
+
* @param {(m: string) => void} params.note
|
|
47
|
+
* @returns {Promise<{ ok: boolean, count: number }>} `ok: false` means abort
|
|
48
|
+
*/
|
|
49
|
+
export async function guardEmptyRecords({ siteDir, args = [], warn, note }) {
|
|
50
|
+
const cfg = await readRecordsConfig(siteDir)
|
|
51
|
+
if (cfg.state !== FOLDER_EMPTY) return { ok: true, count: 0 }
|
|
52
|
+
|
|
53
|
+
const count = countPlacedRecords(readFolderItemUuids(siteDir))
|
|
54
|
+
// Nothing banked ⇒ nothing this push can remove. A first push of an empty
|
|
55
|
+
// folder is a legitimate (if odd) thing to do, and asking about it would train
|
|
56
|
+
// people to type y.
|
|
57
|
+
if (count === 0) return { ok: true, count: 0 }
|
|
58
|
+
|
|
59
|
+
warn(
|
|
60
|
+
`records.yml is empty, and this push would REMOVE ${count} record${count === 1 ? '' : 's'} ` +
|
|
61
|
+
`from the live folder.`
|
|
62
|
+
)
|
|
63
|
+
note(
|
|
64
|
+
'An empty records.yml means "the folder holds nothing" — it is not the same as ' +
|
|
65
|
+
'having no records.yml, which leaves the live folder alone. If you meant to ' +
|
|
66
|
+
'start listing records, delete the file until you have.'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// ⚠️ `--yes` ONLY. `-y` is not a flag this CLI has anywhere, and adding one here
|
|
70
|
+
// would have been caught by `flag-guard-coverage.test.js` — which it was.
|
|
71
|
+
if (args.includes('--yes')) return { ok: true, count }
|
|
72
|
+
if (isNonInteractive(args)) {
|
|
73
|
+
warn(`Refusing to remove ${count} record${count === 1 ? '' : 's'} without confirmation.`)
|
|
74
|
+
note(`Re-run with --yes if that is what you want: ${getCliPrefix()} push --yes`)
|
|
75
|
+
return { ok: false, count }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const yes = await confirm(`Remove ${count} record${count === 1 ? '' : 's'} from the live folder?`, false)
|
|
79
|
+
return { ok: yes, count }
|
|
80
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Report
|
|
2
|
+
* Report queries publishing without a data schema.
|
|
3
3
|
*
|
|
4
4
|
* ⭐ This is a PRODUCT decision the author is making, usually without knowing:
|
|
5
5
|
* entities or static files. It is reported at warn level for that reason — the
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* @param {Array<{name: string, model?: string}>} schemaless
|
|
15
15
|
* @param {{ warn: (m: string) => void, dim: (m: string) => void }} out
|
|
16
16
|
*/
|
|
17
|
-
export function
|
|
17
|
+
export function reportSchemalessQueries(schemaless, out) {
|
|
18
18
|
if (!schemaless?.length) return
|
|
19
19
|
const names = schemaless.map((c) => c.name)
|
|
20
|
-
const label = names.length === 1 ? '
|
|
20
|
+
const label = names.length === 1 ? 'query' : 'queries'
|
|
21
21
|
out.warn(
|
|
22
22
|
`${names.length} ${label} shipping as STATIC FILES, not entities: ${names.join(', ')}`
|
|
23
23
|
)
|
|
@@ -25,6 +25,6 @@ export function reportSchemalessCollections(schemaless, out) {
|
|
|
25
25
|
out.dim(
|
|
26
26
|
`Declare a data schema to get entities. Each resolves its schema by subfolder name (${schemaless
|
|
27
27
|
.map((c) => `${c.name} → ${c.model || c.name}`)
|
|
28
|
-
.join(', ')}), or set \`schema:\` on the
|
|
28
|
+
.join(', ')}), or set \`schema:\` on the query.`
|
|
29
29
|
)
|
|
30
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static collection data — the passthrough lane.
|
|
3
3
|
*
|
|
4
|
-
* A site's **schema-less**
|
|
4
|
+
* A site's **schema-less** queries have no entity model, so their compiled
|
|
5
5
|
* `dist/data/**` JSON is delivered as files rather than synced as entities.
|
|
6
6
|
* This plans and uploads that set: one plan call, one object per file, each
|
|
7
7
|
* PUT to the target the backend returns.
|
|
@@ -93,7 +93,7 @@ export async function uploadSiteData({
|
|
|
93
93
|
//
|
|
94
94
|
// ⛔ And the failure is INVISIBLE from here: the plan succeeds, the PUT
|
|
95
95
|
// succeeds, and only a visitor's fetch 404s. Confirmed against the
|
|
96
|
-
// shipped contract's own example (`
|
|
96
|
+
// shipped contract's own example (`data/articles.json` with a
|
|
97
97
|
// `/data/`-bearing `serve_base`), which supersedes an earlier
|
|
98
98
|
// parenthetical that showed the prefix.
|
|
99
99
|
path: relPath,
|
|
@@ -25,20 +25,38 @@ index: home
|
|
|
25
25
|
# pages/docs: ../../../docs # Mount docs repo at /docs route
|
|
26
26
|
# pages/blog: ../../blog-content # Mount blog content at /blog route
|
|
27
27
|
# layout: ./custom-layout # Custom layout directory
|
|
28
|
-
#
|
|
28
|
+
# entities: ./data # Where this site's entities live
|
|
29
29
|
#
|
|
30
30
|
# To give a mounted route a layout or a title, add a local folder for it
|
|
31
31
|
# (pages/docs/) holding a folder.yml — folder.yml, not page.yml, because the
|
|
32
32
|
# mounted directory is a folder of pages. Whatever that stub leaves unset comes
|
|
33
33
|
# from the mounted directory's own folder.yml, so it need only say what differs.
|
|
34
34
|
|
|
35
|
-
# ───
|
|
36
|
-
#
|
|
37
|
-
# Pages and sections reference sources by name via `data: source-name`.
|
|
35
|
+
# ─── Structured Content ───────────────────────────────────────────────────────
|
|
36
|
+
# Three things, and they are separate on purpose:
|
|
38
37
|
#
|
|
39
|
-
#
|
|
38
|
+
# entities/{schema}/ your stored records. The folder names their data schema:
|
|
39
|
+
# entities/article/ → @/article (your foundation's)
|
|
40
|
+
# entities/std/person/ → @std/person
|
|
41
|
+
# records.yml WHAT IS PUBLISHED. Listing an entity here is what makes it
|
|
42
|
+
# a record; anything you leave out is a draft. Usually three
|
|
43
|
+
# lines:
|
|
44
|
+
# - article/*.md
|
|
45
|
+
# - person/*.md
|
|
46
|
+
# queries.yml HOW CONTENT IS REACHED — named queries over those records:
|
|
47
|
+
# recent:
|
|
48
|
+
# schema: '@/article'
|
|
49
|
+
# sort: date desc
|
|
50
|
+
# limit: 10
|
|
51
|
+
#
|
|
52
|
+
# Pages and sections then name a query: `data: recent`, or
|
|
53
|
+
# `fetch: { query: recent }`.
|
|
54
|
+
#
|
|
55
|
+
# You can also keep queries here instead of in queries.yml:
|
|
56
|
+
#
|
|
57
|
+
# queries:
|
|
40
58
|
# articles:
|
|
41
|
-
#
|
|
59
|
+
# schema: '@/article'
|
|
42
60
|
# sort: date desc # Sort by frontmatter field
|
|
43
61
|
#
|
|
44
62
|
# fetch:
|