@sarimarcus/content-sites-ui 0.26.0 → 0.27.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 +4 -0
- package/README.md +5 -0
- package/package.json +2 -2
- package/src/composed/index.ts +1 -2
- package/src/editorial/ArticleTopicPage.astro +109 -0
- package/src/editorial/index.ts +3 -1
- package/src/editorial/topic.ts +9 -0
- package/src/editorial/types.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All packages in the platform release in lockstep; entries are per release version.
|
|
4
4
|
|
|
5
|
+
## 0.27.0 (2026-10-05)
|
|
6
|
+
|
|
7
|
+
- `./editorial`: `ArticleTopicPage`, the body of an article topic hub (`/articles/topic/<category>`) and its paginated pages, promoted from the tourism sites' identical route copies. Strings, the overview and the author cards are props; the blocks a site renders through its own adapters (start-here, grid, pagination, go-deeper, places, faq) are slots. A page above 1 renders only the topic nav, grid and pagination. Also `rankByCount` and type `TopicAuthorCard`. Proven on both tourism sites: their topic routes rewritten onto it render every page byte-identical (SLA-2975).
|
|
8
|
+
|
|
5
9
|
## 0.26.0 (2026-10-05)
|
|
6
10
|
|
|
7
11
|
- `./affiliate`: `AffiliateCardSection` and `AmazonProductSection` render through one band shell and grid table; `AffiliateCard` and `AmazonProductCard` share their link, body, CTA and badge classes; `TravelpayoutsWidget` renders one widget body for both variants and takes `WidgetKind`. Output is unchanged (SLA-2921).
|
package/README.md
CHANGED
|
@@ -99,6 +99,11 @@ prose stay in the route so the site's link and outbound-domain scanners still se
|
|
|
99
99
|
`cta` box. Carries no Pagefind body.
|
|
100
100
|
- `NotFoundPage`: the 404 body; the route passes `noindex` to Layout and imports `@sarimarcus/content-sites-ui/not-found.css`.
|
|
101
101
|
- `ArticlesListingPage` / `ArticleFeatureCard`: article index, topic hubs and their paginated pages.
|
|
102
|
+
- `ArticleTopicPage`: the body of `/articles/topic/<category>` and its paginated pages, around the topic nav. Blocks
|
|
103
|
+
the site renders through its own adapters arrive as slots: `start-here`, `grid`, `pagination`, `go-deeper`,
|
|
104
|
+
`places`, `faq` (wrapped in a measure column only when filled), plus `breadcrumb`, `header-extra` and
|
|
105
|
+
`overview-byline`. The overview and the author cards are props. A page above 1 renders only the nav, grid and
|
|
106
|
+
pagination. `rankByCount` ranks a topic's authors or collections by article count.
|
|
102
107
|
- `ListingShell`: listing hubs (breadcrumb, `hero`, `before-main`, Container'd `<main>`, `after-main`), one outer
|
|
103
108
|
`data-pagefind-ignore`.
|
|
104
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarimarcus/content-sites-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Universal Astro UI components, layout shell and design tokens shared by the content sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"@lucide/astro": "^1.23.0",
|
|
50
50
|
"astro": "^7.0.6",
|
|
51
51
|
"tailwindcss": "^4.1.18",
|
|
52
|
-
"@sarimarcus/content-sites-core": "0.
|
|
52
|
+
"@sarimarcus/content-sites-core": "0.27.0"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/composed/index.ts
CHANGED
|
@@ -9,5 +9,4 @@ export { default as EntityCard } from './EntityCard.astro';
|
|
|
9
9
|
export { default as ArrowLinkGrid } from './ArrowLinkGrid.astro';
|
|
10
10
|
// RelatedContent sits here, not in ./editorial: it renders CardCarousel, whose <style> ships to every importer of the barrel.
|
|
11
11
|
export { default as RelatedContent } from './RelatedContent.astro';
|
|
12
|
-
export { default as RelatedContentCard } from './RelatedContentCard.astro';
|
|
13
|
-
export type { RelatedItem, RelatedTone } from './related';
|
|
12
|
+
export { default as RelatedContentCard } from './RelatedContentCard.astro';export type { RelatedItem, RelatedTone } from './related';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ArticleTopicPage — an article topic hub (/articles/topic/<category>) or one of its paginated pages, inside
|
|
4
|
+
* ArticlesListingPage. The route keeps paths, data, SEO, schema and the site Layout; every string arrives as a prop.
|
|
5
|
+
* Blocks the site renders through its own adapters (cards, related content, FAQ, pagination) arrive as slots, so its
|
|
6
|
+
* markdown, colour and URL rules still apply. A page above 1 renders only the topic nav, the grid and pagination.
|
|
7
|
+
* Slots:
|
|
8
|
+
* breadcrumb — the site Breadcrumb.
|
|
9
|
+
* header-extra — a line under the lede (e.g. the article count). Page 1 only.
|
|
10
|
+
* start-here — the site's related-content block above the overview. Page 1 only.
|
|
11
|
+
* overview-byline — the overview byline, under its heading.
|
|
12
|
+
* grid — the site's article card grid for this page.
|
|
13
|
+
* pagination — the site's pagination.
|
|
14
|
+
* go-deeper, places — related-content blocks after the list. Page 1 only.
|
|
15
|
+
* faq — the site's FAQ, in a measure column. Page 1 only.
|
|
16
|
+
*/
|
|
17
|
+
import Container from '../layout/Container.astro';
|
|
18
|
+
import Callout from '../primitives/Callout.astro';
|
|
19
|
+
import type { TrustedHtml } from '../types';
|
|
20
|
+
import ArticlesListingPage from './ArticlesListingPage.astro';
|
|
21
|
+
import ArticleCategoryNav from './ArticleCategoryNav.astro';
|
|
22
|
+
import type { ArticleCategoryMeta, TopicAuthorCard } from './types';
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
eyebrow: string;
|
|
26
|
+
heading: string;
|
|
27
|
+
lede: string;
|
|
28
|
+
/** Every article, for the topic nav's counts. */
|
|
29
|
+
articles: readonly { data: { category?: string | null } }[];
|
|
30
|
+
categories: Readonly<Record<string, ArticleCategoryMeta>>;
|
|
31
|
+
category: string;
|
|
32
|
+
/** Page 1 is the hub; a later page carries only the nav, grid and pagination. */
|
|
33
|
+
currentPage: number;
|
|
34
|
+
/** Heading over the page-1 list. */
|
|
35
|
+
listHeading?: string;
|
|
36
|
+
overview?: { heading: string; html: TrustedHtml; guide?: { title: string; lead: string; href: string; linkLabel: string } };
|
|
37
|
+
authors?: { heading: string; items: TopicAuthorCard[] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { eyebrow, heading, lede, articles, categories, category, currentPage, listHeading, overview, authors } = Astro.props;
|
|
41
|
+
const hub = currentPage === 1;
|
|
42
|
+
// Astro.slots.has() is true for a slotted expression that evaluated to false, so test what it rendered.
|
|
43
|
+
const faqHtml = hub && Astro.slots.has('faq') ? (await Astro.slots.render('faq')).trim() : '';
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
{hub ? (
|
|
47
|
+
<ArticlesListingPage eyebrow={eyebrow} heading={heading} lede={lede} pagefindBody>
|
|
48
|
+
<slot name="breadcrumb" slot="breadcrumb" />
|
|
49
|
+
<slot name="header-extra" slot="header-extra" />
|
|
50
|
+
<Container>
|
|
51
|
+
<ArticleCategoryNav articles={articles} categories={categories} current={category} class="mb-12" />
|
|
52
|
+
<slot name="start-here" /></Container>
|
|
53
|
+
|
|
54
|
+
{overview && (
|
|
55
|
+
<section id="overview" class="mb-14">
|
|
56
|
+
<Container size="measure">
|
|
57
|
+
<h2 class="text-2xl sm:text-3xl font-normal font-display text-foreground mb-4">{overview.heading}</h2>
|
|
58
|
+
<slot name="overview-byline" />
|
|
59
|
+
<div class="prose prose-lg max-w-none mt-6" set:html={overview.html} />
|
|
60
|
+
{overview.guide && (
|
|
61
|
+
<Callout type="tip" title={overview.guide.title} class="mt-8">
|
|
62
|
+
{overview.guide.lead}<a href={overview.guide.href} class="font-medium text-primary hover:underline">{overview.guide.linkLabel}</a>
|
|
63
|
+
</Callout>
|
|
64
|
+
)}
|
|
65
|
+
</Container>
|
|
66
|
+
</section>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<Container>
|
|
70
|
+
<section id="all-articles">
|
|
71
|
+
{listHeading && <h2 class="text-2xl sm:text-3xl font-normal font-display text-foreground mb-6">{listHeading}</h2>}
|
|
72
|
+
<slot name="grid" />
|
|
73
|
+
<slot name="pagination" />
|
|
74
|
+
</section>
|
|
75
|
+
<slot name="go-deeper" /><slot name="places" /></Container>
|
|
76
|
+
|
|
77
|
+
{faqHtml && (
|
|
78
|
+
<Container size="measure" class="mt-16">
|
|
79
|
+
<Fragment set:html={faqHtml} />
|
|
80
|
+
</Container>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{authors && authors.items.length > 0 && (
|
|
84
|
+
<Container class="mt-16">
|
|
85
|
+
<section id="authors" aria-labelledby="topic-authors-heading">
|
|
86
|
+
<h2 id="topic-authors-heading" class="text-xl font-semibold font-display text-foreground mb-5">{authors.heading}</h2>
|
|
87
|
+
<div class="grid gap-4 sm:grid-cols-3">
|
|
88
|
+
{authors.items.map((a) => (
|
|
89
|
+
<a href={a.href} class="block rounded-xl border border-border bg-card p-5 hover:border-primary transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50">
|
|
90
|
+
<span class="block font-semibold text-foreground">{a.name}</span>
|
|
91
|
+
<span class="mt-1 block text-xs text-muted-foreground">{a.countLabel}</span>
|
|
92
|
+
{a.credentials && <span class="mt-2 block text-sm text-muted-foreground line-clamp-3">{a.credentials}</span>}
|
|
93
|
+
</a>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
</Container>
|
|
98
|
+
)}
|
|
99
|
+
</ArticlesListingPage>
|
|
100
|
+
) : (
|
|
101
|
+
<ArticlesListingPage eyebrow={eyebrow} heading={heading} lede={lede}>
|
|
102
|
+
<slot name="breadcrumb" slot="breadcrumb" />
|
|
103
|
+
<Container>
|
|
104
|
+
<ArticleCategoryNav articles={articles} categories={categories} current={category} class="mb-10" />
|
|
105
|
+
<slot name="grid" />
|
|
106
|
+
<slot name="pagination" />
|
|
107
|
+
</Container>
|
|
108
|
+
</ArticlesListingPage>
|
|
109
|
+
)}
|
package/src/editorial/index.ts
CHANGED
|
@@ -13,7 +13,9 @@ export { default as AuthorCard } from './AuthorCard.astro';
|
|
|
13
13
|
export { default as ArticleCard } from './ArticleCard.astro';
|
|
14
14
|
export { default as ArticleGrid } from './ArticleGrid.astro';
|
|
15
15
|
export { default as ArticleCategoryNav } from './ArticleCategoryNav.astro';
|
|
16
|
-
export
|
|
16
|
+
export { default as ArticleTopicPage } from './ArticleTopicPage.astro';
|
|
17
|
+
export { rankByCount } from './topic';
|
|
18
|
+
export type { AuthorSummary, ArticleSummary, KeyFact, SourceItem, ArticleCategoryLink, ArticleCategoryMeta, TopicAuthorCard } from './types';
|
|
17
19
|
export { default as InsiderTips } from './InsiderTips.astro';
|
|
18
20
|
export type { InsiderTip, InsiderTipAuthor, InsiderTipKinds, InsiderTipTone } from './types';
|
|
19
21
|
export { default as AuthorStrip } from './AuthorStrip.astro';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keys that occur most often, with their counts, most frequent first. Ties keep first-seen order, so a topic hub
|
|
3
|
+
* ranks its authors or themed collections the same way on every build.
|
|
4
|
+
*/
|
|
5
|
+
export function rankByCount(keys: Iterable<string>, limit: number): [string, number][] {
|
|
6
|
+
const counts = new Map<string, number>();
|
|
7
|
+
for (const key of keys) counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
8
|
+
return [...counts].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
9
|
+
}
|
package/src/editorial/types.ts
CHANGED
|
@@ -42,6 +42,14 @@ export interface ArticleCategoryMeta {
|
|
|
42
42
|
icon?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** One author card at the foot of an article topic hub. */
|
|
46
|
+
export interface TopicAuthorCard {
|
|
47
|
+
href: string;
|
|
48
|
+
name: string;
|
|
49
|
+
countLabel: string;
|
|
50
|
+
credentials?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
/** One category chip in an article topic nav. */
|
|
46
54
|
export interface ArticleCategoryLink {
|
|
47
55
|
href: string;
|