nextblogkit 0.6.2 → 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.
Files changed (111) hide show
  1. package/README.md +83 -21
  2. package/dist/admin/index.cjs +366 -10
  3. package/dist/admin/index.cjs.map +1 -1
  4. package/dist/admin/index.d.cts +7 -3
  5. package/dist/admin/index.d.ts +7 -3
  6. package/dist/admin/index.js +365 -11
  7. package/dist/admin/index.js.map +1 -1
  8. package/dist/api/categories.cjs +32 -32
  9. package/dist/api/categories.cjs.map +1 -1
  10. package/dist/api/categories.d.cts +1 -1
  11. package/dist/api/categories.d.ts +1 -1
  12. package/dist/api/categories.js +6 -6
  13. package/dist/api/categories.js.map +1 -1
  14. package/dist/api/media.cjs +37 -30
  15. package/dist/api/media.cjs.map +1 -1
  16. package/dist/api/media.d.cts +1 -1
  17. package/dist/api/media.d.ts +1 -1
  18. package/dist/api/media.js +13 -6
  19. package/dist/api/media.js.map +1 -1
  20. package/dist/api/posts.cjs +39 -39
  21. package/dist/api/posts.cjs.map +1 -1
  22. package/dist/api/posts.d.cts +1 -1
  23. package/dist/api/posts.d.ts +1 -1
  24. package/dist/api/posts.js +6 -6
  25. package/dist/api/posts.js.map +1 -1
  26. package/dist/api/rss.cjs +3 -3
  27. package/dist/api/rss.js +2 -2
  28. package/dist/api/settings.cjs +13 -13
  29. package/dist/api/settings.cjs.map +1 -1
  30. package/dist/api/settings.d.cts +1 -1
  31. package/dist/api/settings.d.ts +1 -1
  32. package/dist/api/settings.js +5 -5
  33. package/dist/api/settings.js.map +1 -1
  34. package/dist/api/sitemap.cjs +3 -3
  35. package/dist/api/sitemap.js +2 -2
  36. package/dist/api/tokens.cjs +56 -0
  37. package/dist/api/tokens.cjs.map +1 -0
  38. package/dist/api/tokens.d.cts +22 -0
  39. package/dist/api/tokens.d.ts +22 -0
  40. package/dist/api/tokens.js +52 -0
  41. package/dist/api/tokens.js.map +1 -0
  42. package/dist/{chunk-6HKMZOI4.cjs → chunk-3BKPNOES.cjs} +8 -7
  43. package/dist/chunk-3BKPNOES.cjs.map +1 -0
  44. package/dist/{chunk-N5MKAD7J.cjs → chunk-DR7QNI32.cjs} +6 -2
  45. package/dist/chunk-DR7QNI32.cjs.map +1 -0
  46. package/dist/{chunk-QE4VLQYN.cjs → chunk-F47RPOTU.cjs} +13 -10
  47. package/dist/chunk-F47RPOTU.cjs.map +1 -0
  48. package/dist/{chunk-64HUVJOZ.js → chunk-JI2RK6KX.js} +80 -13
  49. package/dist/chunk-JI2RK6KX.js.map +1 -0
  50. package/dist/{chunk-R6MO3QIP.js → chunk-NSR7NYSB.js} +6 -5
  51. package/dist/chunk-NSR7NYSB.js.map +1 -0
  52. package/dist/{chunk-4PY224XM.js → chunk-O3XES5O2.js} +6 -3
  53. package/dist/chunk-O3XES5O2.js.map +1 -0
  54. package/dist/{chunk-4NKOJYWJ.js → chunk-OOUJYUGP.js} +8 -7
  55. package/dist/chunk-OOUJYUGP.js.map +1 -0
  56. package/dist/{chunk-A2S32RZN.js → chunk-OWWWTTUT.js} +8 -3
  57. package/dist/chunk-OWWWTTUT.js.map +1 -0
  58. package/dist/{chunk-E2QLTHKN.cjs → chunk-QBZLGBHQ.cjs} +11 -10
  59. package/dist/chunk-QBZLGBHQ.cjs.map +1 -0
  60. package/dist/{chunk-ZP5XRVVH.cjs → chunk-SUJT6LWH.cjs} +12 -7
  61. package/dist/chunk-SUJT6LWH.cjs.map +1 -0
  62. package/dist/{chunk-JM7QRXXK.js → chunk-TVHY4BR2.js} +10 -7
  63. package/dist/chunk-TVHY4BR2.js.map +1 -0
  64. package/dist/{chunk-JLPJKNRZ.js → chunk-UMIBGO4S.js} +18 -5
  65. package/dist/chunk-UMIBGO4S.js.map +1 -0
  66. package/dist/{chunk-U2ROR6AY.cjs → chunk-VWKVU3SE.cjs} +86 -12
  67. package/dist/chunk-VWKVU3SE.cjs.map +1 -0
  68. package/dist/{chunk-KDZER3PU.cjs → chunk-YTJQ426D.cjs} +19 -5
  69. package/dist/chunk-YTJQ426D.cjs.map +1 -0
  70. package/dist/cli/index.cjs +90 -19
  71. package/dist/components/index.cjs +3 -2
  72. package/dist/components/index.cjs.map +1 -1
  73. package/dist/components/index.d.cts +2 -1
  74. package/dist/components/index.d.ts +2 -1
  75. package/dist/components/index.js +3 -2
  76. package/dist/components/index.js.map +1 -1
  77. package/dist/db-OUSQPM53.js +3 -0
  78. package/dist/db-OUSQPM53.js.map +1 -0
  79. package/dist/db-RFY6O5UE.cjs +108 -0
  80. package/dist/db-RFY6O5UE.cjs.map +1 -0
  81. package/dist/editor/index.cjs +1 -0
  82. package/dist/editor/index.cjs.map +1 -1
  83. package/dist/editor/index.js +1 -0
  84. package/dist/editor/index.js.map +1 -1
  85. package/dist/{index-vjlZDWNr.d.cts → index-Bk8gOqBq.d.cts} +25 -21
  86. package/dist/{index-Cgzphklp.d.ts → index-DsnG2kdW.d.ts} +25 -21
  87. package/dist/index.cjs +47 -47
  88. package/dist/index.d.cts +3 -3
  89. package/dist/index.d.ts +3 -3
  90. package/dist/index.js +5 -5
  91. package/dist/lib/index.cjs +39 -35
  92. package/dist/lib/index.d.cts +2 -2
  93. package/dist/lib/index.d.ts +2 -2
  94. package/dist/lib/index.js +5 -5
  95. package/dist/{types-CBEEBR4A.d.ts → types-Cu515Egx.d.cts} +16 -1
  96. package/dist/{types-CBEEBR4A.d.cts → types-Cu515Egx.d.ts} +16 -1
  97. package/package.json +1 -1
  98. package/dist/chunk-4NKOJYWJ.js.map +0 -1
  99. package/dist/chunk-4PY224XM.js.map +0 -1
  100. package/dist/chunk-64HUVJOZ.js.map +0 -1
  101. package/dist/chunk-6HKMZOI4.cjs.map +0 -1
  102. package/dist/chunk-A2S32RZN.js.map +0 -1
  103. package/dist/chunk-E2QLTHKN.cjs.map +0 -1
  104. package/dist/chunk-JLPJKNRZ.js.map +0 -1
  105. package/dist/chunk-JM7QRXXK.js.map +0 -1
  106. package/dist/chunk-KDZER3PU.cjs.map +0 -1
  107. package/dist/chunk-N5MKAD7J.cjs.map +0 -1
  108. package/dist/chunk-QE4VLQYN.cjs.map +0 -1
  109. package/dist/chunk-R6MO3QIP.js.map +0 -1
  110. package/dist/chunk-U2ROR6AY.cjs.map +0 -1
  111. package/dist/chunk-ZP5XRVVH.cjs.map +0 -1
