@fullstackdatasolutions/articles 0.8.1 → 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 +233 -72
- 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-middleware.cjs +61 -0
- package/dist/nextjs-middleware.cjs.map +1 -0
- package/dist/nextjs-middleware.d.cts +25 -0
- package/dist/nextjs-middleware.d.ts +25 -0
- package/dist/nextjs-middleware.js +33 -0
- package/dist/nextjs-middleware.js.map +1 -0
- package/dist/nextjs.cjs +692 -0
- package/dist/nextjs.cjs.map +1 -0
- package/dist/nextjs.d.cts +116 -0
- package/dist/nextjs.d.ts +116 -0
- package/dist/nextjs.js +658 -0
- package/dist/nextjs.js.map +1 -0
- 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 +11 -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__/nextjs-middleware.test.ts +183 -0
- package/src/__tests__/nextjs.test.ts +137 -0
- 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/nextjs-middleware.ts +48 -0
- package/src/nextjs.ts +27 -0
- 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" />
|
|
@@ -175,7 +175,57 @@ export default async function Page({ params }: CategoryPageProps) {
|
|
|
175
175
|
}
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
### 6.
|
|
178
|
+
### 6. Set up markdown handler (Next.js 16+)
|
|
179
|
+
|
|
180
|
+
Create an API route to serve article markdown at `/api/articles-markdown/[...slug]`:
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
// app/api/articles-markdown/[...slug]/route.ts
|
|
184
|
+
import { createArticleMarkdownHandler } from '@fullstackdatasolutions/articles/nextjs'
|
|
185
|
+
import { siteConfig } from '@/config/articles'
|
|
186
|
+
|
|
187
|
+
export const { GET } = createArticleMarkdownHandler(siteConfig)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Then wire the markdown rewrite into your proxy.ts (Next.js 16+ edge proxy):
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// proxy.ts
|
|
194
|
+
import { NextResponse, NextRequest } from 'next/server'
|
|
195
|
+
import { rewriteArticleMarkdown } from '@fullstackdatasolutions/articles/middleware'
|
|
196
|
+
|
|
197
|
+
export async function proxy(request: NextRequest) {
|
|
198
|
+
const mdRewrite = rewriteArticleMarkdown(request)
|
|
199
|
+
if (mdRewrite) return mdRewrite
|
|
200
|
+
|
|
201
|
+
return NextResponse.next()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const config = {
|
|
205
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This pattern rewrites requests from `/articles/slug.md` to `/api/articles-markdown/slug` where the handler serves the markdown. See the AI markdown twins section below for details on how to expose markdown and configure AI crawling rules.
|
|
210
|
+
|
|
211
|
+
For **Next.js 14/15**, use the middleware.ts pattern below instead.
|
|
212
|
+
|
|
213
|
+
### 6b. Set up middleware (Next.js 14/15 only)
|
|
214
|
+
|
|
215
|
+
If using Next.js 14 or 15 and cannot use a proxy, create middleware.ts to rewrite markdown requests:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// middleware.ts
|
|
219
|
+
import { NextRequest } from 'next/server'
|
|
220
|
+
import { middleware, config } from '@fullstackdatasolutions/articles/middleware'
|
|
221
|
+
|
|
222
|
+
export { config }
|
|
223
|
+
export default middleware
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
This pattern does NOT work in Next.js 16 (use the proxy.ts pattern instead). You still need the handler route above to actually serve the markdown.
|
|
227
|
+
|
|
228
|
+
### 6c. Copy the comments API routes
|
|
179
229
|
|
|
180
230
|
These cannot live inside the package — Next.js file-based routing requires them in your app. Copy these files from the reference implementation:
|
|
181
231
|
|
|
@@ -207,23 +257,105 @@ Article directories can be nested to any depth below `public/articles`. For exam
|
|
|
207
257
|
|
|
208
258
|
---
|
|
209
259
|
|
|
260
|
+
## Markdown handler exports
|
|
261
|
+
|
|
262
|
+
### `./nextjs` — Article markdown handler factory
|
|
263
|
+
|
|
264
|
+
Import from `@fullstackdatasolutions/articles/nextjs`. Node.js runtime only (use in API routes, not Edge middleware).
|
|
265
|
+
|
|
266
|
+
#### `createArticleMarkdownHandler(config)`
|
|
267
|
+
|
|
268
|
+
Factory function that returns a `{ GET }` handler for serving article markdown.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { createArticleMarkdownHandler } from '@fullstackdatasolutions/articles/nextjs'
|
|
272
|
+
import { siteConfig } from '@/config/articles'
|
|
273
|
+
|
|
274
|
+
export const { GET } = createArticleMarkdownHandler(siteConfig)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- **config** - `ArticlesConfig` instance (required). Must include `siteUrl` and `siteName` at minimum.
|
|
278
|
+
- **returns** - Object with `GET` async function matching Next.js Route Handler signature.
|
|
279
|
+
- The handler extracts slug from `params['slug']` and strips `.md` extension if present.
|
|
280
|
+
- Uses `getArticleMarkdownResponse` from the server utilities to fetch and return markdown.
|
|
281
|
+
- Only returns markdown for articles with `aiCrawl: true` in frontmatter (returns 404 otherwise).
|
|
282
|
+
|
|
283
|
+
### `./middleware` — Article markdown rewrite helpers
|
|
284
|
+
|
|
285
|
+
Import from `@fullstackdatasolutions/articles/middleware`. Edge runtime safe (no Node.js built-ins).
|
|
286
|
+
|
|
287
|
+
#### `rewriteArticleMarkdown(request, apiBase?)`
|
|
288
|
+
|
|
289
|
+
Rewrites `/articles/*.md` requests to your markdown handler route.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { rewriteArticleMarkdown } from '@fullstackdatasolutions/articles/middleware'
|
|
293
|
+
|
|
294
|
+
export async function proxy(request: NextRequest) {
|
|
295
|
+
const mdRewrite = rewriteArticleMarkdown(request)
|
|
296
|
+
if (mdRewrite) return mdRewrite
|
|
297
|
+
|
|
298
|
+
return NextResponse.next()
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- **request** - `NextRequest` object (required).
|
|
303
|
+
- **apiBase** - Base path to your markdown handler. Default: `'/api/articles-markdown'`
|
|
304
|
+
- **returns** - `NextResponse` if request matches `/articles/...md`, `undefined` otherwise.
|
|
305
|
+
- Intended use: call in proxy.ts (Next.js 16+) or middleware.ts. If result is truthy, return it; otherwise continue processing.
|
|
306
|
+
- Example: request to `/articles/my-post.md` rewrites to `/api/articles-markdown/my-post`.
|
|
307
|
+
|
|
308
|
+
#### `createArticleMarkdownMiddleware(apiBase?)`
|
|
309
|
+
|
|
310
|
+
Factory for building custom middleware/proxy with a custom API base path.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { createArticleMarkdownMiddleware } from '@fullstackdatasolutions/articles/middleware'
|
|
314
|
+
|
|
315
|
+
const { middleware, config } = createArticleMarkdownMiddleware('/api/my-custom-path')
|
|
316
|
+
|
|
317
|
+
export { config }
|
|
318
|
+
export default middleware
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- **apiBase** - Base path to your markdown handler. Default: `'/api/articles-markdown'`
|
|
322
|
+
- **returns** - Object with `{ middleware, config }` ready to export from middleware.ts.
|
|
323
|
+
- `config.matcher` automatically targets `/articles/:path*.md`.
|
|
324
|
+
|
|
325
|
+
#### `middleware` (default export)
|
|
326
|
+
|
|
327
|
+
Pre-built middleware using the default API base (`'/api/articles-markdown'`).
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
// middleware.ts
|
|
331
|
+
import { middleware, config } from '@fullstackdatasolutions/articles/middleware'
|
|
332
|
+
|
|
333
|
+
export { config }
|
|
334
|
+
export default middleware
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Use this if you don't need a custom API base path. For custom paths, use `createArticleMarkdownMiddleware()` instead.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
210
341
|
## `ArticlesConfig` reference
|
|
211
342
|
|
|
212
|
-
| Field | Type
|
|
213
|
-
| ---------------------- |
|
|
214
|
-
| `siteUrl` | `string`
|
|
215
|
-
| `siteName` | `string`
|
|
216
|
-
| `pageSize` | `number`
|
|
217
|
-
| `categoriesPageSize` | `number`
|
|
218
|
-
| `layout` | `ArticlesSection[]`
|
|
219
|
-
| `theme` | `ArticlesTheme`
|
|
220
|
-
| `categoryDescriptions` | `Record<string, string \| CategoryDescription>`
|
|
221
|
-
| `hero` | `HeroConfig`
|
|
222
|
-
| `comments` | `CommentsConfig`
|
|
223
|
-
| `showToc` | `boolean`
|
|
224
|
-
| `showBackToArticles` | `boolean`
|
|
225
|
-
| `showAuthor` | `boolean`
|
|
226
|
-
| `
|
|
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. |
|
|
227
359
|
|
|
228
360
|
**Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
|
|
229
361
|
|
|
@@ -470,56 +602,14 @@ To enable RSS 2.0 feed generation at `/articles/feed.xml`, copy this route file
|
|
|
470
602
|
|
|
471
603
|
```ts
|
|
472
604
|
// app/articles/feed.xml/route.ts
|
|
473
|
-
import { getAllArticles } from '@fullstackdatasolutions/articles/server'
|
|
605
|
+
import { generateRssFeed, getAllArticles } from '@fullstackdatasolutions/articles/server'
|
|
474
606
|
import { siteConfig } from '@/config/articles'
|
|
475
607
|
|
|
476
608
|
export const dynamic = 'force-static'
|
|
477
609
|
|
|
478
|
-
function escapeXml(str: string): string {
|
|
479
|
-
return str
|
|
480
|
-
.replaceAll('&', '&')
|
|
481
|
-
.replaceAll('<', '<')
|
|
482
|
-
.replaceAll('>', '>')
|
|
483
|
-
.replaceAll('"', '"')
|
|
484
|
-
.replaceAll("'", ''')
|
|
485
|
-
}
|
|
486
|
-
|
|
487
610
|
export async function GET() {
|
|
488
|
-
const siteUrl = siteConfig.siteUrl.replace(/\/$/, '')
|
|
489
611
|
const articles = await getAllArticles()
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
const items = articles
|
|
493
|
-
.map((article) => {
|
|
494
|
-
const url = `${siteUrl}/articles/${article.slug}`
|
|
495
|
-
const pubDate = article.date ? new Date(article.date).toUTCString() : ''
|
|
496
|
-
return [
|
|
497
|
-
' <item>',
|
|
498
|
-
` <title><![CDATA[${article.title}]]></title>`,
|
|
499
|
-
` <link>${url}</link>`,
|
|
500
|
-
` <guid isPermaLink="true">${url}</guid>`,
|
|
501
|
-
pubDate ? ` <pubDate>${pubDate}</pubDate>` : '',
|
|
502
|
-
article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : '',
|
|
503
|
-
showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : '',
|
|
504
|
-
' </item>',
|
|
505
|
-
]
|
|
506
|
-
.filter(Boolean)
|
|
507
|
-
.join('\n')
|
|
508
|
-
})
|
|
509
|
-
.join('\n')
|
|
510
|
-
|
|
511
|
-
const description = siteConfig.description ?? `${siteConfig.siteName} articles`
|
|
512
|
-
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
513
|
-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
514
|
-
<channel>
|
|
515
|
-
<title><![CDATA[${siteConfig.siteName}]]></title>
|
|
516
|
-
<link>${siteUrl}/articles</link>
|
|
517
|
-
<description><![CDATA[${description}]]></description>
|
|
518
|
-
<language>en</language>
|
|
519
|
-
<atom:link href="${siteUrl}/articles/feed.xml" rel="self" type="application/rss+xml" />
|
|
520
|
-
${items}
|
|
521
|
-
</channel>
|
|
522
|
-
</rss>`
|
|
612
|
+
const xml = generateRssFeed(articles, siteConfig)
|
|
523
613
|
|
|
524
614
|
return new Response(xml, {
|
|
525
615
|
headers: {
|
|
@@ -531,6 +621,7 @@ ${items}
|
|
|
531
621
|
```
|
|
532
622
|
|
|
533
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
|
|
534
625
|
- Uses `siteConfig.description` for the channel description field (falls back to `siteName` if omitted)
|
|
535
626
|
- The `<link rel="alternate" type="application/rss+xml">` tag is added automatically to the articles index `<head>` via `generateArticlesIndexMetadata`
|
|
536
627
|
|
|
@@ -548,24 +639,82 @@ aiCrawl: true
|
|
|
548
639
|
Markdown body...
|
|
549
640
|
```
|
|
550
641
|
|
|
551
|
-
|
|
642
|
+
To expose markdown at public URLs like `/articles/my-post.md`, use the markdown handler factories above:
|
|
643
|
+
|
|
644
|
+
- **Next.js 16+** — Use `createArticleMarkdownHandler()` + `rewriteArticleMarkdown()` in proxy.ts (see Step 6 above).
|
|
645
|
+
- **Next.js 14/15** — Use `createArticleMarkdownHandler()` + middleware.ts with `middleware` export from the package (see Step 6b above).
|
|
646
|
+
|
|
647
|
+
For static generation, call `getArticleMarkdown(slug)` during your build and write the returned content beside your generated HTML; it returns `null` unless `aiCrawl: true`.
|
|
648
|
+
|
|
649
|
+
**robots.txt - block AI crawlers from non-aiCrawl articles**
|
|
650
|
+
|
|
651
|
+
Wire `getAiRobotsTxtRules()` into your robots.txt route. It returns disallow rules for known AI crawler agents (GPTBot, Claude-Web, etc.) targeting any article that does not have `aiCrawl: true`. Without this step, AI crawlers can still reach all articles regardless of the frontmatter flag.
|
|
552
652
|
|
|
553
653
|
```ts
|
|
554
|
-
// app/
|
|
555
|
-
import {
|
|
556
|
-
import {
|
|
654
|
+
// app/robots.txt/route.ts
|
|
655
|
+
import { NextResponse } from 'next/server'
|
|
656
|
+
import { getAiRobotsTxtRules } from '@fullstackdatasolutions/articles/server'
|
|
557
657
|
|
|
558
|
-
|
|
658
|
+
export async function GET() {
|
|
659
|
+
const siteUrl = 'https://yoursite.com'
|
|
660
|
+
const aiRules = await getAiRobotsTxtRules()
|
|
559
661
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
662
|
+
const robotsTxt = `User-agent: *
|
|
663
|
+
Allow: /
|
|
664
|
+
|
|
665
|
+
Sitemap: ${siteUrl}/sitemap.xml${aiRules ? `\n\n${aiRules}` : ''}`
|
|
666
|
+
|
|
667
|
+
return new NextResponse(robotsTxt, {
|
|
668
|
+
headers: {
|
|
669
|
+
'Content-Type': 'text/plain',
|
|
670
|
+
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
|
|
671
|
+
},
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
`getAiRobotsTxtRules()` returns a string of `User-agent` / `Disallow` blocks, one per known AI crawler, or `null` if every article is opted in. The template literal appends it only when non-null so no blank lines appear when it is unused.
|
|
677
|
+
|
|
678
|
+
**article page metadata - emit alternate link and noai directive**
|
|
679
|
+
|
|
680
|
+
`generateArticleMetadata` handles the `text/markdown` alternate link automatically when `aiCrawl: true`. If you override `generateMetadata` and build the metadata object yourself, cast the return value as `Metadata` before spreading so TypeScript accepts it:
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
// app/articles/[...slug]/page.tsx
|
|
684
|
+
import type { Metadata } from 'next'
|
|
685
|
+
import {
|
|
686
|
+
generateArticleMetadata,
|
|
687
|
+
getArticleMetadata,
|
|
688
|
+
getArticleMarkdownUrl,
|
|
689
|
+
} from '@fullstackdatasolutions/articles/server'
|
|
690
|
+
import { siteConfig } from '@/config/articles'
|
|
691
|
+
|
|
692
|
+
export async function generateMetadata({ params }): Promise<Metadata> {
|
|
693
|
+
const slug = (await params).slug.join('/')
|
|
694
|
+
const base = await generateArticleMetadata(slug, siteConfig)
|
|
695
|
+
const article = await getArticleMetadata(slug, siteConfig)
|
|
696
|
+
if (!article) return base
|
|
697
|
+
|
|
698
|
+
const markdownUrl = getArticleMarkdownUrl(article, siteConfig)
|
|
699
|
+
if (markdownUrl) {
|
|
700
|
+
return {
|
|
701
|
+
...base,
|
|
702
|
+
alternates: {
|
|
703
|
+
...(base.alternates as Record<string, unknown>),
|
|
704
|
+
types: { 'text/markdown': markdownUrl },
|
|
705
|
+
},
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
...base,
|
|
710
|
+
robots: 'index, follow, noai, noimageai',
|
|
711
|
+
}
|
|
563
712
|
}
|
|
564
713
|
```
|
|
565
714
|
|
|
566
|
-
|
|
715
|
+
Do **not** use `headers()` from `next/headers` to set response headers in a Server Component page - it is read-only in the App Router (reads request headers). Use `generateMetadata` instead.
|
|
567
716
|
|
|
568
|
-
Use `getArticleAiHeaders(article, siteConfig)` in
|
|
717
|
+
Use `getArticleAiHeaders(article, siteConfig)` only in a Route Handler (not a page), where you control the `Response` object directly. Opted-in articles receive a markdown `Link` header; all others receive `X-Robots-Tag: noai, noimageai`.
|
|
569
718
|
|
|
570
719
|
### Sitemaps
|
|
571
720
|
|
|
@@ -645,6 +794,7 @@ import {
|
|
|
645
794
|
generateArticleStaticParams,
|
|
646
795
|
generateCategoryStaticParams,
|
|
647
796
|
getArticleSitemapEntries,
|
|
797
|
+
generateRssFeed,
|
|
648
798
|
generateArticlesIndexMetadata,
|
|
649
799
|
generateArticleMetadata,
|
|
650
800
|
generateCategoryMetadata,
|
|
@@ -739,6 +889,7 @@ For sitemaps and metadata: `import { getArticleSitemapEntries, generateArticlesI
|
|
|
739
889
|
- `hero.title` and `hero.description` in `ArticlesConfig` control the hero heading — pass them to customize per app.
|
|
740
890
|
- `showToc?: boolean` (default `true`) controls TOC visibility on all article detail pages.
|
|
741
891
|
- `description?: string` sets the RSS feed channel description (falls back to `siteName`).
|
|
892
|
+
- `aiCrawl: true` in article frontmatter opts that article into AI indexing. Wire up three things: (1) `getAiRobotsTxtRules()` in `app/robots.txt/route.ts` appended to the robots.txt string; (2) a `app/articles/[slug].md/route.ts` Route Handler calling `getArticleMarkdownResponse`; (3) `generateMetadata` in the article page casting the base metadata as `Metadata` and spreading it before adding `alternates.types` or `robots`. Do NOT use `next/headers` `headers()` to set response headers in a page - it is read-only.
|
|
742
893
|
- Full library docs: `node_modules/@fullstackdatasolutions/articles/README.md`
|
|
743
894
|
```
|
|
744
895
|
|
|
@@ -757,6 +908,16 @@ GitHub Actions runs typecheck → test → build → `npm publish --access publi
|
|
|
757
908
|
|
|
758
909
|
---
|
|
759
910
|
|
|
911
|
+
## Examples
|
|
912
|
+
|
|
913
|
+
See this package in use at:
|
|
914
|
+
|
|
915
|
+
- [voxpopulus.us/articles](https://voxpopulus.us/articles/)
|
|
916
|
+
- [loopvolt.io/articles](https://loopvolt.io/articles)
|
|
917
|
+
- [theroleplayersguild.com/articles](https://theroleplayersguild.com/articles)
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
760
921
|
## Upgrading
|
|
761
922
|
|
|
762
923
|
Follow semver. Breaking changes (major) include a migration note in the changelog.
|