@fullstackdatasolutions/articles 0.6.0 → 0.7.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 CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  A self-contained Next.js articles library providing components, server utilities, SEO helpers, and a comments system. Drop it into any Next.js + Tailwind CSS v4 app.
4
4
 
5
- Built by [Full Stack Data Solutions](https://fullstackdatasolutions.com).
5
+ Built by [Full Stack Data Solutions](https://fullstackdatasolutions.com) for solopreneurs and founders who want a practical articles system without wiring the same SEO, RSS, sitemap, MDX, and content plumbing from scratch.
6
+
7
+ ## Support and consulting
8
+
9
+ If this package saves you time, you can support ongoing maintenance by buying the developer a coffee: [buymeacoffee.com/andrewblase](https://buymeacoffee.com/andrewblase).
10
+
11
+ Need help adding articles, RSS, sitemap, SEO, or MDX workflows to a Next.js app? Contact [Full Stack Data Solutions](https://fullstackdatasolutions.com).
6
12
 
7
13
  ---
8
14
 
@@ -23,12 +29,21 @@ In Tailwind CSS v4, you add a `@source` directive to your global CSS file — **
23
29
  ```css
24
30
  /* app/globals.css */
25
31
  @import "tailwindcss";
32
+ @plugin "@tailwindcss/typography";
26
33
 
27
34
  @source "./node_modules/@fullstackdatasolutions/articles/src";
28
35
  ```
29
36
 
30
37
  The package components use Tailwind semantic tokens (`bg-card`, `text-muted-foreground`, `border-border`, etc.) that shadcn/ui and v0-generated apps already define — so the styling integrates automatically with your existing theme.
31
38
 
39
+ Install `@tailwindcss/typography` if not already present:
40
+
41
+ ```bash
42
+ npm install @tailwindcss/typography
43
+ # or
44
+ pnpm add @tailwindcss/typography
45
+ ```
46
+
32
47
  ### 3. Add TypeScript path aliases
33
48
 
34
49
  If you're using TypeScript and want IDE autocomplete without a pre-built `dist/`, add these paths to `tsconfig.json`. This is **required in a monorepo** (where the package is a workspace dep) and optional when installed from npm (the published `dist/` files work without it).
@@ -71,7 +86,7 @@ export default function Page() {
71
86
  return <ArticlesPage config={siteConfig} />
72
87
  }
73
88
 
74
- // app/articles/[slug]/page.tsx
89
+ // app/articles/[...slug]/page.tsx
75
90
  import { notFound } from 'next/navigation'
76
91
  import {
77
92
  ArticleDetailHero,
@@ -86,18 +101,26 @@ import {
86
101
  ArticleContent,
87
102
  ArticleTOC,
88
103
  generateArticleMetadata,
104
+ generateArticleStaticParams,
89
105
  getAdjacentArticles,
90
106
  getArticleMetadata,
91
107
  } from '@fullstackdatasolutions/articles/server'
92
108
  import { siteConfig } from '@/config/articles'
93
- export { generateArticleStaticParams as generateStaticParams } from '@fullstackdatasolutions/articles/server'
94
- type ArticlePageProps = Readonly<{ params: Promise<{ slug: string }> }>
109
+ type ArticlePageProps = Readonly<{ params: Promise<{ slug: string[] }> }>
110
+ function normalizeSlug(slug: string[]): string {
111
+ return slug.join('/')
112
+ }
113
+ export function generateStaticParams(): { slug: string[] }[] {
114
+ return generateArticleStaticParams().map((params) => ({ slug: params.slug.split('/') }))
115
+ }
95
116
  export async function generateMetadata({ params }: ArticlePageProps) {
96
- const { slug } = await params
117
+ const { slug: slugSegments } = await params
118
+ const slug = normalizeSlug(slugSegments)
97
119
  return generateArticleMetadata(slug, siteConfig)
98
120
  }
99
121
  export default async function Page({ params }: ArticlePageProps) {
100
- const { slug } = await params
122
+ const { slug: slugSegments } = await params
123
+ const slug = normalizeSlug(slugSegments)
101
124
  const article = await getArticleMetadata(slug)
102
125
  if (!article) notFound()
103
126
  const { previous, next } = await getAdjacentArticles(slug)
@@ -173,7 +196,7 @@ tags: [campaigns, strategy]
173
196
  Article body in Markdown...
174
197
  ```
175
198
 
176
- Article directories can also be nested below grouping folders. For example, `public/articles/game-system/article-name/article.mdx` is discovered as the slug `game-system/article-name`. If your app exposes nested article URLs with slash-separated slugs, use a catch-all Next.js route such as `app/articles/[...slug]/page.tsx` and join the slug segments before calling the server helpers.
199
+ Article directories can be nested to any depth below `public/articles`. For example, `public/articles/game-system/article-name/article.mdx` is discovered as the slug `game-system/article-name`, and `public/articles/tov/subdirectory/article-name/article.md` is discovered as `tov/subdirectory/article-name`. Use a catch-all Next.js route such as `app/articles/[...slug]/page.tsx` and join the slug segments before calling the server helpers.
177
200
 
178
201
  ---
179
202
 
@@ -191,6 +214,7 @@ Article directories can also be nested below grouping folders. For example, `pub
191
214
  | `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
192
215
  | `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
193
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`. |
194
218
  | `description` | `string` | — | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
195
219
 
196
220
  **Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
@@ -327,6 +351,36 @@ import { ArticleTOC } from '@fullstackdatasolutions/articles/server'
327
351
 
328
352
  ---
329
353
 
354
+ ## ArticleBackLink
355
+
356
+ `ArticleBackLink` renders a back-navigation link on article detail pages. Returns `null` when `config.showBackToArticles === false`, so it is safe to render unconditionally.
357
+
358
+ ```tsx
359
+ import { ArticleBackLink } from '@fullstackdatasolutions/articles'
360
+
361
+ <ArticleBackLink config={siteConfig} />
362
+
363
+ // With custom props:
364
+ <ArticleBackLink
365
+ config={siteConfig}
366
+ href="/blog"
367
+ label="Back to Blog"
368
+ className="custom-class"
369
+ />
370
+ ```
371
+
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. |
378
+
379
+ - Returns `null` when `config.showBackToArticles === false`
380
+ - Gate used in article detail pages: `config.showBackToArticles !== false`
381
+
382
+ ---
383
+
330
384
  ## ScrollToTop
331
385
 
332
386
  `ScrollToTop` is a fixed-position floating button in the bottom-right corner that appears when the user scrolls past 300px, and smooth-scrolls back to the top on click. Client component (`'use client'`). No props required - fully self-contained, safe to place anywhere in your layout.
@@ -394,10 +448,12 @@ export const metadata: Metadata = generateArticlesIndexMetadata(siteConfig)
394
448
  ### Article and category metadata
395
449
 
396
450
  ```ts
397
- // app/articles/[slug]/page.tsx
451
+ // app/articles/[...slug]/page.tsx
398
452
  import { generateArticleMetadata } from '@fullstackdatasolutions/articles/server'
399
- export const generateMetadata = ({ params }) =>
400
- generateArticleMetadata(params.slug, siteConfig)
453
+ export const generateMetadata = async ({ params }) => {
454
+ const { slug } = await params
455
+ return generateArticleMetadata(slug.join('/'), siteConfig)
456
+ }
401
457
  // Produces: title, description, full OpenGraph, Twitter Card, canonical URL
402
458
 
403
459
  // app/articles/category/[category]/page.tsx
@@ -574,6 +630,7 @@ And point the Tailwind `@source` at the workspace path:
574
630
 
575
631
  ```css
576
632
  /* app/globals.css */
633
+ @plugin "@tailwindcss/typography";
577
634
  @source "../../packages/articles/src";
578
635
  ```
579
636
 
@@ -599,9 +656,11 @@ This app uses `@fullstackdatasolutions/articles` for the articles section.
599
656
 
600
657
  ### CSS (Tailwind v4)
601
658
  The package ships its source TypeScript files so Tailwind can scan them.
602
- `app/globals.css` contains:
659
+ `app/globals.css` must contain:
660
+ @plugin "@tailwindcss/typography";
603
661
  @source "./node_modules/@fullstackdatasolutions/articles/src";
604
- If this 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.
662
+ 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
+ The `@plugin "@tailwindcss/typography"` line is required for article body prose styling. Install with `pnpm add @tailwindcss/typography`.
605
664
 
606
665
  ### Components
607
666
  - `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.
package/dist/index.cjs CHANGED
@@ -69,6 +69,7 @@ var __async = (__this, __arguments, generator) => {
69
69
  // src/index.ts
70
70
  var src_exports = {};
71
71
  __export(src_exports, {
72
+ ArticleBackLink: () => ArticleBackLink,
72
73
  ArticleCard: () => ArticleCard,
73
74
  ArticleCategoryGrid: () => ArticleCategoryGrid,
74
75
  ArticleDetailHero: () => ArticleDetailHero,
@@ -1025,19 +1026,10 @@ function ArticleSocialShare({ title, url, excerpt, shareMessage }) {
1025
1026
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.MessageCircle, { className: "h-4 w-4" }),
1026
1027
  "Telegram"
1027
1028
  ] }),
1028
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1029
- "button",
1030
- {
1031
- className: btnClass,
1032
- onClick: () => {
1033
- globalThis.location.href = shareLinks.email;
1034
- },
1035
- children: [
1036
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.Mail, { className: "h-4 w-4" }),
1037
- "Email"
1038
- ]
1039
- }
1040
- ),
1029
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("a", { className: btnClass, href: shareLinks.email, children: [
1030
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.Mail, { className: "h-4 w-4" }),
1031
+ "Email"
1032
+ ] }),
1041
1033
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("button", { className: btnClass, onClick: copyToClipboard, children: [
1042
1034
  copied ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.Check, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.Copy, { className: "h-4 w-4" }),
1043
1035
  copied ? "Copied!" : "Copy Link"
@@ -1088,18 +1080,42 @@ function ArticleNavigation({ previous, next, basePath }) {
1088
1080
  ] }) });
1089
1081
  }
1090
1082
 
1091
- // src/ArticleTOC.tsx
1083
+ // src/ArticleBackLink.tsx
1084
+ var import_link8 = __toESM(require("next/link"), 1);
1085
+ var import_lucide_react8 = require("lucide-react");
1092
1086
  var import_jsx_runtime14 = require("react/jsx-runtime");
1087
+ function ArticleBackLink({
1088
+ config,
1089
+ href = "/articles",
1090
+ label = "Back to Articles",
1091
+ className
1092
+ }) {
1093
+ if (config.showBackToArticles === false) return null;
1094
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1095
+ import_link8.default,
1096
+ {
1097
+ href,
1098
+ className: className != null ? className : "inline-flex items-center gap-2 mb-6 -ml-3 px-3 py-2 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors",
1099
+ children: [
1100
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react8.ArrowLeft, { className: "h-4 w-4" }),
1101
+ label
1102
+ ]
1103
+ }
1104
+ );
1105
+ }
1106
+
1107
+ // src/ArticleTOC.tsx
1108
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1093
1109
  function ArticleTOC({ toc, className }) {
1094
1110
  if (!toc.length) return null;
1095
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1111
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1096
1112
  "nav",
1097
1113
  {
1098
1114
  "aria-label": "Table of contents",
1099
1115
  className: `mb-8 rounded-lg border border-border bg-muted/40 px-6 py-4 ${className != null ? className : ""}`,
1100
1116
  children: [
1101
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "On this page" }),
1102
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("ul", { className: "space-y-1 text-sm", children: toc.map((item) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("li", { style: { paddingLeft: `${Math.max(0, item.depth - 2) * 1}rem` }, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1117
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "On this page" }),
1118
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "space-y-1 text-sm", children: toc.map((item) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { style: { paddingLeft: `${Math.max(0, item.depth - 2) * 1}rem` }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1103
1119
  "a",
1104
1120
  {
1105
1121
  href: `#${item.id}`,
@@ -1114,8 +1130,8 @@ function ArticleTOC({ toc, className }) {
1114
1130
 
1115
1131
  // src/ScrollToTop.tsx
1116
1132
  var import_react5 = require("react");
1117
- var import_lucide_react8 = require("lucide-react");
1118
- var import_jsx_runtime15 = require("react/jsx-runtime");
1133
+ var import_lucide_react9 = require("lucide-react");
1134
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1119
1135
  function ScrollToTop() {
1120
1136
  const [visible, setVisible] = (0, import_react5.useState)(false);
1121
1137
  (0, import_react5.useEffect)(() => {
@@ -1124,13 +1140,13 @@ function ScrollToTop() {
1124
1140
  return () => globalThis.removeEventListener("scroll", onScroll);
1125
1141
  }, []);
1126
1142
  if (!visible) return null;
1127
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1143
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1128
1144
  "button",
1129
1145
  {
1130
1146
  "aria-label": "Scroll to top",
1131
1147
  onClick: () => globalThis.scrollTo({ top: 0, behavior: "smooth" }),
1132
1148
  className: "fixed bottom-8 right-8 z-50 rounded-full bg-primary p-2 shadow-md text-primary-foreground hover:bg-primary/90 transition-colors",
1133
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react8.ArrowUp, { className: "h-5 w-5" })
1149
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react9.ArrowUp, { className: "h-5 w-5" })
1134
1150
  }
1135
1151
  );
1136
1152
  }
@@ -1141,7 +1157,7 @@ var import_react9 = require("react");
1141
1157
 
1142
1158
  // src/CommentForm.tsx
1143
1159
  var import_react6 = require("react");
1144
- var import_jsx_runtime16 = require("react/jsx-runtime");
1160
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1145
1161
  var MAX_LENGTH = 2e3;
1146
1162
  var WARN_THRESHOLD = 1800;
1147
1163
  function submitLabel(submitting, parentId) {
@@ -1180,8 +1196,8 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
1180
1196
  }
1181
1197
  });
1182
1198
  }
1183
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-2", children: [
1184
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1199
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-2", children: [
1200
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1185
1201
  "textarea",
1186
1202
  {
1187
1203
  value: body,
@@ -1194,13 +1210,13 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
1194
1210
  "aria-label": parentId ? "Reply text" : "Comment text"
1195
1211
  }
1196
1212
  ),
1197
- remaining <= MAX_LENGTH - WARN_THRESHOLD && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: `text-xs ${remaining < 0 ? "text-destructive" : "text-muted-foreground"}`, children: [
1213
+ remaining <= MAX_LENGTH - WARN_THRESHOLD && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: `text-xs ${remaining < 0 ? "text-destructive" : "text-muted-foreground"}`, children: [
1198
1214
  remaining,
1199
1215
  " characters remaining"
1200
1216
  ] }),
