@fullstackdatasolutions/articles 0.11.0 → 1.0.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 (71) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +568 -6
  3. package/dist/index.cjs +970 -389
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +375 -21
  6. package/dist/index.d.ts +375 -21
  7. package/dist/index.js +954 -377
  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 +141 -0
  12. package/dist/nextjs.d.ts +141 -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 +349 -3
  18. package/dist/server.d.ts +349 -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 +37 -1
  23. package/src/ArticleContent.tsx +144 -5
  24. package/src/ArticleDetailHero.tsx +23 -0
  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 +65 -7
  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 +55 -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 +148 -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__/useArticles.test.ts +28 -0
  59. package/src/__tests__/validateArticles.test.ts +312 -0
  60. package/src/articleTypes.ts +109 -0
  61. package/src/articlesConfig.ts +37 -1
  62. package/src/eventTracking.tsx +97 -0
  63. package/src/events.ts +105 -0
  64. package/src/index.ts +26 -1
  65. package/src/markdown.ts +41 -0
  66. package/src/pagination.ts +93 -0
  67. package/src/seoUtils.ts +198 -11
  68. package/src/server-articles.ts +199 -6
  69. package/src/server.ts +46 -2
  70. package/src/useArticles.ts +10 -5
  71. package/src/validateArticles.ts +260 -0
package/dist/index.d.ts 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
  /**
@@ -136,6 +283,18 @@ interface HeroConfig {
136
283
  }
137
284
  /** Controls how article body links set target/rel attributes. */
138
285
  type LinkTargetStrategy = 'external-new-tab' | 'all-new-tab' | 'same-tab';
286
+ /**
287
+ * Controls how listing pages (the articles index, category pages, author
288
+ * pages) surface articles beyond the first `pageSize`.
289
+ * - `'load-more'` (default): client-only "Load more" button, no URL change.
290
+ * Byte-for-byte identical to pre-27D behavior.
291
+ * - `'pages'`: real, directly-navigable paginated routes (`/articles/page/2`,
292
+ * `/articles/category/[category]/page/2`, `/articles/authors/[author]/page/2`)
293
+ * with SSR content, prev/next links, and per-page canonical metadata. The
294
+ * route *files* live in the consuming app - see the pagination primitives
295
+ * exported from `./server` and the `PaginationNav` component.
296
+ */
297
+ type ListingPagination = 'load-more' | 'pages';
139
298
  /** React components that article MDX bodies can reference by JSX tag name. */
140
299
  type MdxComponents = Record<string, ComponentType<never>>;
141
300
  type ArticleBreadcrumbToken = 'home' | 'articles' | 'primaryCategory' | 'folderPath' | 'articleTitle';
@@ -218,6 +377,28 @@ interface ArticlesConfig {
218
377
  linkTargetStrategy?: LinkTargetStrategy;
219
378
  /** Extra components exposed to article MDX bodies by JSX tag name. */
220
379
  mdxComponents?: MdxComponents;
380
+ /**
381
+ * Chooses how listing pages surface articles beyond the first `pageSize`.
382
+ * Default: `'load-more'` (unchanged pre-27D behavior). Set to `'pages'` to
383
+ * opt into real, crawlable paginated routes instead.
384
+ */
385
+ listingPagination?: ListingPagination;
386
+ /**
387
+ * "Start here" curated reader journeys, keyed by an app-chosen path key.
388
+ * Distinct from the label-only `series` field/`seriesSlug` pair - a path
389
+ * can cross series and categories. Every `PathDefinition.articles` slug
390
+ * must exist and not be `draft: true`; validate with `validateArticles`
391
+ * before publishing, since a broken reference produces a dead journey
392
+ * step rather than a build-time failure otherwise.
393
+ */
394
+ paths?: Record<string, PathDefinition>;
395
+ /**
396
+ * Vendor-neutral event callback (Phase 27F). Fired by components/hooks at
397
+ * meaningful reader-journey moments (see `ArticleEvent` in `events.ts`).
398
+ * No PII in any payload. The package never talks to an analytics/email
399
+ * vendor directly - translate events to PostHog/etc. in this callback.
400
+ */
401
+ onEvent?: ArticleEventHandler;
221
402
  }
222
403
  declare const DEFAULT_PAGE_SIZE = 6;
223
404
  declare const DEFAULT_CATEGORIES_PAGE_SIZE = 8;
@@ -225,14 +406,34 @@ declare const DEFAULT_LAYOUT: ArticlesSection[];
225
406
  declare function breadcrumbsAreEnabled(config: ArticlesConfig): boolean;
226
407
  declare function getBreadcrumbsConfig(config: ArticlesConfig): BreadcrumbsConfig;
