@adobe/aem-cli 16.15.12 → 16.16.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/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ # [16.16.0](https://github.com/adobe/helix-cli/compare/v16.15.13...v16.16.0) (2025-11-13)
2
+
3
+
4
+ ### Features
5
+
6
+ * add .plain.html support for HTML folder serving ([#2631](https://github.com/adobe/helix-cli/issues/2631)) ([2186d4c](https://github.com/adobe/helix-cli/commit/2186d4c330f97db86849b9388e119b7d599c9fc1))
7
+
8
+ ## [16.15.13](https://github.com/adobe/helix-cli/compare/v16.15.12...v16.15.13) (2025-11-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **deps:** update dependency ini to v6 ([7c37973](https://github.com/adobe/helix-cli/commit/7c37973c45d7d6956bbc3200d46c2015d7e483fe))
14
+ * **deps:** update external fixes ([#2630](https://github.com/adobe/helix-cli/issues/2630)) ([8671a13](https://github.com/adobe/helix-cli/commit/8671a131f4cb1e3d29e318cfb7c3b3326d2cea77))
15
+
1
16
  ## [16.15.12](https://github.com/adobe/helix-cli/compare/v16.15.11...v16.15.12) (2025-10-30)
2
17
 
3
18
 
package/README.md CHANGED
@@ -74,6 +74,26 @@ This feature is especially helpful when:
74
74
  - Monitoring client-side behavior during development
75
75
  - Working with AI coding assistants that need visibility into both server and client logs
76
76
 
77
+ ### html folder for content preview
78
+
79
+ The `--html-folder` option enables serving HTML files without extensions, useful for previewing content changes when you don't have access to the authoring system.
80
+
81
+ ```
82
+ $ aem up --html-folder content
83
+ ```
84
+
85
+ This enables two features:
86
+
87
+ 1. **Extension-less URLs**: Access `/content/page` to serve `content/page.html`
88
+ 2. **Plain HTML with metadata**: Create `content/page.plain.html` files that are automatically wrapped with proper HTML structure and metadata processing
89
+
90
+ #### Plain HTML files (.plain.html)
91
+
92
+ Plain HTML files contain only the main content and an optional metadata block. The CLI automatically:
93
+ - Wraps content in `<html><head><body><header><main><footer>` structure
94
+ - Merges in `head.html` content
95
+ - The metadata block is removed from the rendered content and converted to meta tags in the `<head>`.
96
+
77
97
  ### setting up a self-signed cert for using https
78
98
 
79
99
  1. create the certificate
@@ -161,6 +181,7 @@ If present, `ALL_PROXY` is used as fallback if there is no other match.
161
181
  | `--livereload` | `AEM_LIVERELOAD` | `true` | Enable automatic reloading of modified sources in browser. |
162
182
  | `--no-livereload` | `AEM_NO_LIVERELOAD` | `false` | Disable live-reload. |
163
183
  | `--forward-browser-logs` | `AEM_FORWARD_BROWSER_LOGS` | `false` | Forward browser console logs to terminal. |
184
+ | `--html-folder` | `AEM_HTML_FOLDER` | undefined | Serve HTML files from folder without extensions. Supports .html and .plain.html files. |
164
185
  | `--open` | `AEM_OPEN` | `/` | Open a browser window at specified path after server start. |
165
186
  | `--no-open` | `AEM_NO_OPEN` | `false` | Disable automatic opening of browser window. |
166
187
  | `--tls-key` | `AEM_TLS_KEY` | undefined | Path to .key file (for enabling TLS) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/aem-cli",
3
- "version": "16.15.12",
3
+ "version": "16.16.0",
4
4
  "description": "AEM CLI",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -64,8 +64,8 @@
64
64
  "http-proxy-agent": "7.0.2",
65
65
  "https-proxy-agent": "7.0.6",
66
66
  "ignore": "7.0.5",
67
- "ini": "5.0.0",
68
- "isomorphic-git": "1.34.0",
67
+ "ini": "6.0.0",
68
+ "isomorphic-git": "1.34.1",
69
69
  "jose": "6.1.0",
70
70
  "livereload-js": "4.0.2",
71
71
  "node-fetch": "3.3.2",
@@ -81,8 +81,8 @@
81
81
  "yargs": "18.0.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@adobe/eslint-config-helix": "3.0.12",
85
- "@eslint/config-helpers": "0.4.0",
84
+ "@adobe/eslint-config-helix": "3.0.13",
85
+ "@eslint/config-helpers": "0.4.1",
86
86
  "@semantic-release/changelog": "6.0.3",
87
87
  "@semantic-release/git": "10.0.1",
88
88
  "c8": "10.1.3",
@@ -90,11 +90,11 @@
90
90
  "esmock": "2.7.3",
91
91
  "husky": "9.1.7",
92
92
  "junit-report-builder": "5.1.1",
93
- "lint-staged": "16.2.4",
93
+ "lint-staged": "16.2.6",
94
94
  "mocha": "11.7.4",
95
95
  "mocha-multi-reporters": "1.5.1",
96
96
  "nock": "13.5.6",
97
- "semantic-release": "25.0.0",
97
+ "semantic-release": "25.0.1",
98
98
  "semantic-release-discord-bot": "^1.1.0",
99
99
  "sinon": "21.0.0"
100
100
  },
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@adobe/eslint-config-helix": "^3.0.9",
16
- "@eslint/config-helpers": "0.4.0",
16
+ "@eslint/config-helpers": "0.4.1",
17
17
  "@esm-bundle/chai": "4.3.4-fix.0",
18
18
  "@web/test-runner": "0.20.2",
19
19
  "@web/test-runner-junit-reporter": "0.8.0",
@@ -22,13 +22,13 @@
22
22
  }
23
23
  },
24
24
  "node_modules/@adobe/eslint-config-helix": {
25
- "version": "3.0.12",
26
- "resolved": "https://registry.npmjs.org/@adobe/eslint-config-helix/-/eslint-config-helix-3.0.12.tgz",
27
- "integrity": "sha512-m2Lsrf7lIhzw39JppqSURNgch8njdclRyN93UU+85HmHZNfJ3L60tfCnFkc/ANU82FXB3BAht2xxSP4zBCy8qw==",
25
+ "version": "3.0.13",
26
+ "resolved": "https://registry.npmjs.org/@adobe/eslint-config-helix/-/eslint-config-helix-3.0.13.tgz",
27
+ "integrity": "sha512-p5FJfGhQVaMGAfwweyh1R46MJi7Pi0XNaZLKH4X7lBA8mgT7oTV9l48H7ifWB+R5rRl2XpsvsZCM2q2HfDRDoA==",
28
28
  "dev": true,
29
29
  "license": "Apache-2.0",
30
30
  "dependencies": {
31
- "@eslint/config-helpers": "0.4.0",
31
+ "@eslint/config-helpers": "0.4.1",
32
32
  "eslint-plugin-import": "2.32.0",
33
33
  "globals": "16.4.0"
34
34
  },
@@ -70,7 +70,6 @@
70
70
  "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
71
71
  "dev": true,
72
72
  "license": "MIT",
73
- "peer": true,
74
73
  "dependencies": {
75
74
  "eslint-visitor-keys": "^3.4.3"
76
75
  },
@@ -90,7 +89,6 @@
90
89
  "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
91
90
  "dev": true,
92
91
  "license": "Apache-2.0",
93
- "peer": true,
94
92
  "engines": {
95
93
  "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
96
94
  },
@@ -104,7 +102,6 @@
104
102
  "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
105
103
  "dev": true,
106
104
  "license": "MIT",
107
- "peer": true,
108
105
  "engines": {
109
106
  "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
110
107
  }
@@ -115,7 +112,6 @@
115
112
  "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
116
113
  "dev": true,
117
114
  "license": "Apache-2.0",
118
- "peer": true,
119
115
  "dependencies": {
120
116
  "@eslint/object-schema": "^2.1.6",
121
117
  "debug": "^4.3.1",
@@ -126,9 +122,9 @@
126
122
  }
127
123
  },
128
124
  "node_modules/@eslint/config-helpers": {
129
- "version": "0.4.0",
130
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
131
- "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
125
+ "version": "0.4.1",
126
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
127
+ "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
132
128
  "dev": true,
133
129
  "license": "Apache-2.0",
134
130
  "dependencies": {
@@ -157,7 +153,6 @@
157
153
  "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
158
154
  "dev": true,
159
155
  "license": "MIT",
160
- "peer": true,
161
156
  "dependencies": {
162
157
  "ajv": "^6.12.4",
163
158
  "debug": "^4.3.2",
@@ -182,7 +177,6 @@
182
177
  "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
183
178
  "dev": true,
184
179
  "license": "MIT",
185
- "peer": true,
186
180
  "engines": {
187
181
  "node": ">=18"
188
182
  },
@@ -196,7 +190,6 @@
196
190
  "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
197
191
  "dev": true,
198
192
  "license": "MIT",
199
- "peer": true,
200
193
  "engines": {
201
194
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
202
195
  },
@@ -210,7 +203,6 @@
210
203
  "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
211
204
  "dev": true,
212
205
  "license": "Apache-2.0",
213
- "peer": true,
214
206
  "engines": {
215
207
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
216
208
  }
@@ -221,7 +213,6 @@
221
213
  "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
222
214
  "dev": true,
223
215
  "license": "Apache-2.0",
224
- "peer": true,
225
216
  "dependencies": {
226
217
  "@eslint/core": "^0.16.0",
227
218
  "levn": "^0.4.1"
@@ -253,7 +244,6 @@
253
244
  "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
254
245
  "dev": true,
255
246
  "license": "Apache-2.0",
256
- "peer": true,
257
247
  "engines": {
258
248
  "node": ">=18.18.0"
259
249
  }
@@ -264,7 +254,6 @@
264
254
  "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
265
255
  "dev": true,
266
256
  "license": "Apache-2.0",
267
- "peer": true,
268
257
  "dependencies": {
269
258
  "@humanfs/core": "^0.19.1",
270
259
  "@humanwhocodes/retry": "^0.4.0"
@@ -279,7 +268,6 @@
279
268
  "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
280
269
  "dev": true,
281
270
  "license": "Apache-2.0",
282
- "peer": true,
283
271
  "engines": {
284
272
  "node": ">=12.22"
285
273
  },
@@ -294,7 +282,6 @@
294
282
  "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
295
283
  "dev": true,
296
284
  "license": "Apache-2.0",
297
- "peer": true,
298
285
  "engines": {
299
286
  "node": ">=18.18"
300
287
  },
@@ -1414,7 +1401,6 @@
1414
1401
  "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
1415
1402
  "dev": true,
1416
1403
  "license": "MIT",
1417
- "peer": true,
1418
1404
  "peerDependencies": {
1419
1405
  "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1420
1406
  }
@@ -1435,7 +1421,6 @@
1435
1421
  "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
1436
1422
  "dev": true,
1437
1423
  "license": "MIT",
1438
- "peer": true,
1439
1424
  "dependencies": {
1440
1425
  "fast-deep-equal": "^3.1.1",
1441
1426
  "fast-json-stable-stringify": "^2.0.0",
@@ -1494,8 +1479,7 @@
1494
1479
  "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
1495
1480
  "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
1496
1481
  "dev": true,
1497
- "license": "Python-2.0",
1498
- "peer": true
1482
+ "license": "Python-2.0"
1499
1483
  },
1500
1484
  "node_modules/array-back": {
1501
1485
  "version": "3.1.0",
@@ -1940,7 +1924,6 @@
1940
1924
  "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
1941
1925
  "dev": true,
1942
1926
  "license": "MIT",
1943
- "peer": true,
1944
1927
  "engines": {
1945
1928
  "node": ">=6"
1946
1929
  }
@@ -2363,8 +2346,7 @@
2363
2346
  "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
2364
2347
  "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
2365
2348
  "dev": true,
2366
- "license": "MIT",
2367
- "peer": true
2349
+ "license": "MIT"
2368
2350
  },
2369
2351
  "node_modules/deepmerge": {
2370
2352
  "version": "4.3.1",
@@ -2493,7 +2475,8 @@
2493
2475
  "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
2494
2476
  "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
2495
2477
  "dev": true,
2496
- "license": "BSD-3-Clause"
2478
+ "license": "BSD-3-Clause",
2479
+ "peer": true
2497
2480
  },
2498
2481
  "node_modules/diff": {
2499
2482
  "version": "5.2.0",
@@ -2968,7 +2951,6 @@
2968
2951
  "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
2969
2952
  "dev": true,
2970
2953
  "license": "BSD-2-Clause",
2971
- "peer": true,
2972
2954
  "dependencies": {
2973
2955
  "esrecurse": "^4.3.0",
2974
2956
  "estraverse": "^5.2.0"
@@ -2986,7 +2968,6 @@
2986
2968
  "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
2987
2969
  "dev": true,
2988
2970
  "license": "Apache-2.0",
2989
- "peer": true,
2990
2971
  "engines": {
2991
2972
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2992
2973
  },
@@ -3000,7 +2981,6 @@
3000
2981
  "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
3001
2982
  "dev": true,
3002
2983
  "license": "BSD-2-Clause",
3003
- "peer": true,
3004
2984
  "dependencies": {
3005
2985
  "acorn": "^8.15.0",
3006
2986
  "acorn-jsx": "^5.3.2",
@@ -3033,7 +3013,6 @@
3033
3013
  "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
3034
3014
  "dev": true,
3035
3015
  "license": "BSD-3-Clause",
3036
- "peer": true,
3037
3016
  "dependencies": {
3038
3017
  "estraverse": "^5.1.0"
3039
3018
  },
@@ -3047,7 +3026,6 @@
3047
3026
  "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
3048
3027
  "dev": true,
3049
3028
  "license": "BSD-2-Clause",
3050
- "peer": true,
3051
3029
  "dependencies": {
3052
3030
  "estraverse": "^5.2.0"
3053
3031
  },
@@ -3168,8 +3146,7 @@
3168
3146
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
3169
3147
  "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
3170
3148
  "dev": true,
3171
- "license": "MIT",
3172
- "peer": true
3149
+ "license": "MIT"
3173
3150
  },
3174
3151
  "node_modules/fast-fifo": {
3175
3152
  "version": "1.3.2",
@@ -3213,16 +3190,14 @@
3213
3190
  "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
3214
3191
  "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
3215
3192
  "dev": true,
3216
- "license": "MIT",
3217
- "peer": true
3193
+ "license": "MIT"
3218
3194
  },
3219
3195
  "node_modules/fast-levenshtein": {
3220
3196
  "version": "2.0.6",
3221
3197
  "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
3222
3198
  "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
3223
3199
  "dev": true,
3224
- "license": "MIT",
3225
- "peer": true
3200
+ "license": "MIT"
3226
3201
  },
3227
3202
  "node_modules/fastq": {
3228
3203
  "version": "1.19.1",
@@ -3250,7 +3225,6 @@
3250
3225
  "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
3251
3226
  "dev": true,
3252
3227
  "license": "MIT",
3253
- "peer": true,
3254
3228
  "dependencies": {
3255
3229
  "flat-cache": "^4.0.0"
3256
3230
  },
@@ -3290,7 +3264,6 @@
3290
3264
  "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
3291
3265
  "dev": true,
3292
3266
  "license": "MIT",
3293
- "peer": true,
3294
3267
  "dependencies": {
3295
3268
  "locate-path": "^6.0.0",
3296
3269
  "path-exists": "^4.0.0"
@@ -3308,7 +3281,6 @@
3308
3281
  "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
3309
3282
  "dev": true,
3310
3283
  "license": "MIT",
3311
- "peer": true,
3312
3284
  "dependencies": {
3313
3285
  "flatted": "^3.2.9",
3314
3286
  "keyv": "^4.5.4"
@@ -3322,8 +3294,7 @@
3322
3294
  "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
3323
3295
  "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
3324
3296
  "dev": true,
3325
- "license": "ISC",
3326
- "peer": true
3297
+ "license": "ISC"
3327
3298
  },
3328
3299
  "node_modules/for-each": {
3329
3300
  "version": "0.3.5",
@@ -3518,7 +3489,6 @@
3518
3489
  "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
3519
3490
  "dev": true,
3520
3491
  "license": "ISC",
3521
- "peer": true,
3522
3492
  "dependencies": {
3523
3493
  "is-glob": "^4.0.3"
3524
3494
  },
@@ -3799,7 +3769,6 @@
3799
3769
  "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
3800
3770
  "dev": true,
3801
3771
  "license": "MIT",
3802
- "peer": true,
3803
3772
  "dependencies": {
3804
3773
  "parent-module": "^1.0.0",
3805
3774
  "resolve-from": "^4.0.0"
@@ -3817,7 +3786,6 @@
3817
3786
  "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
3818
3787
  "dev": true,
3819
3788
  "license": "MIT",
3820
- "peer": true,
3821
3789
  "engines": {
3822
3790
  "node": ">=0.8.19"
3823
3791
  }
@@ -4446,7 +4414,6 @@
4446
4414
  "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
4447
4415
  "dev": true,
4448
4416
  "license": "MIT",
4449
- "peer": true,
4450
4417
  "dependencies": {
4451
4418
  "argparse": "^2.0.1"
4452
4419
  },
@@ -4459,24 +4426,21 @@
4459
4426
  "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
4460
4427
  "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
4461
4428
  "dev": true,
4462
- "license": "MIT",
4463
- "peer": true
4429
+ "license": "MIT"
4464
4430
  },
4465
4431
  "node_modules/json-schema-traverse": {
4466
4432
  "version": "0.4.1",
4467
4433
  "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
4468
4434
  "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
4469
4435
  "dev": true,
4470
- "license": "MIT",
4471
- "peer": true
4436
+ "license": "MIT"
4472
4437
  },
4473
4438
  "node_modules/json-stable-stringify-without-jsonify": {
4474
4439
  "version": "1.0.1",
4475
4440
  "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
4476
4441
  "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
4477
4442
  "dev": true,
4478
- "license": "MIT",
4479
- "peer": true
4443
+ "license": "MIT"
4480
4444
  },
4481
4445
  "node_modules/json5": {
4482
4446
  "version": "1.0.2",
@@ -4510,7 +4474,6 @@
4510
4474
  "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
4511
4475
  "dev": true,
4512
4476
  "license": "MIT",
4513
- "peer": true,
4514
4477
  "dependencies": {
4515
4478
  "json-buffer": "3.0.1"
4516
4479
  }
@@ -4626,7 +4589,6 @@
4626
4589
  "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
4627
4590
  "dev": true,
4628
4591
  "license": "MIT",
4629
- "peer": true,
4630
4592
  "dependencies": {
4631
4593
  "prelude-ls": "^1.2.1",
4632
4594
  "type-check": "~0.4.0"
@@ -4675,7 +4637,6 @@
4675
4637
  "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
4676
4638
  "dev": true,
4677
4639
  "license": "MIT",
4678
- "peer": true,
4679
4640
  "dependencies": {
4680
4641
  "p-locate": "^5.0.0"
4681
4642
  },
@@ -4698,8 +4659,7 @@
4698
4659
  "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
4699
4660
  "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
4700
4661
  "dev": true,
4701
- "license": "MIT",
4702
- "peer": true
4662
+ "license": "MIT"
4703
4663
  },
4704
4664
  "node_modules/log-update": {
4705
4665
  "version": "4.0.0",
@@ -4931,8 +4891,7 @@
4931
4891
  "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
4932
4892
  "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
4933
4893
  "dev": true,
4934
- "license": "MIT",
4935
- "peer": true
4894
+ "license": "MIT"
4936
4895
  },
4937
4896
  "node_modules/negotiator": {
4938
4897
  "version": "0.6.3",
@@ -5133,7 +5092,6 @@
5133
5092
  "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
5134
5093
  "dev": true,
5135
5094
  "license": "MIT",
5136
- "peer": true,
5137
5095
  "dependencies": {
5138
5096
  "deep-is": "^0.1.3",
5139
5097
  "fast-levenshtein": "^2.0.6",
@@ -5196,7 +5154,6 @@
5196
5154
  "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
5197
5155
  "dev": true,
5198
5156
  "license": "MIT",
5199
- "peer": true,
5200
5157
  "dependencies": {
5201
5158
  "yocto-queue": "^0.1.0"
5202
5159
  },
@@ -5213,7 +5170,6 @@
5213
5170
  "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
5214
5171
  "dev": true,
5215
5172
  "license": "MIT",
5216
- "peer": true,
5217
5173
  "dependencies": {
5218
5174
  "p-limit": "^3.0.2"
5219
5175
  },
@@ -5277,7 +5233,6 @@
5277
5233
  "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
5278
5234
  "dev": true,
5279
5235
  "license": "MIT",
5280
- "peer": true,
5281
5236
  "dependencies": {
5282
5237
  "callsites": "^3.0.0"
5283
5238
  },
@@ -5308,7 +5263,6 @@
5308
5263
  "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
5309
5264
  "dev": true,
5310
5265
  "license": "MIT",
5311
- "peer": true,
5312
5266
  "engines": {
5313
5267
  "node": ">=8"
5314
5268
  }
@@ -5439,7 +5393,6 @@
5439
5393
  "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
5440
5394
  "dev": true,
5441
5395
  "license": "MIT",
5442
- "peer": true,
5443
5396
  "engines": {
5444
5397
  "node": ">= 0.8.0"
5445
5398
  }
@@ -5728,7 +5681,6 @@
5728
5681
  "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
5729
5682
  "dev": true,
5730
5683
  "license": "MIT",
5731
- "peer": true,
5732
5684
  "engines": {
5733
5685
  "node": ">=4"
5734
5686
  }
@@ -5818,6 +5770,7 @@
5818
5770
  "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
5819
5771
  "dev": true,
5820
5772
  "license": "MIT",
5773
+ "peer": true,
5821
5774
  "dependencies": {
5822
5775
  "@types/estree": "1.0.8"
5823
5776
  },
@@ -6361,7 +6314,6 @@
6361
6314
  "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
6362
6315
  "dev": true,
6363
6316
  "license": "MIT",
6364
- "peer": true,
6365
6317
  "engines": {
6366
6318
  "node": ">=8"
6367
6319
  },
@@ -6528,7 +6480,6 @@
6528
6480
  "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
6529
6481
  "dev": true,
6530
6482
  "license": "MIT",
6531
- "peer": true,
6532
6483
  "dependencies": {
6533
6484
  "prelude-ls": "^1.2.1"
6534
6485
  },
@@ -6700,7 +6651,6 @@
6700
6651
  "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
6701
6652
  "dev": true,
6702
6653
  "license": "BSD-2-Clause",
6703
- "peer": true,
6704
6654
  "dependencies": {
6705
6655
  "punycode": "^2.1.0"
6706
6656
  }
@@ -6872,7 +6822,6 @@
6872
6822
  "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
6873
6823
  "dev": true,
6874
6824
  "license": "MIT",
6875
- "peer": true,
6876
6825
  "engines": {
6877
6826
  "node": ">=0.10.0"
6878
6827
  }
@@ -7004,7 +6953,6 @@
7004
6953
  "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
7005
6954
  "dev": true,
7006
6955
  "license": "MIT",
7007
- "peer": true,
7008
6956
  "engines": {
7009
6957
  "node": ">=10"
7010
6958
  },
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@adobe/eslint-config-helix": "^3.0.9",
16
- "@eslint/config-helpers": "0.4.0",
16
+ "@eslint/config-helpers": "0.4.1",
17
17
  "@esm-bundle/chai": "4.3.4-fix.0",
18
18
  "@web/test-runner": "0.20.2",
19
19
  "@web/test-runner-playwright": "0.11.1",
@@ -154,6 +154,90 @@ export class HelixServer extends BaseServer {
154
154
  .send('');
155
155
  }
156
156
 
157
+ /**
158
+ * Resolves which HTML file to serve from the HTML folder
159
+ * @param {string} relativePath path relative to HTML folder
160
+ * @returns {Promise<{file: string, isPlain: boolean}|null>} resolved file info or null
161
+ */
162
+ async resolveHtmlFolderFile(relativePath) {
163
+ // Security check: prevent path traversal with /../ anywhere in the path
164
+ if (relativePath.includes('/../')) {
165
+ return null;
166
+ }
167
+
168
+ // Don't process if it already has an extension
169
+ if (relativePath.includes('.')) {
170
+ return null;
171
+ }
172
+
173
+ // Try .html first
174
+ const htmlFile = path.resolve(
175
+ this._project.directory,
176
+ this._htmlFolder,
177
+ `${relativePath}.html`,
178
+ );
179
+
180
+ if (!utils.validatePathSecurity(htmlFile, this._project.directory)) {
181
+ return null;
182
+ }
183
+
184
+ try {
185
+ const stats = await lstat(htmlFile);
186
+ if (stats.isFile()) {
187
+ return { file: htmlFile, isPlain: false };
188
+ }
189
+ } catch (e) {
190
+ // .html not found, try .plain.html
191
+ }
192
+
193
+ // Try .plain.html
194
+ const plainHtmlFile = path.resolve(
195
+ this._project.directory,
196
+ this._htmlFolder,
197
+ `${relativePath}.plain.html`,
198
+ );
199
+
200
+ if (!utils.validatePathSecurity(plainHtmlFile, this._project.directory)) {
201
+ return null;
202
+ }
203
+
204
+ try {
205
+ const stats = await lstat(plainHtmlFile);
206
+ if (stats.isFile()) {
207
+ return { file: plainHtmlFile, isPlain: true };
208
+ }
209
+ } catch (e) {
210
+ // Neither exists
211
+ }
212
+
213
+ return null;
214
+ }
215
+
216
+ /**
217
+ * Transforms .plain.html content into complete HTML with head.html
218
+ * @param {string} plainHtmlFile path to .plain.html file
219
+ * @returns {Promise<string>} complete HTML document
220
+ */
221
+ async transformPlainHtml(plainHtmlFile) {
222
+ const plainContent = await readFile(plainHtmlFile, 'utf-8');
223
+
224
+ // Extract metadata and clean content
225
+ const { content, metadata } = utils.extractMetadataBlock(plainContent);
226
+
227
+ // Extract default metadata values from content
228
+ const defaults = utils.extractDefaultMetadata(content);
229
+
230
+ // Get head HTML using existing HeadHtmlSupport
231
+ await this._project.headHtml.update();
232
+ const headHtml = this._project.headHtml.localHtml || '';
233
+
234
+ // Generate meta tags from metadata with defaults
235
+ const metaTags = utils.generateMetaTags(metadata, defaults);
236
+
237
+ // Wrap in complete HTML structure
238
+ return utils.wrapPlainHtml(content, headHtml, metaTags);
239
+ }
240
+
157
241
  /**
158
242
  * HTML Folder handler - serves HTML files without extensions
159
243
  * @param {Express.Request} req request
@@ -177,41 +261,12 @@ export class HelixServer extends BaseServer {
177
261
  // Extract the path within the HTML folder
178
262
  const relativePath = pathname.slice(folderPrefix.length);
179
263
 
180
- // Security check: prevent path traversal with /../ anywhere in the path
181
- if (relativePath.includes('/../')) {
182
- return next();
183
- }
184
-
185
- // Don't process if it already has an extension
186
- if (relativePath.includes('.')) {
264
+ // Resolve which file to serve (.html or .plain.html)
265
+ const resolvedFile = await this.resolveHtmlFolderFile(relativePath);
266
+ if (!resolvedFile) {
187
267
  return next();
188
268
  }
189
269
 
190
- // Build the HTML file path - only support .html extension
191
- const htmlFile = path.resolve(this._project.directory, this._htmlFolder, `${relativePath}.html`);
192
-
193
- // Security check: ensure the file is within the project directory
194
- // Use resolve to normalize both paths before comparing
195
- const resolvedProjectDir = path.resolve(this._project.directory);
196
- const relPath = path.relative(resolvedProjectDir, htmlFile);
197
-
198
- // Only check for path traversal - remove the incorrect isAbsolute check
199
- if (relPath.startsWith('..')) {
200
- return next();
201
- }
202
-
203
- // Check if the HTML file exists and is a file
204
- try {
205
- const stats = await lstat(htmlFile);
206
- if (!stats.isFile()) {
207
- return next();
208
- }
209
- } catch (e) {
210
- // File doesn't exist, continue to next handler
211
- return next();
212
- }
213
-
214
- const sendFile = promisify(res.sendFile).bind(res);
215
270
  const { log } = this;
216
271
  const liveReload = this._liveReload;
217
272
 
@@ -220,24 +275,33 @@ export class HelixServer extends BaseServer {
220
275
  liveReload.startRequest(req.id, req.url);
221
276
  }
222
277
 
223
- // Serve the file
224
- // Use the root option to specify the base directory for sendFile
225
- // This prevents issues with worktrees in subdirectories
226
- await sendFile(path.basename(htmlFile), {
227
- root: path.dirname(htmlFile),
228
- dotfiles: 'deny',
229
- headers: {
230
- 'access-control-allow-origin': '*',
231
- 'content-type': 'text/html; charset=utf-8',
232
- },
278
+ // Load content (handle .plain.html transformation)
279
+ let htmlContent;
280
+ if (resolvedFile.isPlain) {
281
+ htmlContent = await this.transformPlainHtml(resolvedFile.file);
282
+ } else {
283
+ htmlContent = await readFile(resolvedFile.file, 'utf-8');
284
+ }
285
+
286
+ // Inject live reload script
287
+ if (liveReload) {
288
+ htmlContent = utils.injectLiveReloadScript(htmlContent, this);
289
+ }
290
+
291
+ // Send response
292
+ res.set({
293
+ 'content-type': 'text/html; charset=utf-8',
294
+ 'access-control-allow-origin': '*',
233
295
  });
296
+ res.send(htmlContent);
234
297
 
298
+ // Register with live reload
235
299
  if (liveReload) {
236
- liveReload.registerFile(req.id, htmlFile);
300
+ liveReload.registerFile(req.id, resolvedFile.file);
237
301
  liveReload.endRequest(req.id);
238
302
  }
239
303
 
240
- log.debug(`served HTML file ${htmlFile} for ${req.url}`);
304
+ log.debug(`served HTML file ${resolvedFile.file} for ${req.url}`);
241
305
  return undefined;
242
306
  }
243
307
 
@@ -17,6 +17,8 @@ import { PassThrough } from 'stream';
17
17
  import { readFileSync } from 'fs';
18
18
  import { fileURLToPath } from 'url';
19
19
  import cookie from 'cookie';
20
+ import { unified } from 'unified';
21
+ import rehypeParse from 'rehype-parse';
20
22
  import { getFetch } from '../fetch-utils.js';
21
23
 
22
24
  // Load console interceptor script at startup
@@ -525,6 +527,254 @@ window.LiveReloadOptions = {
525
527
  text = text.replaceAll(re, (match, arg, q1, value, q2) => (`${arg}=${q1}${value || '/'}${q2}`));
526
528
  return Buffer.from(text, 'utf-8');
527
529
  },
530
+
531
+ /**
532
+ * Escapes HTML entities for use in attributes
533
+ * @param {string} text text to escape
534
+ * @returns {string} escaped text
535
+ */
536
+ escapeHtml(text) {
537
+ return text
538
+ .replace(/&/g, '&amp;')
539
+ .replace(/</g, '&lt;')
540
+ .replace(/>/g, '&gt;')
541
+ .replace(/"/g, '&quot;')
542
+ .replace(/'/g, '&#039;');
543
+ },
544
+
545
+ /**
546
+ * Extracts text content from HTML using proper HTML parsing
547
+ * @param {string} html HTML content to extract text from
548
+ * @returns {string} plain text content
549
+ */
550
+ extractTextFromHtml(html) {
551
+ // Helper to recursively extract text from AST nodes
552
+ function extractText(node) {
553
+ if (node.type === 'text') {
554
+ return node.value;
555
+ }
556
+ if (node.children) {
557
+ return node.children.map(extractText).join('');
558
+ }
559
+ return '';
560
+ }
561
+
562
+ try {
563
+ const ast = unified()
564
+ .use(rehypeParse, { fragment: true })
565
+ .parse(html);
566
+ return extractText(ast).trim();
567
+ } catch (e) {
568
+ // If parsing fails, return empty string to avoid XSS vulnerabilities
569
+ return '';
570
+ }
571
+ },
572
+
573
+ /**
574
+ * Validates that a file path is within a base directory (security check)
575
+ * @param {string} filePath absolute file path to validate
576
+ * @param {string} baseDirectory base directory path
577
+ * @returns {boolean} true if path is safe, false if it tries to escape base directory
578
+ */
579
+ validatePathSecurity(filePath, baseDirectory) {
580
+ const resolvedBaseDir = path.resolve(baseDirectory);
581
+ const relPath = path.relative(resolvedBaseDir, filePath);
582
+ return !relPath.startsWith('..');
583
+ },
584
+
585
+ /**
586
+ * Extracts metadata block from plain HTML content
587
+ * @param {string} html HTML content with potential metadata block
588
+ * @returns {{content: string, metadata: object}} cleaned content and metadata object
589
+ */
590
+ extractMetadataBlock(html) {
591
+ // The metadata block structure is:
592
+ // <div>
593
+ // <div class="metadata">
594
+ // <div><div>key</div><div>value</div></div>
595
+ // </div>
596
+ // </div>
597
+
598
+ // Find the outer div containing the metadata div
599
+ const outerDivRegex = /<div>\s*<div class="metadata">/;
600
+ const outerMatch = html.match(outerDivRegex);
601
+
602
+ if (!outerMatch) {
603
+ return { content: html, metadata: {} };
604
+ }
605
+
606
+ // Find the matching closing </div></div> by counting nested divs
607
+ // Start after <div><div class="metadata">
608
+ const startIndex = outerMatch.index + outerMatch[0].length;
609
+ let depth = 2; // Already inside two divs
610
+ let endIndex = startIndex;
611
+ let closingDivCount = 0;
612
+
613
+ for (let i = startIndex; i < html.length && closingDivCount < 2; i += 1) {
614
+ if (html.substr(i, 5) === '<div>') {
615
+ depth += 1;
616
+ } else if (html.substr(i, 6) === '</div>') {
617
+ depth -= 1;
618
+ if (depth < 2) {
619
+ closingDivCount += 1;
620
+ if (closingDivCount === 2) {
621
+ endIndex = i + 6; // Include the last </div>
622
+ }
623
+ }
624
+ }
625
+ }
626
+
627
+ if (closingDivCount !== 2) {
628
+ // Malformed HTML, return as-is
629
+ return { content: html, metadata: {} };
630
+ }
631
+
632
+ // Extract the metadata block content and remove entire outer div from HTML
633
+ const fullMetadataBlock = html.substring(outerMatch.index, endIndex);
634
+ const metadataBlock = html.substring(startIndex, endIndex - 12); // -12 for </div></div>
635
+ const cleanedContent = html.replace(fullMetadataBlock, '').trim();
636
+
637
+ // Parse metadata block to extract key-value pairs
638
+ const pairRegex = /<div>\s*<div>([^<]+)<\/div>\s*<div>([\s\S]*?)<\/div>\s*<\/div>/g;
639
+ const metadata = {};
640
+ let pairMatch;
641
+
642
+ // eslint-disable-next-line no-cond-assign
643
+ while ((pairMatch = pairRegex.exec(metadataBlock)) !== null) {
644
+ const key = pairMatch[1].trim();
645
+ let value = pairMatch[2].trim();
646
+
647
+ // Handle <img> tags - extract src
648
+ const imgMatch = value.match(/<img[^>]+src="([^"]+)"/);
649
+ if (imgMatch) {
650
+ [, value] = imgMatch;
651
+ } else {
652
+ // Use proper HTML parsing to extract text content
653
+ value = utils.extractTextFromHtml(value);
654
+ }
655
+
656
+ metadata[key] = value;
657
+ }
658
+
659
+ return { content: cleanedContent, metadata };
660
+ },
661
+
662
+ /**
663
+ * Extracts default metadata values from HTML content
664
+ * @param {string} content HTML content to extract defaults from
665
+ * @returns {object} default metadata values
666
+ */
667
+ extractDefaultMetadata(content) {
668
+ const defaults = {};
669
+
670
+ // Extract title from first H1
671
+ const h1Match = content.match(/<h1[^>]*>(.*?)<\/h1>/i);
672
+ if (h1Match) {
673
+ // Use proper HTML parsing to extract text
674
+ defaults.title = utils.extractTextFromHtml(h1Match[1]);
675
+ }
676
+
677
+ // Extract description from first paragraph with 10+ words
678
+ const pMatch = content.match(/<p[^>]*>(.*?)<\/p>/i);
679
+ if (pMatch) {
680
+ // Use proper HTML parsing to extract text
681
+ const text = utils.extractTextFromHtml(pMatch[1]);
682
+ const wordCount = text.split(/\s+/).length;
683
+ if (wordCount >= 10) {
684
+ defaults.description = text;
685
+ }
686
+ }
687
+
688
+ // Extract image from first img tag
689
+ const imgMatch = content.match(/<img[^>]+src="([^"]+)"/i);
690
+ if (imgMatch) {
691
+ [, defaults.image] = imgMatch;
692
+ }
693
+
694
+ return defaults;
695
+ },
696
+
697
+ /**
698
+ * Generates meta tags from metadata object with AEM.live special property support
699
+ * @param {object} metadata key-value pairs of metadata
700
+ * @param {object} defaults default values extracted from content
701
+ * @returns {string} HTML string of meta tags
702
+ */
703
+ generateMetaTags(metadata, defaults = {}) {
704
+ // Handle title:suffix special property
705
+ let title = metadata.title || defaults.title || '';
706
+ const titleSuffix = metadata['title:suffix'];
707
+ if (titleSuffix && title) {
708
+ title = `${title} ${titleSuffix}`;
709
+ }
710
+
711
+ // Apply defaults for missing values
712
+ const description = metadata.description || defaults.description || '';
713
+ const image = metadata.image || defaults.image || '/default-meta-image.png';
714
+
715
+ // Build final metadata object with computed values
716
+ const finalMetadata = { ...metadata };
717
+ if (title) finalMetadata.title = title;
718
+ if (description) finalMetadata.description = description;
719
+ if (image) finalMetadata.image = image;
720
+
721
+ // Remove special properties that shouldn't become meta tags
722
+ delete finalMetadata['title:suffix'];
723
+
724
+ // Generate meta tags (both standard and OG format)
725
+ const ogFields = ['title', 'description', 'image', 'url'];
726
+ let metaTags = '';
727
+
728
+ // eslint-disable-next-line no-restricted-syntax
729
+ for (const [key, value] of Object.entries(finalMetadata)) {
730
+ // Skip empty values
731
+ if (!value) {
732
+ // eslint-disable-next-line no-continue
733
+ continue;
734
+ }
735
+
736
+ // Handle 'tags' property specially - create article:tag for each
737
+ if (key === 'tags') {
738
+ // Split by comma or newline
739
+ const tagList = value.split(/[,\n]/).map((t) => t.trim()).filter((t) => t);
740
+ // eslint-disable-next-line no-restricted-syntax
741
+ for (const tag of tagList) {
742
+ metaTags += `<meta property="article:tag" content="${utils.escapeHtml(tag)}">`;
743
+ }
744
+ // Don't create standard name= tag for 'tags'
745
+ } else if (key === 'canonical') {
746
+ // Handle canonical specially
747
+ metaTags += `<link rel="canonical" href="${utils.escapeHtml(value)}">`;
748
+ metaTags += `<meta property="og:url" content="${utils.escapeHtml(value)}">`;
749
+ metaTags += `<meta name="twitter:url" content="${utils.escapeHtml(value)}">`;
750
+ // Don't create standard name= tag
751
+ } else {
752
+ // Standard meta tag
753
+ metaTags += `<meta name="${key}" content="${utils.escapeHtml(value)}">`;
754
+
755
+ // Open Graph meta tag for common fields
756
+ if (ogFields.includes(key.toLowerCase())) {
757
+ metaTags += `<meta property="og:${key}" content="${utils.escapeHtml(value)}">`;
758
+ // Add twitter card tags for common fields
759
+ metaTags += `<meta name="twitter:${key}" content="${utils.escapeHtml(value)}">`;
760
+ }
761
+ }
762
+ }
763
+
764
+ return metaTags;
765
+ },
766
+
767
+ /**
768
+ * Wraps plain HTML content in complete HTML structure
769
+ * @param {string} content plain HTML content
770
+ * @param {string} headHtml head.html content
771
+ * @param {string} metaTags generated meta tags
772
+ * @returns {string} complete HTML document
773
+ */
774
+ wrapPlainHtml(content, headHtml, metaTags) {
775
+ const fullHead = headHtml + metaTags;
776
+ return `<html><head>${fullHead}</head><body><header></header><main>${content}</main><footer></footer></body></html>`;
777
+ },
528
778
  };
529
779
 
530
780
  export default Object.freeze(utils);
package/src/up.js CHANGED
@@ -117,7 +117,7 @@ export default function up() {
117
117
  })
118
118
  .option('html-folder', {
119
119
  alias: 'htmlFolder',
120
- describe: 'Serve HTML files from this folder without extensions (e.g., /folder/file serves folder/file.html) use this to preview content changes if you do not have access to the authoring system',
120
+ describe: 'Serve HTML files from this folder without extensions (e.g., /folder/file serves folder/file.html or folder/file.plain.html) use this to preview content changes if you do not have access to the authoring system',
121
121
  type: 'string',
122
122
  })
123
123