@nextsparkjs/theme-blog 0.1.0-beta.17 → 0.1.0-beta.170

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 (49) 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/config/app.config.ts +4 -5
  9. package/config/billing.config.ts +4 -7
  10. package/config/dashboard.config.ts +13 -0
  11. package/config/permissions.config.ts +11 -0
  12. package/entities/categories/api/docs.md +119 -0
  13. package/entities/categories/api/presets.ts +67 -0
  14. package/entities/posts/api/docs.md +174 -0
  15. package/entities/posts/api/presets.ts +137 -0
  16. package/lib/selectors.ts +2 -3
  17. package/nextsparkjs-theme-blog-0.1.0-beta.137.tgz +0 -0
  18. package/package.json +4 -3
  19. package/styles/globals.css +45 -0
  20. package/tests/cypress/e2e/README.md +170 -0
  21. package/tests/cypress/e2e/categories/categories-crud.cy.ts +322 -0
  22. package/tests/cypress/e2e/categories/categories-crud.md +73 -0
  23. package/tests/cypress/e2e/posts/posts-crud.cy.ts +460 -0
  24. package/tests/cypress/e2e/posts/posts-crud.md +115 -0
  25. package/tests/cypress/e2e/posts/posts-editor.cy.ts +290 -0
  26. package/tests/cypress/e2e/posts/posts-editor.md +139 -0
  27. package/tests/cypress/e2e/posts/posts-status-workflow.cy.ts +302 -0
  28. package/tests/cypress/e2e/posts/posts-status-workflow.md +83 -0
  29. package/tests/cypress/fixtures/blocks.json +9 -0
  30. package/tests/cypress/fixtures/entities.json +42 -0
  31. package/tests/cypress/src/FeaturedImageUpload.js +131 -0
  32. package/tests/cypress/src/PostEditor.js +386 -0
  33. package/tests/cypress/src/PostsList.js +350 -0
  34. package/tests/cypress/src/WysiwygEditor.js +373 -0
  35. package/tests/cypress/src/components/EntityForm.ts +378 -0
  36. package/tests/cypress/src/components/EntityList.ts +378 -0
  37. package/tests/cypress/src/components/PostEditorPOM.ts +447 -0
  38. package/tests/cypress/src/components/PostsPOM.ts +362 -0
  39. package/tests/cypress/src/components/index.ts +18 -0
  40. package/tests/cypress/src/index.js +33 -0
  41. package/tests/cypress/src/selectors.ts +49 -0
  42. package/tests/cypress/src/session-helpers.ts +151 -0
  43. package/tests/cypress/support/e2e.ts +90 -0
  44. package/tests/cypress.config.ts +154 -0
  45. package/tests/jest/__mocks__/jose.js +22 -0
  46. package/tests/jest/__mocks__/next-server.js +56 -0
  47. package/tests/jest/jest.config.cjs +131 -0
  48. package/tests/jest/setup.ts +170 -0
  49. package/tests/tsconfig.json +15 -0
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { NextRequest, NextResponse } from 'next/server'
11
11
  import { queryWithRLS } from '@nextsparkjs/core/lib/db'
12
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
12
13
 
13
14
  interface Author {
14
15
  id: string
@@ -39,10 +40,10 @@ interface RouteContext {
39
40
  }>
40
41
  }
41
42
 
