@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.
- package/CHANGELOG.md +237 -0
- package/README.md +209 -78
- package/dist/index.cjs +635 -274
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +164 -56
- package/dist/index.d.ts +164 -56
- package/dist/index.js +614 -250
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +113 -38
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +71 -0
- package/dist/nextjs.d.ts +71 -0
- package/dist/nextjs.js +113 -38
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +394 -52
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +96 -6
- package/dist/server.d.ts +96 -6
- package/dist/server.js +382 -52
- package/dist/server.js.map +1 -1
- package/package.json +8 -5
- package/src/ArticleContent.tsx +8 -3
- package/src/ArticleDetailHero.tsx +27 -2
- package/src/ArticleSchemas.tsx +27 -27
- package/src/AuthorArticlesPage.tsx +60 -0
- package/src/AuthorCard.tsx +112 -0
- package/src/AuthorDetailHero.tsx +56 -0
- package/src/Breadcrumb.tsx +78 -0
- package/src/CategoryArticlesPage.tsx +62 -11
- package/src/__tests__/ArticleContent.test.tsx +18 -2
- package/src/__tests__/ArticleDetailHero.test.tsx +21 -1
- package/src/__tests__/ArticleSchemas.test.tsx +47 -2
- package/src/__tests__/AuthorArticlesPage.test.tsx +74 -0
- package/src/__tests__/AuthorCard.test.tsx +98 -0
- package/src/__tests__/AuthorDetailHero.test.tsx +51 -0
- package/src/__tests__/CategoryArticlesPage.test.tsx +31 -5
- package/src/__tests__/authorUtils.test.ts +89 -0
- package/src/__tests__/markdown.test.ts +79 -3
- package/src/__tests__/renderMdx.test.tsx +57 -0
- package/src/__tests__/seoUtils-authors.test.ts +160 -0
- package/src/__tests__/seoUtils.test.ts +106 -0
- package/src/__tests__/server-articles.test.ts +174 -3
- package/src/articleTypes.ts +33 -0
- package/src/articlesConfig.ts +67 -0
- package/src/authorUtils.ts +95 -0
- package/src/index.ts +32 -9
- package/src/markdown.ts +67 -8
- package/src/renderMdx.tsx +6 -3
- package/src/seoUtils.ts +279 -6
- package/src/server-articles.ts +124 -34
- 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
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
344
|
-
| ---------------------- |
|
|
345
|
-
| `siteUrl` | `string`
|
|
346
|
-
| `siteName` | `string`
|
|
347
|
-
| `pageSize` | `number`
|
|
348
|
-
| `categoriesPageSize` | `number`
|
|
349
|
-
| `layout` | `ArticlesSection[]`
|
|
350
|
-
| `theme` | `ArticlesTheme`
|
|
351
|
-
| `categoryDescriptions` | `Record<string, string \| CategoryDescription>`
|
|
352
|
-
| `hero` | `HeroConfig`
|
|
353
|
-
| `comments` | `CommentsConfig`
|
|
354
|
-
| `showToc` | `boolean`
|
|
355
|
-
| `showBackToArticles` | `boolean`
|
|
356
|
-
| `showAuthor` | `boolean`
|
|
357
|
-
| `
|
|
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('&', '&')
|
|
612
|
-
.replaceAll('<', '<')
|
|
613
|
-
.replaceAll('>', '>')
|
|
614
|
-
.replaceAll('"', '"')
|
|
615
|
-
.replaceAll("'", ''')
|
|
616
|
-
}
|
|
617
|
-
|
|
618
770
|
export async function GET() {
|
|
619
|
-
const siteUrl = siteConfig.siteUrl.replace(/\/$/, '')
|
|
620
771
|
const articles = await getAllArticles()
|
|
621
|
-
const
|
|
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 =
|
|
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
|
|
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 |
|
|
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
|
|