package/README.md CHANGED
@@ -18,8 +18,8 @@ A complete blog engine for Next.js — admin panel, block editor, SEO, media sto
18
18
 
19
19
  - **Next.js 14+** with App Router
20
20
  - **MongoDB** (Atlas or self-hosted)
21
- - **Cloudflare R2** bucket (for media storage)
22
21
  - **Node.js 18+**
22
+ - **Cloudflare R2** bucket (optional — for persistent image storage)
23
23
 
24
24
  ## Quick Start
25
25
 
@@ -61,24 +61,31 @@ cp .env.local.example .env.local
61
61
  Fill in your values:
62
62
 
63
63
  ```env
64
+ # ── REQUIRED ─────────────────────────────────────────────
64
65
  # MongoDB Connection
65
66
  NEXTBLOGKIT_MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/mydb
66
67
 
67
- # Cloudflare R2 Storage
68
- NEXTBLOGKIT_R2_ACCOUNT_ID=your-account-id
69
- NEXTBLOGKIT_R2_ACCESS_KEY=your-access-key
70
- NEXTBLOGKIT_R2_SECRET_KEY=your-secret-key
71
- NEXTBLOGKIT_R2_BUCKET=blog-media
72
- NEXTBLOGKIT_R2_PUBLIC_URL=https://media.yourdomain.com
73
-
74
- # Authentication
68
+ # Authentication (must be at least 32 characters)
75
69
  NEXTBLOGKIT_API_KEY=your-secure-api-key-must-be-at-least-32-characters-long
76
70
 
77
- # Site Info
78
- NEXTBLOGKIT_SITE_URL=https://yourdomain.com
79
- NEXTBLOGKIT_SITE_NAME="Your Site Name"
71
+ # ── OPTIONAL ─────────────────────────────────────────────
72
+ # Database name (optional — defaults to the database in your connection URI)
73
+ # NEXTBLOGKIT_MONGODB_DB=nextblogkit
74
+
75
+ # Cloudflare R2 Storage (needed for image uploads; without it, images use temporary blob URLs)
76
+ # NEXTBLOGKIT_R2_ACCOUNT_ID=your-account-id
77
+ # NEXTBLOGKIT_R2_ACCESS_KEY=your-access-key
78
+ # NEXTBLOGKIT_R2_SECRET_KEY=your-secret-key
79
+ # NEXTBLOGKIT_R2_BUCKET=blog-media
80
+ # NEXTBLOGKIT_R2_PUBLIC_URL=https://media.yourdomain.com
81
+
82
+ # Site Info (used in SEO meta tags, RSS, sitemap)
83
+ # NEXTBLOGKIT_SITE_URL=https://yourdomain.com
84
+ # NEXTBLOGKIT_SITE_NAME="Your Site Name"
80
85
  ```
81
86
 
