@fullstackdatasolutions/articles 0.6.0 → 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).
@@ -191,6 +200,7 @@ Article directories can also be nested below grouping folders. For example, `pub
191
200
  | `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
192
201
  | `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
193
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`. |
194
204
  | `description` | `string` | — | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
195
205
 
196
206
  **Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
@@ -327,6 +337,36 @@ import { ArticleTOC } from '@fullstackdatasolutions/articles/server'
327
337
 
328
338
  ---
329
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
+
330
370
  ## ScrollToTop
331
371
 
332
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.
@@ -574,6 +614,7 @@ And point the Tailwind `@source` at the workspace path:
574
614
 
575
615
  ```css
576
616
  /* app/globals.css */
617
+ @plugin "@tailwindcss/typography";
577
618
  @source "../../packages/articles/src";
578
619
  ```
579
620
 
@@ -599,9 +640,11 @@ This app uses `@fullstackdatasolutions/articles` for the articles section.
599
640
 
600
641
  ### CSS (Tailwind v4)
601
642
  The package ships its source TypeScript files so Tailwind can scan them.
602
- `app/globals.css` contains:
643
+ `app/globals.css` must contain:
644
+ @plugin "@tailwindcss/typography";
603
645
  @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.
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`.
605
648
 
606
649
  ### Components
607
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.
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,