@sarimarcus/content-sites-ui 0.16.0 → 0.18.0
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/CHANGELOG.md +14 -0
- package/README.md +55 -2
- package/package.json +7 -3
- package/src/affiliate/EmeraldLoader.astro +40 -0
- package/src/affiliate/index.ts +4 -1
- package/src/affiliate/resources.ts +7 -0
- package/src/affiliate/travelpayoutsStyler.ts +557 -0
- package/src/composed/ArrowLinkGrid.astro +36 -0
- package/src/composed/index.ts +1 -0
- package/src/editorial/ArticleFeatureCard.astro +77 -0
- package/src/editorial/ArticlesListingPage.astro +40 -0
- package/src/editorial/NotFoundPage.astro +98 -0
- package/src/editorial/StaticProsePage.astro +48 -0
- package/src/editorial/index.ts +4 -0
- package/src/layout/ConsentAnalytics.astro +121 -0
- package/src/layout/DocumentShell.astro +171 -0
- package/src/layout/ListingShell.astro +32 -0
- package/src/layout/analytics.ts +151 -0
- package/src/layout/index.ts +7 -0
- package/src/layout/resources.ts +19 -0
- package/src/search/SearchPage.astro +222 -0
- package/src/search/index.ts +2 -0
- package/src/search/searchPage.ts +591 -0
- package/src/styles/not-found.css +151 -0
package/src/layout/index.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
// Public surface of @sarimarcus/content-sites-ui/layout.
|
|
2
2
|
export { default as Container } from './Container.astro';
|
|
3
3
|
export { default as BreadcrumbBand } from './BreadcrumbBand.astro';
|
|
4
|
+
export { default as ListingShell } from './ListingShell.astro';
|
|
4
5
|
export { default as AsyncStylesheet } from './AsyncStylesheet.astro';
|
|
6
|
+
export { default as DocumentShell } from './DocumentShell.astro';
|
|
7
|
+
export { default as ConsentAnalytics } from './ConsentAnalytics.astro';
|
|
8
|
+
export type { ConsentType } from './ConsentAnalytics.astro';
|
|
9
|
+
export { shellResources, GTM_ORIGIN, SILKTIDE_BASE, SILKTIDE_STYLESHEET } from './resources.ts';
|
|
10
|
+
export { track, pageTypeOf, setupDelegatedTracking, setupFaqTracking, setupScrollDepthTracking, setupAnalytics } from './analytics.ts';
|
|
11
|
+
export type { AnalyticsOptions } from './analytics.ts';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The third-party hosts the document shell loads, per CSP directive. Every site renders the shell, so every site's
|
|
2
|
+
// Content-Security-Policy must allow these (scripts/validate-csp-coverage.mjs checks each site's _headers against this
|
|
3
|
+
// list). Plain data and erasable types only: the gate imports this file under Node.
|
|
4
|
+
|
|
5
|
+
import type { CspDirective } from '../affiliate/resources.ts';
|
|
6
|
+
|
|
7
|
+
/** Google Tag Manager: gtm.js and, for visitors without JavaScript, the ns.html iframe. */
|
|
8
|
+
export const GTM_ORIGIN = 'https://www.googletagmanager.com';
|
|
9
|
+
/** Silktide Consent Manager, pinned. */
|
|
10
|
+
export const SILKTIDE_BASE = 'https://cdn.jsdelivr.net/gh/silktide/consent-manager@v2.0.0';
|
|
11
|
+
/** The consent manager's stylesheet; the CSP gate recognises a shell page by it. */
|
|
12
|
+
export const SILKTIDE_STYLESHEET = `${SILKTIDE_BASE}/silktide-consent-manager.css`;
|
|
13
|
+
const SILKTIDE_ORIGIN = new URL(SILKTIDE_BASE).origin;
|
|
14
|
+
|
|
15
|
+
export const shellResources: Partial<Record<CspDirective, readonly string[]>> = {
|
|
16
|
+
'script-src': [GTM_ORIGIN, SILKTIDE_ORIGIN],
|
|
17
|
+
'style-src': [SILKTIDE_ORIGIN],
|
|
18
|
+
'frame-src': [GTM_ORIGIN],
|
|
19
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* SearchPage — the markup of a site search page over its Pagefind index: hero and search box, type chips, an optional
|
|
4
|
+
* audience row (Pagefind `audience` filter values), the result states and the type-icon template the result cards
|
|
5
|
+
* clone. The route keeps Layout (`noindex`), imports `@sarimarcus/content-sites-ui/search.css` and calls
|
|
6
|
+
* `initSearchPage` from `@sarimarcus/content-sites-ui/search-page` in its own <script> with the same taxonomy and copy.
|
|
7
|
+
*/
|
|
8
|
+
import Container from '../layout/Container.astro';
|
|
9
|
+
import Icon from '../primitives/Icon.astro';
|
|
10
|
+
import SearchSuggestions from './SearchSuggestions.astro';
|
|
11
|
+
import type { SearchPageStrings, SearchPageType } from './searchPage.ts';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
strings: SearchPageStrings;
|
|
15
|
+
/** Insertion order is the chip order. */
|
|
16
|
+
types: Record<string, SearchPageType>;
|
|
17
|
+
suggestions: { label: string; query: string }[];
|
|
18
|
+
fallbackLinks: { label: string; href: string }[];
|
|
19
|
+
/** Renders the audience row when set (needs the audience strings). */
|
|
20
|
+
audienceFacets?: { value: string; label: string; count: number }[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { strings, types, suggestions, fallbackLinks, audienceFacets } = Astro.props;
|
|
24
|
+
const typeOrder = Object.keys(types);
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<main class="search-page" data-pagefind-ignore>
|
|
28
|
+
<section class="search-hero" aria-labelledby="search-hero-heading">
|
|
29
|
+
<Container>
|
|
30
|
+
<p class="search-hero__eyebrow">{strings.heroEyebrow}</p>
|
|
31
|
+
<h1 id="search-hero-heading" class="search-hero__heading">
|
|
32
|
+
{strings.heroHeading}
|
|
33
|
+
</h1>
|
|
34
|
+
<p class="search-hero__subhead">{strings.heroSubhead}</p>
|
|
35
|
+
|
|
36
|
+
<form class="search-form" role="search" autocomplete="off" novalidate>
|
|
37
|
+
<span class="search-form__icon" aria-hidden="true">
|
|
38
|
+
<Icon name="search" class="w-5 h-5" />
|
|
39
|
+
</span>
|
|
40
|
+
<label class="sr-only" for="search-input">{strings.inputLabel}</label>
|
|
41
|
+
<input
|
|
42
|
+
id="search-input"
|
|
43
|
+
type="search"
|
|
44
|
+
class="search-form__input"
|
|
45
|
+
placeholder={strings.inputPlaceholder}
|
|
46
|
+
spellcheck="false"
|
|
47
|
+
autocapitalize="off"
|
|
48
|
+
autocorrect="off"
|
|
49
|
+
enterkeyhint="search"
|
|
50
|
+
aria-controls="search-results-region"
|
|
51
|
+
aria-describedby="search-announce"
|
|
52
|
+
/>
|
|
53
|
+
<div id="search-announce" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="search-form__clear"
|
|
57
|
+
aria-label={strings.clearLabel}
|
|
58
|
+
data-search-clear
|
|
59
|
+
hidden
|
|
60
|
+
>
|
|
61
|
+
<Icon name="x" class="w-4 h-4" />
|
|
62
|
+
</button>
|
|
63
|
+
</form>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
class="search-filters"
|
|
67
|
+
role="group"
|
|
68
|
+
aria-label={strings.filterGroupLabel}
|
|
69
|
+
hidden
|
|
70
|
+
>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
class="search-chip search-chip--active"
|
|
74
|
+
data-search-filter=""
|
|
75
|
+
aria-pressed="true"
|
|
76
|
+
>
|
|
77
|
+
{strings.filterAllLabel}
|
|
78
|
+
</button>
|
|
79
|
+
{
|
|
80
|
+
typeOrder.map((t) => (
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="search-chip"
|
|
84
|
+
data-search-filter={t}
|
|
85
|
+
aria-pressed="false"
|
|
86
|
+
>
|
|
87
|
+
<Icon name={types[t].iconName} class="search-chip__icon" />
|
|
88
|
+
<span>{types[t].pluralLabel}</span>
|
|
89
|
+
</button>
|
|
90
|
+
))
|
|
91
|
+
}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{audienceFacets && (
|
|
95
|
+
<div
|
|
96
|
+
class="search-filters search-filters--audience"
|
|
97
|
+
role="group"
|
|
98
|
+
aria-label={strings.audienceGroupLabel}
|
|
99
|
+
hidden
|
|
100
|
+
>
|
|
101
|
+
<span class="search-filters__label" aria-hidden="true">{strings.audienceRowLabel}</span>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
class="search-chip search-chip--active"
|
|
105
|
+
data-search-audience=""
|
|
106
|
+
aria-pressed="true"
|
|
107
|
+
>
|
|
108
|
+
{strings.audienceAllLabel}
|
|
109
|
+
</button>
|
|
110
|
+
{
|
|
111
|
+
audienceFacets.map((f) => (
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
class="search-chip"
|
|
115
|
+
data-search-audience={f.value}
|
|
116
|
+
data-search-audience-total={f.count}
|
|
117
|
+
aria-pressed="false"
|
|
118
|
+
>
|
|
119
|
+
<span>{f.label}</span>
|
|
120
|
+
<span class="search-chip__count">{f.count}</span>
|
|
121
|
+
</button>
|
|
122
|
+
))
|
|
123
|
+
}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</Container>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
{/* Hidden icon template: result cards clone an icon node by type. */}
|
|
130
|
+
<template id="search-type-icons">
|
|
131
|
+
{
|
|
132
|
+
typeOrder.map((t) => (
|
|
133
|
+
<span data-icon-for={t}>
|
|
134
|
+
<Icon name={types[t].iconName} class="search-card__badge-icon" />
|
|
135
|
+
</span>
|
|
136
|
+
))
|
|
137
|
+
}
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<section
|
|
141
|
+
id="search-results-region"
|
|
142
|
+
class="search-results-region"
|
|
143
|
+
aria-busy="false"
|
|
144
|
+
>
|
|
145
|
+
<Container>
|
|
146
|
+
<div class="search-state" data-search-state="empty">
|
|
147
|
+
<SearchSuggestions label={strings.emptySuggestionsLabel} suggestions={suggestions} />
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="search-state" data-search-state="loading" hidden>
|
|
151
|
+
<p class="search-state__loading">
|
|
152
|
+
<span class="search-state__spinner" aria-hidden="true"></span>
|
|
153
|
+
{strings.loadingLabel}
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="search-state" data-search-state="results" hidden>
|
|
158
|
+
<h2 class="search-results__heading" data-search-results-heading></h2>
|
|
159
|
+
{/* Only the entity templates and articles carry an `audience` filter value,
|
|
160
|
+
so an audience-filtered search cannot return guides, /discover/ pages,
|
|
161
|
+
neighborhoods or walks — they have no audience tags to match. Rather
|
|
162
|
+
than drop them silently, say how many the filter removed. */}
|
|
163
|
+
<p class="search-filter-note" data-search-filter-note hidden></p>
|
|
164
|
+
<ul class="search-results__list" data-search-results-list></ul>
|
|
165
|
+
<div class="search-results__actions">
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
class="search-load-more"
|
|
169
|
+
data-search-load-more
|
|
170
|
+
hidden
|
|
171
|
+
>
|
|
172
|
+
{strings.loadMoreLabel}
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="search-state" data-search-state="zero" hidden>
|
|
178
|
+
<h2 class="search-state__heading" data-search-zero-heading></h2>
|
|
179
|
+
<p class="search-state__subhead">{strings.zeroSubhead}</p>
|
|
180
|
+
<SearchSuggestions label={strings.emptySuggestionsLabel} suggestions={suggestions} />
|
|
181
|
+
<p class="search-fallback__label">{strings.zeroFallbackLabel}</p>
|
|
182
|
+
<ul class="search-fallback__links">
|
|
183
|
+
{
|
|
184
|
+
fallbackLinks.map((l) => (
|
|
185
|
+
<li>
|
|
186
|
+
<a href={l.href} class="search-fallback__link">{l.label}</a>
|
|
187
|
+
</li>
|
|
188
|
+
))
|
|
189
|
+
}
|
|
190
|
+
</ul>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="search-state" data-search-state="filter-empty" hidden>
|
|
194
|
+
<h2 class="search-state__heading" data-search-filter-empty-heading>{strings.filterEmptyHeading}</h2>
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
class="search-chip search-chip--suggest"
|
|
198
|
+
data-search-clear-filter
|
|
199
|
+
>
|
|
200
|
+
{strings.filterEmptyClearLabel}
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="search-state" data-search-state="error" hidden>
|
|
205
|
+
<h2 class="search-state__heading">{strings.errorHeading}</h2>
|
|
206
|
+
<p class="search-state__subhead">{strings.errorSubhead}</p>
|
|
207
|
+
<button type="button" class="search-load-more" data-search-retry>
|
|
208
|
+
{strings.errorRetryLabel}
|
|
209
|
+
</button>
|
|
210
|
+
<ul class="search-fallback__links" style="margin-top: 1.5rem">
|
|
211
|
+
{
|
|
212
|
+
fallbackLinks.map((l) => (
|
|
213
|
+
<li>
|
|
214
|
+
<a href={l.href} class="search-fallback__link">{l.label}</a>
|
|
215
|
+
</li>
|
|
216
|
+
))
|
|
217
|
+
}
|
|
218
|
+
</ul>
|
|
219
|
+
</div>
|
|
220
|
+
</Container>
|
|
221
|
+
</section>
|
|
222
|
+
</main>
|
package/src/search/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Public surface of @sarimarcus/content-sites-ui/search: search-page building blocks. The site keeps its page, its
|
|
2
2
|
// type taxonomy, its copy and its Pagefind index.
|
|
3
3
|
export { default as SearchSuggestions } from './SearchSuggestions.astro';
|
|
4
|
+
export { default as SearchPage } from './SearchPage.astro';
|
|
5
|
+
export type { SearchPageStrings, SearchPageType } from './searchPage.ts';
|
|
4
6
|
export { appendResults } from './renderResult.ts';
|
|
5
7
|
export type { PagefindResultFragment, SearchTaxonomy, SearchTypeMeta } from './renderResult.ts';
|