@crombolo/cli 0.13.2 → 0.14.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.
- package/bin/crombolo.ts +289 -22
- package/package.json +7 -7
- package/src/operations/commands.ts +108 -4
- package/templates/basic/bundle/package.json +1 -1
- package/templates/demo/harbourstone/bundle/content/bundle.json +11 -4
- package/templates/demo/harbourstone/bundle/content/nodes/40107d59-f3e9-4592-aee8-5b03d4586cbd.json +7 -0
- package/templates/demo/harbourstone/bundle/content/nodes/6ad8f0f3-9334-47f1-b805-80c4b4b4fa88.json +7 -0
- package/templates/demo/harbourstone/bundle/content/nodes/9468d1b4-f521-48ef-b044-2ab07b461b96.json +42 -0
- package/templates/demo/harbourstone/bundle/content/nodes/a562f0a2-cb6e-4ead-a1ce-4d180632419f.json +7 -0
- package/templates/demo/harbourstone/bundle/content/nodes/cd681b4b-9c4e-4624-9c13-450aa6a9c135.json +7 -0
- package/templates/demo/harbourstone/bundle/package.json +1 -1
- package/templates/demo/harbourstone/files/components/pages/searchPage.tsx +76 -0
- package/templates/demo/harbourstone/files/components/shared/site-header.tsx +13 -1
- package/templates/demo/harbourstone/files/css/site.css +113 -0
- package/templates/demo/harbourstone/files/schema/content-types/contact-page.toml +2 -0
- package/templates/demo/harbourstone/files/schema/content-types/content-page.toml +2 -0
- package/templates/demo/harbourstone/files/schema/content-types/expression.toml +8 -0
- package/templates/demo/harbourstone/files/schema/content-types/home-page.toml +5 -1
- package/templates/demo/harbourstone/files/schema/content-types/search-page.toml +33 -0
- package/templates/demo/harbourstone/files/schema/content-types/whisky-list.toml +2 -0
- package/templates/starter/bundle/content/bundle.json +12 -5
- package/templates/starter/bundle/content/nodes/c720ef2a-b5c8-48ec-a504-065e8188d4aa.json +28 -0
- package/templates/starter/bundle/package.json +1 -1
- package/templates/starter/files/components/pages/searchPage.tsx +57 -0
- package/templates/starter/files/components/shared/site-header.tsx +11 -2
- package/templates/starter/files/css/site.css +66 -0
- package/templates/starter/files/schema/content-types/content-page.toml +4 -1
- package/templates/starter/files/schema/content-types/search-page.toml +21 -0
package/templates/demo/harbourstone/bundle/content/nodes/9468d1b4-f521-48ef-b044-2ab07b461b96.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"key": "9468d1b4-f521-48ef-b044-2ab07b461b96",
|
|
3
|
+
"kind": "document",
|
|
4
|
+
"contentType": {
|
|
5
|
+
"key": "4308082e-5024-47fc-950d-1ee93a3e304f",
|
|
6
|
+
"alias": "searchPage"
|
|
7
|
+
},
|
|
8
|
+
"parent": "d60ef12b-0f0a-4db5-9763-21fa52b8d4da",
|
|
9
|
+
"sortOrder": 3,
|
|
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": "eyebrow",
|
|
22
|
+
"culture": null,
|
|
23
|
+
"segment": null,
|
|
24
|
+
"editor": "Umbraco.TextBox",
|
|
25
|
+
"value": "Search"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"property": "standfirst",
|
|
29
|
+
"culture": null,
|
|
30
|
+
"segment": null,
|
|
31
|
+
"editor": "Umbraco.TextArea",
|
|
32
|
+
"value": "Search the range, the tasting notes and the story of the distillery."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"property": "title",
|
|
36
|
+
"culture": null,
|
|
37
|
+
"segment": null,
|
|
38
|
+
"editor": "Umbraco.TextBox",
|
|
39
|
+
"value": "Find it in the cove."
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
package/templates/demo/harbourstone/bundle/content/nodes/a562f0a2-cb6e-4ead-a1ce-4d180632419f.json
CHANGED
|
@@ -67,6 +67,13 @@
|
|
|
67
67
|
"editor": "Umbraco.TextBox",
|
|
68
68
|
"value": "Lightly peated"
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
"property": "productType",
|
|
72
|
+
"culture": null,
|
|
73
|
+
"segment": null,
|
|
74
|
+
"editor": "Umbraco.TextBox",
|
|
75
|
+
"value": "Whisky"
|
|
76
|
+
},
|
|
70
77
|
{
|
|
71
78
|
"property": "summary",
|
|
72
79
|
"culture": null,
|
package/templates/demo/harbourstone/bundle/content/nodes/cd681b4b-9c4e-4624-9c13-450aa6a9c135.json
CHANGED
|
@@ -67,6 +67,13 @@
|
|
|
67
67
|
"editor": "Umbraco.TextBox",
|
|
68
68
|
"value": "Sherry matured"
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
"property": "productType",
|
|
72
|
+
"culture": null,
|
|
73
|
+
"segment": null,
|
|
74
|
+
"editor": "Umbraco.TextBox",
|
|
75
|
+
"value": "Whisky"
|
|
76
|
+
},
|
|
70
77
|
{
|
|
71
78
|
"property": "summary",
|
|
72
79
|
"culture": null,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type PageProps, search } from '@crombolo/crombolo'
|
|
2
|
+
import { Layout } from '../shared/layout.tsx'
|
|
3
|
+
import { Eyebrow } from '../shared/section-intro.tsx'
|
|
4
|
+
|
|
5
|
+
const PER_PAGE = 10
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The pages matching `?q=`, best first, ten at a time. Each result reads its
|
|
9
|
+
* eyebrow and title by the aliases every page type here shares, so a whisky
|
|
10
|
+
* shows as "Single malt — The Harbour 12" rather than by its node name.
|
|
11
|
+
*/
|
|
12
|
+
export default async function SearchPage({ model, nav, request }: PageProps) {
|
|
13
|
+
const url = new URL(request.url)
|
|
14
|
+
const query = url.searchParams.get('q')?.trim() ?? ''
|
|
15
|
+
const page = Math.max(1, Number.parseInt(url.searchParams.get('page') ?? '1', 10) || 1)
|
|
16
|
+
const results = query
|
|
17
|
+
? await search(query, { skip: (page - 1) * PER_PAGE, take: PER_PAGE })
|
|
18
|
+
: { total: 0, items: [] }
|
|
19
|
+
const pages = Math.ceil(results.total / PER_PAGE)
|
|
20
|
+
const link = (n: number) => `${model.url}?q=${encodeURIComponent(query)}&page=${n}`
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Layout model={model} nav={nav}>
|
|
24
|
+
<header class="page-header">
|
|
25
|
+
<Eyebrow>{model.text('eyebrow')}</Eyebrow>
|
|
26
|
+
<h1>{model.text('title')}</h1>
|
|
27
|
+
<search>
|
|
28
|
+
<form class="search-form" action={model.url} method="get">
|
|
29
|
+
<input type="search" name="q" value={query} aria-label="Search the site" />
|
|
30
|
+
<button class="button" type="submit">
|
|
31
|
+
Search
|
|
32
|
+
</button>
|
|
33
|
+
</form>
|
|
34
|
+
</search>
|
|
35
|
+
{query ? (
|
|
36
|
+
<p class="standfirst">
|
|
37
|
+
{results.total === 0
|
|
38
|
+
? `Nothing matches “${query}”.`
|
|
39
|
+
: `${results.total} ${results.total === 1 ? 'page matches' : 'pages match'} “${query}”.`}
|
|
40
|
+
</p>
|
|
41
|
+
) : model.hasValue('standfirst') ? (
|
|
42
|
+
<p class="standfirst">{model.text('standfirst')}</p>
|
|
43
|
+
) : null}
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
{results.items.length > 0 ? (
|
|
47
|
+
<section>
|
|
48
|
+
<ol class="search-results">
|
|
49
|
+
{results.items.map((item) => (
|
|
50
|
+
<li>
|
|
51
|
+
<Eyebrow>{item.content.text('eyebrow')}</Eyebrow>
|
|
52
|
+
<h2>
|
|
53
|
+
<a href={item.url}>{item.content.text('title') || item.name}</a>
|
|
54
|
+
</h2>
|
|
55
|
+
<p>
|
|
56
|
+
{item.excerpt.map((segment) =>
|
|
57
|
+
segment.match ? <mark>{segment.text}</mark> : segment.text,
|
|
58
|
+
)}
|
|
59
|
+
</p>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ol>
|
|
63
|
+
{pages > 1 ? (
|
|
64
|
+
<nav class="search-pages" aria-label="Search results pages">
|
|
65
|
+
{page > 1 ? <a href={link(page - 1)}>Previous</a> : <span />}
|
|
66
|
+
<span>
|
|
67
|
+
Page {page} of {pages}
|
|
68
|
+
</span>
|
|
69
|
+
{page < pages ? <a href={link(page + 1)}>Next</a> : <span />}
|
|
70
|
+
</nav>
|
|
71
|
+
) : null}
|
|
72
|
+
</section>
|
|
73
|
+
) : null}
|
|
74
|
+
</Layout>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -11,12 +11,17 @@ import type { Navigation, PublishedContent } from '@crombolo/crombolo'
|
|
|
11
11
|
* There is no hamburger because there is no client-side JavaScript on this
|
|
12
12
|
* site: below the breakpoint the links wrap under the wordmark instead, which
|
|
13
13
|
* `site.css` does with flex-wrap alone.
|
|
14
|
+
*
|
|
15
|
+
* A Search Page among the children is the search box at the end instead of a
|
|
16
|
+
* link: a plain GET form, which needs no script either.
|
|
14
17
|
*/
|
|
15
18
|
export function SiteHeader({ model, nav }: { model: PublishedContent; nav: Navigation }) {
|
|
16
19
|
const home = nav.root()[0]
|
|
17
20
|
if (!home) return null
|
|
18
21
|
const ancestry = model.path.split(',')
|
|
19
|
-
const
|
|
22
|
+
const children = nav.children(home)
|
|
23
|
+
const searchPage = children.find((child) => child.contentType.alias === 'searchPage')
|
|
24
|
+
const links = [home, ...children.filter((child) => child !== searchPage)]
|
|
20
25
|
return (
|
|
21
26
|
<header class="masthead">
|
|
22
27
|
<div class="masthead__inner">
|
|
@@ -38,6 +43,13 @@ export function SiteHeader({ model, nav }: { model: PublishedContent; nav: Navig
|
|
|
38
43
|
)
|
|
39
44
|
})}
|
|
40
45
|
</nav>
|
|
46
|
+
{searchPage ? (
|
|
47
|
+
<search class="masthead__search">
|
|
48
|
+
<form action={searchPage.url} method="get">
|
|
49
|
+
<input type="search" name="q" placeholder="Search" aria-label="Search the site" />
|
|
50
|
+
</form>
|
|
51
|
+
</search>
|
|
52
|
+
) : null}
|
|
41
53
|
</div>
|
|
42
54
|
</header>
|
|
43
55
|
)
|
|
@@ -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-
|
|
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": "
|
|
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":
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
)
|