1201
- error && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-xs text-destructive", children: error }),
1202
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex gap-2", children: [
1203
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1217
+ error && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-xs text-destructive", children: error }),
1218
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex gap-2", children: [
1219
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1204
1220
  "button",
1205
1221
  {
1206
1222
  type: "submit",
@@ -1209,7 +1225,7 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
1209
1225
  children: submitLabel(submitting, parentId)
1210
1226
  }
1211
1227
  ),
1212
- onCancel && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1228
+ onCancel && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1213
1229
  "button",
1214
1230
  {
1215
1231
  type: "button",
@@ -1224,7 +1240,7 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
1224
1240
 
1225
1241
  // src/CommentItem.tsx
1226
1242
  var import_react7 = require("react");
1227
- var import_jsx_runtime17 = require("react/jsx-runtime");
1243
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1228
1244
  function relativeTime(iso) {
1229
1245
  const diff = Date.now() - new Date(iso).getTime();
1230
1246
  const minutes = Math.floor(diff / 6e4);
@@ -1262,15 +1278,15 @@ function CommentItem({
1262
1278
  }
1263
1279
  });
1264
1280
  }
1265
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: `${isReply ? "ml-8 border-l-2 border-border pl-4" : ""}`, children: [
1266
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "rounded-lg bg-muted/40 p-3 space-y-1", children: comment.isDeleted ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-sm italic text-muted-foreground", children: "Comment removed" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1267
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
1268
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sm font-medium text-foreground", children: comment.authorName }),
1269
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-xs text-muted-foreground", children: relativeTime(comment.createdAt) })
1281
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: `${isReply ? "ml-8 border-l-2 border-border pl-4" : ""}`, children: [
1282
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "rounded-lg bg-muted/40 p-3 space-y-1", children: comment.isDeleted ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-sm italic text-muted-foreground", children: "Comment removed" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
1283
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
1284
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sm font-medium text-foreground", children: comment.authorName }),
1285
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-xs text-muted-foreground", children: relativeTime(comment.createdAt) })
1270
1286
  ] }),
