@crombolo/cli 0.13.2 → 0.14.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.
Files changed (31) hide show
  1. package/bin/crombolo.ts +689 -23
  2. package/package.json +7 -7
  3. package/src/operations/commands.ts +108 -4
  4. package/src/remote/client.ts +288 -0
  5. package/src/remote/remotes-file.ts +80 -0
  6. package/src/remote/transfer.ts +337 -0
  7. package/templates/basic/bundle/package.json +1 -1
  8. package/templates/demo/harbourstone/bundle/content/bundle.json +11 -4
  9. package/templates/demo/harbourstone/bundle/content/nodes/40107d59-f3e9-4592-aee8-5b03d4586cbd.json +7 -0
  10. package/templates/demo/harbourstone/bundle/content/nodes/6ad8f0f3-9334-47f1-b805-80c4b4b4fa88.json +7 -0
  11. package/templates/demo/harbourstone/bundle/content/nodes/9468d1b4-f521-48ef-b044-2ab07b461b96.json +42 -0
  12. package/templates/demo/harbourstone/bundle/content/nodes/a562f0a2-cb6e-4ead-a1ce-4d180632419f.json +7 -0
  13. package/templates/demo/harbourstone/bundle/content/nodes/cd681b4b-9c4e-4624-9c13-450aa6a9c135.json +7 -0
  14. package/templates/demo/harbourstone/bundle/package.json +1 -1
  15. package/templates/demo/harbourstone/files/components/pages/searchPage.tsx +76 -0
  16. package/templates/demo/harbourstone/files/components/shared/site-header.tsx +13 -1
  17. package/templates/demo/harbourstone/files/css/site.css +113 -0
  18. package/templates/demo/harbourstone/files/schema/content-types/contact-page.toml +2 -0
  19. package/templates/demo/harbourstone/files/schema/content-types/content-page.toml +2 -0
  20. package/templates/demo/harbourstone/files/schema/content-types/expression.toml +8 -0
  21. package/templates/demo/harbourstone/files/schema/content-types/home-page.toml +5 -1
  22. package/templates/demo/harbourstone/files/schema/content-types/search-page.toml +33 -0
  23. package/templates/demo/harbourstone/files/schema/content-types/whisky-list.toml +2 -0
  24. package/templates/starter/bundle/content/bundle.json +12 -5
  25. package/templates/starter/bundle/content/nodes/c720ef2a-b5c8-48ec-a504-065e8188d4aa.json +28 -0
  26. package/templates/starter/bundle/package.json +1 -1
  27. package/templates/starter/files/components/pages/searchPage.tsx +57 -0
  28. package/templates/starter/files/components/shared/site-header.tsx +11 -2
  29. package/templates/starter/files/css/site.css +66 -0
  30. package/templates/starter/files/schema/content-types/content-page.toml +4 -1
  31. package/templates/starter/files/schema/content-types/search-page.toml +21 -0
@@ -162,6 +162,27 @@ main > article,
162
162
  color: var(--brass-deep);
163
163
  }
164
164
 
165
+ /* The search box sits after the links, as small and quiet as they are. */
166
+ .masthead__search input {
167
+ width: 9rem;
168
+ padding: 0.35rem 0.6rem;
169
+ border: 1px solid var(--line);
170
+ border-radius: 0;
171
+ background: var(--card);
172
+ color: var(--ink);
173
+ font: inherit;
174
+ font-size: 0.875rem;
175
+ transition:
176
+ border-color var(--ease),
177
+ width var(--ease);
178
+ }
179
+
180
+ .masthead__search input:focus-visible {
181
+ outline: none;
182
+ width: 13rem;
183
+ border-color: var(--brass);
184
+ }
185
+
165
186
  .site-footer {
166
187
  margin-top: 5rem;
167
188
  border-top: 1px solid var(--line);
@@ -718,3 +739,95 @@ main > article,
718
739
  padding-block: 0;
719
740
  }
720
741
  }
