@fullstackdatasolutions/articles 0.8.2 → 0.9.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/README.md CHANGED
@@ -125,7 +125,7 @@ export async function generateMetadata({ params }: ArticlePageProps) {
125
125
  export default async function Page({ params }: ArticlePageProps) {
126
126
  const { slug: slugSegments } = await params
127
127
  const slug = normalizeSlug(slugSegments)
128
- const article = await getArticleMetadata(slug)
128
+ const article = await getArticleMetadata(slug, siteConfig)
129
129
  if (!article) notFound()
130
130
  const { previous, next } = await getAdjacentArticles(slug)
131
131
  const siteUrl = siteConfig.siteUrl.replace(/\/$/, '')
@@ -144,7 +144,7 @@ export default async function Page({ params }: ArticlePageProps) {
144
144
  {siteConfig.showToc !== false && article.toc && article.toc.length > 0 && (
145
145
  <ArticleTOC toc={article.toc} />
146
146
  )}
147
- <ArticleContent article={article} />
147
+ <ArticleContent article={article} config={siteConfig} />
148
148
  <ArticleSocialShare title={article.title} url={articleUrl} excerpt={article.excerpt} />
149
149
  {siteConfig.comments && <CommentsSection articleSlug={slug} config={siteConfig.comments} />}
150
150
  <ArticleNavigation previous={previous} next={next} basePath="/articles" />
@@ -197,7 +197,7 @@ import { rewriteArticleMarkdown } from '@fullstackdatasolutions/articles/middlew
197
197
  export async function proxy(request: NextRequest) {
198
198
  const mdRewrite = rewriteArticleMarkdown(request)
199
199
  if (mdRewrite) return mdRewrite
200
-
200
+
201
201
  return NextResponse.next()
202
202
  }
203
203
 
@@ -294,7 +294,7 @@ import { rewriteArticleMarkdown } from '@fullstackdatasolutions/articles/middlew
294
294
  export async function proxy(request: NextRequest) {
295
295
  const mdRewrite = rewriteArticleMarkdown(request)
296
296
  if (mdRewrite) return mdRewrite
297
-
297
+
298
298
  return NextResponse.next()
299
299
  }
300
300
  ```
@@ -340,21 +340,22 @@ Use this if you don't need a custom API base path. For custom paths, use `create
340
340
 
341
341
  ## `ArticlesConfig` reference
342
342
 
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. |
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
+ | `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. |
358
+ | `description` | `string` | — | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
358
359
 
359
360
  **Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
360
361
 
@@ -601,56 +602,14 @@ To enable RSS 2.0 feed generation at `/articles/feed.xml`, copy this route file
601
602
 
602
603
  ```ts
603
604
  // app/articles/feed.xml/route.ts
604
- import { getAllArticles } from '@fullstackdatasolutions/articles/server'
605
+ import { generateRssFeed, getAllArticles } from '@fullstackdatasolutions/articles/server'
605
606
  import { siteConfig } from '@/config/articles'
606
607
 
607
608
  export const dynamic = 'force-static'
608
609
 
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
610
  export async function GET() {
619
- const siteUrl = siteConfig.siteUrl.replace(/\/$/, '')
620
611
  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>`
612
+ const xml = generateRssFeed(articles, siteConfig)
654
613
 
655
614
  return new Response(xml, {
656
615
  headers: {
@@ -662,6 +621,7 @@ ${items}
662
621
  ```
663
622
 
664
623
  - Uses `force-static` for build-time generation
624
+ - Uses `generateRssFeed(articles, siteConfig)` from the package so RSS escaping, authors, descriptions, and feed URLs stay consistent across consuming apps
665
625
  - Uses `siteConfig.description` for the channel description field (falls back to `siteName` if omitted)
666
626
  - The `<link rel="alternate" type="application/rss+xml">` tag is added automatically to the articles index `<head>` via `generateArticlesIndexMetadata`
667
627
 
@@ -731,8 +691,8 @@ import { siteConfig } from '@/config/articles'
731
691
 
732
692
  export async function generateMetadata({ params }): Promise<Metadata> {
733
693
  const slug = (await params).slug.join('/')
734
- const base = (await generateArticleMetadata(slug, siteConfig))
735
- const article = await getArticleMetadata(slug)
694
+ const base = await generateArticleMetadata(slug, siteConfig)
695
+ const article = await getArticleMetadata(slug, siteConfig)
736
696
  if (!article) return base
737
697
 
738
698
  const markdownUrl = getArticleMarkdownUrl(article, siteConfig)
@@ -834,6 +794,7 @@ import {
834
794
  generateArticleStaticParams,
835
795
  generateCategoryStaticParams,
836
796
  getArticleSitemapEntries,
797
+ generateRssFeed,
837
798
  generateArticlesIndexMetadata,
838
799
  generateArticleMetadata,
839
800
  generateCategoryMetadata,