html-to-gutenberg 4.2.9 → 4.2.10

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 CHANGED
@@ -1,136 +1,170 @@
1
1
  # HTML to Gutenberg Converter
2
2
 
3
- <!-- [![Build Status](https://github.com/DiogoAngelim/html-to-gutenberg/actions/workflows/main.yml/badge.svg?cacheBust=1)](https://github.com/DiogoAngelim/html-to-gutenberg/actions)
4
- [![Coverage Status](https://coveralls.io/repos/github/DiogoAngelim/html-to-gutenberg/badge.svg?branch=main&cacheBust=1)](https://coveralls.io/github/DiogoAngelim/html-to-gutenberg?branch=main) -->
5
3
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/DiogoAngelim/html-to-gutenberg/blob/main/LICENSE.MD)
6
4
 
7
-
5
+ Convert HTML into editable WordPress Gutenberg blocks and publish the generated package to Cloudflare R2 without writing the output to disk.
8
6
 
9
- Convert HTML strings to valid, editable WordPress Gutenberg blocks in seconds instead of hours. With this lib, you can create and build valid Gutenberg blocks that feature editable text, forms, inline and background images, as well as SVGs.
7
+ ## What changed
10
8
 
9
+ - `html-to-gutenberg` now supports a `job` output mode that uploads generated files to R2 and returns a JSON manifest.
10
+ - `fetch-page-assets` can upload downloaded assets directly to R2 and return their metadata.
11
+ - Output bundles are zipped in memory and uploaded to R2 as `output.zip`.
12
+ - Secrets stay in `.env` and should never be committed.
11
13
 
12
- ## Features
13
-
14
-
15
-
16
- - 🪄 **Instantly transforms static HTML into Gutenberg blocks**
17
- Saves hours of manual work by automating block creation from any valid HTML snippet.
18
-
19
- - 🔌 **Generates a complete, installable WordPress block plugin**
20
- Outputs all necessary plugin files (JS, CSS, PHP) so you can drop them into WordPress immediately.
21
-
22
-
23
- - 🎨 **Keeps your design intact**
24
- Automatically extracts and preserves CSS from the original HTML into a separate `style.css`.
25
-
26
-
27
- - 🧩 **Modular and scalable**
28
- Separates assets (JS, CSS, components) into clean files, making it easy to maintain and extend.
29
-
30
-
31
- - 📦 **Seamlessly integrates with dynamic block systems**
32
- Works perfectly for headless or custom Gutenberg setups where blocks are registered via JS, not PHP.
33
-
34
-
35
- - 🚀 **Speeds up prototyping**
36
- Ideal for quickly testing block ideas or converting landing pages and templates into WordPress blocks.
37
-
38
-
39
- - 🧠 **Works with your file system or plugin builder logic**
40
- Since it returns all files as either strings or source files, you can save them however you like (via PHP, APIs, etc.).
41
-
42
-
43
- - 🧰 **Built for automation and customization**
44
- Can be embedded in custom tools, UIs, or pipelines to generate Gutenberg blocks on demand.
14
+ ## Installation
45
15
 
46
- ## How it works
16
+ ```bash
17
+ npm install html-to-gutenberg
18
+ ```
47
19
 
48
- This package is actually an alternative when AI fails converting it, which is usually very common.
20
+ ## Environment
49
21
 
50
- Most of the logic is just hard-coded patterns. It first converts from plain HTML to Jsx (using the `html-to-jsx` package), which is the supported format of Gutenberg. Then, it follows structured conversion rules that build the WordPress block, which is then validated and parsed using Babel.
22
+ Copy `.env.example` to `.env` and keep the real values private.
51
23
 
24
+ ```bash
25
+ cp .env.example .env
26
+ ```
52
27
 
53
- ## The Building Process - An Overview
28
+ Required for R2-backed job output:
54
29
 
30
+ - `CLOUDFLARE_R2_ACCOUNT_ID`
31
+ - `CLOUDFLARE_R2_BUCKET`
32
+ - `CLOUDFLARE_R2_ACCESS_KEY_ID`
33
+ - `CLOUDFLARE_R2_SECRET_ACCESS_KEY`
34
+ - `CLOUDFLARE_R2_PUBLIC_BASE_URL`
55
35
 
56
- Below is a visual overview of the block generation process:
36
+ Optional:
57
37
 
58
- ![Block Generation Process](process.png)
38
+ - `CLOUDFLARE_API_TOKEN`
39
+ - `SNAPAPI_KEY`
59
40
 
41
+ ## Getting and rotating Cloudflare credentials
60
42
 
61
- ## Installation
43
+ 1. Open the Cloudflare dashboard.
44
+ 2. Create or update your R2 access keys for the target bucket.
45
+ 3. Store the new values in `.env`.
46
+ 4. If you use a Cloudflare API token for verification or account workflows, create a new token in the API Tokens section and update `.env`.
47
+ 5. Restart your app or redeploy after updating `.env`.
48
+ 6. Revoke the old token or key after the new one is live.
62
49
 
63
-
64
- Install html-to-gutenberg with npm:
50
+ To verify a Cloudflare API token without exposing it in code, use an environment variable:
65
51
 
66
52
  ```bash
67
-
68
- npm install html-to-gutenberg
69
-
53
+ curl "https://api.cloudflare.com/client/v4/user/tokens/verify" \
54
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
70
55
  ```
71
56
 
72
-
73
-
74
- ## Usage/Examples
75
-
76
-
57
+ ## Usage
77
58
 
78
- ```javascript
59
+ ```js
60
+ import block from 'html-to-gutenberg';
79
61
 
80
- // block-generator.js
62
+ const result = await block('<div>Hello world</div>', {
63
+ title: 'Marketing Hero',
64
+ slug: 'marketing-hero',
65
+ namespace: 'wp',
66
+ baseUrl: 'https://example.com',
67
+ outputMode: 'job',
68
+ uploadToR2: true,
69
+ jobId: 'conv_123'
70
+ });
81
71
 
82
- import block from 'html-to-gutenberg';
72
+ console.log(result);
73
+ ```
83
74
 
84
- const htmlString = '<div>My content</div>';
75
+ Example response:
85
76
 
77
+ ```json
86
78
  {
87
- const files = await block(htmlString, { name: 'My Block' });
88
- console.log(files);
79
+ "jobId": "conv_123",
80
+ "status": "completed",
81
+ "output": {
82
+ "files": [
83
+ {
84
+ "id": "file_1",
85
+ "name": "block.js",
86
+ "type": "text/javascript",
87
+ "size": 18234,
88
+ "path": "/generated/conv_123/block.js",
89
+ "url": "https://storage.example.com/generated/conv_123/block.js",
90
+ "kind": "source"
91
+ },
92
+ {
93
+ "id": "file_2",
94
+ "name": "asset.png",
95
+ "type": "image/png",
96
+ "size": 48211,
97
+ "path": "/generated/conv_123/assets/asset.png",
98
+ "url": "https://storage.example.com/generated/conv_123/assets/asset.png",
99
+ "kind": "asset"
100
+ }
101
+ ],
102
+ "bundle": {
103
+ "name": "output.zip",
104
+ "path": "/generated/conv_123/output.zip",
105
+ "url": "https://storage.example.com/generated/conv_123/output.zip",
106
+ "zipUrl": "https://storage.example.com/generated/conv_123/output.zip"
107
+ }
108
+ }
89
109
  }
90
-
91
110
  ```
92
111
 
93
-
112
+ ## Legacy mode
94
113
 
114
+ If you still need the previous local-string output for existing tooling or tests, use:
95
115
 
96
- When provided with a valid HTML string with the desired options, the block function will generate the necessary WordPress block files with the specified configuration. To install the block and its assets, simply load the generated folder into the plugins folder and activate it as a plugin.
97
-
98
-
99
- ## Options reference
116
+ ```js
117
+ const files = await block('<div>Hello world</div>', {
118
+ title: 'Legacy Block',
119
+ outputPath: process.cwd(),
120
+ writeFiles: false,
121
+ outputMode: 'legacy'
122
+ });
123
+ ```
100
124
 
125
+ In `legacy` mode, the function returns the generated file contents instead of the R2 job manifest.
101
126
 
102
- | Option | Description | Type | Required? | Default |
103
- |---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------|-------------------|
104
- | name | The name of your block. This will also be used for the folder name and internal references. | string | Yes | My block |
105
- | source | A URL where relative paths resolve. E.g., `http://localhost/website`. | string | Yes, only if the HTML string or the stylesheet has relative paths. | null |
106
- | prefix | A namespace prefix for the block name, typically aligned with your project (e.g., "wp" or "myplugins"). | string | No | wp |
107
- | category | The WordPress block category where the block appears in the editor. Use an existing one or register a custom category if needed. | string | No | common |
108
- | basePath | The absolute path where the output files and folders will be saved. | string | No | Current directory |
109
- | generateIconPreview | If you enable the `generateIconPreview` option by setting it to `true`, this package will generate a static image preview of your block using the [SnapAPI](https://snapapi.pics/) screenshot service. You must provide a SnapAPI key in a `.env` file (see below), which will display it a replacement for the block icons in the WP dashboard. | boolean | No | false |
110
- | shouldSaveFiles | When `true`, the generated block files are saved directly to disk. When `false`, returns an object containing the file contents as strings instead. | boolean | No | true |
111
- | jsFiles | An array of external JavaScript file URLs to enqueue with the block on the editor and the frontend. Useful for adding remote libraries. | string[] | No | [] |
112
- | cssFiles | An array of external CSS file URLs to enqueue with the block on the editor and the frontend. Useful for adding additional remote stylesheets. | string[] | No | [] |
127
+ ## Options
113
128
 
129
+ | Option | Description | Type | Default |
130
+ | --- | --- | --- | --- |
131
+ | `title` | Human-readable block title shown in the editor. | `string` | `My block` |
132
+ | `slug` | Filesystem-safe internal block name. Defaults to a slugified title. | `string` | slugified `title` |
133
+ | `baseUrl` | Base URL used to resolve relative asset paths in HTML and CSS. | `string \| null` | `null` |
134
+ | `namespace` | Gutenberg block namespace. | `string` | `wp` |
135
+ | `category` | Gutenberg block category. | `string` | `common` |
136
+ | `registerCategoryIfMissing` | Adds a custom editor category before block registration when needed. | `boolean` | `false` |
137
+ | `outputPath` | Absolute directory used for local legacy output. In `job` mode it is only a logical working base. | `string` | current directory |
138
+ | `writeFiles` | Writes local files in `legacy` mode. When `false`, returns generated files in memory. | `boolean` | `false` in the streamlined API |
139
+ | `generatePreviewImage` | Generates and uploads `preview.jpeg` using SnapAPI. | `boolean` | `false` |
140
+ | `jsFiles` | Remote JS dependencies to enqueue. | `string[]` | `[]` |
141
+ | `cssFiles` | Remote CSS dependencies to enqueue. | `string[]` | `[]` |
142
+ | `outputMode` | Advanced option. `job` uploads to R2 and returns JSON. `legacy` returns raw file contents. | `'job' \| 'legacy'` | `job`, unless local-output options imply `legacy` |
143
+ | `uploadToR2` | Advanced option to force or disable R2 uploads. | `boolean` | `true` in `job` mode |
144
+ | `jobId` | Advanced stable conversion identifier. | `string` | autogenerated |
114
145
 
146
+ Legacy aliases still work for backwards compatibility:
115
147
 
116
- **Special thanks to [Alex Serebryakov](https://snapapi.pics/) for creating and maintaining SnapAPI!**
148
+ - `name` -> `title`
149
+ - `prefix` -> `namespace`
150
+ - `source` -> `baseUrl`
151
+ - `basePath` -> `outputPath`
152
+ - `shouldSaveFiles` -> `writeFiles`
153
+ - `generateIconPreview` -> `generatePreviewImage`
117
154
 
155
+ ## Notes
118
156
 
119
- ## Running Tests
157
+ - Generated output is zipped in memory before upload.
158
+ - R2 uploads use the values from `.env`.
159
+ - Do not hardcode real tokens or keys in source code, docs, or tests.
120
160
 
121
- To run the test suite, use:
161
+ ## Running tests
122
162
 
123
163
  ```bash
124
- cd html-to-gutenberg && npm install
164
+ npm install
125
165
  npm test
126
166
  ```
127
167
 
128
- This will execute all unit and integration tests using Mocha and Chai. Make sure all dependencies are installed with `npm install` before running tests.
129
-
130
- Some tests (such as screenshot preview generation) may require a valid SnapAPI key in your `.env` file.
131
-
132
-
133
168
  ## License
134
-
135
169
 
136
- [MIT](https://github.com/DiogoAngelim/html-to-gutenberg/blob/main/LICENSE.MD)
170
+ [MIT](https://github.com/DiogoAngelim/html-to-gutenberg/blob/main/LICENSE.MD)
@@ -0,0 +1,13 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ const projectRoot = process.cwd();
5
+ const sourcePath = path.join(projectRoot, "vendor", "fetch-page-assets", "index.js");
6
+ const targetPath = path.join(projectRoot, "node_modules", "fetch-page-assets", "index.js");
7
+
8
+ if (!fs.existsSync(sourcePath) || !fs.existsSync(targetPath)) {
9
+ process.exit(0);
10
+ }
11
+
12
+ fs.copyFileSync(sourcePath, targetPath);
13
+ console.log("Applied local fetch-page-assets performance patch");
@@ -0,0 +1,115 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { execFileSync } from 'child_process';
5
+
6
+ const repoRoot = process.cwd();
7
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sync-from-npm-'));
8
+ const targetDir = process.env.NPM_SYNC_TARGET_DIR?.trim() || '.';
9
+ const targetRoot = path.resolve(repoRoot, targetDir);
10
+
11
+ const readPackageName = () => {
12
+ const packageJsonPath = path.join(targetRoot, 'package.json');
13
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
14
+ return packageJson.name;
15
+ };
16
+
17
+ const parsePreserveList = (value) => {
18
+ const rawEntries = (value || '.git,.github,node_modules,.env,scripts')
19
+ .split(',')
20
+ .map((entry) => entry.trim())
21
+ .filter(Boolean);
22
+
23
+ return new Set(rawEntries);
24
+ };
25
+
26
+ const shouldPreserve = (entryName, preserveList) => {
27
+ return preserveList.has(entryName);
28
+ };
29
+
30
+ const removeUnsyncedEntries = (sourceRoot, preserveList, dryRun) => {
31
+ if (!fs.existsSync(targetRoot)) {
32
+ return;
33
+ }
34
+
35
+ for (const entry of fs.readdirSync(targetRoot)) {
36
+ if (shouldPreserve(entry, preserveList)) {
37
+ continue;
38
+ }
39
+
40
+ const targetPath = path.join(targetRoot, entry);
41
+ const sourcePath = path.join(sourceRoot, entry);
42
+
43
+ if (fs.existsSync(sourcePath)) {
44
+ continue;
45
+ }
46
+
47
+ if (dryRun) {
48
+ console.log(`Would remove ${entry}`);
49
+ continue;
50
+ }
51
+
52
+ fs.rmSync(targetPath, { recursive: true, force: true });
53
+ }
54
+ };
55
+
56
+ const copyPackageEntries = (sourceRoot, preserveList, dryRun) => {
57
+ if (!dryRun) {
58
+ fs.mkdirSync(targetRoot, { recursive: true });
59
+ }
60
+
61
+ for (const entry of fs.readdirSync(sourceRoot)) {
62
+ if (shouldPreserve(entry, preserveList)) {
63
+ console.log(`Skipping preserved path ${entry}`);
64
+ continue;
65
+ }
66
+
67
+ const sourcePath = path.join(sourceRoot, entry);
68
+ const targetPath = path.join(targetRoot, entry);
69
+
70
+ if (dryRun) {
71
+ console.log(`Would sync ${entry}`);
72
+ continue;
73
+ }
74
+
75
+ fs.rmSync(targetPath, { recursive: true, force: true });
76
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
77
+ }
78
+ };
79
+
80
+ const packageName = process.env.NPM_PACKAGE_NAME || readPackageName();
81
+ const requestedVersion = process.env.NPM_SYNC_VERSION?.trim();
82
+ const distTag = process.env.NPM_SYNC_DIST_TAG?.trim() || 'latest';
83
+ const preserveList = parsePreserveList(process.env.NPM_SYNC_PRESERVE_PATHS);
84
+ const dryRun = process.env.NPM_SYNC_DRY_RUN === '1';
85
+ const packageSpec = requestedVersion ? `${packageName}@${requestedVersion}` : `${packageName}@${distTag}`;
86
+
87
+ try {
88
+ console.log(`Packing ${packageSpec} into ${targetDir}`);
89
+ const packedTarball = execFileSync('npm', ['pack', packageSpec, '--silent'], {
90
+ cwd: tempRoot,
91
+ encoding: 'utf8',
92
+ }).trim().split('\n').pop();
93
+
94
+ if (!packedTarball) {
95
+ throw new Error(`Failed to pack ${packageSpec}`);
96
+ }
97
+
98
+ execFileSync('tar', ['-xzf', packedTarball], { cwd: tempRoot });
99
+
100
+ const sourceRoot = path.join(tempRoot, 'package');
101
+ if (!fs.existsSync(sourceRoot)) {
102
+ throw new Error('Packed npm tarball did not contain a package/ directory.');
103
+ }
104
+
105
+ removeUnsyncedEntries(sourceRoot, preserveList, dryRun);
106
+ copyPackageEntries(sourceRoot, preserveList, dryRun);
107
+
108
+ const syncedPackageJson = JSON.parse(
109
+ fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8')
110
+ );
111
+
112
+ console.log(`Synced ${packageName} version ${syncedPackageJson.version} into ${targetDir}`);
113
+ } finally {
114
+ fs.rmSync(tempRoot, { recursive: true, force: true });
115
+ }
package/tsconfig.json CHANGED
@@ -13,7 +13,22 @@
13
13
  ],
14
14
  "esModuleInterop": true,
15
15
  },
16
+ "include": [
17
+ "@types.d.ts",
18
+ "globals.ts",
19
+ "utils.ts",
20
+ "src/**/*.ts"
21
+ ],
16
22
  "exclude": [
17
- "node_modules"
23
+ "node_modules",
24
+ "dist",
25
+ "coverage",
26
+ "vendor",
27
+ "**/*.test.ts",
28
+ "index.ts",
29
+ "fetch-page-assets.test.ts",
30
+ "index.test.ts",
31
+ "coverage-demo.test.ts",
32
+ "snapapi-screenshot.test.ts"
18
33
  ]
19
- }
34
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Diogo Angelim
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.
@@ -0,0 +1,117 @@
1
+ # fetch-page-assets
2
+
3
+ Download page assets, rewrite the HTML to point at the fetched assets, and optionally upload those downloads directly to Cloudflare R2 instead of writing them to disk.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install fetch-page-assets
9
+ ```
10
+
11
+ ## Environment
12
+
13
+ Keep real secrets in `.env` and never commit them.
14
+
15
+ ```bash
16
+ cp .env.example .env
17
+ ```
18
+
19
+ Required for R2 uploads:
20
+
21
+ - `CLOUDFLARE_R2_ACCOUNT_ID`
22
+ - `CLOUDFLARE_R2_BUCKET`
23
+ - `CLOUDFLARE_R2_ACCESS_KEY_ID`
24
+ - `CLOUDFLARE_R2_SECRET_ACCESS_KEY`
25
+ - `CLOUDFLARE_R2_PUBLIC_BASE_URL`
26
+
27
+ Optional:
28
+
29
+ - `CLOUDFLARE_API_TOKEN`
30
+ - `CLOUDFLARE_R2_ENDPOINT`
31
+
32
+ ## Getting and rotating Cloudflare credentials
33
+
34
+ 1. Open the Cloudflare dashboard.
35
+ 2. Create or rotate the R2 access keys for the bucket you want to use.
36
+ 3. Update `.env` with the new key values.
37
+ 4. If you use a Cloudflare API token for verification or other account workflows, rotate it in the API Tokens section and update `.env`.
38
+ 5. Restart the service after updating `.env`.
39
+ 6. Revoke the old token or key after the new one is confirmed working.
40
+
41
+ To verify a token without exposing it in source code:
42
+
43
+ ```bash
44
+ curl "https://api.cloudflare.com/client/v4/user/tokens/verify" \
45
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ### Legacy local mode
51
+
52
+ ```js
53
+ import extractAssets from 'fetch-page-assets';
54
+
55
+ const html = await extractAssets('<img src="/logo.png" />', {
56
+ source: 'https://example.com',
57
+ basePath: process.cwd(),
58
+ saveFile: true
59
+ });
60
+ ```
61
+
62
+ ### R2 upload mode
63
+
64
+ ```js
65
+ import extractAssets from 'fetch-page-assets';
66
+
67
+ const result = await extractAssets('<img src="/logo.png" />', {
68
+ source: 'https://example.com',
69
+ uploadToR2: true,
70
+ returnDetails: true,
71
+ jobId: 'conv_123',
72
+ r2Prefix: 'generated/conv_123/assets'
73
+ });
74
+
75
+ console.log(result);
76
+ ```
77
+
78
+ Example response:
79
+
80
+ ```json
81
+ {
82
+ "html": "<img src=\"https://storage.example.com/generated/conv_123/assets/logo.png\">",
83
+ "assets": [
84
+ {
85
+ "id": "file_1",
86
+ "name": "logo.png",
87
+ "type": "image/png",
88
+ "size": 48211,
89
+ "path": "/generated/conv_123/assets/logo.png",
90
+ "url": "https://storage.example.com/generated/conv_123/assets/logo.png",
91
+ "kind": "asset"
92
+ }
93
+ ]
94
+ }
95
+ ```
96
+
97
+ ## Options
98
+
99
+ | Option | Description | Type | Default |
100
+ | --- | --- | --- | --- |
101
+ | `source` | Base URL used to resolve relative asset paths. | `string` | `''` |
102
+ | `basePath` | Local base path used in legacy mode. | `string` | current directory |
103
+ | `saveFile` | Writes downloaded assets to disk in legacy mode. | `boolean` | `true` |
104
+ | `uploadToR2` | Uploads resolved assets to Cloudflare R2. | `boolean` | `false` |
105
+ | `returnDetails` | Returns `{ html, assets }` metadata instead of only the rewritten HTML string. | `boolean` | `false` |
106
+ | `jobId` | Stable conversion identifier used to build remote paths. | `string` | `conv_local` |
107
+ | `r2Prefix` | Remote storage prefix for uploaded assets. | `string` | derived from `jobId` |
108
+ | `concurrency` | Maximum number of simultaneous downloads. | `number` | `8` |
109
+ | `maxRetryAttempts` | Maximum attempts per asset. | `number` | `3` |
110
+ | `retryDelay` | Delay between attempts in milliseconds. | `number` | `1000` |
111
+ | `verbose` | Enables console logging. | `boolean` | `true` |
112
+
113
+ ## Notes
114
+
115
+ - `returnDetails: true` is the recommended mode when using R2 because it gives you the uploaded asset metadata.
116
+ - Keep all Cloudflare credentials in `.env`.
117
+ - Do not hardcode tokens or access keys in code, documentation, tests, or shell history.