@sarimarcus/content-sites-ui 0.31.6 → 0.32.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 +18 -0
- package/README.md +10 -3
- package/package.json +2 -2
- package/src/affiliate/AffiliateCard.astro +7 -6
- package/src/affiliate/AffiliateCardSection.astro +2 -1
- package/src/affiliate/AffiliateCarousel.astro +4 -4
- package/src/affiliate/AffiliateComparisonTable.astro +11 -10
- package/src/affiliate/AmazonProductCard.astro +2 -1
- package/src/affiliate/AmazonProductSection.astro +2 -1
- package/src/affiliate/HotelAffiliateCTA.astro +12 -11
- package/src/affiliate/InlineAffiliateCTA.astro +2 -1
- package/src/affiliate/StickyCTA.astro +4 -3
- package/src/affiliate/TravelpayoutsWidget.astro +2 -6
- package/src/affiliate/provider.ts +3 -1
- package/src/composed/CardCarousel.astro +13 -5
- package/src/composed/EntityCard.astro +0 -2
- package/src/composed/FAQ.astro +2 -1
- package/src/composed/InlineImage.astro +0 -2
- package/src/composed/Pagination.astro +25 -8
- package/src/composed/TableOfContents.astro +10 -5
- package/src/editorial/AlsoExplore.astro +2 -1
- package/src/editorial/ArticleCategoryNav.astro +9 -5
- package/src/editorial/ArticleFeatureCard.astro +2 -1
- package/src/editorial/ArticleTopicPager.astro +7 -4
- package/src/editorial/NotFoundPage.astro +5 -2
- package/src/editorial/VisitTiming.astro +8 -4
- package/src/editorial/index.ts +1 -1
- package/src/editorial/types.ts +0 -4
- package/src/internal/labels.ts +102 -0
- package/src/layout/BreadcrumbBand.astro +4 -3
- package/src/layout/EntityDetailShell.astro +84 -0
- package/src/layout/ListingShell.astro +4 -2
- package/src/layout/analytics.ts +0 -2
- package/src/layout/index.ts +2 -1
- package/src/navigation/ArticlesNavMenu.astro +2 -1
- package/src/navigation/SiteFooter.astro +10 -4
- package/src/navigation/SiteHeader.astro +4 -3
- package/src/primitives/index.ts +1 -0
- package/src/seo/ArticleMetadata.astro +6 -5
- package/src/seo/AuthoritativeSources.astro +7 -6
- package/src/seo/Breadcrumb.astro +3 -2
- package/src/seo/author.ts +5 -3
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* ArticleCategoryNav — "All" plus one chip per populated topic, each with a count badge. `categories` is the site's
|
|
4
4
|
* category map in display order; a category with no article is left out.
|
|
5
5
|
*/
|
|
6
|
-
import { url } from '@sarimarcus/content-sites-core/url';
|
|
6
|
+
import { routePath, url, type SiteRoutes } from '@sarimarcus/content-sites-core/url';
|
|
7
7
|
import Chip from '../primitives/Chip.astro';
|
|
8
8
|
import Badge from '../primitives/Badge.astro';
|
|
9
9
|
import type { ArticleCategoryLink, ArticleCategoryMeta } from './types';
|
|
10
|
+
import { LABELS } from '../internal/labels';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
articles: readonly { data: { category?: string | null } }[];
|
|
@@ -14,6 +15,8 @@ interface Props {
|
|
|
14
15
|
current?: string;
|
|
15
16
|
ariaLabel?: string;
|
|
16
17
|
allLabel?: string;
|
|
18
|
+
/** Overrides of the article routes (`articles`, `articleTopic`). */
|
|
19
|
+
routes?: SiteRoutes;
|
|
17
20
|
class?: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -21,8 +24,9 @@ const {
|
|
|
21
24
|
articles,
|
|
22
25
|
categories: categoryMeta,
|
|
23
26
|
current,
|
|
24
|
-
ariaLabel =
|
|
25
|
-
allLabel =
|
|
27
|
+
ariaLabel = LABELS.articleCategoryNav.ariaLabel,
|
|
28
|
+
allLabel = LABELS.articleCategoryNav.allLabel,
|
|
29
|
+
routes,
|
|
26
30
|
class: className,
|
|
27
31
|
} = Astro.props;
|
|
28
32
|
|
|
@@ -33,13 +37,13 @@ for (const a of articles) {
|
|
|
33
37
|
const categories: ArticleCategoryLink[] = Object.keys(categoryMeta)
|
|
34
38
|
.filter((slug) => counts.has(slug))
|
|
35
39
|
.map((slug) => ({
|
|
36
|
-
href: url(
|
|
40
|
+
href: url(routePath('articleTopic', slug, routes)),
|
|
37
41
|
label: categoryMeta[slug].label,
|
|
38
42
|
icon: categoryMeta[slug].icon,
|
|
39
43
|
count: counts.get(slug)!,
|
|
40
44
|
active: current === slug,
|
|
41
45
|
}));
|
|
42
|
-
const all = { href: url('
|
|
46
|
+
const all = { href: url(routePath('articles', undefined, routes)), label: allLabel, count: articles.length, active: !current };
|
|
43
47
|
const countClass = (active: boolean) => (active ? 'bg-overlay-foreground/20' : 'bg-muted');
|
|
44
48
|
---
|
|
45
49
|
|
|
@@ -7,6 +7,7 @@ import type { ImageMetadata } from 'astro';
|
|
|
7
7
|
import ArticleByline from './ArticleByline.astro';
|
|
8
8
|
import Icon from '../primitives/Icon.astro';
|
|
9
9
|
import ResponsivePicture from '../primitives/ResponsivePicture.astro';
|
|
10
|
+
import { LABELS } from '../internal/labels';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
href: string;
|
|
@@ -32,7 +33,7 @@ interface Props {
|
|
|
32
33
|
class?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const { href, title, description, author, label, image, imageAlt = '', dateTime, dateLabel, updated = false, readLabel =
|
|
36
|
+
const { href, title, description, author, label, image, imageAlt = '', dateTime, dateLabel, updated = false, readLabel = LABELS.articleFeatureCard.readLabel, updatedLabel = LABELS.articleFeatureCard.updatedLabel, readingLabel, loading = 'eager', headingLevel = 2, pagefindIgnore = false, class: className = 'mb-12' } = Astro.props;
|
|
36
37
|
const Heading = `h${headingLevel}` as 'h2' | 'h3';
|
|
37
38
|
---
|
|
38
39
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { formatUtcDate } from '@sarimarcus/content-sites-core/dates';
|
|
8
8
|
import { Icon, LinkArrow } from '../primitives/index.ts';
|
|
9
9
|
import type { TopicPagerLink } from './types';
|
|
10
|
+
import { LABELS } from '../internal/labels';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
topic: { label: string; href: string; count?: number };
|
|
@@ -15,6 +16,8 @@ interface Props {
|
|
|
15
16
|
heading?: string;
|
|
16
17
|
newerLabel?: string;
|
|
17
18
|
olderLabel?: string;
|
|
19
|
+
/** The topic hub link's text. */
|
|
20
|
+
allLabel?: string;
|
|
18
21
|
class?: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -22,9 +25,10 @@ const {
|
|
|
22
25
|
topic,
|
|
23
26
|
newer,
|
|
24
27
|
older,
|
|
25
|
-
heading =
|
|
26
|
-
newerLabel =
|
|
27
|
-
olderLabel =
|
|
28
|
+
heading = LABELS.articleTopicPager.heading(topic.label),
|
|
29
|
+
newerLabel = LABELS.articleTopicPager.newerLabel,
|
|
30
|
+
olderLabel = LABELS.articleTopicPager.olderLabel,
|
|
31
|
+
allLabel = LABELS.articleTopicPager.allLabel(topic.label, topic.count),
|
|
28
32
|
class: className,
|
|
29
33
|
} = Astro.props;
|
|
30
34
|
|
|
@@ -33,7 +37,6 @@ const sides = [
|
|
|
33
37
|
newer && { link: newer, label: newerLabel, dir: 'newer' as const },
|
|
34
38
|
older && { link: older, label: olderLabel, dir: 'older' as const },
|
|
35
39
|
].filter(Boolean) as { link: TopicPagerLink; label: string; dir: 'newer' | 'older' }[];
|
|
36
|
-
const allLabel = topic.count ? `All ${topic.count} ${topic.label} articles` : `All ${topic.label} articles`;
|
|
37
40
|
---
|
|
38
41
|
|
|
39
42
|
{sides.length > 0 && (
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import Button from '../primitives/Button.astro';
|
|
8
8
|
import LinkArrow from '../primitives/LinkArrow.astro';
|
|
9
9
|
import Icon from '../primitives/Icon.astro';
|
|
10
|
+
import { LABELS } from '../internal/labels';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
tagline: string;
|
|
@@ -19,12 +20,14 @@ interface Props {
|
|
|
19
20
|
exploreLabel: string;
|
|
20
21
|
/** Accessible name of the section links. */
|
|
21
22
|
exploreAriaLabel?: string;
|
|
23
|
+
/** The shortcut hint beside the search link. */
|
|
24
|
+
searchShortcut?: string;
|
|
22
25
|
links: { href: string; label: string }[];
|
|
23
26
|
/** Classes of the search link; a dark theme passes its own border and background tokens. */
|
|
24
27
|
searchBoxClass?: string;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
const { tagline, lede, homeHref, homeLabel, searchHref, searchLabel, searchAriaLabel, exploreLabel, exploreAriaLabel =
|
|
30
|
+
const { tagline, lede, homeHref, homeLabel, searchHref, searchLabel, searchAriaLabel, exploreLabel, exploreAriaLabel = LABELS.notFoundPage.exploreAriaLabel, searchShortcut = LABELS.notFoundPage.searchShortcut, links, searchBoxClass = 'group flex items-center gap-3 w-full max-w-sm mx-auto px-4 py-3 rounded-xl border border-foreground/20 bg-white/60 backdrop-blur-sm text-left hover:border-primary/40 hover:bg-white/80 transition-all no-underline' } = Astro.props;
|
|
28
31
|
---
|
|
29
32
|
|
|
30
33
|
<main class="relative min-h-[80vh] flex items-center justify-center overflow-hidden bg-muted">
|
|
@@ -81,7 +84,7 @@ const { tagline, lede, homeHref, homeLabel, searchHref, searchLabel, searchAriaL
|
|
|
81
84
|
>
|
|
82
85
|
<Icon name="search" class="w-4 h-4 text-muted-foreground shrink-0" />
|
|
83
86
|
<span class="text-muted-foreground text-sm flex-1">{searchLabel}</span>
|
|
84
|
-
<kbd class="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono border border-border text-muted-foreground/60 bg-muted/50"
|
|
87
|
+
<kbd class="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-xs font-mono border border-border text-muted-foreground/60 bg-muted/50">{searchShortcut}</kbd>
|
|
85
88
|
</a>
|
|
86
89
|
</div>
|
|
87
90
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* default top margin (`mt-12`); `labelClass` sets the slot label size.
|
|
5
5
|
*/
|
|
6
6
|
import Icon from '../primitives/Icon.astro';
|
|
7
|
+
import { LABELS } from '../internal/labels';
|
|
7
8
|
|
|
8
9
|
export interface VisitTimingData {
|
|
9
10
|
bestSeason?: string;
|
|
@@ -19,26 +20,29 @@ interface Props {
|
|
|
19
20
|
id?: string;
|
|
20
21
|
class?: string;
|
|
21
22
|
labelClass?: string;
|
|
23
|
+
/** Row labels; default English. */
|
|
24
|
+
labels?: { bestSeason: string; quietestHours: string; visitDuration: string };
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
const {
|
|
25
28
|
visitTiming,
|
|
26
|
-
title =
|
|
29
|
+
title = LABELS.visitTiming.title,
|
|
27
30
|
id = 'when-to-visit',
|
|
28
31
|
class: className = 'mt-12',
|
|
29
32
|
labelClass = 'text-[0.8125rem]',
|
|
33
|
+
labels = LABELS.visitTiming,
|
|
30
34
|
} = Astro.props;
|
|
31
35
|
|
|
32
36
|
const slots = visitTiming
|
|
33
37
|
? [
|
|
34
38
|
visitTiming.bestSeason
|
|
35
|
-
? { icon: 'calendar', label:
|
|
39
|
+
? { icon: 'calendar', label: labels.bestSeason, value: visitTiming.bestSeason }
|
|
36
40
|
: null,
|
|
37
41
|
visitTiming.quietestHours
|
|
38
|
-
? { icon: 'clock', label:
|
|
42
|
+
? { icon: 'clock', label: labels.quietestHours, value: visitTiming.quietestHours }
|
|
39
43
|
: null,
|
|
40
44
|
visitTiming.visitDuration
|
|
41
|
-
? { icon: 'hourglass', label:
|
|
45
|
+
? { icon: 'hourglass', label: labels.visitDuration, value: visitTiming.visitDuration }
|
|
42
46
|
: null,
|
|
43
47
|
].filter((s): s is { icon: string; label: string; value: string } => s !== null)
|
|
44
48
|
: [];
|
package/src/editorial/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { default as ArticleCategoryNav } from './ArticleCategoryNav.astro';
|
|
|
16
16
|
export { default as ArticleTopicPage } from './ArticleTopicPage.astro';
|
|
17
17
|
export { default as ArticleTopicPager } from './ArticleTopicPager.astro';
|
|
18
18
|
export { rankByCount } from './topic';
|
|
19
|
-
export type { AuthorSummary, ArticleSummary, KeyFact,
|
|
19
|
+
export type { AuthorSummary, ArticleSummary, KeyFact, ArticleCategoryLink, ArticleCategoryMeta, TopicAuthorCard, TopicPagerLink } from './types';
|
|
20
20
|
export { default as InsiderTips } from './InsiderTips.astro';
|
|
21
21
|
export type { InsiderTip, InsiderTipAuthor, InsiderTipKinds, InsiderTipTone } from './types';
|
|
22
22
|
export { default as AuthorStrip } from './AuthorStrip.astro';
|
package/src/editorial/types.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ImageMetadata } from 'astro';
|
|
2
2
|
import type { BrandTone, TrustedHtml } from '../types';
|
|
3
|
-
import type { SourceInput } from '../seo/types';
|
|
4
3
|
|
|
5
4
|
/** A person as listed on author strips and cards. The byline shape is ui/seo `ArticleAuthor`. */
|
|
6
5
|
export interface AuthorSummary {
|
|
@@ -34,9 +33,6 @@ export interface KeyFact {
|
|
|
34
33
|
detailHtml?: TrustedHtml;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
/** @deprecated Import `SourceInput` from `@sarimarcus/content-sites-ui/seo`. Removed in the next minor (SLA-3020). */
|
|
38
|
-
export type SourceItem = SourceInput;
|
|
39
|
-
|
|
40
36
|
/** The fields ArticleCategoryNav reads from a site's category map entry. */
|
|
41
37
|
export interface ArticleCategoryMeta {
|
|
42
38
|
label: string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// The English copy ui components fall back to when the site passes no prop (SLA-2991). A template never writes
|
|
2
|
+
// reader-visible text itself (validate-packages refuses it); it reads a prop whose default is here.
|
|
3
|
+
export const LABELS = {
|
|
4
|
+
affiliateCard: {
|
|
5
|
+
pricePrefix: 'From ',
|
|
6
|
+
bestSeller: 'Best Seller',
|
|
7
|
+
topRated: 'Top Rated',
|
|
8
|
+
skipTheLine: 'Skip the line',
|
|
9
|
+
cta: 'Reserve Your Spot',
|
|
10
|
+
reviewCount: (n: string) => `(${n} reviews)`,
|
|
11
|
+
},
|
|
12
|
+
affiliateCardSection: { title: 'Recommended Tours & Activities' },
|
|
13
|
+
affiliateCarousel: { heading: 'Recommended tours' },
|
|
14
|
+
affiliateComparisonTable: {
|
|
15
|
+
nameLabel: 'Option',
|
|
16
|
+
priceLabel: 'Price',
|
|
17
|
+
featureLabel: 'Highlight',
|
|
18
|
+
caption: 'Comparison of options',
|
|
19
|
+
ctaColumn: 'Book or view',
|
|
20
|
+
externalCta: 'Check availability',
|
|
21
|
+
internalCta: 'View details',
|
|
22
|
+
externalAria: (cta: string, name: string) => `${cta}: ${name} (opens in a new tab)`,
|
|
23
|
+
internalAria: (cta: string, name: string) => `${cta}: ${name}`,
|
|
24
|
+
rated: (rating: number, count?: number) => `Rated ${rating} out of 5${count ? ` from ${count} reviews` : ''}`,
|
|
25
|
+
},
|
|
26
|
+
amazonProductCard: { ctaLabel: 'View on Amazon' },
|
|
27
|
+
amazonProductSection: { title: 'Recommended reading' },
|
|
28
|
+
hotelAffiliateCTA: {
|
|
29
|
+
headline: (name: string) => `Find the best rate for ${name}`,
|
|
30
|
+
bookCta: (noun: string) => `Book this ${noun}`,
|
|
31
|
+
searchCta: 'Check prices & availability',
|
|
32
|
+
bookSubhead: 'Check current availability and rates.',
|
|
33
|
+
searchSubhead: (noun: string) => `Search current prices and availability for this ${noun}.`,
|
|
34
|
+
from: 'from ',
|
|
35
|
+
perNight: ' / night',
|
|
36
|
+
reviews: (n: string) => `(${n} reviews)`,
|
|
37
|
+
ratingOn: (source: string) => `on ${source}`,
|
|
38
|
+
freeToSearch: 'Free to search · No sign-up needed',
|
|
39
|
+
officialSite: (noun: string) => `Visit the ${noun}'s official site`,
|
|
40
|
+
},
|
|
41
|
+
inlineAffiliateCTA: { cta: 'See tours' },
|
|
42
|
+
stickyCTA: {
|
|
43
|
+
label: 'Reserve Your Spot',
|
|
44
|
+
note: 'Found the right tour? Reserve your spot before it fills.',
|
|
45
|
+
dismissLabel: 'Dismiss',
|
|
46
|
+
},
|
|
47
|
+
travelpayoutsWidget: {
|
|
48
|
+
hotel: 'Hotel search',
|
|
49
|
+
activity: 'Activity search',
|
|
50
|
+
train: 'Train and bus search',
|
|
51
|
+
car: 'Car rental search',
|
|
52
|
+
},
|
|
53
|
+
cardCarousel: {
|
|
54
|
+
viewAllLabel: 'View all',
|
|
55
|
+
regionLabel: 'Carousel',
|
|
56
|
+
previousLabel: (name: string | undefined) => `Previous, ${name ?? 'carousel'}`,
|
|
57
|
+
nextLabel: (name: string | undefined) => `Next, ${name ?? 'carousel'}`,
|
|
58
|
+
},
|
|
59
|
+
faq: { title: 'Frequently asked questions' },
|
|
60
|
+
pagination: {
|
|
61
|
+
ariaLabel: 'Pagination',
|
|
62
|
+
previousLabel: 'Previous',
|
|
63
|
+
nextLabel: 'Next',
|
|
64
|
+
pageLabel: (page: number) => `Go to page ${page}`,
|
|
65
|
+
currentPageLabel: (page: number) => `Page ${page}, current page`,
|
|
66
|
+
},
|
|
67
|
+
tableOfContents: {
|
|
68
|
+
ariaLabel: 'Table of contents',
|
|
69
|
+
jumpLabel: 'Jump to a section',
|
|
70
|
+
stickyLabel: (name: string) => `${name} (sticky)`,
|
|
71
|
+
},
|
|
72
|
+
alsoExplore: { eyebrow: 'Also explore' },
|
|
73
|
+
articleCategoryNav: { ariaLabel: 'Browse articles by topic', allLabel: 'All' },
|
|
74
|
+
articleFeatureCard: { readLabel: 'Read article', updatedLabel: 'Updated' },
|
|
75
|
+
articleTopicPager: {
|
|
76
|
+
heading: (topic: string) => `Next in ${topic}`,
|
|
77
|
+
newerLabel: 'Newer',
|
|
78
|
+
olderLabel: 'Older',
|
|
79
|
+
allLabel: (topic: string, count?: number) => (count ? `All ${count} ${topic} articles` : `All ${topic} articles`),
|
|
80
|
+
},
|
|
81
|
+
notFoundPage: { exploreAriaLabel: 'Explore sections', searchShortcut: '⌘K' },
|
|
82
|
+
visitTiming: { title: 'When to Visit', bestSeason: 'Best season', quietestHours: 'Quietest hours', visitDuration: 'Visit duration' },
|
|
83
|
+
articlesNavMenu: { latestHeading: 'Latest' },
|
|
84
|
+
siteFooter: { cookieLabel: 'Cookie preferences', rightsLabel: 'All rights reserved.', legalAriaLabel: 'Legal' },
|
|
85
|
+
siteHeader: { label: 'Main navigation', openLabel: 'Open menu', closeLabel: 'Close menu' },
|
|
86
|
+
articleMetadata: {
|
|
87
|
+
writtenByLabel: 'Written by',
|
|
88
|
+
portraitLabel: 'Portrait of',
|
|
89
|
+
updatedLabel: 'Updated',
|
|
90
|
+
publishedLabel: 'Published',
|
|
91
|
+
ariaLabel: 'Article information',
|
|
92
|
+
},
|
|
93
|
+
authoritativeSources: {
|
|
94
|
+
title: 'Sources',
|
|
95
|
+
eyebrow: 'Further reading',
|
|
96
|
+
countLabel: 'references',
|
|
97
|
+
showLabel: 'Show',
|
|
98
|
+
hideLabel: 'Hide',
|
|
99
|
+
newTabLabel: '(opens in a new tab)',
|
|
100
|
+
},
|
|
101
|
+
breadcrumb: { homeLabel: 'Home', ariaLabel: 'Breadcrumb navigation' },
|
|
102
|
+
};
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* BreadcrumbBand — the muted strip under the fixed header that holds a page's breadcrumb trail (default slot).
|
|
4
|
-
* `ignore` keeps it out of the Pagefind body.
|
|
4
|
+
* `ignore` keeps it out of the Pagefind body. `class` replaces the band's own classes (keep `pt-16`, the header offset).
|
|
5
5
|
*/
|
|
6
6
|
import Container from './Container.astro';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
ignore?: boolean;
|
|
10
|
+
class?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const { ignore = true } = Astro.props;
|
|
13
|
+
const { ignore = true, class: className = 'pt-16 bg-muted/50 border-b border-border' } = Astro.props;
|
|
13
14
|
---
|
|
14
15
|
|
|
15
|
-
<div class=
|
|
16
|
+
<div class={className} data-pagefind-ignore={ignore ? '' : undefined}>
|
|
16
17
|
<Container class="py-3">
|
|
17
18
|
<slot />
|
|
18
19
|
</Container>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* EntityDetailShell — the frame of an entity detail page: breadcrumb band, a Pagefind body wrapper around the hero
|
|
4
|
+
* (`data-pagefind-meta="type:<pagefindType>"`, with one sr-only `audience:<a>` filter span per `audiences` entry
|
|
5
|
+
* before the hero), then <main> holding the column (default slot), `after-column` and the `closer` Container.
|
|
6
|
+
* The route keeps Layout, StructuredData (a direct Layout child), the data, the hero and every body block.
|
|
7
|
+
*
|
|
8
|
+
* - `pagefindType` omitted: no wrapper; a route that indexes its body puts its own wrapper in the default slot.
|
|
9
|
+
* - `pagefindScope="page"`: the wrapper also holds <main>, for a page whose whole body is indexed.
|
|
10
|
+
* - `column={false}`: the default slot goes into <main> as is, for a body that leaves the column.
|
|
11
|
+
* - `closer`: renders the `closer` slot in a default Container; it defaults to whether that slot was passed.
|
|
12
|
+
*/
|
|
13
|
+
import BreadcrumbBand from './BreadcrumbBand.astro';
|
|
14
|
+
import Container from './Container.astro';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
pagefindType?: string;
|
|
18
|
+
pagefindScope?: 'hero' | 'page';
|
|
19
|
+
audiences?: readonly string[];
|
|
20
|
+
/** `false` keeps the breadcrumb band in the Pagefind body. */
|
|
21
|
+
breadcrumbIgnore?: boolean;
|
|
22
|
+
mainClass?: string;
|
|
23
|
+
column?: 'measure' | 'content' | false;
|
|
24
|
+
closer?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
pagefindType,
|
|
29
|
+
pagefindScope = 'hero',
|
|
30
|
+
audiences = [],
|
|
31
|
+
breadcrumbIgnore = true,
|
|
32
|
+
mainClass = 'pt-6 pb-12 lg:pt-8 lg:pb-20',
|
|
33
|
+
column = 'measure',
|
|
34
|
+
closer = Astro.slots.has('closer'),
|
|
35
|
+
} = Astro.props;
|
|
36
|
+
const meta = `type:${pagefindType}`;
|
|
37
|
+
const wrapsPage = Boolean(pagefindType) && pagefindScope === 'page';
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<BreadcrumbBand ignore={breadcrumbIgnore}><slot name="breadcrumb" /></BreadcrumbBand>
|
|
41
|
+
|
|
42
|
+
{/* <main> is spelled out per branch: a dynamic wrapper tag buffers its children and moved hoisted scripts above it. */}
|
|
43
|
+
{wrapsPage ? (
|
|
44
|
+
<div data-pagefind-body data-pagefind-meta={meta}>
|
|
45
|
+
{audiences.map((a) => <span class="sr-only" data-pagefind-filter={`audience:${a}`}>{a}</span>)}
|
|
46
|
+
<slot name="hero" />
|
|
47
|
+
<main class={mainClass}>
|
|
48
|
+
{column ? (
|
|
49
|
+
<Container size={column}><slot /></Container>
|
|
50
|
+
) : (
|
|
51
|
+
<slot />
|
|
52
|
+
)}
|
|
53
|
+
<slot name="after-column" />
|
|
54
|
+
{closer && (
|
|
55
|
+
<Container><slot name="closer" /></Container>
|
|
56
|
+
)}
|
|
57
|
+
</main>
|
|
58
|
+
</div>
|
|
59
|
+
) : (
|
|
60
|
+
<Fragment>
|
|
61
|
+
{pagefindType ? (
|
|
62
|
+
<div data-pagefind-body data-pagefind-meta={meta}>
|
|
63
|
+
{audiences.map((a) => <span class="sr-only" data-pagefind-filter={`audience:${a}`}>{a}</span>)}
|
|
64
|
+
<slot name="hero" />
|
|
65
|
+
</div>
|
|
66
|
+
) : (
|
|
67
|
+
<Fragment>
|
|
68
|
+
{audiences.map((a) => <span class="sr-only" data-pagefind-filter={`audience:${a}`}>{a}</span>)}
|
|
69
|
+
<slot name="hero" />
|
|
70
|
+
</Fragment>
|
|
71
|
+
)}
|
|
72
|
+
<main class={mainClass}>
|
|
73
|
+
{column ? (
|
|
74
|
+
<Container size={column}><slot /></Container>
|
|
75
|
+
) : (
|
|
76
|
+
<slot />
|
|
77
|
+
)}
|
|
78
|
+
<slot name="after-column" />
|
|
79
|
+
{closer && (
|
|
80
|
+
<Container><slot name="closer" /></Container>
|
|
81
|
+
)}
|
|
82
|
+
</main>
|
|
83
|
+
</Fragment>
|
|
84
|
+
)}
|
|
@@ -15,13 +15,15 @@ interface Props {
|
|
|
15
15
|
contained?: boolean;
|
|
16
16
|
/** `false` keeps the page indexable, for a listing whose bands carry `data-pagefind-body` prose. */
|
|
17
17
|
pagefindIgnore?: boolean;
|
|
18
|
+
/** Replaces the breadcrumb band's own classes, for a hub whose band takes the hero's colour. */
|
|
19
|
+
breadcrumbClass?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
const { mainClass = 'py-12 lg:py-20', contained = true, pagefindIgnore = true } = Astro.props;
|
|
22
|
+
const { mainClass = 'py-12 lg:py-20', contained = true, pagefindIgnore = true, breadcrumbClass } = Astro.props;
|
|
21
23
|
---
|
|
22
24
|
|
|
23
25
|
<div data-pagefind-ignore={pagefindIgnore ? '' : undefined}>
|
|
24
|
-
<BreadcrumbBand ignore={false}>
|
|
26
|
+
<BreadcrumbBand ignore={false} class={breadcrumbClass}>
|
|
25
27
|
<slot name="breadcrumb" />
|
|
26
28
|
</BreadcrumbBand>
|
|
27
29
|
|
package/src/layout/analytics.ts
CHANGED
|
@@ -22,8 +22,6 @@ export function track(eventName: string, params: Record<string, unknown> = {}):
|
|
|
22
22
|
window.dataLayer.push({ event: eventName, page_path: window.location.pathname, ...params });
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export { pageTypeOf };
|
|
26
|
-
|
|
27
25
|
// Param key derived from the data-track-* attribute name where the
|
|
28
26
|
// event-specific prefix (e.g. 'cta_', 'affiliate_', 'link_') has been stripped.
|
|
29
27
|
const HREF_DESTINATION_KEYS: Record<string, string> = {
|
package/src/layout/index.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
export { default as Container } from './Container.astro';
|
|
3
3
|
export { default as BreadcrumbBand } from './BreadcrumbBand.astro';
|
|
4
4
|
export { default as ListingShell } from './ListingShell.astro';
|
|
5
|
+
export { default as EntityDetailShell } from './EntityDetailShell.astro';
|
|
5
6
|
export { default as AsyncStylesheet } from './AsyncStylesheet.astro';
|
|
6
7
|
export { default as DocumentShell } from './DocumentShell.astro';
|
|
7
8
|
export { default as ConsentAnalytics } from './ConsentAnalytics.astro';
|
|
8
9
|
export type { ConsentType } from './ConsentAnalytics.astro';
|
|
9
10
|
export { shellResources, GTM_ORIGIN, SILKTIDE_BASE, SILKTIDE_STYLESHEET } from './resources.ts';
|
|
10
|
-
export { track,
|
|
11
|
+
export { track, setupDelegatedTracking, setupFaqTracking, setupScrollDepthTracking, setupAnalytics } from './analytics.ts';
|
|
11
12
|
export type { AnalyticsOptions } from './analytics.ts';
|
|
@@ -15,6 +15,7 @@ import MobileNavLink from './MobileNavLink.astro';
|
|
|
15
15
|
import { isActivePath } from './activePath.ts';
|
|
16
16
|
import { TONES } from './tones.ts';
|
|
17
17
|
import type { ArticlesNavProps } from './types.ts';
|
|
18
|
+
import { LABELS } from '../internal/labels';
|
|
18
19
|
|
|
19
20
|
type Props = ArticlesNavProps;
|
|
20
21
|
|
|
@@ -26,7 +27,7 @@ const {
|
|
|
26
27
|
tone = 'primary',
|
|
27
28
|
topicsHeading,
|
|
28
29
|
topics,
|
|
29
|
-
latestHeading =
|
|
30
|
+
latestHeading = LABELS.articlesNavMenu.latestHeading,
|
|
30
31
|
latest = [],
|
|
31
32
|
all,
|
|
32
33
|
iconStyle = 'plain',
|
|
@@ -7,12 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Icon } from '../primitives/index.ts';
|
|
9
9
|
import type { FooterSection, NavLink, SiteIdentity } from './types.ts';
|
|
10
|
+
import { LABELS } from '../internal/labels';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
identity: SiteIdentity;
|
|
13
14
|
sections?: FooterSection[];
|
|
14
15
|
legal?: NavLink[];
|
|
15
16
|
cookieLabel?: string;
|
|
17
|
+
/** Follows the copyright line's site name. */
|
|
18
|
+
rightsLabel?: string;
|
|
19
|
+
/** Accessible name of the legal links. */
|
|
20
|
+
legalAriaLabel?: string;
|
|
16
21
|
/** Shown between legal items (aria-hidden), e.g. `|`. */
|
|
17
22
|
separator?: string;
|
|
18
23
|
year?: number;
|
|
@@ -34,7 +39,9 @@ const {
|
|
|
34
39
|
identity,
|
|
35
40
|
sections = [],
|
|
36
41
|
legal = [],
|
|
37
|
-
cookieLabel =
|
|
42
|
+
cookieLabel = LABELS.siteFooter.cookieLabel,
|
|
43
|
+
rightsLabel = LABELS.siteFooter.rightsLabel,
|
|
44
|
+
legalAriaLabel = LABELS.siteFooter.legalAriaLabel,
|
|
38
45
|
separator,
|
|
39
46
|
year = new Date().getFullYear(),
|
|
40
47
|
class: className = 'bg-inverse text-inverse-muted',
|
|
@@ -81,9 +88,8 @@ const legalItemClass = `${smallClass} inline-flex min-h-11 items-center hover:te
|
|
|
81
88
|
<slot name="bottom" />
|
|
82
89
|
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
|
|
83
90
|
<p class={smallClass}>
|
|
84
|
-
© {year} {identity.name}.
|
|
85
|
-
|
|
86
|
-
<nav class:list={['flex flex-wrap items-center justify-center sm:justify-end', legalClass]} aria-label="Legal">
|
|
91
|
+
© {year} {identity.name}. {`${rightsLabel}\n`}</p>
|
|
92
|
+
<nav class:list={['flex flex-wrap items-center justify-center sm:justify-end', legalClass]} aria-label={legalAriaLabel}>
|
|
87
93
|
{legal.map((link) => (
|
|
88
94
|
<>
|
|
89
95
|
<a href={link.href} class={legalItemClass}>{link.label}</a>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `<nav>`, so it carries its own `data-pagefind-ignore`.
|
|
13
13
|
*/
|
|
14
14
|
import { Icon } from '../primitives/index.ts';
|
|
15
|
+
import { LABELS } from '../internal/labels';
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
17
18
|
label?: string;
|
|
@@ -29,12 +30,12 @@ interface Props {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const {
|
|
32
|
-
label =
|
|
33
|
+
label = LABELS.siteHeader.label,
|
|
33
34
|
surfaceClass = 'bg-background/95 backdrop-blur-md',
|
|
34
35
|
class: className = `fixed top-0 left-0 right-0 z-50 transition-[background-color,backdrop-filter,border-color,box-shadow] duration-300 border-b border-border ${surfaceClass}`,
|
|
35
36
|
toggleClass = 'p-2 min-h-[44px] min-w-[44px] flex items-center justify-center rounded-lg transition-colors text-foreground hover:bg-muted cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
|
|
36
|
-
openLabel =
|
|
37
|
-
closeLabel =
|
|
37
|
+
openLabel = LABELS.siteHeader.openLabel,
|
|
38
|
+
closeLabel = LABELS.siteHeader.closeLabel,
|
|
38
39
|
searchPath,
|
|
39
40
|
id = 'main-nav',
|
|
40
41
|
panelId = 'mobile-menu',
|
package/src/primitives/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Icon } from './Icon.astro';
|
|
|
3
3
|
export { default as Badge } from './Badge.astro';
|
|
4
4
|
export { default as Chip } from './Chip.astro';
|
|
5
5
|
export { default as ArrowLabel } from './ArrowLabel.astro';
|
|
6
|
+
export { default as ArrowGlyph } from './ArrowGlyph.astro';
|
|
6
7
|
export { default as RatingStars } from './RatingStars.astro';
|
|
7
8
|
export { default as SectionDivider } from './SectionDivider.astro';
|
|
8
9
|
export { default as LinkArrow } from './LinkArrow.astro';
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Image } from 'astro:assets';
|
|
17
17
|
import type { ArticleAuthor } from './types';
|
|
18
|
+
import { LABELS } from '../internal/labels';
|
|
18
19
|
|
|
19
20
|
interface Props {
|
|
20
21
|
author?: ArticleAuthor | null;
|
|
@@ -42,11 +43,11 @@ const {
|
|
|
42
43
|
dateModified,
|
|
43
44
|
timeZone = 'UTC',
|
|
44
45
|
locale = 'en-GB',
|
|
45
|
-
writtenByLabel =
|
|
46
|
-
portraitLabel =
|
|
47
|
-
updatedLabel =
|
|
48
|
-
publishedLabel =
|
|
49
|
-
ariaLabel =
|
|
46
|
+
writtenByLabel = LABELS.articleMetadata.writtenByLabel,
|
|
47
|
+
portraitLabel = LABELS.articleMetadata.portraitLabel,
|
|
48
|
+
updatedLabel = LABELS.articleMetadata.updatedLabel,
|
|
49
|
+
publishedLabel = LABELS.articleMetadata.publishedLabel,
|
|
50
|
+
ariaLabel = LABELS.articleMetadata.ariaLabel,
|
|
50
51
|
placeholderClass = 'bg-gradient-to-br from-primary/20 to-accent/20',
|
|
51
52
|
updatedIconClass = 'text-primary',
|
|
52
53
|
bylineWhitespace = 'flat',
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { outboundClassifier } from '@sarimarcus/content-sites-core/url';
|
|
19
19
|
import type { SourceInput, DomainClassifier } from './types';
|
|
20
|
+
import { LABELS } from '../internal/labels';
|
|
20
21
|
|
|
21
22
|
interface Props {
|
|
22
23
|
sources: SourceInput[];
|
|
@@ -39,18 +40,18 @@ interface Props {
|
|
|
39
40
|
|
|
40
41
|
const {
|
|
41
42
|
sources,
|
|
42
|
-
title =
|
|
43
|
+
title = LABELS.authoritativeSources.title,
|
|
43
44
|
class: className = '',
|
|
44
|
-
eyebrow =
|
|
45
|
+
eyebrow = LABELS.authoritativeSources.eyebrow,
|
|
45
46
|
eyebrowClass = 'text-xs font-semibold uppercase tracking-[0.18em] text-primary',
|
|
46
47
|
domainLabels = {},
|
|
47
48
|
domainClass = {},
|
|
48
49
|
disambiguate = false,
|
|
49
50
|
disambiguationSeparator = ' — ',
|
|
50
|
-
countLabel =
|
|
51
|
-
showLabel =
|
|
52
|
-
hideLabel =
|
|
53
|
-
newTabLabel =
|
|
51
|
+
countLabel = LABELS.authoritativeSources.countLabel,
|
|
52
|
+
showLabel = LABELS.authoritativeSources.showLabel,
|
|
53
|
+
hideLabel = LABELS.authoritativeSources.hideLabel,
|
|
54
|
+
newTabLabel = LABELS.authoritativeSources.newTabLabel,
|
|
54
55
|
toggleSizeClass = 'text-sm',
|
|
55
56
|
} = Astro.props;
|
|
56
57
|
|
package/src/seo/Breadcrumb.astro
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { url } from '@sarimarcus/content-sites-core/url';
|
|
14
14
|
import Icon from '../primitives/Icon.astro';
|
|
15
15
|
import type { BreadcrumbItem } from './types';
|
|
16
|
+
import { LABELS } from '../internal/labels';
|
|
16
17
|
|
|
17
18
|
interface Props {
|
|
18
19
|
items: BreadcrumbItem[];
|
|
@@ -36,9 +37,9 @@ const {
|
|
|
36
37
|
compactMobile = false,
|
|
37
38
|
separator = 'chevron',
|
|
38
39
|
tapTargets = true,
|
|
39
|
-
homeLabel =
|
|
40
|
+
homeLabel = LABELS.breadcrumb.homeLabel,
|
|
40
41
|
homeHref = '/',
|
|
41
|
-
ariaLabel =
|
|
42
|
+
ariaLabel = LABELS.breadcrumb.ariaLabel,
|
|
42
43
|
basePath = '/',
|
|
43
44
|
} = Astro.props;
|
|
44
45
|
|