742
+
743
+ /* -------------------------------------------------------------- search */
744
+
745
+ .search-form {
746
+ display: flex;
747
+ flex-wrap: wrap;
748
+ gap: 0.75rem;
749
+ align-items: stretch;
750
+ margin-top: 2rem;
751
+ max-width: 36rem;
752
+ }
753
+
754
+ .search-form input {
755
+ flex: 1;
756
+ min-width: 12rem;
757
+ padding: 0.75rem;
758
+ border: 1px solid var(--line);
759
+ border-radius: 0;
760
+ background: var(--card);
761
+ color: var(--ink);
762
+ font: inherit;
763
+ font-size: 1rem;
764
+ transition: border-color var(--ease);
765
+ }
766
+
767
+ .search-form input:focus-visible {
768
+ outline: none;
769
+ border-color: var(--brass);
770
+ }
771
+
772
+ .search-form .button {
773
+ margin-top: 0;
774
+ background: none;
775
+ cursor: pointer;
776
+ font-family: inherit;
777
+ }
778
+
779
+ .search-results {
780
+ margin: 0;
781
+ padding: 0;
782
+ max-width: 44rem;
783
+ list-style: none;
784
+ }
785
+
786
+ .search-results li {
787
+ padding-block: 1.75rem;
788
+ border-top: 1px solid var(--line);
789
+ }
790
+
791
+ .search-results h2 {
792
+ margin: 0.5rem 0 0;
793
+ font-size: 1.5rem;
794
+ }
795
+
796
+ .search-results h2 a {
797
+ text-decoration: none;
798
+ transition: color var(--ease);
799
+ }
800
+
801
+ .search-results h2 a:hover {
802
+ color: var(--brass-deep);
803
+ }
804
+
805
+ .search-results p {
806
+ margin: 0.75rem 0 0;
807
+ color: var(--muted);
808
+ }
809
+
810
+ .search-results mark {
811
+ background: none;
812
+ color: var(--ink);
813
+ font-weight: 500;
814
+ }
815
+
816
+ .search-pages {
817
+ display: flex;
818
+ justify-content: space-between;
819
+ align-items: baseline;
820
+ max-width: 44rem;
821
+ padding-top: 1.75rem;
822
+ border-top: 1px solid var(--line);
823
+ font-size: 0.75rem;
824
+ font-weight: 500;
825
+ letter-spacing: 0.18em;
826
+ text-transform: uppercase;
827
+ color: var(--muted);
828
+ }
829
+
830
+ nav.search-pages a {
831
+ color: var(--brass-deep);
832
+ text-decoration: none;
833
+ }
@@ -19,6 +19,7 @@ key = "753d8c52-4949-4e92-a76a-fa02da0fd199"
19
19
  alias = "eyebrow"
20
20
  name = "Eyebrow"
21
21
  type = "textstring"
22
+ site-search = "low"
22
23
 
23
24
  [[tab.property]]
24
25
  key = "1e8f3a9d-5bdf-4a01-a6c7-cc4a055fae22"
@@ -26,6 +27,7 @@ alias = "title"
26
27
  name = "Title"
27
28
  type = "textstring"
28
29
  mandatory = true
30
+ site-search = "high"
29
31
 
30
32
  [[tab.property]]
31
33
  key = "cf4561c0-82bb-4ad5-9f88-bd3fbd210b7c"
@@ -17,6 +17,7 @@ alias = "eyebrow"
17
17
  name = "Eyebrow"
18
18
  description = "The small line above the heading"
19
19
  type = "textstring"
20
+ site-search = "low"
20
21
 
21
22
  [[tab.property]]
22
23
  key = "4501d20d-3b78-40a4-97e4-dd25b2d8535a"
@@ -24,6 +25,7 @@ alias = "title"
24
25
  name = "Title"
25
26
  type = "textstring"
26
27
  mandatory = true
28
+ site-search = "high"
27
29
 
28
30
  [[tab.property]]
29
31
  key = "33199ac7-8128-4dc4-bae1-554b2a622447"
@@ -16,6 +16,7 @@ alias = "title"
16
16
  name = "Title"
17
17
  type = "textstring"
18
18
  mandatory = true
19
+ site-search = "high"
19
20
 
20
21
  [[tab.property]]
21
22
  key = "25738367-ed14-4985-9843-04716417c88a"