42
- export async function GET(
43
+ const getHandler = async (
43
44
  request: NextRequest,
44
45
  context: RouteContext
45
- ) {
46
+ ) => {
46
47
  try {
47
48
  const { username } = await context.params
48
49
 
@@ -148,3 +149,5 @@ export async function GET(
148
149
  )
149
150
  }
150
151
  }
152
+
153
+ export const GET = withRateLimitTier(getHandler, 'read')
@@ -0,0 +1,135 @@
1
+ # Authors API
2
+
3
+ Public author profiles and post listings. No authentication required.
4
+
5
+ ## Overview
6
+
7
+ The Authors API provides read-only access to author profiles and their published posts. This endpoint is designed for public consumption and does not require authentication.
8
+
9
+ ## Authentication
10
+
11
+ **No authentication required** - This is a public endpoint.
12
+
13
+ ## Endpoints
14
+
15
+ ### List Authors
16
+ `GET /api/v1/theme/blog/authors`
17
+
18
+ Returns a list of all authors who have published posts.
19
+
20
+ **Example Request:**
21
+ ```bash
22
+ curl https://example.com/api/v1/theme/blog/authors
23
+ ```
24
+
25
+ **Example Response:**
26
+ ```json
27
+ {
28
+ "data": [
29
+ {
30
+ "id": "user_abc123",
31
+ "name": "John Doe",
32
+ "username": "johndoe",
33
+ "bio": "Full-stack developer and tech writer",
34
+ "image": "https://example.com/avatar.jpg",
35
+ "postCount": 15
36
+ },
37
+ {
38
+ "id": "user_def456",
39
+ "name": "Jane Smith",
40
+ "username": "janesmith",
41
+ "bio": "Product designer and UX enthusiast",
42
+ "image": "https://example.com/avatar2.jpg",
43
+ "postCount": 8
44
+ }
45
+ ]
46
+ }
47
+ ```
48
+
49
+ ### Get Author Profile
50
+ `GET /api/v1/theme/blog/authors/[username]`
51
+
52
+ Returns an author's profile with their published posts.
53
+
54
+ **Path Parameters:**
55
+ - `username` (string, required): Author's username
56
+
57
+ **Example Request:**
58
+ ```bash
59
+ curl https://example.com/api/v1/theme/blog/authors/johndoe
60
+ ```
61
+
62
+ **Example Response:**
63
+ ```json
64
+ {
65
+ "author": {
66
+ "id": "user_abc123",
67
+ "name": "John Doe",
68
+ "username": "johndoe",
69
+ "bio": "Full-stack developer and tech writer",
70
+ "image": "https://example.com/avatar.jpg"
71
+ },
72
+ "posts": [
73
+ {
74
+ "id": "post_123",
75
+ "title": "Getting Started with Next.js",
76
+ "slug": "getting-started-with-nextjs",
77
+ "excerpt": "A comprehensive guide...",
78
+ "featuredImage": "https://example.com/image.jpg",
79
+ "publishedAt": "2024-01-15T10:30:00Z",
80
+ "categories": [
81
+ {
82
+ "id": "cat_123",
83
+ "name": "Technology",
84
+ "slug": "technology"
85
+ }
86
+ ]
87
+ }
88
+ ],
89
+ "stats": {
90
+ "totalPosts": 15
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Response Fields
96
+
97
+ ### Author Object
98
+
99
+ | Field | Type | Description |
100
+ |-------|------|-------------|
101
+ | id | string | Author user ID |
102
+ | name | string | Author display name |
103
+ | username | string | Author username (URL-friendly) |
104
+ | bio | string | Author biography |
105
+ | image | string | Author avatar URL |
106
+ | postCount | number | Number of published posts (list endpoint only) |
107
+
108
+ ### Author Profile Response
109
+
110
+ | Field | Type | Description |
111
+ |-------|------|-------------|
112
+ | author | object | Author profile information |
113
+ | posts | array | Array of published posts |
114
+ | stats.totalPosts | number | Total number of published posts |
115
+
116
+ ## Features
117
+
118
+ - **No Auth Required**: Accessible without authentication
119
+ - **Rate Limited**: 100 requests per minute per IP
120
+ - **Cached**: Responses cached for 60 seconds
121
+ - **Sorted by Post Count**: Authors with more posts appear first
122
+
123
+ ## Error Responses
124
+
125
+ | Status | Description |
126
+ |--------|-------------|
127
+ | 400 | Bad Request - Invalid parameters |
128
+ | 404 | Not Found - Author not found |
129
+ | 429 | Too Many Requests - Rate limit exceeded |
130
+ | 500 | Internal Server Error |
131
+
132
+ ## Related APIs
133
+
134
+ - **[Public Posts](/api/v1/theme/blog/posts/public)** - Public post feed
135
+ - **[Posts](/api/v1/posts)** - Authenticated CRUD operations
@@ -0,0 +1,45 @@
1
+ /**
2
+ * API Presets for Authors Route
3
+ *
4
+ * Public author profiles and their published posts
5
+ */
6
+
7
+ import { defineApiEndpoint } from '@nextsparkjs/core/types/api-presets'
8
+
9
+ export default defineApiEndpoint({
10
+ endpoint: '/api/v1/theme/blog/authors',
11
+ summary: 'Public author profiles with published posts',
12
+ presets: [
13
+ {
14
+ id: 'list-all',
15
+ title: 'List All Authors',
16
+ description: 'Get all authors with published posts',
17
+ method: 'GET',
18
+ params: {
19
+ limit: 50
20
+ },
21
+ tags: ['read', 'list', 'public']
22
+ },
23
+ {
24
+ id: 'get-by-username',
25
+ title: 'Get Author by Username',
26
+ description: 'Get author profile with their posts',
27
+ method: 'GET',
28
+ pathParams: {
29
+ username: '{{username}}'
30
+ },
31
+ tags: ['read', 'detail', 'public']
32
+ },
33
+ {
34
+ id: 'list-with-post-count',
35
+ title: 'List with Post Count',
36
+ description: 'Get authors sorted by number of posts',
37
+ method: 'GET',
38
+ params: {
39
+ sortBy: 'postCount',
40
+ sortOrder: 'desc'
41
+ },
42
+ tags: ['read', 'list', 'public']
43
+ }
44
+ ]
45
+ })
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { NextRequest, NextResponse } from 'next/server'
11
11
  import { queryWithRLS } from '@nextsparkjs/core/lib/db'
12
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
12
13
 
13
14
  interface AuthorWithCount {
14
15
  id: string
@@ -19,7 +20,7 @@ interface AuthorWithCount {
19
20
  postCount: number
20
21
  }
21
22
 
22
- export async function GET(request: NextRequest) {
23
+ const getHandler = async (request: NextRequest) => {
23
24
  try {
24
25
  // Get all authors who have at least one published post
25
26
  const authorsQuery = `
@@ -61,3 +62,5 @@ export async function GET(request: NextRequest) {
61
62
  )
62
63
  }
