@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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-08-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Configurable heading font family.** New `ArticlesTheme.headerFontFamily`, optional and falling back to inherit/nothing when unset - existing consumers who don't set it see byte-identical output. Threaded through `ArticleDetailHero` (new optional `config` prop), `RelatedArticlesSection`, and `ArticleCard` (both already accepted `config`) via inline `style={{ fontFamily: config?.theme?.headerFontFamily }}`, and into `ArticlesPage`'s `buildThemeVars` as `--articles-header-font-family`, consistent with the package's existing CSS-var theming pattern. Lets a site use a distinct display face for headings (e.g. a serif) separate from its body `fontFamily`, without hardcoding either into the package. Found auditing theroleplayersguild.com's article pages, which use a serif heading treatment the package couldn't previously express.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`ArticleCard` hardcoded an indigo hover color.** The title link's `hover:text-indigo-600` was a stray leftover unrelated to any theme token and would visibly clash with any site not using an indigo-ish primary color. Replaced with `hover:text-primary`, matching the token already used elsewhere in the same component.
|
|
17
|
+
|
|
18
|
+
## [1.0.0] - 2026-08-05
|
|
19
|
+
|
|
20
|
+
### Breaking
|
|
21
|
+
|
|
22
|
+
- **Removed `ArticleSchema`** (component and its type export). It was the original, minimal `Article` JSON-LD component (added in PR #192) before `ArticleSEO` was introduced (PR #229) as a strictly more complete replacement - FAQ/HowTo sub-schemas, `dateModified`, `articleType`, `siteLogo`, series support, and now `articleSection`/`keywords`/`wordCount`/author images too. Confirmed unused by both known consumers of this package (this monorepo's own frontend and theroleplayersguild.com both only ever imported `ArticleSEO`). If you were rendering `ArticleSchema` directly, switch to `ArticleSEO` - it produces a superset of the same `Article` JSON-LD plus the additional schemas above; rendering both on the same page would have produced duplicate/conflicting `Article` entities anyway.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Author avatar on `ArticleCard`.** `Article` gains two new optional fields, `authorSlug` and `authorAvatar`, resolved server-side in `getArticleSummary` against `config.authors` (via the existing `getArticleAuthors` resolution chain, so slug/name-based frontmatter both work identically to how `getArticleAuthors` already resolves them) - no client-side `config` access needed. `ArticleCard` renders a small (24px) avatar circle, falling back to two-letter initials when no avatar is configured, linked to `/articles/authors/[slug]`, whenever `authorSlug` is present; omitted entirely when it isn't, matching prior behavior. The avatar/initials are marked decorative (`alt=""` / `aria-hidden`) since the adjacent author name already conveys the same information - avoids duplicated screen-reader announcements. Backward compatible: existing consumers who don't pass `config` to article-fetching functions, or whose author isn't in `config.authors`, see no change.
|
|
27
|
+
- **`RelatedArticlesSection` + `getRelatedArticlesByCategory`.** New component/server-function pair for category-scoped article interlinking on the article detail template. `getRelatedArticlesByCategory(currentSlug, category, limit?, config?)` returns up to `limit` (default `3`) other articles sharing `category` with `currentSlug`, most recent first - built on the existing `getArticlesByCategory` slug-matching filter rather than duplicating it. `RelatedArticlesSection` renders them via `ArticleCard` in a responsive grid with a "More in {category}" heading; renders nothing when the list is empty. Purely additive - no existing exports or behavior changed.
|
|
28
|
+
- **Richer `Article` JSON-LD.** `ArticleSEO` (and `getPersonSchemas` internally) now accepts an optional `config?: ArticlesConfig` prop; when passed, each author's `Person` schema gains `image` via the existing `getAuthorAvatar` resolution (matching what `AuthorArticlesPage`'s Person schema already did). The `Article` schema also always includes `articleSection` (from `article.category`) and, when present, `keywords` (from `article.tags`, comma-joined) and `wordCount` (new `Article.wordCount` field, computed from the same `reading-time` pass already used for `readTime` - no extra parsing cost). All additive/optional; omitting `config` or these fields reproduces prior output exactly.
|
|
29
|
+
- **`CollectionPage`/`ItemList` schema on every listing page.** `CollectionPageSchema` accepts an optional `items?: CollectionPageItem[]` prop (`{ position, url, name }[]`, new exported type); when non-empty, adds `mainEntity: { '@type': 'ItemList', itemListElement: [...] }` alongside the existing `numberOfItems`. `ArticlesPage` and `AuthorArticlesPage` now pass this (previously `numberOfItems`-only); `CategoryArticlesPage` previously emitted **no JSON-LD at all** and now renders a full `CollectionPageSchema` too. All three build `items` from the articles actually present in the page's initial HTML (the first `pageSize`), matching what a crawler sees before "Load more" is clicked, not the full underlying list.
|
|
30
|
+
- **Discovery & reader-journey layer (Phase 27F).** Additive frontmatter/config/component surface, all optional - legacy consumers with none of it set render/behave identically to before.
|
|
31
|
+
- **Discovery metadata overrides:** `Article` gains `searchTitle`/`searchDescription` and `socialTitle`/`socialDescription`/`socialImage`, all optional. New `resolveSearchMetadata`/`resolveSocialMetadata` helpers (exported from `./server`) define the fallback rules. `generateArticleMetadata`'s `<title>`/meta description now prefer `searchTitle`/`searchDescription`; its Open Graph/Twitter Card output now prefers `socialTitle`/`socialDescription`/`socialImage`. Canonical URLs, JSON-LD (`ArticleSEO`), `ArticleCard`, and RSS are unchanged - they keep reading `title`/`excerpt`/`featuredImage` directly.
|
|
32
|
+
- **Series as a navigable journey:** `Article` gains `seriesSlug?: string`/`seriesOrder?: number`, additive alongside the existing label-only `series` string (unchanged). New `getArticlesBySeries(seriesSlug, config?)` (sorted by `seriesOrder`, stable-falling-back to date order), `getAdjacentArticlesInSeries` (a series-aware sibling of `getAdjacentArticles` returning the same `{ previous, next }` shape, so the existing `ArticleNavigation` component works unchanged for series nav), `generateSeriesStaticParams`/`generateSeriesMetadata`, and a new lightweight `SeriesArticlesPage` component.
|
|
33
|
+
- **`Path` primitive:** new `PathDefinition` type (`{ name, promise, articles: string[], nextAction: { label, href } }`) and `ArticlesConfig.paths?: Record<string, PathDefinition>` - a distinct, config-driven "start here" journey that can cross series/categories, not the same as `series`. New `getPath`/`getPathArticles` server functions.
|
|
34
|
+
- **Article-detail extension points:** `ArticleContent` gains four optional slot props - `afterHero`, `afterIntro`, `midContent`, `afterContent` - accepting a `ReactNode` or a function receiving a sanitized `ArticleSlotContext` (slug/title/category/tags/readTime/wordCount/authorSlug/seriesSlug/primaryActionId, no raw content or author PII). `afterIntro`/`midContent` resolve placement deterministically from the parsed markdown/MDX AST (new `getContentSlotBoundaries`, exported from `./server`) rather than splitting rendered HTML strings; when no paragraph structure is detected, they're silently omitted (never thrown) while `afterHero`/`afterContent` still render. Omitting all four slots reproduces `ArticleContent`'s exact pre-27F output byte-for-byte.
|
|
35
|
+
- **Primary action reference:** new optional `Article.primaryAction?: { actionId: string }`, parsed from a frontmatter string shorthand or object. The package never interprets `actionId` - no coupling to a specific form/email/analytics vendor; the consuming app resolves it via its own registry inside a slot, rendering nothing on no match.
|
|
36
|
+
- **Reusable related-content selection:** new `getRelatedContent(article, config, limit?)` prefers a configured `Path` containing the article, then its `seriesSlug`, falling back to 27B's `getRelatedArticlesByCategory` (imported, not reimplemented) when neither applies. `RelatedArticlesSection` gains optional `heading` (overrides the default `"More in {category}"`), `config`, `fromSlug`, and `source` props.
|
|
37
|
+
- **Vendor-neutral event contract:** new `ArticleEvent` discriminated union (`article_viewed`, `meaningful_read`, `author_clicked`, `cta_viewed`, `cta_clicked`, `shared`, `related_article_clicked`, `path_step_advanced`) and `ArticlesConfig.onEvent?: (event: ArticleEvent) => void`. Every payload is PII-free (slugs/IDs/enums only). Wired into `ArticleCard` (`author_clicked`), `RelatedArticlesSection` (`related_article_clicked`), `ArticleNavigation` (`path_step_advanced`, opt-in via new `pathKey`/`fromSlug` props), `ArticleSocialShare` (`shared`, via new `articleSlug` prop), and `AuthorArticlesPage`'s `'cta'` section (`cta_viewed`/`cta_clicked`). New standalone components `ArticleViewTracker` (fires `article_viewed` on mount, `meaningful_read` after roughly half the estimated read time) and `CtaViewTracker` (fires `cta_viewed` on scroll-into-view, immediate fallback without `IntersectionObserver`). No PostHog/Plunk dependency added to `package.json` - translate events to your own analytics stack in `onEvent`.
|
|
38
|
+
- **Package validator:** new `validateArticles(articles, config)` (pure function) and `validateAllArticles(config)` (loads via `getAllArticles` first), both exported from `./server`. Checks unique canonical URLs, valid author references, series order/slug collisions, `Path` article references (missing or draft = hard error, not a silently dead step), unsafe URL schemes (`javascript:`/`data:`/`vbscript:`), and discovery field length limits, returning `{ ok, errors, warnings }` - errors are broken-reader-journey issues, warnings are optional-field gaps. No standalone CLI binary shipped; `validateAllArticles` is the intended integration point for a consuming app's own validation script (see README).
|
|
39
|
+
- **Rich `AuthorProfile` fields + composable `AuthorArticlesPage` render API (Phase 27E).** `AuthorProfile` gains seven optional, additive fields: `promise?: string` (one-line audience promise), `originStory?: RichText` (new `RichTextSection[]` shape - `{ heading?: string; paragraphs: string[] }[]`, structured rather than a single HTML blob), `servesWho?: string[]`, `principles?: string[]`, `credentials?: string[]`, `proof?: ProofItem[]` (new type, `{ claim: string; source?: string; url?: string }`), and `primaryCta?: { label: string; href: string }`. `AuthorDetailHero` is untouched by this phase; `AuthorArticlesPage` gains a new optional `sections?: AuthorPageSection[]` prop (`'hero' | 'promise' | 'servesWho' | 'originStory' | 'principles' | 'proof' | 'cta' | 'articles' | 'custom'`) plus a `customSection?: ReactNode` slot for the `'custom'` key - omitting `sections` reproduces prior `AuthorArticlesPage` output exactly (article list + JSON-LD only, no hero), even when the new profile fields are populated. Each new section renders nothing when its backing field is unset and uses a semantic `<section aria-label="...">` landmark, matching the package's existing accessibility convention. `getPersonSchema`/`getPersonSchemas` (Person JSON-LD) deliberately exclude all seven new fields - `promise`/`principles` are marketing copy, `servesWho` is an audience segment not a `knowsAbout` topic, and `credentials`/`proof` are unverifiable claims - `description`/`image`/`sameAs`/`knowsAbout` stay sourced exactly as before. Added a one-sentence author authority statement to `ArticleDetailHero`'s byline (sourced from `promise`, single configured author only) rather than `ArticleCard`, since a listing grid repeats the same author across many cards - the detail-page byline is the one placement where it adds trust without adding clutter. New exported types: `AuthorPageSection`, `RichText`, `RichTextSection`, `ProofItem`.
|
|
40
|
+
- **Real, config-gated pagination for listing pages (Phase 27D).** New `listingPagination?: 'load-more' | 'pages'` field on `ArticlesConfig`, defaulting to `'load-more'` - the existing client-only "Load more" button, byte-for-byte unchanged. Setting `'pages'` opts `ArticlesPage`/`CategoryArticlesPage`/`AuthorArticlesPage` into real, directly-navigable paginated routes instead, once the consuming app passes the new optional `page`/`totalPages`/`totalCount` props (a no-op unless `listingPagination === 'pages'`, so existing consumers see zero behavior change). New pagination primitives exported from `./server`: `getTotalPages`, `paginateArticles`, `buildPageUrl`, `buildPaginationLinks`, `generateListingPageStaticParams`, `parsePageParam`, `isPageOutOfRange`, plus per-page metadata generators `generateArticlesIndexPageMetadata`, `generateCategoryPageMetadata`, `generateAuthorPageMetadata` (each page gets its own self-referencing canonical, never pointing back to page 1 - the mechanism Google's current pagination guidance actually relies on, since Google stopped reading `rel="next"`/`rel="prev"` as a signal in 2019). New `PaginationNav` component (main entry) renders real `<a href>` Previous/Next links plus `rel="prev"`/`rel="next"` `<link>` tags (still emitted for Bing/other tools, hoisted into `<head>` via React 19's built-in support for `<link>`/`<meta>` rendered anywhere in the tree) - rendered automatically by `LatestArticles` when a `pagination` context is passed, in place of the "Load more" button. Search (`ArticleSearchBar`/`useArticles`) stays unpaginated/client-only in both modes - search result URLs aren't meant to be indexed, so `LatestArticlesSection` drops the pagination context while a search query is active. Paginated listing URLs are intentionally **not** added to `getArticleSitemapEntries` - every article/category/author URL is already listed directly, so paginated listing pages add no new sitemap-discoverable URLs; this feature is about internal link equity and crawlers/AI agents that weight on-page links over sitemaps. See the README's "Pagination" section for full route-wiring examples for all three listing surfaces.
|
|
41
|
+
|
|
8
42
|
## [0.12.0] - 2026-08-03
|
|
9
43
|
|
|
10
44
|
### Fixed
|