87
+ > **Only `NEXTBLOGKIT_MONGODB_URI` and `NEXTBLOGKIT_API_KEY` are required to start.** `NEXTBLOGKIT_MONGODB_DB` overrides the database name from the URI. R2 variables are needed for persistent image uploads. Site URL/name are used for SEO — defaults are empty/`"Blog"` if omitted.
88
+
82
89
  ### 4. Run database migrations
83
90
 
84
91
  ```bash
@@ -525,7 +532,7 @@ import {
525
532
  | Component | Description |
526
533
  |-----------|-------------|
527
534
  | `BlogCard` | Post preview card (vertical or horizontal layout) |
528
- | `BlogSearch` | Search input with instant results dropdown |
535
+ | `BlogSearch` | Search input with instant results dropdown (accepts `basePath` prop) |
529
536
  | `TableOfContents` | Heading list with scroll-to and active heading tracking |
530
537
  | `ShareButtons` | Social share buttons (Twitter, Facebook, LinkedIn, copy link) |
531
538
  | `ReadingProgressBar` | Top-of-page progress bar tied to scroll position |
@@ -552,6 +559,8 @@ import {
552
559
  SEOPanel,
553
560
  useAdminApi,
554
561
  setApiBase,
562
+ setBasePath,
563
+ getBasePath,
555
564
  } from 'nextblogkit/admin';
556
565
  ```
557
566
 
@@ -565,8 +574,9 @@ Wraps all admin pages with sidebar navigation and authentication.
565
574
  | `apiKey` | `string` | — | Pre-set API key (bypasses login prompt) |
566
575
  | `apiPath` | `string` | `'/api/blog'` | API route prefix for all admin API calls |
567
576
  | `adminPath` | `string` | `'/admin/blog'` | Admin route prefix for sidebar nav links |
577
+ | `basePath` | `string` | `'/blog'` | Public blog URL prefix (used for "View" links in post list and SEO preview) |
568
578
 
569
- **Important:** If you change `apiPath` in your config, you **must** pass it to `AdminLayout`:
579
+ **Important:** If you change `apiPath` or `basePath` in your config, you **must** pass them to `AdminLayout`:
570
580
 
571
581
  ```tsx
572
582
  // app/admin/blog/layout.tsx
@@ -575,7 +585,7 @@ import 'nextblogkit/styles/admin.css';
575
585
 
576
586
  export default function AdminBlogLayout({ children }: { children: React.ReactNode }) {
577
587
  return (
578
- <AdminLayout apiPath="/api/blogs" adminPath="/admin/blog">
588
+ <AdminLayout apiPath="/api/blogs" adminPath="/admin/blog" basePath="/blogs">
579
589
  {children}
580
590
  </AdminLayout>
581
591
  );
@@ -636,6 +646,7 @@ export { GET, POST, PUT, DELETE } from 'nextblogkit/api/posts';
636
646
  export { GET, POST, DELETE } from 'nextblogkit/api/media';
637
647
  export { GET, POST, PUT, DELETE } from 'nextblogkit/api/categories';
638
648
  export { GET, PUT } from 'nextblogkit/api/settings';
649
+ export { GET, POST, DELETE } from 'nextblogkit/api/tokens';
639
650
  export { GET } from 'nextblogkit/api/sitemap';
640
651
  export { GET } from 'nextblogkit/api/rss';
641
652
 
@@ -727,17 +738,67 @@ All API routes require a Bearer token (`NEXTBLOGKIT_API_KEY`) for write operatio
727
738
 
728
739
  > **Note:** These endpoints use the default `/api/blog` prefix. If you changed `apiPath` in your config, replace `/api/blog` with your custom path.
729
740
 
741
+ ### Tokens
742
+
743
+ | Method | Endpoint | Description |
744
+ |--------|----------|-------------|
745
+ | GET | `/api/blog/tokens` | List API tokens (master key only) |
746
+ | POST | `/api/blog/tokens` | Generate new token (master key only) |
747
+ | DELETE | `/api/blog/tokens?id=abc123` | Revoke token (master key only) |
748
+
730
749
  ### Authentication
731
750
 
732
- Include the API key as a Bearer token:
751
+ Include the API key or a generated token as a Bearer token:
733
752
 
734
753
  ```bash
735
754
  curl -X POST http://localhost:3000/api/blog/posts \
736
755
  -H "Authorization: Bearer your-api-key-here" \
737
756
  -H "Content-Type: application/json" \
