@mx-space/byline-api-client 0.0.0-stage → 0.1.0

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 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
- # Temporary Holding Version
1
+ # @mx-space/byline-api-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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