@sarimarcus/content-sites-ui 0.22.1 → 0.23.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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All packages in the platform release in lockstep; entries are per release version.
4
4
 
5
+ ## 0.23.0 (2026-10-04)
6
+
7
+ - No changes; lockstep release with core (SLA-2958).
8
+
9
+ ## 0.22.2 (2026-10-04)
10
+
11
+ - `./seo`: `toArticleAuthor` (a byline name to ArticleMetadata's author), `astroSchemaInput` (the Astro half of core `createSchemaContext`'s input) and `COMMON_SOURCE_LABELS` (SLA-2918).
12
+ - `./seo` `AuthoritativeSources`: `domainClass` also accepts the synced outbound-domains store, and classifies through core `outboundClassifier`; output unchanged (SLA-2918).
13
+
5
14
  ## 0.22.1 (2026-10-04)
6
15
 
7
16
  - `./affiliate` `resources.ts` declares each widget kind's `connect-src` hosts and the hotel widget's `script-src 'unsafe-eval'`, which the CSP coverage gate now requires. A site rendering a hotel widget needs `'unsafe-eval'` in script-src and `https://suggest.apistp.com` in connect-src; a train widget needs `https://api.omio.com`; activity, train and car need `https://tpo.gg` (SLA-2920).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarimarcus/content-sites-ui",
3
- "version": "0.22.1",
3
+ "version": "0.23.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.22.1"
52
+ "@sarimarcus/content-sites-core": "0.23.0"
53
53
  }
54
54
  }
@@ -7,7 +7,7 @@
7
7
  * A source is a URL string, labelled from `domainLabels` (by host, without `www.`) or else by its host, or an object
8
8
  * { url, name?, description?, quote? }: `description` is a curated blurb, `quote` the verbatim cited sentence.
9
9
  *
10
- * Link policy (Rule 38), from `domainClass` (host → class, or a function of the URL): a competitor renders its label
10
+ * Link policy (Rule 38), from `domainClass` (host → class, the outbound-domains store, or a function of the URL): a competitor renders its label
11
11
  * unlinked (the item stays, so footnote numbers hold), an affiliate link carries rel="noopener sponsored", any other
12
12
  * rel="noopener".
13
13
  *
@@ -15,6 +15,7 @@
15
15
  * ("Host — Prices"), so two pages of one domain stay distinguishable. Explicit names, repeat citations of one URL and
16
16
  * domain-root URLs keep their label.
17
17
  */
18
+ import { outboundClassifier } from '@sarimarcus/content-sites-core/url';
18
19
  import type { SourceInput, DomainClassifier } from './types';
19
20
 
