@fullstackdatasolutions/articles 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/README.md +209 -78
  3. package/dist/index.cjs +635 -274
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +164 -56
  6. package/dist/index.d.ts +164 -56
  7. package/dist/index.js +614 -250
  8. package/dist/index.js.map +1 -1
  9. package/dist/nextjs.cjs +113 -38
  10. package/dist/nextjs.cjs.map +1 -1
  11. package/dist/nextjs.d.cts +71 -0
  12. package/dist/nextjs.d.ts +71 -0
  13. package/dist/nextjs.js +113 -38
  14. package/dist/nextjs.js.map +1 -1
  15. package/dist/server.cjs +394 -52
  16. package/dist/server.cjs.map +1 -1
  17. package/dist/server.d.cts +96 -6
  18. package/dist/server.d.ts +96 -6
  19. package/dist/server.js +382 -52
  20. package/dist/server.js.map +1 -1
  21. package/package.json +8 -5
  22. package/src/ArticleContent.tsx +8 -3
  23. package/src/ArticleDetailHero.tsx +27 -2
  24. package/src/ArticleSchemas.tsx +27 -27
  25. package/src/AuthorArticlesPage.tsx +60 -0
  26. package/src/AuthorCard.tsx +112 -0
  27. package/src/AuthorDetailHero.tsx +56 -0
  28. package/src/Breadcrumb.tsx +78 -0
  29. package/src/CategoryArticlesPage.tsx +62 -11
  30. package/src/__tests__/ArticleContent.test.tsx +18 -2
  31. package/src/__tests__/ArticleDetailHero.test.tsx +21 -1
  32. package/src/__tests__/ArticleSchemas.test.tsx +47 -2
  33. package/src/__tests__/AuthorArticlesPage.test.tsx +74 -0
  34. package/src/__tests__/AuthorCard.test.tsx +98 -0
  35. package/src/__tests__/AuthorDetailHero.test.tsx +51 -0
  36. package/src/__tests__/CategoryArticlesPage.test.tsx +31 -5
  37. package/src/__tests__/authorUtils.test.ts +89 -0
  38. package/src/__tests__/markdown.test.ts +79 -3
  39. package/src/__tests__/renderMdx.test.tsx +57 -0
  40. package/src/__tests__/seoUtils-authors.test.ts +160 -0
  41. package/src/__tests__/seoUtils.test.ts +106 -0
  42. package/src/__tests__/server-articles.test.ts +174 -3
  43. package/src/articleTypes.ts +33 -0
  44. package/src/articlesConfig.ts +67 -0
  45. package/src/authorUtils.ts +95 -0
  46. package/src/index.ts +32 -9
  47. package/src/markdown.ts +67 -8
  48. package/src/renderMdx.tsx +6 -3
  49. package/src/seoUtils.ts +279 -6
  50. package/src/server-articles.ts +124 -34
  51. package/src/server.ts +21 -2
package/README.md CHANGED
@@ -74,6 +74,33 @@ export const siteConfig: ArticlesConfig = {
74
74
  siteUrl: 'https://yoursite.com',
75
75
  siteName: 'Your Site',
76
76
  pageSize: 6,
77
+ defaultAuthor: 'jane-doe',
78
+ authors: {
79
+ 'jane-doe': {
80
+ slug: 'jane-doe',
81
+ name: 'Jane Doe',
82
+ bio: 'Jane writes about campaign operations and civic technology.',
83
+ avatar: '/authors/jane-doe.jpg',
84
+ social: {
85
+ website: 'https://yoursite.com/about',
86
+ facebook: 'janedoe',
87
+ twitter: '@janedoe',
88
+ x: 'janedoe',
89
+ linkedin: 'https://www.linkedin.com/in/janedoe',
90
+ other: {
91
+ Podcast: 'https://example.com/podcast',
92
+ },
93
+ },
94
+ },
95
+ },
96
+ breadcrumbs: {
97
+ article: [
98
+ { name: 'Resources', url: '/resources' },
99
+ 'primaryCategory',
100
+ 'folderPath',
101
+ 'articleTitle',
102
+ ],
103
+ },
77
104
  }
