uniweb 1.20.3 → 1.20.4

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.
@@ -244,6 +244,28 @@ async function openDoor({ url, say, open }) {
244
244
  return { opened }
245
245
  }
246
246
 
247
+ /**
248
+ * Where a charge that was not asked is confirmed — at a terminal with `--yes`, or with no one at
249
+ * the terminal at all.
250
+ *
251
+ * ⛔ **UNATTENDED, NEVER "AT A TERMINAL".** The run with no one at the terminal is often an AI agent
252
+ * working for someone who does not use one, and it passes this line on: the person confirms in
253
+ * their browser, at the door the refusal printed, or in the app when there is none. *(Until
254
+ * 2026-10-26 every case said "run `uniweb publish` at a terminal".)* Either way a person confirms
255
+ * it, which is why the line says that `--yes` cannot.
256
+ *
257
+ * @param {{ interactive: boolean, door: boolean }} o - someone is at the terminal; a door was printed
258
+ * @returns {string}
259
+ */
260
+ function confirmChargeLine({ interactive, door }) {
261
+ if (interactive) {
262
+ return 'This change charges the site\'s card, so only you can confirm it: run `uniweb publish` without `--yes`.'
263
+ }
264
+ return door
265
+ ? 'This change charges the site\'s card, so a person confirms it, in a browser at the address above. `--yes` never confirms a charge.'
266
+ : 'This change charges the site\'s card, so a person confirms it, in the app. `--yes` never confirms a charge.'
267
+ }
268
+
247
269
  const CONFIRM = 'Confirm and publish'
248
270
  const CONTINUE = 'Continue and publish'
249
271
  const OPEN = 'Open it in the app'
@@ -275,7 +297,8 @@ const MAX_UNATTENDED = 3
275
297
  * "allow it for reductions only, keep charges interactive".]*
276
298
  *
277
299
  * With no one at the terminal and nothing to answer it, there is no prompt: the refusal is reported as
278
- * before, its door printed, and the publish fails. A refusal with no `confirm` behaves as it always has.
300
+ * before, its door printed, and the publish fails — and a charge says where a person confirms it, in a
301
+ * browser (`confirmChargeLine`). A refusal with no `confirm` behaves as it always has.
279
302
  *
280
303
  * @param {object} o
281
304
  * @param {ReturnType<typeof readPaymentRefusal>} o.verdict - the first refusal
