@freelygive/canvas-jsonapi 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freelygive/canvas-jsonapi",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for managing content in Drupal Canvas / Acquia Source via JSON:API.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,8 +8,14 @@
8
8
  },
9
9
  "files": [
10
10
  "src",
11
+ "skills",
11
12
  "README.md"
12
13
  ],
14
+ "npmScaffold": {
15
+ "file-mapping": {
16
+ ".claude/skills/content-management/SKILL.md": "skills/content-management/SKILL.md"
17
+ }
18
+ },
13
19
  "engines": {
14
20
  "node": ">=18"
15
21
  },
@@ -0,0 +1,450 @@
1
+ ---
2
+ name: content-management
3
+ description:
4
+ Managing content in Acquia Source via JSON:API including pages, media, and
5
+ components
6
+ ---
7
+
8
+ # Content Management Skill
9
+
10
+ This skill provides tools for managing content in Acquia Source via JSON:API.
11
+ Use these commands to list, fetch, update, and create pages and other content
12
+ entities.
13
+
14
+ ## Available Commands
15
+
16
+ All commands use the `@freelygive/canvas-jsonapi` CLI:
17
+
18
+ ```bash
19
+ npx canvas-jsonapi <command> [args...]
20
+ ```
21
+
22
+ ### List Content
23
+
24
+ List content items of a specific type:
25
+
26
+ ```bash
27
+ npx canvas-jsonapi list <type>
28
+ npx canvas-jsonapi list --types # Discover available types
29
+ ```
30
+
31
+ ### Get Content
32
+
33
+ Fetch one or more content items and save them locally:
34
+
35
+ ```bash
36
+ npx canvas-jsonapi get <type> <uuid> [<uuid>...]
37
+ npx canvas-jsonapi get <type> <uuid> --include <relationships>
38
+ ```
39
+
40
+ Examples:
41
+
42
+ ```bash
43
+ npx canvas-jsonapi get page abc-123-def
44
+ npx canvas-jsonapi get page abc-123 def-456 ghi-789
45
+ npx canvas-jsonapi get media--image uuid1 uuid2 uuid3
46
+ npx canvas-jsonapi get page abc-123-def --include image,owner
47
+ ```
48
+
49
+ Saves content to `content/<type>/<uuid>.json`. For `media--image`, automatically
50
+ includes the file relationship and displays the thumbnail URL and `target_id`.
51
+
52
+ ### Create Content
53
+
54
+ Create a new content item from a local file:
55
+
56
+ ```bash
57
+ npx canvas-jsonapi create <file-path>
58
+ ```
59
+
60
+ After creation, the temporary file is removed and the full entity is fetched and
61
+ saved with the UUID returned by the API.
62
+
63
+ ### Update Content
64
+
65
+ Push changes from a local JSON file back to the API:
66
+
67
+ ```bash
68
+ npx canvas-jsonapi update <file-path>
69
+ ```
70
+
71
+ The file must contain valid JSON:API data with `data.type` and `data.id` fields.
72
+
73
+ ### Delete Content
74
+
75
+ Delete one or more content items:
76
+
77
+ ```bash
78
+ npx canvas-jsonapi delete <type> <uuid> [<uuid>...]
79
+ ```
80
+
81
+ Examples:
82
+
83
+ ```bash
84
+ npx canvas-jsonapi delete page abc-123-def
85
+ npx canvas-jsonapi delete media--image uuid1 uuid2 uuid3
86
+ ```
87
+
88
+ Also removes the local JSON file if it exists.
89
+
90
+ ### Upload Image
91
+
92
+ Upload an image and create a media entity:
93
+
94
+ ```bash
95
+ npx canvas-jsonapi upload-image <image-path> [alt-text]
96
+ ```
97
+
98
+ Example:
99
+
100
+ ```bash
101
+ npx canvas-jsonapi upload-image src/stories/assets/photo.jpg "Photo description"
102
+ ```
103
+
104
+ Output includes:
105
+
106
+ - Media UUID
107
+ - File path
108
+ - Thumbnail URL
109
+ - `target_id` for use in components
110
+
111
+ ### Generate UUID
112
+
113
+ Generate random UUIDs for new components:
114
+
115
+ ```bash
116
+ npx canvas-jsonapi uuid # Generate 1 UUID
117
+ npx canvas-jsonapi uuid 5 # Generate 5 UUIDs
118
+ ```
119
+
120
+ ## Local File Storage
121
+
122
+ Content is stored in the `/content` directory (gitignored):
123
+
124
+ ```
125
+ content/
126
+ page/
127
+ abc-123-def-456.json
128
+ media--image/
129
+ img-123-456.json
130
+ ```
131
+
132
+ ## Page Structure
133
+
134
+ Pages in Acquia Source contain these key attributes:
135
+
136
+ - `title` - Page title
137
+ - `status` - Published status (true/false)
138
+ - `path` - URL path configuration
139
+ - `components` - Array of canvas components
140
+ - `metatags` - SEO metadata
141
+ - `include_in_search` - Whether to index for search
142
+
143
+ ### Canvas Component Structure
144
+
145
+ Components are stored in `data.attributes.components` as a flat array. Nesting
146
+ is defined via `parent_uuid` and `slot` fields:
147
+
148
+ ```json
149
+ {
150
+ "data": {
151
+ "type": "page",
152
+ "attributes": {
153
+ "title": "My Page",
154
+ "status": true,
155
+ "components": [
156
+ {
157
+ "uuid": "comp-001",
158
+ "component_id": "js.section",
159
+ "inputs": { "width": "Normal" },
160
+ "parent_uuid": null,
161
+ "slot": null
162
+ },
163
+ {
164
+ "uuid": "comp-002",
165
+ "component_id": "js.heading",
166
+ "inputs": { "text": "Title", "level": "h2" },
167
+ "parent_uuid": "comp-001",
168
+ "slot": "content"
169
+ }
170
+ ]
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ Note: `inputs` are automatically parsed to objects when fetched and stringified
177
+ when sent back to the API.
178
+
179
+ ### Component Fields
180
+
181
+ | Field | Description |
182
+ | ------------------- | ---------------------------------------------------------- |
183
+ | `uuid` | Unique identifier for this component instance |
184
+ | `component_id` | Component type (e.g., `js.heading`, `js.card`) |
185
+ | `component_version` | Version hash of the component definition |
186
+ | `inputs` | Object containing prop values |
187
+ | `parent_uuid` | UUID of parent component (null for root-level) |
188
+ | `slot` | Slot name in parent (null for root-level, e.g., "content") |
189
+
190
+ ## Media Image Handling
191
+
192
+ ### Uploading Images
193
+
194
+ Images must be uploaded to the media library before referencing in components:
195
+
196
+ ```bash
197
+ npx canvas-jsonapi upload-image image.jpg "Alt text"
198
+ ```
199
+
200
+ Output:
201
+
202
+ ```
203
+ Uploading: image.jpg
204
+ Uploaded: image.jpg
205
+ UUID: 98eabd02-c52c-493b-8ca9-cb9d0fe70ceb
206
+ File: /var/www/html/pages/media--image/98eabd02-...json
207
+ Thumbnail: https://...
208
+ target_id: 31
209
+ ```
210
+
211
+ ### Media vs File Entity IDs (Critical)
212
+
213
+ When working with images, there are two different internal IDs:
214
+
215
+ | Entity Type | ID Location | Usage |
216
+ | ----------- | --------------------------------------------- | -------------------------- |
217
+ | **File** | `drupal_internal__target_id` in relationships | Internal file reference |
218
+ | **Media** | `resourceVersion=id%3AXX` in self link URL | **Use this in components** |
219
+
220
+ The `target_id` shown in command output is the correct media internal ID.
221
+
222
+ ### Referencing Images in Components
223
+
224
+ Components that accept images (like `js.card`) use a `target_id` reference:
225
+
226
+ ```json
227
+ {
228
+ "component_id": "js.card",
229
+ "inputs": {
230
+ "heading": "My Card",
231
+ "image": { "target_id": "31" },
232
+ "text": { "value": "<p>Content</p>", "format": "canvas_html_block" }
233
+ }
234
+ }
235
+ ```
236
+
237
+ **Important:** The `target_id` must be the **media entity's internal ID**, not
238
+ the file's internal ID.
239
+
240
+ ### Text Fields with HTML
241
+
242
+ Rich text fields use a specific format:
243
+
244
+ ```json
245
+ {
246
+ "text": {
247
+ "value": "<p>HTML content with <a href=\"/page\">links</a>.</p>",
248
+ "format": "canvas_html_block"
249
+ }
250
+ }
251
+ ```
252
+
253
+ ### Allowed HTML Elements in Formatted Content
254
+
255
+ The following HTML elements are supported in formatted editor fields (like
256
+ article body content):
257
+
258
+ #### Inline Formatting
259
+
260
+ | Element | Usage | Example |
261
+ | ---------- | --------------- | ---------------------------- |
262
+ | `<strong>` | Bold text | `<strong>important</strong>` |
263
+ | `<em>` | Italic/emphasis | `<em>emphasized</em>` |
264
+ | `<u>` | Underlined text | `<u>underlined</u>` |
265
+ | `<s>` | Strikethrough | `<s>deleted</s>` |
266
+ | `<sup>` | Superscript | `x<sup>2</sup>` |
267
+ | `<sub>` | Subscript | `H<sub>2</sub>O` |
268
+ | `<a>` | Links | `<a href="/page">link</a>` |
269
+ | `<code>` | Inline code | `<code>variable</code>` |
270
+
271
+ #### Block Elements
272
+
273
+ | Element | Usage | Example |
274
+ | --------------- | --------------------------- | -------------------------------- |
275
+ | `<p>` | Paragraphs | `<p>Text content</p>` |
276
+ | `<h2>` - `<h6>` | Headings (h2-h6 only) | `<h2>Section Title</h2>` |
277
+ | `<ul>` | Unordered list | `<ul><li>Item</li></ul>` |
278
+ | `<ol>` | Ordered list | `<ol><li>Step 1</li></ol>` |
279
+ | `<blockquote>` | Highlighted external quotes | `<blockquote>Quote</blockquote>` |
280
+
281
+ #### Text Alignment Classes
282
+
283
+ Apply alignment via class attribute on block elements:
284
+
285
+ | Class | Effect |
286
+ | --------------------- | -------------- |
287
+ | `.text-align-center` | Center aligned |
288
+ | `.text-align-right` | Right aligned |
289
+ | `.text-align-justify` | Justified text |
290
+
291
+ Example:
292
+
293
+ ```html
294
+ <p class="text-align-center">Centered paragraph</p>
295
+ <h2 class="text-align-right">Right-aligned heading</h2>
296
+ ```
297
+
298
+ #### Embedded Media
299
+
300
+ Media items (images, videos) can be embedded within formatted content using
301
+ Drupal's `<drupal-media>` tag. This allows images to appear inline within
302
+ article body content.
303
+
304
+ **Format:**
305
+
306
+ ```html
307
+ <drupal-media data-entity-type="media" data-entity-uuid="MEDIA-UUID"
308
+ >&nbsp;</drupal-media
309
+ >
310
+ ```
311
+
312
+ **Steps to embed an image:**
313
+
314
+ 1. First, upload the image to create a media entity:
315
+
316
+ ```bash
317
+ npx canvas-jsonapi upload-image path/to/image.jpg "Alt text description"
318
+ ```
319
+
320
+ Note the UUID returned (e.g., `c4324a2c-6a1d-43a1-98b5-bb2d73e42c54`).
321
+
322
+ 2. Or find an existing media item:
323
+
324
+ ```bash
325
+ npx canvas-jsonapi list media--image
326
+ ```
327
+
328
+ 3. Add the `<drupal-media>` tag to the content's `value` field:
329
+
330
+ ```json
331
+ {
332
+ "content": {
333
+ "value": "<p>Article text here...</p><drupal-media data-entity-type=\"media\" data-entity-uuid=\"c4324a2c-6a1d-43a1-98b5-bb2d73e42c54\">&nbsp;</drupal-media><p>More text after image...</p>",
334
+ "format": "filtered_html"
335
+ }
336
+ }
337
+ ```
338
+
339
+ **Important notes:**
340
+
341
+ - The `&nbsp;` inside the tag is required (non-breaking space placeholder)
342
+ - Use the media entity's UUID, not the file UUID
343
+ - The `processed` field in the response shows the rendered HTML with actual
344
+ `<img>` tags - this is read-only and generated by Drupal
345
+ - Only edit the `value` field, never the `processed` field
346
+
347
+ ### Content Formatting Best Practices
348
+
349
+ 1. **Use semantic elements**: Use `<strong>` for important text, `<em>` for
350
+ emphasis, not just for visual styling.
351
+
352
+ 2. **Blockquotes for highlighted external quotes only**: Use `<blockquote>` only
353
+ for external quotes that need visual emphasis (e.g., testimonials, key
354
+ statements from interviews). Do not use for inline quotes within regular
355
+ paragraphs. Use `<em>` for speaker attribution:
356
+
357
+ ```html
358
+ <blockquote>
359
+ <p>"Quote text here"</p>
360
+ <p><em>— Speaker Name, Title</em></p>
361
+ </blockquote>
362
+ ```
363
+
364
+ For regular inline quotes, simply use quotation marks within a `<p>` tag.
365
+
366
+ 3. **Lists for related items**: Use `<ul>` or `<ol>` when listing multiple items
367
+ rather than comma-separated text.
368
+
369
+ 4. **Headings for structure**: Use `<h2>`-`<h6>` to create clear content
370
+ sections.
371
+
372
+ ## Workflow Examples
373
+
374
+ ### Download and Edit a Page
375
+
376
+ ```bash
377
+ npx canvas-jsonapi list page
378
+ npx canvas-jsonapi get page abc-123-def
379
+ # Edit content/page/abc-123-def.json
380
+ npx canvas-jsonapi update content/page/abc-123-def.json
381
+ ```
382
+
383
+ ### Create a Page with Images
384
+
385
+ 1. Upload images:
386
+
387
+ ```bash
388
+ npx canvas-jsonapi upload-image image1.jpg "Description"
389
+ # Note target_id: 31
390
+ ```
391
+
392
+ 2. Generate UUIDs:
393
+
394
+ ```bash
395
+ npx canvas-jsonapi uuid 3
396
+ ```
397
+
398
+ 3. Create page JSON at `content/page/new-my-page.json`:
399
+
400
+ ```json
401
+ {
402
+ "data": {
403
+ "type": "page",
404
+ "attributes": {
405
+ "title": "My Page",
406
+ "status": true,
407
+ "components": [
408
+ {
409
+ "uuid": "generated-uuid",
410
+ "component_id": "js.card",
411
+ "inputs": {
412
+ "heading": "Card",
413
+ "image": { "target_id": "31" }
414
+ },
415
+ "parent_uuid": null,
416
+ "slot": null
417
+ }
418
+ ],
419
+ "path": { "alias": "/my-page" },
420
+ "include_in_search": true
421
+ }
422
+ }
423
+ }
424
+ ```
425
+
426
+ 4. Create the page:
427
+
428
+ ```bash
429
+ npx canvas-jsonapi create content/page/new-my-page.json
430
+ ```
431
+
432
+ ## Common Pitfalls
433
+
434
+ 1. **Wrong ID type**: Using file's `drupal_internal__target_id` instead of
435
+ media's internal ID causes "image.src NULL value found" errors.
436
+
437
+ 2. **Missing langcode permission**: When creating pages, omit the `langcode`
438
+ field as the API may reject it with permission errors.
439
+
440
+ 3. **Patching limitations**: PATCH requests may not work for all fields. Create
441
+ a new page with a different alias if updates fail.
442
+
443
+ ## Environment Variables
444
+
445
+ Required in `.env`:
446
+
447
+ - `CANVAS_SITE_URL` - Base URL of Acquia Source site
448
+ - `CANVAS_JSONAPI_PREFIX` - API prefix (default: "api")
449
+ - `CANVAS_CLIENT_ID` - OAuth client ID
450
+ - `CANVAS_CLIENT_SECRET` - OAuth client secret