78
105
  ```
79
106
 
@@ -97,16 +124,18 @@ import {
97
124
  ArticleNavigation,
98
125
  ArticleSEO,
99
126
  ArticleSocialShare,
100
- BreadcrumbSchema,
127
+ Breadcrumb,
101
128
  CommentsSection,
102
129
  ScrollToTop,
103
130
  } from '@fullstackdatasolutions/articles'
104
131
  import {
105
132
  ArticleContent,
106
133
  ArticleTOC,
134
+ buildArticleBreadcrumbs,
107
135
  generateArticleMetadata,
108
136
  generateArticleStaticParams,
109
137
  getAdjacentArticles,
138
+ getArticleAuthors,
110
139
  getArticleMetadata,
111
140
  } from '@fullstackdatasolutions/articles/server'
112
141
  import { siteConfig } from '@/config/articles'
@@ -125,26 +154,31 @@ export async function generateMetadata({ params }: ArticlePageProps) {
125
154
  export default async function Page({ params }: ArticlePageProps) {
126
155
  const { slug: slugSegments } = await params
127
156
  const slug = normalizeSlug(slugSegments)
128
- const article = await getArticleMetadata(slug)
157
+ const article = await getArticleMetadata(slug, siteConfig)
129
158
  if (!article) notFound()
130
159
  const { previous, next } = await getAdjacentArticles(slug)
160
+ const authors = getArticleAuthors(article, siteConfig)
161
+ const breadcrumbItems = buildArticleBreadcrumbs(article, siteConfig)
131
162
  const siteUrl = siteConfig.siteUrl.replace(/\/$/, '')
132
163
  const articleUrl = `${siteUrl}/articles/${slug}`
133
164
  return (
134
165
  <>
135
- <ArticleSEO article={article} articleUrl={articleUrl} siteName={siteConfig.siteName} />
136
- <BreadcrumbSchema
137
- items={[
138
- { name: 'Home', url: siteUrl },
139
- { name: 'Articles', url: `${siteUrl}/articles` },
140
- { name: article.title, url: articleUrl },
141
- ]}
166
+ <ArticleSEO
167
+ article={article}
168
+ articleUrl={articleUrl}
169
+ siteName={siteConfig.siteName}
170
+ authors={authors}
171
+ />
172
+ <Breadcrumb items={breadcrumbItems} />
173
+ <ArticleDetailHero
174
+ article={article}
175
+ authors={authors}
176
+ categoryBasePath="/articles/category"
142
177
  />
143
- <ArticleDetailHero article={article} categoryBasePath="/articles/category" />
144
178
  {siteConfig.showToc !== false && article.toc && article.toc.length > 0 && (
145
179
  <ArticleTOC toc={article.toc} />
146
180
  )}
147
- <ArticleContent article={article} />
181
+ <ArticleContent article={article} config={siteConfig} />
148
182
  <ArticleSocialShare title={article.title} url={articleUrl} excerpt={article.excerpt} />
149
183
  {siteConfig.comments && <CommentsSection articleSlug={slug} config={siteConfig.comments} />}
150
184
  <ArticleNavigation previous={previous} next={next} basePath="/articles" />
@@ -169,10 +203,43 @@ export async function generateMetadata({ params }: CategoryPageProps) {
169
203
  }
170
204
  export default async function Page({ params }: CategoryPageProps) {
171
205
  const { category } = await params
172
- const articles = await getArticlesByCategory(category)
206
+ const articles = await getArticlesByCategory(category, siteConfig)
173
207
  if (articles.length === 0) notFound()
174
208
  return <CategoryArticlesPage category={category} articles={articles} config={siteConfig} />
175
209
  }
210
+
211
+ // app/articles/authors/[author]/page.tsx
212
+ import { notFound } from 'next/navigation'
213
+ import { AuthorArticlesPage, AuthorDetailHero, Breadcrumb } from '@fullstackdatasolutions/articles'
214
+ import {
215
+ buildAuthorBreadcrumbs,
216
+ generateAuthorMetadata,
217
+ generateAuthorStaticParams,
218
+ getArticlesByAuthor,
219
+ getAuthorBySlug,
220
+ } from '@fullstackdatasolutions/articles/server'
221
+ import { siteConfig } from '@/config/articles'
222
+ type AuthorPageProps = Readonly<{ params: Promise<{ author: string }> }>
223
+ export function generateStaticParams(): { author: string }[] {
224
+ return generateAuthorStaticParams(siteConfig)
225
+ }
226
+ export async function generateMetadata({ params }: AuthorPageProps) {
227
+ const { author } = await params
228
+ return generateAuthorMetadata(author, siteConfig)
229
+ }
230
+ export default async function Page({ params }: AuthorPageProps) {
231
+ const { author: authorSlug } = await params
232
+ const author = getAuthorBySlug(authorSlug, siteConfig)
233
+ if (!author) notFound()
234
+ const articles = await getArticlesByAuthor(authorSlug, siteConfig)
235
+ return (
236
+ <>
237
+ <Breadcrumb items={buildAuthorBreadcrumbs(author, siteConfig)} />
238
+ <AuthorDetailHero author={author} />
239
+ <AuthorArticlesPage author={author} articles={articles} config={siteConfig} />
240
+ </>
241
+ )
242
+ }
176
243
  ```