63
64
  }
65
+
66
+ export const GET = withRateLimitTier(getHandler, 'read')
@@ -0,0 +1,124 @@
1
+ # Public Posts API
2
+
3
+ Public feed of published blog posts. No authentication required.
4
+
5
+ ## Overview
6
+
7
+ The Public Posts API provides read-only access to published blog posts. This endpoint is designed for public consumption by blog visitors and does not require authentication.
8
+
9
+ ## Authentication
10
+
11
+ **No authentication required** - This is a public endpoint.
12
+
13
+ ## Endpoints
14
+
15
+ ### List Published Posts
16
+ `GET /api/v1/theme/blog/posts/public`
17
+
18
+ Returns a paginated list of published posts.
19
+
20
+ **Query Parameters:**
21
+ - `limit` (number, optional): Maximum records to return. Default: 20, Max: 100
22
+ - `offset` (number, optional): Number of records to skip. Default: 0
23
+ - `category` (string, optional): Filter by category slug
24
+
25
+ **Example Request:**
26
+ ```bash
27
+ curl https://example.com/api/v1/theme/blog/posts/public?limit=10&category=technology
28
+ ```
29
+
30
+ **Example Response:**
31
+ ```json
32
+ {
33
+ "data": [
34
+ {
35
+ "id": "post_abc123",
36
+ "title": "Getting Started with Next.js",
37
+ "slug": "getting-started-with-nextjs",
38
+ "excerpt": "A comprehensive guide to building modern web apps",
39
+ "featuredImage": "https://example.com/image.jpg",
40
+ "featured": true,
41
+ "publishedAt": "2024-01-15T10:30:00Z",
42
+ "author": {
43
+ "id": "user_123",
44
+ "name": "John Doe",
45
+ "username": "johndoe",
46
+ "image": "https://example.com/avatar.jpg"
47
+ },
48
+ "categories": [
49
+ {
50
+ "id": "cat_123",
51
+ "name": "Technology",
52
+ "slug": "technology"
53
+ }
54
+ ]
55
+ }
56
+ ],
57
+ "pagination": {
58
+ "total": 25,
59
+ "limit": 10,
60
+ "offset": 0,
61
+ "hasMore": true
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Response Fields
67
+
68
+ | Field | Type | Description |
69
+ |-------|------|-------------|
70
+ | id | string | Post unique identifier |
71
+ | title | string | Post title |
72
+ | slug | string | URL-friendly slug |
73
+ | excerpt | string | Brief description |
74
+ | featuredImage | string | Featured image URL |
75
+ | featured | boolean | Whether post is featured |
76
+ | publishedAt | datetime | Publication timestamp |
77
+ | author | object | Author information |
78
+ | author.id | string | Author user ID |
79
+ | author.name | string | Author display name |
80
+ | author.username | string | Author username |
81
+ | author.image | string | Author avatar URL |
82
+ | categories | array | Associated categories |
83
+
84
+ ## Features
85
+
86
+ - **No Auth Required**: Accessible without authentication
87
+ - **Rate Limited**: 100 requests per minute per IP
88
+ - **Cached**: Responses cached for 60 seconds
89
+ - **Category Filter**: Filter posts by category slug
90
+
91
+ ## Pagination
92
+
93
+ The API uses offset-based pagination:
94
+
95
+ ```json
96
+ {
97
+ "pagination": {
98
+ "total": 100,
99
+ "limit": 20,
100
+ "offset": 0,
101
+ "hasMore": true
102
+ }
103
+ }
104
+ ```
105
+
106
+ To get the next page:
107
+ ```
108
+ GET /api/v1/theme/blog/posts/public?offset=20&limit=20
109
+ ```
110
+
111
+ ## Error Responses
112
+
113
+ | Status | Description |
114
+ |--------|-------------|
115
+ | 400 | Bad Request - Invalid query parameters |
116
+ | 404 | Not Found - Category not found |
117
+ | 429 | Too Many Requests - Rate limit exceeded |
118
+ | 500 | Internal Server Error |
119
+
120
+ ## Related APIs
121
+
122
+ - **[Posts](/api/v1/posts)** - Authenticated CRUD operations
123
+ - **[Authors](/api/v1/theme/blog/authors)** - Author profiles
124
+ - **[Categories](/api/v1/categories)** - Category management
@@ -0,0 +1,65 @@
1
+ /**
2
+ * API Presets for Public Posts
3
+ *
4
+ * Public feed of published blog posts (no authentication required)
5
+ */
6
+
7
+ import { defineApiEndpoint } from '@nextsparkjs/core/types/api-presets'
8
+
9
+ export default defineApiEndpoint({
10
+ endpoint: '/api/v1/theme/blog/posts/public',
11
+ summary: 'Public feed of published blog posts',
12
+ presets: [
13
+ {
14
+ id: 'list-recent',
15
+ title: 'List Recent Posts',
16
+ description: 'Get recent published posts',
17
+ method: 'GET',
18
+ params: {
19
+ limit: 20
20
+ },
21
+ tags: ['read', 'list', 'public']
22
+ },
23
+ {
24
+ id: 'list-with-limit',
25
+ title: 'List with Custom Limit',
26
+ description: 'Get posts with specific limit',
27
+ method: 'GET',
28
+ params: {
29
+ limit: '{{limit}}'
30
+ },
31
+ tags: ['read', 'list', 'public']
32
+ },
33
+ {
34
+ id: 'list-by-category',
35
+ title: 'List by Category',
36
+ description: 'Get posts filtered by category slug',
37
+ method: 'GET',
38
+ params: {
39
+ category: '{{category}}'
40
+ },
41
+ tags: ['read', 'filter', 'public']
42
+ },
43
+ {
44
+ id: 'list-paginated',
45
+ title: 'List Paginated',
46
+ description: 'Get paginated posts',
47
+ method: 'GET',
48
+ params: {
49
+ limit: '{{limit}}',
50
+ offset: '{{offset}}'
51
+ },
52
+ tags: ['read', 'list', 'public']
53
+ },
54
+ {
55
+ id: 'list-featured',
56
+ title: 'List Featured Posts',
57
+ description: 'Get featured published posts',
58
+ method: 'GET',
59
+ params: {
60
+ featured: 'true'
61
+ },
62
+ tags: ['read', 'filter', 'public']
63
+ }
64
+ ]
65
+ })
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { NextRequest, NextResponse } from 'next/server'
12
12
  import { queryWithRLS } from '@nextsparkjs/core/lib/db'
13
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
13
14
 
14
15
  interface Post {
15
16
  id: string
@@ -31,7 +32,7 @@ interface Post {
31
32
  authorImage: string | null
32
33
  }
33
34
 
34
- export async function GET(request: NextRequest) {
35
+ const getHandler = async (request: NextRequest) => {
35
36
  try {
36
37
  const { searchParams } = new URL(request.url)
37
38
 
@@ -149,3 +150,5 @@ export async function GET(request: NextRequest) {
149
150
  )
150
151
  }
151
152
  }
153
+
154
+ export const GET = withRateLimitTier(getHandler, 'read')
@@ -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
  },
@@ -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