uniweb 1.0.0 → 1.0.2

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.
Files changed (129) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +532 -3
  3. package/package.json +60 -11
  4. package/partials/adding-pages.hbs +77 -0
  5. package/partials/agents.md +3000 -0
  6. package/partials/ai-assistance.hbs +28 -0
  7. package/partials/components-docs.hbs +13 -0
  8. package/partials/config-reference.hbs +66 -0
  9. package/partials/deployment.hbs +13 -0
  10. package/partials/learn-more.hbs +6 -0
  11. package/partials/quick-start.hbs +8 -0
  12. package/partials/search-docs.hbs +110 -0
  13. package/src/backend/asset-download.js +222 -0
  14. package/src/backend/client.js +986 -0
  15. package/src/backend/foundation-bring-along.js +692 -0
  16. package/src/backend/payment-handoff.js +362 -0
  17. package/src/backend/service-request.js +210 -0
  18. package/src/backend/site-media.js +255 -0
  19. package/src/backend/site-preview.js +77 -0
  20. package/src/backend/site-sync.js +2515 -0
  21. package/src/backend/workspace.js +234 -0
  22. package/src/commands/add.js +1900 -0
  23. package/src/commands/build.js +1214 -0
  24. package/src/commands/clone.js +466 -0
  25. package/src/commands/content.js +202 -0
  26. package/src/commands/deploy.js +537 -0
  27. package/src/commands/dev.js +202 -0
  28. package/src/commands/docs.js +530 -0
  29. package/src/commands/doctor.js +1982 -0
  30. package/src/commands/export.js +116 -0
  31. package/src/commands/families.js +82 -0
  32. package/src/commands/forget.js +197 -0
  33. package/src/commands/handoff.js +21 -0
  34. package/src/commands/i18n.js +1707 -0
  35. package/src/commands/inspect.js +375 -0
  36. package/src/commands/invite.js +24 -0
  37. package/src/commands/org.js +96 -0
  38. package/src/commands/publish.js +1087 -0
  39. package/src/commands/pull.js +1257 -0
  40. package/src/commands/push.js +663 -0
  41. package/src/commands/refresh.js +212 -0
  42. package/src/commands/register.js +1092 -0
  43. package/src/commands/rename.js +647 -0
  44. package/src/commands/site.js +358 -0
  45. package/src/commands/snapshot.js +461 -0
  46. package/src/commands/starter.js +206 -0
  47. package/src/commands/status.js +302 -0
  48. package/src/commands/sync.js +93 -0
  49. package/src/commands/template.js +107 -0
  50. package/src/commands/update.js +887 -0
  51. package/src/commands/validate.js +376 -0
  52. package/src/framework-index.json +248 -0
  53. package/src/index.js +2241 -0
  54. package/src/templates/fetchers/github.js +113 -0
  55. package/src/templates/fetchers/npm.js +85 -0
  56. package/src/templates/fetchers/release.js +92 -0
  57. package/src/templates/index.js +190 -0
  58. package/src/templates/processor.js +333 -0
  59. package/src/templates/resolver.js +232 -0
  60. package/src/templates/validator.js +155 -0
  61. package/src/utils/agents-stamp.js +110 -0
  62. package/src/utils/args.js +126 -0
  63. package/src/utils/asset-upload.js +270 -0
  64. package/src/utils/bytes.js +30 -0
  65. package/src/utils/code-upload.js +394 -0
  66. package/src/utils/config.js +417 -0
  67. package/src/utils/conformance.js +274 -0
  68. package/src/utils/dep-survey.js +176 -0
  69. package/src/utils/destination-prompt.js +146 -0
  70. package/src/utils/discover.js +75 -0
  71. package/src/utils/env.js +22 -0
  72. package/src/utils/exit.js +30 -0
  73. package/src/utils/fetch-retry.js +109 -0
  74. package/src/utils/flag-guard.js +246 -0
  75. package/src/utils/foundation-name.js +179 -0
  76. package/src/utils/git.js +342 -0
  77. package/src/utils/host-prompt.js +51 -0
  78. package/src/utils/install-integrity.js +210 -0
  79. package/src/utils/interactive.js +118 -0
  80. package/src/utils/json-file.js +72 -0
  81. package/src/utils/names.js +91 -0
  82. package/src/utils/placement.js +151 -0
  83. package/src/utils/pm.js +165 -0
  84. package/src/utils/preview.js +20 -0
  85. package/src/utils/pull-written.js +132 -0
  86. package/src/utils/records-guard.js +88 -0
  87. package/src/utils/registry-auth.js +994 -0
  88. package/src/utils/registry-orgs.js +290 -0
  89. package/src/utils/scaffold.js +483 -0
  90. package/src/utils/schemaless-report.js +30 -0
  91. package/src/utils/semver-precedence.js +80 -0
  92. package/src/utils/session-file.js +95 -0
  93. package/src/utils/site-content-refs.js +23 -0
  94. package/src/utils/site-copies.js +123 -0
  95. package/src/utils/site-data-upload.js +302 -0
  96. package/src/utils/site-identity.js +137 -0
  97. package/src/utils/snapshot-settings.js +112 -0
  98. package/src/utils/update-check.js +302 -0
  99. package/src/utils/uwx-read.js +157 -0
  100. package/src/utils/workspace.js +223 -0
  101. package/src/versions.js +467 -0
  102. package/starter/foundation/main.js +50 -0
  103. package/starter/foundation/sections/Section/index.jsx +95 -0
  104. package/starter/foundation/sections/Section/meta.js +44 -0
  105. package/starter/site/pages/about/1-about.md.hbs +13 -0
  106. package/starter/site/pages/about/page.yml +3 -0
  107. package/starter/site/pages/home/1-welcome.md.hbs +14 -0
  108. package/starter/site/pages/home/page.yml +3 -0
  109. package/templates/foundation/_gitignore +5 -0
  110. package/templates/foundation/components/.gitkeep +0 -0
  111. package/templates/foundation/main.js.hbs +33 -0
  112. package/templates/foundation/package.json.hbs +51 -0
  113. package/templates/foundation/sections/.gitkeep +0 -0
  114. package/templates/foundation/styles.css +6 -0
  115. package/templates/foundation/vite.config.js +3 -0
  116. package/templates/site/_gitignore +12 -0
  117. package/templates/site/entry.js +7 -0
  118. package/templates/site/index.html.hbs +13 -0
  119. package/templates/site/package.json.hbs +27 -0
  120. package/templates/site/public/favicon.svg +7 -0
  121. package/templates/site/site.yml.hbs +91 -0
  122. package/templates/site/theme.yml +99 -0
  123. package/templates/site/vite.config.js +7 -0
  124. package/templates/workspace/AGENTS.md.hbs +2 -0
  125. package/templates/workspace/README.md.hbs +87 -0
  126. package/templates/workspace/_gitignore +29 -0
  127. package/templates/workspace/_vscode/settings.json +6 -0
  128. package/templates/workspace/package.json.hbs +20 -0
  129. package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