177
244
 
178
245
  ### 6. Set up markdown handler (Next.js 16+)
@@ -197,7 +264,7 @@ import { rewriteArticleMarkdown } from '@fullstackdatasolutions/articles/middlew
197
264
  export async function proxy(request: NextRequest) {
198
265
  const mdRewrite = rewriteArticleMarkdown(request)
199
266
  if (mdRewrite) return mdRewrite
200
-
267
+
201
268
  return NextResponse.next()
202
269
  }
203
270
 
@@ -246,7 +313,7 @@ Create `public/articles/[slug]/article.md` or `public/articles/[slug]/article.md
246
313
  ---
247
314
  title: Your Article Title
248
315
  excerpt: A one-sentence summary shown in cards and meta descriptions.
249
- author: Jane Doe
316
+ authors: [jane-doe]
250
317
  tags: [campaigns, strategy]
251
318
  ---
252
319
 
@@ -255,6 +322,34 @@ Article body in Markdown...
255
322
 
256
323
  Article directories can be nested to any depth below `public/articles`. For example, `public/articles/game-system/article-name/article.mdx` is discovered as the slug `game-system/article-name`, and `public/articles/tov/subdirectory/article-name/article.md` is discovered as `tov/subdirectory/article-name`. Use a catch-all Next.js route such as `app/articles/[...slug]/page.tsx` and join the slug segments before calling the server helpers.
257
324
 
325
+ Articles can have zero, one, or many authors:
326
+
327
+ ```markdown
328
+ ---
329
+ title: Team Update
330
+ excerpt: A post with no visible author.
331
+ authors: []
332
+ ---
333
+ ```
334
+
335
+ ```markdown
336
+ ---
337
+ title: Joint Post
338
+ excerpt: A post with multiple authors.
339
+ authors:
340
+ - jane-doe
341
+ - alex-smith
342
+ ---
343
+ ```
344
+
345
+ The legacy `author: Jane Doe` string still works. Resolution order is:
346
+
347
+ 1. `authors: [...]` frontmatter
348
+ 2. `author: ...` frontmatter
349
+ 3. `defaultAuthor` from `ArticlesConfig`
350
+
351
+ Use `authors: []` when an article should intentionally show no author, even if `defaultAuthor` is configured.
352
+
258
353
  ---
259
354
 
260
355
  ## Markdown handler exports
@@ -294,7 +389,7 @@ import { rewriteArticleMarkdown } from '@fullstackdatasolutions/articles/middlew
294
389
  export async function proxy(request: NextRequest) {
295
390
  const mdRewrite = rewriteArticleMarkdown(request)
296
391
  if (mdRewrite) return mdRewrite
297
-
392
+
298
393
  return NextResponse.next()
299
394
  }
300
395
  ```
@@ -340,26 +435,92 @@ Use this if you don't need a custom API base path. For custom paths, use `create
340
435
 
341
436
  ## `ArticlesConfig` reference
342
437
 
