edgepress 2026.10.1 → 2026.10.13

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.
Files changed (485) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -16
  3. package/config.yml +99 -127
  4. package/content/assets/edgepress/brand/avatar.png +0 -0
  5. package/content/assets/edgepress/brand/deploy-esa.svg +1 -0
  6. package/content/assets/edgepress/brand/home-hero-768.webp +0 -0
  7. package/content/assets/edgepress/brand/home-hero.png +0 -0
  8. package/content/assets/edgepress/brand/home-hero.webp +0 -0
  9. package/content/assets/edgepress/brand/mascot.png +0 -0
  10. package/content/assets/edgepress/brand/mascot.webp +0 -0
  11. package/content/assets/edgepress/brand/og-image.png +0 -0
  12. package/content/assets/edgepress/brand/og-image.webp +0 -0
  13. package/content/assets/edgepress/brand/sources/edgepress-avatar-master.png +0 -0
  14. package/content/assets/edgepress/brand/sources/export-manifest.json +181 -0
  15. package/content/assets/edgepress/brand/sources/prompts.json +12 -0
  16. package/content/assets/edgepress/favicon/apple-touch-icon.png +0 -0
  17. package/content/assets/edgepress/favicon/favicon-16.png +0 -0
  18. package/content/assets/edgepress/favicon/favicon-32.png +0 -0
  19. package/content/assets/edgepress/favicon/favicon-48.png +0 -0
  20. package/content/assets/edgepress/favicon/favicon-64.png +0 -0
  21. package/content/assets/edgepress/favicon/favicon.ico +0 -0
  22. package/content/assets/edgepress/icons/LICENSE.txt +43 -0
  23. package/content/assets/edgepress/icons/arrow-right.svg +16 -0
  24. package/content/assets/edgepress/icons/arrow-up-right.svg +16 -0
  25. package/content/assets/edgepress/icons/book-open.svg +16 -0
  26. package/content/assets/edgepress/icons/check-circle.svg +16 -0
  27. package/content/assets/edgepress/icons/check.svg +15 -0
  28. package/content/assets/edgepress/icons/cloud.svg +15 -0
  29. package/content/assets/edgepress/icons/code.svg +16 -0
  30. package/content/assets/edgepress/icons/cpu.svg +28 -0
  31. package/content/assets/edgepress/icons/file-text.svg +19 -0
  32. package/content/assets/edgepress/icons/github.svg +17 -0
  33. package/content/assets/edgepress/icons/globe.svg +17 -0
  34. package/content/assets/edgepress/icons/help-circle.svg +17 -0
  35. package/content/assets/edgepress/icons/home.svg +16 -0
  36. package/content/assets/edgepress/icons/layers.svg +17 -0
  37. package/content/assets/edgepress/icons/layout-grid.svg +18 -0
  38. package/content/assets/edgepress/icons/mail.svg +16 -0
  39. package/content/assets/edgepress/icons/newspaper.svg +18 -0
  40. package/content/assets/edgepress/icons/palette.svg +19 -0
  41. package/content/assets/edgepress/icons/plug.svg +18 -0
  42. package/content/assets/edgepress/icons/provenance.json +8 -0
  43. package/content/assets/edgepress/icons/puzzle.svg +15 -0
  44. package/content/assets/edgepress/icons/rocket.svg +18 -0
  45. package/content/assets/edgepress/icons/rss.svg +17 -0
  46. package/content/assets/edgepress/icons/search.svg +16 -0
  47. package/content/assets/edgepress/icons/settings.svg +16 -0
  48. package/content/assets/edgepress/icons/shield-check.svg +16 -0
  49. package/content/assets/edgepress/icons/sparkles.svg +18 -0
  50. package/content/assets/edgepress/icons/terminal.svg +16 -0
  51. package/content/assets/edgepress/icons/x.svg +16 -0
  52. package/{static → content/assets}/edgepress-markdown-guide.svg +1 -1
  53. package/content/assets/images/previews/edgepress-en.webp +0 -0
  54. package/content/assets/images/previews/edgepress-revisions-en.png +0 -0
  55. package/content/assets/images/previews/edgepress-workflow-en.90f4650b21013786.gif +0 -0
  56. package/content/assets/images/previews/edgepress-workflow-zh.41f1e0a685f47267.gif +0 -0
  57. package/content/assets/images/previews/edgepress-zh.webp +0 -0
  58. package/content/assets/images/previews/provenance.json +49 -0
  59. package/content/guides/markdown-media.md +38 -0
  60. package/content/guides/platforms.md +15 -0
  61. package/content/guides/product-demos.md +37 -0
  62. package/content/guides/zh-cn.md +62 -0
  63. package/content/pages/deployment/en.json +74 -0
  64. package/content/pages/deployment/zh-cn.json +74 -0
  65. package/content/pages/home/en.json +248 -0
  66. package/content/pages/home/zh-cn.json +248 -0
  67. package/content/pages/local-tools/en.json +14 -0
  68. package/content/pages/local-tools/zh-cn.json +14 -0
  69. package/content/pages/page-authoring/en.json +107 -0
  70. package/content/pages/page-authoring/zh-cn.json +107 -0
  71. package/content/pages/plugin-development/en.json +202 -0
  72. package/content/pages/plugin-development/zh-cn.json +202 -0
  73. package/content/pages/privacy-policy/en.json +171 -0
  74. package/content/pages/privacy-policy/zh-cn.json +171 -0
  75. package/content/pages/project-introduction/en.json +158 -0
  76. package/content/pages/project-introduction/zh-cn.json +158 -0
  77. package/content/pages/quick-start/en.json +325 -0
  78. package/content/pages/quick-start/zh-cn.json +325 -0
  79. package/content/pages/theme-development/en.json +353 -0
  80. package/content/pages/theme-development/zh-cn.json +349 -0
  81. package/content/posts/2026-09-24-edgepress-release-2026-9-1/en.md +1 -1
  82. package/content/posts/2026-09-24-edgepress-release-2026-9-1/zh-cn.md +1 -1
  83. package/content/posts/2026-10-03-edgepress-release-2026-10-3/en.md +16 -0
  84. package/content/posts/2026-10-03-edgepress-release-2026-10-3/zh-cn.md +16 -0
  85. package/content/posts/2026-10-03-edgepress-release-202610-4/en.md +22 -0
  86. package/content/posts/2026-10-03-edgepress-release-202610-4/zh-cn.md +20 -0
  87. package/content/posts/2026-10-03-edgepress-release-202610-5/en.md +15 -0
  88. package/content/posts/2026-10-03-edgepress-release-202610-5/zh-cn.md +15 -0
  89. package/edgeone.json +43 -0
  90. package/esa.jsonc +9 -0
  91. package/languages/base/en.json +75 -10
  92. package/languages/packs/zh-CN.json +74 -9
  93. package/netlify.toml +6 -0
  94. package/package.json +37 -6
  95. package/plugins/consent/index.js +10 -17
  96. package/project-compatibility.json +142 -25
  97. package/schemas/pages.schema.json +74 -0
  98. package/src/assets.js +146 -18
  99. package/src/browser-audit.js +158 -0
  100. package/src/build.js +34 -9
  101. package/src/cli.js +46 -41
  102. package/src/config.js +114 -63
  103. package/src/consent-csp.js +137 -0
  104. package/src/content.js +55 -30
  105. package/src/data-saver.js +194 -0
  106. package/src/deploy.js +28 -0
  107. package/src/document.js +44 -0
  108. package/src/embed.js +60 -0
  109. package/src/generators.js +101 -59
  110. package/src/i18n.js +19 -3
  111. package/src/icons.js +5 -4
  112. package/src/image-dimensions.js +17 -0
  113. package/src/local-tool.js +29 -0
  114. package/src/maintenance.js +25 -8
  115. package/src/markdown-fixture.js +20 -0
  116. package/src/markdown.js +80 -16
  117. package/src/media-assets.js +19 -0
  118. package/src/media-viewer.js +47 -0
  119. package/src/oembed.js +53 -0
  120. package/src/page-blocks.js +124 -54
  121. package/src/page-check.js +29 -39
  122. package/src/plugin-api.js +7 -1
  123. package/src/post-details.js +60 -0
  124. package/src/post-template.js +5 -0
  125. package/src/publication-time.js +47 -0
  126. package/src/report-path.js +19 -0
  127. package/src/report-visuals.js +91 -18
  128. package/src/revision-cards.js +30 -0
  129. package/src/revision-images.js +44 -0
  130. package/src/revisions.js +77 -0
  131. package/src/rss.js +100 -0
  132. package/src/service-block.js +18 -0
  133. package/src/theme.js +59 -9
  134. package/src/video-metadata.js +36 -0
  135. package/static/edgepress/data-saver.css +12 -0
  136. package/static/edgepress/data-saver.js +333 -0
  137. package/static/edgepress/document-viewer.js +35 -0
  138. package/static/edgepress/external-links-policy.js +14 -0
  139. package/static/edgepress/external-links.css +13 -0
  140. package/static/edgepress/external-links.js +120 -0
  141. package/static/edgepress/fonts/faces.css +2217 -0
  142. package/static/edgepress/fonts/noto-sans-OFL.txt +93 -0
  143. package/static/edgepress/fonts/noto-sans-cyrillic-ext-wght-normal.82ff72b28e6610f2.woff2 +0 -0
  144. package/static/edgepress/fonts/noto-sans-cyrillic-wght-normal.6ab64433de6077ca.woff2 +0 -0
  145. package/static/edgepress/fonts/noto-sans-devanagari-wght-normal.ea6a530b75d93de7.woff2 +0 -0
  146. package/static/edgepress/fonts/noto-sans-greek-ext-wght-normal.790b255c10c32e0b.woff2 +0 -0
  147. package/static/edgepress/fonts/noto-sans-greek-wght-normal.7f655207c2f6c140.woff2 +0 -0
  148. package/static/edgepress/fonts/noto-sans-latin-ext-wght-normal.6d5a79315528df19.woff2 +0 -0
  149. package/static/edgepress/fonts/noto-sans-latin-wght-normal.51ca196f49a33e79.woff2 +0 -0
  150. package/static/edgepress/fonts/noto-sans-sc-100-wght-normal.ad5f29028d5699a5.woff2 +0 -0
  151. package/static/edgepress/fonts/noto-sans-sc-101-wght-normal.e189b81fc22ca6af.woff2 +0 -0
  152. package/static/edgepress/fonts/noto-sans-sc-102-wght-normal.941c0c2971fc1d6d.woff2 +0 -0
  153. package/static/edgepress/fonts/noto-sans-sc-103-wght-normal.bafbd591af6493ff.woff2 +0 -0
  154. package/static/edgepress/fonts/noto-sans-sc-104-wght-normal.b0bf80ced89dd61b.woff2 +0 -0
  155. package/static/edgepress/fonts/noto-sans-sc-105-wght-normal.8d8b4e6f4a00621d.woff2 +0 -0
  156. package/static/edgepress/fonts/noto-sans-sc-106-wght-normal.bae74acc629d2e74.woff2 +0 -0
  157. package/static/edgepress/fonts/noto-sans-sc-107-wght-normal.a42eff5010de7b0d.woff2 +0 -0
  158. package/static/edgepress/fonts/noto-sans-sc-108-wght-normal.91882203459867be.woff2 +0 -0
  159. package/static/edgepress/fonts/noto-sans-sc-109-wght-normal.a91f8b038e5ce108.woff2 +0 -0
  160. package/static/edgepress/fonts/noto-sans-sc-110-wght-normal.bbca9081655c4523.woff2 +0 -0
  161. package/static/edgepress/fonts/noto-sans-sc-111-wght-normal.372be010ac0e87cb.woff2 +0 -0
  162. package/static/edgepress/fonts/noto-sans-sc-112-wght-normal.34a4580239eae8dc.woff2 +0 -0
  163. package/static/edgepress/fonts/noto-sans-sc-113-wght-normal.614ab8f616352583.woff2 +0 -0
  164. package/static/edgepress/fonts/noto-sans-sc-114-wght-normal.cf7b8344807a316e.woff2 +0 -0
  165. package/static/edgepress/fonts/noto-sans-sc-115-wght-normal.fb29957c7adccb9a.woff2 +0 -0
  166. package/static/edgepress/fonts/noto-sans-sc-116-wght-normal.a98dc5a726f82141.woff2 +0 -0
  167. package/static/edgepress/fonts/noto-sans-sc-117-wght-normal.d94b5af0c14838af.woff2 +0 -0
  168. package/static/edgepress/fonts/noto-sans-sc-118-wght-normal.76c9ddb85ae4578c.woff2 +0 -0
  169. package/static/edgepress/fonts/noto-sans-sc-119-wght-normal.b3b2374881f4cd93.woff2 +0 -0
  170. package/static/edgepress/fonts/noto-sans-sc-21-wght-normal.c1409c43e55ecff2.woff2 +0 -0
  171. package/static/edgepress/fonts/noto-sans-sc-22-wght-normal.77037ed42ff4e7b0.woff2 +0 -0
  172. package/static/edgepress/fonts/noto-sans-sc-23-wght-normal.38d0df21ac374e93.woff2 +0 -0
  173. package/static/edgepress/fonts/noto-sans-sc-24-wght-normal.8fa21e7e457acf3e.woff2 +0 -0
  174. package/static/edgepress/fonts/noto-sans-sc-25-wght-normal.f760b7a3871d214d.woff2 +0 -0
  175. package/static/edgepress/fonts/noto-sans-sc-26-wght-normal.be8386e7bd7facfe.woff2 +0 -0
  176. package/static/edgepress/fonts/noto-sans-sc-27-wght-normal.4895abcaddbd162e.woff2 +0 -0
  177. package/static/edgepress/fonts/noto-sans-sc-28-wght-normal.82540b6201a9b9ad.woff2 +0 -0
  178. package/static/edgepress/fonts/noto-sans-sc-29-wght-normal.ff21d8fcc60ce391.woff2 +0 -0
  179. package/static/edgepress/fonts/noto-sans-sc-30-wght-normal.1f76f6ffded00b9e.woff2 +0 -0
  180. package/static/edgepress/fonts/noto-sans-sc-31-wght-normal.e6c25be1bb08a63a.woff2 +0 -0
  181. package/static/edgepress/fonts/noto-sans-sc-32-wght-normal.0cda750d7d04a4e1.woff2 +0 -0
  182. package/static/edgepress/fonts/noto-sans-sc-33-wght-normal.b9732b5606bd87c5.woff2 +0 -0
  183. package/static/edgepress/fonts/noto-sans-sc-34-wght-normal.38aa326b4c84902a.woff2 +0 -0
  184. package/static/edgepress/fonts/noto-sans-sc-35-wght-normal.bd98226c8e0aa808.woff2 +0 -0
  185. package/static/edgepress/fonts/noto-sans-sc-36-wght-normal.0db1ca695a491d37.woff2 +0 -0
  186. package/static/edgepress/fonts/noto-sans-sc-37-wght-normal.8df409a4e0a237c2.woff2 +0 -0
  187. package/static/edgepress/fonts/noto-sans-sc-38-wght-normal.672d2d994994a3c3.woff2 +0 -0
  188. package/static/edgepress/fonts/noto-sans-sc-39-wght-normal.272d6d0e153f56e8.woff2 +0 -0
  189. package/static/edgepress/fonts/noto-sans-sc-4-wght-normal.9fafd93ac77b81ff.woff2 +0 -0
  190. package/static/edgepress/fonts/noto-sans-sc-40-wght-normal.5b9ef1ed48420aa7.woff2 +0 -0
  191. package/static/edgepress/fonts/noto-sans-sc-41-wght-normal.30aee47f660fd88a.woff2 +0 -0
  192. package/static/edgepress/fonts/noto-sans-sc-42-wght-normal.ec0014830db682b6.woff2 +0 -0
  193. package/static/edgepress/fonts/noto-sans-sc-43-wght-normal.43df2b5dad9bb835.woff2 +0 -0
  194. package/static/edgepress/fonts/noto-sans-sc-44-wght-normal.acaa6195041b1a92.woff2 +0 -0
  195. package/static/edgepress/fonts/noto-sans-sc-45-wght-normal.8cdb9903f770db21.woff2 +0 -0
  196. package/static/edgepress/fonts/noto-sans-sc-46-wght-normal.6590d0a6fe94571c.woff2 +0 -0
  197. package/static/edgepress/fonts/noto-sans-sc-47-wght-normal.b05afdd32b3bf722.woff2 +0 -0
  198. package/static/edgepress/fonts/noto-sans-sc-48-wght-normal.c9585f7d070700be.woff2 +0 -0
  199. package/static/edgepress/fonts/noto-sans-sc-49-wght-normal.4943895a4dfa5dcf.woff2 +0 -0
  200. package/static/edgepress/fonts/noto-sans-sc-5-wght-normal.e64f57937e3ae4fe.woff2 +0 -0
  201. package/static/edgepress/fonts/noto-sans-sc-50-wght-normal.13e51063d7743cfe.woff2 +0 -0
  202. package/static/edgepress/fonts/noto-sans-sc-51-wght-normal.fd4df1f02c2cb4d7.woff2 +0 -0
  203. package/static/edgepress/fonts/noto-sans-sc-52-wght-normal.06999a39c4c3a7ea.woff2 +0 -0
  204. package/static/edgepress/fonts/noto-sans-sc-53-wght-normal.767b7b9864658d91.woff2 +0 -0
  205. package/static/edgepress/fonts/noto-sans-sc-54-wght-normal.4cd00537a46f6f1a.woff2 +0 -0
  206. package/static/edgepress/fonts/noto-sans-sc-55-wght-normal.0118ec5d8c30b197.woff2 +0 -0
  207. package/static/edgepress/fonts/noto-sans-sc-56-wght-normal.a3e4a78929826930.woff2 +0 -0
  208. package/static/edgepress/fonts/noto-sans-sc-57-wght-normal.7623501009e4a6db.woff2 +0 -0
  209. package/static/edgepress/fonts/noto-sans-sc-58-wght-normal.56ae1a59a32e4a25.woff2 +0 -0
  210. package/static/edgepress/fonts/noto-sans-sc-59-wght-normal.7a377a40e99fd9bc.woff2 +0 -0
  211. package/static/edgepress/fonts/noto-sans-sc-6-wght-normal.17e15c4f0a5bfd92.woff2 +0 -0
  212. package/static/edgepress/fonts/noto-sans-sc-60-wght-normal.3657f66babdf6aff.woff2 +0 -0
  213. package/static/edgepress/fonts/noto-sans-sc-61-wght-normal.23919a2eb11d25dd.woff2 +0 -0
  214. package/static/edgepress/fonts/noto-sans-sc-62-wght-normal.a901edf25bc1cfae.woff2 +0 -0
  215. package/static/edgepress/fonts/noto-sans-sc-63-wght-normal.92f56f713cc0b62f.woff2 +0 -0
  216. package/static/edgepress/fonts/noto-sans-sc-64-wght-normal.4929c9f39a5a4bd9.woff2 +0 -0
  217. package/static/edgepress/fonts/noto-sans-sc-65-wght-normal.8daa61282fb62a11.woff2 +0 -0
  218. package/static/edgepress/fonts/noto-sans-sc-66-wght-normal.6479166925eb462b.woff2 +0 -0
  219. package/static/edgepress/fonts/noto-sans-sc-67-wght-normal.a6b0e11c4c761ffd.woff2 +0 -0
  220. package/static/edgepress/fonts/noto-sans-sc-68-wght-normal.65b77f1fe9d68dae.woff2 +0 -0
  221. package/static/edgepress/fonts/noto-sans-sc-69-wght-normal.b34f765cbbf4d02e.woff2 +0 -0
  222. package/static/edgepress/fonts/noto-sans-sc-70-wght-normal.a99d55c00e4f95b7.woff2 +0 -0
  223. package/static/edgepress/fonts/noto-sans-sc-71-wght-normal.3298bed30dc8d233.woff2 +0 -0
  224. package/static/edgepress/fonts/noto-sans-sc-72-wght-normal.6597209ca9707726.woff2 +0 -0
  225. package/static/edgepress/fonts/noto-sans-sc-73-wght-normal.e7ac1d4930eabd78.woff2 +0 -0
  226. package/static/edgepress/fonts/noto-sans-sc-74-wght-normal.1f0287be83a28e7a.woff2 +0 -0
  227. package/static/edgepress/fonts/noto-sans-sc-75-wght-normal.d9a4bb7bcc2be578.woff2 +0 -0
  228. package/static/edgepress/fonts/noto-sans-sc-76-wght-normal.5a019946bcadcec6.woff2 +0 -0
  229. package/static/edgepress/fonts/noto-sans-sc-77-wght-normal.7c445e85d04f5319.woff2 +0 -0
  230. package/static/edgepress/fonts/noto-sans-sc-78-wght-normal.7d4afb70f641781c.woff2 +0 -0
  231. package/static/edgepress/fonts/noto-sans-sc-79-wght-normal.5798753a721bb4ec.woff2 +0 -0
  232. package/static/edgepress/fonts/noto-sans-sc-80-wght-normal.db887159ca6e46f3.woff2 +0 -0
  233. package/static/edgepress/fonts/noto-sans-sc-81-wght-normal.71e5743bbd225b3d.woff2 +0 -0
  234. package/static/edgepress/fonts/noto-sans-sc-82-wght-normal.8c5094c3785fd04f.woff2 +0 -0
  235. package/static/edgepress/fonts/noto-sans-sc-83-wght-normal.4a60a1ccba905284.woff2 +0 -0
  236. package/static/edgepress/fonts/noto-sans-sc-84-wght-normal.3ed54e59ef4758e3.woff2 +0 -0
  237. package/static/edgepress/fonts/noto-sans-sc-85-wght-normal.e87e0a1342e1778c.woff2 +0 -0
  238. package/static/edgepress/fonts/noto-sans-sc-86-wght-normal.6d3ac3e20959dbcb.woff2 +0 -0
  239. package/static/edgepress/fonts/noto-sans-sc-87-wght-normal.0eeffd5daad5c17b.woff2 +0 -0
  240. package/static/edgepress/fonts/noto-sans-sc-88-wght-normal.b1d9582dfa268463.woff2 +0 -0
  241. package/static/edgepress/fonts/noto-sans-sc-89-wght-normal.8b437aeb9e9fcb5a.woff2 +0 -0
  242. package/static/edgepress/fonts/noto-sans-sc-90-wght-normal.83b0a59e0a5e045a.woff2 +0 -0
  243. package/static/edgepress/fonts/noto-sans-sc-91-wght-normal.382f72b98d4bb9d7.woff2 +0 -0
  244. package/static/edgepress/fonts/noto-sans-sc-97-wght-normal.5e52856457c5cdab.woff2 +0 -0
  245. package/static/edgepress/fonts/noto-sans-sc-98-wght-normal.ffded4e7fd560d93.woff2 +0 -0
  246. package/static/edgepress/fonts/noto-sans-sc-99-wght-normal.54e302c23f11a8fd.woff2 +0 -0
  247. package/static/edgepress/fonts/noto-sans-sc-OFL.txt +93 -0
  248. package/static/edgepress/fonts/noto-sans-sc-cyrillic-wght-normal.5932d441e7126669.woff2 +0 -0
  249. package/static/edgepress/fonts/noto-sans-sc-latin-ext-wght-normal.e316201f3fc493d1.woff2 +0 -0
  250. package/static/edgepress/fonts/noto-sans-sc-latin-wght-normal.c1d919c8701a86ed.woff2 +0 -0
  251. package/static/edgepress/fonts/noto-sans-sc-vietnamese-wght-normal.cba3d00c8db989f0.woff2 +0 -0
  252. package/static/edgepress/fonts/noto-sans-tc-0-wght-normal.63c21eda5d20203f.woff2 +0 -0
  253. package/static/edgepress/fonts/noto-sans-tc-100-wght-normal.87a7c76301c24a28.woff2 +0 -0
  254. package/static/edgepress/fonts/noto-sans-tc-101-wght-normal.c9e83fc4ffd53746.woff2 +0 -0
  255. package/static/edgepress/fonts/noto-sans-tc-102-wght-normal.9a00ae0e5735852b.woff2 +0 -0
  256. package/static/edgepress/fonts/noto-sans-tc-103-wght-normal.7cfe93b3221f4def.woff2 +0 -0
  257. package/static/edgepress/fonts/noto-sans-tc-104-wght-normal.2d5ac7497d84cb99.woff2 +0 -0
  258. package/static/edgepress/fonts/noto-sans-tc-105-wght-normal.0989885bb16bcd9b.woff2 +0 -0
  259. package/static/edgepress/fonts/noto-sans-tc-106-wght-normal.1faa921b035064e7.woff2 +0 -0
  260. package/static/edgepress/fonts/noto-sans-tc-107-wght-normal.2f6006745ae16d69.woff2 +0 -0
  261. package/static/edgepress/fonts/noto-sans-tc-108-wght-normal.a61e48c10a2cf4b9.woff2 +0 -0
  262. package/static/edgepress/fonts/noto-sans-tc-109-wght-normal.f28c767724736d2d.woff2 +0 -0
  263. package/static/edgepress/fonts/noto-sans-tc-110-wght-normal.4b04bd88c3dc7c5d.woff2 +0 -0
  264. package/static/edgepress/fonts/noto-sans-tc-111-wght-normal.b6251e1967932a52.woff2 +0 -0
  265. package/static/edgepress/fonts/noto-sans-tc-112-wght-normal.e264513b32f90660.woff2 +0 -0
  266. package/static/edgepress/fonts/noto-sans-tc-113-wght-normal.46cb347d88f3338e.woff2 +0 -0
  267. package/static/edgepress/fonts/noto-sans-tc-114-wght-normal.9f9e1e0d0ab2aeb4.woff2 +0 -0
  268. package/static/edgepress/fonts/noto-sans-tc-115-wght-normal.653dd2ab7d551b40.woff2 +0 -0
  269. package/static/edgepress/fonts/noto-sans-tc-116-wght-normal.4718bf3627191297.woff2 +0 -0
  270. package/static/edgepress/fonts/noto-sans-tc-117-wght-normal.b241a9409682af5f.woff2 +0 -0
  271. package/static/edgepress/fonts/noto-sans-tc-118-wght-normal.fda2800dc1c06852.woff2 +0 -0
  272. package/static/edgepress/fonts/noto-sans-tc-119-wght-normal.948af36bd63a72c7.woff2 +0 -0
  273. package/static/edgepress/fonts/noto-sans-tc-19-wght-normal.62191cdc3a10e60a.woff2 +0 -0
  274. package/static/edgepress/fonts/noto-sans-tc-20-wght-normal.65c90892c2e01657.woff2 +0 -0
  275. package/static/edgepress/fonts/noto-sans-tc-21-wght-normal.d3f59b094c334866.woff2 +0 -0
  276. package/static/edgepress/fonts/noto-sans-tc-22-wght-normal.b1983a041d7e4e1f.woff2 +0 -0
  277. package/static/edgepress/fonts/noto-sans-tc-23-wght-normal.16e2163b66c7732a.woff2 +0 -0
  278. package/static/edgepress/fonts/noto-sans-tc-24-wght-normal.72c8561976116537.woff2 +0 -0
  279. package/static/edgepress/fonts/noto-sans-tc-25-wght-normal.b2948d928ce4dd54.woff2 +0 -0
  280. package/static/edgepress/fonts/noto-sans-tc-26-wght-normal.d583236b90abedc9.woff2 +0 -0
  281. package/static/edgepress/fonts/noto-sans-tc-27-wght-normal.7d6073ba07597c9b.woff2 +0 -0
  282. package/static/edgepress/fonts/noto-sans-tc-28-wght-normal.517204d716b089cd.woff2 +0 -0
  283. package/static/edgepress/fonts/noto-sans-tc-29-wght-normal.10189a8f8ca38099.woff2 +0 -0
  284. package/static/edgepress/fonts/noto-sans-tc-30-wght-normal.695a71d72dfab3f7.woff2 +0 -0
  285. package/static/edgepress/fonts/noto-sans-tc-31-wght-normal.868d59473ef0e73c.woff2 +0 -0
  286. package/static/edgepress/fonts/noto-sans-tc-32-wght-normal.92efa2d4b3702ec0.woff2 +0 -0
  287. package/static/edgepress/fonts/noto-sans-tc-33-wght-normal.d2274bcd5636197d.woff2 +0 -0
  288. package/static/edgepress/fonts/noto-sans-tc-34-wght-normal.1ac8c3718ee2eed2.woff2 +0 -0
  289. package/static/edgepress/fonts/noto-sans-tc-35-wght-normal.00b3df730d5a4c73.woff2 +0 -0
  290. package/static/edgepress/fonts/noto-sans-tc-36-wght-normal.6963feb6c628b81f.woff2 +0 -0
  291. package/static/edgepress/fonts/noto-sans-tc-37-wght-normal.a1b4bd294af24cd1.woff2 +0 -0
  292. package/static/edgepress/fonts/noto-sans-tc-38-wght-normal.8edb2f7f2955921b.woff2 +0 -0
  293. package/static/edgepress/fonts/noto-sans-tc-39-wght-normal.37bf7394eb3148c6.woff2 +0 -0
  294. package/static/edgepress/fonts/noto-sans-tc-40-wght-normal.3efa655bc48f9133.woff2 +0 -0
  295. package/static/edgepress/fonts/noto-sans-tc-41-wght-normal.48d47014544ebd5d.woff2 +0 -0
  296. package/static/edgepress/fonts/noto-sans-tc-42-wght-normal.214c733a59d423e7.woff2 +0 -0
  297. package/static/edgepress/fonts/noto-sans-tc-43-wght-normal.c2c9876f138bb20f.woff2 +0 -0
  298. package/static/edgepress/fonts/noto-sans-tc-44-wght-normal.28928bfea29db06a.woff2 +0 -0
  299. package/static/edgepress/fonts/noto-sans-tc-45-wght-normal.bf3743f8155fa9d2.woff2 +0 -0
  300. package/static/edgepress/fonts/noto-sans-tc-46-wght-normal.06970903593781c3.woff2 +0 -0
  301. package/static/edgepress/fonts/noto-sans-tc-47-wght-normal.8932ed83a91794d4.woff2 +0 -0
  302. package/static/edgepress/fonts/noto-sans-tc-48-wght-normal.dfd9225628bcf234.woff2 +0 -0
  303. package/static/edgepress/fonts/noto-sans-tc-49-wght-normal.c27d341ad39d44eb.woff2 +0 -0
  304. package/static/edgepress/fonts/noto-sans-tc-50-wght-normal.01a0f4e7dfba0a56.woff2 +0 -0
  305. package/static/edgepress/fonts/noto-sans-tc-51-wght-normal.5a30022af3d30e0f.woff2 +0 -0
  306. package/static/edgepress/fonts/noto-sans-tc-52-wght-normal.4884f032342a3274.woff2 +0 -0
  307. package/static/edgepress/fonts/noto-sans-tc-53-wght-normal.537adcae44511270.woff2 +0 -0
  308. package/static/edgepress/fonts/noto-sans-tc-54-wght-normal.6bc8c7d3e47c6fde.woff2 +0 -0
  309. package/static/edgepress/fonts/noto-sans-tc-55-wght-normal.725a48711f1e1997.woff2 +0 -0
  310. package/static/edgepress/fonts/noto-sans-tc-56-wght-normal.f43ecaa73e0cd8fc.woff2 +0 -0
  311. package/static/edgepress/fonts/noto-sans-tc-57-wght-normal.15b24e9d42a3270f.woff2 +0 -0
  312. package/static/edgepress/fonts/noto-sans-tc-58-wght-normal.9b2d8a71667d3da6.woff2 +0 -0
  313. package/static/edgepress/fonts/noto-sans-tc-59-wght-normal.a68535a6b3289d89.woff2 +0 -0
  314. package/static/edgepress/fonts/noto-sans-tc-6-wght-normal.32cece1ae80c4f2b.woff2 +0 -0
  315. package/static/edgepress/fonts/noto-sans-tc-60-wght-normal.3b2c56a921d1cad4.woff2 +0 -0
  316. package/static/edgepress/fonts/noto-sans-tc-61-wght-normal.54f51f80a0740878.woff2 +0 -0
  317. package/static/edgepress/fonts/noto-sans-tc-62-wght-normal.ba029c5bb6c07bf1.woff2 +0 -0
  318. package/static/edgepress/fonts/noto-sans-tc-63-wght-normal.380919beec4c7f3d.woff2 +0 -0
  319. package/static/edgepress/fonts/noto-sans-tc-64-wght-normal.f96fe0be479d201f.woff2 +0 -0
  320. package/static/edgepress/fonts/noto-sans-tc-65-wght-normal.7d544b80701fe678.woff2 +0 -0
  321. package/static/edgepress/fonts/noto-sans-tc-66-wght-normal.fab669c07a1e3bee.woff2 +0 -0
  322. package/static/edgepress/fonts/noto-sans-tc-67-wght-normal.0001bb744743fdfa.woff2 +0 -0
  323. package/static/edgepress/fonts/noto-sans-tc-68-wght-normal.1a57eda03886b43c.woff2 +0 -0
  324. package/static/edgepress/fonts/noto-sans-tc-69-wght-normal.017fecc749bcd6d5.woff2 +0 -0
  325. package/static/edgepress/fonts/noto-sans-tc-7-wght-normal.cd0f0cac743dbbfe.woff2 +0 -0
  326. package/static/edgepress/fonts/noto-sans-tc-70-wght-normal.79fb9d70b9534ec7.woff2 +0 -0
  327. package/static/edgepress/fonts/noto-sans-tc-71-wght-normal.04a6ea581b5f1195.woff2 +0 -0
  328. package/static/edgepress/fonts/noto-sans-tc-72-wght-normal.d3a4eb35499157b7.woff2 +0 -0
  329. package/static/edgepress/fonts/noto-sans-tc-73-wght-normal.d81f8484404ee465.woff2 +0 -0
  330. package/static/edgepress/fonts/noto-sans-tc-74-wght-normal.4a5da9e0c2702f7d.woff2 +0 -0
  331. package/static/edgepress/fonts/noto-sans-tc-75-wght-normal.8b87f9c6c164513a.woff2 +0 -0
  332. package/static/edgepress/fonts/noto-sans-tc-76-wght-normal.a71463dbfd9fa7b5.woff2 +0 -0
  333. package/static/edgepress/fonts/noto-sans-tc-77-wght-normal.d094414ea752cd52.woff2 +0 -0
  334. package/static/edgepress/fonts/noto-sans-tc-78-wght-normal.04f7c97bd9cbc73b.woff2 +0 -0
  335. package/static/edgepress/fonts/noto-sans-tc-79-wght-normal.1ed089d037a02049.woff2 +0 -0
  336. package/static/edgepress/fonts/noto-sans-tc-8-wght-normal.d052677dfb5db5bd.woff2 +0 -0
  337. package/static/edgepress/fonts/noto-sans-tc-80-wght-normal.6d41ca59cfd50442.woff2 +0 -0
  338. package/static/edgepress/fonts/noto-sans-tc-81-wght-normal.f09bd0afa8a58114.woff2 +0 -0
  339. package/static/edgepress/fonts/noto-sans-tc-82-wght-normal.7920841c526b1b02.woff2 +0 -0
  340. package/static/edgepress/fonts/noto-sans-tc-83-wght-normal.0c6f7f82db8da0d8.woff2 +0 -0
  341. package/static/edgepress/fonts/noto-sans-tc-84-wght-normal.f37c0a853b31265e.woff2 +0 -0
  342. package/static/edgepress/fonts/noto-sans-tc-85-wght-normal.4173bff087653b82.woff2 +0 -0
  343. package/static/edgepress/fonts/noto-sans-tc-86-wght-normal.dac35605ebec15c4.woff2 +0 -0
  344. package/static/edgepress/fonts/noto-sans-tc-87-wght-normal.8ac85fdd0b15a6bd.woff2 +0 -0
  345. package/static/edgepress/fonts/noto-sans-tc-88-wght-normal.61625b0b21e8407f.woff2 +0 -0
  346. package/static/edgepress/fonts/noto-sans-tc-89-wght-normal.a1da3dccbf00698e.woff2 +0 -0
  347. package/static/edgepress/fonts/noto-sans-tc-90-wght-normal.bf08f0355222a782.woff2 +0 -0
  348. package/static/edgepress/fonts/noto-sans-tc-91-wght-normal.2ecf295bba2bfaa3.woff2 +0 -0
  349. package/static/edgepress/fonts/noto-sans-tc-92-wght-normal.36508326bb68115f.woff2 +0 -0
  350. package/static/edgepress/fonts/noto-sans-tc-97-wght-normal.c712899e0fc9e947.woff2 +0 -0
  351. package/static/edgepress/fonts/noto-sans-tc-98-wght-normal.13a648e521d18d7f.woff2 +0 -0
  352. package/static/edgepress/fonts/noto-sans-tc-99-wght-normal.7f8c9a777e64159a.woff2 +0 -0
  353. package/static/edgepress/fonts/noto-sans-tc-OFL.txt +93 -0
  354. package/static/edgepress/fonts/noto-sans-tc-cyrillic-wght-normal.dd0980ced8e46cb3.woff2 +0 -0
  355. package/static/edgepress/fonts/noto-sans-tc-latin-ext-wght-normal.817d96335dc52ce6.woff2 +0 -0
  356. package/static/edgepress/fonts/noto-sans-tc-latin-wght-normal.50cb5fb0a522cfaf.woff2 +0 -0
  357. package/static/edgepress/fonts/noto-sans-tc-vietnamese-wght-normal.2a77fb7b70fa304d.woff2 +0 -0
  358. package/static/edgepress/fonts/noto-sans-vietnamese-wght-normal.791e3f213c155a5b.woff2 +0 -0
  359. package/static/edgepress/fonts/noto-serif-OFL.txt +93 -0
  360. package/static/edgepress/fonts/noto-serif-cyrillic-ext-wght-normal.7c7afb20d33e30ec.woff2 +0 -0
  361. package/static/edgepress/fonts/noto-serif-cyrillic-wght-normal.54e3bd0c4a002b0f.woff2 +0 -0
  362. package/static/edgepress/fonts/noto-serif-greek-ext-wght-normal.9b3f4c4ff3adefd2.woff2 +0 -0
  363. package/static/edgepress/fonts/noto-serif-greek-wght-normal.1252037b6f1d6e86.woff2 +0 -0
  364. package/static/edgepress/fonts/noto-serif-latin-ext-wght-normal.2654f8d1261a420f.woff2 +0 -0
  365. package/static/edgepress/fonts/noto-serif-latin-wght-normal.46281456234014ce.woff2 +0 -0
  366. package/static/edgepress/fonts/noto-serif-math-wght-normal.d4a6aba2cb4636b3.woff2 +0 -0
  367. package/static/edgepress/fonts/noto-serif-vietnamese-wght-normal.fb6a5ed734c2fbe3.woff2 +0 -0
  368. package/static/edgepress/fonts/provenance.json +1356 -0
  369. package/static/edgepress/fonts.css +14 -0
  370. package/static/edgepress/image-viewer.js +23 -0
  371. package/static/edgepress/images.css +4 -0
  372. package/static/edgepress/link-directory.css +14 -0
  373. package/static/edgepress/local-time.js +12 -0
  374. package/static/edgepress/media-viewer.css +1 -0
  375. package/static/edgepress/media-viewer.js +111 -0
  376. package/static/edgepress/navigation-select.js +14 -0
  377. package/static/edgepress/oembed.js +129 -0
  378. package/static/edgepress/plugins/backend/external-api.js +4 -0
  379. package/static/edgepress/plugins/backend/external-widget.js +13 -0
  380. package/static/edgepress/plugins/consent/choices.js +47 -0
  381. package/static/edgepress/plugins/consent/manager.js +56 -74
  382. package/static/edgepress/project-demo.js +93 -0
  383. package/static/edgepress/search.js +2 -2
  384. package/static/edgepress/services-consent.js +3 -0
  385. package/static/edgepress/services.js +15 -0
  386. package/static/edgepress/webmcp.js +86 -0
  387. package/tests/assets-runtime.test.mjs +91 -0
  388. package/tests/audit.test.mjs +142 -0
  389. package/tests/brand-regression.test.mjs +110 -0
  390. package/tests/consent-browser.test.mjs +132 -0
  391. package/tests/consent-config.test.mjs +85 -0
  392. package/tests/consent-csp.test.mjs +90 -0
  393. package/tests/data-saver-loading.test.mjs +158 -0
  394. package/tests/data-saver-network.test.mjs +99 -0
  395. package/tests/data-saver.test.mjs +294 -0
  396. package/tests/document-package.test.mjs +28 -0
  397. package/tests/document-viewer.test.mjs +152 -0
  398. package/tests/edge-issues.test.mjs +65 -0
  399. package/tests/embed-syntax.test.mjs +68 -0
  400. package/tests/external-links.test.mjs +198 -0
  401. package/tests/fixtures/recording.mp4 +0 -0
  402. package/tests/fonts.test.mjs +99 -0
  403. package/tests/image-dimensions.test.mjs +15 -0
  404. package/tests/image-viewer.test.mjs +36 -0
  405. package/tests/json-pages.test.mjs +66 -0
  406. package/tests/link-directory.test.mjs +51 -0
  407. package/tests/local-tool.test.mjs +34 -0
  408. package/tests/markdown-media.test.mjs +119 -0
  409. package/tests/media-block.test.mjs +102 -0
  410. package/tests/navigation.test.mjs +87 -0
  411. package/tests/oembed-layout.test.mjs +98 -0
  412. package/tests/oembed.test.mjs +96 -0
  413. package/tests/page-authoring.test.mjs +27 -0
  414. package/tests/page-formatting.test.mjs +37 -0
  415. package/tests/post-list.test.mjs +96 -0
  416. package/tests/project-demo.test.mjs +73 -0
  417. package/tests/publication-time.test.mjs +43 -0
  418. package/tests/release-version.test.mjs +28 -0
  419. package/tests/revisions.test.mjs +93 -0
  420. package/tests/rss.test.mjs +54 -0
  421. package/tests/services.test.mjs +35 -0
  422. package/tests/theme-layout.test.mjs +95 -0
  423. package/tests/video-metadata.test.mjs +28 -0
  424. package/tests/webmcp.test.mjs +121 -0
  425. package/tests/writing.test.mjs +101 -0
  426. package/themes/atelier/assets/style.css +144 -22
  427. package/themes/atelier/layouts/partials/head.html +8 -1
  428. package/themes/atelier/layouts/partials/header.html +3 -1
  429. package/themes/default/assets/data-saver-theme.css +2 -0
  430. package/themes/default/assets/style.css +139 -23
  431. package/themes/default/layouts/partials/head.html +9 -2
  432. package/themes/default/layouts/partials/header.html +3 -1
  433. package/themes/default/theme.json +1 -0
  434. package/themes/folio/README.md +7 -0
  435. package/themes/folio/assets/data-saver-theme.css +3 -0
  436. package/themes/folio/assets/folio/navigation.js +10 -0
  437. package/themes/folio/assets/style.css +516 -0
  438. package/themes/folio/layouts/layout.html +15 -0
  439. package/themes/folio/layouts/partials/footer.html +6 -0
  440. package/themes/folio/layouts/partials/head.html +30 -0
  441. package/themes/folio/layouts/partials/header.html +8 -0
  442. package/themes/folio/theme.json +7 -0
  443. package/themes/signal/assets/style.css +143 -21
  444. package/themes/signal/layouts/partials/head.html +8 -1
  445. package/themes/signal/layouts/partials/header.html +3 -1
  446. package/tools/README.md +14 -0
  447. package/tools/assets/format-pages.mjs +56 -0
  448. package/tools/assets/generate-brand-assets.py +160 -0
  449. package/tools/assets/sync-fonts.mjs +71 -0
  450. package/tools/assets/sync-icons.mjs +17 -0
  451. package/tools/assets/video-preview.mjs +15 -0
  452. package/tools/recordings/capture-document-demo.mjs +147 -0
  453. package/tools/recordings/demo-workbench.mjs +57 -0
  454. package/tools/recordings/desktop-recording.mjs +92 -0
  455. package/tools/recordings/provision-vm.sh +18 -0
  456. package/tools/recordings/start-vm-desktop.sh +28 -0
  457. package/tools/recordings/vm-desktop.mjs +124 -0
  458. package/tools/verification/README.md +44 -0
  459. package/tools/verification/data-saver-site.mjs +405 -0
  460. package/tools/verification/page-performance.mjs +76 -0
  461. package/tools/verification/verify-brand-assets.py +52 -0
  462. package/tools/verification/verify-brand.mjs +180 -0
  463. package/vercel.json +36 -0
  464. package/wrangler.jsonc +5 -7
  465. package/content/pages/home/en.md +0 -99
  466. package/content/pages/home/zh-cn.md +0 -99
  467. package/content/pages/plugin-development/en.md +0 -109
  468. package/content/pages/plugin-development/zh-cn.md +0 -109
  469. package/content/pages/privacy-policy/en.md +0 -93
  470. package/content/pages/privacy-policy/zh-cn.md +0 -93
  471. package/content/pages/project-introduction/en.md +0 -88
  472. package/content/pages/project-introduction/zh-cn.md +0 -88
  473. package/content/pages/quick-start/en.md +0 -153
  474. package/content/pages/quick-start/zh-cn.md +0 -153
  475. package/content/pages/theme-development/en.md +0 -132
  476. package/content/pages/theme-development/zh-cn.md +0 -132
  477. package/src/worker.js +0 -139
  478. package/static/edgepress/brand/edgepress-mark.svg +0 -7
  479. package/static/edgepress/brand/mascot-209.webp +0 -0
  480. package/static/edgepress/brand/mascot-418.png +0 -0
  481. package/static/edgepress/brand/mascot-418.webp +0 -0
  482. package/static/edgepress/brand/mascot.png +0 -0
  483. package/static/edgepress/brand/og-image.png +0 -0
  484. package/static/edgepress/brand/og-image.svg +0 -46
  485. package/static/edgepress/icons.svg +0 -24
