@fullstackdatasolutions/articles 0.5.1 → 0.7.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
@@ -23,12 +23,21 @@ In Tailwind CSS v4, you add a `@source` directive to your global CSS file — **
23
23
  ```css
24
24
  /* app/globals.css */
25
25
  @import "tailwindcss";
26
+ @plugin "@tailwindcss/typography";
26
27
 
27
28
  @source "./node_modules/@fullstackdatasolutions/articles/src";
28
29
  ```
29
30
 
30
31
  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
32
 
33
+ Install `@tailwindcss/typography` if not already present:
34
+
35
+ ```bash
36
+ npm install @tailwindcss/typography
37
+ # or
38
+ pnpm add @tailwindcss/typography
39
+ ```
40
+
32
41
  ### 3. Add TypeScript path aliases
33
42
 
34
43
  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).
@@ -160,7 +169,7 @@ The route implementations import from `@fullstackdatasolutions/articles/server`.
160
169
 
161
170
  ### 7. Add your content
162
171
 
163
- Create `public/articles/[slug]/article.md` for each article. Required frontmatter:
172
+ Create `public/articles/[slug]/article.md` or `public/articles/[slug]/article.mdx` for each article. Required frontmatter:
164
173
 
165
174
  ```markdown
166
175
  ---
@@ -173,6 +182,8 @@ tags: [campaigns, strategy]
173
182
  Article body in Markdown...
174
183
  ```
175
184
 
185
+ 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.
186
+
176
187
  ---
177
188
 
178
189
  ## `ArticlesConfig` reference
@@ -189,6 +200,7 @@ Article body in Markdown...
189
200
  | `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
190
201
  | `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
191
202
  | `showToc` | `boolean` | `true` | Show the table of contents on article detail pages. Set to `false` to hide on all articles. |
203
+ | `showBackToArticles` | `boolean` | `true` | Show the back-navigation link on article detail pages. Set to `false` to hide `ArticleBackLink`. |
192
204
  | `description` | `string` | — | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
193
205
 
194
206
  **Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
@@ -325,6 +337,36 @@ import { ArticleTOC } from '@fullstackdatasolutions/articles/server'
325
337
 
326
338
  ---
327
339
 
340
+ ## ArticleBackLink
341
+
342
+ `ArticleBackLink` renders a back-navigation link on article detail pages. Returns `null` when `config.showBackToArticles === false`, so it is safe to render unconditionally.
343
+
344
+ ```tsx
345
+ import { ArticleBackLink } from '@fullstackdatasolutions/articles'
346
+
347
+ <ArticleBackLink config={siteConfig} />
348
+
349
+ // With custom props:
350
+ <ArticleBackLink
351
+ config={siteConfig}
352
+ href="/blog"
353
+ label="Back to Blog"
354
+ className="custom-class"
355
+ />
356
+ ```
357
+
358
+ | Prop | Type | Required | Description |
359
+ |---|---|---|---|
360
+ | `config` | `ArticlesConfig` | yes | Site config. Must include `showBackToArticles?: boolean` from ArticlesConfig. |
361
+ | `href` | `string` | no | Link destination. Defaults to `/articles`. |
362
+ | `label` | `string` | no | Link text. Defaults to `Back to Articles`. |
363
+ | `className` | `string` | no | Optional CSS class to override default styling. |
364
+
365
+ - Returns `null` when `config.showBackToArticles === false`
366
+ - Gate used in article detail pages: `config.showBackToArticles !== false`
367
+
368
+ ---
369
+
328
370
  ## ScrollToTop
329
371
 
330
372
  `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.
@@ -518,7 +560,7 @@ JSON-LD structured data (`ArticleSchema`, `BreadcrumbSchema`, `CollectionPageSch
518
560
  | `articleType` | `string` | no | Optional article type value included in article metadata/structured data. |
519
561
  | `series` | `string` | no | Optional series label for grouping related articles. |
520
562
 
