@nextsparkjs/theme-blog 0.1.0-beta.19 → 0.1.0-beta.191
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/api/authors/[username]/route.ts +5 -2
- package/api/authors/docs.md +135 -0
- package/api/authors/presets.ts +45 -0
- package/api/authors/route.ts +4 -1
- package/api/posts/public/docs.md +124 -0
- package/api/posts/public/presets.ts +65 -0
- package/api/posts/public/route.ts +4 -1
- package/components/ExportPostsButton.tsx +2 -1
- package/components/ImportPostsDialog.tsx +2 -1
- package/components/editor/FeaturedImageUpload.tsx +2 -1
- package/components/editor/WysiwygEditor.tsx +56 -7
- package/components/public/AuthorBio.tsx +3 -2
- package/components/public/AuthorCard.tsx +3 -2
- package/components/public/BlogFooter.tsx +2 -1
- package/components/public/PostCard.tsx +8 -7
- package/components/public/RelatedPosts.tsx +2 -1
- package/config/app.config.ts +8 -15
- package/config/billing.config.ts +4 -7
- package/config/dashboard.config.ts +13 -0
- package/config/permissions.config.ts +11 -0
- package/entities/categories/api/docs.md +119 -0
- package/entities/categories/api/presets.ts +67 -0
- package/entities/posts/api/docs.md +174 -0
- package/entities/posts/api/presets.ts +137 -0
- package/lib/sanitize-post-html.ts +52 -0
- package/lib/selectors.ts +2 -3
- package/migrations/090_billing_sample_data.sql +99 -0
- package/migrations/999_theme_sample_data.sql +13 -13
- package/nextsparkjs-theme-blog-0.1.0-beta.137.tgz +0 -0
- package/package.json +13 -5
- package/styles/globals.css +58 -0
- package/templates/(public)/author/[username]/page.tsx +6 -5
- package/templates/(public)/authors/page.tsx +3 -2
- package/templates/(public)/page.tsx +2 -1
- package/templates/(public)/posts/[slug]/page.tsx +7 -5
- package/templates/dashboard/(main)/page.tsx +2 -1
- package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +4 -3
- package/templates/dashboard/(main)/posts/create/page.tsx +2 -1
- package/templates/dashboard/(main)/posts/page.tsx +7 -6
- package/tests/cypress/e2e/README.md +170 -0
- package/tests/cypress/e2e/categories/categories-crud.cy.ts +322 -0
- package/tests/cypress/e2e/categories/categories-crud.md +73 -0
- package/tests/cypress/e2e/posts/posts-crud.cy.ts +460 -0
- package/tests/cypress/e2e/posts/posts-crud.md +115 -0
- package/tests/cypress/e2e/posts/posts-editor.cy.ts +290 -0
- package/tests/cypress/e2e/posts/posts-editor.md +139 -0
- package/tests/cypress/e2e/posts/posts-status-workflow.cy.ts +302 -0
- package/tests/cypress/e2e/posts/posts-status-workflow.md +83 -0
- package/tests/cypress/fixtures/blocks.json +9 -0
- package/tests/cypress/fixtures/entities.json +51 -0
- package/tests/cypress/src/FeaturedImageUpload.js +131 -0
- package/tests/cypress/src/PostEditor.js +386 -0
- package/tests/cypress/src/PostsList.js +350 -0
- package/tests/cypress/src/WysiwygEditor.js +373 -0
- package/tests/cypress/src/components/EntityForm.ts +378 -0
- package/tests/cypress/src/components/EntityList.ts +378 -0
- package/tests/cypress/src/components/PostEditorPOM.ts +447 -0
- package/tests/cypress/src/components/PostsPOM.ts +362 -0
- package/tests/cypress/src/components/index.ts +18 -0
- package/tests/cypress/src/index.js +33 -0
- package/tests/cypress/src/selectors.ts +49 -0
- package/tests/cypress/src/session-helpers.ts +151 -0
- package/tests/cypress/support/e2e.ts +90 -0
- package/tests/cypress.config.ts +154 -0
- package/tests/jest/__mocks__/jose.js +22 -0
- package/tests/jest/__mocks__/next-server.js +56 -0
- package/tests/jest/components/WysiwygEditor.test.tsx +117 -0
- package/tests/jest/jest.config.cjs +148 -0
- package/tests/jest/lib/sanitize-post-html.test.ts +47 -0
- package/tests/jest/setup.ts +170 -0
- package/tests/jest/tsconfig.jest.json +6 -0
- package/tests/tsconfig.json +15 -0
|
@@ -11,6 +11,7 @@ import Link from 'next/link'
|
|
|
11
11
|
import Image from 'next/image'
|
|
12
12
|
import { Calendar, Clock, User } from 'lucide-react'
|
|
13
13
|
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
14
|
+
import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
14
15
|
|
|
15
16
|
interface PostCardProps {
|
|
16
17
|
id: string
|
|
@@ -73,7 +74,7 @@ export function PostCard({
|
|
|
73
74
|
<div data-cy={`post-card-image-${id}`} className="aspect-[21/9] relative overflow-hidden">
|
|
74
75
|
{featuredImage ? (
|
|
75
76
|
<Image
|
|
76
|
-
src={featuredImage}
|
|
77
|
+
src={withBasePathIfInApp(featuredImage)}
|
|
77
78
|
alt={title}
|
|
78
79
|
fill
|
|
79
80
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
|
@@ -115,7 +116,7 @@ export function PostCard({
|
|
|
115
116
|
<Link href={authorUrl} data-cy={`post-card-author-${id}`} className="flex items-center gap-2 hover:text-foreground transition-colors">
|
|
116
117
|
{authorAvatar ? (
|
|
117
118
|
<Image
|
|
118
|
-
src={authorAvatar}
|
|
119
|
+
src={withBasePathIfInApp(authorAvatar)}
|
|
119
120
|
alt={authorName}
|
|
120
121
|
width={24}
|
|
121
122
|
height={24}
|
|
@@ -132,7 +133,7 @@ export function PostCard({
|
|
|
132
133
|
<div data-cy={`post-card-author-${id}`} className="flex items-center gap-2">
|
|
133
134
|
{authorAvatar ? (
|
|
134
135
|
<Image
|
|
135
|
-
src={authorAvatar}
|
|
136
|
+
src={withBasePathIfInApp(authorAvatar)}
|
|
136
137
|
alt={authorName}
|
|
137
138
|
width={24}
|
|
138
139
|
height={24}
|
|
@@ -174,7 +175,7 @@ export function PostCard({
|
|
|
174
175
|
<div data-cy={`post-card-image-${id}`} className="flex-shrink-0 w-24 h-24 relative overflow-hidden rounded-md">
|
|
175
176
|
{featuredImage ? (
|
|
176
177
|
<Image
|
|
177
|
-
src={featuredImage}
|
|
178
|
+
src={withBasePathIfInApp(featuredImage)}
|
|
178
179
|
alt={title}
|
|
179
180
|
fill
|
|
180
181
|
className="object-cover"
|
|
@@ -216,7 +217,7 @@ export function PostCard({
|
|
|
216
217
|
<div data-cy={`post-card-image-${id}`} className="aspect-[16/10] relative overflow-hidden">
|
|
217
218
|
{featuredImage ? (
|
|
218
219
|
<Image
|
|
219
|
-
src={featuredImage}
|
|
220
|
+
src={withBasePathIfInApp(featuredImage)}
|
|
220
221
|
alt={title}
|
|
221
222
|
fill
|
|
222
223
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
|
@@ -256,7 +257,7 @@ export function PostCard({
|
|
|
256
257
|
<Link href={authorUrl} data-cy={`post-card-author-${id}`} className="flex items-center gap-2 hover:text-foreground transition-colors">
|
|
257
258
|
{authorAvatar ? (
|
|
258
259
|
<Image
|
|
259
|
-
src={authorAvatar}
|
|
260
|
+
src={withBasePathIfInApp(authorAvatar)}
|
|
260
261
|
alt={authorName}
|
|
261
262
|
width={20}
|
|
262
263
|
height={20}
|
|
@@ -273,7 +274,7 @@ export function PostCard({
|
|
|
273
274
|
<div data-cy={`post-card-author-${id}`} className="flex items-center gap-2">
|
|
274
275
|
{authorAvatar ? (
|
|
275
276
|
<Image
|
|
276
|
-
src={authorAvatar}
|
|
277
|
+
src={withBasePathIfInApp(authorAvatar)}
|
|
277
278
|
alt={authorName}
|
|
278
279
|
width={20}
|
|
279
280
|
height={20}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import Link from 'next/link'
|
|
10
10
|
import Image from 'next/image'
|
|
11
11
|
import { Clock } from 'lucide-react'
|
|
12
|
+
import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
12
13
|
|
|
13
14
|
interface RelatedPost {
|
|
14
15
|
id: string
|
|
@@ -45,7 +46,7 @@ export function RelatedPosts({
|
|
|
45
46
|
<div data-cy={`related-post-image-${post.id}`} className="aspect-[16/9] relative overflow-hidden">
|
|
46
47
|
{post.featuredImage ? (
|
|
47
48
|
<Image
|
|
48
|
-
src={post.featuredImage}
|
|
49
|
+
src={withBasePathIfInApp(post.featuredImage)}
|
|
49
50
|
alt={post.title}
|
|
50
51
|
fill
|
|
51
52
|
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
package/config/app.config.ts
CHANGED
|
@@ -86,11 +86,10 @@ export const APP_CONFIG_OVERRIDES = {
|
|
|
86
86
|
// =============================================================================
|
|
87
87
|
api: {
|
|
88
88
|
cors: {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
],
|
|
89
|
+
// Theme-specific CORS origins (extends core defaults, does not replace)
|
|
90
|
+
// No additional origins needed for blog theme - uses core defaults
|
|
91
|
+
additionalOrigins: {
|
|
92
|
+
development: [],
|
|
94
93
|
production: [],
|
|
95
94
|
},
|
|
96
95
|
},
|
|
@@ -101,25 +100,19 @@ export const APP_CONFIG_OVERRIDES = {
|
|
|
101
100
|
// =============================================================================
|
|
102
101
|
docs: {
|
|
103
102
|
enabled: true,
|
|
104
|
-
|
|
103
|
+
publicAccess: true,
|
|
105
104
|
searchEnabled: true,
|
|
106
105
|
breadcrumbs: true,
|
|
107
|
-
|
|
106
|
+
public: {
|
|
108
107
|
enabled: true,
|
|
109
108
|
open: true,
|
|
110
109
|
label: "Blog Theme",
|
|
111
110
|
},
|
|
112
|
-
|
|
113
|
-
enabled: false,
|
|
114
|
-
open: false,
|
|
115
|
-
label: "Plugins",
|
|
116
|
-
},
|
|
117
|
-
core: {
|
|
111
|
+
superadmin: {
|
|
118
112
|
enabled: true,
|
|
119
113
|
open: false,
|
|
120
|
-
label: "
|
|
114
|
+
label: "Admin Docs",
|
|
121
115
|
},
|
|
122
|
-
showPluginsDocsInProd: false,
|
|
123
116
|
},
|
|
124
117
|
|
|
125
118
|
// =============================================================================
|
package/config/billing.config.ts
CHANGED
|
@@ -79,8 +79,7 @@ export const billingConfig: BillingConfig = {
|
|
|
79
79
|
storage_gb: 1,
|
|
80
80
|
authors: 1,
|
|
81
81
|
},
|
|
82
|
-
|
|
83
|
-
stripePriceIdYearly: null,
|
|
82
|
+
providerPriceIds: { monthly: null, yearly: null },
|
|
84
83
|
},
|
|
85
84
|
{
|
|
86
85
|
slug: 'pro',
|
|
@@ -100,9 +99,8 @@ export const billingConfig: BillingConfig = {
|
|
|
100
99
|
storage_gb: 10,
|
|
101
100
|
authors: 5,
|
|
102
101
|
},
|
|
103
|
-
// Configure
|
|
104
|
-
|
|
105
|
-
stripePriceIdYearly: 'price_blog_pro_yearly',
|
|
102
|
+
// Configure price IDs in your payment provider dashboard
|
|
103
|
+
providerPriceIds: { monthly: 'price_blog_pro_monthly', yearly: 'price_blog_pro_yearly' },
|
|
106
104
|
},
|
|
107
105
|
{
|
|
108
106
|
slug: 'enterprise',
|
|
@@ -118,8 +116,7 @@ export const billingConfig: BillingConfig = {
|
|
|
118
116
|
storage_gb: -1,
|
|
119
117
|
authors: -1,
|
|
120
118
|
},
|
|
121
|
-
|
|
122
|
-
stripePriceIdYearly: null,
|
|
119
|
+
providerPriceIds: { monthly: null, yearly: null },
|
|
123
120
|
},
|
|
124
121
|
],
|
|
125
122
|
|
|
@@ -48,6 +48,19 @@ export const DASHBOARD_CONFIG = {
|
|
|
48
48
|
devtoolsAccess: {
|
|
49
49
|
enabled: true,
|
|
50
50
|
},
|
|
51
|
+
/**
|
|
52
|
+
* Settings menu dropdown (gear icon)
|
|
53
|
+
*/
|
|
54
|
+
settingsMenu: {
|
|
55
|
+
enabled: true,
|
|
56
|
+
links: [
|
|
57
|
+
{
|
|
58
|
+
label: 'navigation.patterns',
|
|
59
|
+
href: '/dashboard/patterns',
|
|
60
|
+
icon: 'layers',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
51
64
|
userMenu: {
|
|
52
65
|
enabled: true,
|
|
53
66
|
showAvatar: true,
|
|
@@ -48,6 +48,17 @@ export const PERMISSIONS_CONFIG_OVERRIDES: ThemePermissionsConfig = {
|
|
|
48
48
|
{ action: 'update', label: 'Edit categories', description: 'Can modify category information', roles: ['owner'] },
|
|
49
49
|
{ action: 'delete', label: 'Delete categories', description: 'Can delete categories', roles: ['owner'], dangerous: true },
|
|
50
50
|
],
|
|
51
|
+
|
|
52
|
+
// ------------------------------------------
|
|
53
|
+
// PATTERNS
|
|
54
|
+
// ------------------------------------------
|
|
55
|
+
patterns: [
|
|
56
|
+
{ action: 'create', label: 'Create Patterns', description: 'Can create reusable patterns', roles: ['owner'] },
|
|
57
|
+
{ action: 'read', label: 'View Patterns', description: 'Can view pattern details', roles: ['owner'] },
|
|
58
|
+
{ action: 'list', label: 'List Patterns', description: 'Can see the patterns list', roles: ['owner'] },
|
|
59
|
+
{ action: 'update', label: 'Edit Patterns', description: 'Can modify patterns', roles: ['owner'] },
|
|
60
|
+
{ action: 'delete', label: 'Delete Patterns', description: 'Can delete patterns', roles: ['owner'], dangerous: true },
|
|
61
|
+
],
|
|
51
62
|
},
|
|
52
63
|
|
|
53
64
|
// ==========================================
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Categories API
|
|
2
|
+
|
|
3
|
+
Manage blog categories for organizing and filtering posts.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Categories API allows you to create, read, update, and delete category records. Categories provide a hierarchical organization system for blog posts through a many-to-many relationship.
|
|
8
|
+
|
|
9
|
+
## Authentication
|
|
10
|
+
|
|
11
|
+
All endpoints require authentication via:
|
|
12
|
+
- **Session cookie** (for browser-based requests)
|
|
13
|
+
- **API Key** header (for server-to-server requests)
|
|
14
|
+
|
|
15
|
+
## Endpoints
|
|
16
|
+
|
|
17
|
+
### List Categories
|
|
18
|
+
`GET /api/v1/categories`
|
|
19
|
+
|
|
20
|
+
Returns a paginated list of categories.
|
|
21
|
+
|
|
22
|
+
**Query Parameters:**
|
|
23
|
+
- `limit` (number, optional): Maximum records to return. Default: 20
|
|
24
|
+
- `offset` (number, optional): Number of records to skip. Default: 0
|
|
25
|
+
- `search` (string, optional): Search by name, description
|
|
26
|
+
- `sortBy` (string, optional): Field to sort by
|
|
27
|
+
- `sortOrder` (string, optional): Sort direction (asc, desc)
|
|
28
|
+
|
|
29
|
+
**Example Response:**
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"data": [
|
|
33
|
+
{
|
|
34
|
+
"id": "category_abc123",
|
|
35
|
+
"name": "Technology",
|
|
36
|
+
"slug": "technology",
|
|
37
|
+
"description": "Posts about tech and software development",
|
|
38
|
+
"createdAt": "2024-01-15T10:30:00Z",
|
|
39
|
+
"updatedAt": "2024-01-15T10:30:00Z"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"pagination": {
|
|
43
|
+
"total": 8,
|
|
44
|
+
"limit": 20,
|
|
45
|
+
"offset": 0
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Get Single Category
|
|
51
|
+
`GET /api/v1/categories/[id]`
|
|
52
|
+
|
|
53
|
+
Returns a single category by ID.
|
|
54
|
+
|
|
55
|
+
### Create Category
|
|
56
|
+
`POST /api/v1/categories`
|
|
57
|
+
|
|
58
|
+
Create a new category.
|
|
59
|
+
|
|
60
|
+
**Request Body:**
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"name": "Technology",
|
|
64
|
+
"slug": "technology",
|
|
65
|
+
"description": "Posts about tech and software development"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Update Category
|
|
70
|
+
`PATCH /api/v1/categories/[id]`
|
|
71
|
+
|
|
72
|
+
Update an existing category. Supports partial updates.
|
|
73
|
+
|
|
74
|
+
### Delete Category
|
|
75
|
+
`DELETE /api/v1/categories/[id]`
|
|
76
|
+
|
|
77
|
+
Delete a category record. This will remove the category from all associated posts.
|
|
78
|
+
|
|
79
|
+
## Fields
|
|
80
|
+
|
|
81
|
+
| Field | Type | Required | Description |
|
|
82
|
+
|-------|------|----------|-------------|
|
|
83
|
+
| name | text | Yes | Category name |
|
|
84
|
+
| slug | text | Yes | URL-friendly slug (auto-generated if not provided) |
|
|
85
|
+
| description | textarea | No | Category description |
|
|
86
|
+
| createdAt | datetime | Auto | Creation timestamp |
|
|
87
|
+
| updatedAt | datetime | Auto | Last update timestamp |
|
|
88
|
+
|
|
89
|
+
## Post-Category Relationship
|
|
90
|
+
|
|
91
|
+
Categories are linked to posts through a `post_categories` pivot table. When filtering posts by category:
|
|
92
|
+
|
|
93
|
+
1. Use the [Public Posts API](/api/v1/theme/blog/posts/public) with `category` query parameter
|
|
94
|
+
2. Query the pivot table directly for advanced filtering
|
|
95
|
+
|
|
96
|
+
## Features
|
|
97
|
+
|
|
98
|
+
- **Searchable**: name, description
|
|
99
|
+
- **Sortable**: All fields
|
|
100
|
+
- **Metadata**: Supported
|
|
101
|
+
|
|
102
|
+
## Permissions
|
|
103
|
+
|
|
104
|
+
- **Create/Update/Delete**: Owner only
|
|
105
|
+
|
|
106
|
+
## Error Responses
|
|
107
|
+
|
|
108
|
+
| Status | Description |
|
|
109
|
+
|--------|-------------|
|
|
110
|
+
| 400 | Bad Request - Invalid parameters |
|
|
111
|
+
| 401 | Unauthorized - Missing or invalid auth |
|
|
112
|
+
| 403 | Forbidden - Insufficient permissions |
|
|
113
|
+
| 404 | Not Found - Category doesn't exist |
|
|
114
|
+
| 422 | Validation Error - Invalid data |
|
|
115
|
+
|
|
116
|
+
## Related APIs
|
|
117
|
+
|
|
118
|
+
- **[Posts](/api/v1/posts)** - Blog posts
|
|
119
|
+
- **[Public Posts](/api/v1/theme/blog/posts/public)** - Filter by category
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Presets for Categories Entity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineApiEndpoint } from '@nextsparkjs/core/types/api-presets'
|
|
6
|
+
|
|
7
|
+
export default defineApiEndpoint({
|
|
8
|
+
summary: 'Manage blog categories for organizing posts',
|
|
9
|
+
presets: [
|
|
10
|
+
{
|
|
11
|
+
id: 'list-all',
|
|
12
|
+
title: 'List All Categories',
|
|
13
|
+
description: 'Get all categories with pagination',
|
|
14
|
+
method: 'GET',
|
|
15
|
+
params: {
|
|
16
|
+
limit: 50
|
|
17
|
+
},
|
|
18
|
+
tags: ['read', 'list']
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'search-by-name',
|
|
22
|
+
title: 'Search by Name',
|
|
23
|
+
description: 'Search categories by name',
|
|
24
|
+
method: 'GET',
|
|
25
|
+
params: {
|
|
26
|
+
search: '{{name}}'
|
|
27
|
+
},
|
|
28
|
+
tags: ['read', 'search']
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'create-category',
|
|
32
|
+
title: 'Create Category',
|
|
33
|
+
description: 'Create a new category',
|
|
34
|
+
method: 'POST',
|
|
35
|
+
payload: {
|
|
36
|
+
name: 'New Category',
|
|
37
|
+
slug: 'new-category',
|
|
38
|
+
description: 'Category description'
|
|
39
|
+
},
|
|
40
|
+
tags: ['write', 'create']
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'update-category',
|
|
44
|
+
title: 'Update Category',
|
|
45
|
+
description: 'Update an existing category',
|
|
46
|
+
method: 'PATCH',
|
|
47
|
+
pathParams: {
|
|
48
|
+
id: '{{id}}'
|
|
49
|
+
},
|
|
50
|
+
payload: {
|
|
51
|
+
name: '{{name}}',
|
|
52
|
+
description: '{{description}}'
|
|
53
|
+
},
|
|
54
|
+
tags: ['write', 'update']
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'delete-category',
|
|
58
|
+
title: 'Delete Category',
|
|
59
|
+
description: 'Delete a category record',
|
|
60
|
+
method: 'DELETE',
|
|
61
|
+
pathParams: {
|
|
62
|
+
id: '{{id}}'
|
|
63
|
+
},
|
|
64
|
+
tags: ['write', 'delete']
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Posts API
|
|
2
|
+
|
|
3
|
+
Manage blog posts with content builder, featured images, categories, and publication workflow.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Posts API allows you to create, read, update, and delete blog posts. Posts support a flexible content builder system, category organization, and a draft-to-published workflow.
|
|
8
|
+
|
|
9
|
+
## Authentication
|
|
10
|
+
|
|
11
|
+
All endpoints require authentication via:
|
|
12
|
+
- **Session cookie** (for browser-based requests)
|
|
13
|
+
- **API Key** header (for server-to-server requests)
|
|
14
|
+
|
|
15
|
+
**Note:** For public access to published posts, use the [Public Posts API](/api/v1/theme/blog/posts/public) instead.
|
|
16
|
+
|
|
17
|
+
## Endpoints
|
|
18
|
+
|
|
19
|
+
### List Posts
|
|
20
|
+
`GET /api/v1/posts`
|
|
21
|
+
|
|
22
|
+
Returns a paginated list of posts.
|
|
23
|
+
|
|
24
|
+
**Query Parameters:**
|
|
25
|
+
- `limit` (number, optional): Maximum records to return. Default: 20
|
|
26
|
+
- `offset` (number, optional): Number of records to skip. Default: 0
|
|
27
|
+
- `status` (string, optional): Filter by status (draft, published)
|
|
28
|
+
- `featured` (boolean, optional): Filter by featured flag
|
|
29
|
+
- `search` (string, optional): Search by title, excerpt, content
|
|
30
|
+
- `sortBy` (string, optional): Field to sort by
|
|
31
|
+
- `sortOrder` (string, optional): Sort direction (asc, desc)
|
|
32
|
+
|
|
33
|
+
**Example Response:**
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"data": [
|
|
37
|
+
{
|
|
38
|
+
"id": "post_abc123",
|
|
39
|
+
"title": "Getting Started with Next.js",
|
|
40
|
+
"slug": "getting-started-with-nextjs",
|
|
41
|
+
"excerpt": "A comprehensive guide to building modern web apps",
|
|
42
|
+
"content": "[{\"type\":\"text\",\"content\":\"...\"}]",
|
|
43
|
+
"featuredImage": "https://example.com/image.jpg",
|
|
44
|
+
"featured": true,
|
|
45
|
+
"status": "published",
|
|
46
|
+
"publishedAt": "2024-01-15T10:30:00Z",
|
|
47
|
+
"createdAt": "2024-01-10T08:00:00Z",
|
|
48
|
+
"updatedAt": "2024-01-15T10:30:00Z"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"pagination": {
|
|
52
|
+
"total": 25,
|
|
53
|
+
"limit": 20,
|
|
54
|
+
"offset": 0
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Get Single Post
|
|
60
|
+
`GET /api/v1/posts/[id]`
|
|
61
|
+
|
|
62
|
+
Returns a single post by ID.
|
|
63
|
+
|
|
64
|
+
### Create Post
|
|
65
|
+
`POST /api/v1/posts`
|
|
66
|
+
|
|
67
|
+
Create a new blog post.
|
|
68
|
+
|
|
69
|
+
**Request Body:**
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"title": "My New Post",
|
|
73
|
+
"slug": "my-new-post",
|
|
74
|
+
"excerpt": "A brief description of the post",
|
|
75
|
+
"content": "[{\"type\":\"text\",\"content\":\"Post content here\"}]",
|
|
76
|
+
"featuredImage": "https://example.com/image.jpg",
|
|
77
|
+
"featured": false,
|
|
78
|
+
"status": "draft"
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Update Post
|
|
83
|
+
`PATCH /api/v1/posts/[id]`
|
|
84
|
+
|
|
85
|
+
Update an existing post. Supports partial updates.
|
|
86
|
+
|
|
87
|
+
### Publish Post
|
|
88
|
+
`PATCH /api/v1/posts/[id]`
|
|
89
|
+
|
|
90
|
+
Publish a draft post by setting status and publishedAt.
|
|
91
|
+
|
|
92
|
+
**Request Body:**
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"status": "published",
|
|
96
|
+
"publishedAt": "2024-01-15T10:30:00Z"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Delete Post
|
|
101
|
+
`DELETE /api/v1/posts/[id]`
|
|
102
|
+
|
|
103
|
+
Delete a post record.
|
|
104
|
+
|
|
105
|
+
## Fields
|
|
106
|
+
|
|
107
|
+
| Field | Type | Required | Description |
|
|
108
|
+
|-------|------|----------|-------------|
|
|
109
|
+
| title | text | Yes | Post title |
|
|
110
|
+
| slug | text | Yes | URL-friendly slug (auto-generated if not provided) |
|
|
111
|
+
| excerpt | text | No | Brief description for previews |
|
|
112
|
+
| content | json | No | Page builder content blocks |
|
|
113
|
+
| featuredImage | url | No | Featured image URL |
|
|
114
|
+
| featured | boolean | No | Whether post is featured. Default: false |
|
|
115
|
+
| status | select | No | Status: draft, published. Default: draft |
|
|
116
|
+
| publishedAt | datetime | No | Publication date/time |
|
|
117
|
+
| createdAt | datetime | Auto | Creation timestamp |
|
|
118
|
+
| updatedAt | datetime | Auto | Last update timestamp |
|
|
119
|
+
|
|
120
|
+
## Content Builder
|
|
121
|
+
|
|
122
|
+
Posts use a flexible content builder system. The `content` field stores an array of content blocks in JSON format:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
[
|
|
126
|
+
{
|
|
127
|
+
"type": "text",
|
|
128
|
+
"content": "Paragraph content here..."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"type": "heading",
|
|
132
|
+
"level": 2,
|
|
133
|
+
"content": "Section Title"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"type": "image",
|
|
137
|
+
"src": "https://example.com/image.jpg",
|
|
138
|
+
"alt": "Image description"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Categories
|
|
144
|
+
|
|
145
|
+
Posts can be organized into categories using a many-to-many relationship. Categories are managed via the `/api/v1/categories` endpoint and linked to posts through a pivot table.
|
|
146
|
+
|
|
147
|
+
To filter posts by category, use the Public Posts API with the `category` query parameter.
|
|
148
|
+
|
|
149
|
+
## Features
|
|
150
|
+
|
|
151
|
+
- **Searchable**: title, excerpt, content
|
|
152
|
+
- **Sortable**: Most fields
|
|
153
|
+
- **Public Access**: Published posts available via public API
|
|
154
|
+
- **Metadata**: Supported
|
|
155
|
+
|
|
156
|
+
## Permissions
|
|
157
|
+
|
|
158
|
+
- **Create/Update/Delete**: Owner only (single-user blog mode)
|
|
159
|
+
|
|
160
|
+
## Error Responses
|
|
161
|
+
|
|
162
|
+
| Status | Description |
|
|
163
|
+
|--------|-------------|
|
|
164
|
+
| 400 | Bad Request - Invalid parameters |
|
|
165
|
+
| 401 | Unauthorized - Missing or invalid auth |
|
|
166
|
+
| 403 | Forbidden - Insufficient permissions |
|
|
167
|
+
| 404 | Not Found - Post doesn't exist |
|
|
168
|
+
| 422 | Validation Error - Invalid data |
|
|
169
|
+
|
|
170
|
+
## Related APIs
|
|
171
|
+
|
|
172
|
+
- **[Categories](/api/v1/categories)** - Post categorization
|
|
173
|
+
- **[Public Posts](/api/v1/theme/blog/posts/public)** - Public feed (no auth required)
|
|
174
|
+
- **[Authors](/api/v1/theme/blog/authors)** - Author profiles
|