@fullstackdatasolutions/articles 0.7.2 → 0.8.1
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 +139 -76
- package/dist/index.cjs +30 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +30 -13
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +180 -25
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +33 -14
- package/dist/server.d.ts +33 -14
- package/dist/server.js +173 -24
- package/dist/server.js.map +1 -1
- package/package.json +16 -3
- package/src/ArticleDetailHero.tsx +8 -4
- package/src/ArticleSchemas.tsx +17 -4
- package/src/ArticlesPage.tsx +5 -1
- package/src/FeaturedArticle.tsx +8 -5
- package/src/__tests__/ArticleDetailHero.test.tsx +12 -0
- package/src/__tests__/ArticleSchemas.test.tsx +28 -0
- package/src/__tests__/FeaturedArticle.test.tsx +5 -0
- package/src/__tests__/errorReporting.test.ts +89 -0
- package/src/__tests__/seoUtils.test.ts +38 -0
- package/src/__tests__/server-articles.test.ts +194 -0
- package/src/articleTypes.ts +1 -0
- package/src/articlesConfig.ts +2 -0
- package/src/errorReporting.ts +34 -0
- package/src/markdown.ts +27 -5
- package/src/seoUtils.ts +10 -2
- package/src/server-articles.ts +124 -9
- package/src/server.ts +12 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ In Tailwind CSS v4, you add a `@source` directive to your global CSS file — **
|
|
|
28
28
|
|
|
29
29
|
```css
|
|
30
30
|
/* app/globals.css */
|
|
31
|
-
@import
|
|
31
|
+
@import 'tailwindcss';
|
|
32
32
|
@plugin "@tailwindcss/typography";
|
|
33
33
|
|
|
34
34
|
@source "./node_modules/@fullstackdatasolutions/articles/src";
|
|
@@ -53,8 +53,12 @@ If you're using TypeScript and want IDE autocomplete without a pre-built `dist/`
|
|
|
53
53
|
{
|
|
54
54
|
"compilerOptions": {
|
|
55
55
|
"paths": {
|
|
56
|
-
"@fullstackdatasolutions/articles": [
|
|
57
|
-
|
|
56
|
+
"@fullstackdatasolutions/articles": [
|
|
57
|
+
"./node_modules/@fullstackdatasolutions/articles/src/index.ts"
|
|
58
|
+
],
|
|
59
|
+
"@fullstackdatasolutions/articles/server": [
|
|
60
|
+
"./node_modules/@fullstackdatasolutions/articles/src/server.ts"
|
|
61
|
+
]
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
}
|
|
@@ -67,7 +71,7 @@ If you're using TypeScript and want IDE autocomplete without a pre-built `dist/`
|
|
|
67
71
|
import type { ArticlesConfig } from '@fullstackdatasolutions/articles'
|
|
68
72
|
|
|
69
73
|
export const siteConfig: ArticlesConfig = {
|
|
70
|
-
siteUrl:
|
|
74
|
+
siteUrl: 'https://yoursite.com',
|
|
71
75
|
siteName: 'Your Site',
|
|
72
76
|
pageSize: 6,
|
|
73
77
|
}
|
|
@@ -152,7 +156,10 @@ export default async function Page({ params }: ArticlePageProps) {
|
|
|
152
156
|
// app/articles/category/[category]/page.tsx
|
|
153
157
|
import { notFound } from 'next/navigation'
|
|
154
158
|
import { CategoryArticlesPage } from '@fullstackdatasolutions/articles'
|
|
155
|
-
import {
|
|
159
|
+
import {
|
|
160
|
+
generateCategoryMetadata,
|
|
161
|
+
getArticlesByCategory,
|
|
162
|
+
} from '@fullstackdatasolutions/articles/server'
|
|
156
163
|
import { siteConfig } from '@/config/articles'
|
|
157
164
|
export { generateCategoryStaticParams as generateStaticParams } from '@fullstackdatasolutions/articles/server'
|
|
158
165
|
type CategoryPageProps = Readonly<{ params: Promise<{ category: string }> }>
|
|
@@ -202,20 +209,21 @@ Article directories can be nested to any depth below `public/articles`. For exam
|
|
|
202
209
|
|
|
203
210
|
## `ArticlesConfig` reference
|
|
204
211
|
|
|
205
|
-
| Field
|
|
206
|
-
|
|
207
|
-
| `siteUrl`
|
|
208
|
-
| `siteName`
|
|
209
|
-
| `pageSize`
|
|
210
|
-
| `categoriesPageSize`
|
|
211
|
-
| `layout`
|
|
212
|
-
| `theme`
|
|
213
|
-
| `categoryDescriptions` | `Record<string, string \| CategoryDescription>` | —
|
|
214
|
-
| `hero`
|
|
215
|
-
| `comments`
|
|
216
|
-
| `showToc`
|
|
217
|
-
| `showBackToArticles`
|
|
218
|
-
| `
|
|
212
|
+
| Field | Type | Default | Description |
|
|
213
|
+
| ---------------------- | ----------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
214
|
+
| `siteUrl` | `string` | — | Canonical base URL. Used in metadata and JSON-LD. |
|
|
215
|
+
| `siteName` | `string` | — | Site name in title tags and JSON-LD. |
|
|
216
|
+
| `pageSize` | `number` | `6` | Articles per page / per "Load more" click. |
|
|
217
|
+
| `categoriesPageSize` | `number` | `8` | Category cards before "Load more categories". |
|
|
218
|
+
| `layout` | `ArticlesSection[]` | see below | Ordered sections to render. Omit a key to hide it. |
|
|
219
|
+
| `theme` | `ArticlesTheme` | — | CSS custom-property overrides for colors and fonts. |
|
|
220
|
+
| `categoryDescriptions` | `Record<string, string \| CategoryDescription>` | — | Short/long text per category slug. |
|
|
221
|
+
| `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
|
|
222
|
+
| `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
|
|
223
|
+
| `showToc` | `boolean` | `true` | Show the table of contents on article detail pages. Set to `false` to hide on all articles. |
|
|
224
|
+
| `showBackToArticles` | `boolean` | `true` | Show the back-navigation link on article detail pages. Set to `false` to hide `ArticleBackLink`. |
|
|
225
|
+
| `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. |
|
|
226
|
+
| `description` | `string` | — | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
|
|
219
227
|
|
|
220
228
|
**Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
|
|
221
229
|
|
|
@@ -262,23 +270,19 @@ hero: {
|
|
|
262
270
|
|
|
263
271
|
```tsx
|
|
264
272
|
import { ArticleDetailHero } from '@fullstackdatasolutions/articles'
|
|
265
|
-
|
|
266
|
-
<ArticleDetailHero
|
|
267
|
-
article={article}
|
|
268
|
-
categoryBasePath="/articles/category"
|
|
269
|
-
/>
|
|
273
|
+
;<ArticleDetailHero article={article} categoryBasePath="/articles/category" />
|
|
270
274
|
```
|
|
271
275
|
|
|
272
|
-
| Prop
|
|
273
|
-
|
|
274
|
-
| `article`
|
|
275
|
-
| `categoryBasePath` | `string`
|
|
276
|
-
| `showDate`
|
|
276
|
+
| Prop | Type | Required | Description |
|
|
277
|
+
| ------------------ | --------- | -------- | --------------------------------------------------------------------------------------------------------------- |
|
|
278
|
+
| `article` | `Article` | yes | Article object returned by `getArticleMetadata` |
|
|
279
|
+
| `categoryBasePath` | `string` | no | Base path for category links. Defaults to `"/articles/category"`. Pass `""` to render categories as plain text. |
|
|
280
|
+
| `showDate` | `boolean` | no | Show the article date. Defaults to `false` (hidden). |
|
|
277
281
|
|
|
278
282
|
- Category tags are capped at 4. When an article has more than 4 categories, only the first 4 are shown.
|
|
279
283
|
- The date field is hidden by default. Pass `showDate={true}` to display it.
|
|
280
284
|
- All layout styles are inline so the component renders correctly regardless of whether `@source` is configured for the package.
|
|
281
|
-
|
|
285
|
+
- Hero image / categories behave automatically once you use this component; apps do not need to reimplement the hero section for those features.
|
|
282
286
|
|
|
283
287
|
---
|
|
284
288
|
|
|
@@ -288,8 +292,7 @@ import { ArticleDetailHero } from '@fullstackdatasolutions/articles'
|
|
|
288
292
|
|
|
289
293
|
```tsx
|
|
290
294
|
import { ArticleSocialShare } from '@fullstackdatasolutions/articles'
|
|
291
|
-
|
|
292
|
-
<ArticleSocialShare
|
|
295
|
+
;<ArticleSocialShare
|
|
293
296
|
title={article.title}
|
|
294
297
|
url={articleUrl}
|
|
295
298
|
excerpt={article.excerpt}
|
|
@@ -297,12 +300,12 @@ import { ArticleSocialShare } from '@fullstackdatasolutions/articles'
|
|
|
297
300
|
/>
|
|
298
301
|
```
|
|
299
302
|
|
|
300
|
-
| Prop
|
|
301
|
-
|
|
302
|
-
| `title`
|
|
303
|
-
| `url`
|
|
304
|
-
| `excerpt`
|
|
305
|
-
| `shareMessage` | `string` | no
|
|
303
|
+
| Prop | Type | Required | Description |
|
|
304
|
+
| -------------- | -------- | -------- | --------------------------------------------------- |
|
|
305
|
+
| `title` | `string` | yes | Article title - used in share text |
|
|
306
|
+
| `url` | `string` | yes | Canonical URL to share |
|
|
307
|
+
| `excerpt` | `string` | no | Used as email body pre-fill |
|
|
308
|
+
| `shareMessage` | `string` | no | Optional paragraph rendered below the share buttons |
|
|
306
309
|
|
|
307
310
|
---
|
|
308
311
|
|
|
@@ -319,11 +322,11 @@ const { previous, next } = await getAdjacentArticles(slug)
|
|
|
319
322
|
<ArticleNavigation previous={previous} next={next} basePath="/articles" />
|
|
320
323
|
```
|
|
321
324
|
|
|
322
|
-
| Prop
|
|
323
|
-
|
|
324
|
-
| `previous` | `{ slug: string; title: string } \| null` | yes
|
|
325
|
-
| `next`
|
|
326
|
-
| `basePath` | `string`
|
|
325
|
+
| Prop | Type | Required | Description |
|
|
326
|
+
| ---------- | ----------------------------------------- | -------- | -------------------------------------------------------- |
|
|
327
|
+
| `previous` | `{ slug: string; title: string } \| null` | yes | Previous article, or `null` |
|
|
328
|
+
| `next` | `{ slug: string; title: string } \| null` | yes | Next article, or `null` |
|
|
329
|
+
| `basePath` | `string` | yes | Route prefix (e.g. `"/articles"`) prepended to each slug |
|
|
327
330
|
|
|
328
331
|
- Titles longer than 60 characters are truncated with an ellipsis.
|
|
329
332
|
- No output is rendered when both `previous` and `next` are `null`.
|
|
@@ -336,14 +339,13 @@ const { previous, next } = await getAdjacentArticles(slug)
|
|
|
336
339
|
|
|
337
340
|
```tsx
|
|
338
341
|
import { ArticleTOC } from '@fullstackdatasolutions/articles/server'
|
|
339
|
-
|
|
340
|
-
<ArticleTOC toc={article.toc} />
|
|
342
|
+
;<ArticleTOC toc={article.toc} />
|
|
341
343
|
```
|
|
342
344
|
|
|
343
|
-
| Prop
|
|
344
|
-
|
|
345
|
-
| `toc`
|
|
346
|
-
| `className` | `string`
|
|
345
|
+
| Prop | Type | Required | Description |
|
|
346
|
+
| ----------- | ----------- | -------- | ------------------------------------------------- |
|
|
347
|
+
| `toc` | `TocItem[]` | yes | Array of table of contents items from the article |
|
|
348
|
+
| `className` | `string` | no | Optional CSS class to add to the nav wrapper |
|
|
347
349
|
|
|
348
350
|
- Indentation: h2=0rem, h3=1rem, h4=2rem (based on `item.depth - 2`)
|
|
349
351
|
- Returns `null` when `toc` is empty
|
|
@@ -369,12 +371,12 @@ import { ArticleBackLink } from '@fullstackdatasolutions/articles'
|
|
|
369
371
|
/>
|
|
370
372
|
```
|
|
371
373
|
|
|
372
|
-
| Prop
|
|
373
|
-
|
|
374
|
-
| `config`
|
|
375
|
-
| `href`
|
|
376
|
-
| `label`
|
|
377
|
-
| `className` | `string`
|
|
374
|
+
| Prop | Type | Required | Description |
|
|
375
|
+
| ----------- | ---------------- | -------- | ----------------------------------------------------------------------------- |
|
|
376
|
+
| `config` | `ArticlesConfig` | yes | Site config. Must include `showBackToArticles?: boolean` from ArticlesConfig. |
|
|
377
|
+
| `href` | `string` | no | Link destination. Defaults to `/articles`. |
|
|
378
|
+
| `label` | `string` | no | Link text. Defaults to `Back to Articles`. |
|
|
379
|
+
| `className` | `string` | no | Optional CSS class to override default styling. |
|
|
378
380
|
|
|
379
381
|
- Returns `null` when `config.showBackToArticles === false`
|
|
380
382
|
- Gate used in article detail pages: `config.showBackToArticles !== false`
|
|
@@ -387,8 +389,7 @@ import { ArticleBackLink } from '@fullstackdatasolutions/articles'
|
|
|
387
389
|
|
|
388
390
|
```tsx
|
|
389
391
|
import { ScrollToTop } from '@fullstackdatasolutions/articles'
|
|
390
|
-
|
|
391
|
-
<ScrollToTop />
|
|
392
|
+
;<ScrollToTop />
|
|
392
393
|
```
|
|
393
394
|
|
|
394
395
|
- No props
|
|
@@ -408,7 +409,7 @@ export const siteConfig: ArticlesConfig = {
|
|
|
408
409
|
comments: {
|
|
409
410
|
enabled: true,
|
|
410
411
|
perArticleOverride: {
|
|
411
|
-
'sensitive-article': false,
|
|
412
|
+
'sensitive-article': false, // opt individual articles out
|
|
412
413
|
},
|
|
413
414
|
},
|
|
414
415
|
}
|
|
@@ -455,6 +456,7 @@ export const generateMetadata = async ({ params }) => {
|
|
|
455
456
|
return generateArticleMetadata(slug.join('/'), siteConfig)
|
|
456
457
|
}
|
|
457
458
|
// Produces: title, description, full OpenGraph, Twitter Card, canonical URL
|
|
459
|
+
// If article frontmatter has aiCrawl: true, it also emits a text/markdown alternate link.
|
|
458
460
|
|
|
459
461
|
// app/articles/category/[category]/page.tsx
|
|
460
462
|
import { generateCategoryMetadata } from '@fullstackdatasolutions/articles/server'
|
|
@@ -485,6 +487,7 @@ function escapeXml(str: string): string {
|
|
|
485
487
|
export async function GET() {
|
|
486
488
|
const siteUrl = siteConfig.siteUrl.replace(/\/$/, '')
|
|
487
489
|
const articles = await getAllArticles()
|
|
490
|
+
const showAuthor = siteConfig.showAuthor !== false
|
|
488
491
|
|
|
489
492
|
const items = articles
|
|
490
493
|
.map((article) => {
|
|
@@ -497,7 +500,7 @@ export async function GET() {
|
|
|
497
500
|
` <guid isPermaLink="true">${url}</guid>`,
|
|
498
501
|
pubDate ? ` <pubDate>${pubDate}</pubDate>` : '',
|
|
499
502
|
article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : '',
|
|
500
|
-
article.author ? ` <author>${escapeXml(article.author)}</author>` : '',
|
|
503
|
+
showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : '',
|
|
501
504
|
' </item>',
|
|
502
505
|
]
|
|
503
506
|
.filter(Boolean)
|
|
@@ -531,6 +534,39 @@ ${items}
|
|
|
531
534
|
- Uses `siteConfig.description` for the channel description field (falls back to `siteName` if omitted)
|
|
532
535
|
- The `<link rel="alternate" type="application/rss+xml">` tag is added automatically to the articles index `<head>` via `generateArticlesIndexMetadata`
|
|
533
536
|
|
|
537
|
+
### AI markdown twins
|
|
538
|
+
|
|
539
|
+
Articles are not exposed as markdown unless their frontmatter explicitly opts in:
|
|
540
|
+
|
|
541
|
+
```markdown
|
|
542
|
+
---
|
|
543
|
+
title: AI-readable article
|
|
544
|
+
excerpt: A short summary.
|
|
545
|
+
aiCrawl: true
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
Markdown body...
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Runtime route example for Next App Router:
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
// app/articles-md/[...slug]/route.ts
|
|
555
|
+
import { getArticleMarkdownResponse } from '@fullstackdatasolutions/articles/server'
|
|
556
|
+
import { siteConfig } from '@/config/articles'
|
|
557
|
+
|
|
558
|
+
type RouteProps = Readonly<{ params: Promise<{ slug: string[] }> }>
|
|
559
|
+
|
|
560
|
+
export async function GET(_request: Request, { params }: RouteProps) {
|
|
561
|
+
const { slug } = await params
|
|
562
|
+
return getArticleMarkdownResponse(slug.join('/'), siteConfig)
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
If you want public URLs like `/articles/my-post.md`, add a rewrite from `/articles/:path*.md` to `/articles-md/:path*` in the consuming app. For static generation, call `getArticleMarkdown(slug)` during your build and write the returned content beside your generated HTML; it returns `null` unless `aiCrawl: true`.
|
|
567
|
+
|
|
568
|
+
Use `getArticleAiHeaders(article, siteConfig)` in an HTML article route if you want response headers in addition to Next metadata. Opted-in articles receive a markdown `Link` header. Blocked/default articles receive `X-Robots-Tag: noai, noimageai`. `getAiRobotsTxtRules()` returns optional robots.txt blocks for known AI crawler user agents and articles that are not opted in.
|
|
569
|
+
|
|
534
570
|
### Sitemaps
|
|
535
571
|
|
|
536
572
|
Use `getArticleSitemapEntries` to generate article and category sitemap entries. Pass your `ArticlesConfig` object (preferred) or a plain URL string.
|
|
@@ -560,21 +596,22 @@ JSON-LD structured data (`ArticleSchema`, `BreadcrumbSchema`, `CollectionPageSch
|
|
|
560
596
|
|
|
561
597
|
## Article frontmatter reference
|
|
562
598
|
|
|
563
|
-
| Field
|
|
564
|
-
|
|
565
|
-
| `title`
|
|
566
|
-
| `excerpt`
|
|
567
|
-
| `author`
|
|
568
|
-
| `tags`
|
|
569
|
-
| `date`
|
|
570
|
-
| `lastmod`
|
|
571
|
-
| `featuredImage` | `string`
|
|
572
|
-
| `draft`
|
|
573
|
-
| `faq`
|
|
574
|
-
| `howTo`
|
|
575
|
-
| `canonicalUrl`
|
|
576
|
-
| `articleType`
|
|
577
|
-
| `series`
|
|
599
|
+
| Field | Type | Required | Description |
|
|
600
|
+
| --------------- | ---------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
601
|
+
| `title` | `string` | yes | Article title. If omitted, falls back to the article slug with dashes replaced by spaces. |
|
|
602
|
+
| `excerpt` | `string` | yes | One-sentence summary for cards and meta. If omitted, resolves to an empty string. |
|
|
603
|
+
| `author` | `string` | no | Defaults to `'Andrew Blase'` |
|
|
604
|
+
| `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. |
|
|
605
|
+
| `date` | `YYYY-MM-DD` | no | Omit to publish immediately. Future dates hide until that date. |
|
|
606
|
+
| `lastmod` | `YYYY-MM-DD` | no | Last modified date used by sitemap metadata when available. |
|
|
607
|
+
| `featuredImage` | `string` | no | Filename or nested path relative to the article directory, or an absolute `http(s)` URL. Auto-detected from the first image in the article directory if omitted. Falls back to `/placeholder-logo.png`. Relative paths are sanitized and resolved to `/articles/{slug}/{featuredImage}`. |
|
|
608
|
+
| `draft` | `boolean` | no | Set to `true` to hide the article in production. Drafts remain visible outside production. |
|
|
609
|
+
| `faq` | `{ question: string; answer: string }[]` | no | FAQ items used by the article structured data. Invalid entries are ignored. |
|
|
610
|
+
| `howTo` | `{ name: string; text: string }[]` | no | How-to steps used by the article structured data. Invalid entries are ignored. |
|
|
611
|
+
| `canonicalUrl` | `string` | no | Canonical URL override for article metadata. |
|
|
612
|
+
| `articleType` | `string` | no | Optional article type value included in article metadata/structured data. |
|
|
613
|
+
| `series` | `string` | no | Optional series label for grouping related articles. |
|
|
614
|
+
| `aiCrawl` | `boolean` | no | Set to `true` to publish the article's markdown twin and add a `text/markdown` alternate link. Omitted or `false` keeps markdown private and opts the HTML page out of AI indexing via helper headers. |
|
|
578
615
|
|
|
579
616
|
Articles can be authored as either `article.md` or `article.mdx` inside `public/articles/{slug}/`. The `{slug}` may be a path-like slug such as `game-system/article-name`. Markdown files are converted to HTML; MDX files are returned as `mdxSource` for the consuming app to render. Reading time and the table of contents are generated from the article body automatically.
|
|
580
617
|
|
|
@@ -590,6 +627,12 @@ import {
|
|
|
590
627
|
ArticleTOC,
|
|
591
628
|
getAllArticles,
|
|
592
629
|
getArticleMetadata,
|
|
630
|
+
getArticleMarkdown,
|
|
631
|
+
getArticleMarkdownResponse,
|
|
632
|
+
getArticleMarkdownUrl,
|
|
633
|
+
getArticleAiHeaders,
|
|
634
|
+
getAiRobotsTxtRules,
|
|
635
|
+
setArticlesErrorHandler,
|
|
593
636
|
getArticlesByCategory,
|
|
594
637
|
getAllCategories,
|
|
595
638
|
getAvailableArticleSlugs,
|
|
@@ -608,6 +651,21 @@ import {
|
|
|
608
651
|
} from '@fullstackdatasolutions/articles/server'
|
|
609
652
|
```
|
|
610
653
|
|
|
654
|
+
### Error reporting
|
|
655
|
+
|
|
656
|
+
Server utilities fail closed by returning `null`, `[]`, or fallback content when article files cannot be read or markdown cannot be rendered. By default, the package logs concise warnings only outside production. Consuming apps can route those reports to their own observability stack:
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
import { setArticlesErrorHandler } from '@fullstackdatasolutions/articles/server'
|
|
660
|
+
|
|
661
|
+
setArticlesErrorHandler((report) => {
|
|
662
|
+
// Send to Sentry, Datadog, Honeycomb, or your app logger.
|
|
663
|
+
console.warn(report.code, report.message, report.context, report.error)
|
|
664
|
+
})
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Call `setArticlesErrorHandler()` with no argument to restore the default handler.
|
|
668
|
+
|
|
611
669
|
---
|
|
612
670
|
|
|
613
671
|
## Using in a monorepo (workspace)
|
|
@@ -648,6 +706,7 @@ Paste this block into your app's `AGENTS.md` so AI assistants know how the libra
|
|
|
648
706
|
This app uses `@fullstackdatasolutions/articles` for the articles section.
|
|
649
707
|
|
|
650
708
|
### Key files
|
|
709
|
+
|
|
651
710
|
- `config/articles.ts` — all library behaviour (layout, theme, page size, comments, TOC, RSS) is controlled here
|
|
652
711
|
- `app/api/articles/` — thin API route wrappers; do not add business logic here
|
|
653
712
|
- `app/api/articles/comments-store.ts` — reference file-backed comment persistence helper used by the comment API routes
|
|
@@ -655,21 +714,25 @@ This app uses `@fullstackdatasolutions/articles` for the articles section.
|
|
|
655
714
|
- `public/articles/[slug]/article.md` or `article.mdx` — one directory per article; nested slugs such as `game-system/article-name` are supported
|
|
656
715
|
|
|
657
716
|
### CSS (Tailwind v4)
|
|
717
|
+
|
|
658
718
|
The package ships its source TypeScript files so Tailwind can scan them.
|
|
659
719
|
`app/globals.css` must contain:
|
|
660
|
-
|
|
661
|
-
|
|
720
|
+
@plugin "@tailwindcss/typography";
|
|
721
|
+
@source "./node_modules/@fullstackdatasolutions/articles/src";
|
|
662
722
|
If the `@source` line is missing, most component classes will not render correctly. Note: `ArticleCategoryGrid` images, `CategoryArticlesPage` hero, and `ArticleDetailHero` image use inline styles for critical layout so they render correctly without this line. All other styling still requires it.
|
|
663
723
|
The `@plugin "@tailwindcss/typography"` line is required for article body prose styling. Install with `pnpm add @tailwindcss/typography`.
|
|
664
724
|
|
|
665
725
|
### Components
|
|
726
|
+
|
|
666
727
|
- `ArticleTOC` — server component imported from `@fullstackdatasolutions/articles/server`; renders `article.toc[]` as a nav block. Controlled by `siteConfig.showToc` (defaults `true`). Returns `null` when empty.
|
|
667
728
|
- `ScrollToTop` — client component, no props. Fixed-position button appears after 300px scroll.
|
|
668
729
|
|
|
669
730
|
### Server utilities
|
|
731
|
+
|
|
670
732
|
For sitemaps and metadata: `import { getArticleSitemapEntries, generateArticlesIndexMetadata, generateArticleMetadata, generateCategoryMetadata } from '@fullstackdatasolutions/articles/server'`. See README for usage examples.
|
|
671
733
|
|
|
672
734
|
### Rules
|
|
735
|
+
|
|
673
736
|
- Change `config/articles.ts` first; only edit library source if config cannot express the need.
|
|
674
737
|
- When adding a new article: create `public/articles/[slug]/article.md` or `article.mdx` with title, excerpt, author, tags. Nested slugs such as `public/articles/game-system/article-name/article.mdx` are supported.
|
|
675
738
|
- API routes are copied from the library — update the package version and re-copy if the library API changes.
|
package/dist/index.cjs
CHANGED
|
@@ -152,15 +152,19 @@ function ArticleCategoryGrid({ categories, pageSize = 8 }) {
|
|
|
152
152
|
|
|
153
153
|
// src/ArticleSchemas.tsx
|
|
154
154
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
155
|
-
function ArticleSchema({
|
|
156
|
-
|
|
155
|
+
function ArticleSchema({
|
|
156
|
+
article,
|
|
157
|
+
articleUrl,
|
|
158
|
+
siteName,
|
|
159
|
+
showAuthor = true
|
|
160
|
+
}) {
|
|
161
|
+
const schema = __spreadProps(__spreadValues(__spreadValues({
|
|
157
162
|
"@context": "https://schema.org",
|
|
158
163
|
"@type": "Article",
|
|
159
164
|
headline: article.title,
|
|
160
165
|
description: article.excerpt,
|
|
161
166
|
image: article.featuredImage
|
|
162
|
-
}, article.date && { datePublished: new Date(article.date).toISOString() }), {
|
|
163
|
-
author: { "@type": "Person", name: article.author },
|
|
167
|
+
}, article.date && { datePublished: new Date(article.date).toISOString() }), showAuthor && { author: { "@type": "Person", name: article.author } }), {
|
|
164
168
|
publisher: { "@type": "Organization", name: siteName },
|
|
165
169
|
mainEntityOfPage: { "@type": "WebPage", "@id": articleUrl }
|
|
166
170
|
});
|
|
@@ -191,17 +195,22 @@ function BreadcrumbSchema({ items }) {
|
|
|
191
195
|
}
|
|
192
196
|
);
|
|
193
197
|
}
|
|
194
|
-
function ArticleSEO({
|
|
198
|
+
function ArticleSEO({
|
|
199
|
+
article,
|
|
200
|
+
articleUrl,
|
|
201
|
+
siteName,
|
|
202
|
+
siteLogo,
|
|
203
|
+
showAuthor = true
|
|
204
|
+
}) {
|
|
195
205
|
const publishedAt = article.date ? new Date(article.date).toISOString() : void 0;
|
|
196
206
|
const modifiedAt = article.lastmod ? new Date(article.lastmod).toISOString() : publishedAt;
|
|
197
|
-
const structuredData = __spreadValues(__spreadProps(__spreadValues(__spreadValues({
|
|
207
|
+
const structuredData = __spreadValues(__spreadProps(__spreadValues(__spreadValues(__spreadValues({
|
|
198
208
|
"@context": "https://schema.org",
|
|
199
209
|
"@type": article.articleType || "Article",
|
|
200
210
|
mainEntityOfPage: { "@type": "WebPage", "@id": articleUrl },
|
|
201
211
|
headline: article.title,
|
|
202
212
|
image: article.featuredImage ? { "@type": "ImageObject", url: article.featuredImage } : void 0
|
|
203
|
-
}, publishedAt && { datePublished: publishedAt }), modifiedAt && { dateModified: modifiedAt }), {
|
|
204
|
-
author: { "@type": "Person", name: article.author },
|
|
213
|
+
}, publishedAt && { datePublished: publishedAt }), modifiedAt && { dateModified: modifiedAt }), showAuthor && { author: { "@type": "Person", name: article.author } }), {
|
|
205
214
|
publisher: __spreadValues({
|
|
206
215
|
"@type": "Organization",
|
|
207
216
|
name: siteName
|
|
@@ -370,7 +379,7 @@ var import_link3 = __toESM(require("next/link"), 1);
|
|
|
370
379
|
var import_image2 = __toESM(require("next/image"), 1);
|
|
371
380
|
var import_lucide_react2 = require("lucide-react");
|
|
372
381
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
373
|
-
function FeaturedArticle({ article }) {
|
|
382
|
+
function FeaturedArticle({ article, showAuthor = true }) {
|
|
374
383
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("section", { className: "py-16 bg-background", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "grid lg:grid-cols-2 gap-12 items-center", children: [
|
|
375
384
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
|
|
376
385
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "inline-block bg-primary/10 text-primary text-sm font-medium px-3 py-1 rounded-full mb-4", children: "FEATURED ARTICLE" }),
|
|
@@ -385,7 +394,7 @@ function FeaturedArticle({ article }) {
|
|
|
385
394
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.Clock, { className: "h-4 w-4" }),
|
|
386
395
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: article.readTime })
|
|
387
396
|
] }),
|
|
388
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
397
|
+
showAuthor && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
389
398
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.User, { className: "h-4 w-4" }),
|
|
390
399
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: article.author })
|
|
391
400
|
] })
|
|
@@ -675,7 +684,14 @@ function renderSection(section, ctx, config) {
|
|
|
675
684
|
"search"
|
|
676
685
|
);
|
|
677
686
|
case "featured":
|
|
678
|
-
return articles.length > 0 && !searchQuery ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
687
|
+
return articles.length > 0 && !searchQuery ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
688
|
+
FeaturedArticle,
|
|
689
|
+
{
|
|
690
|
+
article: articles[0],
|
|
691
|
+
showAuthor: config.showAuthor !== false
|
|
692
|
+
},
|
|
693
|
+
"featured"
|
|
694
|
+
) : null;
|
|
679
695
|
case "latest":
|
|
680
696
|
if (loading && articles.length === 0) {
|
|
681
697
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("section", { id: "latest", className: "py-16 bg-muted/50 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-center py-24", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "text-center", children: [
|
|
@@ -758,7 +774,8 @@ function toSlug(cat) {
|
|
|
758
774
|
function ArticleDetailHero({
|
|
759
775
|
article,
|
|
760
776
|
categoryBasePath = "/articles/category",
|
|
761
|
-
showDate = false
|
|
777
|
+
showDate = false,
|
|
778
|
+
showAuthor = true
|
|
762
779
|
}) {
|
|
763
780
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
764
781
|
"section",
|
|
@@ -859,7 +876,7 @@ function ArticleDetailHero({
|
|
|
859
876
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react5.Clock, { className: "h-4 w-4" }),
|
|
860
877
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: article.readTime })
|
|
861
878
|
] }),
|
|
862
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
|
|
879
|
+
showAuthor && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
|
|
863
880
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react5.User, { className: "h-4 w-4" }),
|
|
864
881
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { children: [
|
|
865
882
|
"By ",
|