@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.
- package/CHANGELOG.md +34 -0
- package/README.md +559 -11
- package/dist/index.cjs +984 -388
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +383 -21
- package/dist/index.d.ts +383 -21
- package/dist/index.js +968 -376
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +74 -6
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +149 -0
- package/dist/nextjs.d.ts +149 -0
- package/dist/nextjs.js +74 -6
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +665 -27
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +357 -3
- package/dist/server.d.ts +357 -3
- package/dist/server.js +643 -29
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/ArticleCard.tsx +42 -6
- package/src/ArticleContent.tsx +144 -5
- package/src/ArticleDetailHero.tsx +33 -1
- package/src/ArticleNavigation.tsx +32 -1
- package/src/ArticleSchemas.tsx +43 -39
- package/src/ArticleSocialShare.tsx +54 -10
- package/src/ArticlesPage.tsx +56 -5
- package/src/AuthorArticlesPage.tsx +308 -14
- package/src/AuthorCard.tsx +1 -1
- package/src/CategoryArticlesPage.tsx +34 -2
- package/src/LatestArticles.tsx +28 -1
- package/src/LatestArticlesSection.tsx +15 -1
- package/src/PaginationNav.tsx +78 -0
- package/src/RelatedArticlesSection.tsx +58 -0
- package/src/SeriesArticlesPage.tsx +66 -0
- package/src/__tests__/ArticleCard.test.tsx +63 -3
- package/src/__tests__/ArticleContent.test.tsx +143 -0
- package/src/__tests__/ArticleDetailHero.test.tsx +30 -0
- package/src/__tests__/ArticleNavigation.test.tsx +81 -3
- package/src/__tests__/ArticleSchemas.test.tsx +155 -81
- package/src/__tests__/ArticleSocialShare.test.tsx +54 -0
- package/src/__tests__/ArticlesPage.test.tsx +131 -0
- package/src/__tests__/AuthorArticlesPage.test.tsx +304 -3
- package/src/__tests__/CategoryArticlesPage.test.tsx +116 -1
- package/src/__tests__/LatestArticles.test.tsx +52 -0
- package/src/__tests__/LatestArticlesSection.test.tsx +28 -0
- package/src/__tests__/PaginationNav.test.tsx +73 -0
- package/src/__tests__/RelatedArticlesSection.test.tsx +132 -0
- package/src/__tests__/SeriesArticlesPage.test.tsx +121 -0
- package/src/__tests__/eventTracking.test.tsx +145 -0
- package/src/__tests__/events.test.ts +82 -0
- package/src/__tests__/markdown.test.ts +78 -1
- package/src/__tests__/pagination.test.ts +178 -0
- package/src/__tests__/seoUtils-authors.test.ts +37 -0
- package/src/__tests__/seoUtils.test.ts +246 -0
- package/src/__tests__/server-articles.test.ts +356 -1
- package/src/__tests__/validateArticles.test.ts +312 -0
- package/src/articleTypes.ts +109 -0
- package/src/articlesConfig.ts +45 -1
- package/src/eventTracking.tsx +97 -0
- package/src/events.ts +105 -0
- package/src/index.ts +26 -1
- package/src/markdown.ts +41 -0
- package/src/pagination.ts +93 -0
- package/src/seoUtils.ts +198 -11
- package/src/server-articles.ts +199 -6
- package/src/server.ts +46 -2
- 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,
|
|
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 };
|