20
21
  interface Props {
@@ -55,19 +56,7 @@ const {
55
56
 
56
57
  if (!sources || sources.length === 0) return null;
57
58
 
58
- const hostKey = (host: string) => host.toLowerCase().replace(/^www\./, '');
59
- const classMap = typeof domainClass === 'function'
60
- ? null
61
- : new Map(Object.entries(domainClass).map(([d, c]) => [hostKey(d), c]));
62
-
63
- function classify(url: string): string {
64
- if (typeof domainClass === 'function') return domainClass(url);
65
- try {
66
- return classMap?.get(hostKey(new URL(url).hostname)) ?? 'other';
67
- } catch {
68
- return 'other';
69
- }
70
- }
59
+ const classify = typeof domainClass === 'function' ? domainClass : outboundClassifier(domainClass);
71
60
 
72
61
  function domainLabel(url: string): string {
73
62
  try {
@@ -0,0 +1,30 @@
1
+ import type { ImageMetadata } from 'astro';
2
+ import { url } from '@sarimarcus/content-sites-core/url';
3
+ import type { ArticleAuthor } from './types';
4
+
5
+ /** The fields of a site's author profile that a byline uses. */
6
+ export interface ArticleAuthorProfile {
7
+ slug: string;
8
+ photo?: string;
9
+ credentials?: string;
10
+ }
11
+
12
+ /**
13
+ * A byline name as ArticleMetadata's author: its profile (from `findProfile`) URL, portrait (from `portrait(slug)`),
14
+ * photo URL and credentials. An unknown name keeps only the name; no name gives no author.
15
+ */
16
+ export function toArticleAuthor(
17
+ name: string | undefined,
18
+ findProfile: (name: string) => ArticleAuthorProfile | undefined,
19
+ portrait: (slug: string) => ImageMetadata | null,
20
+ ): ArticleAuthor | undefined {
21
+ if (!name) return undefined;
22
+ const profile = findProfile(name);
23
+ return {
24
+ name,
25
+ href: profile ? url(`/author/${profile.slug}`) : null,
26
+ image: profile?.slug ? portrait(profile.slug) : null,
27
+ photoUrl: profile?.photo ? url(profile.photo) : null,
28
+ credentials: profile?.credentials ?? null,
29
+ };
30
+ }
package/src/seo/index.ts CHANGED
@@ -5,4 +5,8 @@ export { default as Breadcrumb } from './Breadcrumb.astro';
5
5
  export { default as ArticleMetadata } from './ArticleMetadata.astro';
6
6
  export { default as AuthoritativeSources } from './AuthoritativeSources.astro';
7
7
  export { escapeJsonLd } from './jsonLd';
8
+ export { toArticleAuthor } from './author';
9
+ export type { ArticleAuthorProfile } from './author';
10
+ export { astroSchemaInput } from './schemaContext';
11
+ export { COMMON_SOURCE_LABELS } from './sourceLabels';
8
12
  export type { BreadcrumbItem, ArticleAuthor, SourceInput, DomainClassifier } from './types';
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * The page half of core `createSchemaContext`'s input, from Astro: site URL (else `fallbackSiteUrl`), base path,
6
+ * page path, and `fileExists` on `public/`.
7
+ */
8
+ export function astroSchemaInput(astro: { site?: URL; url: URL }, fallbackSiteUrl: string) {
9
+ return {
10
+ siteUrl: astro.site?.href || fallbackSiteUrl,
11
+ basePath: import.meta.env.BASE_URL.replace(/\/$/, ''),
12
+ pagePath: astro.url.pathname,
13
+ fileExists: (p: string) => fs.existsSync(path.join(process.cwd(), 'public', p)),
14
+ };
15
+ }
@@ -0,0 +1,12 @@
1
+ /** Labels (by host, without `www.`) for the reference sources every site cites; a site spreads its own over these. */
2
+ export const COMMON_SOURCE_LABELS: Readonly<Record<string, string>> = {
3
+ 'wikipedia.org': 'Wikipedia',
4
+ 'en.wikipedia.org': 'Wikipedia',
5
+ 'britannica.com': 'Britannica',
6
+ 'unesco.org': 'UNESCO',
7
+ 'whc.unesco.org': 'UNESCO World Heritage',
8
+ 'tripadvisor.com': 'TripAdvisor',
9
+ 'booking.com': 'Booking.com',
10
+ 'getyourguide.com': 'GetYourGuide',
11
+ 'viator.com': 'Viator',
12
+ };
package/src/seo/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ImageMetadata } from 'astro';
2
+ import type { OutboundDomainStore } from '@sarimarcus/content-sites-core/url';
2
3
 
3
4
  export interface BreadcrumbItem {
4
5
  name: string;
@@ -16,5 +17,5 @@ export interface ArticleAuthor {
16
17
 
17
18
  export type SourceInput = string | { name?: string; url: string; description?: string; quote?: string };
18
19
 
19
- /** Host → outbound class (`competitor`, `affiliate`, …), or a function of the URL. */
20
- export type DomainClassifier = Record<string, string> | ((url: string) => string);
20
+ /** Host → outbound class (`competitor`, `affiliate`, …), the synced outbound-domains store, or a function of the URL. */
21
+ export type DomainClassifier = Record<string, string> | OutboundDomainStore | ((url: string) => string);