@fullstackdatasolutions/articles 0.12.0 → 1.1.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +559 -11
  3. package/dist/index.cjs +984 -388
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +383 -21
  6. package/dist/index.d.ts +383 -21
  7. package/dist/index.js +968 -376
  8. package/dist/index.js.map +1 -1
  9. package/dist/nextjs.cjs +74 -6
  10. package/dist/nextjs.cjs.map +1 -1
  11. package/dist/nextjs.d.cts +149 -0
  12. package/dist/nextjs.d.ts +149 -0
  13. package/dist/nextjs.js +74 -6
  14. package/dist/nextjs.js.map +1 -1
  15. package/dist/server.cjs +665 -27
  16. package/dist/server.cjs.map +1 -1
  17. package/dist/server.d.cts +357 -3
  18. package/dist/server.d.ts +357 -3
  19. package/dist/server.js +643 -29
  20. package/dist/server.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/ArticleCard.tsx +42 -6
  23. package/src/ArticleContent.tsx +144 -5
  24. package/src/ArticleDetailHero.tsx +33 -1
  25. package/src/ArticleNavigation.tsx +32 -1
  26. package/src/ArticleSchemas.tsx +43 -39
  27. package/src/ArticleSocialShare.tsx +54 -10
  28. package/src/ArticlesPage.tsx +56 -5
  29. package/src/AuthorArticlesPage.tsx +308 -14
  30. package/src/AuthorCard.tsx +1 -1
  31. package/src/CategoryArticlesPage.tsx +34 -2
  32. package/src/LatestArticles.tsx +28 -1
  33. package/src/LatestArticlesSection.tsx +15 -1
  34. package/src/PaginationNav.tsx +78 -0
  35. package/src/RelatedArticlesSection.tsx +58 -0
  36. package/src/SeriesArticlesPage.tsx +66 -0
  37. package/src/__tests__/ArticleCard.test.tsx +63 -3
  38. package/src/__tests__/ArticleContent.test.tsx +143 -0
  39. package/src/__tests__/ArticleDetailHero.test.tsx +30 -0
  40. package/src/__tests__/ArticleNavigation.test.tsx +81 -3
  41. package/src/__tests__/ArticleSchemas.test.tsx +155 -81
  42. package/src/__tests__/ArticleSocialShare.test.tsx +54 -0
  43. package/src/__tests__/ArticlesPage.test.tsx +131 -0
  44. package/src/__tests__/AuthorArticlesPage.test.tsx +304 -3
  45. package/src/__tests__/CategoryArticlesPage.test.tsx +116 -1
  46. package/src/__tests__/LatestArticles.test.tsx +52 -0
  47. package/src/__tests__/LatestArticlesSection.test.tsx +28 -0
  48. package/src/__tests__/PaginationNav.test.tsx +73 -0
  49. package/src/__tests__/RelatedArticlesSection.test.tsx +132 -0
  50. package/src/__tests__/SeriesArticlesPage.test.tsx +121 -0
  51. package/src/__tests__/eventTracking.test.tsx +145 -0
  52. package/src/__tests__/events.test.ts +82 -0
  53. package/src/__tests__/markdown.test.ts +78 -1
  54. package/src/__tests__/pagination.test.ts +178 -0
  55. package/src/__tests__/seoUtils-authors.test.ts +37 -0
  56. package/src/__tests__/seoUtils.test.ts +246 -0
  57. package/src/__tests__/server-articles.test.ts +356 -1
  58. package/src/__tests__/validateArticles.test.ts +312 -0
  59. package/src/articleTypes.ts +109 -0
  60. package/src/articlesConfig.ts +45 -1
  61. package/src/eventTracking.tsx +97 -0
  62. package/src/events.ts +105 -0
  63. package/src/index.ts +26 -1
  64. package/src/markdown.ts +41 -0
  65. package/src/pagination.ts +93 -0
  66. package/src/seoUtils.ts +198 -11
  67. package/src/server-articles.ts +199 -6
  68. package/src/server.ts +46 -2
  69. package/src/validateArticles.ts +260 -0
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentType } from 'react';
2
+ import { ComponentType, ReactNode } from 'react';
3
3
 