521
- Articles can be authored as either `article.md` or `article.mdx` inside `public/articles/{slug}/`. 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.
563
+ 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.
522
564
 
523
565
  ---
524
566
 
@@ -572,6 +614,7 @@ And point the Tailwind `@source` at the workspace path:
572
614
 
573
615
  ```css
574
616
  /* app/globals.css */
617
+ @plugin "@tailwindcss/typography";
575
618
  @source "../../packages/articles/src";
576
619
  ```
577
620
 
@@ -593,13 +636,15 @@ This app uses `@fullstackdatasolutions/articles` for the articles section.
593
636
  - `app/api/articles/` — thin API route wrappers; do not add business logic here
594
637
  - `app/api/articles/comments-store.ts` — reference file-backed comment persistence helper used by the comment API routes
595
638
  - `app/articles/feed.xml/route.ts` — RSS 2.0 feed (copy from reference implementation)
596
- - `public/articles/[slug]/article.md` or `article.mdx` — one directory per article
639
+ - `public/articles/[slug]/article.md` or `article.mdx` — one directory per article; nested slugs such as `game-system/article-name` are supported
597
640
 
598
641
  ### CSS (Tailwind v4)
599
642
  The package ships its source TypeScript files so Tailwind can scan them.
600
- `app/globals.css` contains:
643
+ `app/globals.css` must contain:
644
+ @plugin "@tailwindcss/typography";
601
645
  @source "./node_modules/@fullstackdatasolutions/articles/src";
602
- 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.
646
+ 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.
647
+ The `@plugin "@tailwindcss/typography"` line is required for article body prose styling. Install with `pnpm add @tailwindcss/typography`.
603
648
 
604
649
  ### Components
605
650
  - `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.
@@ -610,7 +655,7 @@ For sitemaps and metadata: `import { getArticleSitemapEntries, generateArticlesI
610
655
 
611
656
  ### Rules
612
657
  - Change `config/articles.ts` first; only edit library source if config cannot express the need.
613
- - When adding a new article: create `public/articles/[slug]/article.md` with title, excerpt, author, tags.
658
+ - 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.
614
659
  - API routes are copied from the library — update the package version and re-copy if the library API changes.
615
660
  - `hero.title` and `hero.description` in `ArticlesConfig` control the hero heading — pass them to customize per app.
616
661
  - `showToc?: boolean` (default `true`) controls TOC visibility on all article detail pages.
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,
@@ -1088,18 +1089,42 @@ function ArticleNavigation({ previous, next, basePath }) {
1088
1089
  ] }) });
1089
1090
  }
1090
1091
 
1091
- // src/ArticleTOC.tsx
1092
+ // src/ArticleBackLink.tsx
1093
+ var import_link8 = __toESM(require("next/link"), 1);
1094
+ var import_lucide_react8 = require("lucide-react");
1092
1095
  var import_jsx_runtime14 = require("react/jsx-runtime");
