@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 +26 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +74 -34
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +4 -0
- package/dist/nextjs.d.ts +4 -0
- package/dist/nextjs.js +74 -34
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +117 -38
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +10 -4
- package/dist/server.d.ts +10 -4
- package/dist/server.js +116 -38
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/ArticleContent.tsx +8 -3
- package/src/__tests__/ArticleContent.test.tsx +18 -2
- package/src/__tests__/markdown.test.ts +79 -3
- package/src/__tests__/renderMdx.test.tsx +22 -0
- package/src/__tests__/seoUtils.test.ts +102 -0
- package/src/__tests__/server-articles.test.ts +15 -1
- package/src/articlesConfig.ts +5 -0
- package/src/index.ts +1 -0
- package/src/markdown.ts +67 -8
- package/src/renderMdx.tsx +3 -2
- package/src/seoUtils.ts +54 -0
- package/src/server-articles.ts +27 -25
- package/src/server.ts +2 -1
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
|
|
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
|
-
| `
|
|
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('&', '&')
|
|
612
|
-
.replaceAll('<', '<')
|
|
613
|
-
.replaceAll('>', '>')
|
|
614
|
-
.replaceAll('"', '"')
|
|
615
|
-
.replaceAll("'", ''')
|
|
616
|
-
}
|
|
617
|
-
|
|
618
610
|
export async function GET() {
|
|
619
|
-
const siteUrl = siteConfig.siteUrl.replace(/\/$/, '')
|
|
620
611
|
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>`
|
|
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 =
|
|
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,
|