738
- -d '{"title": "My Post", "content": {...}}'
757
+ -d '{"title": "My Post", "content": [...], "status": "published"}'
758
+ ```
759
+
760
+ **API Tokens** — You can generate scoped API tokens from the admin Settings page (API Access section). Generated tokens work the same as the master key for read/write operations, but cannot manage other tokens. This is useful for CI pipelines, n8n workflows, and other external integrations.
761
+
762
+ ### Create Post — Sample JSON
763
+
764
+ ```json
765
+ {
766
+ "title": "My Blog Post",
767
+ "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Hello world!" }] }],
768
+ "contentHTML": "<p>Hello world!</p>",
769
+ "excerpt": "A short summary of the post",
770
+ "status": "published",
771
+ "categories": ["tech"],
772
+ "tags": ["nextjs", "blog"],
773
+ "author": {
774
+ "name": "John Doe",
775
+ "bio": "Software engineer",
776
+ "avatar": "https://example.com/avatar.jpg"
777
+ },
778
+ "seo": {
779
+ "metaTitle": "My Blog Post | MySite",
780
+ "metaDescription": "A short summary for search engines",
781
+ "focusKeyword": "blog post"
782
+ }
783
+ }
739
784
  ```
740
785
 
786
+ | Field | Type | Required | Description |
787
+ |-------|------|----------|-------------|
788
+ | `title` | string | Yes | Post title |
789
+ | `content` | BlockContent[] | No | TipTap JSON content blocks |
790
+ | `contentHTML` | string | No | HTML version of the content |
791
+ | `excerpt` | string | No | Short summary (auto-generated if omitted) |
792
+ | `slug` | string | No | URL slug (auto-generated from title if omitted) |
793
+ | `status` | `"draft"` \| `"published"` \| `"scheduled"` | No | Defaults to `"draft"` |
794
+ | `categories` | string[] | No | Category slugs |
795
+ | `tags` | string[] | No | Tag strings |
796
+ | `author` | `{ name, bio?, avatar?, url? }` | No | Post author info |
797
+ | `seo` | `{ metaTitle?, metaDescription?, focusKeyword?, ... }` | No | SEO metadata |
798
+ | `coverImage` | `{ _id, url, alt?, caption? }` | No | Cover image reference |
799
+ | `publishedAt` | ISO date string | No | Publish date (auto-set when status is `"published"`) |
800
+ | `scheduledAt` | ISO date string | No | Schedule date for future publishing |
801
+
741
802
  ---
742
803
 
743
804
  ## Editor Slash Commands
@@ -852,6 +913,7 @@ app/
852
913
  ├── media/route.ts # Media upload/list/delete
853
914
  ├── categories/route.ts # Categories CRUD
854
915
  ├── settings/route.ts # Settings read/update
916
+ ├── tokens/route.ts # API token management
855
917
  ├── sitemap.xml/route.ts # Dynamic sitemap
856
918
  └── rss.xml/route.ts # RSS feed
857
919
  ```
@@ -862,7 +924,7 @@ Because these are thin wrappers, you can customize any page by editing the gener
862
924
 
863
925
  ## Local Development (without R2)
864
926
 
