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/.env.example +20 -3
- package/.github/workflows/sync-npm.yml +154 -0
- package/fetch-page-assets.test.ts +448 -0
- package/index.d.ts +173 -0
- package/index.js +570 -224
- package/index.test.ts +633 -4
- package/index.ts +168 -63
- package/package.json +25 -24
- package/r2.js +163 -0
- package/readme.md +122 -88
- package/scripts/patch-fetch-page-assets.mjs +13 -0
- package/scripts/sync-from-npm.mjs +115 -0
- package/tsconfig.json +17 -2
- package/vendor/fetch-page-assets/LICENSE.MD +21 -0
- package/vendor/fetch-page-assets/README.md +117 -0
- package/vendor/fetch-page-assets/index.js +362 -0
- package/vendor/fetch-page-assets/package.json +48 -0
- package/.env +0 -1
package/readme.md
CHANGED
|
@@ -1,136 +1,170 @@
|
|
|
1
1
|
# HTML to Gutenberg Converter
|
|
2
2
|
|
|
3
|
-
<!-- [](https://github.com/DiogoAngelim/html-to-gutenberg/actions)
|
|
4
|
-
[](https://coveralls.io/github/DiogoAngelim/html-to-gutenberg?branch=main) -->
|
|
5
3
|
[](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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npm install html-to-gutenberg
|
|
18
|
+
```
|
|
47
19
|
|
|
48
|
-
|
|
20
|
+
## Environment
|
|
49
21
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
Optional:
|
|
57
37
|
|
|
58
|
-
|
|
38
|
+
- `CLOUDFLARE_API_TOKEN`
|
|
39
|
+
- `SNAPAPI_KEY`
|
|
59
40
|
|
|
41
|
+
## Getting and rotating Cloudflare credentials
|
|
60
42
|
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
59
|
+
```js
|
|
60
|
+
import block from 'html-to-gutenberg';
|
|
79
61
|
|
|
80
|
-
|
|
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
|
-
|
|
72
|
+
console.log(result);
|
|
73
|
+
```
|
|
83
74
|
|
|
84
|
-
|
|
75
|
+
Example response:
|
|
85
76
|
|
|
77
|
+
```json
|
|
86
78
|
{
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
## Running tests
|
|
122
162
|
|
|
123
163
|
```bash
|
|
124
|
-
|
|
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.
|