343
- | Field | Type | Default | Description |
344
- | ---------------------- | ----------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
345
- | `siteUrl` | `string` | — | Canonical base URL. Used in metadata and JSON-LD. |
346
- | `siteName` | `string` | — | Site name in title tags and JSON-LD. |
347
- | `pageSize` | `number` | `6` | Articles per page / per "Load more" click. |
348
- | `categoriesPageSize` | `number` | `8` | Category cards before "Load more categories". |
349
- | `layout` | `ArticlesSection[]` | see below | Ordered sections to render. Omit a key to hide it. |
350
- | `theme` | `ArticlesTheme` | — | CSS custom-property overrides for colors and fonts. |
351
- | `categoryDescriptions` | `Record<string, string \| CategoryDescription>` | — | Short/long text per category slug. |
352
- | `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
353
- | `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
354
- | `showToc` | `boolean` | `true` | Show the table of contents on article detail pages. Set to `false` to hide on all articles. |
355
- | `showBackToArticles` | `boolean` | `true` | Show the back-navigation link on article detail pages. Set to `false` to hide `ArticleBackLink`. |
356
- | `showAuthor` | `boolean` | `true` | Show author names in UI and metadata. Set to `false` to omit author display, OpenGraph authors, JSON-LD author fields, and RSS authors. |
357
- | `description` | `string` | | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
438
+ | Field | Type | Default | Description |
439
+ | ---------------------- | --------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
440
+ | `siteUrl` | `string` | — | Canonical base URL. Used in metadata and JSON-LD. |
441
+ | `siteName` | `string` | — | Site name in title tags and JSON-LD. |
442
+ | `pageSize` | `number` | `6` | Articles per page / per "Load more" click. |
443
+ | `categoriesPageSize` | `number` | `8` | Category cards before "Load more categories". |
444
+ | `layout` | `ArticlesSection[]` | see below | Ordered sections to render. Omit a key to hide it. |
445
+ | `theme` | `ArticlesTheme` | — | CSS custom-property overrides for colors and fonts. |
446
+ | `categoryDescriptions` | `Record<string, string \| CategoryDescription>` | — | Short/long text per category slug. |
447
+ | `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
448
+ | `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
449
+ | `showToc` | `boolean` | `true` | Show the table of contents on article detail pages. Set to `false` to hide on all articles. |
450
+ | `showBackToArticles` | `boolean` | `true` | Show the back-navigation link on article detail pages. Set to `false` to hide `ArticleBackLink`. |
451
+ | `showAuthor` | `boolean` | `true` | Show author names in UI and metadata. Set to `false` to omit author display, OpenGraph authors, JSON-LD author fields, and RSS authors. |
452
+ | `showAuthorPage` | `boolean` | `true` | Include configured author pages in sitemap output and enable author page helpers. Set to `false` to suppress author page generation. |
453
+ | `authors` | `Record<string, AuthorProfile>` | `{}` | Configured author profiles keyed by slug. Slugs are used by frontmatter, author URLs, and JSON-LD `Person` data. |
454
+ | `defaultAuthor` | `string` | — | Default author slug or name used when article frontmatter omits both `authors` and `author`. |
455
+ | `breadcrumbs` | `false \| BreadcrumbsConfig` | default trails | Set to `false` to hide breadcrumbs, or configure article/category/author breadcrumb trails, custom URL items, separator, schema output, and labels. |
456
+ | `linkTargetStrategy` | `'external-new-tab' \| 'all-new-tab' \| 'same-tab'` | `'external-new-tab'` | Controls article body links. Internal links open in the same window by default; external `http`/`https` links open in a new window. |
457
+ | `description` | `string` | — | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
358
458
 
359
459
  **Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
360
460
 
361
461
  To add a newsletter section, pass `layout: ['hero','search','featured','latest','categories','newsletter']` — the library renders `null` for `'newsletter'` so you can append your own component after `<ArticlesPage />`.
362
462
 
