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.
- package/README.md +83 -21
- package/dist/admin/index.cjs +366 -10
- package/dist/admin/index.cjs.map +1 -1
- package/dist/admin/index.d.cts +7 -3
- package/dist/admin/index.d.ts +7 -3
- package/dist/admin/index.js +365 -11
- package/dist/admin/index.js.map +1 -1
- package/dist/api/categories.cjs +32 -32
- package/dist/api/categories.cjs.map +1 -1
- package/dist/api/categories.d.cts +1 -1
- package/dist/api/categories.d.ts +1 -1
- package/dist/api/categories.js +6 -6
- package/dist/api/categories.js.map +1 -1
- package/dist/api/media.cjs +37 -30
- package/dist/api/media.cjs.map +1 -1
- package/dist/api/media.d.cts +1 -1
- package/dist/api/media.d.ts +1 -1
- package/dist/api/media.js +13 -6
- package/dist/api/media.js.map +1 -1
- package/dist/api/posts.cjs +39 -39
- package/dist/api/posts.cjs.map +1 -1
- package/dist/api/posts.d.cts +1 -1
- package/dist/api/posts.d.ts +1 -1
- package/dist/api/posts.js +6 -6
- package/dist/api/posts.js.map +1 -1
- package/dist/api/rss.cjs +3 -3
- package/dist/api/rss.js +2 -2
- package/dist/api/settings.cjs +13 -13
- package/dist/api/settings.cjs.map +1 -1
- package/dist/api/settings.d.cts +1 -1
- package/dist/api/settings.d.ts +1 -1
- package/dist/api/settings.js +5 -5
- package/dist/api/settings.js.map +1 -1
- package/dist/api/sitemap.cjs +3 -3
- package/dist/api/sitemap.js +2 -2
- package/dist/api/tokens.cjs +56 -0
- package/dist/api/tokens.cjs.map +1 -0
- package/dist/api/tokens.d.cts +22 -0
- package/dist/api/tokens.d.ts +22 -0
- package/dist/api/tokens.js +52 -0
- package/dist/api/tokens.js.map +1 -0
- package/dist/{chunk-6HKMZOI4.cjs → chunk-3BKPNOES.cjs} +8 -7
- package/dist/chunk-3BKPNOES.cjs.map +1 -0
- package/dist/{chunk-N5MKAD7J.cjs → chunk-DR7QNI32.cjs} +6 -2
- package/dist/chunk-DR7QNI32.cjs.map +1 -0
- package/dist/{chunk-QE4VLQYN.cjs → chunk-F47RPOTU.cjs} +13 -10
- package/dist/chunk-F47RPOTU.cjs.map +1 -0
- package/dist/{chunk-64HUVJOZ.js → chunk-JI2RK6KX.js} +80 -13
- package/dist/chunk-JI2RK6KX.js.map +1 -0
- package/dist/{chunk-R6MO3QIP.js → chunk-NSR7NYSB.js} +6 -5
- package/dist/chunk-NSR7NYSB.js.map +1 -0
- package/dist/{chunk-4PY224XM.js → chunk-O3XES5O2.js} +6 -3
- package/dist/chunk-O3XES5O2.js.map +1 -0
- package/dist/{chunk-4NKOJYWJ.js → chunk-OOUJYUGP.js} +8 -7
- package/dist/chunk-OOUJYUGP.js.map +1 -0
- package/dist/{chunk-A2S32RZN.js → chunk-OWWWTTUT.js} +8 -3
- package/dist/chunk-OWWWTTUT.js.map +1 -0
- package/dist/{chunk-E2QLTHKN.cjs → chunk-QBZLGBHQ.cjs} +11 -10
- package/dist/chunk-QBZLGBHQ.cjs.map +1 -0
- package/dist/{chunk-ZP5XRVVH.cjs → chunk-SUJT6LWH.cjs} +12 -7
- package/dist/chunk-SUJT6LWH.cjs.map +1 -0
- package/dist/{chunk-JM7QRXXK.js → chunk-TVHY4BR2.js} +10 -7
- package/dist/chunk-TVHY4BR2.js.map +1 -0
- package/dist/{chunk-JLPJKNRZ.js → chunk-UMIBGO4S.js} +18 -5
- package/dist/chunk-UMIBGO4S.js.map +1 -0
- package/dist/{chunk-U2ROR6AY.cjs → chunk-VWKVU3SE.cjs} +86 -12
- package/dist/chunk-VWKVU3SE.cjs.map +1 -0
- package/dist/{chunk-KDZER3PU.cjs → chunk-YTJQ426D.cjs} +19 -5
- package/dist/chunk-YTJQ426D.cjs.map +1 -0
- package/dist/cli/index.cjs +90 -19
- package/dist/components/index.cjs +3 -2
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.cts +2 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +3 -2
- package/dist/components/index.js.map +1 -1
- package/dist/db-OUSQPM53.js +3 -0
- package/dist/db-OUSQPM53.js.map +1 -0
- package/dist/db-RFY6O5UE.cjs +108 -0
- package/dist/db-RFY6O5UE.cjs.map +1 -0
- package/dist/editor/index.cjs +1 -0
- package/dist/editor/index.cjs.map +1 -1
- package/dist/editor/index.js +1 -0
- package/dist/editor/index.js.map +1 -1
- package/dist/{index-vjlZDWNr.d.cts → index-Bk8gOqBq.d.cts} +25 -21
- package/dist/{index-Cgzphklp.d.ts → index-DsnG2kdW.d.ts} +25 -21
- package/dist/index.cjs +47 -47
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -5
- package/dist/lib/index.cjs +39 -35
- package/dist/lib/index.d.cts +2 -2
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.js +5 -5
- package/dist/{types-CBEEBR4A.d.ts → types-Cu515Egx.d.cts} +16 -1
- package/dist/{types-CBEEBR4A.d.cts → types-Cu515Egx.d.ts} +16 -1
- package/package.json +1 -1
- package/dist/chunk-4NKOJYWJ.js.map +0 -1
- package/dist/chunk-4PY224XM.js.map +0 -1
- package/dist/chunk-64HUVJOZ.js.map +0 -1
- package/dist/chunk-6HKMZOI4.cjs.map +0 -1
- package/dist/chunk-A2S32RZN.js.map +0 -1
- package/dist/chunk-E2QLTHKN.cjs.map +0 -1
- package/dist/chunk-JLPJKNRZ.js.map +0 -1
- package/dist/chunk-JM7QRXXK.js.map +0 -1
- package/dist/chunk-KDZER3PU.cjs.map +0 -1
- package/dist/chunk-N5MKAD7J.cjs.map +0 -1
- package/dist/chunk-QE4VLQYN.cjs.map +0 -1
- package/dist/chunk-R6MO3QIP.js.map +0 -1
- package/dist/chunk-U2ROR6AY.cjs.map +0 -1
- 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
|
-
#
|
|
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
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
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. **
|
|
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)
|
package/dist/admin/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
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
|