@fullstackdatasolutions/articles 0.7.1 → 0.8.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
@@ -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 "tailwindcss";
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": ["./node_modules/@fullstackdatasolutions/articles/src/index.ts"],
57
- "@fullstackdatasolutions/articles/server": ["./node_modules/@fullstackdatasolutions/articles/src/server.ts"]
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: 'https://yoursite.com',
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 { generateCategoryMetadata, getArticlesByCategory } from '@fullstackdatasolutions/articles/server'
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 | Type | Default | Description |
206
- |---|---|---|---|
207
- | `siteUrl` | `string` | — | Canonical base URL. Used in metadata and JSON-LD. |
208
- | `siteName` | `string` | — | Site name in title tags and JSON-LD. |
209
- | `pageSize` | `number` | `6` | Articles per page / per "Load more" click. |
210
- | `categoriesPageSize` | `number` | `8` | Category cards before "Load more categories". |
211
- | `layout` | `ArticlesSection[]` | see below | Ordered sections to render. Omit a key to hide it. |
212
- | `theme` | `ArticlesTheme` | — | CSS custom-property overrides for colors and fonts. |
213
- | `categoryDescriptions` | `Record<string, string \| CategoryDescription>` | — | Short/long text per category slug. |
214
- | `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
215
- | `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
216
- | `showToc` | `boolean` | `true` | Show the table of contents on article detail pages. Set to `false` to hide on all articles. |
217
- | `showBackToArticles` | `boolean` | `true` | Show the back-navigation link on article detail pages. Set to `false` to hide `ArticleBackLink`. |
218
- | `description` | `string` | | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
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 | Type | Required | Description |
273
- |---|---|---|---|
274
- | `article` | `Article` | yes | Article object returned by `getArticleMetadata` |
275
- | `categoryBasePath` | `string` | no | Base path for category links. Defaults to `"/articles/category"`. Pass `""` to render categories as plain text. |
276
- | `showDate` | `boolean` | no | Show the article date. Defaults to `false` (hidden). |
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
- - Hero image / categories behave automatically once you use this component; apps do not need to reimplement the hero section for those features.
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 | Type | Required | Description |
301
- |---|---|---|---|
302
- | `title` | `string` | yes | Article title - used in share text |
303
- | `url` | `string` | yes | Canonical URL to share |
304
- | `excerpt` | `string` | no | Used as email body pre-fill |
305
- | `shareMessage` | `string` | no | Optional paragraph rendered below the share buttons |
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 | Type | Required | Description |
323
- |---|---|---|---|
324
- | `previous` | `{ slug: string; title: string } \| null` | yes | Previous article, or `null` |
325
- | `next` | `{ slug: string; title: string } \| null` | yes | Next article, or `null` |
326
- | `basePath` | `string` | yes | Route prefix (e.g. `"/articles"`) prepended to each slug |
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 | Type | Required | Description |
344
- |---|---|---|---|
345
- | `toc` | `TocItem[]` | yes | Array of table of contents items from the article |
346
- | `className` | `string` | no | Optional CSS class to add to the nav wrapper |
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 | Type | Required | Description |
373
- |---|---|---|---|
374
- | `config` | `ArticlesConfig` | yes | Site config. Must include `showBackToArticles?: boolean` from ArticlesConfig. |
375
- | `href` | `string` | no | Link destination. Defaults to `/articles`. |
376
- | `label` | `string` | no | Link text. Defaults to `Back to Articles`. |
377
- | `className` | `string` | no | Optional CSS class to override default styling. |
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, // opt individual articles out
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 | Type | Required | Description |
564
- |---|---|---|---|
565
- | `title` | `string` | yes | Article title. If omitted, falls back to the article slug with dashes replaced by spaces. |
566
- | `excerpt` | `string` | yes | One-sentence summary for cards and meta. If omitted, resolves to an empty string. |
567
- | `author` | `string` | no | Defaults to `'Andrew Blase'` |
568
- | `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. |
569
- | `date` | `YYYY-MM-DD` | no | Omit to publish immediately. Future dates hide until that date. |
570
- | `lastmod` | `YYYY-MM-DD` | no | Last modified date used by sitemap metadata when available. |
571
- | `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}`. |
572
- | `draft` | `boolean` | no | Set to `true` to hide the article in production. Drafts remain visible outside production. |
573
- | `faq` | `{ question: string; answer: string }[]` | no | FAQ items used by the article structured data. Invalid entries are ignored. |
574
- | `howTo` | `{ name: string; text: string }[]` | no | How-to steps used by the article structured data. Invalid entries are ignored. |
575
- | `canonicalUrl` | `string` | no | Canonical URL override for article metadata. |
576
- | `articleType` | `string` | no | Optional article type value included in article metadata/structured data. |
577
- | `series` | `string` | no | Optional series label for grouping related articles. |
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
- @plugin "@tailwindcss/typography";
661
- @source "./node_modules/@fullstackdatasolutions/articles/src";
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({ article, articleUrl, siteName }) {
156
- const schema = __spreadProps(__spreadValues({
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({ article, articleUrl, siteName, siteLogo }) {
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)(FeaturedArticle, { article: articles[0] }, "featured") : null;
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 ",