4
4
  interface TocItem {
5
5
  id: string;
@@ -22,9 +22,12 @@ interface Article {
22
22
  lastmod?: string;
23
23
  author: string;
24
24
  authors?: string[];
25
+ authorSlug?: string;
26
+ authorAvatar?: string;
25
27
  category: string;
26
28
  categories: string[];
27
29
  readTime: string;
30
+ wordCount?: number;
28
31
  featuredImage: string;
29
32
  tags?: string[];
30
33
  content?: string;
@@ -38,7 +41,65 @@ interface Article {
38
41
  canonicalUrl?: string;
39
42
  articleType?: string;
40
43
  series?: string;
44
+ /**
45
+ * Machine-safe series identifier (Phase 27F) - separate from the
46
+ * label-only `series` string, which stays supported unchanged for
47
+ * consumers who only set it. `seriesSlug`/`seriesOrder` turn `series` into
48
+ * a navigable reader journey via `getArticlesBySeries`/
49
+ * `getAdjacentArticlesInSeries`. Both optional; omitted on every article
50
+ * reproduces pre-27F behavior exactly.
51
+ */
52
+ seriesSlug?: string;
53
+ /** Position within `seriesSlug`, ascending. Ties/omissions fall back to date order (see `getArticlesBySeries`). */
54
+ seriesOrder?: number;
41
55
  aiCrawl?: boolean;
56
+ /**
57
+ * Discovery metadata overrides (Phase 27F), all optional and additive.
58
+ * `searchTitle`/`searchDescription` feed `generateArticleMetadata`'s
59
+ * `<title>`/meta description ONLY - canonical URLs, JSON-LD, RSS, and
60
+ * `ArticleCard` keep reading `title`/`excerpt` unchanged. `socialTitle`/
61
+ * `socialDescription`/`socialImage` feed Open Graph/Twitter Card output
62
+ * ONLY, falling back to `title`/`excerpt`/`featuredImage`. See
63
+ * `resolveSearchMetadata`/`resolveSocialMetadata` in `seoUtils.ts` for the
64
+ * exact fallback/sanitization rules.
65
+ */
66
+ searchTitle?: string;
67
+ searchDescription?: string;
68
+ socialTitle?: string;
69
+ socialDescription?: string;
70
+ socialImage?: string;
71
+ /**
72
+ * References an app-owned CTA/offer by opaque ID (Phase 27F). The package
73
+ * never interprets `actionId` - it doesn't know about forms, email
74
+ * providers, or analytics vendors. The consuming app looks `actionId` up
75
+ * in its own registry when rendering a detail-page slot (see
76
+ * `ArticleContent`'s `afterHero`/`afterIntro`/`midContent`/`afterContent`
77
+ * props); an unmatched ID must render nothing, never throw.
78
+ */
79
+ primaryAction?: {
80
+ actionId: string;
81
+ };
82
+ }
83
+ /**
84
+ * A "start here" curated reader journey that can cross series/categories -
85
+ * a distinct primitive from the label-only `series` field/`seriesSlug`
86
+ * pair (Phase 27F). Configured via `ArticlesConfig.paths`, keyed by an
87
+ * app-chosen path key. `articles` is an ordered list of slugs; every
88
+ * referenced slug must exist and not be `draft: true` - enforced by
89
+ * `validateArticles`, not silently at render time.
90
+ */
91
+ interface PathDefinition {
92
+ /** Display name, e.g. "New GM Starter Path". */
93
+ name: string;
94
+ /** One-sentence value proposition shown on the path's landing/step UI. */
95
+ promise: string;
96
+ /** Ordered article slugs making up the journey. */
97
+ articles: string[];
98
+ /** The one next action offered once a reader completes the path. */
99
+ nextAction: {
100
+ label: string;
101
+ href: string;
102
+ };
42
103
  }
43
104
  interface CategoryInfo {
44
105
  name: string;
@@ -63,6 +124,23 @@ interface AuthorSocial {
63
124
  newsletter?: string;
64
125
  other?: Record<string, string>;
65
126
  }
127
+ /**
128
+ * One headed block of structured long-form content (used by
129
+ * `AuthorProfile.originStory`). An array of these, not a single HTML blob,
130
+ * so consuming apps can render/style each block themselves rather than
131
+ * `dangerouslySetInnerHTML`-ing raw markup.
132
+ */
133
+ interface RichTextSection {
134
+ heading?: string;
135
+ paragraphs: string[];
136
+ }
137
+ type RichText = RichTextSection[];
138
+ /** A single sourceable claim used by `AuthorProfile.proof`. */
139
+ interface ProofItem {
140
+ claim: string;
141
+ source?: string;
142
+ url?: string;
143
+ }
66
144
  interface AuthorProfile {
67
145
  name: string;
68
146
  slug: string;
@@ -70,12 +148,81 @@ interface AuthorProfile {
70
148
  avatar?: string;
71
149
  url?: string;
72
150
  social?: AuthorSocial;
151
+ /** Short one-line audience promise, e.g. "Helping new GMs run confident first sessions." Optional, additive - omitted fields never change existing rendering. */
152
+ promise?: string;
153
+ /** Structured long-form origin story - see `RichText`/`RichTextSection`. */
154
+ originStory?: RichText;
155
+ /** Who this author's content/work is for, e.g. "New game masters", "Streaming DMs". */
156
+ servesWho?: string[];
157
+ /** Core beliefs/approach statements. */
158
+ principles?: string[];
159
+ /**
160
+ * Experience/credential claims (e.g. "10+ years running published campaigns").
161
+ * Intentionally never included in Person JSON-LD - unverifiable claims don't
162
+ * belong in structured data (see `getPersonSchema`/`getPersonSchemas`).
163
+ */
164
+ credentials?: string[];
165
+ /** Concrete, sourceable proof points. */
166
+ proof?: ProofItem[];
167
+ /** Primary call-to-action rendered on the author's page. */
168
+ primaryCta?: {
169
+ label: string;
170
+ href: string;
171
+ };
73
172
  }
74
173
  interface BreadcrumbItem {
75
174
  name: string;
76
175
  url?: string;
77
176
  }
78
177
 
178
+ type ArticleEventName = 'article_viewed' | 'meaningful_read' | 'author_clicked' | 'cta_viewed' | 'cta_clicked' | 'shared' | 'related_article_clicked' | 'path_step_advanced';
179
+ interface ArticleEventBase<Name extends ArticleEventName> {
180
+ name: Name;
181
+ /** `Date.now()` at emit time. */
182
+ timestamp: number;
183
+ }
184
+ interface ArticleViewedEvent extends ArticleEventBase<'article_viewed'> {
185
+ articleSlug: string;
186
+ category?: string;
187
+ seriesSlug?: string;
188
+ }
189
+ /** Fired once per view after the reader has spent roughly half the article's estimated read time on the page (see `ArticleViewTracker`). */
190
+ interface MeaningfulReadEvent extends ArticleEventBase<'meaningful_read'> {
191
+ articleSlug: string;
192
+ }
193
+ interface AuthorClickedEvent extends ArticleEventBase<'author_clicked'> {
194
+ articleSlug: string;
195
+ authorSlug: string;
196
+ }
197
+ /** `ctaId` is `primaryAction.actionId`, an `AuthorProfile.primaryCta` slug, or a `PathDefinition` key - always an app-chosen ID, never label text. */
198
+ interface CtaViewedEvent extends ArticleEventBase<'cta_viewed'> {
199
+ ctaId: string;
200
+ articleSlug?: string;
201
+ }
202
+ interface CtaClickedEvent extends ArticleEventBase<'cta_clicked'> {
203
+ ctaId: string;
204
+ articleSlug?: string;
205
+ }
206
+ interface SharedEvent extends ArticleEventBase<'shared'> {
207
+ articleSlug: string;
208
+ /** Share channel key, e.g. `'linkedin'`, `'copy-link'` - never the shared URL/message text. */
209
+ channel: string;
210
+ }
211
+ interface RelatedArticleClickedEvent extends ArticleEventBase<'related_article_clicked'> {
212
+ fromSlug: string;
213
+ toSlug: string;
214
+ source: 'path' | 'series' | 'category';
215
+ }
216
+ interface PathStepAdvancedEvent extends ArticleEventBase<'path_step_advanced'> {
217
+ pathKey: string;
218
+ fromSlug: string;
219
+ toSlug: string;
220
+ direction: 'previous' | 'next';
221
+ }
222
+ type ArticleEvent = ArticleViewedEvent | MeaningfulReadEvent | AuthorClickedEvent | CtaViewedEvent | CtaClickedEvent | SharedEvent | RelatedArticleClickedEvent | PathStepAdvancedEvent;
223
+ /** Register this on `ArticlesConfig.onEvent` to receive every emitted event and translate it to your own analytics stack. */
224
+ type ArticleEventHandler = (event: ArticleEvent) => void;
225
+
79
226
  /** Keys for each renderable section of the articles listing page. */
80
227
  type ArticlesSection = 'hero' | 'search' | 'featured' | 'latest' | 'categories' | 'newsletter';
81
228
  /**
@@ -85,6 +232,14 @@ type ArticlesSection = 'hero' | 'search' | 'featured' | 'latest' | 'categories'
85
232
  interface ArticlesTheme {
86
233
  /** Font family for the articles section. Example: `"'Inter', sans-serif"` */
87
234
  fontFamily?: string;
235
+ /**
236
+ * Font family for headings only (article title, card titles, section
237
+ * headings) - falls back to `fontFamily` when omitted. Lets a site use a
238
+ * distinct display face for headings (e.g. a serif) while keeping a
239
+ * separate body font, without hardcoding either into the package.
240
+ * Example: `"'Cinzel', serif"`
241
+ */
242
+ headerFontFamily?: string;
88
243
  /** Color for article card titles and section headings. Example: `'#111827'` */
89
244
  headerColor?: string;
90
245
  /** Color for body and excerpt text. Example: `'#6b7280'` */
@@ -136,6 +291,18 @@ interface HeroConfig {
136
291
  }
137
292
  /** Controls how article body links set target/rel attributes. */
138
293
  type LinkTargetStrategy = 'external-new-tab' | 'all-new-tab' | 'same-tab';
294
+ /**
295
+ * Controls how listing pages (the articles index, category pages, author
296
+ * pages) surface articles beyond the first `pageSize`.
297
+ * - `'load-more'` (default): client-only "Load more" button, no URL change.
298
+ * Byte-for-byte identical to pre-27D behavior.
299
+ * - `'pages'`: real, directly-navigable paginated routes (`/articles/page/2`,
300
+ * `/articles/category/[category]/page/2`, `/articles/authors/[author]/page/2`)
301
+ * with SSR content, prev/next links, and per-page canonical metadata. The
302
+ * route *files* live in the consuming app - see the pagination primitives
303
+ * exported from `./server` and the `PaginationNav` component.
304
+ */
305
+ type ListingPagination = 'load-more' | 'pages';
139
306
  /** React components that article MDX bodies can reference by JSX tag name. */
140
307
  type MdxComponents = Record<string, ComponentType<never>>;
141
308
  type ArticleBreadcrumbToken = 'home' | 'articles' | 'primaryCategory' | 'folderPath' | 'articleTitle';
@@ -218,6 +385,28 @@ interface ArticlesConfig {
218
385
  linkTargetStrategy?: LinkTargetStrategy;
219
386
  /** Extra components exposed to article MDX bodies by JSX tag name. */
220
387
  mdxComponents?: MdxComponents;
388
+ /**
389
+ * Chooses how listing pages surface articles beyond the first `pageSize`.
390
+ * Default: `'load-more'` (unchanged pre-27D behavior). Set to `'pages'` to
391
+ * opt into real, crawlable paginated routes instead.
392
+ */
393
+ listingPagination?: ListingPagination;
394
+ /**
395
+ * "Start here" curated reader journeys, keyed by an app-chosen path key.
396
+ * Distinct from the label-only `series` field/`seriesSlug` pair - a path
397
+ * can cross series and categories. Every `PathDefinition.articles` slug
398
+ * must exist and not be `draft: true`; validate with `validateArticles`
399
+ * before publishing, since a broken reference produces a dead journey
400
+ * step rather than a build-time failure otherwise.
401
+ */
402
+ paths?: Record<string, PathDefinition>;
403
+ /**
404
+ * Vendor-neutral event callback (Phase 27F). Fired by components/hooks at
405
+ * meaningful reader-journey moments (see `ArticleEvent` in `events.ts`).
406
+ * No PII in any payload. The package never talks to an analytics/email
407
+ * vendor directly - translate events to PostHog/etc. in this callback.
408
+ */
409
+ onEvent?: ArticleEventHandler;
221
410
  }
222
411
  declare const DEFAULT_PAGE_SIZE = 6;
223
412
  declare const DEFAULT_CATEGORIES_PAGE_SIZE = 8;
@@ -225,16 +414,34 @@ declare const DEFAULT_LAYOUT: ArticlesSection[];
225
414
  declare function breadcrumbsAreEnabled(config: ArticlesConfig): boolean;
226
415
  declare function getBreadcrumbsConfig(config: ArticlesConfig): BreadcrumbsConfig;
227
416
 
228
- declare function ArticlesPage({ config, initialArticles, initialCategories, }: Readonly<{
417
+ declare function ArticlesPage({ config, initialArticles, initialCategories, page, totalPages, totalCount, }: Readonly<{
229
418
  config: ArticlesConfig;
230
419
  initialArticles?: Article[];
231
420
  initialCategories?: CategoryInfo[];
421
+ /**
422
+ * Current page number in `listingPagination: 'pages'` mode. Ignored
423
+ * (along with `totalPages`/`totalCount`) unless `config.listingPagination
424
+ * === 'pages'` - the default `'load-more'` behavior never reads these.
425
+ * `initialArticles` should already be this page's slice (see
426
+ * `paginateArticles` from `./server`).
427
+ */
428
+ page?: number;
429
+ /** Total page count in `'pages'` mode, from `getTotalPages`. */
430
+ totalPages?: number;
431
+ /**
432
+ * True total article count across every page (not just `initialArticles`'
433
+ * length) - used for `CollectionPageSchema.articleCount`. Defaults to
434
+ * `state.articles.length`, matching prior behavior when omitted.
435
+ */
436
+ totalCount?: number;
232
437
  }>): react_jsx_runtime.JSX.Element;
233
438
 
234
439
  type ArticleCardProps = Readonly<{
235
440
  article: Article;
441
+ /** Optional (Phase 27F) - when passed with `config.onEvent`, clicking the author link fires an `author_clicked` event. Omitting it changes nothing. */
442
+ config?: ArticlesConfig;
236
443
  }>;
237
- declare function ArticleCard({ article }: ArticleCardProps): react_jsx_runtime.JSX.Element;
444
+ declare function ArticleCard({ article, config }: ArticleCardProps): react_jsx_runtime.JSX.Element;
238
445
 
239
446
  type ArticleCategoryGridProps = Readonly<{
240
447
  categories: CategoryInfo[];
@@ -254,8 +461,10 @@ type ArticleDetailHeroProps = Readonly<{
254
461
  showDate?: boolean;
255
462
  showAuthor?: boolean;
256
463
  authors?: AuthorProfile[];
464
+ /** Optional - when `config.theme.headerFontFamily` is set, applies it to the title. */
465
+ config?: ArticlesConfig;
257
466
  }>;
258
- declare function ArticleDetailHero({ article, categoryBasePath, showDate, showAuthor, authors, }: ArticleDetailHeroProps): react_jsx_runtime.JSX.Element;
467
+ declare function ArticleDetailHero({ article, categoryBasePath, showDate, showAuthor, authors, config, }: ArticleDetailHeroProps): react_jsx_runtime.JSX.Element;
259
468
 
260
469
  type ArticleSearchBarProps = Readonly<{
261
470
  value: string;
@@ -271,33 +480,107 @@ type FeaturedArticleProps = Readonly<{
271
480
  }>;
272
481
  declare function FeaturedArticle({ article, showAuthor }: FeaturedArticleProps): react_jsx_runtime.JSX.Element;
273
482
 
483
+ /** Context threaded from a listing page component down into `LatestArticles`/`PaginationNav` in `'pages'` mode. */
484
+ interface ListingPaginationContext {
485
+ page: number;
486
+ totalPages: number;
487
+ /** Un-paginated route path for this listing, e.g. `/articles` or `/articles/category/campaigns`. */
488
+ basePath: string;
489
+ }
490
+
274
491
  interface LatestArticlesProps {
275
492
  readonly articles: Article[];
276
493
  readonly pageSize: number;
494
+ /**
495
+ * Switches from the default client-only "Load more" button to real
496
+ * paginated routes: renders every article in `articles` as-is (the caller
497
+ * has already sliced to the current page, e.g. via `paginateArticles`) and
498
+ * a `PaginationNav` instead of the button. Omit to keep the existing
499
+ * `listingPagination: 'load-more'` behavior, unchanged.
500
+ */
501
+ readonly pagination?: ListingPaginationContext;
277
502
  }
278
- declare function LatestArticles({ articles, pageSize }: Readonly<LatestArticlesProps>): react_jsx_runtime.JSX.Element;
503
+ declare function LatestArticles({ articles, pageSize, pagination }: Readonly<LatestArticlesProps>): react_jsx_runtime.JSX.Element;
279
504
 
280
505
  interface LatestArticlesSectionProps {
281
506
  readonly articles: Article[];
282
507
  readonly searchQuery: string;
283
508
  readonly onClearSearch: () => void;
284
509
  readonly pageSize?: number;
510
+ /**
511
+ * `'pages'` mode pagination context. Ignored while `searchQuery` is set -
512
+ * search results stay unpaginated/client-only in both `listingPagination`
513
+ * modes (search URLs aren't meant to be indexed, so real pagination adds
514
+ * no SEO value there).
515
+ */
516
+ readonly pagination?: ListingPaginationContext;
285
517
  }
286
- declare function LatestArticlesSection({ articles, searchQuery, onClearSearch, pageSize, }: Readonly<LatestArticlesSectionProps>): react_jsx_runtime.JSX.Element;
518
+ declare function LatestArticlesSection({ articles, searchQuery, onClearSearch, pageSize, pagination, }: Readonly<LatestArticlesSectionProps>): react_jsx_runtime.JSX.Element;
287
519
 
288
520
  type CategoryArticlesPageProps = Readonly<{
289
521
  category: string;
290
522
  articles: Article[];
291
523
  config: ArticlesConfig;
524
+ /** Current page number in `listingPagination: 'pages'` mode. Ignored (with `totalPages`/`totalCount`) unless `config.listingPagination === 'pages'`. `articles` should already be this page's slice. */
525
+ page?: number;
526
+ /** Total page count in `'pages'` mode, from `getTotalPages`. */
527
+ totalPages?: number;
528
+ /** True total article count across every page. Defaults to `articles.length`. */
529
+ totalCount?: number;
292
530
  }>;
293
- declare function CategoryArticlesPage({ category, articles, config }: CategoryArticlesPageProps): react_jsx_runtime.JSX.Element | null;
531
+ declare function CategoryArticlesPage({ category, articles, config, page, totalPages, totalCount, }: CategoryArticlesPageProps): react_jsx_runtime.JSX.Element | null;
532
+
533
+ type SeriesArticlesPageProps = Readonly<{
534
+ /** Machine-safe series identifier - matches `Article.seriesSlug`. */
535
+ seriesSlug: string;
536
+ /** Already resolved via `getArticlesBySeries` (seriesOrder-sorted). */
537
+ articles: Article[];
538
+ config: ArticlesConfig;
539
+ }>;
540
+ /**
541
+ * Lightweight series landing page (Phase 27F) - a thinner sibling of
542
+ * `CategoryArticlesPage` for a `seriesSlug`'s ordered reader journey rather
543
+ * than a category's chronological listing. Displays series position badges
544
+ * (1-indexed, following `getArticlesBySeries`' order) so a reader can see
545
+ * where in the journey each article sits; doesn't attempt real ('pages'
546
+ * mode) pagination - series are expected to be short, curated lists rather
547
+ * than open-ended listings, so `LatestArticles` is used purely for its
548
+ * card grid, sized to show every article without a "Load more" step.
549
+ */
550
+ declare function SeriesArticlesPage({ seriesSlug, articles, config }: SeriesArticlesPageProps): react_jsx_runtime.JSX.Element | null;
294
551
 
552
+ /**
553
+ * Ordered section keys for the composable author-page render API (Phase
554
+ * 27E). `'custom'` is the one consumer-supplied slot - pass its content via
555
+ * the `customSection` prop. Passing `sections` is fully opt-in: omitting it
556
+ * keeps `AuthorArticlesPage`'s original output (Person/CollectionPage JSON-LD
557
+ * + article list only, no hero) byte-for-byte unchanged.
558
+ */
559
+ type AuthorPageSection = 'hero' | 'promise' | 'servesWho' | 'originStory' | 'principles' | 'proof' | 'cta' | 'articles' | 'custom';
295
560
  type AuthorArticlesPageProps = Readonly<{
296
561
  author: AuthorProfile;
297
562
  articles: Article[];
298
563
  config: ArticlesConfig;
564
+ /** Current page number in `listingPagination: 'pages'` mode. Ignored (with `totalPages`/`totalCount`) unless `config.listingPagination === 'pages'`. `articles` should already be this page's slice. */
565
+ page?: number;
566
+ /** Total page count in `'pages'` mode, from `getTotalPages`. */
567
+ totalPages?: number;
568
+ /** True total article count across every page. Defaults to `articles.length` (also used as the `Person` schema's `interactionStatistic` count). */
569
+ totalCount?: number;
570
+ /**
571
+ * Ordered list of sections to render (Phase 27E composable render API).
572
+ * When omitted, `AuthorArticlesPage` renders exactly as it did before this
573
+ * prop existed - the article list only, no hero and no rich-profile
574
+ * sections, even if `author` has the new optional fields populated.
575
+ * Include `'hero'` to render `AuthorDetailHero` here instead of composing
576
+ * it separately; each other section renders nothing when its backing
577
+ * field (`author.promise`, `author.servesWho`, etc.) is unset.
578
+ */
579
+ sections?: AuthorPageSection[];
580
+ /** Content for the one `'custom'` slot in `sections`. Ignored unless `sections` includes `'custom'`. */
581
+ customSection?: ReactNode;
299
582
  }>;
300
- declare function AuthorArticlesPage({ author, articles, config }: AuthorArticlesPageProps): react_jsx_runtime.JSX.Element;
583
+ declare function AuthorArticlesPage({ author, articles, config, page, totalPages, totalCount, sections, customSection, }: AuthorArticlesPageProps): react_jsx_runtime.JSX.Element;
301
584
 
302
585
  type AuthorCardProps = Readonly<{
303
586
  author: AuthorProfile;
@@ -327,14 +610,6 @@ declare function BreadcrumbSchema({ items }: Readonly<{
327
610
  items: BreadcrumbItem[];
328
611
  }>): react_jsx_runtime.JSX.Element;
329
612
 
330
- type ArticleSchemaProps = Readonly<{
331
- article: Article;
332
- articleUrl: string;
333
- siteName: string;
334
- showAuthor?: boolean;
335
- authors?: AuthorProfile[];
336
- }>;
337
- declare function ArticleSchema({ article, articleUrl, siteName, showAuthor, authors, }: ArticleSchemaProps): react_jsx_runtime.JSX.Element;
338
613
  type ArticleSEOProps = Readonly<{
339
614
  article: Article;
340
615
  articleUrl: string;
@@ -342,8 +617,9 @@ type ArticleSEOProps = Readonly<{
342
617
  siteLogo?: string;
343
618
  showAuthor?: boolean;
344
619
  authors?: AuthorProfile[];
620
+ config?: ArticlesConfig;
345
621
  }>;
346
- declare function ArticleSEO({ article, articleUrl, siteName, siteLogo, showAuthor, authors, }: ArticleSEOProps): react_jsx_runtime.JSX.Element;
622
+ declare function ArticleSEO({ article, articleUrl, siteName, siteLogo, showAuthor, authors, config, }: ArticleSEOProps): react_jsx_runtime.JSX.Element;
347
623
  type FAQPageSchemaProps = Readonly<{
348
624
  items: ReadonlyArray<{
349
625
  question: string;
@@ -351,21 +627,31 @@ type FAQPageSchemaProps = Readonly<{
351
627
  }>;
352
628
  }>;
353
629
  declare function FAQPageSchema({ items }: FAQPageSchemaProps): react_jsx_runtime.JSX.Element;
630
+ type CollectionPageItem = Readonly<{
631
+ position: number;
632
+ url: string;
633
+ name: string;
634
+ }>;
354
635
  type CollectionPageSchemaProps = Readonly<{
355
636
  title: string;
356
637
  description: string;
357
638
  url: string;
358
639
  articleCount: number;
640
+ items?: CollectionPageItem[];
359
641
  }>;
360
- declare function CollectionPageSchema({ title, description, url, articleCount, }: CollectionPageSchemaProps): react_jsx_runtime.JSX.Element;
642
+ declare function CollectionPageSchema({ title, description, url, articleCount, items, }: CollectionPageSchemaProps): react_jsx_runtime.JSX.Element;
361
643
 
362
644
  interface ArticleSocialShareProps {
363
645
  readonly title: string;
364
646
  readonly url: string;
365
647
  readonly excerpt?: string;
366
648
  readonly shareMessage?: string;
649
+ /** Optional (Phase 27F). Enables `shared` events - requires `articleSlug` too. */
650
+ readonly config?: ArticlesConfig;
651
+ /** The article slug being shared. Required to emit `shared`. */
652
+ readonly articleSlug?: string;
367
653
  }
368
- declare function ArticleSocialShare({ title, url, excerpt, shareMessage }: ArticleSocialShareProps): react_jsx_runtime.JSX.Element;
654
+ declare function ArticleSocialShare({ title, url, excerpt, shareMessage, config, articleSlug, }: ArticleSocialShareProps): react_jsx_runtime.JSX.Element;
369
655
 
370
656
  interface ArticleNavigationProps {
371
657
  readonly previous: {
@@ -377,8 +663,33 @@ interface ArticleNavigationProps {
377
663
  title: string;
378
664
  } | null;
379
665
  readonly basePath: string;
666
+ /** The article slug this navigation is shown on. Combined with `pathKey`, enables `path_step_advanced` events. */
667
+ readonly fromSlug?: string;
668
+ /**
669
+ * Set when this navigation walks a configured `Path` (as opposed to plain
670
+ * chronological/series adjacency) - fires `path_step_advanced` on click.
671
+ * Omit for ordinary previous/next navigation; no event fires without it.
672
+ */
673
+ readonly pathKey?: string;
674
+ readonly config?: ArticlesConfig;
380
675
  }
381
- declare function ArticleNavigation({ previous, next, basePath }: ArticleNavigationProps): react_jsx_runtime.JSX.Element | null;
676
+ declare function ArticleNavigation({ previous, next, basePath, fromSlug, pathKey, config, }: ArticleNavigationProps): react_jsx_runtime.JSX.Element | null;
677
+
678
+ type RelatedContentSource = 'path' | 'series' | 'category';
679
+
680
+ type RelatedArticlesSectionProps = Readonly<{
681
+ articles: Article[];
682
+ category: string;
683
+ /** Overrides the default "More in {category}" heading - used by `getRelatedContent` callers when `source` is `'path'`/`'series'`. Omit to keep the default category-based heading. */
684
+ heading?: string;
685
+ /** Optional (Phase 27F). Enables `related_article_clicked` events - requires `fromSlug` too. */
686
+ config?: ArticlesConfig;
687
+ /** The article slug this related section is shown on. Required to emit `related_article_clicked`. */
688
+ fromSlug?: string;
689
+ /** Selection source that produced `articles`, from `getRelatedContent`. Defaults to `'category'`. */
690
+ source?: RelatedContentSource;
691
+ }>;
692
+ declare function RelatedArticlesSection({ articles, category, heading, config, fromSlug, source, }: RelatedArticlesSectionProps): react_jsx_runtime.JSX.Element | null;
382
693
 
383
694
  type ArticleBackLinkProps = Readonly<{
384
695
  config: Pick<ArticlesConfig, 'showBackToArticles'>;
@@ -396,6 +707,57 @@ declare function ArticleTOC({ toc, className }: ArticleTOCProps): react_jsx_runt
396
707
 
397
708
  declare function ScrollToTop(): react_jsx_runtime.JSX.Element | null;
398
709
 
710
+ interface PaginationNavProps {
711
+ readonly basePath: string;
712
+ readonly page: number;
713
+ readonly totalPages: number;
714
+ }
715
+ /**
716
+ * Renders real `<a href>` prev/next links between listing pages (used only
717
+ * in `listingPagination: 'pages'` mode), plus `<link rel="prev"/"next">`
718
+ * tags for crawlers/tools that still read them.
719
+ *
720
+ * Google stopped using rel=next/prev as an indexing signal in 2019 (Google
721
+ * Search Central's pagination guidance now recommends a unique
722
+ * self-referencing canonical per page - see `generateArticlesIndexPageMetadata`
723
+ * et al in seoUtils.ts - plus real crawlable links between pages, which is
724
+ * what this component's visible Previous/Next links provide). rel=next/prev
725
+ * remains valid HTML and is still read by Bing and some third-party tools,
726
+ * so it's emitted here for free: React 19 hoists `<link>`/`<meta>` elements
727
+ * rendered anywhere in a Server Component tree into `<head>` automatically
728
+ * (not just from layout.js/page.js directly), so no separate Head API call
729
+ * is needed.
730
+ */
731
+ declare function PaginationNav({ basePath, page, totalPages }: PaginationNavProps): react_jsx_runtime.JSX.Element | null;
732
+
733
+ type ArticleViewTrackerProps = Readonly<{
734
+ article: Pick<Article, 'slug'> & Partial<Pick<Article, 'category' | 'seriesSlug' | 'wordCount'>>;
735
+ config?: ArticlesConfig;
736
+ }>;
737
+ /**
738
+ * Renders nothing - fires `article_viewed` on mount and `meaningful_read`
739
+ * once, after roughly half the article's estimated read time has elapsed
740
+ * (a deterministic, testable timing approximation rather than scroll-depth
741
+ * tracking, which this package has no reliable cross-app way to measure
742
+ * since it doesn't own the article body's scroll container). Place once on
743
+ * the article detail page alongside `ArticleContent`.
744
+ */
745
+ declare function ArticleViewTracker({ article, config }: ArticleViewTrackerProps): null;
746
+ type CtaViewTrackerProps = Readonly<{
747
+ /** Opaque CTA/offer ID - never label text (no PII/marketing copy in event payloads). */
748
+ ctaId: string;
749
+ articleSlug?: string;
750
+ config?: ArticlesConfig;
751
+ children: ReactNode;
752
+ }>;
753
+ /**
754
+ * Wraps any CTA block and fires `cta_viewed` once, the first time at least
755
+ * half of it scrolls into the viewport (`IntersectionObserver`). Falls back
756
+ * to firing immediately when `IntersectionObserver` isn't available (older
757
+ * browsers, non-DOM test environments) rather than never firing.
758
+ */
759
+ declare function CtaViewTracker({ ctaId, articleSlug, config, children }: CtaViewTrackerProps): react_jsx_runtime.JSX.Element;
760
+
399
761
  interface CommentsSectionProps {
400
762
  readonly articleSlug: string;
401
763
  readonly config: CommentsConfig;
@@ -451,4 +813,4 @@ interface UseArticlesReturn {
451
813
  }
452
814
  declare function useArticles(initialArticles?: Article[], initialCategories?: CategoryInfo[]): UseArticlesReturn;
453
815
 
454
- export { type Article, ArticleBackLink, type ArticleBreadcrumbEntry, type ArticleBreadcrumbToken, ArticleCard, ArticleCategoryGrid, type ArticleComment, type ArticleCommentWithReplies, ArticleDetailHero, ArticleNavigation, ArticleSEO, ArticleSchema, ArticleSearchBar, ArticleSocialShare, ArticleTOC, type ArticlesConfig, ArticlesHero, ArticlesPage, type ArticlesSection, type ArticlesTheme, AuthorArticlesPage, type AuthorBreadcrumbEntry, type AuthorBreadcrumbToken, AuthorCard, AuthorDetailHero, type AuthorProfile, type AuthorSocial, AuthorSocialLinks, Breadcrumb, type BreadcrumbItem, type BreadcrumbLabels, BreadcrumbSchema, type BreadcrumbsConfig, CategoryArticlesPage, type CategoryBreadcrumbEntry, type CategoryBreadcrumbToken, type CategoryDescription, type CategoryInfo, CollectionPageSchema, CommentForm, CommentItem, CommentThread, type CommentsConfig, CommentsSection, type CustomBreadcrumbItem, DEFAULT_CATEGORIES_PAGE_SIZE, DEFAULT_LAYOUT, DEFAULT_PAGE_SIZE, FAQPageSchema, type FaqItem, FeaturedArticle, type HowToStep, LatestArticles, LatestArticlesSection, type LinkTargetStrategy, type MdxComponents, ScrollToTop, type TocItem, type UseArticlesReturn, breadcrumbsAreEnabled, getBreadcrumbsConfig, useArticles };
816
+ export { type Article, ArticleBackLink, type ArticleBreadcrumbEntry, type ArticleBreadcrumbToken, ArticleCard, ArticleCategoryGrid, type ArticleComment, type ArticleCommentWithReplies, ArticleDetailHero, type ArticleEvent, type ArticleEventHandler, type ArticleEventName, ArticleNavigation, ArticleSEO, ArticleSearchBar, ArticleSocialShare, ArticleTOC, ArticleViewTracker, type ArticleViewedEvent, type ArticlesConfig, ArticlesHero, ArticlesPage, type ArticlesSection, type ArticlesTheme, AuthorArticlesPage, type AuthorBreadcrumbEntry, type AuthorBreadcrumbToken, AuthorCard, type AuthorClickedEvent, AuthorDetailHero, type AuthorPageSection, type AuthorProfile, type AuthorSocial, AuthorSocialLinks, Breadcrumb, type BreadcrumbItem, type BreadcrumbLabels, BreadcrumbSchema, type BreadcrumbsConfig, CategoryArticlesPage, type CategoryBreadcrumbEntry, type CategoryBreadcrumbToken, type CategoryDescription, type CategoryInfo, type CollectionPageItem, CollectionPageSchema, CommentForm, CommentItem, CommentThread, type CommentsConfig, CommentsSection, type CtaClickedEvent, CtaViewTracker, type CtaViewedEvent, type CustomBreadcrumbItem, DEFAULT_CATEGORIES_PAGE_SIZE, DEFAULT_LAYOUT, DEFAULT_PAGE_SIZE, FAQPageSchema, type FaqItem, FeaturedArticle, type HowToStep, LatestArticles, LatestArticlesSection, type LinkTargetStrategy, type ListingPagination, type ListingPaginationContext, type MdxComponents, type MeaningfulReadEvent, PaginationNav, type PathDefinition, type PathStepAdvancedEvent, type ProofItem, type RelatedArticleClickedEvent, RelatedArticlesSection, type RichText, type RichTextSection, ScrollToTop, SeriesArticlesPage, type SharedEvent, type TocItem, type UseArticlesReturn, breadcrumbsAreEnabled, getBreadcrumbsConfig, useArticles };