@@ -312,7 +335,7 @@ export async function settleRefusal({ verdict, args = [], say, republish, intera
312
335
  } else if (!offer || !interactive || (yes && !reduction)) {
313
336
  await reportPaymentRefusal({ verdict: current, args, say, open, interactive: interactive && !yes })
314
337
  if (offer && !reduction) {
315
- say.dim('This change charges the site\'s card, so only you can confirm it: run `uniweb publish` at a terminal, without `--yes`.')
338
+ say.dim(confirmChargeLine({ interactive, door: current.kind === 'settle' }))
316
339
  } else if (reduction) {
317
340
  say.dim('This change only lowers what the site pays: `uniweb publish --yes` continues it without a prompt.')
318
341
  }
@@ -421,6 +421,8 @@ export async function clone(args = [], deps = {}) {
421
421
  {
422
422
  projectName: workspaceName,
423
423
  workspaceGlobs: ['site'],
424
+ packages: [{ path: 'site', site: true }],
425
+ cloned: true,
424
426
  scripts: { dev: 'uniweb dev', build: 'uniweb build' }
425
427
  },
426
428
  { onProgress }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `uniweb start` — the guide an AI agent reads first.
3
+ *
4
+ * ⭐ FOR AN AGENT WORKING FOR SOMEONE WHO DOES NOT USE A TERMINAL. The person asks their
5
+ * agent for a website; the agent runs `npx uniweb@latest start` and follows what it
6
+ * prints: create a project, read its AGENTS.md, run it, sign the person in, and bring the
7
+ * site into the Uniweb app. The person's part is their browser — signing in, and paying
8
+ * when the site goes live — so nothing here asks them to run a command.
9
+ *
10
+ * ⛔ READ-ONLY, AND SAFE ANYWHERE. It asks nothing, writes nothing and makes no request,
11
+ * so an agent can run it in any folder, as often as it likes. That is also why
12
+ * `index.js` answers it before the update check, which can write a cache and fetch.
13
+ *
14
+ * ⭐ THE TEXT SHIPS WITH THIS CLI (`partials/start.md`), so it describes the version that
15
+ * prints it. Inside a Uniweb project (`findUniwebWorkspace`) it starts from that project
16
+ * instead of creating one, and names that project's own package manager.
17
+ *
18
+ * It is answered by the CLI that was invoked, never delegated to a project's own copy
19
+ * (`STANDALONE_COMMANDS`): an older project CLI does not have it.
20
+ */
21
+
22
+ import { existsSync, readFileSync } from 'node:fs'
23
+ import { dirname, join } from 'node:path'
24
+ import { fileURLToPath } from 'node:url'
25
+ import Handlebars from 'handlebars'
26
+ import { findUniwebWorkspace, findWorkspaceRoot } from '../utils/workspace.js'
27
+ import { detectWorkspacePm, installCmd } from '../utils/pm.js'
28
+
29
+ const PARTIAL = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'partials', 'start.md')
30
+
31
+ /**
32
+ * The guide, as Markdown, for a command run in `cwd`.
33
+ *
34
+ * Outside a project, `otherProject` names a workspace of another kind that holds `cwd`:
35
+ * `create` refuses to make a project inside one, so the guide says to create it elsewhere.
36
+ *
37
+ * @param {{ cwd?: string }} [o]
38
+ * @returns {string}
39
+ */
40
+ export function startGuide({ cwd = process.cwd() } = {}) {
41
+ const root = findUniwebWorkspace(cwd)
42
+ const template = Handlebars.compile(readFileSync(PARTIAL, 'utf8'), { noEscape: true })
43
+ return template({
44
+ project: !!root,
45
+ root,
46
+ // A project `create` makes has one; one already here may not.
47
+ agents: root ? existsSync(join(root, 'AGENTS.md')) : true,
48
+ otherProject: root ? null : findWorkspaceRoot(cwd),
49
+ install: installCmd(root ? detectWorkspacePm(root) : 'npm')
50
+ })
51
+ }
52
+
53
+ /**
54
+ * @param {string[]} [args] - none are read
55
+ * @returns {Promise<{ exitCode: number }>}
56
+ */
57
+ export async function start(args = []) {
58
+ console.log(startGuide())
59
+ return { exitCode: 0 }
60
+ }
@@ -44,7 +44,7 @@ import { join } from 'node:path'
44
44
  import { spawn } from 'node:child_process'
45
45
  import prompts from 'prompts'
46
46
 
47
- import { findWorkspaceRoot } from '../utils/workspace.js'
47
+ import { findUniwebWorkspace } from '../utils/workspace.js'
48
48
  import {
49
49
  readAgentsVersion,
50
50
  refreshAgentsContent,
@@ -108,28 +108,6 @@ function isNpxInvocation() {
108
108
  return p.includes('/_npx/')
109
109
  }
110
110
 
111
- /**
112
- * Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
113
- * — also requires that the workspace's root package.json declares uniweb
114
- * as a dep or devDep. Otherwise we'd write AGENTS.md and edit package.json
115
- * files in unrelated monorepos.
116
- */
117
- function findUniwebWorkspace(cwd) {
118
- const workspaceDir = findWorkspaceRoot(cwd)
119
- if (!workspaceDir) return null
120
- const pkgPath = join(workspaceDir, 'package.json')
121
- if (!existsSync(pkgPath)) return null
122
- try {
123
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
124
- const hasUniwebDep = !!(
125
- pkg.devDependencies?.uniweb || pkg.dependencies?.uniweb
126
- )
127
- return hasUniwebDep ? workspaceDir : null
128
- } catch {
129
- return null
130
- }
131
- }
132
-
133
111
  /**
134
112
  * Trailing advisory: a newer CLI exists, and this run could not have used it.
135
113
  *
package/src/index.js CHANGED
@@ -152,6 +152,8 @@ function getCliVersion() {
152
152
  // against *this* CLI's version matrix — when run from a (newer) global
153
153
  // install that's the whole point; delegating to the project-local copy
154
154
  // would align the project to the version it already has, i.e. a no-op.
155
+ // `start` is here because a project's older CLI may not have it, and its
156
+ // guide describes the CLI that prints it.
155
157
  const STANDALONE_COMMANDS = new Set([
156
158
  'create',
157
159
  'clone',
@@ -161,6 +163,7 @@ const STANDALONE_COMMANDS = new Set([
161
163
  '-v',
162
164
  'login',
163
165
  'logout',
166
+ 'start',
164
167
  'update'
165
168
  ])
166
169
 
@@ -347,6 +350,10 @@ async function createFromPackageTemplates(
347
350
  {
348
351
  projectName,
349
352
  workspaceGlobs: ['site', 'src'],
353
+ packages: [
354
+ { path: 'src', foundation: true },
355
+ { path: 'site', site: true }
356
+ ],
350
357
  scripts: {
351
358
  dev: 'uniweb dev',
352
359
  build: 'uniweb build',
@@ -413,6 +420,7 @@ async function createBlankWorkspace(projectDir, projectName, options = {}) {
413
420
  {
414
421
  projectName,
415
422
  workspaceGlobs: [],
423
+ packages: [],
416
424
  scripts: {
417
425
  build: 'uniweb build'
418
426
  }
@@ -480,6 +488,8 @@ async function createFromContentTemplate(
480
488
  {
481
489
  projectName,
482
490
  workspaceGlobs,
491
+ // `type` is foundation, extension or site: the README's table reads it as a flag.
492
+ packages: placed.map((p) => ({ path: p.relativePath, [p.type]: true })),
483
493
  scripts
484
494
  },
485
495
  { onProgress, onWarning }
@@ -696,6 +706,19 @@ async function main() {
696
706
  // Commands that need @uniweb/build will get a helpful error via importProjectCommand().
697
707
  }
698
708
 
709
+ // `start` — the guide an AI agent reads first (commands/start.js). Answered before the
710
+ // update check: it asks nothing, writes nothing and makes no request, and the check can
711
+ // write a cache and fetch.
712
+ if (command === 'start') {
713
+ if (args.slice(1).some((a) => a === '--help' || a === '-h')) {
714
+ printCommandHelp('start')
715
+ return
716
+ }
717
+ const { start } = await import('./commands/start.js')
718
+ await start(args.slice(1))
719
+ return
720
+ }
721
+
699
722
  // Update check for global installs.
700
723
  //
701
724
  // - `create` / `clone` STOP when a newer release exists, before anything is
@@ -1132,6 +1155,9 @@ async function main() {
1132
1155
  if (nonInteractive && !projectName) {
1133
1156
  error(`Missing project name.\n`)
1134
1157
  log(`Usage: ${prefix} create <project-name> [--template <name>] [--blank]`)
1158
+ log(
1159
+ `\nAn AI agent building a site for someone: run ${colors.cyan}npx uniweb@latest start${colors.reset} first — it says how, step by step.`
1160
+ )
1135
1161
  await exitWhenDrained(1)
1136
1162
  }
1137
1163
 
@@ -1389,6 +1415,10 @@ async function main() {
1389
1415
  ` ${colors.cyan}${runCmd(recPm, 'dev')}${colors.reset} ${colors.dim}# start the dev server${colors.reset}`
1390
1416
  )
1391
1417
  }
1418
+ log('')
1419
+ log(
1420
+ ` ${colors.bright}AGENTS.md${colors.reset} is the project's manual, for you and for AI agents: its Part 0 says where to begin.`
1421
+ )
1392
1422
  if (recPm !== 'pnpm') {
1393
1423
  log('')
1394
1424
  log(
@@ -1529,6 +1559,21 @@ ${colors.bright}Examples:${colors.reset}
1529
1559
  uniweb create . # Scaffold into current dir
1530
1560
  uniweb create . --template docs # In place + a content template
1531
1561
  uniweb create . --name=my-app # In place, explicit slug
1562
+ `,
1563
+ start: `
1564
+ ${colors.cyan}${colors.bright}uniweb start${colors.reset} ${colors.dim}— How an AI agent begins a Uniweb site${colors.reset}
1565
+
1566
+ ${colors.bright}Usage:${colors.reset}
1567
+ npx uniweb@latest start Print the guide, in Markdown
1568
+
1569
+ A short guide for an AI agent building a website for someone: create a
1570
+ project, read its AGENTS.md, run it, sign the person in, and bring the site
1571
+ into the Uniweb app. The person only uses their browser, to sign in and to
1572
+ pay when the site goes live; the guide has the agent run every command.
1573
+
1574
+ Inside a Uniweb project it starts from that project instead of creating one.
1575
+ It asks nothing, writes nothing and makes no request, so it is safe to run
1576
+ anywhere. To run a site's dev server, use \`uniweb dev\`.
1532
1577
  `,
1533
1578
  dev: `
1534
1579
  ${colors.cyan}${colors.bright}uniweb dev${colors.reset} ${colors.dim}— Start a dev server for a site${colors.reset}
@@ -2075,6 +2120,7 @@ ${colors.bright}Usage:${colors.reset}
2075
2120
  uniweb <command> [options]
2076
2121
 
2077
2122
  ${colors.bright}Commands:${colors.reset}
2123
+ start How an AI agent begins a site (a guide; changes nothing)
2078
2124
  create [name] Create a new project
2079
2125
  clone <site-uuid> Clone a backend site into a local file project
2080
2126
  add <type> [name] Add a foundation, site, or extension to a project
@@ -311,12 +311,15 @@ export async function ensureRegistryAuth({
311
311
  `\x1b[31m✗\x1b[0m ${reason} ${command} requires a Uniweb account, and the CLI is non-interactive (CI / no TTY / --non-interactive).`
312
312
  )
313
313
  console.error(' Options:')
314
- console.error(` • Set UNIWEB_TOKEN to a bearer token.`)
314
+ // ⛔ Not "run `login` interactively": with no one at the terminal this is often an AI
315
+ // agent working for someone who does not use one, and it passes the line on. A browser
316
+ // sign-in asks nothing here. *(It said "interactively" until 2026-10-26.)*
315
317
  console.error(
316
- ` • Set UNIWEB_USERNAME + UNIWEB_PASSWORD to log in non-interactively.`
318
+ ` • Run \`${prefix} login --browser\` first — someone signs in, in a browser on this machine — then re-run.`
317
319
  )
320
+ console.error(` • Set UNIWEB_TOKEN to a bearer token.`)
318
321
  console.error(
319
- ` • Run \`${prefix} login\` interactively first, then re-run.`
322
+ ` • Set UNIWEB_USERNAME + UNIWEB_PASSWORD to log in non-interactively.`
320
323
  )
321
324
  process.exit(1)
322
325
  }
@@ -515,7 +518,9 @@ export async function awaitBrowserCallback({
515
518
  res.end(
516
519
  `<!doctype html><html><body style="font-family:system-ui,sans-serif;text-align:center;padding:60px">` +
517
520
  `<h2 style="color:${failed ? '#dc2626' : '#16a34a'}">${failed ? errTitle : okTitle}</h2>` +
518
- `<p>You can close this tab and return to your terminal.</p></body></html>`
521
+ // ⛔ Not "return to your terminal": the person signing in may have none — an AI
522
+ // agent ran the login for them. *(It said so until 2026-10-26.)*
523
+ `<p>You can close this tab.</p></body></html>`
519
524
  )
520
525
  cleanup()
521
526
  resolve(failed ? { error: verdict.error } : { value: verdict.value })
@@ -817,11 +822,16 @@ export async function runRegistryLogin({ apiBase, args = [] } = {}) {
817
822
  } else {
818
823
  // Only what works without a terminal: `--password` and `--token-paste` prompt. The
819
824
  // command keeps the backend named, or re-run as printed it would go to the default.
825
+ // ⭐ `--browser` first: it asks nothing here, and it is the way for an AI agent working
826
+ // for someone who signs in in their browser. *(It was missing until 2026-10-26.)*
820
827
  const server = args.some((a) => a === '--server' || a.startsWith('--server='))
821
828
  ? ` --server ${key}`
822
829
  : ''
823
830
  console.error(
824
- '\x1b[31m✗\x1b[0m Without a terminal, a login needs a way to sign in that asks nothing:'
831
+ '\x1b[31m✗\x1b[0m Without a terminal, a login needs a way to sign in that asks nothing here:'
832
+ )
833
+ console.error(
834
+ ` ${getCliPrefix()} login${server} --browser (someone signs in, in a browser on this machine)`
825
835
  )
826
836
  console.error(
827
837
  ` ${getCliPrefix()} login${server} --token <bearer> (with --org @org or --personal)`
@@ -31,6 +31,10 @@ const STARTER_DIR = join(__dirname, '..', '..', 'starter')
31
31
  * @param {string} context.projectName - Workspace/project name
32
32
  * @param {string[]} context.workspaceGlobs - Workspace glob patterns
33
33
  * @param {Object} context.scripts - Root package.json scripts
34
+ * @param {Array<{ path: string, foundation?: boolean, extension?: boolean, site?: boolean }>} context.packages -
35
+ * The packages the project starts with, each by its folder and kind, for the README's
36
+ * table. `[]` for a blank workspace, which the README tells how to grow.
37
+ * @param {boolean} [context.cloned] - The project is a `uniweb clone` of a site in the app
34
38
  * @param {Object} [options] - Processing options
35
39
  */
36
40
  export async function scaffoldWorkspace(targetDir, context, options = {}) {
@@ -60,6 +60,32 @@ export function findWorkspaceRoot(startDir = process.cwd()) {
60
60
  return null
61
61
  }
62
62
 
63
+ /**
64
+ * Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
65
+ * — also requires that the workspace's root package.json declares uniweb
66
+ * as a dep or devDep. Otherwise `update` would write AGENTS.md and edit
67
+ * package.json files in unrelated monorepos, and `start` would take one for
68
+ * a Uniweb project.
69
+ *
70
+ * @param {string} [cwd]
71
+ * @returns {string|null} - Workspace root path or null
72
+ */
73
+ export function findUniwebWorkspace(cwd = process.cwd()) {
74
+ const workspaceDir = findWorkspaceRoot(cwd)
75
+ if (!workspaceDir) return null
76
+ const pkgPath = join(workspaceDir, 'package.json')
77
+ if (!existsSync(pkgPath)) return null
78
+ try {
79
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
80
+ const hasUniwebDep = !!(
81
+ pkg.devDependencies?.uniweb || pkg.dependencies?.uniweb
82
+ )
83
+ return hasUniwebDep ? workspaceDir : null
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
63
89
  /**
64
90
  * Get workspace packages from pnpm-workspace.yaml or package.json workspaces —
65
91
  * read as pnpm reads them (`workspacePackages`, ./workspace-packages.js).
@@ -5,14 +5,14 @@ import { H1, H2, P, Link, cn } from '@uniweb/kit'
5
5
  *
6
6
  * A versatile content section that handles headings, text, and links.
7
7
  * Uses semantic tokens so it adapts to any theme context automatically.
8
+ *
9
+ * No fallbacks needed: the runtime hands every section the full content shape
10
+ * (empty strings and arrays where the markdown has nothing), and fills each
11
+ * param from its `default` in meta.js.
8
12
  */
9
13
  export default function Section({ content, params }) {
10
- const { title, pretitle, subtitle, paragraphs = [], links = [], images = [] } = content || {}
11
-
12
- const {
13
- align = 'center',
14
- width = 'default',
15
- } = params || {}
14
+ const { title, pretitle, subtitle, paragraphs, links, images } = content
15
+ const { align, width } = params
16
16
 
17
17
  const alignments = {
18
18
  left: 'text-left',
@@ -11,4 +11,4 @@ align: center
11
11
  This is a minimal starting point for your Uniweb project. Edit the content in `site/pages/` and build section types in `src/sections/`.
12
12
 
13
13
  [About](/about)
14
- [Documentation](https://github.com/uniweb)
14
+ [Documentation](https://uniweb.io/docs)
@@ -1,87 +1,169 @@
1
1
  # {{projectName}}
2
2
 
3
- A website built with [Uniweb](https://github.com/uniweb/cli) — a component web platform that separates content from code.
3
+ A website made with [Uniweb](https://uniweb.io): an open-source React framework and a connected
4
+ visual authoring platform.
4
5
 
5
- {{> quick-start}}
6
+ **Developers define the vocabulary. Authors compose with it. Uniweb keeps both sides in sync.**
6
7
 
7
- ## Project Structure
8
+ This project and the Uniweb app are two interfaces to the same website. Here, in code and git,
9
+ you build its **foundation**: React components, called section types, and the schemas of its
10
+ structured content. In the app, authors compose pages from those section types and manage records
11
+ through those schemas: visually, with live previews, and with no markdown, YAML or git to learn.
12
+ You build the components. Uniweb builds the authoring experience.
13
+ {{#if cloned}}
8
14
 
15
+ This project was cloned from a site in the Uniweb app (`uniweb clone`), so it is already in sync
16
+ with it. Its foundation is a registered one, not a folder of this project.
17
+ {{/if}}
18
+
19
+ ## Quick start
20
+
21
+ {{#if packages.length}}
22
+ ```bash
23
+ pnpm install
24
+ pnpm dev # prints the address it serves the site at
9
25
  ```
10
- {{projectName}}/
11
- ├── src/ # Foundation package (the site's source code; pkg name: src)
12
- │ ├── sections/ # Section types (selectable by content authors)
13
- │ ├── styles.css # Tailwind CSS v4 theme
14
- │ ├── main.js # Foundation declarations (name, vars, defaultLayout)
15
- │ └── vite.config.js # defineFoundationConfig()
16
- │
17
- ├── site/ # Content (markdown)
18
- │ ├── pages/ # Content pages
19
- │ │ └── home/
20
- │ │ ├── page.yml
21
- │ │ └── 1-welcome.md
22
- │ ├── site.yml # Site configuration (foundation: src)
23
- │ └── vite.config.js # defineSiteConfig()
24
- │
25
- └── AGENTS.md # Developer guide (human + AI)
26
+
27
+ Edit a page or a section type and the page updates as you save. With npm, use `npm install`,
28
+ `npm run dev` and `npx uniweb`.
29
+ {{else}}
30
+ This workspace started empty. Add a foundation and a site, then run them:
31
+
32
+ ```bash
33
+ npx uniweb add project <name> # a foundation in <name>/src, a site in <name>/site
34
+ pnpm install
35
+ pnpm uniweb dev # prints the address it serves the site at
26
36
  ```
37
+ {{/if}}
27
38
 
28
- A site is pure content. A foundation is the site's source code — that's why it lives in `src/`.
39
+ ## What's here
29
40
 
30
- ## Content Authoring
41
+ | Folder | What it holds |
42
+ |---|---|
43
+ {{#each packages}}
44
+ | `{{path}}/` | {{#if foundation}}a foundation: section types in `sections/`, written in React, and any data schemas in `schemas/`{{/if}}{{#if extension}}an extension: more section types, loaded beside a foundation{{/if}}{{#if site}}a site: its pages in `pages/`, with `site.yml` and `theme.yml`{{/if}} |
45
+ {{/each}}
46
+ | `AGENTS.md` | the manual for this project and its version of Uniweb, for you and for AI agents |
31
47
 
32
- Content lives in markdown files under `site/pages/`:
48
+ Each markdown file in a site's `pages/` is one section of a page. Its frontmatter names a
49
+ section type, and the component receives the content already parsed. For example:
33
50
 
34
51
  ```markdown
35
52
  ---
36
- type: Section
37
- theme: light
53
+ type: Hero
38
54
  ---
39
55
 
40
- # Welcome
41
-
42
- Your content here.
56
+ # Build something great
43
57
 
44
- [Call to Action](#)
58
+ Edit this text, not the component.
45
59
  ```
46
60
 
47
- {{> adding-pages}}
48
-
49
- ## Component Development
50
-
51
- Section types live in `src/sections/`. Each is a folder with an `index.jsx` and a `meta.js`:
52
-
53
61
  ```jsx
54
- // src/sections/Hero/index.jsx
62
+ // sections/Hero/index.jsx
55
63
  import { H1, P } from '@uniweb/kit'
56
64
 
57
65
  export default function Hero({ content }) {
58
- const { title, paragraphs = [] } = content || {}
59
-
60
66
  return (
61
- <div className="max-w-4xl mx-auto px-6">
62
- <H1 text={title} className="text-heading text-4xl font-bold" />
63
- <P text={paragraphs} className="text-body mt-4" />
67
+ <div className="mx-auto max-w-4xl px-6">
68
+ <H1 text={content.title} className="text-4xl font-bold text-heading" />
69
+ <P text={content.paragraphs} className="mt-4 text-body" />
64
70
  </div>
65
71
  )
66
72
  }
67
73
  ```
68
74
 
69
- Components receive parsed content from markdown. Classes like `text-heading` and `text-body` are semantic tokens — the site controls their actual colors through theming, so the same component adapts to any context automatically.
75
+ `text-heading` and `text-body` are theme tokens. A site's `theme.yml` sets their colors, so one
76
+ component works in light and dark sections, and on every site that uses it.
70
77
 
71
- {{> components-docs}}
78
+ ## Connect the authoring app
72
79
 
73
- ## Building
80
+ Start with a normal project, and connect the app when you want visual authoring:
74
81
 
75
82
  ```bash
76
- # Build foundation (generates dist/foundation.js)
77
- cd foundation && pnpm build
83
+ pnpm uniweb login # sign in to your Uniweb account
84
+ pnpm uniweb sync # the first sync brings the site and its foundation into the app
85
+ ```
86
+
87
+ From then on, the CLI is the bridge between the two workflows:
88
+
89
+ | Command | What it does |
90
+ |---|---|
91
+ | `pnpm uniweb push` | sends your section types, schemas and content to the app: a draft, not live |
92
+ | `pnpm uniweb pull` | brings authors' edits into your files, as a git merge |
93
+ | `pnpm uniweb sync` | takes your teammates' commits and the authors' edits, then pushes yours |
94
+ | `pnpm uniweb publish` | pushes, then takes the site live on Uniweb hosting, a paid service |
95
+
96
+ Authors never need to think about synchronization: in the app, their changes appear live. On
97
+ your side, edits to different sections combine, and nothing overwrites an author's edit. When
98
+ the same section changed on both sides, `sync` stops at a git conflict for you to settle. Like
99
+ git, a push sends what you have committed; `--commit` commits the site's content first.
100
+
101
+ ### Structured content, editable without an admin app
102
+
103
+ A section type declares the data it shows, by schema, in its `meta.js`:
78
104
 
79
- # Build site for production
80
- cd site && pnpm build
105
+ ```js
106
+ export default {
107
+ data: {
108
+ people: '@std/person', // a standard schema, from @uniweb/schemas
109
+ events: '@/event' // this foundation's own, in schemas/event.yml
110
+ }
111
+ }
81
112
  ```
82
113
 
83
- Deploy `site/dist/` to any static host (Vercel, Netlify, Cloudflare Pages, etc.)
114
+ A standard schema needs `@uniweb/schemas` as a dependency of the foundation. In the app, authors
115
+ get forms and records for those schemas. No separate admin application is needed to make the
116
+ data editable.
117
+
118
+ ## Other ways to use it
119
+
120
+ The framework works on its own, too.
121
+
122
+ - **Content in files only.** If you manage the content yourself, the site needs no Uniweb account.
123
+ `pnpm build` makes each site's `dist/`, a static site for any host, and `pnpm uniweb deploy`
124
+ ships it to GitHub Pages, Cloudflare Pages, Netlify, Vercel or S3 + CloudFront.
125
+ - **Foundations for your clients' sites.** If the sites belong to your clients, who make and edit
126
+ them in the app, this project is where you build and test the foundation.
127
+ `pnpm uniweb register` publishes it to Uniweb for their sites.
128
+
129
+ ## Working with AI agents
130
+
131
+ A Uniweb project gives an agent a system to work within, its section types, schemas and content
132
+ conventions, instead of a blank canvas. What an agent builds here, authors can use in the app
133
+ after the next sync.
134
+
135
+ [AGENTS.md](./AGENTS.md) is written for developers and AI agents alike. Many coding agents read it
136
+ on their own when they start in this folder; point any other agent at it first.
137
+ `npx uniweb@latest update` moves the project to the latest release of Uniweb, and AGENTS.md with
138
+ it.
139
+
140
+ Some prompts to try:
141
+
142
+ **Converting an existing design:**
143
+ ```
144
+ I have a landing page design in `homepage.jsx`. Convert it into this Uniweb
145
+ project. Decompose each visual section into a section type in the foundation,
146
+ and extract all text content into markdown files in the site's pages/home/.
147
+ The final page should render identically to the original. See AGENTS.md.
148
+ ```
149
+
150
+ **Adding a section type:**
151
+ ```
152
+ Create a Pricing section type with 3-column cards. It should work in light and
153
+ dark sections. Use content.items for the pricing tiers — each item has a title,
154
+ a paragraph (the price), and a link (the call to action).
155
+ ```
156
+
157
+ **Adding a page:**
158
+ ```
159
+ Add a /contact page with a hero section and a contact details section. Reuse
160
+ the Hero section type from the homepage.
161
+ ```
84
162
 
85
- {{> ai-assistance}}
163
+ ## Learn more
86
164
 
87
- {{> learn-more}}
165
+ - [Documentation](https://uniweb.io/docs): guides and reference. For AI agents, its index is
166
+ https://uniweb.io/llms.txt
167
+ - [Uniweb app](https://uniweb.app): the visual authoring platform
168
+ - `pnpm uniweb <command> --help`: every command's options
169
+ - `pnpm uniweb doctor`: checks the project's configuration