@@ -24,6 +25,13 @@ name = "Kind"
24
25
  description = "House malt, Lightly peated, Sherry matured, Cask strength"
25
26
  type = "textstring"
26
27
 
28
+ [[tab.property]]
29
+ key = "96f868bf-a368-4457-843a-1f38accf6cc1"
30
+ alias = "productType"
31
+ name = "Product type"
32
+ description = "What the bottling is, e.g. Whisky"
33
+ type = "textstring"
34
+
27
35
  [[tab.property]]
28
36
  key = "f4238bf1-3d9d-4dbc-b6f4-9800e1dfdc79"
29
37
  alias = "summary"
@@ -5,7 +5,7 @@ name = "Home Page"
5
5
  description = "The site's landing page"
6
6
  icon = "icon-home"
7
7
  allow-at-root = true
8
- allow-children = ["contentPage", "whiskyList", "contactPage"]
8
+ allow-children = ["contentPage", "whiskyList", "contactPage", "searchPage"]
9
9
  components = ["pages/homePage"]
10
10
  default-component = "pages/homePage"
11
11
 
@@ -18,6 +18,7 @@ alias = "eyebrow"
18
18
  name = "Eyebrow"
19
19
  description = "The small line above the heading"
20
20
  type = "textstring"
21
+ site-search = "low"
21
22
 
22
23
  [[tab.property]]
23
24
  key = "be74a0df-3e9d-47f3-b60e-778a89682a80"
@@ -26,6 +27,7 @@ name = "Title"
26
27
  description = "Shown as the page heading"
27
28
  type = "textstring"
28
29
  mandatory = true
30
+ site-search = "high"
29
31
 
30
32
  [[tab.property]]
31
33
  key = "a540c93a-228b-49f6-931a-be1b986bedd1"
@@ -46,6 +48,7 @@ alias = "heroCtaLabel"
46
48
  name = "Hero button"
47
49
  description = "Links to the range. Leave empty for no button."
48
50
  type = "textstring"
51
+ site-search = false
49
52
 
50
53
  [[tab]]
51
54
  name = "House style"
@@ -102,3 +105,4 @@ alias = "rangeCtaLabel"
102
105
  name = "Button"
103
106
  description = "Links to the range. Leave empty for no button."
104
107
  type = "textstring"
108
+ site-search = false
@@ -0,0 +1,33 @@
1
+ [content-type]
2
+ key = "4308082e-5024-47fc-950d-1ee93a3e304f"
3
+ alias = "searchPage"
4
+ name = "Search Page"
5
+ description = "Lists the pages matching what a visitor searched for"
6
+ icon = "icon-search"
7
+ components = ["pages/searchPage"]
8
+ default-component = "pages/searchPage"
9
+ site-search = false
10
+
11
+ [[tab]]
12
+ name = "Header"
13
+
14
+ [[tab.property]]
15
+ key = "3a5d2f03-8bf6-41e2-9971-3848df983f0a"
16
+ alias = "eyebrow"
17
+ name = "Eyebrow"
18
+ description = "The small line above the heading"
19
+ type = "textstring"
20
+
21
+ [[tab.property]]
22
+ key = "e5be09c2-12bd-4647-a6c9-2549459d97b4"
23
+ alias = "title"
24
+ name = "Title"
25
+ type = "textstring"
26
+ mandatory = true
27
+
28
+ [[tab.property]]
29
+ key = "16eab79f-e753-48ca-a3f8-7e326a3f513f"
30
+ alias = "standfirst"
31
+ name = "Standfirst"
32
+ description = "A sentence under the heading, shown before anyone has searched"
33
+ type = "textarea"
@@ -19,6 +19,7 @@ key = "13785a42-6d0a-4152-89ec-c765db766547"
19
19
  alias = "eyebrow"
20
20
  name = "Eyebrow"
21
21
  type = "textstring"
22
+ site-search = "low"
22
23
 
23
24
  [[tab.property]]
24
25
  key = "727804c0-7d4a-43d9-a473-71c9e27397c0"
@@ -26,6 +27,7 @@ alias = "title"
26
27
  name = "Title"
27
28
  type = "textstring"
28
29
  mandatory = true
30
+ site-search = "high"
29
31
 
