uniweb 0.56.5 → 0.56.7

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.56.5",
3
+ "version": "0.56.7",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,15 +42,16 @@
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
44
  "@uniweb/core": "^0.29.4",
45
- "@uniweb/kit": "^0.19.2",
46
45
  "@uniweb/runtime": "^0.26.4",
47
- "@uniweb/schemas": "^0.3.0",
48
- "@uniweb/semantic-parser": "^1.4.0"
46
+ "@uniweb/schemas": "^0.3.1",
47
+ "@uniweb/content-writer": "^0.3.4",
48
+ "@uniweb/kit": "^0.19.3",
49
+ "@uniweb/semantic-parser": "^1.4.1"
49
50
  },
50
51
  "peerDependencies": {
51
- "@uniweb/build": "^0.52.4",
52
+ "@uniweb/build": "^0.52.6",
52
53
  "@uniweb/content-reader": "^1.2.5",
53
- "@uniweb/semantic-parser": "^1.4.0"
54
+ "@uniweb/semantic-parser": "^1.4.1"
54
55
  },
55
56
  "peerDependenciesMeta": {
56
57
  "@uniweb/build": {
@@ -341,7 +341,7 @@ Description paragraph.
341
341
  ![Image](./image.jpg)
342
342
  ```
343
343
 
344
- Heading levels set *structure* (pretitle, title, subtitle), not font size — the component controls visual sizing. The `#>` label line marks a pretitle explicitly (any number of leading `#`s spells the same label); a smaller ordinary heading directly above the title also becomes the pretitle.
344
+ Heading levels set *structure* (pretitle, title, subtitle), not font size — the component controls visual sizing. Write a pretitle as a `#>` label line any number of leading `#`s spells the same label, so match the heading you are labelling. A smaller ordinary heading directly above the title is *also* read as a pretitle, which is how files written before label lines keep working; prefer `#>`, because it says what it is wherever it lands rather than depending on what follows it.
345
345
 
346
346
  **A section with no `type:` renders through the foundation's default section type — a component named `Section`, unless the foundation's `main.js` sets `defaultSection` to something else.** This is what lets a folder of plain markdown with no frontmatter at all become pages: mounted documentation, an imported wiki, anything written before it met this framework. If such content renders blank, the foundation has no `Section` — that, not the markdown, is what to fix.
347
347
 
@@ -356,8 +356,9 @@ The semantic parser produces a flat, guaranteed structure. No null checks needed
356
356
  ```js
357
357
  content = {
358
358
  title: '', // Main heading (string or string[] for multi-line)
359
- pretitle: '', // `#>` label line(s), or smaller headings stacked above
360
- // the title (string or string[])
359
+ pretitle: '', // `#>` label line(s) also filled by smaller headings
360
+ // stacked above the title, for older content
361
+ // (string or string[])
361
362
  subtitle: '', // Line(s) one step below the title — each further
362
363
  // one-step descent is another line (string or string[])
363
364
  paragraphs: [], // Text blocks
@@ -401,7 +402,7 @@ Lightning quick. │ content.items[0].paragraphs[0] = "Lightning
401
402
  Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise-grade…"
402
403
  ```
403
404
 
404
- The staircase rule produces this — each heading relates to the one before it: the same size adds another line to the same part; **one step smaller** joins the headline as the next part down (the subtitle, then further subtitle lines); **two steps smaller** starts an item; and once body content has begun, *any* heading starts an item. `#>` label lines, and smaller headings stacked above the title, become `pretitle`.
405
+ The staircase rule produces this — each heading relates to the one before it: the same size adds another line to the same part; **one step smaller** joins the headline as the next part down (the subtitle, then further subtitle lines); **two steps smaller** starts an item; and once body content has begun, *any* heading starts an item. `#>` label lines become `pretitle`; so do smaller headings stacked above the title, which is the older spelling.
405
406
 
406
407
  ### Items have the full content shape
407
408
 
@@ -35,6 +35,7 @@ import {
35
35
  updateRootScripts
36
36
  } from '../utils/config.js'
37
37
  import { discoverFoundations, discoverSites } from '../utils/discover.js'
38
+ import { generateStarter, reportStarter, declarationFor } from './starter.js'
38
39
  import {
39
40
  validatePackageName,
40
41
  getExistingPackageNames,
@@ -73,17 +74,30 @@ const colors = {
73
74
  red: '\x1b[31m'
74
75
  }
75
76
 
77
+ // Porcelain (`--json`) mode: stdout carries ONLY the JSON, so every human line
78
+ // diverts to stderr — the convention `register.js` already follows and
79
+ // `families.js` states ("stdout carries JSON and nothing else, so it pipes").
80
+ //
81
+ // ⛔ It is set per run rather than only when true, because a module-level flag
82
+ // that is only ever turned ON leaks into the next call in the same process —
83
+ // which is every call in the test suite.
84
+ let jsonMode = false
85
+ export function setAddJsonMode(on) {
86
+ jsonMode = Boolean(on)
87
+ }
88
+
76
89
  function log(message) {
77
- console.log(message)
90
+ if (jsonMode) console.error(message)
91
+ else console.log(message)
78
92
  }
79
93
  function success(message) {
80
- console.log(`${colors.green}✓${colors.reset} ${message}`)
94
+ log(`${colors.green}✓${colors.reset} ${message}`)
81
95
  }
82
96
  function error(message) {
83
97
  console.error(`${colors.red}✗${colors.reset} ${message}`)
84
98
  }
85
99
  function info(message) {
86
- console.log(`${colors.dim}${message}${colors.reset}`)
100
+ log(`${colors.dim}${message}${colors.reset}`)
87
101
  }
88
102
 
89
103
  /**
@@ -106,12 +120,19 @@ function parseArgs(args) {
106
120
  // scaffold PR-preview workflows.
107
121
  target: null,
108
122
  projectName: null,
109
- previews: true
123
+ previews: true,
124
+ // `add section` only: generate starter content for the section type from
125
+ // its `content:` declaration, and which preset's params to frontmatter it
126
+ // with. `--write` sends the markdown to a file instead of stdout.
127
+ starter: false,
128
+ preset: null,
129
+ write: null,
130
+ json: false
110
131
  }
111
132
 
112
133
  // Booleans (no value) consumed up-front so the value-flag loop below
113
134
  // doesn't accidentally swallow the next positional.
114
- const BOOLEAN_FLAGS = new Set(['--force', '--no-previews'])
135
+ const BOOLEAN_FLAGS = new Set(['--force', '--no-previews', '--starter', '--json'])
115
136
 
116
137
  // Value flags, mapped to their result key. Both spellings are accepted:
117
138
  // `--host github-pages` and `--host=github-pages`.
@@ -131,7 +152,9 @@ function parseArgs(args) {
131
152
  '--host': 'host',
132
153
  '--domain': 'domain',
133
154
  '--target': 'target',
134
- '--project-name': 'projectName'
155
+ '--project-name': 'projectName',
156
+ '--preset': 'preset',
157
+ '--write': 'write'
135
158
  }
136
159
 
137
160
  /** Split `--flag=value` into [flag, value]; `--flag` into [flag, null]. */
@@ -169,6 +192,10 @@ function parseArgs(args) {
169
192
  result.force = true
170
193
  } else if (flag === '--no-previews') {
171
194
  result.previews = false
195
+ } else if (flag === '--starter') {
196
+ result.starter = true
197
+ } else if (flag === '--json') {
198
+ result.json = true
172
199
  }
173
200
  }
174
201
 
@@ -182,11 +209,6 @@ export async function add(rawArgs) {
182
209
  const nonInteractive = isNonInteractive(rawArgs)
183
210
  const args = stripNonInteractiveFlag(rawArgs)
184
211
 
185
- if (args[0] === '--help' || args[0] === '-h') {
186
- showAddHelp()
187
- return
188
- }
189
-
190
212
  const pm = detectPackageManager()
191
213
  const prefix = getCliPrefix()
192
214
 
@@ -1074,6 +1096,7 @@ async function wireExtensionToSite(
1074
1096
  * Add a section type to a foundation
1075
1097
  */
1076
1098
  async function addSection(rootDir, opts) {
1099
+ setAddJsonMode(opts.json)
1077
1100
  let name = opts.name
1078
1101
 
1079
1102
  // Interactive name prompt when not provided
@@ -1169,10 +1192,31 @@ async function addSection(rootDir, opts) {
1169
1192
  const sectionDir = join(sectionsDir, name)
1170
1193
  const relSectionPath = relative(foundationDir, sectionDir)
1171
1194
 
1195
+ // ⭐ `--starter` ON AN EXISTING SECTION IS NOT AN ERROR. The flag asks one
1196
+ // question — *what content would an author start this section with?* — and a
1197
+ // section type that already exists is the case where it has a real `content:`
1198
+ // declaration to answer from. Refusing here would make the flag testable only
1199
+ // against stubs, which is the one case where the answer is least interesting.
1200
+ // Nothing is scaffolded and nothing is overwritten on this path.
1172
1201
  if (existsSync(sectionDir)) {
1202
+ if (opts.starter) {
1203
+ const { markdown, result } = await generateStarter({
1204
+ name,
1205
+ sectionDir,
1206
+ preset: opts.preset,
1207
+ json: opts.json,
1208
+ write: opts.write,
1209
+ })
1210
+ if (!opts.json) {
1211
+ if (!opts.write) log('\n' + markdown.trimEnd())
1212
+ reportStarter(result, { write: opts.write })
1213
+ }
1214
+ return
1215
+ }
1173
1216
  error(
1174
1217
  `Section '${name}' already exists at ${foundation.path}/${relSectionPath}/`
1175
1218
  )
1219
+ log(` ${colors.dim}--starter generates starter content for it without touching the files.${colors.reset}`)
1176
1220
  process.exit(1)
1177
1221
  }
1178
1222
 
@@ -1207,9 +1251,21 @@ export default function ${name}({ content, params }) {
1207
1251
  }
1208
1252
  `
1209
1253
 
1254
+ // With `--starter`, the scaffold gets a `content:` declaration derived from
1255
+ // the family the name resolves to — so the three pieces agree: a declaration,
1256
+ // a component that reads it, and content that fills it. Without the flag the
1257
+ // stub declares nothing, exactly as before.
1258
+ let starterDeclaration = ''
1259
+ let starterResult = null
1260
+ if (opts.starter) {
1261
+ const preview = await generateStarter({ name, sectionDir: sectionDir })
1262
+ starterResult = preview.result
1263
+ starterDeclaration = await declarationFor(preview.result)
1264
+ }
1265
+
1210
1266
  const metaContent = `export default {
1211
1267
  title: '${name}',
1212
- description: '',
1268
+ description: '',${starterDeclaration}
1213
1269
  params: {},
1214
1270
  }
1215
1271
  `
@@ -1230,6 +1286,37 @@ export default function ${name}({ content, params }) {
1230
1286
  `${colors.dim}The dev server will pick it up automatically.${colors.reset}`
1231
1287
  )
1232
1288
  }
1289
+
1290
+ if (starterResult) {
1291
+ // Regenerate against the declaration just written, so what is printed is
1292
+ // what this section's own `meta.js` now asks for rather than the family's
1293
+ // guess — the two agree here, and saying it from the file keeps them so.
1294
+ const { markdown, result } = await generateStarter({
1295
+ name,
1296
+ sectionDir,
1297
+ preset: opts.preset,
1298
+ json: opts.json,
1299
+ write: opts.write,
1300
+ })
1301
+ if (!opts.json) {
1302
+ log('')
1303
+ log(`${colors.dim}Starter content for a page section:${colors.reset}`)
1304
+ if (!opts.write) log('\n' + markdown.trimEnd())
1305
+ reportStarter(result, { write: opts.write })
1306
+ // The stub component reads three elements. The declaration above may name
1307
+ // more, because it comes from the family rather than from the stub — say
1308
+ // so, rather than leave a developer wondering why half the content they
1309
+ // were just handed renders as nothing.
1310
+ const stubReads = new Set(['title', 'paragraphs', 'links'])
1311
+ const unread = Object.keys(result.content).filter((slot) => !stubReads.has(slot))
1312
+ if (unread.length) {
1313
+ log(
1314
+ ` ${colors.dim}index.jsx reads title, paragraphs and links — extend it for: ${unread.join(', ')}${colors.reset}`
1315
+ )
1316
+ log('')
1317
+ }
1318
+ }
1319
+ }
1233
1320
  }
1234
1321
 
1235
1322
  /**
@@ -1729,74 +1816,3 @@ function parseNodeMajor(engines) {
1729
1816
  return match ? match[1] : null
1730
1817
  }
1731
1818
 
1732
- /**
1733
- * Show help for the add command
1734
- */
1735
- function showAddHelp() {
1736
- log(`
1737
- ${colors.cyan}${colors.bright}Uniweb Add${colors.reset}
1738
-
1739
- Add projects, foundations, sites, extensions, section types, or CI workflows to your workspace.
1740
-
1741
- ${colors.bright}Usage:${colors.reset}
1742
- uniweb add project [name] [options]
1743
- uniweb add foundation [name] [options]
1744
- uniweb add site [name] [options]
1745
- uniweb add extension <name> [options]
1746
- uniweb add section <name> [options]
1747
- uniweb add ci [options]
1748
-
1749
- ${colors.bright}Common Options:${colors.reset}
1750
- --from <template> Apply content from a template after scaffolding
1751
- --path <dir> Custom directory for the package
1752
-
1753
- ${colors.bright}Foundation Options:${colors.reset}
1754
- --project <name> Group under a project directory (co-located layout)
1755
-
1756
- ${colors.bright}Site Options:${colors.reset}
1757
- --foundation <n> Foundation to wire to (prompted if multiple exist)
1758
- --project <name> Group under a project directory (co-located layout)
1759
-
1760
- ${colors.bright}Extension Options:${colors.reset}
1761
- --site <name> Site to wire extension URL into
1762
-
1763
- ${colors.bright}Section Options:${colors.reset}
1764
- --foundation <n> Foundation to add section to (prompted if multiple exist)
1765
-
1766
- ${colors.bright}CI Options:${colors.reset}
1767
- --host <name> github-pages | cloudflare-pages | netlify | vercel
1768
- (prompted when omitted)
1769
- --target <what> site (default) | foundation
1770
- 'foundation' publishes built foundations at permanent
1771
- versioned URLs — the free alternative to the catalog
1772
- --site <name> Site the workflow builds (prompted if multiple exist)
1773
- --foundation <name> With --target foundation: publish just this one
1774
- --domain <host> Custom domain (GitHub Pages: writes CNAME, serves at root)
1775
- --project-name <name> Name to register under on the host (default: the
1776
- workspace name; Cloudflare Pages project, etc.)
1777
- --no-previews Skip the per-PR preview workflow
1778
- --force Overwrite an existing workflow file
1779
-
1780
- ${colors.dim}Hosts that support PR previews: cloudflare-pages, netlify, vercel.
1781
- GitHub Pages has no preview environment, so it scaffolds a deploy workflow only.${colors.reset}
1782
-
1783
- ${colors.bright}Examples:${colors.reset}
1784
- uniweb add project docs # Create docs/foundation/ + docs/site/
1785
- uniweb add project docs --from academic # Co-located pair + academic content
1786
- uniweb add foundation # Create ./foundation/ at root
1787
- uniweb add foundation ui # Create ./foundations/ui/
1788
- uniweb add site # Create ./site/ at root
1789
- uniweb add site blog --foundation marketing # Create ./sites/blog/ wired to marketing
1790
- uniweb add extension effects --site site # Create ./extensions/effects/
1791
- uniweb add section Hero # Create Hero section type
1792
- uniweb add section Hero --foundation ui # Target specific foundation
1793
- uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
1794
- uniweb add site --project docs # Create ./docs/site/ (co-located)
1795
- uniweb add ci # Pick a host, add a deploy workflow
1796
- uniweb add ci --host github-pages --site marketing # Pick host + site explicitly
1797
- uniweb add ci --host netlify # Deploy + PR-preview workflows
1798
- uniweb add ci --host vercel --no-previews # Deploy workflow only
1799
- uniweb add ci --domain mysite.com # Custom domain → writes CNAME + UNIWEB_BASE=/
1800
- uniweb add ci --target foundation # Publish foundations at versioned URLs
1801
- `)
1802
- }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Starter content for a section type — `uniweb add section <Name> --starter`.
3
+ *
4
+ * ⭐ IT IS DERIVED FROM `content:`, NOT AUTHORED. A developer declares what
5
+ * their component expects; this turns that declaration into something an author
6
+ * can edit instead of an empty box. There is no `starter:` key in `meta.js` and
7
+ * there should not be [Diego, 2026-09-16] — authored sample copy drifts against
8
+ * the declaration it is meant to match, and it can never be localized, because a
9
+ * developer's string is the foundation's own words and is shown verbatim in
10
+ * every UI language.
11
+ *
12
+ * ⚖️ THE GENERATOR IS NOT HERE. It is `@uniweb/schemas/starter`, because the
13
+ * visual editor is its other caller and must reach it in a browser — this file
14
+ * is the CLI's rendering of the same answer. Anything that decides WHAT the
15
+ * content is belongs there; what is left here is reading a `meta.js` off disk
16
+ * and serializing.
17
+ */
18
+
19
+ import { existsSync } from 'node:fs'
20
+ import { readFile, writeFile } from 'node:fs/promises'
21
+ import { join, dirname } from 'node:path'
22
+ import { pathToFileURL } from 'node:url'
23
+ import { createHash } from 'node:crypto'
24
+
25
+ const colors = {
26
+ reset: '\x1b[0m',
27
+ bright: '\x1b[1m',
28
+ dim: '\x1b[2m',
29
+ cyan: '\x1b[36m',
30
+ yellow: '\x1b[33m',
31
+ }
32
+
33
+ /**
34
+ * Load a section's `meta.js` as it is on disk NOW.
35
+ *
36
+ * ⛔ The URL carries the file's content hash. Node caches an ES module by URL
37
+ * for the life of the process, and `@uniweb/build`'s own loader hit exactly this
38
+ * — a dev server regenerating after a `meta.js` edit re-imported the file it had
39
+ * started with. Same rule here, same reason.
40
+ */
41
+ async function loadMeta(metaPath) {
42
+ const version = createHash('sha1').update(await readFile(metaPath)).digest('hex').slice(0, 16)
43
+ const mod = await import(`${pathToFileURL(metaPath).href}?content=${version}`)
44
+ return mod.default
45
+ }
46
+
47
+ /**
48
+ * The three framework functions this composes, imported at call time.
49
+ *
50
+ * ⚖️ Dynamic, matching `inspect.js`: these resolve through the project's own
51
+ * workspace, and a clear message beats a stack trace when one is missing.
52
+ */
53
+ async function loadPipeline() {
54
+ try {
55
+ const [schemas, content, parser, writer] = await Promise.all([
56
+ import('@uniweb/schemas/starter'),
57
+ import('@uniweb/schemas/content'),
58
+ import('@uniweb/semantic-parser'),
59
+ import('@uniweb/content-writer'),
60
+ ])
61
+ return {
62
+ starterContent: schemas.starterContent,
63
+ describeContent: content.describeContent,
64
+ buildDoc: parser.buildDoc,
65
+ serializeSection: writer.serializeSection,
66
+ }
67
+ } catch (err) {
68
+ throw new Error(
69
+ `Starter content needs @uniweb/schemas, @uniweb/semantic-parser and @uniweb/content-writer.\n` +
70
+ ` ${err.message}\n` +
71
+ ` Run your package manager's install in this workspace and try again.`,
72
+ )
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Generate starter content for one section type and render it.
78
+ *
79
+ * @param {object} args
80
+ * @param {string} args.name - the section type (PascalCase).
81
+ * @param {string} args.sectionDir - the section's directory in the foundation.
82
+ * @param {string} [args.preset] - a declared preset whose params become the frontmatter.
83
+ * @param {boolean} [args.json] - emit the structure and the ProseMirror doc instead of markdown.
84
+ * @param {string} [args.write] - write the markdown to this path instead of printing it.
85
+ * @returns {Promise<{markdown: string, result: object}>}
86
+ */
87
+ export async function generateStarter({ name, sectionDir, preset, json, write }) {
88
+ const { starterContent, describeContent, buildDoc, serializeSection } = await loadPipeline()
89
+
90
+ const metaPath = join(sectionDir, 'meta.js')
91
+ const meta = existsSync(metaPath) ? await loadMeta(metaPath) : {}
92
+ const result = starterContent({ name, ...meta }, { preset })
93
+
94
+ const doc = buildDoc(result.content)
95
+ const markdown = doc ? serializeSection(result.params, doc) : ''
96
+
97
+ if (json) {
98
+ process.stdout.write(
99
+ JSON.stringify(
100
+ {
101
+ section: name,
102
+ family: result.family,
103
+ elementsInferred: result.elementsInferred,
104
+ unfilled: result.unfilled,
105
+ // What the component SAYS it expects, parsed — the same structure an
106
+ // editor renders a "what does this section want?" panel from. Carried
107
+ // here so the declaration and what was generated from it can be read
108
+ // side by side, without the app.
109
+ expects: describeContent({ name, ...meta }),
110
+ params: result.params,
111
+ content: result.content,
112
+ doc,
113
+ },
114
+ null,
115
+ 2,
116
+ ) + '\n',
117
+ )
118
+ return { markdown, result }
119
+ }
120
+
121
+ if (write) {
122
+ await writeFile(write, markdown, 'utf-8')
123
+ }
124
+
125
+ return { markdown, result }
126
+ }
127
+
128
+ /**
129
+ * The human-facing report that follows generation.
130
+ *
131
+ * ⭐ It SAYS when the element list was ours. A component declaring no `content:`
132
+ * gets its family's canonical set, and a developer reading generated content
133
+ * they never specified should be told why — otherwise the natural conclusion is
134
+ * that the generator invented a declaration on their behalf.
135
+ */
136
+ export function reportStarter(result, { write } = {}) {
137
+ const c = colors
138
+ const fam = result.family.id
139
+ ? `${c.cyan}${result.family.id}${c.reset} ${c.dim}(${result.family.source})${c.reset}`
140
+ : `${c.dim}no family — generic copy${c.reset}`
141
+
142
+ console.log('')
143
+ console.log(` ${c.dim}family:${c.reset} ${fam}`)
144
+ console.log(` ${c.dim}slots:${c.reset} ${Object.keys(result.content).join(', ') || '—'}`)
145
+
146
+ if (result.elementsInferred) {
147
+ console.log('')
148
+ console.log(
149
+ ` ${c.yellow}!${c.reset} This section declares no ${c.bright}content:${c.reset} — the elements above came from its family.`,
150
+ )
151
+ console.log(
152
+ ` ${c.dim}Declare what the component expects and the starter content follows it instead.${c.reset}`,
153
+ )
154
+ }
155
+
156
+ if (result.unfilled.length) {
157
+ console.log('')
158
+ console.log(` ${c.yellow}!${c.reset} Not filled: ${result.unfilled.join(', ')}`)
159
+ console.log(
160
+ ` ${c.dim}\`background\` is frontmatter, not content. A video needs an address we cannot invent.${c.reset}`,
161
+ )
162
+ console.log(
163
+ ` ${c.dim}A \`data\` block needs a schema: a @/ ref resolves at build, and an empty {} declares no shape.${c.reset}`,
164
+ )
165
+ }
166
+
167
+ if (write) {
168
+ console.log('')
169
+ console.log(` ${c.cyan}→${c.reset} written to ${write}`)
170
+ }
171
+ console.log('')
172
+ }
173
+
174
+ /**
175
+ * A `content:` declaration for a section being scaffolded, derived from the
176
+ * family the name resolves to. Written into the new `meta.js` so the scaffold is
177
+ * coherent: a declaration, a component, and content that fills it.
178
+ */
179
+ export async function declarationFor(result) {
180
+ const LABELS = {
181
+ title: 'Headline',
182
+ pretitle: 'Small label above the headline',
183
+ subtitle: 'Secondary headline',
184
+ paragraphs: 'Body copy [1-2]',
185
+ links: 'Calls to action [0-2]',
186
+ lists: 'Bullet points [0-1]',
187
+ items: 'One per entry [3-6]',
188
+ images: 'Image [1]',
189
+ icons: 'Icon [1]',
190
+ videos: 'Video [1]',
191
+ snippets: 'Code sample [1]',
192
+ data: 'The data block the author writes',
193
+ }
194
+ // ⛔ The slot → declaration spelling comes from @uniweb/schemas, not from a
195
+ // copy here. `image` declares and `images` delivers; that mapping is the
196
+ // generator's, and a second copy rots the moment a row is added there.
197
+ const { declarationKey } = await import('@uniweb/schemas/starter')
198
+ const lines = Object.keys(result.content).map(
199
+ (slot) => ` ${declarationKey(slot)}: '${LABELS[slot] || slot}',`,
200
+ )
201
+ return lines.length ? `\n content: {\n${lines.join('\n')}\n },\n` : ''
202
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-09-16T12:39:33.539Z",
3
+ "generatedAt": "2026-09-16T20:57:12.370Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "0.3.6",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "@uniweb/build": {
13
- "version": "0.52.4",
13
+ "version": "0.52.6",
14
14
  "path": "framework/build",
15
15
  "deps": [
16
16
  "@uniweb/content-reader",
@@ -18,6 +18,7 @@
18
18
  "@uniweb/core",
19
19
  "@uniweb/projections",
20
20
  "@uniweb/runtime",
21
+ "@uniweb/runtime",
21
22
  "@uniweb/schemas",
22
23
  "@uniweb/semantic-parser",
23
24
  "@uniweb/theming"
@@ -54,7 +55,7 @@
54
55
  ]
55
56
  },
56
57
  "@uniweb/kit": {
57
- "version": "0.19.2",
58
+ "version": "0.19.3",
58
59
  "path": "framework/kit",
59
60
  "deps": [
60
61
  "@uniweb/core",
@@ -95,7 +96,7 @@
95
96
  "deps": []
96
97
  },
97
98
  "@uniweb/schemas": {
98
- "version": "0.3.0",
99
+ "version": "0.3.1",
99
100
  "path": "framework/schemas",
100
101
  "deps": []
101
102
  },
@@ -105,12 +106,12 @@
105
106
  "deps": []
106
107
  },
107
108
  "@uniweb/semantic-parser": {
108
- "version": "1.4.0",
109
+ "version": "1.4.1",
109
110
  "path": "framework/semantic-parser",
110
111
  "deps": []
111
112
  },
112
113
  "@uniweb/templates": {
113
- "version": "0.14.4",
114
+ "version": "0.14.5",
114
115
  "path": "framework/templates",
115
116
  "deps": []
116
117
  },
package/src/index.js CHANGED
@@ -1498,22 +1498,82 @@ ${colors.bright}Options:${colors.reset}
1498
1498
  --platform <name> (Deprecated alias for --host)
1499
1499
  `,
1500
1500
  add: `
1501
- ${colors.cyan}${colors.bright}uniweb add${colors.reset} ${colors.dim}— Add a foundation, site, or extension${colors.reset}
1501
+ ${colors.cyan}${colors.bright}Uniweb Add${colors.reset}
1502
1502
 
1503
- ${colors.bright}Subcommands:${colors.reset}
1504
- add project [name] Add a co-located foundation + site pair
1505
- add foundation [name] Add a foundation (--from, --path, --project)
1506
- add site [name] Add a site (--from, --foundation, --path, --project)
1507
- add extension <name> Add an extension (--from, --site, --path)
1508
- add section <name> Add a section type to a foundation (--foundation)
1509
- add ci Add a CI workflow so every push deploys (--host, --target)
1503
+ Add projects, foundations, sites, extensions, section types, or CI workflows to your workspace.
1510
1504
 
1511
- ${colors.bright}Common options:${colors.reset}
1512
- --from <template> Source content from a template
1513
- --path <dir> Override default folder location
1514
- --foundation <name> Wire site/extension to this foundation (CI-friendly)
1515
- --site <name> Wire extension to this site (CI-friendly)
1516
- --non-interactive Fail with usage info instead of prompting
1505
+ ${colors.bright}Usage:${colors.reset}
1506
+ uniweb add project [name] [options]
1507
+ uniweb add foundation [name] [options]
1508
+ uniweb add site [name] [options]
1509
+ uniweb add extension <name> [options]
1510
+ uniweb add section <name> [options]
1511
+ uniweb add ci [options]
1512
+
1513
+ ${colors.bright}Common Options:${colors.reset}
1514
+ --from <template> Apply content from a template after scaffolding
1515
+ --path <dir> Custom directory for the package
1516
+ --non-interactive Fail with usage info instead of prompting
1517
+
1518
+ ${colors.bright}Foundation Options:${colors.reset}
1519
+ --project <name> Group under a project directory (co-located layout)
1520
+
1521
+ ${colors.bright}Site Options:${colors.reset}
1522
+ --foundation <n> Foundation to wire to (prompted if multiple exist)
1523
+ --project <name> Group under a project directory (co-located layout)
1524
+
1525
+ ${colors.bright}Extension Options:${colors.reset}
1526
+ --site <name> Site to wire extension URL into
1527
+
1528
+ ${colors.bright}Section Options:${colors.reset}
1529
+ --foundation <n> Foundation to add section to (prompted if multiple exist)
1530
+ --starter Generate starter content from the section's \`content:\`
1531
+ declaration — what an author would begin editing. Works on
1532
+ a section that already exists (nothing is written), and on
1533
+ a new one (the scaffold gets a matching declaration)
1534
+ --preset <name> Frontmatter the starter content with this preset's params
1535
+ --write <file> Write the starter markdown to a file instead of printing it
1536
+ --json Emit the content structure and ProseMirror doc instead
1537
+
1538
+ ${colors.bright}CI Options:${colors.reset}
1539
+ --host <name> github-pages | cloudflare-pages | netlify | vercel
1540
+ (prompted when omitted)
1541
+ --target <what> site (default) | foundation
1542
+ 'foundation' publishes built foundations at permanent
1543
+ versioned URLs — the free alternative to the catalog
1544
+ --site <name> Site the workflow builds (prompted if multiple exist)
1545
+ --foundation <name> With --target foundation: publish just this one
1546
+ --domain <host> Custom domain (GitHub Pages: writes CNAME, serves at root)
1547
+ --project-name <name> Name to register under on the host (default: the
1548
+ workspace name; Cloudflare Pages project, etc.)
1549
+ --no-previews Skip the per-PR preview workflow
1550
+ --force Overwrite an existing workflow file
1551
+
1552
+ ${colors.dim}Hosts that support PR previews: cloudflare-pages, netlify, vercel.
1553
+ GitHub Pages has no preview environment, so it scaffolds a deploy workflow only.${colors.reset}
1554
+
1555
+ ${colors.bright}Examples:${colors.reset}
1556
+ uniweb add project docs # Create docs/foundation/ + docs/site/
1557
+ uniweb add project docs --from academic # Co-located pair + academic content
1558
+ uniweb add foundation # Create ./foundation/ at root
1559
+ uniweb add foundation ui # Create ./foundations/ui/
1560
+ uniweb add site # Create ./site/ at root
1561
+ uniweb add site blog --foundation marketing # Create ./sites/blog/ wired to marketing
1562
+ uniweb add extension effects --site site # Create ./extensions/effects/
1563
+ uniweb add section Hero # Create Hero section type
1564
+ uniweb add section Hero --foundation ui # Target specific foundation
1565
+ uniweb add section Hero --starter # Starter content for an existing Hero
1566
+ uniweb add section Pricing --starter # Scaffold Pricing + content that fills it
1567
+ uniweb add section Hero --starter --preset split # Frontmatter it with the 'split' preset
1568
+ uniweb add section Hero --starter --json # The structure + ProseMirror, for a script
1569
+ uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
1570
+ uniweb add site --project docs # Create ./docs/site/ (co-located)
1571
+ uniweb add ci # Pick a host, add a deploy workflow
1572
+ uniweb add ci --host github-pages --site marketing # Pick host + site explicitly
1573
+ uniweb add ci --host netlify # Deploy + PR-preview workflows
1574
+ uniweb add ci --host vercel --no-previews # Deploy workflow only
1575
+ uniweb add ci --domain mysite.com # Custom domain → writes CNAME + UNIWEB_BASE=/
1576
+ uniweb add ci --target foundation # Publish foundations at versioned URLs
1517
1577
  `,
1518
1578
  export: `
1519
1579
  ${colors.cyan}${colors.bright}uniweb export${colors.reset} ${colors.dim}— Export a self-contained site for third-party hosting${colors.reset}
@@ -1888,7 +1948,7 @@ ${colors.bright}Add Subcommands:${colors.reset}
1888
1948
  add foundation [name] Add a foundation (--from, --path, --project)
1889
1949
  add site [name] Add a site (--from, --foundation, --path, --project)
1890
1950
  add extension <name> Add an extension (--from, --site, --path)
1891
- add section <name> Add a section type to a foundation (--foundation)
1951
+ add section <name> Add a section type to a foundation (--foundation, --starter)
1892
1952
  add ci Add a CI workflow so every push deploys (--host, --target)
1893
1953
 
1894
1954
  ${colors.bright}Global Options:${colors.reset}
@@ -122,7 +122,16 @@ export async function scaffoldSite(targetDir, context, options = {}) {
122
122
  registerVersions(getVersionsForTemplates())
123
123
 
124
124
  const templatePath = join(TEMPLATES_DIR, 'site')
125
- await copyTemplateDirectory(templatePath, targetDir, context, {
125
+ // A ref is written as a YAML scalar, not pasted as text: a scoped ref
126
+ // (`@acme/base@1.0.0`, `@acme/marketing`) starts with `@`, which plain YAML
127
+ // reserves, so the file would not parse — and every reader that swallows
128
+ // the parse error then reports the site as having no `$uuid`. js-yaml quotes
129
+ // only when needed, so `src` stays `src`. Rendered with `{{{ }}}`, because
130
+ // Handlebars' HTML escaping would also rewrite a URL's `&` and `=`.
131
+ const siteContext = context.foundationRef
132
+ ? { ...context, foundationRefYaml: yaml.dump(context.foundationRef, { lineWidth: -1 }).trim() }
133
+ : context
134
+ await copyTemplateDirectory(templatePath, targetDir, siteContext, {
126
135
  onProgress: options.onProgress,
127
136
  onWarning: options.onWarning
128
137
  })
@@ -4,7 +4,7 @@
4
4
  name: {{projectName}}
5
5
 
6
6
  {{#if foundationRef}}
7
- foundation: {{foundationRef}}
7
+ foundation: {{{foundationRefYaml}}}
8
8
 
9
9
  {{/if}}
10
10
  index: home