1271
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-sm text-foreground whitespace-pre-wrap break-words", children: comment.body }),
1272
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex gap-3 pt-1", children: [
1273
- canReply && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1287
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-sm text-foreground whitespace-pre-wrap break-words", children: comment.body }),
1288
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex gap-3 pt-1", children: [
1289
+ canReply && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1274
1290
  "button",
1275
1291
  {
1276
1292
  onClick: () => setShowReplyForm((v) => !v),
@@ -1278,7 +1294,7 @@ function CommentItem({
1278
1294
  children: showReplyForm ? "Cancel" : "Reply"
1279
1295
  }
1280
1296
  ),
1281
- canDelete && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1297
+ canDelete && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1282
1298
  "button",
1283
1299
  {
1284
1300
  onClick: handleDelete,
@@ -1289,7 +1305,7 @@ function CommentItem({
1289
1305
  )
1290
1306
  ] })
1291
1307
  ] }) }),
1292
- showReplyForm && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "mt-2 ml-2", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1308
+ showReplyForm && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "mt-2 ml-2", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1293
1309
  CommentForm,
1294
1310
  {
1295
1311
  articleSlug,
@@ -1301,7 +1317,7 @@ function CommentItem({
1301
1317
  onCancel: () => setShowReplyForm(false)
1302
1318
  }
1303
1319
  ) }),