865
- If you want to try NextBlogKit locally without Cloudflare R2, images will fall back to `URL.createObjectURL` (browser-only, non-persistent). For a fully working local setup:
927
+ NextBlogKit works with just **MongoDB + API key**. Cloudflare R2 is optional — without it, the editor uses temporary blob URLs for images (they won't persist across reloads), and the upload API returns a clear 503 error. This lets you develop locally and add R2 when you're ready.
866
928
 
867
929
  1. **MongoDB** — Use a free [MongoDB Atlas](https://www.mongodb.com/atlas) cluster, or run locally:
868
930
  ```bash
@@ -871,13 +933,13 @@ If you want to try NextBlogKit locally without Cloudflare R2, images will fall b
871
933
  # Then use: NEXTBLOGKIT_MONGODB_URI=mongodb://localhost:27017/nextblogkit
872
934
  ```
873
935
 
874
- 2. **R2** — Create a free Cloudflare R2 bucket at [dash.cloudflare.com](https://dash.cloudflare.com). The free tier includes 10 GB storage and 10 million reads/month.
875
-
876
- 3. **API Key** — Generate a secure key:
936
+ 2. **API Key** — Generate a secure key:
877
937
  ```bash
878
938
  openssl rand -hex 32
879
939
  ```
880
940
 
941
+ 3. **R2 (optional)** — Create a free Cloudflare R2 bucket at [dash.cloudflare.com](https://dash.cloudflare.com) for persistent image storage. The free tier includes 10 GB storage and 10 million reads/month.
942
+
881
943
  ---
882
944
 
883
945
  ## Project Structure (package internals)
@@ -39,9 +39,16 @@ var CodeBlockLowlight__default = /*#__PURE__*/_interopDefault(CodeBlockLowlight)
39
39
 
40
40
  // src/admin/AdminLayout.tsx
41
41
  var _apiBase = "/api/blog";
42
+ var _basePath = "/blog";
42
43
  function setApiBase(path) {
43
44
  _apiBase = path;
44
45
  }
46
+ function setBasePath(path) {
47
+ _basePath = path;
48
+ }
49
+ function getBasePath() {
50
+ return _basePath;
51
+ }
45
52
  function getApiKey() {
46
53
  if (typeof window === "undefined") return "";
47
54
  return sessionStorage.getItem("nbk_api_key") || "";
@@ -102,7 +109,7 @@ function buildNavItems(adminPath) {
102
109
  { label: "Settings", href: `${adminPath}/settings`, icon: "\u2699\uFE0F" }
103
110
  ];
104
111
  }
105
- function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog" }) {
112
+ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog", basePath = "/blog" }) {
106
113
  const [isAuthenticated, setIsAuthenticated] = react.useState(false);
107
114
  const [inputKey, setInputKey] = react.useState("");
108
115
  const [sidebarOpen, setSidebarOpen] = react.useState(true);
@@ -111,7 +118,10 @@ function AdminLayout({ children, apiKey, apiPath, adminPath = "/admin/blog" }) {
111
118
  if (apiPath) {
112
119
  setApiBase(apiPath);
113
120
  }
114
- }, [apiPath]);
121
+ if (basePath) {
122
+ setBasePath(basePath);
123
+ }
124
+ }, [apiPath, basePath]);
115
125
  react.useEffect(() => {
116
126
  if (typeof window !== "undefined") {
117
127
  setCurrentPath(window.location.pathname);
@@ -449,7 +459,7 @@ function PostList() {
449
459
  /* @__PURE__ */ jsxRuntime.jsx(
450
460
  "a",
451
461
  {
452
- href: `/blog/${post.slug}`,
462
+ href: `${getBasePath()}/${post.slug}`,
453
463
  target: "_blank",
454
464
  rel: "noopener noreferrer",
455
465
  className: "nbk-btn nbk-btn-sm",
@@ -997,6 +1007,7 @@ function BlogEditor({
997
1007
  const lastSavedRef = react.useRef("");
998
1008
  const defaultUpload = react.useCallback(async (file) => {
999
1009
  if (!uploadImage) {
1010
+ console.warn("[NextBlogKit] No uploadImage handler provided. Using blob URL \u2014 image will not persist across page reloads. Configure Cloudflare R2 for persistent image storage.");
1000
1011
  return { url: URL.createObjectURL(file), alt: file.name };
1001
1012
  }
1002
1013
  return uploadImage(file);
@@ -1450,7 +1461,7 @@ function stripTags(html) {
1450
1461
  function slugify(text) {
1451
1462
  return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-");
1452
1463
  }
1453
- function SEOPanel({ seo, onChange, title, slug, excerpt }) {
1464
+ function SEOPanel({ seo, onChange, title, slug, excerpt, basePath = "/blog" }) {
1454
1465
  const metaTitle = seo.metaTitle || "";
1455
1466
  const metaDescription = seo.metaDescription || "";
1456
1467
  const focusKeyword = seo.focusKeyword || "";
@@ -1459,7 +1470,7 @@ function SEOPanel({ seo, onChange, title, slug, excerpt }) {
1459
1470
  const noIndex = seo.noIndex || false;
1460
1471
  const displayTitle = metaTitle || title || "Post Title";
1461
1472
  const displayDesc = metaDescription || excerpt || "Post description will appear here...";
1462
- const displayUrl = `/blog/${slug || "post-url"}`;
1473
+ const displayUrl = `${basePath}/${slug || "post-url"}`;
1463
1474
  const titleLength = displayTitle.length;
1464
1475
  const descLength = displayDesc.length;
1465
1476
  const titleColor = titleLength >= 50 && titleLength <= 60 ? "nbk-count-good" : titleLength > 70 ? "nbk-count-bad" : "nbk-count-warn";
@@ -1671,10 +1682,20 @@ function PostEditor({ postId }) {
1671
1682
  [postId]
1672
1683
  );
1673
1684
  const uploadImage = async (file) => {
1674
- const formData = new FormData();
1675
- formData.append("file", file);
1676
- const res = await api.post("/media", formData);
1677
- return { url: res.data.url, alt: res.data.alt || file.name };
1685
+ try {
1686
+ const formData = new FormData();
1687
+ formData.append("file", file);
1688
+ const res = await api.post("/media", formData);
1689
+ return { url: res.data.url, alt: res.data.alt || file.name };
1690
+ } catch (err) {
1691
+ const msg = err.message || "Upload failed";
1692
+ if (msg.includes("R2") || msg.includes("STORAGE_NOT_CONFIGURED")) {
1693
+ setError("Image upload requires Cloudflare R2 configuration. Set R2 environment variables or use an external image URL instead.");
1694
+ } else {
1695
+ setError(msg);
1696
+ }
1697
+ throw err;
1698
+ }
1678
1699
  };
1679
1700
  if (loading) {
1680
1701
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-post-editor", children: [
@@ -1896,7 +1917,8 @@ function PostEditor({ postId }) {
1896
1917
  onChange: setSeo,
1897
1918
  title,
1898
1919
  slug,
1899
- excerpt
1920
+ excerpt,
1921
+ basePath: getBasePath()
1900
1922
  }
1901
1923
  )
1902
1924
  ] })
@@ -2255,6 +2277,333 @@ function CategoryManager() {
2255
2277
  ] })
2256
2278
  ] });
2257
2279
  }
2280
+ function ApiTokensSection() {
2281
+ const api = useAdminApi();
2282
+ const [tokens, setTokens] = react.useState([]);
2283
+ const [loading, setLoading] = react.useState(true);
2284
+ const [tokenName, setTokenName] = react.useState("");
2285
+ const [generating, setGenerating] = react.useState(false);
2286
+ const [newToken, setNewToken] = react.useState("");
2287
+ const [copied, setCopied] = react.useState(false);
2288
+ const [error, setError] = react.useState("");
2289
+ const [showDialog, setShowDialog] = react.useState(false);
2290
+ const [revoking, setRevoking] = react.useState(null);
2291
+ const fetchTokens = react.useCallback(async () => {
2292
+ try {
2293
+ const res = await api.get("/tokens");
2294
+ setTokens(res.data || []);
2295
+ } catch (err) {
2296
+ setError(err.message || "Failed to load tokens");
2297
+ } finally {
2298
+ setLoading(false);
2299
+ }
2300
+ }, []);
2301
+ react.useEffect(() => {
2302
+ fetchTokens();
2303
+ }, [fetchTokens]);
2304
+ const handleGenerate = async () => {
2305
+ if (!tokenName.trim()) return;
2306
+ setGenerating(true);
2307
+ setError("");
2308
+ try {
2309
+ const res = await api.post("/tokens", { name: tokenName.trim() });
2310
+ setNewToken(res.data.plainToken);
2311
+ setTokenName("");
2312
+ fetchTokens();
2313
+ } catch (err) {
2314
+ setError(err.message || "Failed to generate token");
2315
+ } finally {
2316
+ setGenerating(false);
2317
+ }
2318
+ };
2319
+ const handleRevoke = async (id) => {
2320
+ if (!confirm("Are you sure you want to revoke this token? Any integrations using it will stop working.")) return;
2321
+ setRevoking(id);
2322
+ setError("");
2323
+ try {
2324
+ await api.del(`/tokens?id=${id}`);
2325
+ setTokens((prev) => prev.filter((t) => t._id !== id));
2326
+ } catch (err) {
2327
+ setError(err.message || "Failed to revoke token");
2328
+ } finally {
2329
+ setRevoking(null);
2330
+ }
2331
+ };
2332
+ const copyToken = () => {
2333
+ navigator.clipboard.writeText(newToken);
2334
+ setCopied(true);
2335
+ setTimeout(() => setCopied(false), 2e3);
2336
+ };
2337
+ const formatDate = (d) => {
2338
+ if (!d) return "Never";
2339
+ return new Date(d).toLocaleDateString("en-US", {
2340
+ month: "short",
2341
+ day: "numeric",
2342
+ year: "numeric",
2343
+ hour: "2-digit",
2344
+ minute: "2-digit"
2345
+ });
2346
+ };
2347
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-settings-section", children: [
2348
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbk-section-title", children: "API Tokens" }),
2349
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "var(--nbk-text-muted)", fontSize: "0.875rem", marginBottom: "1rem" }, children: "Generate tokens for external services (CI pipelines, automation tools, CMS integrations). Tokens can access the API like the master key but cannot manage other tokens." }),
2350
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nbk-error", style: { marginBottom: "1rem" }, children: error }),
2351
+ newToken && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
2352
+ background: "var(--nbk-bg-secondary)",
2353
+ border: "1px solid var(--nbk-warning, #f59e0b)",
2354
+ borderRadius: "var(--nbk-radius)",
2355
+ padding: "1rem",
2356
+ marginBottom: "1rem"
2357
+ }, children: [
2358
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, marginBottom: "0.5rem", color: "var(--nbk-warning, #f59e0b)" }, children: "Save this token \u2014 it will only be shown once" }),
2359
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.5rem", alignItems: "center" }, children: [
2360
+ /* @__PURE__ */ jsxRuntime.jsx("code", { style: {
2361
+ flex: 1,
2362
+ background: "var(--nbk-bg)",
2363
+ padding: "0.5rem 0.75rem",
2364
+ borderRadius: "var(--nbk-radius)",
2365
+ border: "1px solid var(--nbk-border)",
2366
+ fontSize: "0.813rem",
2367
+ fontFamily: "var(--nbk-font-code)",
2368
+ wordBreak: "break-all"
2369
+ }, children: newToken }),
2370
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: copyToken, className: "nbk-btn nbk-btn-primary", style: { whiteSpace: "nowrap" }, children: copied ? "Copied!" : "Copy" })
2371
+ ] }),
2372
+ /* @__PURE__ */ jsxRuntime.jsx(
2373
+ "button",
2374
+ {
2375
+ onClick: () => setNewToken(""),
2376
+ style: {
2377
+ marginTop: "0.5rem",
2378
+ background: "none",
2379
+ border: "none",
2380
+ color: "var(--nbk-text-muted)",
2381
+ cursor: "pointer",
2382
+ fontSize: "0.813rem",
2383
+ padding: 0
2384
+ },
2385
+ children: "Dismiss"
2386
+ }
2387
+ )
2388
+ ] }),
2389
+ !showDialog ? /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setShowDialog(true), className: "nbk-btn nbk-btn-primary", style: { marginBottom: "1rem" }, children: "Generate New Token" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
2390
+ display: "flex",
2391
+ gap: "0.5rem",
2392
+ marginBottom: "1rem",
2393
+ alignItems: "flex-end"
2394
+ }, children: [
2395
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1 }, children: [
2396
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "nbk-label", children: "Token Name" }),
2397
+ /* @__PURE__ */ jsxRuntime.jsx(
2398
+ "input",
2399
+ {
2400
+ type: "text",
2401
+ value: tokenName,
2402
+ onChange: (e) => setTokenName(e.target.value),
2403
+ className: "nbk-input",
2404
+ placeholder: "e.g. CI Pipeline, n8n Automation",
2405
+ onKeyDown: (e) => e.key === "Enter" && handleGenerate(),
2406
+ autoFocus: true
2407
+ }
2408
+ )
2409
+ ] }),
2410
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleGenerate, className: "nbk-btn nbk-btn-primary", disabled: generating || !tokenName.trim(), children: generating ? "Generating..." : "Generate" }),
2411
+ /* @__PURE__ */ jsxRuntime.jsx(
2412
+ "button",
2413
+ {
2414
+ onClick: () => {
2415
+ setShowDialog(false);
2416
+ setTokenName("");
2417
+ },
2418
+ className: "nbk-btn",
2419
+ style: { background: "var(--nbk-bg-secondary)", border: "1px solid var(--nbk-border)" },
2420
+ children: "Cancel"
2421
+ }
2422
+ )
2423
+ ] }),
2424
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "var(--nbk-text-muted)" }, children: "Loading tokens..." }) : tokens.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "var(--nbk-text-muted)", fontSize: "0.875rem" }, children: "No API tokens generated yet." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { style: {
2425
+ width: "100%",
2426
+ borderCollapse: "collapse",
2427
+ fontSize: "0.875rem"
2428
+ }, children: [
2429
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { borderBottom: "2px solid var(--nbk-border)" }, children: [
2430
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Name" }),
2431
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Key Prefix" }),
2432
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Last Used" }),
2433
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Created" }),
2434
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "right", padding: "0.5rem 0.75rem", fontWeight: 600 } })
2435
+ ] }) }),
2436
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: tokens.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { borderBottom: "1px solid var(--nbk-border)" }, children: [
2437
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem" }, children: t.name }),
2438
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem" }, children: /* @__PURE__ */ jsxRuntime.jsxs("code", { style: { fontFamily: "var(--nbk-font-code)", fontSize: "0.813rem" }, children: [
2439
+ t.prefix,
2440
+ "..."
2441
+ ] }) }),
2442
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", color: "var(--nbk-text-muted)" }, children: formatDate(t.lastUsedAt) }),
2443
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", color: "var(--nbk-text-muted)" }, children: formatDate(t.createdAt) }),
2444
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", textAlign: "right" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2445
+ "button",
2446
+ {
2447
+ onClick: () => handleRevoke(t._id),
2448
+ disabled: revoking === t._id,
2449
+ style: {
2450
+ background: "none",
2451
+ border: "1px solid var(--nbk-danger, #ef4444)",
2452
+ color: "var(--nbk-danger, #ef4444)",
2453
+ padding: "0.25rem 0.75rem",
2454
+ borderRadius: "var(--nbk-radius)",
2455
+ cursor: "pointer",
2456
+ fontSize: "0.813rem"
2457
+ },
2458
+ children: revoking === t._id ? "Revoking..." : "Revoke"
2459
+ }
2460
+ ) })
2461
+ ] }, t._id)) })
2462
+ ] }) })
2463
+ ] });
2464
+ }
2465
+ function ApiReferenceSection() {
2466
+ const [open, setOpen] = react.useState(false);
2467
+ const [copiedCurl, setCopiedCurl] = react.useState(false);
2468
+ const [copiedJson, setCopiedJson] = react.useState(false);
2469
+ const sampleJson = `{
2470
+ "title": "My Blog Post",
2471
+ "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Hello world!" }] }],
2472
+ "contentHTML": "<p>Hello world!</p>",
2473
+ "excerpt": "A short summary of the post",
2474
+ "status": "published",
2475
+ "categories": ["tech"],
2476
+ "tags": ["nextjs", "blog"],
2477
+ "author": {
2478
+ "name": "John Doe",
2479
+ "bio": "Software engineer",
2480
+ "avatar": "https://example.com/avatar.jpg"
2481
+ },
2482
+ "seo": {
2483
+ "metaTitle": "My Blog Post | MySite",
2484
+ "metaDescription": "A short summary for search engines",
2485
+ "focusKeyword": "blog post"
2486
+ }
2487
+ }`;
2488
+ const sampleCurl = `curl -X POST https://yoursite.com/api/blog/posts \\
2489
+ -H "Authorization: Bearer nbk_your-token-here" \\
2490
+ -H "Content-Type: application/json" \\
2491
+ -d '{ "title": "My Post", "content": [{"type":"paragraph","content":[{"type":"text","text":"Hello!"}]}], "contentHTML": "<p>Hello!</p>", "status": "published" }'`;
2492
+ const copyText = (text, setCopied) => {
2493
+ navigator.clipboard.writeText(text);
2494
+ setCopied(true);
2495
+ setTimeout(() => setCopied(false), 2e3);
2496
+ };
2497
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nbk-settings-section", children: [
2498
+ /* @__PURE__ */ jsxRuntime.jsxs(
2499
+ "h2",
2500
+ {
2501
+ className: "nbk-section-title",
2502
+ onClick: () => setOpen(!open),
2503
+ style: { cursor: "pointer", userSelect: "none", display: "flex", alignItems: "center", gap: "0.5rem" },
2504
+ children: [
2505
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { transform: open ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.2s", display: "inline-block" }, children: "\u25B6" }),
2506
+ "API Reference"
2507
+ ]
2508
+ }
2509
+ ),
2510
+ open && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: "1rem" }, children: [
2511
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: { fontSize: "0.938rem", fontWeight: 600, marginBottom: "0.75rem" }, children: "Create Post \u2014 POST /api/blog/posts" }),
2512
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1rem" }, children: [
2513
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.25rem" }, children: [
2514
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.813rem", fontWeight: 600, color: "var(--nbk-text-muted)" }, children: "Sample JSON Body" }),
2515
+ /* @__PURE__ */ jsxRuntime.jsx(
2516
+ "button",
2517
+ {
2518
+ onClick: () => copyText(sampleJson, setCopiedJson),
2519
+ style: {
2520
+ background: "none",
2521
+ border: "1px solid var(--nbk-border)",
2522
+ borderRadius: "var(--nbk-radius)",
2523
+ padding: "0.125rem 0.5rem",
2524
+ cursor: "pointer",
2525
+ fontSize: "0.75rem",
2526
+ color: "var(--nbk-text-muted)"
2527
+ },
2528
+ children: copiedJson ? "Copied!" : "Copy"
2529
+ }
2530
+ )
2531
+ ] }),
2532
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { style: {
2533
+ background: "var(--nbk-bg-secondary)",
2534
+ border: "1px solid var(--nbk-border)",
2535
+ borderRadius: "var(--nbk-radius)",
2536
+ padding: "0.75rem",
2537
+ overflow: "auto",
2538
+ fontSize: "0.813rem",
2539
+ fontFamily: "var(--nbk-font-code)",
2540
+ lineHeight: 1.5,
2541
+ maxHeight: "400px"
2542
+ }, children: sampleJson })
2543
+ ] }),
2544
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1rem" }, children: [
2545
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.25rem" }, children: [
2546
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.813rem", fontWeight: 600, color: "var(--nbk-text-muted)" }, children: "Sample curl Command" }),
2547
+ /* @__PURE__ */ jsxRuntime.jsx(
2548
+ "button",
2549
+ {
2550
+ onClick: () => copyText(sampleCurl, setCopiedCurl),
2551
+ style: {
2552
+ background: "none",
2553
+ border: "1px solid var(--nbk-border)",
2554
+ borderRadius: "var(--nbk-radius)",
2555
+ padding: "0.125rem 0.5rem",
2556
+ cursor: "pointer",
2557
+ fontSize: "0.75rem",
2558
+ color: "var(--nbk-text-muted)"
2559
+ },
2560
+ children: copiedCurl ? "Copied!" : "Copy"
2561
+ }
2562
+ )
2563
+ ] }),
2564
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { style: {
2565
+ background: "var(--nbk-bg-secondary)",
2566
+ border: "1px solid var(--nbk-border)",
2567
+ borderRadius: "var(--nbk-radius)",
2568
+ padding: "0.75rem",
2569
+ overflow: "auto",
2570
+ fontSize: "0.813rem",
2571
+ fontFamily: "var(--nbk-font-code)",
2572
+ lineHeight: 1.5
2573
+ }, children: sampleCurl })
2574
+ ] }),
2575
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: { fontSize: "0.938rem", fontWeight: 600, marginBottom: "0.75rem" }, children: "Field Reference" }),
2576
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: "0.813rem" }, children: [
2577
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { borderBottom: "2px solid var(--nbk-border)" }, children: [
2578
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Field" }),
2579
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Type" }),
2580
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Required" }),
2581
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { textAlign: "left", padding: "0.5rem 0.75rem", fontWeight: 600 }, children: "Description" })
2582
+ ] }) }),
2583
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: [
2584
+ ["title", "string", "Yes", "Post title"],
2585
+ ["content", "BlockContent[]", "No", "TipTap JSON content blocks"],
2586
+ ["contentHTML", "string", "No", "HTML version of the content"],
2587
+ ["excerpt", "string", "No", "Short summary (auto-generated if omitted)"],
2588
+ ["slug", "string", "No", "URL slug (auto-generated from title if omitted)"],
2589
+ ["status", '"draft" | "published" | "scheduled"', "No", 'Defaults to "draft"'],
2590
+ ["categories", "string[]", "No", "Category slugs"],
2591
+ ["tags", "string[]", "No", "Tag strings"],
2592
+ ["author", "{ name, bio?, avatar?, url? }", "No", "Post author info"],
2593
+ ["seo", "{ metaTitle?, metaDescription?, focusKeyword?, ... }", "No", "SEO metadata"],
2594
+ ["coverImage", "{ _id, url, alt?, caption? }", "No", "Cover image reference"],
2595
+ ["publishedAt", "ISO date string", "No", 'Publish date (auto-set when status is "published")'],
2596
+ ["scheduledAt", "ISO date string", "No", "Schedule date for future publishing"]
2597
+ ].map(([field, type, required, desc]) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { style: { borderBottom: "1px solid var(--nbk-border)" }, children: [
2598
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem" }, children: /* @__PURE__ */ jsxRuntime.jsx("code", { style: { fontFamily: "var(--nbk-font-code)", fontSize: "0.813rem" }, children: field }) }),
2599
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", color: "var(--nbk-text-muted)" }, children: type }),
2600
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem" }, children: required }),
2601
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "0.5rem 0.75rem", color: "var(--nbk-text-muted)" }, children: desc })
2602
+ ] }, field)) })
2603
+ ] }) })
2604
+ ] })
2605
+ ] });
2606
+ }
2258
2607
  function SettingsPage() {
2259
2608
  const api = useAdminApi();
2260
2609
  const [settings, setSettings] = react.useState({});
@@ -2447,6 +2796,11 @@ function SettingsPage() {
2447
2796
  placeholder: "/* Custom CSS styles */"
2448
2797
  }
2449
2798
  ) })
2799
+ ] }),
2800
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: "2rem" }, children: [
2801
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "nbk-page-title", style: { fontSize: "1.25rem", marginBottom: "1rem" }, children: "API Access" }),
2802
+ /* @__PURE__ */ jsxRuntime.jsx(ApiTokensSection, {}),
2803
+ /* @__PURE__ */ jsxRuntime.jsx(ApiReferenceSection, {})
2450
2804
  ] })
2451
2805
  ] });
2452
2806
  }
@@ -2459,7 +2813,9 @@ exports.PostEditor = PostEditor;
2459
2813
  exports.PostList = PostList;
2460
2814
  exports.SEOPanel = SEOPanel;
2461
2815
  exports.SettingsPage = SettingsPage;
2816
+ exports.getBasePath = getBasePath;
2462
2817
  exports.setApiBase = setApiBase;
2818
+ exports.setBasePath = setBasePath;
2463
2819
  exports.useAdminApi = useAdminApi;
2464
2820
  //# sourceMappingURL=index.cjs.map
2465
2821
  //# sourceMappingURL=index.cjs.map