1096
+ function ArticleBackLink({
1097
+ config,
1098
+ href = "/articles",
1099
+ label = "Back to Articles",
1100
+ className
1101
+ }) {
1102
+ if (config.showBackToArticles === false) return null;
1103
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1104
+ import_link8.default,
1105
+ {
1106
+ href,
1107
+ 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",
1108
+ children: [
1109
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react8.ArrowLeft, { className: "h-4 w-4" }),
1110
+ label
1111
+ ]
1112
+ }
1113
+ );
1114
+ }
1115
+
1116
+ // src/ArticleTOC.tsx
1117
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1093
1118
  function ArticleTOC({ toc, className }) {
1094
1119
  if (!toc.length) return null;
1095
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1120
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1096
1121
  "nav",
1097
1122
  {
1098
1123
  "aria-label": "Table of contents",
1099
1124
  className: `mb-8 rounded-lg border border-border bg-muted/40 px-6 py-4 ${className != null ? className : ""}`,
1100
1125
  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)(
1126
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "On this page" }),
1127
+ /* @__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
1128
  "a",
1104
1129
  {
1105
1130
  href: `#${item.id}`,
@@ -1114,8 +1139,8 @@ function ArticleTOC({ toc, className }) {
1114
1139
 
1115
1140
  // src/ScrollToTop.tsx
1116
1141
  var import_react5 = require("react");
1117
- var import_lucide_react8 = require("lucide-react");
1118
- var import_jsx_runtime15 = require("react/jsx-runtime");
1142
+ var import_lucide_react9 = require("lucide-react");
1143
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1119
1144
  function ScrollToTop() {
1120
1145
  const [visible, setVisible] = (0, import_react5.useState)(false);
1121
1146
  (0, import_react5.useEffect)(() => {
@@ -1124,13 +1149,13 @@ function ScrollToTop() {
1124
1149
  return () => globalThis.removeEventListener("scroll", onScroll);
1125
1150
  }, []);
1126
1151
  if (!visible) return null;
1127
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1152
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1128
1153
  "button",
1129
1154
  {
1130
1155
  "aria-label": "Scroll to top",
1131
1156
  onClick: () => globalThis.scrollTo({ top: 0, behavior: "smooth" }),
1132
1157
  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" })
1158
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react9.ArrowUp, { className: "h-5 w-5" })
1134
1159
  }
1135
1160
  );
1136
1161
  }
@@ -1141,7 +1166,7 @@ var import_react9 = require("react");
1141
1166
 
1142
1167
  // src/CommentForm.tsx
1143
1168
  var import_react6 = require("react");
1144
- var import_jsx_runtime16 = require("react/jsx-runtime");
1169
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1145
1170
  var MAX_LENGTH = 2e3;
1146
1171
  var WARN_THRESHOLD = 1800;
1147
1172
  function submitLabel(submitting, parentId) {
@@ -1180,8 +1205,8 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
1180
1205
  }
1181
1206
  });
1182
1207
  }
1183
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-2", children: [
1184
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1208
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-2", children: [
1209
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1185
1210
  "textarea",
1186
1211
  {
1187
1212
  value: body,
@@ -1194,13 +1219,13 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
1194
1219
  "aria-label": parentId ? "Reply text" : "Comment text"
1195
1220
  }
1196
1221
  ),
1197
- remaining <= MAX_LENGTH - WARN_THRESHOLD && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: `text-xs ${remaining < 0 ? "text-destructive" : "text-muted-foreground"}`, children: [
1222
+ remaining <= MAX_LENGTH - WARN_THRESHOLD && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: `text-xs ${remaining < 0 ? "text-destructive" : "text-muted-foreground"}`, children: [
1198
1223
  remaining,
1199
1224
  " characters remaining"
1200
1225
  ] }),
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)(
1226
+ error && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-xs text-destructive", children: error }),
1227
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex gap-2", children: [
1228
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1204
1229
  "button",
1205
1230
  {
1206
1231
  type: "submit",
@@ -1209,7 +1234,7 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
1209
1234
  children: submitLabel(submitting, parentId)
1210
1235
  }
1211
1236
  ),
1212
- onCancel && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1237
+ onCancel && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1213
1238
  "button",
1214
1239
  {
1215
1240
  type: "button",
@@ -1224,7 +1249,7 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
1224
1249
 
1225
1250
  // src/CommentItem.tsx
1226
1251
  var import_react7 = require("react");
1227
- var import_jsx_runtime17 = require("react/jsx-runtime");
1252
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1228
1253
  function relativeTime(iso) {
1229
1254
  const diff = Date.now() - new Date(iso).getTime();
1230
1255
  const minutes = Math.floor(diff / 6e4);
@@ -1262,15 +1287,15 @@ function CommentItem({
1262
1287
  }
1263
1288
  });
1264
1289
  }
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) })
1290
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: `${isReply ? "ml-8 border-l-2 border-border pl-4" : ""}`, children: [
1291
+ /* @__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: [
1292
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
1293
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sm font-medium text-foreground", children: comment.authorName }),
1294
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-xs text-muted-foreground", children: relativeTime(comment.createdAt) })
1270
1295
  ] }),
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)(
1296
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-sm text-foreground whitespace-pre-wrap break-words", children: comment.body }),
1297
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex gap-3 pt-1", children: [
1298
+ canReply && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1274
1299
  "button",
1275
1300
  {
1276
1301
  onClick: () => setShowReplyForm((v) => !v),
@@ -1278,7 +1303,7 @@ function CommentItem({
1278
1303
  children: showReplyForm ? "Cancel" : "Reply"
1279
1304
  }
1280
1305
  ),
1281
- canDelete && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1306
+ canDelete && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1282
1307
  "button",
1283
1308
  {
1284
1309
  onClick: handleDelete,
@@ -1289,7 +1314,7 @@ function CommentItem({
1289
1314
  )
1290
1315
  ] })
1291
1316
  ] }) }),
1292
- showReplyForm && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "mt-2 ml-2", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1317
+ showReplyForm && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "mt-2 ml-2", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1293
1318
  CommentForm,
1294
1319
  {
1295
1320
  articleSlug,
@@ -1301,7 +1326,7 @@ function CommentItem({
1301
1326
  onCancel: () => setShowReplyForm(false)
1302
1327
  }
1303
1328
  ) }),
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)(
1329
+ 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
1330
  CommentItem,
1306
1331
  {
1307
1332
  comment: __spreadProps(__spreadValues({}, reply), { replies: [] }),
@@ -1316,7 +1341,7 @@ function CommentItem({
1316
1341
  }
1317
1342
 
1318
1343
  // src/CommentThread.tsx
1319
- var import_jsx_runtime18 = require("react/jsx-runtime");
1344
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1320
1345
  function CommentThread({
1321
1346
  comments,
1322
1347
  articleSlug,
@@ -1324,9 +1349,9 @@ function CommentThread({
1324
1349
  onRefresh
1325
1350
  }) {
1326
1351
  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." });
1352
+ 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
1353
  }
1329
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "space-y-4", children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1354
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "space-y-4", children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1330
1355
  CommentItem,
1331
1356
  {
1332
1357
  comment,
@@ -1339,7 +1364,7 @@ function CommentThread({
1339
1364
  }
1340
1365
 
1341
1366
  // src/CommentsSection.tsx
1342
- var import_jsx_runtime19 = require("react/jsx-runtime");
1367
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1343
1368
  function CommentsSection({ articleSlug, config }) {
1344
1369
  var _a, _b, _c, _d, _e, _f;
1345
1370
  const { data: session, status } = (0, import_react8.useSession)();
@@ -1371,10 +1396,10 @@ function CommentsSection({ articleSlug, config }) {
1371
1396
  if (!enabled) return null;
1372
1397
  const count = comments.length;
1373
1398
  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)(
1399
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("section", { "aria-label": "Comments", className: "mt-12 pt-8 border-t border-border space-y-6", children: [
1400
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h2", { className: "text-xl font-semibold text-foreground", children: loading ? "Comments" : label }),
1401
+ 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: [
1402
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1378
1403
  "button",
1379
1404
  {
1380
1405
  onClick: () => (0, import_react8.signIn)(),
@@ -1385,8 +1410,8 @@ function CommentsSection({ articleSlug, config }) {
1385
1410
  " ",
1386
1411
  "to join the discussion."
1387
1412
  ] }),
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)(
1413
+ fetchError && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "text-sm text-destructive", children: fetchError }),
1414
+ 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
1415
  CommentThread,
1391
1416
  {
1392
1417
  comments,
@@ -1399,6 +1424,7 @@ function CommentsSection({ articleSlug, config }) {
1399
1424
  }
1400
1425
  // Annotate the CommonJS export names for ESM import in node:
1401
1426
  0 && (module.exports = {
1427
+ ArticleBackLink,
1402
1428
  ArticleCard,
1403
1429
  ArticleCategoryGrid,
1404
1430
  ArticleDetailHero,