1304
- replies.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "mt-2 space-y-2", children: replies.map((reply) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1320
+ replies.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "mt-2 space-y-2", children: replies.map((reply) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1305
1321
  CommentItem,
1306
1322
  {
1307
1323
  comment: __spreadProps(__spreadValues({}, reply), { replies: [] }),
@@ -1316,7 +1332,7 @@ function CommentItem({
1316
1332
  }
1317
1333
 
1318
1334
  // src/CommentThread.tsx
1319
- var import_jsx_runtime18 = require("react/jsx-runtime");
1335
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1320
1336
  function CommentThread({
1321
1337
  comments,
1322
1338
  articleSlug,
@@ -1324,9 +1340,9 @@ function CommentThread({
1324
1340
  onRefresh
1325
1341
  }) {
1326
1342
  if (comments.length === 0) {
1327
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-sm text-muted-foreground text-center py-6", children: "No comments yet. Be the first to share your thoughts." });
1343
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "text-sm text-muted-foreground text-center py-6", children: "No comments yet. Be the first to share your thoughts." });
1328
1344
  }
1329
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "space-y-4", children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1345
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "space-y-4", children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1330
1346
  CommentItem,
1331
1347
  {
1332
1348
  comment,
@@ -1339,7 +1355,7 @@ function CommentThread({
1339
1355
  }
1340
1356
 
1341
1357
  // src/CommentsSection.tsx
1342
- var import_jsx_runtime19 = require("react/jsx-runtime");
1358
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1343
1359
  function CommentsSection({ articleSlug, config }) {
1344
1360
  var _a, _b, _c, _d, _e, _f;
1345
1361
  const { data: session, status } = (0, import_react8.useSession)();
@@ -1371,10 +1387,10 @@ function CommentsSection({ articleSlug, config }) {
1371
1387
  if (!enabled) return null;
1372
1388
  const count = comments.length;
1373
1389
  const label = count === 1 ? "1 comment" : `${count} comments`;
1374
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "aria-label": "Comments", className: "mt-12 pt-8 border-t border-border space-y-6", children: [
1375
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("h2", { className: "text-xl font-semibold text-foreground", children: loading ? "Comments" : label }),
1376
- status === "authenticated" ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CommentForm, { articleSlug, onSuccess: fetchComments }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { className: "text-sm text-muted-foreground", children: [
1377
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1390
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("section", { "aria-label": "Comments", className: "mt-12 pt-8 border-t border-border space-y-6", children: [
1391
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h2", { className: "text-xl font-semibold text-foreground", children: loading ? "Comments" : label }),
1392
+ status === "authenticated" ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CommentForm, { articleSlug, onSuccess: fetchComments }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { className: "text-sm text-muted-foreground", children: [
1393
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1378
1394
  "button",
1379
1395
  {
1380
1396
  onClick: () => (0, import_react8.signIn)(),
@@ -1385,8 +1401,8 @@ function CommentsSection({ articleSlug, config }) {
1385
1401
  " ",
1386
1402
  "to join the discussion."
1387
1403
  ] }),
1388
- fetchError && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "text-sm text-destructive", children: fetchError }),
1389
- loading ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "text-sm text-muted-foreground", children: "Loading comments\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1404
+ fetchError && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "text-sm text-destructive", children: fetchError }),
1405
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "text-sm text-muted-foreground", children: "Loading comments\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1390
1406
  CommentThread,
1391
1407
  {
1392
1408
  comments,
@@ -1399,6 +1415,7 @@ function CommentsSection({ articleSlug, config }) {
1399
1415
  }
1400
1416
  // Annotate the CommonJS export names for ESM import in node:
1401
1417
  0 && (module.exports = {
1418
+ ArticleBackLink,
1402
1419
  ArticleCard,
1403
1420
  ArticleCategoryGrid,
1404
1421
  ArticleDetailHero,