@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.
- package/CHANGELOG.md +30 -0
- package/README.md +568 -6
- package/dist/index.cjs +970 -389
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +375 -21
- package/dist/index.d.ts +375 -21
- package/dist/index.js +954 -377
- 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 +141 -0
- package/dist/nextjs.d.ts +141 -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 +349 -3
- package/dist/server.d.ts +349 -3
- package/dist/server.js +643 -29
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/ArticleCard.tsx +37 -1
- package/src/ArticleContent.tsx +144 -5
- package/src/ArticleDetailHero.tsx +23 -0
- package/src/ArticleNavigation.tsx +32 -1
- package/src/ArticleSchemas.tsx +43 -39
- package/src/ArticleSocialShare.tsx +54 -10
- package/src/ArticlesPage.tsx +65 -7
- 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 +55 -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 +148 -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__/useArticles.test.ts +28 -0
- package/src/__tests__/validateArticles.test.ts +312 -0
- package/src/articleTypes.ts +109 -0
- package/src/articlesConfig.ts +37 -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/useArticles.ts +10 -5
- 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
|
/**
|
|
@@ -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,
|
|
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 };
|