@@ -0,0 +1,376 @@
1
+ /**
2
+ * uniweb validate - Check your content against the data schemas your
3
+ * foundation declares.
4
+ *
5
+ * For each section that consumes file-based data, this resolves the schema the
6
+ * foundation bound to that input (via `meta.js` `data:`) and checks the data
7
+ * items against it — and every record file in the records directory against the
8
+ * schema its folder names, whether or not a section reads it. It answers "does my
9
+ * data match what I promised?" — distinct from `doctor`, which checks your project
10
+ * against framework conventions.
11
+ *
12
+ * ⭐ A violation fails it (exit 1); `--lax` reports without failing. It warned by
13
+ * default until 2026-09-24, with `--strict` to fail — a gate that passes is one
14
+ * nobody reads, and `push` / `publish` now refuse the same findings. The live render
15
+ * path stays tolerant. Remote (`url:`) inputs are not in the project, so they're
16
+ * reported as deferred, never silently skipped. The value of an entity reference is
17
+ * not checked against the entity it names. ⛔ This said references were "reported as
18
+ * deferred" until 2026-09-24; no check ever reported them.
19
+ */
20
+
21
+ import { existsSync, readFileSync } from 'node:fs'
22
+ import { join, resolve, basename } from 'node:path'
23
+ import yaml from 'js-yaml'
24
+ import { validateDataInputs } from '@uniweb/build'
25
+ import { discoverFoundations, discoverSites } from '../utils/discover.js'
26
+ import { findWorkspaceRoot } from '../utils/workspace.js'
27
+
28
+ const colors = {
29
+ reset: '\x1b[0m',
30
+ bright: '\x1b[1m',
31
+ dim: '\x1b[2m',
32
+ red: '\x1b[31m',
33
+ green: '\x1b[32m',
34
+ yellow: '\x1b[33m',
35
+ blue: '\x1b[36m'
36
+ }
37
+
38
+ const log = console.log
39
+ const success = (msg) => log(`${colors.green}✓${colors.reset} ${msg}`)
40
+ const warn = (msg) => log(`${colors.yellow}⚠${colors.reset} ${msg}`)
41
+ const error = (msg) => console.error(`${colors.red}✗${colors.reset} ${msg}`)
42
+ const info = (msg) => log(`${colors.blue}→${colors.reset} ${msg}`)
43
+
44
+ /**
45
+ * Read a flag that takes a value: `--site foo` or `--site=foo`.
46
+ */
47
+ function flagValue(args, name) {
48
+ const eq = args.find((a) => a.startsWith(`${name}=`))
49
+ if (eq) return eq.slice(name.length + 1)
50
+ const idx = args.indexOf(name)
51
+ if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('--'))
52
+ return args[idx + 1]
53
+ return null
54
+ }
55
+
56
+ function loadSiteYml(dir) {
57
+ for (const f of ['site.yml', 'site.yaml']) {
58
+ const p = join(dir, f)
59
+ if (existsSync(p)) {
60
+ try {
61
+ return yaml.load(readFileSync(p, 'utf8'))
62
+ } catch {
63
+ return null
64
+ }
65
+ }
66
+ }
67
+ return null
68
+ }
69
+
70
+ /**
71
+ * Validate one site against its local foundation.
72
+ *
73
+ * @returns {Promise<Object>} { site, foundation, status, report?, reason? }
74
+ * status: 'checked' | 'skipped'
75
+ */
76
+ async function validateSite(site, foundations, workspaceDir) {
77
+ const sitePath = join(workspaceDir, site.path)
78
+ const siteYml = loadSiteYml(sitePath)
79
+ const foundationName = siteYml?.foundation
80
+
81
+ // No local foundation to check against → out of static scope (the schemas
82
+ // live in the foundation; a registry-ref / URL foundation isn't on disk).
83
+ if (!foundationName) {
84
+ return {
85
+ site: site.name,
86
+ status: 'skipped',
87
+ reason: 'no foundation declared in site.yml (runtime-loaded?)'
88
+ }
89
+ }
90
+ const match = foundations.find(
91
+ (f) => f.name === foundationName || basename(f.path) === foundationName
92
+ )
93
+ if (!match) {
94
+ return {
95
+ site: site.name,
96
+ status: 'skipped',
97
+ reason: `foundation "${foundationName}" is not a local workspace foundation — its schemas aren't on disk to check against`
98
+ }
99
+ }
100
+
101
+ const foundationPath = join(workspaceDir, match.path)
102
+ const report = await validateDataInputs({
103
+ siteRoot: sitePath,
104
+ foundationPath
105
+ })
106
+ return { site: site.name, foundation: match.name, status: 'checked', report }
107
+ }
108
+
109
+ /**
110
+ * Print one site's findings in human form. Groups by the (file, schema) pair
111
+ * so a collection feeding many sections lists its findings once, with the
112
+ * sections that use it — every link of the chain (route › section › key › file
113
+ * › item › field) is present, de-duped.
114
+ */
115
+ function printSiteHuman(result) {
116
+ log('')
117
+ if (result.status === 'skipped') {
118
+ warn(
119
+ `${colors.bright}${result.site}${colors.reset} — skipped: ${result.reason}`
120
+ )
121
+ return
122
+ }
123
+
124
+ const { report, foundation } = result
125
+ const { violations, deferred, setupErrors, summary } = report
126
+ const header = `${colors.bright}${result.site}${colors.reset} ${colors.dim}(foundation: ${foundation})${colors.reset}`
127
+
128
+ if (violations.length === 0 && setupErrors.length === 0) {
129
+ success(
130
+ `${header} — ${summary.records} record(s) / ${summary.schemas} schema(s) conform`
131
+ )
132
+ } else {
133
+ info(header)
134
+ }
135
+
136
+ // Group violations by file+schema pair.
137
+ const groups = new Map()
138
+ for (const v of violations) {
139
+ const key = `${v.file} ${v.schema}`
140
+ if (!groups.has(key))
141
+ groups.set(key, {
142
+ file: v.file,
143
+ schema: v.schema,
144
+ users: v.users,
145
+ findings: []
146
+ })
147
+ groups.get(key).findings.push(v)
148
+ }
149
+ for (const g of groups.values()) {
150
+ log(
151
+ ` ${colors.red}✗${colors.reset} ${g.file} ${colors.dim}· schema ${g.schema}${colors.reset}`
152
+ )
153
+ for (const u of dedupeUsers(g.users)) {
154
+ log(
155
+ ` ${colors.dim}used by${colors.reset} ${u.route} › ${u.section} › data.${u.key}`
156
+ )
157
+ }
158
+ for (const f of g.findings) {
159
+ log(
160
+ ` ${colors.yellow}•${colors.reset} item ${colors.bright}"${f.item}"${colors.reset} › ${f.field} — ${f.message}`
161
+ )
162
+ }
163
+ }
164
+
165
+ for (const e of setupErrors) {
166
+ log(` ${colors.red}✗${colors.reset} ${e.file} — ${e.message}`)
167
+ for (const u of dedupeUsers(e.users)) {
168
+ log(
169
+ ` ${colors.dim}used by${colors.reset} ${u.route} › ${u.section} › data.${u.key}`
170
+ )
171
+ }
172
+ }
173
+
174
+ if (deferred.length > 0) {
175
+ log(` ${colors.dim}↪ deferred (not checked):${colors.reset}`)
176
+ for (const d of deferred) {
177
+ const extra = d.url
178
+ ? ` ${colors.dim}(${d.url})${colors.reset}`
179
+ : d.ref
180
+ ? ` ${colors.dim}(${d.ref})${colors.reset}`
181
+ : ''
182
+ log(
183
+ ` ${colors.dim}•${colors.reset} ${d.route} › ${d.section} › data.${d.key} — ${d.reason}${extra}`
184
+ )
185
+ }
186
+ }
187
+
188
+ log(
189
+ ` ${colors.dim}${summary.records} record(s) · ${summary.schemas} schema(s) · ` +
190
+ `${summary.violations} violation(s) · ${summary.deferred} deferred${colors.reset}`
191
+ )
192
+ }
193
+
194
+ function dedupeUsers(users) {
195
+ const seen = new Set()
196
+ const out = []
197
+ for (const u of users || []) {
198
+ const k = `${u.route} ${u.section} ${u.key}`
199
+ if (seen.has(k)) continue
200
+ seen.add(k)
201
+ out.push(u)
202
+ }
203
+ return out
204
+ }
205
+
206
+ export async function validate(args = []) {
207
+ const asJson = args.includes('--json')
208
+ // A violation fails the run unless `--lax` says to only report it. (`--strict`, the
209
+ // flag that turned failing on until 2026-09-24, is simply the default now.)
210
+ const strict = !args.includes('--lax')
211
+ const siteFilter = flagValue(args, '--site')
212
+ const positional = args.find(
213
+ (a, i) => !a.startsWith('--') && args[i - 1] !== '--site'
214
+ )
215
+
216
+ const target = positional ? resolve(process.cwd(), positional) : process.cwd()
217
+ const workspaceDir = findWorkspaceRoot(target)
218
+
219
+ if (!workspaceDir) {
220
+ if (asJson)
221
+ log(
222
+ JSON.stringify(
223
+ { ok: false, error: 'not in a Uniweb workspace' },
224
+ null,
225
+ 2
226
+ )
227
+ )
228
+ else
229
+ error(
230
+ 'Not in a Uniweb workspace. Run this from a project root or a site directory.'
231
+ )
232
+ return { exitCode: 2 }
233
+ }
234
+
235
+ const [sites, foundations] = await Promise.all([
236
+ discoverSites(workspaceDir),
237
+ discoverFoundations(workspaceDir)
238
+ ])
239
+
240
+ // Select which sites to check: --site filter, an explicitly targeted site
241
+ // directory, or all sites in the workspace.
242
+ let selected = sites
243
+ if (siteFilter) {
244
+ selected = sites.filter(
245
+ (s) => s.name === siteFilter || basename(s.path) === siteFilter
246
+ )
247
+ if (selected.length === 0) {
248
+ const names = sites.map((s) => s.name).join(', ') || '(none)'
249
+ if (asJson)
250
+ log(
251
+ JSON.stringify(
252
+ {
253
+ ok: false,
254
+ error: `site "${siteFilter}" not found`,
255
+ sites: sites.map((s) => s.name)
256
+ },
257
+ null,
258
+ 2
259
+ )
260
+ )
261
+ else error(`Site "${siteFilter}" not found. Available: ${names}`)
262
+ return { exitCode: 2 }
263
+ }
264
+ } else if (target !== workspaceDir) {
265
+ const match = sites.find((s) => join(workspaceDir, s.path) === target)
266
+ if (match) selected = [match]
267
+ }
268
+
269
+ if (selected.length === 0) {
270
+ if (asJson)
271
+ log(
272
+ JSON.stringify({ ok: true, sites: [], note: 'no sites found' }, null, 2)
273
+ )
274
+ else warn('No sites found in this workspace.')
275
+ return { exitCode: 0 }
276
+ }
277
+
278
+ // The data pipeline (collectSiteContent / processQueries) prints progress
279
+ // via console.log. Route that to stderr while the engine runs so stdout stays
280
+ // clean — pure JSON for `--json`, just the report otherwise. `log` captured
281
+ // the original stdout writer at module load, so our own output is unaffected.
282
+ const results = []
283
+ const origConsoleLog = console.log
284
+ console.log = (...a) => process.stderr.write(a.join(' ') + '\n')
285
+ try {
286
+ for (const site of selected) {
287
+ try {
288
+ results.push(await validateSite(site, foundations, workspaceDir))
289
+ } catch (err) {
290
+ results.push({ site: site.name, status: 'error', reason: err.message })
291
+ }
292
+ }
293
+ } finally {
294
+ console.log = origConsoleLog
295
+ }
296
+
297
+ const totalViolations = results.reduce(
298
+ (n, r) => n + (r.report?.violations.length || 0),
299
+ 0
300
+ )
301
+ const totalSetupErrors = results.reduce(
302
+ (n, r) => n + (r.report?.setupErrors.length || 0),
303
+ 0
304
+ )
305
+ const hadError = results.some((r) => r.status === 'error')
306
+
307
+ if (asJson) {
308
+ const payload = {
309
+ ok: totalViolations === 0 && !hadError,
310
+ strict,
311
+ sites: results.map((r) => ({
312
+ site: r.site,
313
+ foundation: r.foundation || null,
314
+ status: r.status,
315
+ reason: r.reason || null,
316
+ ...(r.report || {})
317
+ })),
318
+ summary: {
319
+ sites: results.length,
320
+ violations: totalViolations,
321
+ setupErrors: totalSetupErrors,
322
+ deferred: results.reduce(
323
+ (n, r) => n + (r.report?.deferred.length || 0),
324
+ 0
325
+ )
326
+ }
327
+ }
328
+ log(JSON.stringify(payload, null, 2))
329
+ } else {
330
+ log('')
331
+ log(`${colors.blue}${colors.bright}Uniweb Validate${colors.reset}`)
332
+ log(
333
+ `${colors.dim}Checking content against the data schemas your foundation declares…${colors.reset}`
334
+ )
335
+ for (const r of results) {
336
+ if (r.status === 'error') {
337
+ log('')
338
+ error(
339
+ `${colors.bright}${r.site}${colors.reset} — could not check: ${r.reason}`
340
+ )
341
+ } else {
342
+ printSiteHuman(r)
343
+ }
344
+ }
345
+
346
+ log('')
347
+ log('─'.repeat(50))
348
+ if (totalViolations === 0 && !hadError) {
349
+ log('')
350
+ success(`${colors.bright}All data conforms.${colors.reset}`)
351
+ if (!strict && totalSetupErrors === 0) {
352
+ // nothing more to say
353
+ }
354
+ log('')
355
+ } else {
356
+ log('')
357
+ if (totalViolations > 0) {
358
+ const mode = strict
359
+ ? `${colors.red}error${colors.reset}`
360
+ : `${colors.yellow}warning${colors.reset}`
361
+ log(
362
+ `${totalViolations} violation(s) — reported as ${mode}${strict ? '' : ` ${colors.dim}(--lax: without it, this fails)${colors.reset}`}`
363
+ )
364
+ }
365
+ if (hadError)
366
+ log(`${colors.red}Some sites could not be checked.${colors.reset}`)
367
+ log('')
368
+ }
369
+ }
370
+
371
+ // Exit semantics: 2 = couldn't run; 1 = violations (unless --lax); 0 = clean, or
372
+ // violations reported under --lax. Setup/read failures are surfaced but don't fail.
373
+ if (hadError) return { exitCode: 2 }
374
+ if (totalViolations > 0 && strict) return { exitCode: 1 }
375
+ return { exitCode: 0 }
376
+ }
@@ -0,0 +1,248 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "generatedAt": "2026-10-13T14:23:15.630Z",
4
+ "packages": {
5
+ "@uniweb/api": {
6
+ "version": "1.0.1"
7
+ },
8
+ "@uniweb/build": {
9
+ "version": "1.0.1"
10
+ },
11
+ "@uniweb/content-reader": {
12
+ "version": "1.2.8"
13
+ },
14
+ "@uniweb/content-writer": {
15
+ "version": "1.0.1"
16
+ },
17
+ "@uniweb/core": {
18
+ "version": "1.0.1"
19
+ },
20
+ "@uniweb/frame-bridge": {
21
+ "version": "1.0.1"
22
+ },
23
+ "@uniweb/icons": {
24
+ "version": "1.0.1"
25
+ },
26
+ "@uniweb/kit": {
27
+ "version": "1.0.1"
28
+ },
29
+ "@uniweb/loom": {
30
+ "version": "1.0.1"
31
+ },
32
+ "@uniweb/press": {
33
+ "version": "1.0.1"
34
+ },
35
+ "@uniweb/projections": {
36
+ "version": "1.0.1"
37
+ },
38
+ "@uniweb/runtime": {
39
+ "version": "1.0.2"
40
+ },
41
+ "@uniweb/scene": {
42
+ "version": "1.0.1"
43
+ },
44
+ "@uniweb/schemas": {
45
+ "version": "1.0.1"
46
+ },
47
+ "@uniweb/scholar": {
48
+ "version": "1.0.1"
49
+ },
50
+ "@uniweb/semantic-parser": {
51
+ "version": "1.5.1"
52
+ },
53
+ "@uniweb/snapshot": {
54
+ "version": "1.0.1"
55
+ },
56
+ "@uniweb/templates": {
57
+ "version": "1.0.1"
58
+ },
59
+ "@uniweb/theming": {
60
+ "version": "1.0.1"
61
+ },
62
+ "@uniweb/unipress": {
63
+ "version": "1.0.2"
64
+ }
65
+ },
66
+ "templates": {
67
+ "marketing": {
68
+ "name": "Marketing",
69
+ "description": "Landing pages and marketing sites with Tailwind CSS v4",
70
+ "tags": [
71
+ "marketing",
72
+ "landing-page",
73
+ "tailwind"
74
+ ]
75
+ },
76
+ "academic": {
77
+ "name": "Academic",
78
+ "description": "Academic sites for researchers, labs, and departments",
79
+ "tags": [
80
+ "academic",
81
+ "research",
82
+ "portfolio"
83
+ ]
84
+ },
85
+ "docs": {
86
+ "name": "Docs",
87
+ "description": "Documentation sites with navigation levels",
88
+ "tags": [
89
+ "docs",
90
+ "documentation",
91
+ "technical"
92
+ ]
93
+ },
94
+ "international": {
95
+ "name": "International",
96
+ "description": "Multilingual corporate website with English, Spanish, and French",
97
+ "tags": [
98
+ "i18n",
99
+ "multilingual",
100
+ "corporate",
101
+ "business",
102
+ "localization"
103
+ ]
104
+ },
105
+ "blog": {
106
+ "name": "Blog",
107
+ "description": "A blog built on the @std/article standard schema. Markdown articles render as a card grid and as full posts on a parametric [slug] page. The first template to use a shared standard data schema — no custom schema file needed.",
108
+ "tags": [
109
+ "blog",
110
+ "articles",
111
+ "collections",
112
+ "standard-schema",
113
+ "data-schema"
114
+ ]
115
+ },
116
+ "dynamic": {
117
+ "name": "Dynamic",
118
+ "description": "Live API data fetching with loading states, transforms, and the portable data pattern",
119
+ "tags": [
120
+ "data",
121
+ "api",
122
+ "fetch",
123
+ "loading-states",
124
+ "dynamic"
125
+ ]
126
+ },
127
+ "store": {
128
+ "name": "Store",
129
+ "description": "Artisan e-commerce with a product catalog, Shopify Buy Button, journal blog, and stone-amber design",
130
+ "tags": [
131
+ "store",
132
+ "e-commerce",
133
+ "shopify",
134
+ "artisan",
135
+ "products"
136
+ ]
137
+ },
138
+ "learning": {
139
+ "name": "Learning",
140
+ "description": "Course-based learning site with quizzes, code challenges, and AI grading integration",
141
+ "tags": [
142
+ "learning",
143
+ "course",
144
+ "education",
145
+ "quiz",
146
+ "lms",
147
+ "tutorial"
148
+ ]
149
+ },
150
+ "extensions": {
151
+ "name": "Extensions",
152
+ "description": "Primary foundation with a visual effects extension, demonstrating multi-foundation sites",
153
+ "tags": [
154
+ "extensions",
155
+ "multi-foundation",
156
+ "visual-effects",
157
+ "runtime"
158
+ ]
159
+ },
160
+ "cv-loom": {
161
+ "name": "CV (Loom + Press)",
162
+ "description": "An academic CV whose sections are written with Loom {placeholder} expressions, filled in from a single profile at render time by the foundation's content handler, and downloadable as a branded Word document through Press. Ships with Charles Darwin's CV as sample data.",
163
+ "tags": [
164
+ "cv",
165
+ "loom",
166
+ "template-engine",
167
+ "content-handler",
168
+ "dynamic-prose",
169
+ "academic"
170
+ ]
171
+ },
172
+ "monograph": {
173
+ "name": "Monograph",
174
+ "description": "A richly illustrated long-form document — numbered chapters with nested subsections, figures with captions, data-driven specimen and measurement tables, and a citestyle bibliography. The same JSX drives the themed web preview and a branded .docx with headers, footers, and page numbers. Ships with Darwin's Galapagos observations as sample content.",
175
+ "tags": [
176
+ "monograph",
177
+ "docusite",
178
+ "press",
179
+ "document",
180
+ "figures",
181
+ "tables",
182
+ "bibliography"
183
+ ]
184
+ },
185
+ "academic-metrics": {
186
+ "name": "Academic Metrics",
187
+ "description": "A docusite whose content is also a downloadable Excel workbook. Aggregates academic metrics (publications, funding, supervisions) across multiple members of a university unit. Rich web previews + flat tabular xlsx sheets from the same data. Ships with three 19th-century naturalists as sample members.",
188
+ "tags": [
189
+ "academic-metrics",
190
+ "docusite",
191
+ "press",
192
+ "xlsx",
193
+ "report",
194
+ "members",
195
+ "aggregate",
196
+ "academic"
197
+ ]
198
+ },
199
+ "business-docs": {
200
+ "name": "Business Documents",
201
+ "description": "A complete business-document site: invoice and SOW records, pages listing them, and a filtered engagement report with XLSX export. Built on @uniweb/business-docs and demonstrates the same foundation that ships the unipress invoice template.",
202
+ "tags": [
203
+ "business-docs",
204
+ "invoice",
205
+ "sow",
206
+ "report",
207
+ "press",
208
+ "xlsx",
209
+ "docx",
210
+ "pagedjs"
211
+ ]
212
+ },
213
+ "paste": {
214
+ "name": "Paste a Design (Level 0)",
215
+ "description": "Bring an existing React design (AI-generated or hand-built) into Uniweb with zero rewiring — replace App.jsx and run. Ships lucide-react preinstalled and SPA-first. The Level 0 conversion starting point.",
216
+ "tags": [
217
+ "paste",
218
+ "level-0",
219
+ "convert",
220
+ "ai-generated",
221
+ "starter"
222
+ ]
223
+ },
224
+ "services": {
225
+ "name": "Services",
226
+ "description": "Local-services site built around an author-designed quote request form",
227
+ "tags": [
228
+ "services",
229
+ "local-business",
230
+ "forms",
231
+ "quote",
232
+ "trades"
233
+ ]
234
+ },
235
+ "conference": {
236
+ "name": "Conference",
237
+ "description": "A site that is also an app — sign-in, per-viewer data, and an editable programme. Ships a local backend so it runs with nothing installed.",
238
+ "tags": [
239
+ "app",
240
+ "api",
241
+ "backend",
242
+ "auth",
243
+ "permissions",
244
+ "conference"
245
+ ]
246
+ }
247
+ }
248
+ }