@mx-space/byline-api-client 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +200 -2
- package/dist/index.cjs +573 -0
- package/dist/index.d.cts +653 -0
- package/dist/index.d.ts +653 -0
- package/dist/index.js +566 -0
- package/package.json +54 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Innei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,201 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mx-space/byline-api-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed `fetch` client for the [Byline](https://github.com/mx-space/byline) headless CMS API.
|
|
4
|
+
It has no runtime dependencies and works in Node.js 20+, browsers, Deno, Bun and edge
|
|
5
|
+
runtimes. It ships ESM and CommonJS builds with type declarations.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @mx-space/byline-api-client
|
|
11
|
+
# or: npm install @mx-space/byline-api-client
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createClient } from '@mx-space/byline-api-client'
|
|
18
|
+
|
|
19
|
+
const byline = createClient({
|
|
20
|
+
endpoint: 'https://cms.example.com/api/v3',
|
|
21
|
+
lang: 'zh-TW',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const { data: posts, meta } = await byline.posts.list({ size: 20 })
|
|
25
|
+
const { data: post } = await byline.posts.getBySlug('product', 'hello-world')
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`endpoint` is the server's API prefix (`/api/v{version}` in production). Every method
|
|
29
|
+
resolves to `{ data, meta? }`, mirroring the server envelope. Responses are converted
|
|
30
|
+
from the snake_case wire format to camelCase, except free-form subtrees (post `meta`,
|
|
31
|
+
dynamic component `propsSchema`). Pass `camelCase: false` to keep the wire keys.
|
|
32
|
+
Failures throw `BylineApiError` with the server's `status`, `code`, `message` and
|
|
33
|
+
`details`; use `isBylineApiError(error)` to narrow.
|
|
34
|
+
|
|
35
|
+
| Option | Purpose |
|
|
36
|
+
| ----------- | --------------------------------------------------------------- |
|
|
37
|
+
| `endpoint` | API base URL, required |
|
|
38
|
+
| `lang` | default `?lang=` for post and category reads |
|
|
39
|
+
| `apiKey` | sent as `x-api-key` |
|
|
40
|
+
| `token` | session token sent as `Authorization: Bearer` |
|
|
41
|
+
| `headers` | extra headers on every request |
|
|
42
|
+
| `fetch` | custom `fetch` implementation (tests, proxies, Next.js caching) |
|
|
43
|
+
| `camelCase` | `false` keeps snake_case keys |
|
|
44
|
+
|
|
45
|
+
### Languages
|
|
46
|
+
|
|
47
|
+
Reads take `lang` per call or from the client option. The server tries the exact
|
|
48
|
+
language, then its base (`en-US` falls back to `en`, `zh-Hant-HK` to `zh`), then serves
|
|
49
|
+
the source. Sibling regions are never used. `meta.translation` reports the language
|
|
50
|
+
actually served, and the post `seo` is the source SEO with the translation's non-empty
|
|
51
|
+
`title`, `description` and `image` merged over it.
|
|
52
|
+
|
|
53
|
+
### Authors
|
|
54
|
+
|
|
55
|
+
Public author profiles are readable without credentials. Listed authors are active and
|
|
56
|
+
either profile-only or credited on a published post.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const { data: authors, meta } = await byline.authors.list({ page: 1, size: 20 })
|
|
60
|
+
const everyone = await byline.authors.listAll()
|
|
61
|
+
const { data: ada } = await byline.authors.get('ada')
|
|
62
|
+
const { data: posts } = await byline.posts.list({ authorId: ada.id })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`authors.get` accepts an id or a username (case-insensitive). Hidden or unknown authors
|
|
66
|
+
throw `USER_NOT_FOUND` (404).
|
|
67
|
+
|
|
68
|
+
## Authentication
|
|
69
|
+
|
|
70
|
+
Reads of published content are public. Writes need either an API key or a session
|
|
71
|
+
token:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const admin = createClient({ endpoint, apiKey: process.env.BYLINE_API_KEY })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Writing posts and translations
|
|
78
|
+
|
|
79
|
+
`posts.create` and `posts.revise` go through the draft and publish-job flow the admin
|
|
80
|
+
uses: they save a draft revision, start a publish job and wait for it. `revise`
|
|
81
|
+
branches from the post's published revision, so fields you omit keep their current
|
|
82
|
+
values.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const { postId } = await admin.posts.create(
|
|
86
|
+
{
|
|
87
|
+
title: 'Hello',
|
|
88
|
+
content: lexicalState,
|
|
89
|
+
text: 'Hello',
|
|
90
|
+
categoryId,
|
|
91
|
+
authors: [userId],
|
|
92
|
+
sourceLang: 'en',
|
|
93
|
+
seo: { title: 'Hello', description: 'A greeting' },
|
|
94
|
+
},
|
|
95
|
+
{ aiResources: [] },
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
await admin.translations.writeManual(postId!, 'zh-TW', {
|
|
99
|
+
title: '你好',
|
|
100
|
+
content: translatedLexicalState,
|
|
101
|
+
seo: { title: '你好', description: '一句問候' },
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A manual translation is stored locked, so publish-time AI translation skips it unless
|
|
106
|
+
regeneration is requested. Writing into the post's source language is rejected with
|
|
107
|
+
`INVALID_PARAMETER`. `translations.update(id, { seo })` edits a stored row; pass
|
|
108
|
+
`seo: null` to clear it.
|
|
109
|
+
|
|
110
|
+
### Users and invites
|
|
111
|
+
|
|
112
|
+
Byline is invite-only. A profile-only user (an author without credentials) can be
|
|
113
|
+
upgraded into a sign-in account, keeping its id and bylines:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const { data: author } = await admin.users.createProfileOnly({
|
|
117
|
+
displayName: 'Ada Lovelace',
|
|
118
|
+
username: 'ada',
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const { data: invite } = await admin.users.invite(author.id, {
|
|
122
|
+
email: 'ada@example.com',
|
|
123
|
+
})
|
|
124
|
+
console.log(invite.acceptUrl)
|
|
125
|
+
|
|
126
|
+
await byline.invites.accept(invite.token, { username: 'ada', password: '…' })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`users.invite` defaults the email and display name to the stored profile.
|
|
130
|
+
`invites.create({ email })` with the email of an existing profile-only user attaches the
|
|
131
|
+
invite to that user too (`invite.userId`).
|
|
132
|
+
|
|
133
|
+
## Resources
|
|
134
|
+
|
|
135
|
+
| Resource | Methods |
|
|
136
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| `posts` | `list`, `get`, `getBySlug`, `getPath`, `findBySlug`, `latest`, `create`, `revise`, `update`, `setPublished`, `delete` |
|
|
138
|
+
| `authors` | `list`, `listAll`, `get` |
|
|
139
|
+
| `drafts` | `list`, `get`, `context`, `create`, `update`, `delete` |
|
|
140
|
+
| `publishJobs` | `create`, `run` (create and wait) |
|
|
141
|
+
| `tasks` | `get`, `wait`, `cancel` |
|
|
142
|
+
| `categories` | `list`, `get`, `create`, `update`, `delete` |
|
|
143
|
+
| `translations` | `getForArticle`, `languages`, `listByRef`, `writeManual`, `update`, `delete`, `createTask` |
|
|
144
|
+
| `users` | `allowLogin`, `checkLogged`, `me`, `updateMe`, `list`, `listAll`, `get`, `update`, `createProfileOnly`, `invite`, `deactivate`, `reactivate`, `initFirstUser` |
|
|
145
|
+
| `invites` | `list`, `create`, `revoke`, `get`, `accept` |
|
|
146
|
+
| `dynamicComponents` | `list`, `register` |
|
|
147
|
+
| `files` | `upload` |
|
|
148
|
+
|
|
149
|
+
## Example: LobeHub blog
|
|
150
|
+
|
|
151
|
+
A LobeHub-style marketing site serves its blog in 17 locales from one Byline instance.
|
|
152
|
+
Category slugs, locales and author profiles live in Byline, not in the site.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { createClient, isBylineApiError } from '@mx-space/byline-api-client'
|
|
156
|
+
|
|
157
|
+
const byline = createClient({ endpoint: process.env.BYLINE_API_URL! })
|
|
158
|
+
|
|
159
|
+
export const loadBlogIndex = async (locale: string, page = 1) => {
|
|
160
|
+
const { data: posts, meta } = await byline.posts.list({
|
|
161
|
+
lang: locale,
|
|
162
|
+
page,
|
|
163
|
+
size: 12,
|
|
164
|
+
sortBy: 'createdAt',
|
|
165
|
+
sortOrder: 'desc',
|
|
166
|
+
})
|
|
167
|
+
return { posts, pagination: meta?.pagination }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const loadBlogPost = async (
|
|
171
|
+
locale: string,
|
|
172
|
+
category: string,
|
|
173
|
+
slug: string,
|
|
174
|
+
) => {
|
|
175
|
+
try {
|
|
176
|
+
const { data: post, meta } = await byline.posts.getBySlug(category, slug, {
|
|
177
|
+
lang: locale,
|
|
178
|
+
})
|
|
179
|
+
return { post, seo: post.seo, translation: meta?.translation }
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (isBylineApiError(error) && error.status === 404) return null
|
|
182
|
+
throw error
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const loadAuthorPage = async (username: string, locale: string) => {
|
|
187
|
+
const { data: author } = await byline.authors.get(username)
|
|
188
|
+
const { data: posts } = await byline.posts.list({
|
|
189
|
+
authorId: author.id,
|
|
190
|
+
lang: locale,
|
|
191
|
+
})
|
|
192
|
+
return { author, posts }
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`post.content` is Lexical JSON; render it with a haklex renderer. LobeHub-specific
|
|
197
|
+
components are haklex Dynamic Nodes (`{ url, props }`) that the site resolves itself.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|