@crombolo/cli 0.0.0-stage → 0.9.1

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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/bin/crombolo.ts +2108 -0
  4. package/package.json +42 -4
  5. package/src/index.ts +7 -0
  6. package/src/operations/commands.ts +775 -0
  7. package/src/operations/sections.ts +423 -0
  8. package/src/templates.ts +309 -0
  9. package/templates/basic/bundle/bundle.json +45 -0
  10. package/templates/basic/bundle/nodes/36955f6e-d110-469e-b280-72096eccd148.json +38 -0
  11. package/templates/basic/files/components/homePage.tsx +22 -0
  12. package/templates/basic/files/schema/content-types/home-page.toml +28 -0
  13. package/templates/basic/files/schema/schema.toml +2 -0
  14. package/templates/basic/template.json +4 -0
  15. package/templates/demo/harbourstone/CREDITS.md +42 -0
  16. package/templates/demo/harbourstone/bundle/blobs/180cc8ef/distillery-from-the-cliff-path.jpg +0 -0
  17. package/templates/demo/harbourstone/bundle/blobs/68c9cfdf/bottle-brine-and-kiln.jpg +0 -0
  18. package/templates/demo/harbourstone/bundle/blobs/6be45535/bottle-oloroso-pier.jpg +0 -0
  19. package/templates/demo/harbourstone/bundle/blobs/6d8d6398/bottle-the-cove.jpg +0 -0
  20. package/templates/demo/harbourstone/bundle/blobs/7b4713f8/copper-pot-stills.jpg +0 -0
  21. package/templates/demo/harbourstone/bundle/blobs/c114b7d8/bottle-neap-tide.jpg +0 -0
  22. package/templates/demo/harbourstone/bundle/blobs/fd337567/casks-in-the-sea-facing-warehouse.jpg +0 -0
  23. package/templates/demo/harbourstone/bundle/bundle.json +197 -0
  24. package/templates/demo/harbourstone/bundle/nodes/069cc3c5-c9f5-4792-90c9-e2bfcc4855b6.json +35 -0
  25. package/templates/demo/harbourstone/bundle/nodes/13788fe7-1a45-4b48-8ae4-f4227e154e1e.json +35 -0
  26. package/templates/demo/harbourstone/bundle/nodes/180cc8ef-caf1-4316-b6a7-e3b5aee41ffd.json +60 -0
  27. package/templates/demo/harbourstone/bundle/nodes/22144db2-e214-4006-a0bb-53b5bd121fd6.json +35 -0
  28. package/templates/demo/harbourstone/bundle/nodes/26e87610-e2d6-4b47-bf92-3967f7186907.json +35 -0
  29. package/templates/demo/harbourstone/bundle/nodes/2caa099b-3884-472d-9cea-797a0f0c376f.json +35 -0
  30. package/templates/demo/harbourstone/bundle/nodes/2efbdb5b-7888-4a49-bca5-c0f85b76e472.json +42 -0
  31. package/templates/demo/harbourstone/bundle/nodes/2fe03891-1294-4344-8e24-47e8b212e5b0.json +35 -0
  32. package/templates/demo/harbourstone/bundle/nodes/38b009f7-03c0-4fda-b5c7-843ad6044e9c.json +35 -0
  33. package/templates/demo/harbourstone/bundle/nodes/3d312cc8-2937-40e1-ba14-fd781c9a862d.json +95 -0
  34. package/templates/demo/harbourstone/bundle/nodes/3dc30fc1-973a-4217-b26f-24902210be17.json +35 -0
  35. package/templates/demo/harbourstone/bundle/nodes/40107d59-f3e9-4592-aee8-5b03d4586cbd.json +101 -0
  36. package/templates/demo/harbourstone/bundle/nodes/483a0d3b-a10c-4c9d-8c81-d9607d37e968.json +35 -0
  37. package/templates/demo/harbourstone/bundle/nodes/4b760c0b-b83a-4eee-aa29-c225cfabf19b.json +35 -0
  38. package/templates/demo/harbourstone/bundle/nodes/4e5c7f3f-54cb-4328-ad61-bc4c2736b890.json +35 -0
  39. package/templates/demo/harbourstone/bundle/nodes/62f479ff-0875-453e-99ae-3bf1bec5e3c8.json +35 -0
  40. package/templates/demo/harbourstone/bundle/nodes/68c9cfdf-6b40-47da-a1d2-17256a6cc6db.json +60 -0
  41. package/templates/demo/harbourstone/bundle/nodes/6ad8f0f3-9334-47f1-b805-80c4b4b4fa88.json +101 -0
  42. package/templates/demo/harbourstone/bundle/nodes/6be45535-83e5-4469-acf0-4127016721ae.json +60 -0
  43. package/templates/demo/harbourstone/bundle/nodes/6d8d6398-738f-4f38-ad3c-e5d6e9a971fe.json +60 -0
  44. package/templates/demo/harbourstone/bundle/nodes/7b4713f8-d917-457d-a9d8-7dd01a3617b4.json +60 -0
  45. package/templates/demo/harbourstone/bundle/nodes/88ec1185-53b5-439a-a2ce-3f56603be4ff.json +35 -0
  46. package/templates/demo/harbourstone/bundle/nodes/97469df2-107c-4010-8c14-4de88f735529.json +35 -0
  47. package/templates/demo/harbourstone/bundle/nodes/a2eb439f-71d9-4ea6-89d7-c5bc72618281.json +35 -0
  48. package/templates/demo/harbourstone/bundle/nodes/a562f0a2-cb6e-4ead-a1ce-4d180632419f.json +101 -0
  49. package/templates/demo/harbourstone/bundle/nodes/a5e36a18-a2ef-4eb5-85b4-7e12b21cca06.json +35 -0
  50. package/templates/demo/harbourstone/bundle/nodes/b1876d3a-50a7-4358-84bf-a0899932ffa9.json +35 -0
  51. package/templates/demo/harbourstone/bundle/nodes/b4e38bbd-c1a3-431a-b7a9-5868aa32993a.json +35 -0
  52. package/templates/demo/harbourstone/bundle/nodes/c114b7d8-0138-404b-9d8f-a39d992577a2.json +60 -0
  53. package/templates/demo/harbourstone/bundle/nodes/cb4c569e-c28a-43b6-a4d7-d33bb544e973.json +136 -0
  54. package/templates/demo/harbourstone/bundle/nodes/cd681b4b-9c4e-4624-9c13-450aa6a9c135.json +101 -0
  55. package/templates/demo/harbourstone/bundle/nodes/d60ef12b-0f0a-4db5-9763-21fa52b8d4da.json +150 -0
  56. package/templates/demo/harbourstone/bundle/nodes/d7f13222-d060-4496-b86d-bef21da963c1.json +35 -0
  57. package/templates/demo/harbourstone/bundle/nodes/f36dd4a9-927c-4ca3-a790-c9c859ac7302.json +35 -0
  58. package/templates/demo/harbourstone/bundle/nodes/fd337567-df67-41ca-ae94-9d84d8d6042f.json +60 -0
  59. package/templates/demo/harbourstone/files/components/pages/contactPage.tsx +36 -0
  60. package/templates/demo/harbourstone/files/components/pages/contentPage.tsx +54 -0
  61. package/templates/demo/harbourstone/files/components/pages/expression.tsx +33 -0
  62. package/templates/demo/harbourstone/files/components/pages/homePage.tsx +71 -0
  63. package/templates/demo/harbourstone/files/components/pages/whiskyList.tsx +19 -0
  64. package/templates/demo/harbourstone/files/components/shared/bottling.tsx +59 -0
  65. package/templates/demo/harbourstone/files/components/shared/highlights.tsx +39 -0
  66. package/templates/demo/harbourstone/files/components/shared/layout.tsx +49 -0
  67. package/templates/demo/harbourstone/files/components/shared/section-intro.tsx +40 -0
  68. package/templates/demo/harbourstone/files/components/shared/site-footer.tsx +14 -0
  69. package/templates/demo/harbourstone/files/components/shared/site-header.tsx +57 -0
  70. package/templates/demo/harbourstone/files/css/site.css +720 -0
  71. package/templates/demo/harbourstone/files/schema/content-types/contact-page.toml +75 -0
  72. package/templates/demo/harbourstone/files/schema/content-types/content-page.toml +89 -0
  73. package/templates/demo/harbourstone/files/schema/content-types/expression.toml +78 -0
  74. package/templates/demo/harbourstone/files/schema/content-types/highlight.toml +27 -0
  75. package/templates/demo/harbourstone/files/schema/content-types/home-page.toml +104 -0
  76. package/templates/demo/harbourstone/files/schema/content-types/tasting-note.toml +26 -0
  77. package/templates/demo/harbourstone/files/schema/content-types/whisky-list.toml +35 -0
  78. package/templates/demo/harbourstone/files/schema/data-types/highlight-picker.toml +10 -0
  79. package/templates/demo/harbourstone/files/schema/data-types/tasting-note-picker.toml +11 -0
  80. package/templates/demo/harbourstone/files/schema/forms/contact.toml +119 -0
  81. package/templates/demo/harbourstone/files/schema/schema.toml +2 -0
  82. package/templates/demo/harbourstone/template.json +10 -0
  83. package/templates/starter/bundle/bundle.json +49 -0
  84. package/templates/starter/bundle/nodes/634dd948-cf83-4cad-9b7b-7c938b33da76.json +62 -0
  85. package/templates/starter/bundle/nodes/64d95359-d91a-4050-b440-3e5124a74999.json +62 -0
  86. package/templates/starter/bundle/nodes/d2997ebb-36ca-47cb-bbc8-31fefca787c0.json +45 -0
  87. package/templates/starter/files/components/pages/contentPage.tsx +25 -0
  88. package/templates/starter/files/components/shared/layout.tsx +24 -0
  89. package/templates/starter/files/components/shared/site-footer.tsx +9 -0
  90. package/templates/starter/files/components/shared/site-header.tsx +40 -0
  91. package/templates/starter/files/css/site.css +117 -0
  92. package/templates/starter/files/schema/content-types/content-page.toml +51 -0
  93. package/templates/starter/files/schema/schema.toml +2 -0
  94. package/templates/starter/template.json +4 -0
  95. package/README.md +0 -3
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Starter templates: what `crombolo init` writes, and the catalogue
3
+ * `--template list` prints.
4
+ *
5
+ * A template is a directory under `templates/`, one or two levels deep so a
6
+ * family can share a prefix (`demo/harbourstone`):
7
+ *
8
+ * template.json name and description, for the catalogue
9
+ * files/ copied verbatim into the new site
10
+ * bundle/ a content bundle, which the scaffolded `start` script
11
+ * applies; it carries its own media, so nothing else does
12
+ *
13
+ * Content arrives as a bundle rather than as seed code so that it is the same
14
+ * artifact `content export` produces: reviewable, committed with the site, and
15
+ * applied by the one import path that is already tested.
16
+ */
17
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
18
+ import { join, relative } from 'node:path'
19
+ import { siteMediaTypeFiles } from '@crombolo/schema'
20
+ import { VERSION } from '@crombolo/server'
21
+
22
+ /** `templates/`, beside `src/` and `bin/` in the published package. */
23
+ export const TEMPLATES_DIR = join(import.meta.dir, '..', 'templates')
24
+
25
+ /**
26
+ * A bundle whose server half a template wires into the site it scaffolds: the
27
+ * dependency, plus the import and the `bundles` entry in `crombolo.config.ts`.
28
+ *
29
+ * Written into the site's own files rather than hidden anywhere, because that
30
+ * import is the whole of what lets a bundle answer a request — so it has to be
31
+ * a line the developer owns from the first commit and can delete
32
+ * (`docs/17-bundles.md`).
33
+ */
34
+ export interface TemplateServerBundle {
35
+ /** The npm package, added to the site's dependencies. */
36
+ package: string
37
+ /** The factory it exports, imported by name and called in `bundles`. */
38
+ import: string
39
+ /**
40
+ * The range to depend on. Defaults to this release, which is right for a
41
+ * bundle that ships with the CMS; a third-party one names its own.
42
+ */
43
+ version?: string
44
+ }
45
+
46
+ export interface SiteTemplate {
47
+ /** `basic`, or `demo/harbourstone` for one of a family. */
48
+ id: string
49
+ name: string
50
+ description: string
51
+ dir: string
52
+ /** `bundles/<slug>` in the new site, when the template ships content. */
53
+ bundle?: string
54
+ /** Server bundles the scaffolded site runs; empty for most templates. */
55
+ serverBundles: TemplateServerBundle[]
56
+ }
57
+
58
+ const MANIFEST = 'template.json'
59
+
60
+ function read(dir: string, id: string): SiteTemplate | undefined {
61
+ const manifest = join(dir, MANIFEST)
62
+ if (!existsSync(manifest)) return undefined
63
+ const raw = JSON.parse(readFileSync(manifest, 'utf8')) as Record<string, unknown>
64
+ return {
65
+ id,
66
+ name: typeof raw.name === 'string' ? raw.name : id,
67
+ description: typeof raw.description === 'string' ? raw.description : '',
68
+ dir,
69
+ bundle: existsSync(join(dir, 'bundle')) ? `bundles/${id.replaceAll('/', '-')}` : undefined,
70
+ serverBundles: readServerBundles(raw.bundles),
71
+ }
72
+ }
73
+
74
+ /** `bundles` in `template.json`; anything malformed is dropped rather than guessed at. */
75
+ function readServerBundles(raw: unknown): TemplateServerBundle[] {
76
+ if (!Array.isArray(raw)) return []
77
+ const out: TemplateServerBundle[] = []
78
+ for (const entry of raw) {
79
+ if (!entry || typeof entry !== 'object') continue
80
+ const record = entry as Record<string, unknown>
81
+ const name = typeof record.package === 'string' ? record.package : ''
82
+ const imported = typeof record.import === 'string' ? record.import : ''
83
+ if (!name || !imported) continue
84
+ out.push({
85
+ package: name,
86
+ import: imported,
87
+ ...(typeof record.version === 'string' ? { version: record.version } : {}),
88
+ })
89
+ }
90
+ return out
91
+ }
92
+
93
+ /** Every template, in id order; a directory without a manifest is a family. */
94
+ export function listTemplates(root = TEMPLATES_DIR): SiteTemplate[] {
95
+ if (!existsSync(root)) return []
96
+ const out: SiteTemplate[] = []
97
+ for (const entry of readdirSync(root).sort()) {
98
+ const dir = join(root, entry)
99
+ if (!statSync(dir).isDirectory()) continue
100
+ const own = read(dir, entry)
101
+ if (own) {
102
+ out.push(own)
103
+ continue
104
+ }
105
+ for (const child of readdirSync(dir).sort()) {
106
+ const nested = read(join(dir, child), `${entry}/${child}`)
107
+ if (nested) out.push(nested)
108
+ }
109
+ }
110
+ return out
111
+ }
112
+
113
+ export function findTemplate(id: string, root = TEMPLATES_DIR): SiteTemplate | undefined {
114
+ return listTemplates(root).find((t) => t.id === id)
115
+ }
116
+
117
+ export interface ScaffoldOptions {
118
+ /** `--name`; also the package name, slugified. */
119
+ siteName?: string
120
+ /** `--postgres`: write a `.env` naming the Postgres variables instead of using the SQLite default. */
121
+ postgres?: boolean
122
+ template?: SiteTemplate
123
+ /**
124
+ * A bundle the site already has, for the `start` script to apply: the path,
125
+ * relative to the site, and any `content import` flags it needs beyond
126
+ * `--publish`. A template's own bundle is used when this is absent.
127
+ */
128
+ bundle?: { path: string; flags?: readonly string[] }
129
+ }
130
+
131
+ /** One file to write: literal text, or a path to copy (template media and bundles). */
132
+ export interface ScaffoldFile {
133
+ /** Relative to the new site, `/`-separated. */
134
+ path: string
135
+ text?: string
136
+ copyFrom?: string
137
+ }
138
+
139
+ const DEFAULT_SITE_NAME = 'My Site'
140
+
141
+ /** Kept in step with the root `package.json`, which is what the views are checked with. */
142
+ const TYPESCRIPT_VERSION = '^7.0.2'
143
+
144
+ /** Single quotes, because the file is TypeScript the site's own formatter owns. */
145
+ const quote = (value: string): string => `'${value.replace(/[\\']/g, '\\$&')}'`
146
+
147
+ /** A directory name npm accepts: lowercase, no spaces, no leading punctuation. */
148
+ export function packageNameFor(siteName: string): string {
149
+ const slug = siteName
150
+ .toLowerCase()
151
+ .replace(/[^a-z0-9]+/g, '-')
152
+ .replace(/^-+|-+$/g, '')
153
+ return slug || 'my-site'
154
+ }
155
+
156
+ function tsconfig(): string {
157
+ return `${JSON.stringify(
158
+ {
159
+ compilerOptions: {
160
+ target: 'ESNext',
161
+ module: 'ESNext',
162
+ moduleResolution: 'bundler',
163
+ jsx: 'react-jsx',
164
+ jsxImportSource: '@crombolo/crombolo',
165
+ strict: true,
166
+ allowImportingTsExtensions: true,
167
+ noEmit: true,
168
+ types: ['bun'],
169
+ },
170
+ },
171
+ null,
172
+ 2,
173
+ )}\n`
174
+ }
175
+
176
+ /** Search & sharing defaults, commented out: editors can also set them in Settings. */
177
+ const SEO_EXAMPLE = ` // Search & sharing defaults; each can also be set in Settings, which wins.
178
+ // seo: { titleSuffix: ' | My site', notFoundPage: '<page key>', indexable: true },
179
+ `
180
+
181
+ /**
182
+ * `crombolo.config.ts`. A template's server bundles are imported by name and
183
+ * listed in `bundles`, which is the only thing that lets their endpoints
184
+ * answer — so the scaffolded file carries a note saying so, and deleting the
185
+ * line is how a site declines one.
186
+ */
187
+ function config(siteName: string, bundles: readonly TemplateServerBundle[]): string {
188
+ const imports = bundles
189
+ .map((bundle) => `import { ${bundle.import} } from ${quote(bundle.package)}\n`)
190
+ .join('')
191
+ if (bundles.length === 0)
192
+ return `import { defineConfig } from '@crombolo/crombolo'
193
+
194
+ export default defineConfig({
195
+ siteName: ${quote(siteName)},
196
+ ${SEO_EXAMPLE}})
197
+ `
198
+ return `${imports}import { defineConfig } from '@crombolo/crombolo'
199
+
200
+ export default defineConfig({
201
+ siteName: ${quote(siteName)},
202
+ // Installing a bundle gives you its backoffice screen. This line is what lets
203
+ // its endpoints answer a request, so remove it to turn one off.
204
+ bundles: [${bundles.map((bundle) => `${bundle.import}()`).join(', ')}],
205
+ ${SEO_EXAMPLE}})
206
+ `
207
+ }
208
+
209
+ function packageJson(options: {
210
+ name: string
211
+ start: string
212
+ bundles: readonly TemplateServerBundle[]
213
+ }): string {
214
+ return `${JSON.stringify(
215
+ {
216
+ name: options.name,
217
+ private: true,
218
+ type: 'module',
219
+ scripts: { start: options.start, dev: 'bun --watch server.ts' },
220
+ dependencies: {
221
+ '@crombolo/crombolo': `^${VERSION}`,
222
+ ...Object.fromEntries(
223
+ options.bundles.map((bundle) => [bundle.package, bundle.version ?? `^${VERSION}`]),
224
+ ),
225
+ },
226
+ // So the tsconfig written beside this has something to run it, and
227
+ // `crombolo views check` checks types as well as compiling. The version
228
+ // is the one crombolo itself is checked with.
229
+ devDependencies: { typescript: TYPESCRIPT_VERSION },
230
+ },
231
+ null,
232
+ 2,
233
+ )}\n`
234
+ }
235
+
236
+ /** Every file in a scaffolded site, template files last so they win on a clash. */
237
+ export function scaffoldFiles(options: ScaffoldOptions = {}): ScaffoldFile[] {
238
+ const siteName = options.siteName?.trim() || DEFAULT_SITE_NAME
239
+ const bundle = options.bundle?.path ?? options.template?.bundle
240
+ const bundleFlags = ['--publish', ...(options.bundle?.flags ?? [])].join(' ')
241
+ const serverBundles = options.template?.serverBundles ?? []
242
+ const files: ScaffoldFile[] = [
243
+ {
244
+ path: 'package.json',
245
+ text: packageJson({
246
+ name: packageNameFor(siteName),
247
+ // The import is a boot step rather than a config setting so that it is
248
+ // visible here, and so that dropping it is editing one line.
249
+ start: bundle ? `crombolo start --bundle ${bundle} ${bundleFlags}` : 'crombolo start',
250
+ bundles: serverBundles,
251
+ }),
252
+ },
253
+ { path: 'crombolo.config.ts', text: config(siteName, serverBundles) },
254
+ {
255
+ path: 'server.ts',
256
+ text: `import { crombolo } from '@crombolo/crombolo'
257
+
258
+ export default await crombolo()
259
+ `,
260
+ },
261
+ { path: 'tsconfig.json', text: tsconfig() },
262
+ { path: 'schema/schema.toml', text: '[schema]\nversion = "1.0.0"\n' },
263
+ // The media types a site owns as files rather than inheriting silently.
264
+ ...Object.entries(siteMediaTypeFiles()).map(([path, text]) => ({ path, text })),
265
+ { path: 'components/.gitkeep', text: '' },
266
+ {
267
+ path: '.gitignore',
268
+ // `.crombolo/` holds the view snapshots: pure cache, rebuilt from `components/`
269
+ // at every boot, and per node.
270
+ text: '*.sqlite\n*.sqlite-*\nnode_modules/\n.env\n.crombolo/\nmedia/\nlogs/\n',
271
+ },
272
+ ]
273
+
274
+ if (options.postgres)
275
+ files.push({
276
+ path: '.env',
277
+ text: `CROMBOLO_DB=postgres
278
+ CROMBOLO_POSTGRES_URL=postgres://crombolo:crombolo@localhost:5432/${packageNameFor(siteName)}
279
+
280
+ # Postgres has no file to copy, so anything that writes needs a backup first:
281
+ # a command that takes one, or --backup-taken to vouch that you have.
282
+ # CROMBOLO_PG_DUMP=pg_dump "$CROMBOLO_POSTGRES_URL" -f "backup-$CROMBOLO_BACKUP_STAMP.sql"
283
+ `,
284
+ })
285
+
286
+ for (const file of componentFiles(options.template)) files.push(file)
287
+ // One entry per path, the last winning, so a template's own `schema.toml` is
288
+ // the one written rather than being skipped as a file that already exists.
289
+ return [...new Map(files.map((file) => [file.path, file])).values()]
290
+ }
291
+
292
+ /** `files/` into the site root, `bundle/` under `bundles/<slug>`. */
293
+ export function componentFiles(template: SiteTemplate | undefined): ScaffoldFile[] {
294
+ if (!template) return []
295
+ const out: ScaffoldFile[] = []
296
+ const walk = (dir: string, target: (path: string) => string): void => {
297
+ if (!existsSync(dir)) return
298
+ for (const entry of readdirSync(dir, { recursive: true, withFileTypes: true })) {
299
+ if (!entry.isFile()) continue
300
+ const source = join(entry.parentPath, entry.name)
301
+ out.push({ path: target(relative(dir, source).split(sepRe).join('/')), copyFrom: source })
302
+ }
303
+ }
304
+ walk(join(template.dir, 'files'), (path) => path)
305
+ if (template.bundle) walk(join(template.dir, 'bundle'), (path) => `${template.bundle}/${path}`)
306
+ return out
307
+ }
308
+
309
+ const sepRe = /[\\/]/
@@ -0,0 +1,45 @@
1
+ {
2
+ "formatVersion": 1,
3
+ "id": "2de473a2-d391-4161-801e-e34b202759b8",
4
+ "createdAt": "2026-10-01T00:00:00.000Z",
5
+ "createdBy": "bun run build:template",
6
+ "integrity": "sha256-5e0caa815b7238c8f691bb51692c4b222e8d564fd6490366752f80b98bbd9c70",
7
+ "snapshot": "published",
8
+ "provenance": {
9
+ "siteName": "Basic",
10
+ "schemaVersion": "1.0.0",
11
+ "schemaRevision": "0",
12
+ "schemaHash": "c138d226529a718d25ebca8e0c10e5ca3a5cba85b239b4327e7dd6f21fa45a27"
13
+ },
14
+ "selector": {
15
+ "roots": [
16
+ "36955f6e-d110-469e-b280-72096eccd148"
17
+ ],
18
+ "descendants": true,
19
+ "asGiven": [
20
+ "basic"
21
+ ]
22
+ },
23
+ "counts": {
24
+ "document": 1
25
+ },
26
+ "dependencies": {
27
+ "carried": [
28
+ "36955f6e-d110-469e-b280-72096eccd148"
29
+ ],
30
+ "expected": [],
31
+ "schema": {
32
+ "contentTypes": [
33
+ {
34
+ "key": "b8188492-dc05-43e6-98bf-04ee758e4c17",
35
+ "alias": "homePage"
36
+ }
37
+ ],
38
+ "components": [
39
+ "homePage"
40
+ ],
41
+ "languages": []
42
+ }
43
+ },
44
+ "blobs": []
45
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "key": "36955f6e-d110-469e-b280-72096eccd148",
3
+ "kind": "document",
4
+ "contentType": {
5
+ "key": "b8188492-dc05-43e6-98bf-04ee758e4c17",
6
+ "alias": "homePage"
7
+ },
8
+ "parent": null,
9
+ "sortOrder": 0,
10
+ "template": "homePage",
11
+ "variants": [
12
+ {
13
+ "culture": null,
14
+ "segment": null,
15
+ "name": "Home",
16
+ "published": true
17
+ }
18
+ ],
19
+ "values": [
20
+ {
21
+ "property": "bodyText",
22
+ "culture": null,
23
+ "segment": null,
24
+ "editor": "Umbraco.RichText",
25
+ "value": {
26
+ "markup": "<p>This page is content: it lives in the database and you edit it in the backoffice. Its <em>shape</em> is code — <code>schema/content-types/home-page.toml</code> — and the markup around it is <code>components/homePage.tsx</code>.</p><p>Add a property to the TOML, run <code>crombolo schema check</code> to see what it would do to this page, then <code>crombolo start</code> to apply it.</p>",
27
+ "blocks": null
28
+ }
29
+ },
30
+ {
31
+ "property": "title",
32
+ "culture": null,
33
+ "segment": null,
34
+ "editor": "Umbraco.TextBox",
35
+ "value": "A new crombolo site"
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,22 @@
1
+ import { type PageProps, SeoHead } from '@crombolo/crombolo'
2
+
3
+ export default function HomePage(props: PageProps) {
4
+ const { model } = props
5
+ return (
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10
+ {/* The title, description and sharing tags, from the page's Search & sharing tab. */}
11
+ <SeoHead {...props} title={model.text('title')} />
12
+ </head>
13
+ <body>
14
+ <main>
15
+ <h1>{model.text('title')}</h1>
16
+ {/* Rich text is stored as HTML, so it is written out as HTML. */}
17
+ <div setInnerHTML={{ __html: model.text('bodyText'), dangerously: true }} />
18
+ </main>
19
+ </body>
20
+ </html>
21
+ )
22
+ }
@@ -0,0 +1,28 @@
1
+ [content-type]
2
+ key = "b8188492-dc05-43e6-98bf-04ee758e4c17"
3
+ alias = "homePage"
4
+ name = "Home Page"
5
+ description = "The site's landing page"
6
+ icon = "icon-home"
7
+ allow-at-root = true
8
+ allow-children = ["homePage"]
9
+ compositions = ["seoFields"]
10
+ components = ["homePage"]
11
+ default-component = "homePage"
12
+
13
+ [[tab]]
14
+ name = "Content"
15
+
16
+ [[tab.property]]
17
+ key = "41fc551c-4bc9-4a1e-acab-d3c8a11e35ff"
18
+ alias = "title"
19
+ name = "Title"
20
+ description = "Shown as the page heading"
21
+ type = "textstring"
22
+ mandatory = true
23
+
24
+ [[tab.property]]
25
+ key = "f4b0420a-125c-49c6-863d-c40ad6f09703"
26
+ alias = "bodyText"
27
+ name = "Body"
28
+ type = "richtext"
@@ -0,0 +1,2 @@
1
+ [schema]
2
+ version = "1.0.0"
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "Basic",
3
+ "description": "a home page type, one view and one published page"
4
+ }
@@ -0,0 +1,42 @@
1
+ # Harbourstone demo site — image credits
2
+
3
+ The Harbourstone site is distributed (`"distribute": true` in its `site.json`), so
4
+ it is staged into `@crombolo/cli`'s `templates/` when the CLI is packed, and every
5
+ binary in its bundle is **redistributed** to anyone who runs
6
+ `crombolo init --template demo/harbourstone`. Each one therefore needs a recorded
7
+ source and licence, and `tests/packaging.test.ts` asserts that none is missing
8
+ from this file.
9
+
10
+ | File | Source | Licence | Attribution |
11
+ | --- | --- | --- | --- |
12
+ | `bundles/demo-harbourstone/blobs/180cc8ef/distillery-from-the-cliff-path.jpg` | Generated with Grok (xAI), 5 October 2026 | No third-party rights | — |
13
+ | `bundles/demo-harbourstone/blobs/7b4713f8/copper-pot-stills.jpg` | Generated with Grok (xAI), 5 October 2026 | No third-party rights | — |
14
+ | `bundles/demo-harbourstone/blobs/fd337567/casks-in-the-sea-facing-warehouse.jpg` | Generated with Grok (xAI), 5 October 2026 | No third-party rights | — |
15
+ | `bundles/demo-harbourstone/blobs/6d8d6398/bottle-the-cove.jpg` | Generated with Grok (xAI), 5 October 2026 | No third-party rights | — |
16
+ | `bundles/demo-harbourstone/blobs/68c9cfdf/bottle-brine-and-kiln.jpg` | Generated with Grok (xAI), 5 October 2026 | No third-party rights | — |
17
+ | `bundles/demo-harbourstone/blobs/6be45535/bottle-oloroso-pier.jpg` | Generated with Grok (xAI), 5 October 2026 | No third-party rights | — |
18
+ | `bundles/demo-harbourstone/blobs/c114b7d8/bottle-neap-tide.jpg` | Generated with Grok (xAI), 5 October 2026 | No third-party rights | — |
19
+
20
+ These are synthetic images of a distillery that does not exist. They depict no
21
+ real place, product or person, and no photographer's work is being
22
+ redistributed — which is the question this file exists to answer.
23
+
24
+ ## Where the originals live
25
+
26
+ `assets/templates/harbourstone/` in this repository, at the size they ship at.
27
+ That directory is **not** in the `files` list in `packages/cli/package.json`, so
28
+ the photographs are published once, in the bundle, rather than twice.
29
+ `bun run build:template demo/harbourstone` copies the bytes through untouched:
30
+ re-encoding an already-compressed JPEG on every build would churn the committed
31
+ bundle and lose a little more of the image each time.
32
+
33
+ ## Adding one
34
+
35
+ Add a row. The test fails otherwise, which is the point: this is the kind of
36
+ thing that is only ever noticed years later. Three ways to fill one in:
37
+
38
+ 1. **A stock source** (Unsplash, Pexels and the like) — record the URL, the
39
+ licence and the photographer, and satisfy any attribution the licence asks
40
+ for.
41
+ 2. **Generated** — record the tool and the date; nothing further is needed.
42
+ 3. **Origin unknown** — replace the image rather than ship it.