227
408
 
228
- declare function ArticlesPage({ config }: Readonly<{
409
+ declare function ArticlesPage({ config, initialArticles, initialCategories, page, totalPages, totalCount, }: Readonly<{
229
410
  config: ArticlesConfig;
411
+ initialArticles?: Article[];
412
+ initialCategories?: CategoryInfo[];
413
+ /**
414
+ * Current page number in `listingPagination: 'pages'` mode. Ignored
415
+ * (along with `totalPages`/`totalCount`) unless `config.listingPagination
416
+ * === 'pages'` - the default `'load-more'` behavior never reads these.
417
+ * `initialArticles` should already be this page's slice (see
418
+ * `paginateArticles` from `./server`).
419
+ */
420
+ page?: number;
421
+ /** Total page count in `'pages'` mode, from `getTotalPages`. */
422
+ totalPages?: number;
423
+ /**
424
+ * True total article count across every page (not just `initialArticles`'
425
+ * length) - used for `CollectionPageSchema.articleCount`. Defaults to
426
+ * `state.articles.length`, matching prior behavior when omitted.
427
+ */
428
+ totalCount?: number;
230
429
  }>): react_jsx_runtime.JSX.Element;
231
430
 
232
431
  type ArticleCardProps = Readonly<{
233
432
  article: Article;
433
+ /** Optional (Phase 27F) - when passed with `config.onEvent`, clicking the author link fires an `author_clicked` event. Omitting it changes nothing. */
434
+ config?: ArticlesConfig;
234
435
  }>;
235
- declare function ArticleCard({ article }: ArticleCardProps): react_jsx_runtime.JSX.Element;
436
+ declare function ArticleCard({ article, config }: ArticleCardProps): react_jsx_runtime.JSX.Element;
236
437
 
237
438
  type ArticleCategoryGridProps = Readonly<{
238
439
  categories: CategoryInfo[];
@@ -269,33 +470,107 @@ type FeaturedArticleProps = Readonly<{
269
470
  }>;
270
471
  declare function FeaturedArticle({ article, showAuthor }: FeaturedArticleProps): react_jsx_runtime.JSX.Element;
271
472
 
473
+ /** Context threaded from a listing page component down into `LatestArticles`/`PaginationNav` in `'pages'` mode. */
474
+ interface ListingPaginationContext {
475
+ page: number;
476
+ totalPages: number;
477
+ /** Un-paginated route path for this listing, e.g. `/articles` or `/articles/category/campaigns`. */
478
+ basePath: string;
479
+ }
480
+
272
481
  interface LatestArticlesProps {
273
482
  readonly articles: Article[];
274
483
  readonly pageSize: number;
484
+ /**
485
+ * Switches from the default client-only "Load more" button to real
486
+ * paginated routes: renders every article in `articles` as-is (the caller
487
+ * has already sliced to the current page, e.g. via `paginateArticles`) and
488
+ * a `PaginationNav` instead of the button. Omit to keep the existing
489
+ * `listingPagination: 'load-more'` behavior, unchanged.
490
+ */
491
+ readonly pagination?: ListingPaginationContext;
275
492
  }
276
- declare function LatestArticles({ articles, pageSize }: Readonly<LatestArticlesProps>): react_jsx_runtime.JSX.Element;
493
+ declare function LatestArticles({ articles, pageSize, pagination }: Readonly<LatestArticlesProps>): react_jsx_runtime.JSX.Element;
277
494
 
278
495
  interface LatestArticlesSectionProps {
279
496
  readonly articles: Article[];
280
497
  readonly searchQuery: string;
281
498
  readonly onClearSearch: () => void;
282
499
  readonly pageSize?: number;
500
+ /**
501
+ * `'pages'` mode pagination context. Ignored while `searchQuery` is set -
502
+ * search results stay unpaginated/client-only in both `listingPagination`
503
+ * modes (search URLs aren't meant to be indexed, so real pagination adds
504
+ * no SEO value there).
505
+ */
506
+ readonly pagination?: ListingPaginationContext;
283
507
  }
284
- declare function LatestArticlesSection({ articles, searchQuery, onClearSearch, pageSize, }: Readonly<LatestArticlesSectionProps>): react_jsx_runtime.JSX.Element;
508
+ declare function LatestArticlesSection({ articles, searchQuery, onClearSearch, pageSize, pagination, }: Readonly<LatestArticlesSectionProps>): react_jsx_runtime.JSX.Element;
285
509
 
286
510
  type CategoryArticlesPageProps = Readonly<{
287
511
  category: string;
288
512
  articles: Article[];
289
513
  config: ArticlesConfig;
514
+ /** Current page number in `listingPagination: 'pages'` mode. Ignored (with `totalPages`/`totalCount`) unless `config.listingPagination === 'pages'`. `articles` should already be this page's slice. */
515
+ page?: number;
516
+ /** Total page count in `'pages'` mode, from `getTotalPages`. */
517
+ totalPages?: number;
518
+ /** True total article count across every page. Defaults to `articles.length`. */
519
+ totalCount?: number;
290
520
  }>;
291
- declare function CategoryArticlesPage({ category, articles, config }: CategoryArticlesPageProps): react_jsx_runtime.JSX.Element | null;
521
+ declare function CategoryArticlesPage({ category, articles, config, page, totalPages, totalCount, }: CategoryArticlesPageProps): react_jsx_runtime.JSX.Element | null;
522
+
523
+ type SeriesArticlesPageProps = Readonly<{
524
+ /** Machine-safe series identifier - matches `Article.seriesSlug`. */
525
+ seriesSlug: string;
526
+ /** Already resolved via `getArticlesBySeries` (seriesOrder-sorted). */
527
+ articles: Article[];
528
+ config: ArticlesConfig;
529
+ }>;
530
+ /**
531
+ * Lightweight series landing page (Phase 27F) - a thinner sibling of
532
+ * `CategoryArticlesPage` for a `seriesSlug`'s ordered reader journey rather
533
+ * than a category's chronological listing. Displays series position badges
534
+ * (1-indexed, following `getArticlesBySeries`' order) so a reader can see
535
+ * where in the journey each article sits; doesn't attempt real ('pages'
536
+ * mode) pagination - series are expected to be short, curated lists rather
537
+ * than open-ended listings, so `LatestArticles` is used purely for its
538
+ * card grid, sized to show every article without a "Load more" step.
539
+ */
540
+ declare function SeriesArticlesPage({ seriesSlug, articles, config }: SeriesArticlesPageProps): react_jsx_runtime.JSX.Element | null;
292
541
 
542
+ /**
543
+ * Ordered section keys for the composable author-page render API (Phase
544
+ * 27E). `'custom'` is the one consumer-supplied slot - pass its content via
545
+ * the `customSection` prop. Passing `sections` is fully opt-in: omitting it
546
+ * keeps `AuthorArticlesPage`'s original output (Person/CollectionPage JSON-LD
547
+ * + article list only, no hero) byte-for-byte unchanged.
548
+ */
549
+ type AuthorPageSection = 'hero' | 'promise' | 'servesWho' | 'originStory' | 'principles' | 'proof' | 'cta' | 'articles' | 'custom';
293
550
  type AuthorArticlesPageProps = Readonly<{
294
551
  author: AuthorProfile;
295
552
  articles: Article[];
296
553
  config: ArticlesConfig;
554
+ /** Current page number in `listingPagination: 'pages'` mode. Ignored (with `totalPages`/`totalCount`) unless `config.listingPagination === 'pages'`. `articles` should already be this page's slice. */
555
+ page?: number;
556
+ /** Total page count in `'pages'` mode, from `getTotalPages`. */
557
+ totalPages?: number;
558
+ /** True total article count across every page. Defaults to `articles.length` (also used as the `Person` schema's `interactionStatistic` count). */
559
+ totalCount?: number;
560
+ /**
561
+ * Ordered list of sections to render (Phase 27E composable render API).
562
+ * When omitted, `AuthorArticlesPage` renders exactly as it did before this
563
+ * prop existed - the article list only, no hero and no rich-profile
564
+ * sections, even if `author` has the new optional fields populated.
565
+ * Include `'hero'` to render `AuthorDetailHero` here instead of composing
566
+ * it separately; each other section renders nothing when its backing
567
+ * field (`author.promise`, `author.servesWho`, etc.) is unset.
568
+ */
569
+ sections?: AuthorPageSection[];
570
+ /** Content for the one `'custom'` slot in `sections`. Ignored unless `sections` includes `'custom'`. */
571
+ customSection?: ReactNode;
297
572
  }>;
298
- declare function AuthorArticlesPage({ author, articles, config }: AuthorArticlesPageProps): react_jsx_runtime.JSX.Element;
573
+ declare function AuthorArticlesPage({ author, articles, config, page, totalPages, totalCount, sections, customSection, }: AuthorArticlesPageProps): react_jsx_runtime.JSX.Element;
299
574
 
300
575
  type AuthorCardProps = Readonly<{
301
576
  author: AuthorProfile;
@@ -325,14 +600,6 @@ declare function BreadcrumbSchema({ items }: Readonly<{
325
600
  items: BreadcrumbItem[];
326
601
  }>): react_jsx_runtime.JSX.Element;
327
602
 
328
- type ArticleSchemaProps = Readonly<{
329
- article: Article;
330
- articleUrl: string;
331
- siteName: string;
332
- showAuthor?: boolean;
333
- authors?: AuthorProfile[];
334
- }>;
335
- declare function ArticleSchema({ article, articleUrl, siteName, showAuthor, authors, }: ArticleSchemaProps): react_jsx_runtime.JSX.Element;
336
603
  type ArticleSEOProps = Readonly<{
337
604
  article: Article;
338
605
  articleUrl: string;
@@ -340,8 +607,9 @@ type ArticleSEOProps = Readonly<{
340
607
  siteLogo?: string;
341
608
  showAuthor?: boolean;
342
609
  authors?: AuthorProfile[];
610
+ config?: ArticlesConfig;
343
611
  }>;
344
- declare function ArticleSEO({ article, articleUrl, siteName, siteLogo, showAuthor, authors, }: ArticleSEOProps): react_jsx_runtime.JSX.Element;
612
+ declare function ArticleSEO({ article, articleUrl, siteName, siteLogo, showAuthor, authors, config, }: ArticleSEOProps): react_jsx_runtime.JSX.Element;
345
613
  type FAQPageSchemaProps = Readonly<{
346
614
  items: ReadonlyArray<{
347
615
  question: string;
@@ -349,21 +617,31 @@ type FAQPageSchemaProps = Readonly<{
349
617
  }>;
350
618
  }>;
351
619
  declare function FAQPageSchema({ items }: FAQPageSchemaProps): react_jsx_runtime.JSX.Element;
620
+ type CollectionPageItem = Readonly<{
621
+ position: number;
622
+ url: string;
623
+ name: string;
624
+ }>;
352
625
  type CollectionPageSchemaProps = Readonly<{
353
626
  title: string;
354
627
  description: string;
355
628
  url: string;
356
629
  articleCount: number;
630
+ items?: CollectionPageItem[];
357
631
  }>;
358
- declare function CollectionPageSchema({ title, description, url, articleCount, }: CollectionPageSchemaProps): react_jsx_runtime.JSX.Element;
632
+ declare function CollectionPageSchema({ title, description, url, articleCount, items, }: CollectionPageSchemaProps): react_jsx_runtime.JSX.Element;
359
633
 
360
634
  interface ArticleSocialShareProps {
361
635
  readonly title: string;
362
636
  readonly url: string;
363
637
  readonly excerpt?: string;
364
638
  readonly shareMessage?: string;
639
+ /** Optional (Phase 27F). Enables `shared` events - requires `articleSlug` too. */
640
+ readonly config?: ArticlesConfig;
641
+ /** The article slug being shared. Required to emit `shared`. */
642
+ readonly articleSlug?: string;
365
643
  }
366
- declare function ArticleSocialShare({ title, url, excerpt, shareMessage }: ArticleSocialShareProps): react_jsx_runtime.JSX.Element;
644
+ declare function ArticleSocialShare({ title, url, excerpt, shareMessage, config, articleSlug, }: ArticleSocialShareProps): react_jsx_runtime.JSX.Element;
367
645
 
368
646
  interface ArticleNavigationProps {
369
647
  readonly previous: {
@@ -375,8 +653,33 @@ interface ArticleNavigationProps {
375
653
  title: string;
376
654
  } | null;
377
655
  readonly basePath: string;
656
+ /** The article slug this navigation is shown on. Combined with `pathKey`, enables `path_step_advanced` events. */
657
+ readonly fromSlug?: string;
658
+ /**
659
+ * Set when this navigation walks a configured `Path` (as opposed to plain
660
+ * chronological/series adjacency) - fires `path_step_advanced` on click.
661
+ * Omit for ordinary previous/next navigation; no event fires without it.
662
+ */
663
+ readonly pathKey?: string;
664
+ readonly config?: ArticlesConfig;
378
665
  }
379
- declare function ArticleNavigation({ previous, next, basePath }: ArticleNavigationProps): react_jsx_runtime.JSX.Element | null;
666
+ declare function ArticleNavigation({ previous, next, basePath, fromSlug, pathKey, config, }: ArticleNavigationProps): react_jsx_runtime.JSX.Element | null;
667
+
668
+ type RelatedContentSource = 'path' | 'series' | 'category';
669
+
670
+ type RelatedArticlesSectionProps = Readonly<{
671
+ articles: Article[];
672
+ category: string;
673
+ /** Overrides the default "More in {category}" heading - used by `getRelatedContent` callers when `source` is `'path'`/`'series'`. Omit to keep the default category-based heading. */
674
+ heading?: string;
675
+ /** Optional (Phase 27F). Enables `related_article_clicked` events - requires `fromSlug` too. */
676
+ config?: ArticlesConfig;
677
+ /** The article slug this related section is shown on. Required to emit `related_article_clicked`. */
678
+ fromSlug?: string;
679
+ /** Selection source that produced `articles`, from `getRelatedContent`. Defaults to `'category'`. */
680
+ source?: RelatedContentSource;
681
+ }>;
682
+ declare function RelatedArticlesSection({ articles, category, heading, config, fromSlug, source, }: RelatedArticlesSectionProps): react_jsx_runtime.JSX.Element | null;
380
683
 
381
684
  type ArticleBackLinkProps = Readonly<{
382
685
  config: Pick<ArticlesConfig, 'showBackToArticles'>;
@@ -394,6 +697,57 @@ declare function ArticleTOC({ toc, className }: ArticleTOCProps): react_jsx_runt
394
697
 
395
698
  declare function ScrollToTop(): react_jsx_runtime.JSX.Element | null;
396
699
 
700
+ interface PaginationNavProps {
701
+ readonly basePath: string;
702
+ readonly page: number;
703
+ readonly totalPages: number;
704
+ }
705
+ /**
706
+ * Renders real `<a href>` prev/next links between listing pages (used only
707
+ * in `listingPagination: 'pages'` mode), plus `<link rel="prev"/"next">`
708
+ * tags for crawlers/tools that still read them.
709
+ *
710
+ * Google stopped using rel=next/prev as an indexing signal in 2019 (Google
711
+ * Search Central's pagination guidance now recommends a unique
712
+ * self-referencing canonical per page - see `generateArticlesIndexPageMetadata`
713
+ * et al in seoUtils.ts - plus real crawlable links between pages, which is
714
+ * what this component's visible Previous/Next links provide). rel=next/prev
715
+ * remains valid HTML and is still read by Bing and some third-party tools,
716
+ * so it's emitted here for free: React 19 hoists `<link>`/`<meta>` elements
717
+ * rendered anywhere in a Server Component tree into `<head>` automatically
718
+ * (not just from layout.js/page.js directly), so no separate Head API call
719
+ * is needed.
720
+ */
721
+ declare function PaginationNav({ basePath, page, totalPages }: PaginationNavProps): react_jsx_runtime.JSX.Element | null;
722
+
723
+ type ArticleViewTrackerProps = Readonly<{
724
+ article: Pick<Article, 'slug'> & Partial<Pick<Article, 'category' | 'seriesSlug' | 'wordCount'>>;
725
+ config?: ArticlesConfig;
726
+ }>;
727
+ /**
728
+ * Renders nothing - fires `article_viewed` on mount and `meaningful_read`
729
+ * once, after roughly half the article's estimated read time has elapsed
730
+ * (a deterministic, testable timing approximation rather than scroll-depth
731
+ * tracking, which this package has no reliable cross-app way to measure
732
+ * since it doesn't own the article body's scroll container). Place once on
733
+ * the article detail page alongside `ArticleContent`.
734
+ */
735
+ declare function ArticleViewTracker({ article, config }: ArticleViewTrackerProps): null;
736
+ type CtaViewTrackerProps = Readonly<{
737
+ /** Opaque CTA/offer ID - never label text (no PII/marketing copy in event payloads). */
738
+ ctaId: string;
739
+ articleSlug?: string;
740
+ config?: ArticlesConfig;
741
+ children: ReactNode;
742
+ }>;
743
+ /**
744
+ * Wraps any CTA block and fires `cta_viewed` once, the first time at least
745
+ * half of it scrolls into the viewport (`IntersectionObserver`). Falls back
746
+ * to firing immediately when `IntersectionObserver` isn't available (older
747
+ * browsers, non-DOM test environments) rather than never firing.
748
+ */
749
+ declare function CtaViewTracker({ ctaId, articleSlug, config, children }: CtaViewTrackerProps): react_jsx_runtime.JSX.Element;
750
+
397
751
  interface CommentsSectionProps {
398
752
  readonly articleSlug: string;
399
753
  readonly config: CommentsConfig;
@@ -447,6 +801,6 @@ interface UseArticlesReturn {
447
801
  error: string | null;
448
802
  handleSearch: (query: string) => void;
449
803
  }
450
- declare function useArticles(): UseArticlesReturn;
804
+ declare function useArticles(initialArticles?: Article[], initialCategories?: CategoryInfo[]): UseArticlesReturn;
451
805
 
452
- 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 };
806
+ 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 };