30
32
  [[tab.property]]
31
33
  key = "9c83bcea-a70e-4a3f-8185-ad04ddec55f1"
@@ -3,19 +3,20 @@
3
3
  "id": "da9fd74e-b964-44b4-8d61-afa7f65a28d2",
4
4
  "createdAt": "2026-10-01T00:00:00.000Z",
5
5
  "createdBy": "bun run build:template",
6
- "integrity": "sha256-3ef21d4b047d85bfef8aeaac90255348bf636ba6ff3c0d0e155a06a2225269de",
6
+ "integrity": "sha256-c8b50fa21bb3bed8ecf30380427ef65894dafb97734e9bf17ed465080cba7866",
7
7
  "snapshot": "published",
8
8
  "provenance": {
9
9
  "siteName": "Starter",
10
10
  "schemaVersion": "1.0.0",
11
11
  "schemaRevision": "0",
12
- "schemaHash": "81d3ebeb2c46ec1a17f0bf253e1bf711c47390ac49eb3f32a60b23e3ace89b16"
12
+ "schemaHash": "1d47f00fd4feb3085e530673fceb33320ddb1c604fbb385c73e631eaace62a73"
13
13
  },
14
14
  "selector": {
15
15
  "roots": [
16
16
  "64d95359-d91a-4050-b440-3e5124a74999",
17
17
  "634dd948-cf83-4cad-9b7b-7c938b33da76",
18
- "d2997ebb-36ca-47cb-bbc8-31fefca787c0"
18
+ "d2997ebb-36ca-47cb-bbc8-31fefca787c0",
19
+ "c720ef2a-b5c8-48ec-a504-065e8188d4aa"
19
20
  ],
20
21
  "descendants": true,
21
22
  "asGiven": [
@@ -23,12 +24,13 @@
23
24
  ]
24
25
  },
25
26
  "counts": {
26
- "document": 3
27
+ "document": 4
27
28
  },
28
29
  "dependencies": {
29
30
  "carried": [
30
31
  "634dd948-cf83-4cad-9b7b-7c938b33da76",
31
32
  "64d95359-d91a-4050-b440-3e5124a74999",
33
+ "c720ef2a-b5c8-48ec-a504-065e8188d4aa",
32
34
  "d2997ebb-36ca-47cb-bbc8-31fefca787c0"
33
35
  ],
34
36
  "expected": [],
@@ -37,10 +39,15 @@
37
39
  {
38
40
  "key": "8491ffa4-9b2f-48b0-a3c0-f40c1034216e",
39
41
  "alias": "contentPage"
42
+ },
43
+ {
44
+ "key": "938c7f88-8df2-45d2-a4a2-49ecb3e68db7",
45
+ "alias": "searchPage"
40
46
  }
41
47
  ],
42
48
  "components": [
43
- "pages/contentPage"
49
+ "pages/contentPage",
50
+ "pages/searchPage"
44
51
  ],
45
52
  "languages": []
46
53
  }
@@ -0,0 +1,28 @@
1
+ {
2
+ "key": "c720ef2a-b5c8-48ec-a504-065e8188d4aa",
3
+ "kind": "document",
4
+ "contentType": {
5
+ "key": "938c7f88-8df2-45d2-a4a2-49ecb3e68db7",
6
+ "alias": "searchPage"
7
+ },
8
+ "parent": "64d95359-d91a-4050-b440-3e5124a74999",
9
+ "sortOrder": 2,
10
+ "template": "pages/searchPage",
11
+ "variants": [
12
+ {
13
+ "culture": null,
14
+ "segment": null,
15
+ "name": "Search",
16
+ "published": true
17
+ }
18
+ ],
19
+ "values": [
20
+ {
21
+ "property": "title",
22
+ "culture": null,
23
+ "segment": null,
24
+ "editor": "Umbraco.TextBox",
25
+ "value": "Search"
26
+ }
27
+ ]
28
+ }
@@ -6,7 +6,7 @@
6
6
  "content"
7
7
  ],
8
8
  "peerDependencies": {
9
- "@crombolo/crombolo": ">=0.13.2"
9
+ "@crombolo/crombolo": ">=0.14.4"
10
10
  },