463
+ ### Authors
464
+
465
+ Author profiles are optional. If an article has no configured author and no plain `author` frontmatter, author UI and author metadata are omitted for that article.
466
+
467
+ ```ts
468
+ authors: {
469
+ 'jane-doe': {
470
+ slug: 'jane-doe',
471
+ name: 'Jane Doe',
472
+ bio: 'Jane writes about campaign operations and civic technology.',
473
+ avatar: '/authors/jane-doe.jpg',
474
+ url: 'https://example.com/team/jane-doe',
475
+ social: {
476
+ website: 'https://example.com',
477
+ facebook: 'janedoe',
478
+ twitter: '@janedoe',
479
+ x: 'janedoe',
480
+ linkedin: 'https://www.linkedin.com/in/janedoe',
481
+ instagram: 'janedoe',
482
+ youtube: '@janedoe',
483
+ tiktok: '@janedoe',
484
+ github: 'janedoe',
485
+ bluesky: 'janedoe.bsky.social',
486
+ threads: 'janedoe',
487
+ mastodon: 'https://mastodon.social/@janedoe',
488
+ medium: 'janedoe',
489
+ newsletter: 'https://example.com/newsletter',
490
+ other: {
491
+ Podcast: 'https://example.com/podcast',
492
+ },
493
+ },
494
+ },
495
+ }
496
+ ```
497
+
498
+ Only social fields with values are rendered. Handle-style values such as `twitter: '@janedoe'` and `linkedin: 'janedoe'` are normalized to full profile URLs; absolute URLs are left unchanged. `social.other` lets you add custom social or profile links with your own labels.
499
+
500
+ ### Breadcrumbs
501
+
502
+ Breadcrumbs are visible by default. Disable them with `breadcrumbs: false`, or customize an article trail:
503
+
504
+ ```ts
505
+ breadcrumbs: {
506
+ separator: '/',
507
+ showSchema: true,
508
+ article: [
509
+ { name: 'Resources', url: '/resources' },
510
+ 'primaryCategory',
511
+ 'folderPath',
512
+ { name: 'Campaign Library', url: 'https://example.com/library' },
513
+ 'articleTitle',
514
+ ],
515
+ category: ['home', { name: 'Resources', url: '/resources' }, 'category'],
516
+ author: ['home', 'articles', { name: 'Team', url: '/team' }, 'authorName'],
517
+ }
518
+ ```
519
+
520
+ Breadcrumb trails can mix built-in tokens with custom URL items shaped as `{ name: string, url: string }`. Relative custom URLs beginning with `/` are resolved against `siteUrl`; absolute `http` and `https` URLs are left unchanged. The `folderPath` article token expands the article slug folders. For `public/articles/guides/field/example/article.md`, the trail above renders `Resources`, the primary tag, `Guides`, `Field`, `Campaign Library`, and the article title.
521
+
522
+ Use `breadcrumbs: { show: false }` to disable both visible breadcrumbs and breadcrumb JSON-LD while keeping the rest of the breadcrumb config nearby. Use `showSchema: false` to keep visible breadcrumbs but omit breadcrumb JSON-LD.
523
+
363
524
  ---
364
525
 
365
526
  ## Theming
@@ -601,56 +762,14 @@ To enable RSS 2.0 feed generation at `/articles/feed.xml`, copy this route file
601
762
 
602
763
  ```ts
603
764
  // app/articles/feed.xml/route.ts
604
- import { getAllArticles } from '@fullstackdatasolutions/articles/server'
765
+ import { generateRssFeed, getAllArticles } from '@fullstackdatasolutions/articles/server'
605
766
  import { siteConfig } from '@/config/articles'
606
767
 
607
768
  export const dynamic = 'force-static'
608
769
 
609
- function escapeXml(str: string): string {
610
- return str
611
- .replaceAll('&', '&amp;')
612
- .replaceAll('<', '&lt;')
613
- .replaceAll('>', '&gt;')
614
- .replaceAll('"', '&quot;')
615
- .replaceAll("'", '&apos;')
616
- }
617
-
618
770
  export async function GET() {
619
- const siteUrl = siteConfig.siteUrl.replace(/\/$/, '')
620
771
  const articles = await getAllArticles()
621
- const showAuthor = siteConfig.showAuthor !== false
622
-
623
- const items = articles
624
- .map((article) => {
625
- const url = `${siteUrl}/articles/${article.slug}`
626
- const pubDate = article.date ? new Date(article.date).toUTCString() : ''
627
- return [
628
- ' <item>',
629
- ` <title><![CDATA[${article.title}]]></title>`,
630
- ` <link>${url}</link>`,
631
- ` <guid isPermaLink="true">${url}</guid>`,
632
- pubDate ? ` <pubDate>${pubDate}</pubDate>` : '',
633
- article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : '',
634
- showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : '',
635
- ' </item>',
636
- ]
637
- .filter(Boolean)
638
- .join('\n')
639
- })
640
- .join('\n')
641
-
642
- const description = siteConfig.description ?? `${siteConfig.siteName} articles`
643
- const xml = `<?xml version="1.0" encoding="UTF-8" ?>
644
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
645
- <channel>
646
- <title><![CDATA[${siteConfig.siteName}]]></title>
647
- <link>${siteUrl}/articles</link>
648
- <description><![CDATA[${description}]]></description>
649
- <language>en</language>
650
- <atom:link href="${siteUrl}/articles/feed.xml" rel="self" type="application/rss+xml" />
651
- ${items}
652
- </channel>
653
- </rss>`
772
+ const xml = generateRssFeed(articles, siteConfig)
654
773
 
655
774
  return new Response(xml, {
656
775
  headers: {
@@ -662,6 +781,7 @@ ${items}
662
781
  ```
663
782
 
664
783
  - Uses `force-static` for build-time generation
784
+ - Uses `generateRssFeed(articles, siteConfig)` from the package so RSS escaping, authors, descriptions, and feed URLs stay consistent across consuming apps
665
785
  - Uses `siteConfig.description` for the channel description field (falls back to `siteName` if omitted)
