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/generators.js CHANGED
@@ -1,7 +1,11 @@
1
+ import {renderPostServices,hasServiceBlock} from './service-block.js';
2
+ import {generateRssRoute} from './rss.js';
3
+ import {renderRevisionCards} from './revision-cards.js';
4
+ import { authorForPost, readingMinutes, filterPostsByCategory, categorySlug, normalizeCategory } from './post-details.js';
1
5
  import { plainText, renderMarkdownExcerpt } from './markdown.js';
2
6
  import { renderBlocks } from './page-blocks.js';
3
7
  import { renderLayout } from './theme.js';
4
- import { localizedUrl, translate } from './i18n.js';
8
+ import { localizedContentUrl, localizedUrl, translate } from './i18n.js';
5
9
  import { postsForLocale, slugify, sortPostsNewest } from './content.js';
6
10
  import { mapLimit } from './concurrency.js';
7
11
  import { renderIcon } from './icons.js';
@@ -23,13 +27,8 @@ function urlFor(path) {
23
27
  return '/' + parts.join('/') + (String(path).endsWith('/') && parts.length ? '/' : '');
24
28
  }
25
29
 
26
- function tagSlug(tag) {
27
- if (/[^\x00-\x7F]/.test(tag)) return 'tag-' + Array.from(tag).map((char) => char.codePointAt(0).toString(16)).join('-');
28
- return slugify(tag);
29
- }
30
-
31
- function dateLabel(date, language) {
32
- return new Intl.DateTimeFormat(language, { dateStyle: 'long', timeZone: 'UTC' }).format(date);
30
+ function dateLabel(date, language, timeZone = 'UTC', dateOnly = true) {
31
+ return new Intl.DateTimeFormat(language, dateOnly?{dateStyle:'long',timeZone}:{year:'numeric',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit',timeZoneName:'short',timeZone}).format(date);
33
32
  }
34
33
 
35
34
  function fileRoute(path, body, contentType = 'text/html; charset=utf-8') {
@@ -87,7 +86,7 @@ function addPostContents(html, locale, config) {
87
86
  function renderPostVideo(video, locale) {
88
87
  if (!video) return '';
89
88
  const videoTitle = escapeHtml(video.title);
90
- return '<figure class="post-video"><video controls playsinline preload="metadata" poster="' + escapeHtml(video.poster) + '" aria-label="' +
89
+ return '<figure class="post-video"><video data-edgepress-video controls playsinline preload="none" poster="' + escapeHtml(video.poster) + '" aria-label="' +
91
90
  escapeHtml(video.alt) + '"><source src="' + escapeHtml(video.src) + '" type="video/mp4"><track kind="captions" src="' +
92
91
  escapeHtml(video.captions) + '" srclang="' + escapeHtml(video.captionLanguage) + '" label="' + escapeHtml(video.captionLabel) + '" default>' +
93
92
  '<a href="' + escapeHtml(video.src) + '">' + (locale.toLowerCase().startsWith('zh') ? '下载 MP4 视频' : 'Download the MP4 video') +
@@ -128,16 +127,50 @@ function renderPostAuthor(post, locale, config) {
128
127
  }
129
128
 
130
129
  async function renderPostCard(post, locale, config, showLanguage = false) {
131
- const summary = await renderMarkdownExcerpt(post.description || post.markdown, config.markdown);
130
+ const summary = await renderMarkdownExcerpt(post.description || post.markdown, {...config.markdown,linkResolver:value=>localizedContentUrl(config,post.locale,value)});
131
+ const postLanguage = showLanguage && post.locale !== locale ? translate(config, post.locale, 'languageName') : '';
132
132
  return '<article class="post-card" lang="' + escapeHtml(post.locale) + '"><h2><a href="' + escapeHtml(urlFor(post.path)) + '">' + escapeHtml(post.title) + '</a></h2>' +
133
- '<p class="meta"><time datetime="' + post.date.toISOString() + '">' + escapeHtml(dateLabel(post.date, post.locale)) + '</time>' +
134
- (showLanguage ? ' · <span class="post-language" lang="' + escapeHtml(post.locale) + '">' + escapeHtml(post.locale) + '</span>' : '') +
135
- (post.author ? ' · ' + renderPostAuthor(post, locale, config) : '') + '</p><div class="post-excerpt" lang="' + escapeHtml(post.locale) + '">' +
133
+ '<p class="meta"><time data-local-time data-time-locale="' + escapeHtml(post.locale || locale) + '" data-date-only="' + (post.dateOnly === true) + '" datetime="' + (post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString()) + '">' + escapeHtml(dateLabel(post.date, post.locale || locale, post.dateOnly ? 'UTC' : config.site.timeZone, post.dateOnly)) + '</time>' +
134
+ (postLanguage ? ' <span class="post-language" lang="' + escapeHtml(post.locale) + '">' + escapeHtml(postLanguage) + '</span>' : '') +
135
+ (post.author ? ' ' + renderPostAuthor(post, locale, config) : '') + '</p><div class="post-excerpt" lang="' + escapeHtml(post.locale) + '">' +
136
136
  summary + '</div></article>';
137
137
  }
138
138
 
139
+ function paginationPageNumbers(currentPage, totalPages) {
140
+ const visible = new Set([1, totalPages]);
141
+ for (let page = Math.max(1, currentPage - 2); page <= Math.min(totalPages, currentPage + 2); page += 1) visible.add(page);
142
+ const pages = [...visible].sort((left, right) => left - right);
143
+ const entries = [];
144
+ for (let index = 0; index < pages.length; index += 1) {
145
+ const page = pages[index];
146
+ const previous = pages[index - 1];
147
+ if (previous !== undefined && page - previous > 1) {
148
+ if (page - previous === 2) entries.push(previous + 1);
149
+ else entries.push(null);
150
+ }
151
+ entries.push(page);
152
+ }
153
+ return entries;
154
+ }
155
+
156
+ function renderPagination(locale, currentPage, totalPages, config, pageUrl) {
157
+ const label = (key) => translate(config, locale, key);
158
+ const pageLinks = paginationPageNumbers(currentPage, totalPages).map((page) => {
159
+ if (page === null) return '<li class="pagination-ellipsis" aria-hidden="true">…</li>';
160
+ if (page === currentPage) return '<li><span class="pagination-current" aria-current="page">' + page + '</span></li>';
161
+ const pageLabel = label('pageNumber').replace('{page}', String(page));
162
+ return '<li><a href="' + escapeHtml(pageUrl(page)) + '" aria-label="' + escapeHtml(pageLabel) + '">' + page + '</a></li>';
163
+ }).join('');
164
+ const previous = currentPage > 1
165
+ ? '<a rel="prev" href="' + escapeHtml(pageUrl(currentPage - 1)) + '">' + escapeHtml(label('newer')) + '</a>' : '';
166
+ const next = currentPage < totalPages
167
+ ? '<a rel="next" href="' + escapeHtml(pageUrl(currentPage + 1)) + '">' + escapeHtml(label('older')) + '</a>' : '';
168
+ return '<nav class="pagination" aria-label="' + escapeHtml(label('pagination')) + '">' + previous +
169
+ '<ol class="pagination-pages">' + pageLinks + '</ol>' + next + '</nav>';
170
+ }
171
+
139
172
  async function pageBodyForDocument(page, site, config, locale, isHomepage = false, renderContext = {}) {
140
- const content = await renderBlocks(page.blocks, { config, locale, site, isHomepage, ...renderContext });
173
+ const content = await renderBlocks(page.blocks, { config, locale, site, document: page, isHomepage, ...renderContext });
141
174
  const firstElement = page.blocks[0]?.cells?.[0]?.[0];
142
175
  const needsTitle = !isHomepage || firstElement?.type !== 'hero';
143
176
  const title = needsTitle ? '<header class="page-title"><h1>' + escapeHtml(page.title) + '</h1></header>' : '';
@@ -179,7 +212,8 @@ export async function generateBuiltinRoutes(site, config, extensions) {
179
212
  const allPosts = sortPostsNewest(site.posts);
180
213
  for (const locale of config.i18n.locales) {
181
214
  const posts = allPosts.filter((post) => post.locale === locale);
182
- const archivePosts = postsForLocale(allPosts, locale, defaultLocale);
215
+ const localizedPosts = postsForLocale(allPosts, locale, defaultLocale);
216
+ const archivePosts = filterPostsByCategory(localizedPosts, config.site.archive?.categories);
183
217
  const pages = site.pages.filter((page) => page.locale === locale && !page.homepage);
184
218
  const homePage = homepageDocument(site.pages, locale, defaultLocale);
185
219
  const latestPostsPageSize = paginatedLatestPostsSize(homePage);
@@ -220,12 +254,11 @@ export async function generateBuiltinRoutes(site, config, extensions) {
220
254
  const cards = await Promise.all(subset.map((post) => renderPostCard(post, locale, config, true)));
221
255
  const body = '<section class="intro"><h1>' + escapeHtml(config.site.title) + '</h1><p>' + escapeHtml(config.site.description) +
222
256
  '</p></section><section class="post-list" aria-label="' + escapeHtml(label('latestPosts')) + '">' +
223
- (cards.length ? cards.join('') : '<p>' + escapeHtml(label('noPosts')) + '</p>') +
257
+ (cards.length ? '<div class="post-list archive-post-list">' + cards.join('') + '</div>' : '<p>' + escapeHtml(label('noPosts')) + '</p>') +
224
258
  '</section>' + (pageNumber === 1 && pages.length ? '<section class="page-links"><h2>' + escapeHtml(label('pages')) + '</h2><ul>' +
225
259
  pages.map((page) => '<li><a href="' + escapeHtml(urlFor(page.path)) + '">' + escapeHtml(page.title) + '</a></li>').join('') + '</ul></section>' : '') +
226
- (totalPages > 1 ? '<nav class="pagination" aria-label="' + escapeHtml(label('pagination')) + '">' +
227
- (pageNumber > 1 ? '<a href="' + escapeHtml(pageNumber === 2 ? localizedUrl(config, locale, '') : localizedUrl(config, locale, 'page/' + (pageNumber - 1) + '/')) + '">' + escapeHtml(label('newer')) + '</a>' : '') +
228
- (pageNumber < totalPages ? '<a href="' + escapeHtml(localizedUrl(config, locale, 'page/' + (pageNumber + 1) + '/')) + '">' + escapeHtml(label('older')) + '</a>' : '') + '</nav>' : '');
260
+ (totalPages > 1 ? renderPagination(locale, pageNumber, totalPages, config, (page) =>
261
+ localizedUrl(config, locale, page === 1 ? '' : 'page/' + page + '/')) : '');
229
262
  const routePath = pageNumber === 1 ? prefix + 'index.html' : prefix + 'page/' + pageNumber + '/index.html';
230
263
  routes.push(await pageRoute(routePath, pageNumber === 1 ? config.site.title : 'Page ' + pageNumber,
231
264
  config.site.description, body, locale, config, extensions));
@@ -235,12 +268,18 @@ export async function generateBuiltinRoutes(site, config, extensions) {
235
268
  for (const post of posts) allPagePaths.add(urlFor(post.path));
236
269
  const postRoutes = await mapLimit(posts, config.concurrency, async (post) => {
237
270
  const renderedMarkdown = addPostContents(post.html, locale, config);
238
- const body = '<article class="post"><header><h1>' + escapeHtml(post.title) + '</h1><p class="meta"><time datetime="' + post.date.toISOString() + '">' +
239
- escapeHtml(dateLabel(post.date, locale)) + '</time>' + (post.author ? ' · ' + renderPostAuthor(post, locale, config) : '') +
240
- '</p></header>' + renderPostVideo(post.video, locale) + renderedMarkdown.toc + '<div class="post-content">' + renderedMarkdown.html + '</div>' +
241
- (post.tags.length ? '<nav class="post-tags" aria-label="' + escapeHtml(label('tags')) + '"><span class="post-tags-label">' + escapeHtml(label('tags')) + ':</span><ul>' +
242
- post.tags.map((tag) => '<li><a rel="tag" href="' + escapeHtml(localizedUrl(config, locale, 'tags/' + tagSlug(tag) + '/')) + '">' + escapeHtml(tag) + '</a></li>').join('') +
243
- '</ul></nav>' : '') + '</article>';
271
+ const author = authorForPost(post, config);
272
+ const minutes = post.readingMinutes || readingMinutes(post.markdown);
273
+ const authorHtml = author.name ? '<span class="post-author">' +
274
+ (author.avatar ? '<img class="author-avatar" src="' + escapeHtml(author.avatar) + '" width="40" height="40" alt="" decoding="async">' : '') +
275
+ escapeHtml(label('postAuthor').replace('{author}', author.name)) + '</span>' : '';
276
+ const tagsHtml = post.tags.length ? '<div class="post-tags" role="group" aria-label="' + escapeHtml(label('tags')) + '"><span class="post-tags-label">' +
277
+ escapeHtml(label('tags')) + ':</span>' + post.tags.map(tag => '<span class="post-tag">' + escapeHtml(tag) + '</span>').join('') + '</div>' : '';
278
+ const body = '<article class="post"><header><h1>' + escapeHtml(post.title) + '</h1><div class="meta post-byline">' + authorHtml +
279
+ '<time data-local-time data-time-locale="' + escapeHtml(post.locale || locale) + '" data-date-only="' + (post.dateOnly === true) + '" datetime="' + (post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString()) + '">' + escapeHtml(dateLabel(post.date, post.locale || locale, post.dateOnly ? 'UTC' : config.site.timeZone, post.dateOnly)) + '</time>' +
280
+ '<span class="post-reading-time" data-reading-minutes="' + minutes + '">' + escapeHtml(label('postReadingTime').replace('{minutes}', minutes)) + '</span>' +
281
+ (post.updated ? '<span class="post-updated">' + escapeHtml(label('postUpdated')) + ' <time data-local-time data-time-locale="' + escapeHtml(locale) + '" datetime="' + post.updated.toISOString() + '">' + escapeHtml(dateLabel(post.updated, locale, config.site.timeZone, false)) + '</time></span>' : '') +
282
+ '</div>' + tagsHtml + '</header>' + (post.showChanges && post.changes ? await renderRevisionCards(post.changes,label,post.revisionImages,{linkResolver:value=>localizedContentUrl(config,locale,value)}) : '') + renderPostVideo(post.video, locale) + renderedMarkdown.toc + '<div class="post-content">' + renderedMarkdown.html + '</div></article>' + renderPostServices(post, locale, config);
244
283
  const canonicalPath = urlFor(post.path);
245
284
  return pageRoute(post.path + 'index.html', post.title, post.description || plainText(post.markdown).slice(0, 160), body,
246
285
  locale, config, extensions, {
@@ -248,8 +287,8 @@ export async function generateBuiltinRoutes(site, config, extensions) {
248
287
  structuredData: {
249
288
  '@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.title,
250
289
  description: post.description || plainText(post.markdown).slice(0, 160),
251
- datePublished: post.date.toISOString(), dateModified: post.date.toISOString(),
252
- author: { '@type': 'Person', name: post.author || config.site.seo.author || config.site.title },
290
+ datePublished: post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString(), dateModified: post.updated ? post.updated.toISOString() : post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString(),
291
+ author: { '@type': 'Person', name: author.name || config.site.title },
253
292
  mainEntityOfPage: config.site.url ? config.site.url + canonicalPath : undefined,
254
293
  inLanguage: locale
255
294
  }
@@ -275,36 +314,41 @@ export async function generateBuiltinRoutes(site, config, extensions) {
275
314
  const cards = await Promise.all(subset.map((post) => renderPostCard(post, locale, config, true)));
276
315
  const archiveUrl = pageNumber === 1 ? 'archives/' : 'archives/page/' + pageNumber + '/';
277
316
  const pagination = archivePageCount > 1
278
- ? '<nav class="pagination" aria-label="' + escapeHtml(label('pagination')) + '">' +
279
- (pageNumber > 1 ? '<a rel="prev" href="' + escapeHtml(pageNumber === 2 ? localizedUrl(config, locale, 'archives/') :
280
- localizedUrl(config, locale, 'archives/page/' + (pageNumber - 1) + '/')) + '">' + escapeHtml(label('newer')) + '</a>' : '') +
281
- (pageNumber < archivePageCount ? '<a rel="next" href="' + escapeHtml(localizedUrl(config, locale, 'archives/page/' + (pageNumber + 1) + '/')) + '">' +
282
- escapeHtml(label('older')) + '</a>' : '') + '</nav>' : '';
317
+ ? renderPagination(locale, pageNumber, archivePageCount, config, (page) => localizedUrl(config, locale,
318
+ page === 1 ? 'archives/' : 'archives/page/' + page + '/')) : '';
283
319
  const archiveBody = '<section><h1>' + escapeHtml(label('archives')) + '</h1>' +
284
- (cards.length ? cards.join('') : '<p>' + escapeHtml(label('noPosts')) + '</p>') + pagination + '</section>';
320
+ (cards.length ? '<div class="post-list archive-post-list">' + cards.join('') + '</div>' : '<p>' + escapeHtml(label('noPosts')) + '</p>') + pagination + '</section>';
285
321
  routes.push(await pageRoute(prefix + (pageNumber === 1 ? 'archives/index.html' : 'archives/page/' + pageNumber + '/index.html'),
286
- pageNumber === 1 ? label('archives') : label('archives') + ' · ' + pageNumber, label('allPosts'), archiveBody, locale, config, extensions,
322
+ pageNumber === 1 ? label('archives') : label('archives') + ' ' + pageNumber, label('allPosts'), archiveBody, locale, config, extensions,
287
323
  { urlPath: localizedUrl(config, locale, archiveUrl) }));
288
324
  allPagePaths.add(localizedUrl(config, locale, archiveUrl));
289
325
  }
290
326
 
291
- const tags = new Map();
292
- for (const post of posts) for (const tag of post.tags) {
293
- const slug = tagSlug(tag);
294
- if (!tags.has(slug)) tags.set(slug, { label: tag, posts: [] });
295
- tags.get(slug).posts.push(post);
327
+ const categories = new Map();
328
+ for (const post of localizedPosts) {
329
+ const category = normalizeCategory(post.category);
330
+ if (!categories.has(category)) categories.set(category, []);
331
+ categories.get(category).push(post);
332
+ }
333
+ for (const [category, categoryPosts] of categories) {
334
+ const slug = categorySlug(category);
335
+ const title = category === 'blog' ? label('categoryBlog') : category === 'uncategorized' ? label('categoryUncategorized') : category;
336
+ const totalPages = Math.ceil(categoryPosts.length / config.pagination.perPage);
337
+ const categoryPath = number => 'categories/' + slug + '/' + (number === 1 ? '' : 'page/' + number + '/');
338
+ for (let number = 1; number <= totalPages; number += 1) {
339
+ const cards = await Promise.all(categoryPosts.slice((number - 1) * config.pagination.perPage, number * config.pagination.perPage)
340
+ .map(post => renderPostCard(post, locale, config, true)));
341
+ const pagination = totalPages > 1 ? renderPagination(locale, number, totalPages, config,
342
+ page => localizedUrl(config, locale, categoryPath(page))) : '';
343
+ const body = '<section><h1>' + escapeHtml(title) + '</h1><div class="post-list archive-post-list">' + cards.join('') + '</div>' + pagination + '</section>';
344
+ const urlPath = localizedUrl(config, locale, categoryPath(number));
345
+ allPagePaths.add(urlPath);
346
+ routes.push(await pageRoute(prefix + categoryPath(number) + 'index.html', title, title, body, locale, config, extensions, { urlPath }));
347
+ }
296
348
  }
297
- const tagRoutes = await mapLimit([...tags.entries()], config.concurrency, async ([slug, value]) => {
298
- const cards = await Promise.all(value.posts.map((post) => renderPostCard(post, locale, config)));
299
- const body = '<section><h1>' + escapeHtml(label('tag')) + ': ' + escapeHtml(value.label) + '</h1>' +
300
- cards.join('') + '</section>';
301
- return pageRoute(prefix + 'tags/' + slug + '/index.html', label('tag') + ': ' + value.label,
302
- label('tag') + ' ' + value.label, body, locale, config, extensions);
303
- });
304
- routes.push(...tagRoutes);
305
349
 
306
350
  const searchable = [
307
- ...posts.map((post) => ({ title: post.title, author: post.author, date: post.date.toISOString(), url: urlFor(post.path), summary: plainText(post.description || post.markdown).slice(0, 220), content: plainText(post.markdown), type: 'post' })),
351
+ ...posts.map((post) => ({ title: post.title, author: post.author, date: post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString(), dateOnly: post.dateOnly, url: urlFor(post.path), summary: plainText(post.description || post.markdown).slice(0, 220), content: plainText(post.markdown), type: 'post' })),
308
352
  ...pages.map((page) => ({ title: page.title, url: urlFor(page.path), summary: page.description || plainText(page.markdown).slice(0, 220), type: 'page' }))
309
353
  ];
310
354
  routes.push(fileRoute(prefix + 'search.json', JSON.stringify(searchable), 'application/json; charset=utf-8'));
@@ -313,21 +357,14 @@ export async function generateBuiltinRoutes(site, config, extensions) {
313
357
  '<p>' + escapeHtml(label('searchIntro')) + '</p><form class="local-search-form" data-edgepress-search data-index="' +
314
358
  escapeHtml(localizedUrl(config, locale, 'search.json')) + '"><label for="edgepress-search-query">' + escapeHtml(label('searchLabel')) +
315
359
  '</label><div class="local-search-controls"><input id="edgepress-search-query" name="q" type="search" autocomplete="off" ' +
316
- 'aria-describedby="edgepress-search-hint"><button type="submit">' + escapeHtml(label('searchButton')) + '</button></div>' +
317
- '<p id="edgepress-search-hint">' + escapeHtml(label('searchHint')) + '</p></form><p id="edgepress-search-status" role="status" aria-live="polite">' +
318
- escapeHtml(label('searchPrompt')) + '</p><ol id="edgepress-search-results" class="local-search-results"></ol>' +
360
+ '><button type="submit">' + escapeHtml(label('searchButton')) + '</button></div>' +
361
+ '</form><p id="edgepress-search-status" role="status" aria-live="polite">' +
362
+ '' + '</p><ol id="edgepress-search-results" class="local-search-results"></ol>' +
319
363
  '<noscript><p>' + escapeHtml(label('searchNeedsJavaScript')) + '</p></noscript></section>';
320
364
  routes.push(await pageRoute(prefix + 'search/index.html', label('searchPosts'), label('searchIntro'), searchBody,
321
365
  locale, config, extensions));
322
366
 
323
- const base = config.site.url;
324
- const entries = posts.slice(0, 20).map((post) => '<entry><title>' + escapeXml(post.title) + '</title>' +
325
- (post.author ? '<author><name>' + escapeXml(post.author) + '</name></author>' : '') + '<link href="' +
326
- escapeXml(base + urlFor(post.path)) + '"/><id>' + escapeXml(base + urlFor(post.path)) + '</id><updated>' + post.date.toISOString() +
327
- '</updated><summary>' + escapeXml(plainText(post.description || post.markdown).slice(0, 220)) + '</summary></entry>').join('');
328
- routes.push(fileRoute(prefix + 'feed.xml', '<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>' +
329
- escapeXml(config.site.title) + '</title><id>' + escapeXml(base || 'urn:edgepress:site') + '</id><updated>' +
330
- (posts[0]?.date.toISOString() || new Date(0).toISOString()) + '</updated>' + entries + '</feed>', 'application/atom+xml; charset=utf-8'));
367
+ routes.push(await generateRssRoute(posts, config, locale));
331
368
  }
332
369
 
333
370
  const base = config.site.url;
@@ -368,5 +405,10 @@ export async function generateBuiltinRoutes(site, config, extensions) {
368
405
  escapeHtml(translate(config, locale, 'returnHome')) + '</a></p></section>';
369
406
  routes.push(await pageRoute(prefix + '403.html', forbiddenTitle, forbiddenText, forbiddenBody, locale, config, extensions, { robots: 'noindex,nofollow' }));
370
407
  }
408
+ const discussions = config.browserPlugins.services.some(service => service.enabled !== false && service.provider === 'external-widget') ? [
409
+ ...site.posts.filter(post => !post.draft).map(post => ({thread:post.bundlePath,title:post.title})),
410
+ ...site.pages.filter(page => !page.draft && hasServiceBlock(page.blocks)).map(page => ({thread:'page:' + page.bundlePath,title:page.title}))
411
+ ] : [];
412
+ routes.push(fileRoute('edgepress/service-contexts.json', JSON.stringify([...new Map(discussions.map(item => [item.thread,item])).values()]), 'application/json; charset=utf-8'));
371
413
  return routes;
372
414
  }
package/src/i18n.js CHANGED
@@ -1,4 +1,5 @@
1
- import { readFile, realpath } from 'node:fs/promises';
1
+ import { readFile, realpath, stat } from 'node:fs/promises';
2
+ import {fileURLToPath} from 'node:url';
2
3
  import { isAbsolute, relative, resolve, sep } from 'node:path';
3
4
 
4
5
  const MAX_LANGUAGE_FILE_BYTES = 64 * 1024;
@@ -39,9 +40,16 @@ async function loadDictionary(root, directory, locale, label) {
39
40
  export async function loadLanguagePacks(config) {
40
41
  const baseLocale = config.i18n.defaultLocale;
41
42
  const translationsByLocale = Object.create(null);
42
- translationsByLocale[baseLocale] = await loadDictionary(config.root, 'languages/base', baseLocale, 'Base');
43
+ const builtinRoot=fileURLToPath(new URL('../',import.meta.url)),english=await loadDictionary(builtinRoot,'languages/base','en','Built-in');
44
+ async function defaults(locale){
45
+ if(locale==='en')return english;
46
+ const file=resolve(builtinRoot,'languages/packs',locale+'.json');
47
+ const exists=await stat(file).then(()=>true,error=>{if(error.code==='ENOENT')return false;throw error;});
48
+ return exists?{...english,...await loadDictionary(builtinRoot,'languages/packs',locale,'Built-in')}:english;
49
+ }
50
+ translationsByLocale[baseLocale] = {...await defaults(baseLocale),...await loadDictionary(config.root, 'languages/base', baseLocale, 'Base')};
43
51
  for (const locale of config.i18n.languagePacks) {
44
- translationsByLocale[locale] = await loadDictionary(config.root, 'languages/packs', locale, 'Language pack');
52
+ translationsByLocale[locale] = {...await defaults(locale),...await loadDictionary(config.root, 'languages/packs', locale, 'Language pack')};
45
53
  }
46
54
  config.i18n.translationsByLocale = translationsByLocale;
47
55
  return translationsByLocale;
@@ -68,3 +76,11 @@ export function localizedUrl(config, locale, path) {
68
76
  const suffix = String(path ?? '').replace(/^\/+/, '');
69
77
  return prefix + '/' + suffix;
70
78
  }
79
+
80
+
81
+ export function localizedContentUrl(config, locale, value, label = 'Content URL') {
82
+ const url=String(value),token=/^\/(?:\[launge\]|%5blaunge%5d)(?=\/|$)/i;
83
+ if(!url.startsWith('/'))return url;
84
+ if(/\[launge\]|%5blaunge%5d/i.test(url)&&!token.test(url))throw new Error(label+' may use [launge] only as the first site-path segment');
85
+ return token.test(url)?localePrefix(config,locale)+(url.replace(token,'')||'/'):url;
86
+ }
package/src/icons.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const iconNames = new Set([
2
2
  'home', 'book-open', 'rocket', 'newspaper', 'search', 'shield-check', 'rss', 'file-text',
3
3
  'layers', 'code', 'puzzle', 'plug', 'cloud', 'palette', 'globe', 'help-circle', 'arrow-up-right',
4
- 'check-circle', 'layout-grid', 'terminal', 'settings', 'sparkles'
4
+ 'check-circle', 'layout-grid', 'terminal', 'settings', 'sparkles', 'cpu', 'mail', 'github', 'check', 'x', 'arrow-right'
5
5
  ]);
6
6
 
7
7
  export function isIconName(name) {
@@ -10,7 +10,8 @@ export function isIconName(name) {
10
10
 
11
11
  export function renderIcon(name, className = 'icon') {
12
12
  if (!isIconName(name)) throw new Error('Unsupported EdgePress icon: ' + String(name));
13
- if (!/^[a-z][a-z0-9-]*$/.test(className)) throw new Error('Icon class must be a simple CSS identifier');
14
- return '<svg class="' + className + '" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><use href="/edgepress/icons.svg#' +
15
- name + '"></use></svg>';
13
+ if (typeof className !== 'string' || !/^[a-zA-Z_][a-zA-Z0-9_-]*(?: [a-zA-Z_][a-zA-Z0-9_-]*)*$/.test(className)) {
14
+ throw new Error('Icon class must contain space-separated CSS identifiers');
15
+ }
16
+ return '<img class="' + className + ' icon-library" src="/edgepress/icons/' + name + '.svg" width="24" height="24" alt="" aria-hidden="true" decoding="async">';
16
17
  }
@@ -0,0 +1,17 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import {imageSize} from 'image-size';
3
+ import {mapLimit} from './concurrency.js';
4
+ export async function collectImageDimensions(assets){
5
+ const result=new Map();await mapLimit(assets.filter(item=>/\.(?:png|jpe?g|gif|webp|avif|svg)$/i.test(item.path)),8,async item=>{
6
+ try{let {width,height,orientation}=imageSize(item.content??await readFile(item.source));if([5,6,7,8].includes(orientation))[width,height]=[height,width];if(width>0&&height>0)result.set('/'+item.path,{width,height});}catch{}
7
+ });return result;
8
+ }
9
+ export function reserveImageDimensions(html,dimensions,path,origin){
10
+ return html.replace(/<img\b[^>]*>/gi,tag=>{
11
+ const value=name=>tag.match(new RegExp('\\s'+name+'\\s*=\\s*["\']([^"\']+)["\']','i'))?.[1],src=value('src');if(!src)return tag;
12
+ let url,key;try{url=new URL(src,new URL('/'+path,origin));key=decodeURI(url.pathname);}catch{return tag;}
13
+ if(url.origin!==new URL(origin).origin)return tag;const size=dimensions.get(key);if(!size)return tag;
14
+ const w=Number(value('width')),h=Number(value('height'));if(w>0&&h>0)return tag;
15
+ return tag.replace(/\s*\/?>$/,end=>(value('width')?'':' width="'+Math.max(1,Math.round(h>0?h*size.width/size.height:size.width))+'"')+(value('height')?'':' height="'+Math.max(1,Math.round(w>0?w*size.height/size.width:size.height))+'"')+end);
16
+ });
17
+ }
@@ -0,0 +1,29 @@
1
+ // Local tool modules are trusted site source, like theme scripts. External
2
+ // providers belong to consent-controlled services, never this element.
3
+ const escape = value => String(value).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
4
+
5
+ export function renderLocalTool(block, context) {
6
+ for (const key of Object.keys(block)) if (!['type','id','title','text','script','stylesheet','data','fallback'].includes(key)) throw new Error('Unsupported local-tool option: ' + key);
7
+ if (typeof block.id !== 'string' || !/^[a-z][a-z0-9-]{0,79}$/.test(block.id)) throw new Error('local-tool.id must be a lowercase HTML identifier');
8
+ if (context.localToolIds?.has(block.id)) throw new Error('Duplicate local-tool.id: ' + block.id);
9
+ context.localToolIds?.add(block.id);
10
+ const string = (key, required = false) => {
11
+ const value = block[key];
12
+ if (value === undefined && !required) return '';
13
+ if (typeof value !== 'string' || !value.trim() || value.length > 2000) throw new Error('local-tool.' + key + ' must be a non-empty string of at most 2000 characters');
14
+ return escape(value);
15
+ };
16
+ const asset = (key, extension, required = false) => {
17
+ if (block[key] === undefined && !required) return '';
18
+ const value = block[key];
19
+ if (typeof value !== 'string' || !/^\/(?!\/)[a-zA-Z0-9_./-]+$/.test(value) || value.split('/').some(p => p==='.' || p==='..') || !extension.test(value)) throw new Error('local-tool.' + key + ' must be a same-origin local asset path');
20
+ if (!context.config.assetManifest?.[value]) throw new Error('local-tool.' + key + ' asset does not exist: ' + value);
21
+ return escape(value);
22
+ };
23
+ const script = asset('script', /\.m?js$/, true);
24
+ const stylesheet = asset('stylesheet', /\.css$/);
25
+ const data = asset('data', /\.json$/);
26
+ const title = string('title', true), description = string('text'), fallback = string('fallback', true);
27
+ const heading = block.id + '-title';
28
+ return '<section class="local-tool" id="' + block.id + '" data-edgepress-local-tool' + (data ? ' data-tool-data="' + data + '"' : '') + ' aria-labelledby="' + heading + '"><h2 id="' + heading + '">' + title + '</h2>' + (description ? '<p class="local-tool-description">' + description + '</p>' : '') + '<div data-tool-mount></div><noscript><p>' + fallback + '</p></noscript></section>' + (stylesheet ? '<link rel="stylesheet" href="' + stylesheet + '">' : '') + '<script type="module" src="' + script + '"></script>';
29
+ }
@@ -1,5 +1,5 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
1
+ import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
3
 
4
4
  async function readJson(file) {
5
5
  return JSON.parse(await readFile(file, 'utf8'));
@@ -34,16 +34,33 @@ export async function checkCompatibility(config) {
34
34
  } else if (workerDate < manifest.requirements.minimumWorkersDate) {
35
35
  issues.push({ severity: 'error', message: 'Workers compatibility_date is older than the supported minimum.' });
36
36
  }
37
- if (!wrangler.assets || !wrangler.assets.directory || wrangler.assets.binding !== 'ASSETS') {
37
+ if (!wrangler.assets || !wrangler.assets.directory) {
38
38
  issues.push({ severity: 'error', message: 'Configure the ASSETS binding and static assets directory in Wrangler.' });
39
39
  }
40
- const workerSource = await readFile(resolve(root, 'src/worker.js'), 'utf8');
41
- if (/from\s+['"]node:|require\s*\(\s*['"]node:/i.test(workerSource)) {
42
- issues.push({ severity: 'error', message: 'The Worker entry imports a Node.js built-in; keep build-only APIs out of src/worker.js.' });
40
+ const entry = wrangler.main;
41
+ if (entry !== undefined && (typeof entry !== 'string' || !entry.trim())) {
42
+ issues.push({severity:'error',message:'The optional Worker entry must be a non-empty string.'});
43
+ } else if (entry !== undefined) {
44
+ try {
45
+ const realRoot = await realpath(root);
46
+ const workerFile = await realpath(resolve(root, entry));
47
+ const rel = relative(realRoot, workerFile);
48
+ if (rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel)) {
49
+ issues.push({ severity: 'error', message: 'The configured Worker entry must stay inside the project root.' });
50
+ } else {
51
+ const workerSource = await readFile(workerFile, 'utf8');
52
+ if (/from\s+['"]node:|require\s*\(\s*['"]node:/i.test(workerSource)) {
53
+ issues.push({ severity: 'error', message: 'The configured Worker entry imports a Node.js built-in; keep build-only APIs out of the Worker.' });
54
+ }
55
+ }
56
+ } catch (error) {
57
+ if (!['ENOENT', 'EISDIR', 'EACCES', 'EPERM'].includes(error.code)) throw error;
58
+ issues.push({ severity: 'error', message: 'Cannot read the configured Worker entry: ' + entry + ' (' + error.code + ').' });
59
+ }
43
60
  }
44
61
  const first = wrangler.assets?.run_worker_first;
45
- if (!Array.isArray(first) || !first.includes('/api/*')) {
46
- issues.push({ severity: 'warning', message: 'Keep Worker-first routing limited to API paths so static pages bypass script execution.' });
62
+ if (first === true || (Array.isArray(first) && first.length)) {
63
+ issues.push({ severity: 'warning', message: 'Static websites should use assets-only hosting; deploy backend services separately.' });
47
64
  }
48
65
 
49
66
  const report = {
@@ -0,0 +1,20 @@
1
+ // Renderer verification only: never added to a site's content or generated routes.
2
+ export const markdownFixture = [
3
+ '## Heading two', '### Heading three', '#### Heading four', '##### Heading five', '###### Heading six', '',
4
+ 'Setext heading level two', '------------------------', '',
5
+ 'A paragraph with a hard break. ', 'The next line.', '',
6
+ '---', '', '***', '', '___', '',
7
+ '*Emphasis* and **strong emphasis** with ~~strikethrough~~.', '',
8
+ 'Inline `code`.', '', '```js', 'console.log("fixture");', '```', '',
9
+ '~~~text', 'Plain fenced code.', '~~~', '', ' const indented = true;', '',
10
+ '`` ` ``', '', '\\*escaped punctuation\\* &lt; &amp;', '',
11
+ '[Inline](/quick-start/) and [reference][guide].', '', '[guide]: /quick-start/', '',
12
+ 'https://www.markdownguide.org/', '', '<https://commonmark.org> www.example.com <team@example.com>', '',
13
+ '![An illustrated Markdown guide](/edgepress-markdown-guide.png)', '',
14
+ '![A kite crossing a field](https://media.example.org/field-recording.mp4 "Wind test")', '',
15
+ '- **Formatted** item', ' 1. Nested ordered item', '',
16
+ '- [x] Completed task', '- [ ] Pending task', '',
17
+ '> - A list in a quotation', '> - Another item', '', '> Outer quotation', '>', '> > Nested quotation', '',
18
+ '| Heading | Value |', '| --- | --- |', '| *Emphasis* | Detail |', '',
19
+ 'Use <kbd>Ctrl</kbd>.', '', '<script>alert("unsafe")</script>'
20
+ ].join('\n');