11
11
  "crombolo": {
12
12
  "id": "starter",
@@ -0,0 +1,57 @@
1
+ import { type PageProps, search } from '@crombolo/crombolo'
2
+ import { Layout } from '../shared/layout.tsx'
3
+
4
+ const PER_PAGE = 10
5
+
6
+ /** The pages matching `?q=`, best first, ten at a time, each with the text around the match. */
7
+ export default async function SearchPage(props: PageProps) {
8
+ const { model, request } = props
9
+ const url = new URL(request.url)
10
+ const query = url.searchParams.get('q')?.trim() ?? ''
11
+ const page = Math.max(1, Number.parseInt(url.searchParams.get('page') ?? '1', 10) || 1)
12
+ const results = query
13
+ ? await search(query, { skip: (page - 1) * PER_PAGE, take: PER_PAGE })
14
+ : { total: 0, items: [] }
15
+ const pages = Math.ceil(results.total / PER_PAGE)
16
+ const link = (n: number) => `${model.url}?q=${encodeURIComponent(query)}&page=${n}`
17
+
18
+ return (
19
+ <Layout {...props}>
20
+ <h1>{model.text('title') || model.name}</h1>
21
+ <search>
22
+ <form class="search-form" action={model.url} method="get">
23
+ <input type="search" name="q" value={query} aria-label="Search the site" />
24
+ <button type="submit">Search</button>
25
+ </form>
26
+ </search>
27
+ {query ? (
28
+ <p class="search-summary">
29
+ {results.total === 0
30
+ ? `Nothing matches “${query}”.`
31
+ : `${results.total} ${results.total === 1 ? 'page matches' : 'pages match'} “${query}”.`}
32
+ </p>
33
+ ) : null}
34
+ <ol class="search-results">
35
+ {results.items.map((item) => (
36
+ <li>
37
+ <a href={item.url}>{item.content.text('title') || item.name}</a>
38
+ <p>
39
+ {item.excerpt.map((segment) =>
40
+ segment.match ? <mark>{segment.text}</mark> : segment.text,
41
+ )}
42
+ </p>
43
+ </li>
44
+ ))}
45
+ </ol>
46
+ {pages > 1 ? (
47
+ <nav class="search-pages" aria-label="Search results pages">
48
+ {page > 1 ? <a href={link(page - 1)}>Previous</a> : null}
49
+ <span>
50
+ Page {page} of {pages}
51
+ </span>
52
+ {page < pages ? <a href={link(page + 1)}>Next</a> : null}
53
+ </nav>
54
+ ) : null}
55
+ </Layout>
56
+ )
57
+ }
@@ -2,7 +2,7 @@ import type { Navigation, PublishedContent } from '@crombolo/crombolo'
2
2
 
3
3
  /**
4
4
  * The site name, from the config, and the primary nav: the home page followed by its children, so
5
- * adding a page in the backoffice adds it here.
5
+ * adding a page in the backoffice adds it here. A Search Page among them is the search box instead.
6
6
  */
7
7
  export function SiteHeader({
8
8
  model,
@@ -16,7 +16,9 @@ export function SiteHeader({
16
16
  const home = nav.root()[0]
17
17
  if (!home) return null
18
18
  const ancestry = model.path.split(',')
19
- const links = [home, ...nav.children(home, { visibleOnly: true })]
19
+ const children = nav.children(home, { visibleOnly: true })
20
+ const searchPage = children.find((child) => child.contentType.alias === 'searchPage')
21
+ const links = [home, ...children.filter((child) => child !== searchPage)]
20
22
  return (
21
23
  <header class="site-header">
22
24
  <div class="container site-header__inner">
@@ -34,6 +36,13 @@ export function SiteHeader({
34
36
  )
35
37
  })}
36
38
  </nav>
39
+ {searchPage ? (
40
+ <search>
41
+ <form class="site-search" action={searchPage.url} method="get">
42
+ <input type="search" name="q" placeholder="Search" aria-label="Search the site" />
43
+ </form>
44
+ </search>
45
+ ) : null}
37
46
  </div>
38
47
  </header>
39
48
  )
@@ -75,6 +75,72 @@ main {
75
75
  font-weight: 600;
76
76
  }
77
77
 
78
+ .site-search input,
79
+ .search-form input {
80
+ font: inherit;
81
+ padding: 0.4rem 0.75rem;
82
+ border: 1px solid var(--border);
83
+ border-radius: 0.375rem;
84
+ }
85
+
86
+ .search-form {
87
+ display: flex;
88
+ gap: 0.5rem;
89
+ margin-bottom: 1.5rem;
90
+ }
91
+
92
+ .search-form input {
93
+ flex: 1;
94
+ max-width: 32rem;
95
+ }
96
+
97
+ .search-form button {
98
+ font: inherit;
99
+ padding: 0.4rem 1rem;
100
+ border: 0;
101
+ border-radius: 0.375rem;
102
+ background: var(--accent);
103
+ color: #fff;
104
+ cursor: pointer;
105
+ }
106
+
107
+ .search-summary {
108
+ color: var(--muted);
109
+ }
110
+
111
+ .search-results {
112
+ list-style: none;
113
+ padding: 0;
114
+ }
115
+
116
+ .search-results li {
117
+ padding: 1rem 0;
118
+ border-bottom: 1px solid var(--border);
119
+ }
120
+
121
+ .search-results li > a {
122
+ font-weight: 600;
123
+ font-size: 1.1rem;
124
+ }
125
+
126
+ .search-results p {
127
+ margin: 0.25rem 0 0;
128
+ color: var(--muted);
129
+ }
130
+
131
+ .search-results mark {
132
+ background: none;
133
+ color: var(--text);
134
+ font-weight: 600;
135
+ }
136
+
137
+ .search-pages {
138
+ display: flex;
139
+ gap: 1.5rem;
140
+ align-items: center;
141
+ margin-top: 1.5rem;
142
+ }
143
+
78
144
  h1 {
79
145
  font-size: 2.25rem;
80
146
  line-height: 1.2;
@@ -5,7 +5,7 @@ name = "Content Page"
5
5
  description = "A page with a main column and a sidebar"
6
6
  icon = "icon-document"
7
7
  allow-at-root = true
8
- allow-children = ["contentPage"]
8
+ allow-children = ["contentPage", "searchPage"]
9
9
  compositions = ["seoFields"]
10
10
  components = ["pages/contentPage"]
11
11
  default-component = "pages/contentPage"
@@ -20,6 +20,7 @@ name = "Title"
20
20
  description = "Shown as the page heading"
21
21
  type = "textstring"
22
22
  mandatory = true
23
+ site-search = "high"
23
24
 
24
25
  [[tab.property]]
25
26
  key = "7028eeb8-9739-4cc9-b3e1-8b424dd1e929"
@@ -42,6 +43,7 @@ key = "d049eba5-ae38-4fe9-9b05-64d1fa50b35e"
42
43
  alias = "sidebarHeading"
43
44
  name = "Heading"
44
45
  type = "textstring"
46
+ site-search = "low"
45
47
 
46
48
  [[tab.property]]
47
49
  key = "fff4d170-e192-419a-9490-59affd2f6148"
@@ -49,3 +51,4 @@ alias = "sidebarText"
49
51
  name = "Text"
50
52
  description = "Leave empty for a single-column page"
51
53
  type = "richtext"
54
+ site-search = "low"
@@ -0,0 +1,21 @@
1
+ [content-type]
2
+ key = "938c7f88-8df2-45d2-a4a2-49ecb3e68db7"
3
+ alias = "searchPage"
4
+ name = "Search Page"
5
+ description = "Lists the pages matching what a visitor searched for"
6
+ icon = "icon-search"
7
+ compositions = ["seoFields"]
8
+ components = ["pages/searchPage"]
9
+ default-component = "pages/searchPage"
10
+ site-search = false
11
+
12
+ [[tab]]
13
+ name = "Content"
14
+
15
+ [[tab.property]]
16
+ key = "324e2bbc-2c0e-4984-a5d0-37032a5bec74"
17
+ alias = "title"
18
+ name = "Title"
19
+ description = "Shown as the page heading"
20
+ type = "textstring"
21
+ mandatory = true