666
786
  - The `<link rel="alternate" type="application/rss+xml">` tag is added automatically to the articles index `<head>` via `generateArticlesIndexMetadata`
667
787
 
@@ -731,8 +851,8 @@ import { siteConfig } from '@/config/articles'
731
851
 
732
852
  export async function generateMetadata({ params }): Promise<Metadata> {
733
853
  const slug = (await params).slug.join('/')
734
- const base = (await generateArticleMetadata(slug, siteConfig))
735
- const article = await getArticleMetadata(slug)
854
+ const base = await generateArticleMetadata(slug, siteConfig)
855
+ const article = await getArticleMetadata(slug, siteConfig)
736
856
  if (!article) return base
737
857
 
738
858
  const markdownUrl = getArticleMarkdownUrl(article, siteConfig)
@@ -779,7 +899,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
779
899
 
780
900
  Returns article entries (priority 0.8, changeFrequency 'weekly') and category entries (priority 0.7, changeFrequency 'weekly'). Alternative: pass a plain URL string instead of `siteConfig`: `getArticleSitemapEntries('https://yoursite.com')`.
781
901
 
782
- JSON-LD structured data (`ArticleSchema`, `BreadcrumbSchema`, `CollectionPageSchema`) is rendered automatically inside the library components no extra wiring required.
902
+ JSON-LD structured data (`ArticleSchema`, `Breadcrumb`, `BreadcrumbSchema`, `CollectionPageSchema`) is rendered automatically inside the library components. Use `Breadcrumb` for visible navigation plus schema, or `BreadcrumbSchema` when you only need visually hidden breadcrumb schema output.
783
903
 
784
904
  ---
785
905
 
@@ -789,7 +909,8 @@ JSON-LD structured data (`ArticleSchema`, `BreadcrumbSchema`, `CollectionPageSch
789
909
  | --------------- | ---------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
790
910
  | `title` | `string` | yes | Article title. If omitted, falls back to the article slug with dashes replaced by spaces. |
791
911
  | `excerpt` | `string` | yes | One-sentence summary for cards and meta. If omitted, resolves to an empty string. |
792
- | `author` | `string` | no | Defaults to `'Andrew Blase'` |
912
+ | `author` | `string` | no | Legacy single author slug or display name. Used when `authors` is omitted. Falls back to `defaultAuthor` only when configured. |
913
+ | `authors` | `string[]` | no | Preferred multi-author list. Values should match `ArticlesConfig.authors` keys; unknown values still render as fallback author names. Use `authors: []` for an explicitly authorless article. |
793
914
  | `tags` | `string[]` | no | Used as categories. First tag = primary category. ArticleDetailHero displays at most 4 category tags; articles with more than 4 show only the first 4 in the hero. |
794
915
  | `date` | `YYYY-MM-DD` | no | Omit to publish immediately. Future dates hide until that date. |
795
916
  | `lastmod` | `YYYY-MM-DD` | no | Last modified date used by sitemap metadata when available. |
@@ -816,6 +937,7 @@ import {
816
937
  ArticleTOC,
817
938
  getAllArticles,
818
939
  getArticleMetadata,
940
+ getArticleAuthors,
819
941
  getArticleMarkdown,
820
942
  getArticleMarkdownResponse,
821
943
  getArticleMarkdownUrl,
@@ -823,6 +945,9 @@ import {
823
945
  getAiRobotsTxtRules,
824
946
  setArticlesErrorHandler,
825
947
  getArticlesByCategory,
948
+ getAuthorBySlug,
949
+ getAllAuthors,
950
+ getArticlesByAuthor,
826
951
  getAllCategories,
827
952
  getAvailableArticleSlugs,
828
953
  getAdjacentArticles,
@@ -833,10 +958,16 @@ import {
833
958
  extractToc,
834
959
  generateArticleStaticParams,
835
960
  generateCategoryStaticParams,
961
+ generateAuthorStaticParams,
836
962
  getArticleSitemapEntries,
963
+ generateRssFeed,
837
964
  generateArticlesIndexMetadata,
838
965
  generateArticleMetadata,
839
966
  generateCategoryMetadata,
967
+ generateAuthorMetadata,
968
+ buildArticleBreadcrumbs,
969
+ buildCategoryBreadcrumbs,
970
+ buildAuthorBreadcrumbs,
840
971
  } from '@fullstackdatasolutions/articles/server'
841
972
  ```
842
973