package/src/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import {reportDirectory} from './report-path.js';
3
+ import {publicationTimestamp} from './publication-time.js';
2
4
  import { access, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises';
3
5
  import { dirname, join, relative, resolve, sep } from 'node:path';
4
6
  import { tmpdir } from 'node:os';
@@ -9,8 +11,12 @@ import { loadConfig } from './config.js';
9
11
  import { slugify } from './content.js';
10
12
  import { checkPages } from './page-check.js';
11
13
  import { checkCompatibility, writeIterationPlan } from './maintenance.js';
14
+ import { loadLanguagePacks, translate } from './i18n.js';
15
+ import { postTemplate } from './post-template.js';
16
+ import {deploySite} from './deploy.js';
17
+ import {formatPages} from '../tools/assets/format-pages.mjs';
12
18
 
13
- const siteGitignore = ['node_modules/', 'dist/', '.edgepress/', '.wrangler/', '.dev.vars', '*.tgz', ''].join('\n');
19
+ const siteGitignore = ['node_modules/', 'dist/', '.edgepress/', '.wrangler/', '.dev.vars', '.vercel/', '.netlify/', '.edgeone/', '.esa/', '.env', '.env.*', '!.env.example', '*.tgz', ''].join('\n');
14
20
  const siteReadme = [
15
21
  '# My EdgePress site',
16
22
  '',
@@ -19,19 +25,21 @@ const siteReadme = [
19
25
  '## Start locally',
20
26
  '',
21
27
  ' npm install',
22
- ' edgepress server',
28
+ ' npm run dev',
23
29
  '',
24
- 'Edit localized pages in `content/pages/` and Markdown posts in `content/posts/`. Choose a shared layout with `edgepress theme list` and `edgepress theme use <name>`.',
30
+ 'Create an article with `npm run new -- post "My first article"`. Open the printed file path and write Markdown below the second `---` line. The command fills the title, date, and language. Run `npm run dev` to preview and `npm run build` before publishing.',
25
31
  '',
26
- '## Deploy to Cloudflare Workers',
32
+ 'Pages use localized JSON files such as `content/pages/home/en.json`; VS Code can fold each row, cell and element and read the local schema. Articles stay in Markdown. Page layouts and theme customization are optional for everyday writing.',
27
33
  '',
28
- 'Push this source repository to GitHub, then connect the repository to Cloudflare Workers Builds. The `build` and `deploy` scripts in `package.json` generate the static files and deploy the Worker. Set a unique Worker name in `wrangler.jsonc` first.',
34
+ '## Deploy a static website',
29
35
  '',
30
- 'For a local deployment, run `npx wrangler login`, then `edgepress deploy`.',
36
+ 'Push this source repository to GitHub, then connect the repository to Cloudflare Workers Builds. The `build` and `deploy` scripts in `package.json` generate and deploy the static files. Set a unique Worker name in `wrangler.jsonc` first.',
37
+ '',
38
+ 'Authenticate with the chosen platform, then run `edgepress deploy cloudflare`, `edgepress deploy vercel`, `edgepress deploy edgeone -n my-site`, `edgepress deploy netlify`, or `edgepress deploy esa --name my-site`.',
31
39
  '',
32
40
  '## Use another static web server',
33
41
  '',
34
- 'Run `edgepress generate` and upload the contents of `dist/` to the server document root. OpenResty and Nginx can serve the generated directories as static files. Worker API routes and service bindings need a Worker or an equivalent server-side route.',
42
+ 'Run `edgepress generate` and upload the contents of `dist/` to the server document root. OpenResty and Nginx can serve the generated directories as static files. Optional backends live in separate projects and are registered under plugins.consent.services in config.yml.',
35
43
  ''
36
44
  ].join('\n');
37
45
 
@@ -39,8 +47,8 @@ async function initializeProject() {
39
47
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
40
48
  const root = process.cwd();
41
49
  const directories = ['content', 'languages', 'plugins', 'static', 'themes'];
42
- const files = ['.gitignore', 'README.md', 'config.yml', 'edgepress.config.mjs', 'project-compatibility.json', 'wrangler.jsonc'];
43
- const destinations = [...directories, ...files, 'src/worker.js'];
50
+ const files = ['.gitignore', 'README.md', 'config.yml', 'edgepress.config.mjs', 'project-compatibility.json', 'wrangler.jsonc', 'vercel.json', 'edgeone.json', 'esa.jsonc', 'netlify.toml'];
51
+ const destinations = [...directories, ...files];
44
52
  const existing = [];
45
53
  for (const relativePath of destinations) {
46
54
  try { await access(resolve(root, relativePath)); existing.push(relativePath); }
@@ -76,7 +84,16 @@ async function initializeProject() {
76
84
  preview: 'edgepress server',
77
85
  check: 'edgepress check',
78
86
  doctor: 'edgepress doctor',
79
- deploy: 'edgepress deploy'
87
+ deploy: 'edgepress deploy',
88
+ 'deploy:cloudflare': 'edgepress deploy cloudflare',
89
+ 'deploy:netlify': 'edgepress deploy netlify',
90
+ 'deploy:vercel': 'edgepress deploy vercel',
91
+ 'deploy:edgeone': 'edgepress deploy edgeone',
92
+ 'deploy:esa': 'edgepress deploy esa'
93
+ },
94
+ overrides: {
95
+ ...(packageData.overrides || {}),
96
+ ...(existingManifest.overrides || {})
80
97
  },
81
98
  dependencies: {
82
99
  ...(existingManifest.dependencies || {}),
@@ -85,6 +102,7 @@ async function initializeProject() {
85
102
  }
86
103
  };
87
104
  for (const directory of directories) await cp(resolve(packageRoot, directory), resolve(root, directory), { recursive: true, errorOnExist: true });
105
+ await formatPages(root, {write: true, schemaPath: resolve(root, 'node_modules/edgepress/schemas/pages.schema.json')});
88
106
  for (const file of files) {
89
107
  const destination = resolve(root, file);
90
108
  await mkdir(dirname(destination), { recursive: true });
@@ -92,23 +110,8 @@ async function initializeProject() {
92
110
  else if (file === 'README.md') await writeFile(destination, siteReadme, { flag: 'wx' });
93
111
  else await cp(resolve(packageRoot, file), destination, { errorOnExist: true });
94
112
  }
95
- await mkdir(resolve(root, 'src'), { recursive: true });
96
- await cp(resolve(packageRoot, 'src/worker.js'), resolve(root, 'src/worker.js'), { errorOnExist: true });
97
113
  await writeFile(resolve(root, 'package.json'), JSON.stringify(projectManifest, null, 2) + '\n', { flag: packageManifestExists ? 'w' : 'wx' });
98
- console.log('Created an EdgePress site. Next run npm install, then edgepress server.');
99
- }
100
-
101
- async function runWrangler(command, args = []) {
102
- const wrangler = resolve(process.cwd(), 'node_modules/wrangler/bin/wrangler.js');
103
- await access(wrangler);
104
- await new Promise((resolveExit, reject) => {
105
- const child = spawn(process.execPath, [wrangler, command, ...args], { cwd: process.cwd(), env: process.env, stdio: 'inherit' });
106
- child.once('error', reject);
107
- child.once('exit', (code) => {
108
- if (code === 0) resolveExit();
109
- else process.exitCode = code ?? 1, resolveExit();
110
- });
111
- });
114
+ console.log('Created an EdgePress site. Next run npm install, then npm run dev. Create an article with npm run new -- post "My first article".');
112
115
  }
113
116
 
114
117
  async function runSecurityAudit() {
@@ -246,15 +249,18 @@ async function manageTheme(args, config) {
246
249
  }
247
250
 
248
251
  async function createPost(title, config) {
249
- if (!title) throw new Error('Usage: edgepress new "Post title"');
252
+ if (!title) throw new Error('Usage: edgepress new post "Post title"');
250
253
  const releaseLock = await acquireBuildLock(config.resolvedPaths.cache);
251
254
  try {
252
- const today = new Date().toISOString().slice(0, 10);
255
+ if (title.length > 160 || /[\x00-\x1f\x7f]/.test(title)) throw new Error('Post title must be one line of at most 160 characters.');
256
+ await loadLanguagePacks(config);
257
+ const timestamp = publicationTimestamp(new Date(), config.site.timeZone);
258
+ const today = timestamp.slice(0, 10);
253
259
  const folder = resolve(config.resolvedPaths.content, 'posts', today + '-' + slugify(title));
254
260
  const locale = config.i18n.defaultLocale;
255
261
  const file = resolve(folder, locale.toLowerCase() + '.md');
256
262
  await mkdir(folder, { recursive: true });
257
- const body = '---\ntitle: ' + JSON.stringify(title) + '\ndate: ' + today + '\nlang: ' + locale + '\ntags: []\n---\n\n# ' + title + '\n\nWrite your post here.\n';
263
+ const body = postTemplate(title, timestamp, locale, translate(config, locale, 'postStarter'));
258
264
  let handle;
259
265
  try { handle = await open(file, 'wx'); }
260
266
  catch (error) {
@@ -269,7 +275,9 @@ async function createPost(title, config) {
269
275
  await rm(file, { force: true });
270
276
  throw writeError;
271
277
  }
272
- console.log('Created ' + file);
278
+ console.log(translate(config, locale, 'postCreated').replace('{file}', relative(config.root, file)));
279
+ console.log(translate(config, locale, 'postNextSteps'));
280
+ console.log(translate(config, locale, 'postPreview'));
273
281
  } finally {
274
282
  await releaseLock();
275
283
  }
@@ -288,8 +296,8 @@ async function clean(config) {
288
296
  if (error.code !== 'ENOENT') throw error;
289
297
  }
290
298
  }
291
- const toolsDirectory = resolve(config.root, 'tools');
292
- try {
299
+ const toolsDirectory = await reportDirectory(config.root, false);
300
+ if (toolsDirectory) try {
293
301
  const info = await lstat(toolsDirectory);
294
302
  if (info.isSymbolicLink() || !info.isDirectory()) throw new Error('Refusing to clean reports through an unsafe tools path: ' + toolsDirectory);
295
303
  await rm(resolve(toolsDirectory, 'page-check.pdf'), { force: true });
@@ -323,28 +331,25 @@ try {
323
331
  ? await loadConfig()
324
332
  : null;
325
333
  if (command === '--help' || command === '-h' || command === 'help') {
326
- console.log('EdgePress commands: init, new, build, generate, server, check, doctor, iterate, theme, secret, security, deploy, clean');
334
+ console.log('EdgePress commands: init, new, build, generate, server, check, doctor, iterate, theme, security, deploy, clean');
327
335
  }
328
336
  else if (command === 'build' || command === 'generate') await buildSite();
329
337
  else if (command === 'init') await initializeProject();
330
338
  else if (command === 'preview' || command === 'dev' || command === 'server') await import('./preview.js');
331
339
  else if (command === 'deploy') {
332
- if (process.env.WORKERS_CI !== '1') await buildSite();
333
- await runWrangler('deploy', args);
334
- }
335
- else if (command === 'secret') {
336
- if (!['put', 'delete', 'list'].includes(args[0])) throw new Error('Usage: edgepress secret put|delete|list [name]');
337
- await runWrangler('secret', args);
340
+ const platform = args[0] && !args[0].startsWith('-') ? args.shift() : 'cloudflare';
341
+ const built = process.env.WORKERS_CI === '1' ? {output:(await loadConfig()).resolvedPaths.output} : await buildSite();
342
+ await deploySite(platform, built.output, args);
338
343
  }
339
344
  else if (command === 'theme') await manageTheme(args, await loadConfig());
340
345
  else if (command === 'security') await runSecurityAudit();
341
- else if (command === 'new') await createPost(args.join(' ').trim(), config);
346
+ else if (command === 'new') await createPost((args[0] === 'post' ? args.slice(1) : args).join(' ').trim(), config);
342
347
  else if (command === 'clean') await clean(config);
343
348
  else if (command === 'check') {
344
349
  await buildSite(config.root);
345
350
  const pageReport = await checkPages(config);
346
351
  const compatibilityReport = await checkCompatibility(config);
347
- await showReport('Page check', pageReport, resolve(config.root, 'tools/page-check.pdf'));
352
+ await showReport('Page check', pageReport, resolve(config.root, 'tools/reports/page-check.pdf'));
348
353
  showStatus('Compatibility check', compatibilityReport);
349
354
  if (pageReport.errors || compatibilityReport.issues.some((issue) => issue.severity === 'error')) process.exitCode = 1;
350
355
  } else if (command === 'doctor') {
package/src/config.js CHANGED
@@ -1,15 +1,23 @@
1
+ import {defaultTimeZone,validateTimeZone} from './publication-time.js';
2
+ import { categoryFilter, safeAvatar } from './post-details.js';
1
3
  import { access, readFile, realpath, stat } from 'node:fs/promises';
2
4
  import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
3
5
  import { pathToFileURL } from 'node:url';
4
6
  import { parse as parseYaml } from 'yaml';
7
+ import {validateServiceCsp} from './consent-csp.js';
8
+ import {linkOrigin} from '../static/edgepress/external-links-policy.js';
5
9
 
6
10
  const defaults = {
7
11
  site: {
8
- title: 'EdgePress', description: '', url: '', language: 'en',
12
+ title: 'EdgePress', description: '', url: '', language: 'en', timeZone: defaultTimeZone,
9
13
  head: { titleSuffix: '', titleSeparator: ' · ' },
10
14
  header: { showBrand: true, brandLabel: '' },
11
15
  components: { header: true, primaryNavigation: true, languageNavigation: true, footer: true, footerNavigation: true },
16
+ author: { name: '', avatar: '' },
17
+ archive: { categories: [] },
12
18
  navigation: [],
19
+ externalLinks: { enabled: false, trustedOrigins: [] },
20
+ dataSaver: { enabled: false, debug: false, mode: 'auto', detectSlowConnection: false, respectBrowserPreference: false, promptAfterMs: 5000 },
13
21
  footerNavigation: [],
14
22
  footer: '',
15
23
  seo: { author: '', keywords: [], image: '', robots: 'index,follow' },
@@ -21,10 +29,7 @@ const defaults = {
21
29
  },
22
30
  browserPlugins: {
23
31
  consent: { enabled: true, proposedDate: '', effectiveDate: '', expiresDays: 180 },
24
- tracking: { enabled: true, services: [] },
25
- statistics: { enabled: true, services: [] },
26
- advertising: { enabled: true, services: [] },
27
- captcha: { enabled: true, services: [] }
32
+ services: []
28
33
  },
29
34
  paths: { content: 'content', static: 'static', theme: 'themes/default', output: 'dist', cache: '.edgepress' },
30
35
  permalink: '/:year/:month/:slug/',
@@ -77,15 +82,18 @@ function validateLocalizedServiceText(value, label, maxLength, locales) {
77
82
  }
78
83
 
79
84
  const integrationProviders = {
80
- 'google-tag-manager': { group: 'tracking', credential: 'containerId', pattern: /^GTM-[A-Z0-9]{4,20}$/i },
81
- 'meta-pixel': { group: 'tracking', credential: 'pixelId', pattern: /^[0-9]{6,20}$/ },
82
- 'cloudflare-web-analytics': { group: 'statistics', credential: 'token', pattern: /^[A-Fa-f0-9-]{20,64}$/ },
83
- 'google-analytics': { group: 'statistics', credential: 'measurementId', pattern: /^(?:G|GT)-[A-Z0-9]{4,24}$/i },
84
- 'baidu-tongji': { group: 'statistics', credential: 'siteId', pattern: /^[A-Fa-f0-9]{16,64}$/ },
85
- 'google-adsense': { group: 'advertising', credential: 'clientId', pattern: /^ca-pub-[0-9]{10,24}$/ },
86
- 'cloudflare-turnstile': { group: 'captcha', credential: 'siteKey', pattern: /^[A-Za-z0-9_-]{20,128}$/ },
87
- 'google-recaptcha': { group: 'captcha', credential: 'siteKey', pattern: /^[A-Za-z0-9_-]{20,256}$/ },
88
- 'hcaptcha': { group: 'captcha', credential: 'siteKey', pattern: /^[A-Za-z0-9_-]{20,256}$/ }
85
+ 'oembed': { credential: 'backendUrl', pattern: /^https:\/\/[^\s]+$/ },
86
+ 'external-widget': { credential: 'backendUrl', pattern: /^https:\/\/[^\s]+$/ },
87
+ 'external-api': { credential: 'backendUrl', pattern: /^https:\/\/[^\s]+$/ },
88
+ 'google-tag-manager': { credential: 'containerId', pattern: /^GTM-[A-Z0-9]{4,20}$/i },
89
+ 'meta-pixel': { credential: 'pixelId', pattern: /^[0-9]{6,20}$/ },
90
+ 'cloudflare-web-analytics': { credential: 'token', pattern: /^[A-Fa-f0-9-]{20,64}$/ },
91
+ 'google-analytics': { credential: 'measurementId', pattern: /^(?:G|GT)-[A-Z0-9]{4,24}$/i },
92
+ 'baidu-tongji': { credential: 'siteId', pattern: /^[A-Fa-f0-9]{16,64}$/ },
93
+ 'google-adsense': { credential: 'clientId', pattern: /^ca-pub-[0-9]{10,24}$/ },
94
+ 'cloudflare-turnstile': { credential: 'siteKey', pattern: /^[A-Za-z0-9_-]{20,128}$/ },
95
+ 'google-recaptcha': { credential: 'siteKey', pattern: /^[A-Za-z0-9_-]{20,256}$/ },
96
+ 'hcaptcha': { credential: 'siteKey', pattern: /^[A-Za-z0-9_-]{20,256}$/ }
89
97
  };
90
98
 
91
99
  async function readSiteYaml(root) {
@@ -101,11 +109,21 @@ async function readSiteYaml(root) {
101
109
  try { parsed = parseYaml(source.toString('utf8'), { uniqueKeys: true, maxAliasCount: 20 }) ?? {}; }
102
110
  catch (error) { throw new Error('Invalid YAML in config.yml: ' + error.message, { cause: error }); }
103
111
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('config.yml must contain an object');
104
- for (const key of Object.keys(parsed)) if (!['site', 'privacy', 'plugins'].includes(key)) throw new Error('Unsupported config.yml section: ' + key);
112
+ for (const key of Object.keys(parsed)) if (!['theme', 'site', 'privacy', 'plugins'].includes(key)) throw new Error('Unsupported config.yml section: ' + key);
113
+ if (parsed.theme !== undefined && (typeof parsed.theme !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(parsed.theme))) {
114
+ throw new Error('config.yml theme must be a lowercase theme name such as lumen');
115
+ }
105
116
  return parsed;
106
117
  }
107
118
 
108
119
  function validateSiteConfig(config) {
120
+ const dataSaver = config.site.dataSaver;
121
+ if (!dataSaver || typeof dataSaver !== 'object' || Array.isArray(dataSaver) || ['enabled', 'debug', 'detectSlowConnection', 'respectBrowserPreference'].some(key => typeof dataSaver[key] !== 'boolean') || !['auto', 'text', 'full'].includes(dataSaver.mode) || !Number.isInteger(dataSaver.promptAfterMs) || dataSaver.promptAfterMs < 100 || dataSaver.promptAfterMs > 60000 || Object.keys(dataSaver).some(key => !['enabled', 'debug', 'mode', 'detectSlowConnection', 'respectBrowserPreference', 'promptAfterMs'].includes(key))) throw new Error('Invalid site.dataSaver settings or promptAfterMs (100–60000)');
122
+ const externalLinks = config.site.externalLinks;
123
+ if (!externalLinks || typeof externalLinks !== 'object' || Array.isArray(externalLinks) || typeof externalLinks.enabled !== 'boolean' || !Array.isArray(externalLinks.trustedOrigins) || externalLinks.trustedOrigins.length > 100 || Object.keys(externalLinks).some(key => !['enabled', 'trustedOrigins'].includes(key))) throw new Error('site.externalLinks needs enabled and a trustedOrigins list');
124
+ if (externalLinks.trustedOrigins.some(origin => typeof origin !== 'string')) throw new Error('site.externalLinks.trustedOrigins must contain exact origins');
125
+ externalLinks.trustedOrigins = [...new Set(externalLinks.trustedOrigins.map(linkOrigin))];
126
+ if (externalLinks.enabled && !/^https?:\/\//i.test(config.site.url)) throw new Error('External link prompts need an HTTP(S) site.url');
109
127
  if (!config.site.seo || typeof config.site.seo !== 'object' || Array.isArray(config.site.seo)) throw new Error('site.seo must be an object');
110
128
  if (typeof config.site.footer !== 'string') throw new Error('site.footer must be a string');
111
129
  for (const field of ['author', 'image', 'robots']) {
@@ -139,7 +157,14 @@ function validateSiteConfig(config) {
139
157
  for (const key of Object.keys(components)) if (!componentKeys.includes(key)) throw new Error('Unsupported site.components option: ' + key);
140
158
  for (const key of componentKeys) if (typeof components[key] !== 'boolean') throw new Error('site.components.' + key + ' must be a boolean');
141
159
 
142
- function validateNavigation(items, field) {
160
+ const { author, archive } = config.site;
161
+ if (!author || typeof author !== 'object' || Array.isArray(author) || typeof author.name !== 'string' || author.name.length > 160 ||
162
+ Object.keys(author).some(key => !['name', 'avatar'].includes(key))) throw new Error('site.author needs name and an optional avatar');
163
+ safeAvatar(author.avatar, 'site.author.avatar');
164
+ if (!archive || typeof archive !== 'object' || Array.isArray(archive) || Object.keys(archive).some(key => key !== 'categories')) throw new Error('site.archive needs categories');
165
+ categoryFilter(archive.categories);
166
+
167
+ function validateNavigation(items, field, depth = 0) {
143
168
  if (!Array.isArray(items) || items.length > 30) throw new Error(field + ' must be an array with at most 30 items');
144
169
  const keys = new Set();
145
170
  for (const item of items) {
@@ -148,7 +173,12 @@ function validateSiteConfig(config) {
148
173
  throw new Error(field + ' items need a unique key and URL');
149
174
  }
150
175
  keys.add(item.key);
151
- for (const key of Object.keys(item)) if (!['key', 'url', 'labels'].includes(key)) throw new Error('Unsupported ' + field + ' item option: ' + key);
176
+ if (item.children !== undefined) {
177
+ if (depth || !item.children.length) throw new Error('Navigation children allow one non-empty level');
178
+ validateNavigation(item.children, field + '.children', depth + 1);
179
+ }
180
+ for (const key of Object.keys(item)) if (!['key', 'url', 'labels', 'children', 'target'].includes(key)) throw new Error('Unsupported ' + field + ' item option: ' + key);
181
+ if (item.target !== undefined && !['_self','_blank'].includes(item.target)) throw new Error(field + '.target must be _self or _blank');
152
182
  const url = item.url.trim();
153
183
  if (!['@home', '@archives', '@feed', '@search'].includes(url)) {
154
184
  const safeLocal = url.startsWith('/') && !url.startsWith('//') && !/[\\\x00-\x20]/.test(url) && !/^(?:javascript|data|vbscript):/i.test(url);
@@ -199,29 +229,26 @@ function normalizeBrowserPlugins(value) {
199
229
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('config.yml plugins must be an object');
200
230
  for (const key of Object.keys(value)) {
201
231
  if (key !== 'consent') {
202
- throw new Error('Unsupported plugin configuration group: ' + key + '; place browser services under plugins.consent.' + key);
232
+ throw new Error('Unsupported plugin configuration group: ' + key + '; place browser services under plugins.consent.services');
203
233
  }
204
234
  }
205
235
  if (value.consent === undefined) return {};
206
236
  const input = value.consent;
207
237
  if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('plugins.consent must be an object');
208
238
  const consent = {};
209
- const normalized = {};
210
- const groups = ['tracking', 'statistics', 'advertising', 'captcha'];
211
- for (const [key, section] of Object.entries(input)) {
212
- if (groups.includes(key)) normalized[key] = section;
213
- else if (['enabled', 'proposedDate', 'effectiveDate', 'expiresDays'].includes(key)) consent[key] = section;
214
- else throw new Error('Unsupported plugins.consent option: ' + key);
215
- }
216
- normalized.consent = consent;
217
- return normalized;
239
+ const allowed = ['enabled', 'proposedDate', 'effectiveDate', 'expiresDays', 'services'];
240
+ for (const [key, option] of Object.entries(input)) {
241
+ if (!allowed.includes(key)) throw new Error('Unsupported plugins.consent option: ' + key + '; configure individual services in plugins.consent.services');
242
+ if (key !== 'services') consent[key] = option;
243
+ }
244
+ return {consent, services:input.services ?? []};
218
245
  }
219
246
 
220
247
  function validateBrowserPlugins(config) {
221
248
  const plugins = config.browserPlugins;
222
- const allowedGroups = ['consent', 'tracking', 'statistics', 'advertising', 'captcha'];
249
+ const allowedKeys = ['consent', 'services'];
223
250
  if (!plugins || typeof plugins !== 'object' || Array.isArray(plugins)) throw new Error('plugins in config.yml must be an object');
224
- for (const key of Object.keys(plugins)) if (!allowedGroups.includes(key)) throw new Error('Unsupported plugin configuration group: ' + key);
251
+ for (const key of Object.keys(plugins)) if (!allowedKeys.includes(key)) throw new Error('Unsupported plugin configuration group: ' + key);
225
252
  const consent = plugins.consent;
226
253
  if (!consent || typeof consent !== 'object' || Array.isArray(consent) || typeof consent.enabled !== 'boolean' ||
227
254
  !isDateOnly(consent.proposedDate) ||
@@ -237,43 +264,65 @@ function validateBrowserPlugins(config) {
237
264
  throw new Error('Unsupported plugins.consent option: ' + key);
238
265
  }
239
266
  const ids = new Set();
240
- let servicesCount = 0;
241
- for (const group of ['tracking', 'statistics', 'advertising', 'captcha']) {
242
- const groupPath = 'plugins.consent.' + group;
243
- const section = plugins[group];
244
- if (!section || typeof section !== 'object' || Array.isArray(section) || typeof section.enabled !== 'boolean' || !Array.isArray(section.services)) {
245
- throw new Error(groupPath + ' needs enabled and a services array');
267
+ if (!Array.isArray(plugins.services)) throw new Error('plugins.consent.services must be an array');
268
+ const servicesCount = plugins.services.filter(service => service?.enabled !== false).length;
269
+ for (const service of plugins.services) {
270
+ validateServiceCsp(service || {});
271
+ if (!service || typeof service !== 'object' || Array.isArray(service)) throw new Error('Each plugins.consent.services item must be an object');
272
+ if (service.enabled !== undefined && typeof service.enabled !== 'boolean') throw new Error('Service enabled must be a boolean');
273
+ const { id, provider, purpose } = service;
274
+ if (typeof id !== 'string' || !/^[a-z][a-z0-9-]{1,63}$/.test(id) || ids.has(id)) throw new Error('Each configured service needs a unique lowercase id');
275
+ ids.add(id);
276
+ const definition = integrationProviders[provider];
277
+ if (!definition) throw new Error('Unsupported service provider: ' + provider);
278
+ const locales = config.i18n.locales;
279
+ validateLocalizedServiceText(service.name || provider, 'Service ' + id + '.name', 120, locales);
280
+ validateLocalizedServiceText(purpose, 'Service ' + id + '.purpose', 500, locales);
281
+ validateLocalizedServiceText(service.dataCategories, 'Service ' + id + '.dataCategories', 600, locales);
282
+ validateLocalizedServiceText(service.recipient, 'Service ' + id + '.recipient', 200, locales);
283
+ validateLocalizedServiceText(service.retention, 'Service ' + id + '.retention', 300, locales);
284
+ if (typeof service.privacyUrl !== 'string' || service.privacyUrl.length > 2048 || /[\x00-\x20]/.test(service.privacyUrl)) {
285
+ throw new Error('Service ' + id + '.privacyUrl must be a safe HTTPS URL');
246
286
  }
247
- for (const key of Object.keys(section)) if (!['enabled', 'services'].includes(key)) throw new Error('Unsupported ' + groupPath + ' option: ' + key);
248
- if (!section.enabled && section.services.length) throw new Error('Clear ' + groupPath + '.services or enable the plugin group');
249
- servicesCount += section.services.length;
250
- for (const service of section.services) {
251
- if (!service || typeof service !== 'object' || Array.isArray(service)) throw new Error('Each ' + groupPath + '.services item must be an object');
252
- const { id, provider, purpose } = service;
253
- if (typeof id !== 'string' || !/^[a-z][a-z0-9-]{1,63}$/.test(id) || ids.has(id)) throw new Error('Each configured service needs a unique lowercase id');
254
- ids.add(id);
255
- const definition = integrationProviders[provider];
256
- if (!definition || definition.group !== group) throw new Error('Unsupported provider for ' + groupPath + ': ' + provider);
257
- const locales = config.i18n.locales;
258
- validateLocalizedServiceText(service.name || provider, 'Service ' + id + '.name', 120, locales);
259
- validateLocalizedServiceText(purpose, 'Service ' + id + '.purpose', 500, locales);
260
- validateLocalizedServiceText(service.dataCategories, 'Service ' + id + '.dataCategories', 600, locales);
261
- validateLocalizedServiceText(service.recipient, 'Service ' + id + '.recipient', 200, locales);
262
- validateLocalizedServiceText(service.retention, 'Service ' + id + '.retention', 300, locales);
263
- if (typeof service.privacyUrl !== 'string' || service.privacyUrl.length > 2048 || /[\x00-\x20]/.test(service.privacyUrl)) {
264
- throw new Error('Service ' + id + '.privacyUrl must be a safe HTTPS URL');
287
+ let safePrivacyUrl = false;
288
+ try {
289
+ const parsed = new URL(service.privacyUrl);
290
+ safePrivacyUrl = parsed.protocol === 'https:' && !parsed.username && !parsed.password;
291
+ } catch { safePrivacyUrl = false; }
292
+ if (!safePrivacyUrl) throw new Error('Service ' + id + '.privacyUrl must be a safe HTTPS URL');
293
+ const allowedKeys = new Set(['id', 'enabled', 'provider', 'name', 'purpose', 'dataCategories', 'recipient', 'retention', 'privacyUrl', 'csp', ...(definition.credential ? [definition.credential] : []), ...(provider === 'external-widget' ? ['moduleUrl', 'placement'] : []), ...(provider === 'oembed' ? ['runtimeApi','oembedEndpoint','embedTemplate','embedPathPattern','sourceOrigins','embedOrigins','embedScripts'] : [])]);
294
+ for (const key of Object.keys(service)) if (!allowedKeys.has(key)) throw new Error('Unsupported option for service ' + id + ': ' + key);
295
+ if (provider === 'external-widget' || provider === 'external-api' || provider === 'oembed') {
296
+ let valid=false;try {const url=new URL(service.backendUrl);valid=url.protocol==='https:' && !url.username && !url.password && !url.search && !url.hash;}catch{}
297
+ if(!valid)throw new Error('Service '+id+' needs an exact HTTPS backendUrl origin');
298
+ if(provider === 'oembed') {
299
+ for(const field of ['sourceOrigins','embedOrigins','embedScripts']) {
300
+ if(service[field] === undefined)continue;
301
+ if(!Array.isArray(service[field]) || service[field].length>24)throw new Error('Service '+id+'.'+field+' must be a URL array');
302
+ for(const value of service[field]) {
303
+ let safe=false;try{const url=new URL(value);safe=url.protocol==='https:'&&!url.username&&!url.password&&!url.hash&&(field==='embedScripts'?!url.search:url.origin===value);}catch{}
304
+ if(!safe)throw new Error('Service '+id+'.'+field+' needs exact HTTPS '+(field==='embedScripts'?'script URLs':'origins'));
305
+ }
306
+ }
307
+ if(service.oembedEndpoint !== undefined) {
308
+ let safe=false;try{const url=new URL(service.oembedEndpoint);safe=url.protocol==='https:'&&!url.username&&!url.password&&!url.hash;}catch{}
309
+ if(!safe)throw new Error('Service '+id+' needs a public HTTPS oembedEndpoint');
310
+ }
311
+ if(service.embedTemplate !== undefined) {
312
+ let safe=false;try{const url=new URL(service.embedTemplate.replaceAll('{url}','https%3A%2F%2Fexample.com').replaceAll('{path}','/p/example/').replaceAll('{id}','example'));safe=url.protocol==='https:'&&!url.username&&!url.password&&!url.hash&&(service.embedOrigins || [new URL(service.backendUrl).origin]).includes(url.origin);}catch{}
313
+ if(!safe || service.oembedEndpoint || service.embedScripts?.length)throw new Error('embedTemplate needs an allowed HTTPS iframe origin and cannot use oembedEndpoint or scripts');
314
+ if(service.embedPathPattern !== undefined){if(typeof service.embedPathPattern!=='string'||service.embedPathPattern.length>200)throw new Error('embedPathPattern must be a short path expression');new RegExp(service.embedPathPattern);}
315
+ if(service.embedTemplate.includes('{id}')&&!service.embedPathPattern)throw new Error('The {id} template requires embedPathPattern with a capture group');
316
+ }else if(!service.oembedEndpoint&&!service.embedTemplate&&!service.runtimeApi&&(service.sourceOrigins || service.embedOrigins || service.embedScripts))throw new Error('Third-party oEmbed options require oembedEndpoint, embedTemplate or runtimeApi');
317
+ }
318
+ if (provider === 'external-widget') {
319
+ const module = new URL(service.moduleUrl);
320
+ if (module.protocol !== 'https:' || module.username || module.password || module.origin !== new URL(service.backendUrl).origin || !module.pathname.endsWith('.js') || module.hash) throw new Error('Widget moduleUrl must be a same-service HTTPS JavaScript URL');
321
+ if (service.placement !== undefined && service.placement !== 'posts') throw new Error('Widget placement supports posts');
265
322
  }
266
- let safePrivacyUrl = false;
267
- try {
268
- const parsed = new URL(service.privacyUrl);
269
- safePrivacyUrl = parsed.protocol === 'https:' && !parsed.username && !parsed.password;
270
- } catch { safePrivacyUrl = false; }
271
- if (!safePrivacyUrl) throw new Error('Service ' + id + '.privacyUrl must be a safe HTTPS URL');
272
- const allowedKeys = new Set(['id', 'provider', 'name', 'purpose', 'dataCategories', 'recipient', 'retention', 'privacyUrl', definition.credential]);
273
- for (const key of Object.keys(service)) if (!allowedKeys.has(key)) throw new Error('Unsupported option for service ' + id + ': ' + key);
274
- const credential = service[definition.credential];
275
- if (typeof credential !== 'string' || !definition.pattern.test(credential)) throw new Error('Service ' + id + ' needs a valid public ' + definition.credential);
276
323
  }
324
+ const credential = service[definition.credential];
325
+ if (definition.credential && (typeof credential !== 'string' || !definition.pattern.test(credential))) throw new Error('Service ' + id + ' needs a valid public ' + definition.credential);
277
326
  }
278
327
  if (servicesCount && !consent.enabled) throw new Error('plugins.consent must be enabled when browser services are configured');
279
328
  if (servicesCount) {
@@ -342,6 +391,7 @@ export async function loadConfig(root = process.cwd()) {
342
391
  }
343
392
 
344
393
  const config = merge(defaults, userConfig);
394
+ validateTimeZone(config.site.timeZone);
345
395
  const siteYaml = await readSiteYaml(root);
346
396
  if (siteYaml.site !== undefined && (!siteYaml.site || typeof siteYaml.site !== 'object' || Array.isArray(siteYaml.site))) {
347
397
  throw new Error('config.yml site must be an object');
@@ -355,6 +405,7 @@ export async function loadConfig(root = process.cwd()) {
355
405
  config.site = merge(config.site, siteYaml.site);
356
406
  config.privacy = merge(config.privacy, siteYaml.privacy);
357
407
  config.browserPlugins = merge(config.browserPlugins, normalizeBrowserPlugins(siteYaml.plugins));
408
+ if (siteYaml.theme !== undefined) config.paths.theme = 'themes/' + siteYaml.theme;
358
409
  assertString(config.site.title, 'site.title');
359
410
  assertString(config.site.language, 'site.language');
360
411
  if (!isCanonicalLocale(config.site.language)) throw new Error('site.language must be a valid canonical language tag');
@@ -0,0 +1,137 @@
1
+ import { dataSaverInlineAssets } from './data-saver.js';
2
+
3
+ // Network permission is not consent: browser loaders still require the visitor's choice.
4
+ const defaults = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; media-src 'self' blob:; connect-src 'self'; frame-src 'self'; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'";
5
+ const providers = {
6
+ 'cloudflare-web-analytics': {script:['https://static.cloudflareinsights.com'],connect:['https://cloudflareinsights.com']},
7
+ 'google-analytics': {script:['https://www.googletagmanager.com'],connect:['https://*.google-analytics.com','https://*.analytics.google.com','https://www.googletagmanager.com'],img:['https://*.google-analytics.com']},
8
+ 'baidu-tongji': {script:['https://hm.baidu.com'],connect:['https://hm.baidu.com'],img:['https://hm.baidu.com']},
9
+ 'google-tag-manager': {script:['https://www.googletagmanager.com'],connect:['https://www.googletagmanager.com']},
10
+ 'meta-pixel': {script:['https://connect.facebook.net'],connect:['https://www.facebook.com'],img:['https://www.facebook.com']},
11
+ 'cloudflare-turnstile': {script:['https://challenges.cloudflare.com'],connect:['https://challenges.cloudflare.com'],frame:['https://challenges.cloudflare.com']},
12
+ 'google-recaptcha': {script:['https://www.google.com','https://www.gstatic.com'],connect:['https://www.google.com','https://www.gstatic.com'],frame:['https://www.google.com','https://recaptcha.google.com']},
13
+ 'hcaptcha': {script:['https://js.hcaptcha.com','https://*.hcaptcha.com'],connect:['https://hcaptcha.com','https://*.hcaptcha.com'],frame:['https://hcaptcha.com','https://*.hcaptcha.com'],style:['https://*.hcaptcha.com']},
14
+ 'google-adsense': {script:['https://pagead2.googlesyndication.com','https://*.googlesyndication.com','https://*.doubleclick.net'],connect:['https://*.googlesyndication.com','https://*.doubleclick.net','https://www.google.com'],frame:['https://*.googlesyndication.com','https://*.doubleclick.net','https://www.google.com'],img:['https://*.googlesyndication.com','https://*.doubleclick.net','https://www.google.com']}
15
+ };
16
+ const directiveNames = new Set(['script-src','connect-src','frame-src','img-src','style-src','font-src','media-src','worker-src']);
17
+ export function validateServiceCsp(service) {
18
+ if (service.csp === undefined) return;
19
+ if (!service.csp || typeof service.csp !== 'object' || Array.isArray(service.csp)) throw new Error('Service csp must be a directive map');
20
+ for (const [name,sources] of Object.entries(service.csp)) {
21
+ if (!directiveNames.has(name) || !Array.isArray(sources) || sources.length > 24 || sources.some(source => typeof source !== 'string' || !/^https:\/\/(?:\*\.)?[a-z0-9.-]+(?::[0-9]{1,5})?$/i.test(source))) throw new Error('Service csp needs supported directives and HTTPS origins');
22
+ }
23
+ }
24
+ function permissions(config) {
25
+ const result = new Map();
26
+ const add=(name,values)=>result.set(name,[...new Set([...(result.get(name)||[]),...values])]);
27
+ for (const service of config.browserPlugins?.services || []) {
28
+ if (service.enabled === false || config.browserPlugins?.consent?.enabled === false) continue;
29
+ for (const [name,values] of Object.entries(providers[service.provider] || {})) add(name+'-src',values);
30
+ if (['external-widget','external-api','oembed'].includes(service.provider)) {
31
+ const backend=new URL(service.backendUrl).origin;
32
+ add('connect-src',[backend]);
33
+ if (service.provider==='external-widget') {add('script-src',[new URL(service.moduleUrl).origin]);add('frame-src',[backend]);}
34
+ if (service.provider==='oembed') {
35
+ const media=service.embedOrigins || [backend];
36
+ for (const name of ['frame-src','img-src','connect-src','media-src']) add(name,media);
37
+ if (service.embedScripts?.length) {add('script-src',[...service.embedScripts.map(value=>new URL(value).origin),...media]);add('style-src',media);}
38
+ }
39
+ }
40
+ validateServiceCsp(service);
41
+ for(const [name,values] of Object.entries(service.csp || {})) add(name,values);
42
+ }
43
+ for(const [name,values] of Object.entries(config.contentCsp || {})) add(name,values);
44
+ return result;
45
+ }
46
+ function extend(policy,config) {
47
+ const directives=new Map(policy.split(';').map(value=>value.trim().split(/\s+/)).filter(parts=>parts[0]).map(([name,...sources])=>[name,sources]));
48
+ const inline = dataSaverInlineAssets(config);
49
+ if (inline) {
50
+ for (const name of ['script-src', ...(directives.has('script-src-elem') ? ['script-src-elem'] : [])]) {
51
+ const inherited = directives.get(name) || directives.get('script-src') || directives.get('default-src') || ["'self'"];
52
+ directives.set(name, [...new Set([...inherited.filter(value => value !== "'none'"), inline.scriptHash])]);
53
+ }
54
+ for (const name of ['style-src', ...(directives.has('style-src-elem') ? ['style-src-elem'] : [])]) {
55
+ const inherited = directives.get(name) || directives.get('style-src') || directives.get('default-src') || ["'self'"];
56
+ if (!inherited.includes("'unsafe-inline'")) directives.set(name, [...new Set([...inherited.filter(value => value !== "'none'"), inline.styleHash])]);
57
+ }
58
+ }
59
+ for(const [name,sources] of permissions(config)) {
60
+ if (!sources.length) continue;
61
+ const inherited=directives.get(name) || directives.get('default-src') || ["'self'"];
62
+ directives.set(name,[...new Set([...inherited.filter(source=>source!=="'none'"),...sources])]);
63
+ }
64
+ return [...directives].map(([name,sources])=>name+' '+sources.join(' ')).join('; ');
65
+ }
66
+ export function extendConsentPolicy(headers,config) {
67
+ if (!globalPolicy(headers)) headers += '\n\n/*\n Content-Security-Policy: '+defaults;
68
+ return headers.replace(/(Content-Security-Policy:\s*)([^\r\n]+)/gi,(_match,prefix,policy)=>prefix+extend(policy,config));
69
+ }
70
+ // Content images/videos are independent of optional services. Inert embed metadata
71
+ // must never expand the allowlist, especially when a service has been disabled.
72
+ export function contentPermissions(routes) {
73
+ const result={};
74
+ for(const route of routes) {
75
+ if(route.contentType && !/^text\/html\b/i.test(route.contentType))continue;
76
+ const html=route.body.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi,'').replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi,'');
77
+ if (/data-edgepress-document(?:\s|=|>)/i.test(html)) {
78
+ result['script-src'] = ["'wasm-unsafe-eval'"];
79
+ result['font-src'] = ['blob:'];
80
+ }
81
+ for(const match of html.matchAll(/<(img|video|audio|source)\b[^>]*\bsrc\s*=\s*["'](https:\/\/[^"']+)["']/gi)) {
82
+ try {const origin=new URL(match[2]).origin;const name=match[1].toLowerCase()==='img'?'img-src':'media-src';result[name]=[...new Set([...(result[name]||[]),origin])];}catch{}
83
+ }
84
+ for(const match of html.matchAll(/<img\b[^>]*\bdata-original\s*=\s*["'](https:\/\/[^"']+)["']/gi)){try{const origin=new URL(match[1]).origin;result['img-src']=[...new Set([...(result['img-src']||[]),origin])];}catch{}}
85
+ }
86
+ return result;
87
+ }
88
+ export function injectConsentPolicy(html,headers) {
89
+ if (!/^\s*(?:<!doctype html>\s*)?<html\b/i.test(html) || !/<head\b/i.test(html)) return html;
90
+ // Use the global source policy when provided (e.g. first-party upload endpoints).
91
+ let policy=globalPolicy(headers);
92
+ if (!policy) return html.replace(/(<head\b[^>]*>)([\s\S]*?)<\/head>/i,(_match,head,content)=>head+'\n<meta charset="utf-8">'+content.replace(/<meta\b[^>]*\bcharset\s*=\s*["']?[^\s"'>]+["']?[^>]*>/gi,'')+'</head>');
93
+ policy=policy.split(';').filter(part=>!/^\s*(?:frame-ancestors|sandbox|report-uri|report-to)\b/i.test(part)).join(';');
94
+ const escaped=policy.replaceAll('&','&amp;').replaceAll('"','&quot;').replaceAll('<','&lt;');
95
+ return html.replace(/(<head\b[^>]*>)([\s\S]*?)<\/head>/i,(_match,head,content)=>head+'\n<meta charset="utf-8">\n<meta http-equiv="Content-Security-Policy" content="'+escaped+'">'+content.replace(/<meta\b[^>]*\bcharset\s*=\s*["']?[^\s"'>]+["']?[^>]*>/gi,'')+'</head>');
96
+ }
97
+ function globalPolicy(headers) {
98
+ let scope='';
99
+ for(const line of headers.split(/\r?\n/)){if(line&&!/^\s/.test(line))scope=line.trim();else if(scope==='/*'&&/^\s*Content-Security-Policy:/i.test(line))return line.replace(/^\s*Content-Security-Policy:\s*/i,'');}
100
+ }
101
+ // Static hosts join repeated header values with commas. CSP treats each value
102
+ // as a separate policy, so first expand default fallbacks, then partition only
103
+ // disjoint directives. Splitting an origin list would incorrectly intersect it.
104
+ export function serializeConsentHeaders(headers) {
105
+ return headers.replace(/^([ \t]*Content-Security-Policy:[ \t]*)([^\r\n]+)$/gim,(line,prefix,policy)=>{
106
+ if(line.length<=2000)return line;
107
+ const directives=new Map(policy.split(';').map(part=>part.trim().split(/\s+/)).filter(parts=>parts[0]).map(([name,...sources])=>[name,sources]));
108
+ const fallback=directives.get('default-src');
109
+ if(fallback) {
110
+ for(const name of ['script-src','style-src','img-src','connect-src','font-src','media-src','frame-src','worker-src','manifest-src','child-src','object-src']) {
111
+ if(directives.has(name))continue;
112
+ const inherited=name==='worker-src'?(directives.get('child-src')||directives.get('script-src')||fallback):name==='frame-src'?(directives.get('child-src')||fallback):fallback;
113
+ directives.set(name,inherited);
114
+ }
115
+ directives.delete('default-src');
116
+ }
117
+ // child-src only supplies frame/worker fallbacks. Separate policies would
118
+ // apply those fallbacks again and incorrectly block explicit permissions.
119
+ const child=directives.get('child-src');
120
+ if(child){if(!directives.has('frame-src'))directives.set('frame-src',child);if(!directives.has('worker-src'))directives.set('worker-src',child);directives.delete('child-src');}
121
+ const groups=[];
122
+ for(const names of [['script-src','script-src-elem','script-src-attr','worker-src'],['style-src','style-src-elem','style-src-attr']]) {
123
+ const parts=[];
124
+ for(const name of names){if(directives.has(name)){parts.push(name+' '+directives.get(name).join(' '));directives.delete(name);}}
125
+ if(parts.length)groups.push(parts.join('; '));
126
+ }
127
+ for(const [name,sources] of directives)groups.push(name+' '+sources.join(' '));
128
+ const chunks=[];let chunk='';
129
+ for(const directive of groups) {
130
+ if(prefix.length+directive.length>2000)throw new Error('CSP directive group exceeds the static host line limit; reduce its configured origins.');
131
+ if(prefix.length+chunk.length+(chunk?2:0)+directive.length>2000){chunks.push(prefix+chunk);chunk='';}
132
+ chunk+=(chunk?'; ':'')+directive;
133
+ }
134
+ if(chunk)chunks.push(prefix+chunk);
135
+ return chunks.join('\n');
136
+ });
137
+ }