uniweb 0.60.0 → 0.62.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 +27 -36
- package/src/backend/asset-download.js +5 -5
- package/src/backend/foundation-bring-along.js +72 -60
- package/src/backend/service-request.js +2 -1
- package/src/backend/site-media.js +6 -6
- package/src/backend/site-sync.js +106 -64
- package/src/commands/add.js +62 -4
- package/src/commands/clone.js +1 -1
- package/src/commands/doctor.js +6 -7
- package/src/commands/i18n.js +19 -10
- package/src/commands/publish.js +10 -4
- package/src/commands/pull.js +16 -6
- package/src/commands/push.js +7 -7
- package/src/commands/register.js +126 -21
- package/src/commands/rename.js +12 -9
- package/src/commands/status.js +6 -2
- package/src/framework-index.json +7 -7
- package/src/index.js +30 -3
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +5 -2
- package/src/utils/records-guard.js +35 -28
- package/src/utils/registry-orgs.js +19 -0
- package/src/utils/scaffold.js +7 -5
- package/starter/foundation/main.js +2 -1
- package/templates/foundation/main.js.hbs +9 -0
- package/templates/site/site.yml.hbs +12 -9
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Giving a foundation its name — `name` in `main.js`'s default export.
|
|
3
|
+
*
|
|
4
|
+
* The rule itself (main.js `name`, else package.json `name`; `src` and `foundation`
|
|
5
|
+
* refused) is `@uniweb/build`'s `foundation-name.js`, shared by the build, `register`
|
|
6
|
+
* and `push`. This module only WRITES the name: the scaffold at `create` / `add`, and
|
|
7
|
+
* `register` when it asks for one.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ NO `@uniweb/build` IMPORT, AND NO IMPORT OF main.js. `uniweb create` runs before
|
|
10
|
+
* any project exists: `@uniweb/build` is an optional peer that may not be installed,
|
|
11
|
+
* and main.js's own imports may not be either. Hence text scans rather than a load,
|
|
12
|
+
* and the one duplicated fact below, pinned to the build's list by
|
|
13
|
+
* `test/foundation-name.test.js`. What a foundation IS named is always read by the
|
|
14
|
+
* build (`readFoundationName`); these only decide where to write.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { join, resolve, basename, dirname } from 'node:path'
|
|
19
|
+
|
|
20
|
+
// The folders a foundation's code lives in — never a foundation's name
|
|
21
|
+
// (`@uniweb/build`'s FORBIDDEN_FOUNDATION_NAMES; the test keeps them equal).
|
|
22
|
+
export const SCAFFOLD_FOLDER_NAMES = new Set(['src', 'foundation'])
|
|
23
|
+
|
|
24
|
+
// Written above the name wherever the CLI adds one — the scaffold's main.js
|
|
25
|
+
// template says the same.
|
|
26
|
+
const NAME_COMMENT = '// What this foundation registers as — @<org>/<name> — and what sites pin.'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A name made from free text — a project's name, a folder's — in the form a
|
|
30
|
+
* foundation name takes: lowercase letters, digits and inner hyphens.
|
|
31
|
+
* `My Site` → `my-site`. Null when nothing usable is left, or when what is left
|
|
32
|
+
* names a folder rather than a foundation.
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} raw
|
|
35
|
+
* @returns {string|null}
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeFoundationName(raw) {
|
|
38
|
+
const name = String(raw ?? '')
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/^@[^/]*\//, '')
|
|
42
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
43
|
+
.replace(/^-+|-+$/g, '')
|
|
44
|
+
return name && !SCAFFOLD_FOLDER_NAMES.has(name) ? name : null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A name to offer a foundation that has none of its own: its folder's — or, for
|
|
49
|
+
* the `src/` and `foundation/` a scaffold makes, its project's folder. Null when
|
|
50
|
+
* neither makes one.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} foundationDir
|
|
53
|
+
* @returns {string|null}
|
|
54
|
+
*/
|
|
55
|
+
export function suggestFoundationName(foundationDir) {
|
|
56
|
+
const dir = resolve(foundationDir)
|
|
57
|
+
return (
|
|
58
|
+
normalizeFoundationName(basename(dir)) ||
|
|
59
|
+
normalizeFoundationName(basename(dirname(dir)))
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether `main.js`'s default export names the foundation — a `name` key at the
|
|
65
|
+
* export's own level, not one nested in `props` or `vars`.
|
|
66
|
+
*
|
|
67
|
+
* A scan, not a parse (see the module note): it skips strings and comments and
|
|
68
|
+
* counts brackets, which is all an object literal of declarations needs. A name
|
|
69
|
+
* that arrives by a spread is invisible to it — the build's read is the truth.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} text - main.js source
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function mainNamesFoundation(text) {
|
|
75
|
+
const opening = /export\s+default\s*\{/.exec(text)
|
|
76
|
+
if (!opening) return false
|
|
77
|
+
let depth = 1
|
|
78
|
+
let atKey = true // just after the opening brace, or a comma at the top level
|
|
79
|
+
for (let i = opening.index + opening[0].length; i < text.length && depth > 0; i++) {
|
|
80
|
+
const c = text[i]
|
|
81
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
82
|
+
i = text.indexOf('\n', i)
|
|
83
|
+
if (i < 0) break
|
|
84
|
+
} else if (c === '/' && text[i + 1] === '*') {
|
|
85
|
+
i = text.indexOf('*/', i + 2) + 1
|
|
86
|
+
if (i <= 0) break
|
|
87
|
+
} else if (c === "'" || c === '"' || c === '`') {
|
|
88
|
+
let j = i + 1
|
|
89
|
+
while (j < text.length && text[j] !== c) j += text[j] === '\\' ? 2 : 1
|
|
90
|
+
if (depth === 1 && atKey && text.slice(i + 1, j) === 'name' && /^\s*:/.test(text.slice(j + 1))) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
i = j
|
|
94
|
+
atKey = false
|
|
95
|
+
} else if ('{[('.includes(c)) {
|
|
96
|
+
depth++
|
|
97
|
+
atKey = false
|
|
98
|
+
} else if ('}])'.includes(c)) {
|
|
99
|
+
depth--
|
|
100
|
+
} else if (depth === 1 && c === ',') {
|
|
101
|
+
atKey = true
|
|
102
|
+
} else if (depth === 1 && atKey && /[A-Za-z_$]/.test(c)) {
|
|
103
|
+
const word = /^[A-Za-z_$][\w$]*/.exec(text.slice(i))[0]
|
|
104
|
+
// `name: 'x'`, or the shorthand `{ name }`
|
|
105
|
+
if (word === 'name' && /^\s*[:,}]/.test(text.slice(i + word.length))) return true
|
|
106
|
+
i += word.length - 1
|
|
107
|
+
atKey = false
|
|
108
|
+
} else if (!/\s/.test(c)) {
|
|
109
|
+
atKey = false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Write `name` into a foundation's `main.js`.
|
|
117
|
+
*
|
|
118
|
+
* With `replace`, the one `name: '<replace>'` literal is rewritten — the case where
|
|
119
|
+
* main.js already names the foundation, with a name that cannot register. Without
|
|
120
|
+
* it, `name` becomes the default export's first key. A missing main.js is created.
|
|
121
|
+
*
|
|
122
|
+
* ⚠️ A text edit, not a parse: it handles `export default { … }` — every scaffold
|
|
123
|
+
* and every template — and says so otherwise rather than guessing.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} mainFile
|
|
126
|
+
* @param {string} name - already a valid foundation name
|
|
127
|
+
* @param {{ replace?: string|null }} [opts]
|
|
128
|
+
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
129
|
+
*/
|
|
130
|
+
export function writeFoundationName(mainFile, name, { replace = null } = {}) {
|
|
131
|
+
const line = `${NAME_COMMENT}\n name: '${name}',`
|
|
132
|
+
if (!existsSync(mainFile)) {
|
|
133
|
+
writeFileSync(mainFile, `export default {\n ${line}\n}\n`)
|
|
134
|
+
return { ok: true }
|
|
135
|
+
}
|
|
136
|
+
const text = readFileSync(mainFile, 'utf8')
|
|
137
|
+
|
|
138
|
+
if (replace !== null) {
|
|
139
|
+
const literal = new RegExp(
|
|
140
|
+
`(\\bname\\s*:\\s*)(['"\`])${replace.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\2`,
|
|
141
|
+
'g'
|
|
142
|
+
)
|
|
143
|
+
const hits = text.match(literal) || []
|
|
144
|
+
if (hits.length !== 1) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
reason: `main.js does not name the foundation "${replace}" in exactly one place`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
writeFileSync(mainFile, text.replace(literal, `$1'${name}'`))
|
|
151
|
+
return { ok: true }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const opening = /export\s+default\s*\{/.exec(text)
|
|
155
|
+
if (!opening) {
|
|
156
|
+
return { ok: false, reason: 'main.js has no `export default { … }` to add a name to' }
|
|
157
|
+
}
|
|
158
|
+
// The name leads the export — on lines of its own, so a multi-line export keeps
|
|
159
|
+
// its first line and gains a blank one between.
|
|
160
|
+
const at = opening.index + opening[0].length
|
|
161
|
+
writeFileSync(mainFile, `${text.slice(0, at)}\n ${line}\n${text.slice(at)}`)
|
|
162
|
+
return { ok: true }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Give a freshly scaffolded foundation its name, unless its main.js already has one.
|
|
167
|
+
*
|
|
168
|
+
* Called after template content is applied, because a template's own main.js
|
|
169
|
+
* replaces the scaffolded one — and with it the name the scaffold wrote.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} foundationDir - the scaffolded package (flat layout: main.js at its root)
|
|
172
|
+
* @param {string|null} name
|
|
173
|
+
*/
|
|
174
|
+
export function ensureFoundationName(foundationDir, name) {
|
|
175
|
+
if (!name) return
|
|
176
|
+
const mainFile = join(foundationDir, 'main.js')
|
|
177
|
+
if (existsSync(mainFile) && mainNamesFoundation(readFileSync(mainFile, 'utf8'))) return
|
|
178
|
+
writeFoundationName(mainFile, name)
|
|
179
|
+
}
|
package/src/utils/git.js
CHANGED
|
@@ -37,12 +37,15 @@ export function siteContentRoots(siteDir) {
|
|
|
37
37
|
// root added below already covered it and the bare `'collections.yml'` entry
|
|
38
38
|
// that used to sit in this list resolved to a path no site ever had. A
|
|
39
39
|
// root-level file has no directory entry standing in for it.
|
|
40
|
+
//
|
|
41
|
+
// The records folder's `folder.yml` is the reverse case: it lives in the records
|
|
42
|
+
// directory, which is a root below, so it needs no entry. (It was `records.yml` at
|
|
43
|
+
// the site root until 2026-09-21, and was listed here then.)
|
|
40
44
|
const roots = new Set([
|
|
41
45
|
'site.yml',
|
|
42
46
|
'theme.yml',
|
|
43
47
|
'head.html',
|
|
44
48
|
'queries.yml',
|
|
45
|
-
'records.yml',
|
|
46
49
|
'locales'
|
|
47
50
|
])
|
|
48
51
|
let paths = {}
|
|
@@ -54,7 +57,7 @@ export function siteContentRoots(siteDir) {
|
|
|
54
57
|
}
|
|
55
58
|
roots.add(paths.pages || 'pages')
|
|
56
59
|
roots.add(paths.layout || 'layout')
|
|
57
|
-
roots.add(paths.
|
|
60
|
+
roots.add(paths.records || 'records')
|
|
58
61
|
return [...roots]
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -1,42 +1,47 @@
|
|
|
1
1
|
// ⛔ THE ONE PLACE AN ORDINARY ACT IS DESTRUCTIVE.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// well-shaped — the safe state is the ABSENCE of
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// A push sends the site's records folder whole, and it REPLACES the backend's. A site
|
|
4
|
+
// with no records directory sends none and leaves the backend's alone; a records
|
|
5
|
+
// directory that holds no records sends an EMPTY folder, and the backend removes what
|
|
6
|
+
// is there. The asymmetry is well-shaped — the safe state is the ABSENCE of the
|
|
7
|
+
// directory, so a site whose records live only on the backend is never emptied by a
|
|
8
|
+
// push of its pages — and it leaves exactly one sharp edge: a directory emptied by
|
|
9
|
+
// accident, or kept with only a placeholder (`records/.gitkeep`), and pushed.
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// That is plausible and it is the only path where a normal act destroys content.
|
|
11
|
+
// ⭐ THE CLI DOES THE ASKING. Never make "empty" mean "missing" to dodge this: that
|
|
12
|
+
// would delete a capability to avoid writing a prompt.
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// ⛔ Until 2026-09-21 both states were `records.yml`'s — missing and empty — because
|
|
15
|
+
// that file listed the records. The directory holds them now (`@uniweb/build`'s
|
|
16
|
+
// `sendsFolder` decides what a push sends, and this asks about exactly its empty case).
|
|
17
17
|
//
|
|
18
18
|
// The count comes from the placement identity a previous push banked — what WE
|
|
19
19
|
// last saw the folder hold. It needs no network call, and it is the right source:
|
|
20
20
|
// a site that has never pushed has nothing to lose and is never asked.
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { readEntityPool } from '@uniweb/build/uwx'
|
|
23
23
|
import { readFolderItemUuids } from '../backend/site-sync.js'
|
|
24
24
|
import { confirm, isNonInteractive, getCliPrefix } from './interactive.js'
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* Record placements in a banked placement map.
|
|
28
28
|
*
|
|
29
|
-
* A
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* ⭐ A placement banked since 2026-09-21 is keyed by the record it references —
|
|
30
|
+
* `@<record uuid>`, one per record — so where those exist they ARE the count. A map
|
|
31
|
+
* banked before holds `name` chains only: a branch's path is a prefix of every path
|
|
32
|
+
* beneath it, so anything that is a prefix of another key is a folder rather than a
|
|
33
|
+
* record. Counting raw keys would report a two-record site inside one folder as
|
|
34
|
+
* three things to lose — or, with both kinds of key, each record twice.
|
|
32
35
|
*/
|
|
33
36
|
export function countPlacedRecords(pathToUuid) {
|
|
34
|
-
const
|
|
35
|
-
|
|
37
|
+
const keys = Object.keys(pathToUuid || {})
|
|
38
|
+
const byRecord = keys.filter((k) => k.startsWith('@'))
|
|
39
|
+
if (byRecord.length) return byRecord.length
|
|
40
|
+
return keys.filter((p) => !keys.some((q) => q !== p && q.startsWith(`${p}/`))).length
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
|
-
* Stop an empty
|
|
44
|
+
* Stop an empty records directory from silently emptying the backend's folder.
|
|
40
45
|
*
|
|
41
46
|
* @param {object} params
|
|
42
47
|
* @param {string} params.siteDir
|
|
@@ -48,8 +53,10 @@ export function countPlacedRecords(pathToUuid) {
|
|
|
48
53
|
* @returns {Promise<{ ok: boolean, count: number }>} `ok: false` means abort
|
|
49
54
|
*/
|
|
50
55
|
export async function guardEmptyRecords({ siteDir, backend, args = [], warn, note }) {
|
|
51
|
-
const
|
|
52
|
-
|
|
56
|
+
const pool = await readEntityPool(siteDir)
|
|
57
|
+
// Only a directory that is there and holds nothing at all — no records, and no
|
|
58
|
+
// files that fail to be records — sends the empty folder.
|
|
59
|
+
if (!pool.exists || pool.entities.length > 0 || pool.errors.length > 0) return { ok: true, count: 0 }
|
|
53
60
|
|
|
54
61
|
const count = countPlacedRecords(readFolderItemUuids(siteDir, backend))
|
|
55
62
|
// Nothing banked ⇒ nothing this push can remove. A first push of an empty
|
|
@@ -58,13 +65,13 @@ export async function guardEmptyRecords({ siteDir, backend, args = [], warn, not
|
|
|
58
65
|
if (count === 0) return { ok: true, count: 0 }
|
|
59
66
|
|
|
60
67
|
warn(
|
|
61
|
-
|
|
62
|
-
`from the
|
|
68
|
+
`${pool.dir}/ holds no records, and this push would REMOVE ${count} record${count === 1 ? '' : 's'} ` +
|
|
69
|
+
`from the site's folder on the backend.`
|
|
63
70
|
)
|
|
64
71
|
note(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
`An empty ${pool.dir}/ means "the folder holds nothing" — it is not the same as having no ` +
|
|
73
|
+
`${pool.dir}/ at all, which leaves the backend's folder alone. If you did not mean to remove ` +
|
|
74
|
+
`them, put the files back (or pull) before pushing.`
|
|
68
75
|
)
|
|
69
76
|
|
|
70
77
|
// ⚠️ `--yes` ONLY. `-y` is not a flag this CLI has anywhere, and adding one here
|
|
@@ -76,6 +83,6 @@ export async function guardEmptyRecords({ siteDir, backend, args = [], warn, not
|
|
|
76
83
|
return { ok: false, count }
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
const yes = await confirm(`Remove ${count} record${count === 1 ? '' : 's'} from the
|
|
86
|
+
const yes = await confirm(`Remove ${count} record${count === 1 ? '' : 's'} from the site's folder?`, false)
|
|
80
87
|
return { ok: yes, count }
|
|
81
88
|
}
|
|
@@ -59,6 +59,25 @@ export function bareHandle(scope) {
|
|
|
59
59
|
.replace(/\/.*$/, '')
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A publish scope in the one form the registry names things by — `@acme` — from any
|
|
64
|
+
* spelling `--scope` or `package.json::uniweb.scope` may carry it in (`@acme`, `acme`,
|
|
65
|
+
* `@acme/…`). Null when there is no handle in it.
|
|
66
|
+
*
|
|
67
|
+
* ⛔ Every name built from a scope goes through this. The `.uwx` assembly
|
|
68
|
+
* (`@uniweb/build`'s `buildRegistryPackage`) has always accepted `acme` as well as
|
|
69
|
+
* `@acme`, so a name composed from the RAW value disagreed with the one it registered:
|
|
70
|
+
* until 2026-09-21 `register --scope std` registered `@std/src` and then asked to
|
|
71
|
+
* deliver code for `std/src`, which the registry refuses (a scoped name is `@org/name`).
|
|
72
|
+
*
|
|
73
|
+
* @param {unknown} value
|
|
74
|
+
* @returns {string|null}
|
|
75
|
+
*/
|
|
76
|
+
export function publishScope(value) {
|
|
77
|
+
const handle = bareHandle(value)
|
|
78
|
+
return handle ? `@${handle}` : null
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/**
|
|
63
82
|
* Validate a handle's GRAMMAR client-side (reserved names are the server's
|
|
64
83
|
* call — a 409 carries the verdict). Returns an error string, or null.
|
package/src/utils/scaffold.js
CHANGED
|
@@ -76,14 +76,16 @@ export async function getWorkspaceTemplateOutputs({ blank = false } = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* Scaffold a foundation from the foundation package template
|
|
79
|
+
* Scaffold a foundation from the foundation package template.
|
|
80
80
|
*
|
|
81
|
-
* @param {string} targetDir
|
|
81
|
+
* @param {string} targetDir
|
|
82
82
|
* @param {Object} context - Template context
|
|
83
|
-
* @param {string} context.name - Package name
|
|
83
|
+
* @param {string} context.name - Package name — workspace plumbing (`src`, `docs-src`)
|
|
84
|
+
* @param {string} [context.registryName] - The foundation's own name, written to
|
|
85
|
+
* main.js's `name`: what it registers as (`@org/<name>`). Omitted when none can be
|
|
86
|
+
* made, and `uniweb register` asks for one
|
|
84
87
|
* @param {string} context.projectName - Workspace name
|
|
85
|
-
* @param {boolean} [context.isExtension]
|
|
86
|
-
* @param {Object} [options] - Processing options
|
|
88
|
+
* @param {boolean} [context.isExtension]
|
|
87
89
|
*/
|
|
88
90
|
export async function scaffoldFoundation(targetDir, context, options = {}) {
|
|
89
91
|
registerVersions(getVersionsForTemplates())
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - defaultLayout: Name of the default layout from src/layouts/ (optional)
|
|
7
7
|
* - props: Foundation-wide props accessible via website.foundationProps
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Name: `name` in the default export is what this foundation registers as
|
|
10
|
+
* (@org/<name>) — `uniweb create` adds it. The version comes from package.json.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
// Create a layout at src/layouts/MyLayout/index.jsx
|
|
@@ -14,10 +14,19 @@
|
|
|
14
14
|
|
|
15
15
|
{{#if isExtension}}
|
|
16
16
|
export default {
|
|
17
|
+
{{#if registryName}}
|
|
18
|
+
name: '{{registryName}}',
|
|
19
|
+
{{/if}}
|
|
17
20
|
extension: true,
|
|
18
21
|
}
|
|
19
22
|
{{else}}
|
|
20
23
|
export default {
|
|
24
|
+
{{#if registryName}}
|
|
25
|
+
// ─── Name ───────────────────────────────────────────────────────────────────
|
|
26
|
+
// What this foundation registers as — @<org>/<name> — and what sites pin.
|
|
27
|
+
name: '{{registryName}}',
|
|
28
|
+
|
|
29
|
+
{{/if}}
|
|
21
30
|
// ─── Layout ─────────────────────────────────────────────────────────────────
|
|
22
31
|
// Create layouts in layouts/MyLayout/index.jsx (auto-discovered).
|
|
23
32
|
// defaultLayout: 'MyLayout',
|
|
@@ -25,7 +25,7 @@ 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
|
+
# records: ./data # Where this site's records 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
|
|
@@ -41,14 +41,17 @@ index: home
|
|
|
41
41
|
# ─── Structured Content ───────────────────────────────────────────────────────
|
|
42
42
|
# Three things, and they are separate on purpose:
|
|
43
43
|
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
44
|
+
# records/{schema}/ your site's records — every file here is one. The folder
|
|
45
|
+
# names their data schema:
|
|
46
|
+
# records/article/ → @/article (your foundation's)
|
|
47
|
+
# records/std/person/ → @std/person
|
|
48
|
+
# A file whose name starts with `_` is not a record.
|
|
49
|
+
# records/folder.yml OPTIONAL — sorts records into folders a query can slice
|
|
50
|
+
# with `scope:`. The one file in records/ that is not a
|
|
51
|
+
# record. Most sites have none:
|
|
52
|
+
# - folder: archive
|
|
53
|
+
# records:
|
|
54
|
+
# - article/2025-*.md
|
|
52
55
|
# queries.yml HOW CONTENT IS REACHED — named queries over those records:
|
|
53
56
|
# recent:
|
|
54
57
|
# schema: '@/article'
|