@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.
Files changed (72) hide show
  1. package/api/authors/[username]/route.ts +5 -2
  2. package/api/authors/docs.md +135 -0
  3. package/api/authors/presets.ts +45 -0
  4. package/api/authors/route.ts +4 -1
  5. package/api/posts/public/docs.md +124 -0
  6. package/api/posts/public/presets.ts +65 -0
  7. package/api/posts/public/route.ts +4 -1
  8. package/components/ExportPostsButton.tsx +2 -1
  9. package/components/ImportPostsDialog.tsx +2 -1
  10. package/components/editor/FeaturedImageUpload.tsx +2 -1
  11. package/components/editor/WysiwygEditor.tsx +56 -7
  12. package/components/public/AuthorBio.tsx +3 -2
  13. package/components/public/AuthorCard.tsx +3 -2
  14. package/components/public/BlogFooter.tsx +2 -1
  15. package/components/public/PostCard.tsx +8 -7
  16. package/components/public/RelatedPosts.tsx +2 -1
  17. package/config/app.config.ts +8 -15
  18. package/config/billing.config.ts +4 -7
  19. package/config/dashboard.config.ts +13 -0
  20. package/config/permissions.config.ts +11 -0
  21. package/entities/categories/api/docs.md +119 -0
  22. package/entities/categories/api/presets.ts +67 -0
  23. package/entities/posts/api/docs.md +174 -0
  24. package/entities/posts/api/presets.ts +137 -0
  25. package/lib/sanitize-post-html.ts +52 -0
  26. package/lib/selectors.ts +2 -3
  27. package/migrations/090_billing_sample_data.sql +99 -0
  28. package/migrations/999_theme_sample_data.sql +13 -13
  29. package/nextsparkjs-theme-blog-0.1.0-beta.137.tgz +0 -0
  30. package/package.json +13 -5
  31. package/styles/globals.css +58 -0
  32. package/templates/(public)/author/[username]/page.tsx +6 -5
  33. package/templates/(public)/authors/page.tsx +3 -2
  34. package/templates/(public)/page.tsx +2 -1
  35. package/templates/(public)/posts/[slug]/page.tsx +7 -5
  36. package/templates/dashboard/(main)/page.tsx +2 -1
  37. package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +4 -3
  38. package/templates/dashboard/(main)/posts/create/page.tsx +2 -1
  39. package/templates/dashboard/(main)/posts/page.tsx +7 -6
  40. package/tests/cypress/e2e/README.md +170 -0
  41. package/tests/cypress/e2e/categories/categories-crud.cy.ts +322 -0
  42. package/tests/cypress/e2e/categories/categories-crud.md +73 -0
  43. package/tests/cypress/e2e/posts/posts-crud.cy.ts +460 -0
  44. package/tests/cypress/e2e/posts/posts-crud.md +115 -0
  45. package/tests/cypress/e2e/posts/posts-editor.cy.ts +290 -0
  46. package/tests/cypress/e2e/posts/posts-editor.md +139 -0
  47. package/tests/cypress/e2e/posts/posts-status-workflow.cy.ts +302 -0
  48. package/tests/cypress/e2e/posts/posts-status-workflow.md +83 -0
  49. package/tests/cypress/fixtures/blocks.json +9 -0
  50. package/tests/cypress/fixtures/entities.json +51 -0
  51. package/tests/cypress/src/FeaturedImageUpload.js +131 -0
  52. package/tests/cypress/src/PostEditor.js +386 -0
  53. package/tests/cypress/src/PostsList.js +350 -0
  54. package/tests/cypress/src/WysiwygEditor.js +373 -0
  55. package/tests/cypress/src/components/EntityForm.ts +378 -0
  56. package/tests/cypress/src/components/EntityList.ts +378 -0
  57. package/tests/cypress/src/components/PostEditorPOM.ts +447 -0
  58. package/tests/cypress/src/components/PostsPOM.ts +362 -0
  59. package/tests/cypress/src/components/index.ts +18 -0
  60. package/tests/cypress/src/index.js +33 -0
  61. package/tests/cypress/src/selectors.ts +49 -0
  62. package/tests/cypress/src/session-helpers.ts +151 -0
  63. package/tests/cypress/support/e2e.ts +90 -0
  64. package/tests/cypress.config.ts +154 -0
  65. package/tests/jest/__mocks__/jose.js +22 -0
  66. package/tests/jest/__mocks__/next-server.js +56 -0
  67. package/tests/jest/components/WysiwygEditor.test.tsx +117 -0
  68. package/tests/jest/jest.config.cjs +148 -0
  69. package/tests/jest/lib/sanitize-post-html.test.ts +47 -0
  70. package/tests/jest/setup.ts +170 -0
  71. package/tests/jest/tsconfig.jest.json +6 -0
  72. 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"
@@ -86,11 +86,10 @@ export const APP_CONFIG_OVERRIDES = {
86
86
  // =============================================================================
87
87
  api: {
88
88
  cors: {
89
- allowedOrigins: {
90
- development: [
91
- 'http://localhost:3000',
92
- 'http://localhost:5173',
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
- public: true,
103
+ publicAccess: true,
105
104
  searchEnabled: true,
106
105
  breadcrumbs: true,
107
- theme: {
106
+ public: {
108
107
  enabled: true,
109
108
  open: true,
110
109
  label: "Blog Theme",
111
110
  },
112
- plugins: {
113
- enabled: false,
114
- open: false,
115
- label: "Plugins",
116
- },
117
- core: {
111
+ superadmin: {
118
112
  enabled: true,
119
113
  open: false,
120
- label: "Core",
114
+ label: "Admin Docs",
121
115
  },
122
- showPluginsDocsInProd: false,
123
116
  },
124
117
 
125
118
  // =============================================================================
@@ -79,8 +79,7 @@ export const billingConfig: BillingConfig = {
79
79
  storage_gb: 1,
80
80
  authors: 1,
81
81
  },
82
- stripePriceIdMonthly: null,
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 these in Stripe Dashboard
104
- stripePriceIdMonthly: 'price_blog_pro_monthly',
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
- stripePriceIdMonthly: null,
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