@nuxtblog/plugin-sdk 0.0.1 → 0.0.3
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/README.md +849 -308
- package/README.zh.md +857 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,308 +1,849 @@
|
|
|
1
|
-
# @nuxtblog/plugin-sdk
|
|
2
|
-
|
|
3
|
-
TypeScript type definitions and
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
|
288
|
-
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
1
|
+
# @nuxtblog/plugin-sdk
|
|
2
|
+
|
|
3
|
+
TypeScript type definitions and developer guide for [nuxtblog](https://github.com/nuxtblog/nuxtblog) plugins.
|
|
4
|
+
|
|
5
|
+
English(README.md)|(中文文档](README.zh.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Plugin Structure](#plugin-structure)
|
|
13
|
+
- [Manifest (`package.json`)](#manifest-packagejson)
|
|
14
|
+
- [Settings Fields](#settings-fields)
|
|
15
|
+
- [Capabilities & Permissions](#capabilities--permissions)
|
|
16
|
+
- [Plugin API Reference](#plugin-api-reference)
|
|
17
|
+
- [nuxtblog.on — Event Subscription](#nuxtblogon--event-subscription)
|
|
18
|
+
- [nuxtblog.filter — Data Interception](#nuxtblogfilter--data-interception)
|
|
19
|
+
- [nuxtblog.http — HTTP Requests](#nuxtbloghttp--http-requests)
|
|
20
|
+
- [nuxtblog.store — Persistent KV Store](#nuxtblogstore--persistent-kv-store)
|
|
21
|
+
- [nuxtblog.settings — Read Settings](#nuxtblogsettings--read-settings)
|
|
22
|
+
- [nuxtblog.log — Server Logging](#nuxtbloglog--server-logging)
|
|
23
|
+
- [Declarative Webhooks](#declarative-webhooks)
|
|
24
|
+
- [Declarative Pipelines](#declarative-pipelines)
|
|
25
|
+
- [Event Reference](#event-reference)
|
|
26
|
+
- [Fire-and-forget Events](#fire-and-forget-events-nuxtblogon)
|
|
27
|
+
- [Filter Events](#filter-events-nuxtblogfilter)
|
|
28
|
+
- [Execution Model & Concurrency](#execution-model--concurrency)
|
|
29
|
+
- [Timeouts & Retry](#timeouts--retry)
|
|
30
|
+
- [Observability](#observability)
|
|
31
|
+
- [Building & Installing](#building--installing)
|
|
32
|
+
- [TypeScript Support](#typescript-support)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
37
|
+
|
|
38
|
+
Plugins are **server-side JavaScript scripts** executed by the [goja](https://github.com/dop251/goja) engine (ES2015+ compatible). Each plugin runs in an isolated VM with its own `nuxtblog` global object.
|
|
39
|
+
|
|
40
|
+
**What plugins can do:**
|
|
41
|
+
|
|
42
|
+
- Subscribe to system events (posts, comments, users, media, etc.) asynchronously — fire-and-forget
|
|
43
|
+
- Intercept and modify data synchronously before it is written to the database, or abort the operation entirely
|
|
44
|
+
- Read admin-configured settings (API tokens, webhook URLs, feature flags, etc.)
|
|
45
|
+
- Make outbound HTTP requests to external services
|
|
46
|
+
- Persist runtime state in a per-plugin key-value store backed by the database
|
|
47
|
+
- Declare outbound webhooks and multi-step async pipelines entirely in the manifest — no JS required
|
|
48
|
+
|
|
49
|
+
**What plugins cannot do:**
|
|
50
|
+
|
|
51
|
+
- Call `http.fetch` inside `filter` handlers (this would stall request processing — use `nuxtblog.on` for async side-effects)
|
|
52
|
+
- Access APIs not declared in `capabilities` (undeclared APIs are simply absent in the VM — `undefined`, not an error)
|
|
53
|
+
- Share VM state with other plugins (each plugin has a completely isolated runtime)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Plugin Structure
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
my-plugin/
|
|
61
|
+
├── package.json ← Plugin manifest (required, contains "plugin" field)
|
|
62
|
+
├── index.js ← Bundled single-file entry script (loaded by the server)
|
|
63
|
+
└── src/
|
|
64
|
+
└── index.ts ← TypeScript source (optional, for development)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The installation archive only needs to contain `package.json` and `index.js`:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
my-plugin.zip
|
|
71
|
+
├── package.json
|
|
72
|
+
└── index.js (or the path declared in plugin.entry)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Manifest (`package.json`)
|
|
78
|
+
|
|
79
|
+
The manifest lives in the standard `package.json`. All plugin-specific configuration is nested under the `"plugin"` key.
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"name": "owner/my-plugin",
|
|
84
|
+
"version": "1.0.0",
|
|
85
|
+
"description": "A short description of what the plugin does",
|
|
86
|
+
"author": "owner",
|
|
87
|
+
"license": "MIT",
|
|
88
|
+
"homepage": "https://github.com/owner/my-plugin",
|
|
89
|
+
"keywords": ["nuxtblog-plugin"],
|
|
90
|
+
"plugin": {
|
|
91
|
+
"title": "My Plugin",
|
|
92
|
+
"icon": "i-tabler-plug",
|
|
93
|
+
"entry": "index.js",
|
|
94
|
+
"priority": 10,
|
|
95
|
+
"capabilities": {
|
|
96
|
+
"http": { "allow": ["hooks.slack.com"], "timeout_ms": 5000 },
|
|
97
|
+
"store": { "read": true, "write": true }
|
|
98
|
+
},
|
|
99
|
+
"settings": [
|
|
100
|
+
{ "key": "webhook_url", "label": "Slack Webhook URL", "type": "string", "required": true }
|
|
101
|
+
],
|
|
102
|
+
"webhooks": [],
|
|
103
|
+
"pipelines": []
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Top-level fields (standard npm)
|
|
109
|
+
|
|
110
|
+
| Field | Type | Required | Description |
|
|
111
|
+
| --------------- | -------- | -------- | ------------------------------------------------------------------------------------------ |
|
|
112
|
+
| `name` | string | ✅ | Unique plugin ID. Recommended format:`owner/repo`. Cannot be changed after installation. |
|
|
113
|
+
| `version` | string | ✅ | Semantic version, e.g.`1.0.0` |
|
|
114
|
+
| `description` | string | | Short description shown in the admin UI |
|
|
115
|
+
| `author` | string | | Author name |
|
|
116
|
+
| `license` | string | | License identifier, e.g.`MIT` |
|
|
117
|
+
| `homepage` | string | | Plugin homepage or repository URL |
|
|
118
|
+
| `keywords` | string[] | | Tags for discovery; include `nuxtblog-plugin` |
|
|
119
|
+
|
|
120
|
+
### `"plugin"` field
|
|
121
|
+
|
|
122
|
+
| Field | Type | Required | Description |
|
|
123
|
+
| ---------------- | ------ | -------- | -------------------------------------------------------------------------------------- |
|
|
124
|
+
| `title` | string | ✅ | Display name shown in the admin panel |
|
|
125
|
+
| `icon` | string | | [Tabler Icons](https://tabler.io/icons) identifier, e.g. `i-tabler-bell` |
|
|
126
|
+
| `entry` | string | | Entry script path inside the archive. Default:`index.js` |
|
|
127
|
+
| `priority` | number | | Execution order among plugins. Lower = runs first. Default:`10` |
|
|
128
|
+
| `capabilities` | object | | Declared API permissions. See[Capabilities & Permissions](#capabilities--permissions) |
|
|
129
|
+
| `settings` | array | | Admin-configurable parameters. See[Settings Fields](#settings-fields) |
|
|
130
|
+
| `webhooks` | array | | Declarative outbound webhooks. See[Declarative Webhooks](#declarative-webhooks) |
|
|
131
|
+
| `pipelines` | array | | Declarative async pipelines. See[Declarative Pipelines](#declarative-pipelines) |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Settings Fields
|
|
136
|
+
|
|
137
|
+
The `settings` array declares parameters that administrators configure in the plugin settings UI. Plugins read them at runtime via `nuxtblog.settings.get(key)`.
|
|
138
|
+
|
|
139
|
+
### Field types
|
|
140
|
+
|
|
141
|
+
| `type` | UI control | Use case |
|
|
142
|
+
| ------------ | ----------------------- | ----------------------------------------- |
|
|
143
|
+
| `string` | Single-line text input | URLs, names, arbitrary strings |
|
|
144
|
+
| `password` | Password input (masked) | API keys, tokens, secrets |
|
|
145
|
+
| `number` | Numeric input | Timeouts, limits, counts |
|
|
146
|
+
| `boolean` | Toggle switch | Feature flags |
|
|
147
|
+
| `select` | Dropdown | Enumerated options (use with `options`) |
|
|
148
|
+
| `textarea` | Multi-line text | Templates, JSON config, long strings |
|
|
149
|
+
|
|
150
|
+
### Field properties
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"key": "api_token",
|
|
155
|
+
"label": "API Token",
|
|
156
|
+
"type": "password",
|
|
157
|
+
"required": true,
|
|
158
|
+
"default": "",
|
|
159
|
+
"placeholder": "sk-xxxxxxxx",
|
|
160
|
+
"description": "Copy from your provider's dashboard",
|
|
161
|
+
"options": []
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| Property | Type | Description |
|
|
166
|
+
| --------------- | -------- | ----------------------------------------------------- |
|
|
167
|
+
| `key` | string | Key used in `nuxtblog.settings.get(key)` |
|
|
168
|
+
| `label` | string | Form label shown in the admin UI |
|
|
169
|
+
| `type` | string | Control type (see table above) |
|
|
170
|
+
| `required` | boolean | Whether the field is required (visual indicator only) |
|
|
171
|
+
| `default` | any | Default value populated on install |
|
|
172
|
+
| `placeholder` | string | Input placeholder text |
|
|
173
|
+
| `description` | string | Help text shown below the field |
|
|
174
|
+
| `options` | string[] | Dropdown options (only for `type: "select"`) |
|
|
175
|
+
|
|
176
|
+
### Complete example
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
"settings": [
|
|
180
|
+
{ "key": "enabled", "label": "Enable plugin", "type": "boolean", "default": true },
|
|
181
|
+
{ "key": "api_token", "label": "API Token", "type": "password", "required": true, "placeholder": "sk-xxxxxxxx" },
|
|
182
|
+
{ "key": "webhook_url", "label": "Webhook URL", "type": "string", "placeholder": "https://example.com/hook" },
|
|
183
|
+
{ "key": "timeout", "label": "Timeout (seconds)", "type": "number", "default": 10 },
|
|
184
|
+
{ "key": "log_level", "label": "Log level", "type": "select", "default": "info", "options": ["debug","info","warn","error"] },
|
|
185
|
+
{ "key": "template", "label": "Message template", "type": "textarea", "placeholder": "New post: {{title}}" }
|
|
186
|
+
]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Capabilities & Permissions
|
|
192
|
+
|
|
193
|
+
Capabilities follow a **whitelist model**: only APIs you explicitly declare in `capabilities` are injected into the VM. Undeclared APIs are simply `undefined` — accessing them does not throw.
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
"capabilities": {
|
|
197
|
+
"http": {
|
|
198
|
+
"allow": ["api.openai.com", "hooks.slack.com"],
|
|
199
|
+
"timeout_ms": 8000
|
|
200
|
+
},
|
|
201
|
+
"store": {
|
|
202
|
+
"read": true,
|
|
203
|
+
"write": true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `http`
|
|
209
|
+
|
|
210
|
+
| Property | Type | Default | Description |
|
|
211
|
+
| -------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| `allow` | string[] | `[]` (any) | Domain allowlist. Subdomains are allowed automatically (e.g.`example.com` also permits `api.example.com`). Empty list = any domain permitted. |
|
|
213
|
+
| `timeout_ms` | number | `15000` | Per-request timeout in milliseconds. |
|
|
214
|
+
|
|
215
|
+
### `store`
|
|
216
|
+
|
|
217
|
+
| Property | Type | Description |
|
|
218
|
+
| --------- | ------- | --------------------------------------------------------------------- |
|
|
219
|
+
| `read` | boolean | Grants access to `nuxtblog.store.get` |
|
|
220
|
+
| `write` | boolean | Grants access to `nuxtblog.store.set` and `nuxtblog.store.delete` |
|
|
221
|
+
|
|
222
|
+
> **Security note:** `nuxtblog.on` and `nuxtblog.filter` are always available regardless of capabilities, as is `nuxtblog.log` and `nuxtblog.settings`. These require no declaration.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Plugin API Reference
|
|
227
|
+
|
|
228
|
+
### `nuxtblog.on` — Event Subscription
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
nuxtblog.on(event: string, handler: (payload: object) => void): void
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Subscribes to a fire-and-forget system event. The handler runs **asynchronously** after the operation completes. Errors in the handler are logged and written to the error ring buffer; they never affect the original operation.
|
|
235
|
+
|
|
236
|
+
**Key constraints:**
|
|
237
|
+
|
|
238
|
+
- Handler timeout: **3 seconds** (configurable in manifest)
|
|
239
|
+
- `http.fetch` is allowed inside `nuxtblog.on` handlers
|
|
240
|
+
- Multiple handlers for the same event execute in **priority order** (lower plugin priority = runs first)
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// Notify Slack when a post is published
|
|
244
|
+
nuxtblog.on('post.published', (data) => {
|
|
245
|
+
const url = nuxtblog.settings.get('webhook_url') as string
|
|
246
|
+
if (!url) return
|
|
247
|
+
|
|
248
|
+
const res = nuxtblog.http.fetch(url, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
body: { text: `New post: ${data.title} — ${data.slug}` },
|
|
251
|
+
})
|
|
252
|
+
if (!res.ok) {
|
|
253
|
+
nuxtblog.log.warn(`Slack notify failed: HTTP ${res.status}`)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Track a per-plugin post counter
|
|
258
|
+
nuxtblog.on('post.created', (_data) => {
|
|
259
|
+
const count = ((nuxtblog.store.get('post_count') as number) || 0) + 1
|
|
260
|
+
nuxtblog.store.set('post_count', count)
|
|
261
|
+
nuxtblog.log.info(`Total posts tracked: ${count}`)
|
|
262
|
+
})
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### `nuxtblog.filter` — Data Interception
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
nuxtblog.filter(event: string, handler: (ctx: PluginCtx) => void): void
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Intercepts data **synchronously** before it is written to the database. The handler receives a `ctx` object. Modify `ctx.data` to change what gets saved; call `ctx.abort(reason)` to cancel the entire operation.
|
|
274
|
+
|
|
275
|
+
**Key constraints:**
|
|
276
|
+
|
|
277
|
+
- Handler timeout: **50 milliseconds** (hard limit — keeps request latency acceptable)
|
|
278
|
+
- `http.fetch` is **blocked** inside filter handlers. Use `nuxtblog.on` for async side-effects.
|
|
279
|
+
- All plugins run in priority order; `ctx.meta` is shared across plugins in the same chain
|
|
280
|
+
|
|
281
|
+
**`ctx` object:**
|
|
282
|
+
|
|
283
|
+
| Property | Type | Description |
|
|
284
|
+
| --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
285
|
+
| `ctx.event` | string | The filter event name, e.g.`"filter:post.create"` |
|
|
286
|
+
| `ctx.data` | object | Mutable payload. Changes are returned to the caller. |
|
|
287
|
+
| `ctx.input` | object | Read-only deep-copy snapshot of `ctx.data` taken before the chain starts. Use for audit/diff. |
|
|
288
|
+
| `ctx.meta` | object | Shared KV store across all plugins in this chain. Earlier plugins can leave data for later ones. |
|
|
289
|
+
| `ctx.next()` | function | Optional: explicitly signal that this handler is done. The chain continues regardless unless `abort()` is called. |
|
|
290
|
+
| `ctx.abort(reason)` | function | Cancel the operation immediately. All subsequent plugin handlers are skipped. The caller receives an error wrapping `reason`. |
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
// Trim whitespace and enforce title length
|
|
294
|
+
nuxtblog.filter('post.create', (ctx) => {
|
|
295
|
+
ctx.data.title = (ctx.data.title as string).trim()
|
|
296
|
+
if ((ctx.data.title as string).length > 200) {
|
|
297
|
+
ctx.abort('Title must not exceed 200 characters')
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
// Leave a slug hint for the next plugin in the chain
|
|
301
|
+
ctx.meta.computed_slug = (ctx.data.title as string).toLowerCase().replace(/\s+/g, '-')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// Auto-generate slug from meta set by the previous plugin
|
|
305
|
+
nuxtblog.filter('post.create', (ctx) => {
|
|
306
|
+
if (!ctx.data.slug && ctx.meta.computed_slug) {
|
|
307
|
+
ctx.data.slug = ctx.meta.computed_slug
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// Block comments containing specific words
|
|
312
|
+
nuxtblog.filter('comment.create', (ctx) => {
|
|
313
|
+
const blocked = ['spam', 'advertisement']
|
|
314
|
+
const content = (ctx.data.content as string).toLowerCase()
|
|
315
|
+
if (blocked.some(w => content.includes(w))) {
|
|
316
|
+
ctx.abort('Comment contains prohibited content')
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// Prevent deletion of posts created in the last hour
|
|
321
|
+
nuxtblog.filter('post.delete', (ctx) => {
|
|
322
|
+
// ctx.data: { id }
|
|
323
|
+
// Fetch additional data from store if needed — store.get is allowed in filters
|
|
324
|
+
nuxtblog.log.info(`Post ${ctx.data.id} deletion requested`)
|
|
325
|
+
// ctx.next() — optional
|
|
326
|
+
})
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### `nuxtblog.http` — HTTP Requests
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
nuxtblog.http.fetch(url: string, options?: FetchOptions): FetchResult
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Synchronous HTTP request (not a Promise). Returns immediately with the result.
|
|
338
|
+
|
|
339
|
+
> Requires `capabilities.http` to be declared in the manifest.
|
|
340
|
+
> **Blocked inside `filter` handlers.** Use in `nuxtblog.on` handlers or pipeline `js` steps.
|
|
341
|
+
|
|
342
|
+
**Options:**
|
|
343
|
+
|
|
344
|
+
| Property | Type | Default | Description |
|
|
345
|
+
| ----------- | --------------- | --------- | --------------------------------------------------------------------------------------------------------------- |
|
|
346
|
+
| `method` | string | `"GET"` | HTTP method:`GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
|
|
347
|
+
| `body` | object\| string | — | Request body. Objects are automatically JSON-serialized. |
|
|
348
|
+
| `headers` | object | — | Custom request headers. Setting a body automatically adds `Content-Type: application/json` if not overridden. |
|
|
349
|
+
|
|
350
|
+
**Return value:**
|
|
351
|
+
|
|
352
|
+
| Property | Type | Description |
|
|
353
|
+
| ---------- | ------- | ------------------------------------------------------------------------------------------- |
|
|
354
|
+
| `ok` | boolean | `true` if HTTP status is 200–299 |
|
|
355
|
+
| `status` | number | HTTP status code |
|
|
356
|
+
| `body` | any | Response body, JSON-parsed automatically. Falls back to raw string if parsing fails. |
|
|
357
|
+
| `error` | string | Error message if the request failed (network error, timeout, domain not in allowlist, etc.) |
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
// POST with JSON body and auth header
|
|
361
|
+
const res = nuxtblog.http.fetch('https://api.example.com/notify', {
|
|
362
|
+
method: 'POST',
|
|
363
|
+
headers: { 'Authorization': `Bearer ${nuxtblog.settings.get('api_token')}` },
|
|
364
|
+
body: { title: 'Hello', id: 42 },
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
if (res.ok) {
|
|
368
|
+
nuxtblog.log.info(`Created remote record: ${(res.body as any).id}`)
|
|
369
|
+
} else {
|
|
370
|
+
nuxtblog.log.error(`Request failed: ${res.error ?? `HTTP ${res.status}`}`)
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### `nuxtblog.store` — Persistent KV Store
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
nuxtblog.store.get(key: string): unknown
|
|
380
|
+
nuxtblog.store.set(key: string, value: unknown): void
|
|
381
|
+
nuxtblog.store.delete(key: string): void
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Per-plugin persistent key-value store backed by the database `options` table. Keys are automatically namespaced per plugin — you cannot read another plugin's data.
|
|
385
|
+
|
|
386
|
+
Values can be any JSON-serializable type: strings, numbers, booleans, arrays, objects.
|
|
387
|
+
|
|
388
|
+
> Requires `capabilities.store.read` and/or `capabilities.store.write` to be declared.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
// Track how many posts have been pushed to an external service
|
|
392
|
+
nuxtblog.on('post.published', (data) => {
|
|
393
|
+
const count = ((nuxtblog.store.get('push_count') as number) || 0) + 1
|
|
394
|
+
nuxtblog.store.set('push_count', count)
|
|
395
|
+
|
|
396
|
+
// Cache the last pushed post ID
|
|
397
|
+
nuxtblog.store.set('last_post', { id: data.id, title: data.title, at: new Date().toISOString() })
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// On demand: clear cached state
|
|
401
|
+
nuxtblog.on('plugin.installed', (_data) => {
|
|
402
|
+
nuxtblog.store.delete('push_count')
|
|
403
|
+
nuxtblog.store.delete('last_post')
|
|
404
|
+
})
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
> **Note:** `nuxtblog.store` is for runtime state. For admin-configured values (API keys, URLs, toggles), use `nuxtblog.settings`.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### `nuxtblog.settings` — Read Settings
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
nuxtblog.settings.get(key: string): unknown
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Reads a value from the plugin's admin-configured settings. Results are cached for **30 seconds** to avoid per-call database hits; changes made in the admin UI take effect within the next cache expiry.
|
|
418
|
+
|
|
419
|
+
Always available — no capability declaration required.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
const token = nuxtblog.settings.get('api_token') as string | null
|
|
423
|
+
const enabled = nuxtblog.settings.get('enabled') as boolean | null
|
|
424
|
+
const timeout = nuxtblog.settings.get('timeout') as number | null
|
|
425
|
+
|
|
426
|
+
if (!token) {
|
|
427
|
+
nuxtblog.log.warn('api_token not configured — skipping')
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
### `nuxtblog.log` — Server Logging
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
nuxtblog.log.info(msg: string): void
|
|
438
|
+
nuxtblog.log.warn(msg: string): void
|
|
439
|
+
nuxtblog.log.error(msg: string): void
|
|
440
|
+
nuxtblog.log.debug(msg: string): void
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Writes to the server log. Each message is automatically prefixed with `[plugin:<id>]`.
|
|
444
|
+
|
|
445
|
+
Always available — no capability declaration required.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
nuxtblog.log.info('Plugin initialized')
|
|
449
|
+
nuxtblog.log.debug(`Processing event with data: ${JSON.stringify(data)}`)
|
|
450
|
+
nuxtblog.log.warn('api_token is missing — some features are disabled')
|
|
451
|
+
nuxtblog.log.error(`Unexpected response: ${res.status}`)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Declarative Webhooks
|
|
457
|
+
|
|
458
|
+
Simple outbound HTTP notifications can be declared entirely in the manifest — no JS required. The platform POSTs the event payload as JSON to the configured URL whenever a matching event fires.
|
|
459
|
+
|
|
460
|
+
Webhooks fire **asynchronously** (never block the originating request). Failures are written to the plugin's error ring buffer and do not retry.
|
|
461
|
+
|
|
462
|
+
**Never hardcode secrets.** Use `{{settings.key}}` placeholders in `url` and header values. They are resolved at dispatch time from admin-configured settings (30-second cache).
|
|
463
|
+
|
|
464
|
+
```json
|
|
465
|
+
{
|
|
466
|
+
"plugin": {
|
|
467
|
+
"settings": [
|
|
468
|
+
{ "key": "webhook_url", "label": "Webhook URL", "type": "string", "required": true },
|
|
469
|
+
{ "key": "webhook_token", "label": "Webhook Token", "type": "password", "required": true }
|
|
470
|
+
],
|
|
471
|
+
"webhooks": [
|
|
472
|
+
{
|
|
473
|
+
"url": "{{settings.webhook_url}}",
|
|
474
|
+
"events": ["post.published", "comment.created"],
|
|
475
|
+
"headers": {
|
|
476
|
+
"Authorization": "Bearer {{settings.webhook_token}}",
|
|
477
|
+
"X-Source": "nuxtblog"
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"url": "https://example.com/static-endpoint",
|
|
482
|
+
"events": ["user.registered"]
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### `WebhookDef` properties
|
|
490
|
+
|
|
491
|
+
| Property | Type | Description |
|
|
492
|
+
| ----------- | -------- | ---------------------------------------------------------------------- |
|
|
493
|
+
| `url` | string | Endpoint to POST to. Supports `{{settings.key}}` interpolation. |
|
|
494
|
+
| `events` | string[] | Event names or patterns to match. |
|
|
495
|
+
| `headers` | object | Extra HTTP headers. Values support `{{settings.key}}` interpolation. |
|
|
496
|
+
|
|
497
|
+
### Event patterns
|
|
498
|
+
|
|
499
|
+
| Pattern | Matches |
|
|
500
|
+
| -------------------- | -------------------------------------------------------------------------- |
|
|
501
|
+
| `"post.published"` | Exact match only |
|
|
502
|
+
| `"post.*"` | All events with `post.` prefix: `post.created`, `post.updated`, etc. |
|
|
503
|
+
| `"*"` | Every event |
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## Declarative Pipelines
|
|
508
|
+
|
|
509
|
+
Multi-step async workflows with conditionals, retries, and timeouts can be declared in the manifest. Pipelines fire asynchronously and never block the originating event.
|
|
510
|
+
|
|
511
|
+
JS functions called from pipeline steps must be exported at module scope (top-level `function` declarations in your script).
|
|
512
|
+
|
|
513
|
+
```json
|
|
514
|
+
{
|
|
515
|
+
"plugin": {
|
|
516
|
+
"capabilities": {
|
|
517
|
+
"http": { "allow": ["ai-api.example.com", "hooks.slack.com"] }
|
|
518
|
+
},
|
|
519
|
+
"pipelines": [
|
|
520
|
+
{
|
|
521
|
+
"name": "post-publish",
|
|
522
|
+
"trigger": "post.published",
|
|
523
|
+
"steps": [
|
|
524
|
+
{
|
|
525
|
+
"type": "js",
|
|
526
|
+
"name": "Generate AI summary",
|
|
527
|
+
"fn": "generateSummary",
|
|
528
|
+
"timeout_ms": 8000,
|
|
529
|
+
"retry": 1
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"type": "condition",
|
|
533
|
+
"name": "Branch by post type",
|
|
534
|
+
"if": "ctx.data.post_type === 0",
|
|
535
|
+
"then": [
|
|
536
|
+
{ "type": "js", "name": "Notify Slack", "fn": "notifySlack" }
|
|
537
|
+
],
|
|
538
|
+
"else": [
|
|
539
|
+
{ "type": "webhook", "name": "Page webhook", "url": "https://hooks.example.com/pages" }
|
|
540
|
+
]
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
}
|
|
544
|
+
]
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
// src/index.ts — functions called by steps must be at module scope
|
|
551
|
+
|
|
552
|
+
function generateSummary(ctx: StepContext) {
|
|
553
|
+
const res = nuxtblog.http.fetch<{ summary: string }>('https://ai-api.example.com/summarize', {
|
|
554
|
+
method: 'POST',
|
|
555
|
+
body: { content: ctx.data.content as string },
|
|
556
|
+
})
|
|
557
|
+
if (res.ok) {
|
|
558
|
+
ctx.data.excerpt = res.body.summary // passes to the next step
|
|
559
|
+
} else {
|
|
560
|
+
ctx.abort(`AI API error: HTTP ${res.status}`)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function notifySlack(ctx: StepContext) {
|
|
565
|
+
nuxtblog.http.fetch('https://hooks.slack.com/services/xxx', {
|
|
566
|
+
method: 'POST',
|
|
567
|
+
body: { text: `New post published: ${ctx.data.title}` },
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Step types
|
|
573
|
+
|
|
574
|
+
| `type` | Description |
|
|
575
|
+
| --------------- | ------------------------------------------------------------------------------------- |
|
|
576
|
+
| `"js"` | Call an exported JS function by name (`fn`). Supports `timeout_ms` and `retry`. |
|
|
577
|
+
| `"webhook"` | POST `StepContext.data` as JSON to `url`. Supports `timeout_ms` and `retry`. |
|
|
578
|
+
| `"condition"` | Evaluate a JS boolean expression (`if`), then run `then` or `else` branches. |
|
|
579
|
+
|
|
580
|
+
### `StepContext` object
|
|
581
|
+
|
|
582
|
+
| Property | Type | Description |
|
|
583
|
+
| --------------------- | -------- | ------------------------------------------------ |
|
|
584
|
+
| `ctx.event` | string | The trigger event name |
|
|
585
|
+
| `ctx.data` | object | Shared mutable payload flowing through all steps |
|
|
586
|
+
| `ctx.meta` | object | Step-to-step KV store |
|
|
587
|
+
| `ctx.abort(reason)` | function | Stop the pipeline; subsequent steps are skipped |
|
|
588
|
+
|
|
589
|
+
### Retry backoff
|
|
590
|
+
|
|
591
|
+
When `retry` > 0, failed steps are retried with exponential backoff:
|
|
592
|
+
|
|
593
|
+
| Attempt | Wait before retry |
|
|
594
|
+
| --------- | ----------------------------------------------- |
|
|
595
|
+
| 1st retry | 200 ms |
|
|
596
|
+
| 2nd retry | 400 ms |
|
|
597
|
+
| 3rd retry | 800 ms |
|
|
598
|
+
| … | Doubles each time, capped at**8 seconds** |
|
|
599
|
+
|
|
600
|
+
### Step defaults
|
|
601
|
+
|
|
602
|
+
| Property | Default |
|
|
603
|
+
| -------------- | -------------------- |
|
|
604
|
+
| `timeout_ms` | `5000` (5 seconds) |
|
|
605
|
+
| `retry` | `0` (no retries) |
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Event Reference
|
|
610
|
+
|
|
611
|
+
### Fire-and-forget Events (`nuxtblog.on`)
|
|
612
|
+
|
|
613
|
+
#### Post
|
|
614
|
+
|
|
615
|
+
| Event | Trigger | Payload fields |
|
|
616
|
+
| ------------------ | ------------------------------------------ | ---------------------------------------------------------- |
|
|
617
|
+
| `post.created` | After a post is created | `id, title, slug, excerpt, post_type, author_id, status` |
|
|
618
|
+
| `post.updated` | After a post is updated | `id, title, slug, excerpt, post_type, author_id, status` |
|
|
619
|
+
| `post.published` | After a post's status changes to published | `id, title, slug, excerpt, post_type, author_id` |
|
|
620
|
+
| `post.deleted` | After a post is deleted / moved to trash | `id, title, slug, post_type, author_id` |
|
|
621
|
+
| `post.viewed` | After a post is viewed | `id, user_id` |
|
|
622
|
+
|
|
623
|
+
`post_type`: `0` = post, `1` = page
|
|
624
|
+
`status`: `0` = draft, `1` = published, `2` = trash
|
|
625
|
+
|
|
626
|
+
#### Comment
|
|
627
|
+
|
|
628
|
+
| Event | Trigger | Payload fields |
|
|
629
|
+
| -------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
630
|
+
| `comment.created` | After a comment is submitted | `id, status, object_type, object_id, object_title, object_slug, post_author_id, parent_id?, parent_author_id, author_id, author_name, author_email, content` |
|
|
631
|
+
| `comment.deleted` | After a comment is deleted | `id, object_type, object_id` |
|
|
632
|
+
| `comment.status_changed` | After a comment's moderation status changes | `id, object_type, object_id, old_status, new_status, moderator_id` |
|
|
633
|
+
| `comment.approved` | After a comment is approved | `id, object_type, object_id, moderator_id` |
|
|
634
|
+
|
|
635
|
+
`status`: `0` = pending, `1` = approved, `2` = spam
|
|
636
|
+
|
|
637
|
+
#### User
|
|
638
|
+
|
|
639
|
+
| Event | Trigger | Payload fields |
|
|
640
|
+
| ------------------- | ------------------------------- | ------------------------------------------------------------- |
|
|
641
|
+
| `user.registered` | After a user registers | `id, username, email, display_name, locale, role` |
|
|
642
|
+
| `user.updated` | After a user profile is updated | `id, username, email, display_name, locale, role, status` |
|
|
643
|
+
| `user.deleted` | After a user is deleted | `id, username, email` |
|
|
644
|
+
| `user.followed` | After a user follows another | `follower_id, follower_name, follower_avatar, following_id` |
|
|
645
|
+
| `user.login` | After a user logs in | `id, username, email, role` |
|
|
646
|
+
| `user.logout` | After a user logs out | `id` |
|
|
647
|
+
|
|
648
|
+
`role`: `0` = subscriber, `1` = contributor, `2` = editor, `3` = admin
|
|
649
|
+
`status`: `0` = active, `1` = banned
|
|
650
|
+
|
|
651
|
+
#### Media
|
|
652
|
+
|
|
653
|
+
| Event | Trigger | Payload fields |
|
|
654
|
+
| ------------------ | ------------------------ | --------------------------------------------------------------------------------- |
|
|
655
|
+
| `media.uploaded` | After a file is uploaded | `id, uploader_id, filename, mime_type, file_size, url, category, width, height` |
|
|
656
|
+
| `media.deleted` | After a file is deleted | `id, uploader_id, filename, mime_type, category` |
|
|
657
|
+
|
|
658
|
+
#### Taxonomy / Term
|
|
659
|
+
|
|
660
|
+
| Event | Trigger | Payload fields |
|
|
661
|
+
| -------------------- | --------------------------------------- | ----------------------------------------------- |
|
|
662
|
+
| `taxonomy.created` | After a taxonomy association is created | `id, term_id, term_name, term_slug, taxonomy` |
|
|
663
|
+
| `taxonomy.deleted` | After a taxonomy association is deleted | `id, term_name, term_slug, taxonomy` |
|
|
664
|
+
| `term.created` | After a term is created | `id, name, slug` |
|
|
665
|
+
| `term.deleted` | After a term is deleted | `id, name, slug` |
|
|
666
|
+
|
|
667
|
+
#### Reactions & Check-in
|
|
668
|
+
|
|
669
|
+
| Event | Trigger | Payload fields |
|
|
670
|
+
| -------------------- | ----------------------------------- | ----------------------------------------- |
|
|
671
|
+
| `reaction.added` | After a like or bookmark is added | `user_id, object_type, object_id, type` |
|
|
672
|
+
| `reaction.removed` | After a like or bookmark is removed | `user_id, object_type, object_id, type` |
|
|
673
|
+
| `checkin.done` | After a user checks in | `user_id, streak, already_checked_in` |
|
|
674
|
+
|
|
675
|
+
`type`: `"like"` or `"bookmark"`
|
|
676
|
+
|
|
677
|
+
#### System
|
|
678
|
+
|
|
679
|
+
| Event | Trigger | Payload fields |
|
|
680
|
+
| ---------------------- | ------------------------------ | ------------------------------ |
|
|
681
|
+
| `option.updated` | After a site option is changed | `key, value` |
|
|
682
|
+
| `plugin.installed` | After a plugin is installed | `id, title, version, author` |
|
|
683
|
+
| `plugin.uninstalled` | After a plugin is uninstalled | `id` |
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
### Filter Events (`nuxtblog.filter`)
|
|
688
|
+
|
|
689
|
+
| Event | Trigger | `ctx.data` fields | Notes |
|
|
690
|
+
| ------------------ | -------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ |
|
|
691
|
+
| `post.create` | Before a post is written to DB | `title, slug, content, excerpt, status` | |
|
|
692
|
+
| `post.update` | Before a post update is written | partial fields only | Only fields being changed are present |
|
|
693
|
+
| `post.delete` | Before a post is deleted | `id` | `abort()` cancels the deletion |
|
|
694
|
+
| `comment.create` | Before a comment is written | `content, author_name, author_email` | |
|
|
695
|
+
| `comment.delete` | Before a comment is deleted | `id` | `abort()` cancels the deletion |
|
|
696
|
+
| `term.create` | Before a term is written | `name, slug` | |
|
|
697
|
+
| `user.register` | Before a user is written | `username, email, display_name` | |
|
|
698
|
+
| `user.update` | Before a user update is written | partial fields only | Only fields being changed are present |
|
|
699
|
+
| `media.upload` | Before media metadata is written | `filename, mime_type, category, alt_text, title` | |
|
|
700
|
+
| `content.render` | Before content is rendered for readers | `content` | Modify `ctx.data.content` to change what readers see |
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Execution Model & Concurrency
|
|
705
|
+
|
|
706
|
+
- Each plugin has exactly **one goja VM** and **one mutex**. All VM operations (script execution, handler calls, `ToValue`, etc.) must hold the mutex.
|
|
707
|
+
- The `nuxtblog.on` handler timeout is **3 seconds** by default. Timeouts are enforced via `vm.Interrupt` (goroutine-safe; does not require the mutex).
|
|
708
|
+
- The `nuxtblog.filter` handler timeout is **50 milliseconds** by default. This is intentionally short to keep request latency predictable.
|
|
709
|
+
- Multiple plugins run in ascending **priority** order (lower number = earlier). Within the same priority, plugins are sorted alphabetically by ID.
|
|
710
|
+
- `inFilter` is an atomic flag set during filter chain execution. `http.fetch` checks this flag and returns an error object (not a panic/throw) when called inside a filter.
|
|
711
|
+
- Pipeline goroutines and webhook goroutines are fire-and-forget — they never block `fanOut`.
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Timeouts & Retry
|
|
716
|
+
|
|
717
|
+
| Context | Default timeout | Configurable |
|
|
718
|
+
| --------------------------- | --------------- | -------------------------------- |
|
|
719
|
+
| `nuxtblog.on` handler | 3 seconds | In manifest (future) |
|
|
720
|
+
| `nuxtblog.filter` handler | 50 ms | In manifest (future) |
|
|
721
|
+
| Pipeline `js` step | 5 seconds | `timeout_ms` in step |
|
|
722
|
+
| Pipeline `webhook` step | 5 seconds | `timeout_ms` in step |
|
|
723
|
+
| Pipeline `condition` step | 50 ms | Not configurable |
|
|
724
|
+
| `http.fetch` request | 15 seconds | `capabilities.http.timeout_ms` |
|
|
725
|
+
| Declarative webhook POST | 10 seconds | Not configurable |
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## Observability
|
|
730
|
+
|
|
731
|
+
The plugin engine exposes runtime metrics via the server's internal API.
|
|
732
|
+
|
|
733
|
+
### Stats (`GetStats`)
|
|
734
|
+
|
|
735
|
+
| Field | Description |
|
|
736
|
+
| ------------------- | ------------------------------------------ |
|
|
737
|
+
| `plugin_id` | Plugin identifier |
|
|
738
|
+
| `invocations` | Total handler/filter executions |
|
|
739
|
+
| `errors` | Total executions that resulted in an error |
|
|
740
|
+
| `avg_duration_ms` | Average execution time in milliseconds |
|
|
741
|
+
| `last_error` | Most recent error message |
|
|
742
|
+
| `last_error_at` | Timestamp of the most recent error |
|
|
743
|
+
|
|
744
|
+
### Sliding window (`GetHistory`)
|
|
745
|
+
|
|
746
|
+
60 one-minute buckets covering the last hour. Each bucket contains `invocations` and `errors` for that minute. Buckets with no activity return zero counters.
|
|
747
|
+
|
|
748
|
+
### Error ring buffer (`GetErrors`)
|
|
749
|
+
|
|
750
|
+
Stores the last 100 error entries. Each entry contains:
|
|
751
|
+
|
|
752
|
+
| Field | Description |
|
|
753
|
+
| -------------- | ------------------------------------------------------ |
|
|
754
|
+
| `at` | Timestamp |
|
|
755
|
+
| `event` | Event name that triggered the error |
|
|
756
|
+
| `message` | Error message |
|
|
757
|
+
| `input_diff` | JSON diff of `ctx.data` changes (filter errors only) |
|
|
758
|
+
|
|
759
|
+
The diff format uses prefixes: `+key` = added, `-key` = removed, `~key` = changed (`{ before, after }`).
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## Building & Installing
|
|
764
|
+
|
|
765
|
+
### Bundling with esbuild (recommended)
|
|
766
|
+
|
|
767
|
+
```bash
|
|
768
|
+
npm install -D esbuild
|
|
769
|
+
|
|
770
|
+
npx esbuild src/index.ts \
|
|
771
|
+
--bundle \
|
|
772
|
+
--platform=neutral \
|
|
773
|
+
--main-fields=browser,module,main \
|
|
774
|
+
--target=es2015 \
|
|
775
|
+
--outfile=index.js
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### Creating the archive
|
|
779
|
+
|
|
780
|
+
The server uses [mholt/archives](https://github.com/mholt/archives) for extraction. Supported formats:
|
|
781
|
+
|
|
782
|
+
| Format | Extension |
|
|
783
|
+
| ----------- | ---------------------- |
|
|
784
|
+
| ZIP | `.zip` |
|
|
785
|
+
| Tar + Gzip | `.tar.gz` / `.tgz` |
|
|
786
|
+
| Tar + Bzip2 | `.tar.bz2` |
|
|
787
|
+
| Tar + XZ | `.tar.xz` |
|
|
788
|
+
| Tar + Zstd | `.tar.zst` |
|
|
789
|
+
| 7-Zip | `.7z` |
|
|
790
|
+
| RAR | `.rar` |
|
|
791
|
+
|
|
792
|
+
**ZIP (Python):**
|
|
793
|
+
|
|
794
|
+
```python
|
|
795
|
+
import zipfile
|
|
796
|
+
with zipfile.ZipFile("my-plugin.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
|
797
|
+
z.write("package.json")
|
|
798
|
+
z.write("index.js")
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**tar.gz (Shell):**
|
|
802
|
+
|
|
803
|
+
```bash
|
|
804
|
+
tar -czf my-plugin.tar.gz package.json index.js
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
**PowerShell:**
|
|
808
|
+
|
|
809
|
+
```powershell
|
|
810
|
+
Compress-Archive -Path package.json, index.js -DestinationPath my-plugin.zip
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### Installation methods
|
|
814
|
+
|
|
815
|
+
1. **Local archive** — Admin panel → Plugins → Install → Local ZIP → Upload
|
|
816
|
+
2. **GitHub** — Admin panel → Plugins → Install → GitHub → Enter `owner/repo` (the server automatically downloads the latest Release asset named `plugin.zip`)
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## TypeScript Support
|
|
821
|
+
|
|
822
|
+
Install the SDK package to get full type coverage for the `nuxtblog` global object.
|
|
823
|
+
|
|
824
|
+
```bash
|
|
825
|
+
pnpm add -D @nuxtblog/plugin-sdk
|
|
826
|
+
# or
|
|
827
|
+
npm install -D @nuxtblog/plugin-sdk
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
**`tsconfig.json`:**
|
|
831
|
+
|
|
832
|
+
```json
|
|
833
|
+
{
|
|
834
|
+
"extends": "@nuxtblog/plugin-sdk",
|
|
835
|
+
"include": ["src"]
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**Or add a reference at the top of your entry file:**
|
|
840
|
+
|
|
841
|
+
```ts
|
|
842
|
+
/// <reference path="../../node_modules/@nuxtblog/plugin-sdk/index.d.ts" />
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
## License
|
|
848
|
+
|
|
849
|
+
MIT
|