@geekity/cms 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (660) hide show
  1. package/README.md +2217 -214
  2. package/admin/components/fields.njk +165 -0
  3. package/admin/layouts/base.njk +1 -1
  4. package/admin/layouts/settings-page.njk +26 -0
  5. package/admin/layouts/shell.njk +17 -3
  6. package/admin/pages/account/forgot.njk +47 -0
  7. package/admin/pages/account/login.njk +26 -0
  8. package/admin/pages/account/reset.njk +38 -0
  9. package/admin/pages/account/setup.njk +28 -0
  10. package/admin/pages/appearance/themes.njk +69 -0
  11. package/admin/pages/comments/all.njk +133 -0
  12. package/admin/{layouts/dashboard.njk → pages/dashboard/home.njk} +8 -15
  13. package/admin/{layouts/document-conflict.njk → pages/documents/conflict.njk} +1 -0
  14. package/admin/{layouts/document-editor.njk → pages/documents/editor.njk} +75 -59
  15. package/admin/{layouts/document-list.njk → pages/documents/list.njk} +19 -2
  16. package/admin/pages/documents/taxonomy.njk +95 -0
  17. package/admin/pages/federation/followers.njk +238 -0
  18. package/admin/pages/federation/settings.njk +77 -0
  19. package/admin/pages/media/library.njk +160 -0
  20. package/admin/pages/messages/all.njk +110 -0
  21. package/admin/pages/navigation/menus.njk +129 -0
  22. package/admin/{layouts → pages}/placeholder.njk +5 -1
  23. package/admin/pages/settings/discussion.njk +99 -0
  24. package/admin/pages/settings/email.njk +203 -0
  25. package/admin/pages/settings/general.njk +29 -0
  26. package/admin/pages/settings/permalinks.njk +50 -0
  27. package/admin/pages/settings/reading.njk +64 -0
  28. package/admin/pages/tools/content-index.njk +89 -0
  29. package/admin/pages/users/edit.njk +239 -0
  30. package/admin/pages/users/list.njk +51 -0
  31. package/admin/pages/users/new.njk +41 -0
  32. package/admin/static/admin.css +558 -3
  33. package/admin/static/copy.js +52 -0
  34. package/admin/static/editor.js +16 -16
  35. package/admin/static/slug.js +61 -0
  36. package/dist/access-log.d.ts +44 -0
  37. package/dist/access-log.d.ts.map +1 -0
  38. package/dist/access-log.js +71 -0
  39. package/dist/access-log.js.map +1 -0
  40. package/dist/admin/accounts.d.ts +413 -0
  41. package/dist/admin/accounts.d.ts.map +1 -0
  42. package/dist/admin/accounts.js +680 -0
  43. package/dist/admin/accounts.js.map +1 -0
  44. package/dist/admin/appearance.d.ts +80 -0
  45. package/dist/admin/appearance.d.ts.map +1 -0
  46. package/dist/admin/appearance.js +136 -0
  47. package/dist/admin/appearance.js.map +1 -0
  48. package/dist/admin/comments.d.ts +90 -0
  49. package/dist/admin/comments.d.ts.map +1 -0
  50. package/dist/admin/comments.js +244 -0
  51. package/dist/admin/comments.js.map +1 -0
  52. package/dist/admin/credentials.d.ts +26 -2
  53. package/dist/admin/credentials.d.ts.map +1 -1
  54. package/dist/admin/credentials.js +35 -2
  55. package/dist/admin/credentials.js.map +1 -1
  56. package/dist/admin/documents.d.ts +117 -10
  57. package/dist/admin/documents.d.ts.map +1 -1
  58. package/dist/admin/documents.js +328 -41
  59. package/dist/admin/documents.js.map +1 -1
  60. package/dist/admin/federation.d.ts +161 -50
  61. package/dist/admin/federation.d.ts.map +1 -1
  62. package/dist/admin/federation.js +235 -93
  63. package/dist/admin/federation.js.map +1 -1
  64. package/dist/admin/formatting.d.ts +20 -0
  65. package/dist/admin/formatting.d.ts.map +1 -0
  66. package/dist/admin/formatting.js +72 -0
  67. package/dist/admin/formatting.js.map +1 -0
  68. package/dist/admin/headers.d.ts +65 -0
  69. package/dist/admin/headers.d.ts.map +1 -0
  70. package/dist/admin/headers.js +104 -0
  71. package/dist/admin/headers.js.map +1 -0
  72. package/dist/admin/index.d.ts +38 -12
  73. package/dist/admin/index.d.ts.map +1 -1
  74. package/dist/admin/index.js +25 -7
  75. package/dist/admin/index.js.map +1 -1
  76. package/dist/admin/media.d.ts +168 -0
  77. package/dist/admin/media.d.ts.map +1 -0
  78. package/dist/admin/media.js +295 -0
  79. package/dist/admin/media.js.map +1 -0
  80. package/dist/admin/menu.d.ts +79 -0
  81. package/dist/admin/menu.d.ts.map +1 -0
  82. package/dist/admin/menu.js +143 -0
  83. package/dist/admin/menu.js.map +1 -0
  84. package/dist/admin/messages.d.ts +80 -0
  85. package/dist/admin/messages.d.ts.map +1 -0
  86. package/dist/admin/messages.js +152 -0
  87. package/dist/admin/messages.js.map +1 -0
  88. package/dist/admin/navigation.d.ts +155 -0
  89. package/dist/admin/navigation.d.ts.map +1 -0
  90. package/dist/admin/navigation.js +292 -0
  91. package/dist/admin/navigation.js.map +1 -0
  92. package/dist/admin/preview.d.ts.map +1 -1
  93. package/dist/admin/preview.js +31 -7
  94. package/dist/admin/preview.js.map +1 -1
  95. package/dist/admin/recovery.d.ts +92 -0
  96. package/dist/admin/recovery.d.ts.map +1 -0
  97. package/dist/admin/recovery.js +229 -0
  98. package/dist/admin/recovery.js.map +1 -0
  99. package/dist/admin/routes.d.ts +0 -20
  100. package/dist/admin/routes.d.ts.map +1 -1
  101. package/dist/admin/routes.js +164 -105
  102. package/dist/admin/routes.js.map +1 -1
  103. package/dist/admin/session.d.ts +17 -3
  104. package/dist/admin/session.d.ts.map +1 -1
  105. package/dist/admin/session.js +19 -5
  106. package/dist/admin/session.js.map +1 -1
  107. package/dist/admin/settings-discussion.d.ts +37 -0
  108. package/dist/admin/settings-discussion.d.ts.map +1 -0
  109. package/dist/admin/settings-discussion.js +117 -0
  110. package/dist/admin/settings-discussion.js.map +1 -0
  111. package/dist/admin/settings-email.d.ts +54 -0
  112. package/dist/admin/settings-email.d.ts.map +1 -0
  113. package/dist/admin/settings-email.js +189 -0
  114. package/dist/admin/settings-email.js.map +1 -0
  115. package/dist/admin/settings-federation.d.ts +48 -0
  116. package/dist/admin/settings-federation.d.ts.map +1 -0
  117. package/dist/admin/settings-federation.js +123 -0
  118. package/dist/admin/settings-federation.js.map +1 -0
  119. package/dist/admin/settings-general.d.ts +12 -0
  120. package/dist/admin/settings-general.d.ts.map +1 -0
  121. package/dist/admin/settings-general.js +48 -0
  122. package/dist/admin/settings-general.js.map +1 -0
  123. package/dist/admin/settings-page.d.ts +119 -0
  124. package/dist/admin/settings-page.d.ts.map +1 -0
  125. package/dist/admin/settings-page.js +132 -0
  126. package/dist/admin/settings-page.js.map +1 -0
  127. package/dist/admin/settings-pages.d.ts +30 -0
  128. package/dist/admin/settings-pages.d.ts.map +1 -0
  129. package/dist/admin/settings-pages.js +43 -0
  130. package/dist/admin/settings-pages.js.map +1 -0
  131. package/dist/admin/settings-permalinks.d.ts +16 -0
  132. package/dist/admin/settings-permalinks.d.ts.map +1 -0
  133. package/dist/admin/settings-permalinks.js +32 -0
  134. package/dist/admin/settings-permalinks.js.map +1 -0
  135. package/dist/admin/settings-reading.d.ts +34 -0
  136. package/dist/admin/settings-reading.d.ts.map +1 -0
  137. package/dist/admin/settings-reading.js +49 -0
  138. package/dist/admin/settings-reading.js.map +1 -0
  139. package/dist/admin/settings.d.ts +339 -96
  140. package/dist/admin/settings.d.ts.map +1 -1
  141. package/dist/admin/settings.js +600 -308
  142. package/dist/admin/settings.js.map +1 -1
  143. package/dist/admin/store.d.ts +606 -152
  144. package/dist/admin/store.d.ts.map +1 -1
  145. package/dist/admin/store.js +863 -268
  146. package/dist/admin/store.js.map +1 -1
  147. package/dist/admin/taxonomy.d.ts +114 -0
  148. package/dist/admin/taxonomy.d.ts.map +1 -0
  149. package/dist/admin/taxonomy.js +289 -0
  150. package/dist/admin/taxonomy.js.map +1 -0
  151. package/dist/admin/templates.d.ts +46 -14
  152. package/dist/admin/templates.d.ts.map +1 -1
  153. package/dist/admin/templates.js +46 -14
  154. package/dist/admin/templates.js.map +1 -1
  155. package/dist/admin/throttle.d.ts +84 -0
  156. package/dist/admin/throttle.d.ts.map +1 -0
  157. package/dist/admin/throttle.js +143 -0
  158. package/dist/admin/throttle.js.map +1 -0
  159. package/dist/admin/tools.d.ts +111 -0
  160. package/dist/admin/tools.d.ts.map +1 -0
  161. package/dist/admin/tools.js +182 -0
  162. package/dist/admin/tools.js.map +1 -0
  163. package/dist/admin/uploads.d.ts +28 -2
  164. package/dist/admin/uploads.d.ts.map +1 -1
  165. package/dist/admin/uploads.js +43 -4
  166. package/dist/admin/uploads.js.map +1 -1
  167. package/dist/admin/users.d.ts +100 -13
  168. package/dist/admin/users.d.ts.map +1 -1
  169. package/dist/admin/users.js +412 -39
  170. package/dist/admin/users.js.map +1 -1
  171. package/dist/cache.d.ts +141 -0
  172. package/dist/cache.d.ts.map +1 -0
  173. package/dist/cache.js +207 -0
  174. package/dist/cache.js.map +1 -0
  175. package/dist/cli.d.ts +14 -2
  176. package/dist/cli.d.ts.map +1 -1
  177. package/dist/cli.js +429 -21
  178. package/dist/cli.js.map +1 -1
  179. package/dist/comments/akismet.d.ts +130 -0
  180. package/dist/comments/akismet.d.ts.map +1 -0
  181. package/dist/comments/akismet.js +274 -0
  182. package/dist/comments/akismet.js.map +1 -0
  183. package/dist/comments/conversation.d.ts +29 -0
  184. package/dist/comments/conversation.d.ts.map +1 -0
  185. package/dist/comments/conversation.js +60 -0
  186. package/dist/comments/conversation.js.map +1 -0
  187. package/dist/comments/form.d.ts +123 -0
  188. package/dist/comments/form.d.ts.map +1 -0
  189. package/dist/comments/form.js +79 -0
  190. package/dist/comments/form.js.map +1 -0
  191. package/dist/comments/index.d.ts +16 -0
  192. package/dist/comments/index.d.ts.map +1 -0
  193. package/dist/comments/index.js +10 -0
  194. package/dist/comments/index.js.map +1 -0
  195. package/dist/comments/markdown.d.ts +8 -0
  196. package/dist/comments/markdown.d.ts.map +1 -0
  197. package/dist/comments/markdown.js +85 -0
  198. package/dist/comments/markdown.js.map +1 -0
  199. package/dist/comments/moderate.d.ts +71 -0
  200. package/dist/comments/moderate.d.ts.map +1 -0
  201. package/dist/comments/moderate.js +88 -0
  202. package/dist/comments/moderate.js.map +1 -0
  203. package/dist/comments/policy.d.ts +57 -0
  204. package/dist/comments/policy.d.ts.map +1 -0
  205. package/dist/comments/policy.js +80 -0
  206. package/dist/comments/policy.js.map +1 -0
  207. package/dist/comments/records.d.ts +266 -0
  208. package/dist/comments/records.d.ts.map +1 -0
  209. package/dist/comments/records.js +487 -0
  210. package/dist/comments/records.js.map +1 -0
  211. package/dist/comments/routes.d.ts +60 -0
  212. package/dist/comments/routes.d.ts.map +1 -0
  213. package/dist/comments/routes.js +212 -0
  214. package/dist/comments/routes.js.map +1 -0
  215. package/dist/comments/submission.d.ts +297 -0
  216. package/dist/comments/submission.d.ts.map +1 -0
  217. package/dist/comments/submission.js +280 -0
  218. package/dist/comments/submission.js.map +1 -0
  219. package/dist/comments/viewer.d.ts +21 -0
  220. package/dist/comments/viewer.d.ts.map +1 -0
  221. package/dist/comments/viewer.js +45 -0
  222. package/dist/comments/viewer.js.map +1 -0
  223. package/dist/config.d.ts +225 -5
  224. package/dist/config.d.ts.map +1 -1
  225. package/dist/config.js +121 -6
  226. package/dist/config.js.map +1 -1
  227. package/dist/contact/delivery.d.ts +60 -0
  228. package/dist/contact/delivery.d.ts.map +1 -0
  229. package/dist/contact/delivery.js +87 -0
  230. package/dist/contact/delivery.js.map +1 -0
  231. package/dist/contact/form.d.ts +130 -0
  232. package/dist/contact/form.d.ts.map +1 -0
  233. package/dist/contact/form.js +163 -0
  234. package/dist/contact/form.js.map +1 -0
  235. package/dist/contact/index.d.ts +17 -0
  236. package/dist/contact/index.d.ts.map +1 -0
  237. package/dist/contact/index.js +13 -0
  238. package/dist/contact/index.js.map +1 -0
  239. package/dist/contact/records.d.ts +124 -0
  240. package/dist/contact/records.d.ts.map +1 -0
  241. package/dist/contact/records.js +218 -0
  242. package/dist/contact/records.js.map +1 -0
  243. package/dist/contact/routes.d.ts +17 -0
  244. package/dist/contact/routes.d.ts.map +1 -0
  245. package/dist/contact/routes.js +127 -0
  246. package/dist/contact/routes.js.map +1 -0
  247. package/dist/contact/submission.d.ts +103 -0
  248. package/dist/contact/submission.d.ts.map +1 -0
  249. package/dist/contact/submission.js +166 -0
  250. package/dist/contact/submission.js.map +1 -0
  251. package/dist/content/document.d.ts +7 -2
  252. package/dist/content/document.d.ts.map +1 -1
  253. package/dist/content/document.js +1 -0
  254. package/dist/content/document.js.map +1 -1
  255. package/dist/content/index.d.ts +7 -2
  256. package/dist/content/index.d.ts.map +1 -1
  257. package/dist/content/index.js +4 -1
  258. package/dist/content/index.js.map +1 -1
  259. package/dist/content/markdown.d.ts.map +1 -1
  260. package/dist/content/markdown.js +24 -2
  261. package/dist/content/markdown.js.map +1 -1
  262. package/dist/content/parser.d.ts.map +1 -1
  263. package/dist/content/parser.js +11 -4
  264. package/dist/content/parser.js.map +1 -1
  265. package/dist/content/save.d.ts +5 -0
  266. package/dist/content/save.d.ts.map +1 -1
  267. package/dist/content/save.js +44 -1
  268. package/dist/content/save.js.map +1 -1
  269. package/dist/content/schedule.d.ts +125 -0
  270. package/dist/content/schedule.d.ts.map +1 -0
  271. package/dist/content/schedule.js +207 -0
  272. package/dist/content/schedule.js.map +1 -0
  273. package/dist/content/search.d.ts +64 -0
  274. package/dist/content/search.d.ts.map +1 -0
  275. package/dist/content/search.js +136 -0
  276. package/dist/content/search.js.map +1 -0
  277. package/dist/content/store.d.ts +220 -7
  278. package/dist/content/store.d.ts.map +1 -1
  279. package/dist/content/store.js +392 -67
  280. package/dist/content/store.js.map +1 -1
  281. package/dist/content/sync.d.ts +7 -1
  282. package/dist/content/sync.d.ts.map +1 -1
  283. package/dist/content/sync.js +18 -10
  284. package/dist/content/sync.js.map +1 -1
  285. package/dist/content/time.d.ts +56 -0
  286. package/dist/content/time.d.ts.map +1 -0
  287. package/dist/content/time.js +141 -0
  288. package/dist/content/time.js.map +1 -0
  289. package/dist/content/writer.d.ts.map +1 -1
  290. package/dist/content/writer.js +3 -0
  291. package/dist/content/writer.js.map +1 -1
  292. package/dist/env.d.ts +70 -5
  293. package/dist/env.d.ts.map +1 -1
  294. package/dist/federation/actor.d.ts +95 -41
  295. package/dist/federation/actor.d.ts.map +1 -1
  296. package/dist/federation/actor.js +180 -53
  297. package/dist/federation/actor.js.map +1 -1
  298. package/dist/federation/article.d.ts +44 -17
  299. package/dist/federation/article.d.ts.map +1 -1
  300. package/dist/federation/article.js +98 -46
  301. package/dist/federation/article.js.map +1 -1
  302. package/dist/federation/delivery.d.ts +63 -15
  303. package/dist/federation/delivery.d.ts.map +1 -1
  304. package/dist/federation/delivery.js +154 -80
  305. package/dist/federation/delivery.js.map +1 -1
  306. package/dist/federation/federation.d.ts +22 -17
  307. package/dist/federation/federation.d.ts.map +1 -1
  308. package/dist/federation/federation.js +98 -77
  309. package/dist/federation/federation.js.map +1 -1
  310. package/dist/federation/followers.d.ts +3 -2
  311. package/dist/federation/followers.d.ts.map +1 -1
  312. package/dist/federation/followers.js +6 -5
  313. package/dist/federation/followers.js.map +1 -1
  314. package/dist/federation/import-wordpress.d.ts +128 -0
  315. package/dist/federation/import-wordpress.d.ts.map +1 -0
  316. package/dist/federation/import-wordpress.js +426 -0
  317. package/dist/federation/import-wordpress.js.map +1 -0
  318. package/dist/federation/inbox.d.ts +36 -13
  319. package/dist/federation/inbox.d.ts.map +1 -1
  320. package/dist/federation/inbox.js +120 -49
  321. package/dist/federation/inbox.js.map +1 -1
  322. package/dist/federation/index.d.ts +22 -10
  323. package/dist/federation/index.d.ts.map +1 -1
  324. package/dist/federation/index.js +13 -8
  325. package/dist/federation/index.js.map +1 -1
  326. package/dist/federation/keys.d.ts +98 -20
  327. package/dist/federation/keys.d.ts.map +1 -1
  328. package/dist/federation/keys.js +269 -39
  329. package/dist/federation/keys.js.map +1 -1
  330. package/dist/federation/mount.d.ts +46 -10
  331. package/dist/federation/mount.d.ts.map +1 -1
  332. package/dist/federation/mount.js +300 -28
  333. package/dist/federation/mount.js.map +1 -1
  334. package/dist/federation/paths.d.ts +23 -35
  335. package/dist/federation/paths.d.ts.map +1 -1
  336. package/dist/federation/paths.js +26 -39
  337. package/dist/federation/paths.js.map +1 -1
  338. package/dist/federation/records.d.ts +223 -0
  339. package/dist/federation/records.d.ts.map +1 -0
  340. package/dist/federation/records.js +506 -0
  341. package/dist/federation/records.js.map +1 -0
  342. package/dist/federation/relays.d.ts +114 -0
  343. package/dist/federation/relays.d.ts.map +1 -0
  344. package/dist/federation/relays.js +290 -0
  345. package/dist/federation/relays.js.map +1 -0
  346. package/dist/federation/replies.d.ts +80 -0
  347. package/dist/federation/replies.d.ts.map +1 -0
  348. package/dist/federation/replies.js +166 -0
  349. package/dist/federation/replies.js.map +1 -0
  350. package/dist/federation/wordpress.d.ts +131 -0
  351. package/dist/federation/wordpress.d.ts.map +1 -0
  352. package/dist/federation/wordpress.js +341 -0
  353. package/dist/federation/wordpress.js.map +1 -0
  354. package/dist/files/atomic.d.ts +103 -0
  355. package/dist/files/atomic.d.ts.map +1 -0
  356. package/dist/files/atomic.js +150 -0
  357. package/dist/files/atomic.js.map +1 -0
  358. package/dist/files/index.d.ts +3 -0
  359. package/dist/files/index.d.ts.map +1 -0
  360. package/dist/files/index.js +2 -0
  361. package/dist/files/index.js.map +1 -0
  362. package/dist/forms/protection.d.ts +85 -0
  363. package/dist/forms/protection.d.ts.map +1 -0
  364. package/dist/forms/protection.js +116 -0
  365. package/dist/forms/protection.js.map +1 -0
  366. package/dist/images/icons.d.ts +59 -0
  367. package/dist/images/icons.d.ts.map +1 -0
  368. package/dist/images/icons.js +131 -0
  369. package/dist/images/icons.js.map +1 -0
  370. package/dist/images/index.d.ts +7 -0
  371. package/dist/images/index.d.ts.map +1 -0
  372. package/dist/images/index.js +4 -0
  373. package/dist/images/index.js.map +1 -0
  374. package/dist/images/markup.d.ts +47 -0
  375. package/dist/images/markup.d.ts.map +1 -0
  376. package/dist/images/markup.js +175 -0
  377. package/dist/images/markup.js.map +1 -0
  378. package/dist/images/paths.d.ts +34 -0
  379. package/dist/images/paths.d.ts.map +1 -0
  380. package/dist/images/paths.js +41 -0
  381. package/dist/images/paths.js.map +1 -0
  382. package/dist/images/variants.d.ts +104 -0
  383. package/dist/images/variants.d.ts.map +1 -0
  384. package/dist/images/variants.js +320 -0
  385. package/dist/images/variants.js.map +1 -0
  386. package/dist/index.d.ts +123 -19
  387. package/dist/index.d.ts.map +1 -1
  388. package/dist/index.js +344 -25
  389. package/dist/index.js.map +1 -1
  390. package/dist/init.d.ts +18 -0
  391. package/dist/init.d.ts.map +1 -1
  392. package/dist/init.js +34 -3
  393. package/dist/init.js.map +1 -1
  394. package/dist/mail/brevo.d.ts +35 -0
  395. package/dist/mail/brevo.d.ts.map +1 -0
  396. package/dist/mail/brevo.js +85 -0
  397. package/dist/mail/brevo.js.map +1 -0
  398. package/dist/mail/credentials.d.ts +62 -0
  399. package/dist/mail/credentials.d.ts.map +1 -0
  400. package/dist/mail/credentials.js +86 -0
  401. package/dist/mail/credentials.js.map +1 -0
  402. package/dist/mail/index.d.ts +15 -0
  403. package/dist/mail/index.d.ts.map +1 -0
  404. package/dist/mail/index.js +8 -0
  405. package/dist/mail/index.js.map +1 -0
  406. package/dist/mail/memory.d.ts +35 -0
  407. package/dist/mail/memory.d.ts.map +1 -0
  408. package/dist/mail/memory.js +29 -0
  409. package/dist/mail/memory.js.map +1 -0
  410. package/dist/mail/provider.d.ts +80 -0
  411. package/dist/mail/provider.d.ts.map +1 -0
  412. package/dist/mail/provider.js +23 -0
  413. package/dist/mail/provider.js.map +1 -0
  414. package/dist/mail/service.d.ts +156 -0
  415. package/dist/mail/service.d.ts.map +1 -0
  416. package/dist/mail/service.js +265 -0
  417. package/dist/mail/service.js.map +1 -0
  418. package/dist/mail/smtp.d.ts +39 -0
  419. package/dist/mail/smtp.d.ts.map +1 -0
  420. package/dist/mail/smtp.js +58 -0
  421. package/dist/mail/smtp.js.map +1 -0
  422. package/dist/mail/templates.d.ts +91 -0
  423. package/dist/mail/templates.d.ts.map +1 -0
  424. package/dist/mail/templates.js +69 -0
  425. package/dist/mail/templates.js.map +1 -0
  426. package/dist/notifications/comments.d.ts +66 -0
  427. package/dist/notifications/comments.d.ts.map +1 -0
  428. package/dist/notifications/comments.js +151 -0
  429. package/dist/notifications/comments.js.map +1 -0
  430. package/dist/notifications/digest.d.ts +135 -0
  431. package/dist/notifications/digest.d.ts.map +1 -0
  432. package/dist/notifications/digest.js +287 -0
  433. package/dist/notifications/digest.js.map +1 -0
  434. package/dist/notifications/index.d.ts +13 -0
  435. package/dist/notifications/index.d.ts.map +1 -0
  436. package/dist/notifications/index.js +8 -0
  437. package/dist/notifications/index.js.map +1 -0
  438. package/dist/notifications/links.d.ts +60 -0
  439. package/dist/notifications/links.d.ts.map +1 -0
  440. package/dist/notifications/links.js +63 -0
  441. package/dist/notifications/links.js.map +1 -0
  442. package/dist/notifications/optouts.d.ts +48 -0
  443. package/dist/notifications/optouts.d.ts.map +1 -0
  444. package/dist/notifications/optouts.js +93 -0
  445. package/dist/notifications/optouts.js.map +1 -0
  446. package/dist/notifications/preferences.d.ts +158 -0
  447. package/dist/notifications/preferences.d.ts.map +1 -0
  448. package/dist/notifications/preferences.js +195 -0
  449. package/dist/notifications/preferences.js.map +1 -0
  450. package/dist/notifications/routes.d.ts +28 -0
  451. package/dist/notifications/routes.d.ts.map +1 -0
  452. package/dist/notifications/routes.js +184 -0
  453. package/dist/notifications/routes.js.map +1 -0
  454. package/dist/notifications/tokens.d.ts +60 -0
  455. package/dist/notifications/tokens.d.ts.map +1 -0
  456. package/dist/notifications/tokens.js +125 -0
  457. package/dist/notifications/tokens.js.map +1 -0
  458. package/dist/notify.d.ts +84 -0
  459. package/dist/notify.d.ts.map +1 -0
  460. package/dist/notify.js +149 -0
  461. package/dist/notify.js.map +1 -0
  462. package/dist/web/archive.d.ts +49 -0
  463. package/dist/web/archive.d.ts.map +1 -0
  464. package/dist/web/archive.js +59 -0
  465. package/dist/web/archive.js.map +1 -0
  466. package/dist/web/assets.d.ts +6 -5
  467. package/dist/web/assets.d.ts.map +1 -1
  468. package/dist/web/assets.js +5 -7
  469. package/dist/web/assets.js.map +1 -1
  470. package/dist/web/authors.d.ts +143 -0
  471. package/dist/web/authors.d.ts.map +1 -0
  472. package/dist/web/authors.js +178 -0
  473. package/dist/web/authors.js.map +1 -0
  474. package/dist/web/comments.d.ts +60 -0
  475. package/dist/web/comments.d.ts.map +1 -0
  476. package/dist/web/comments.js +207 -0
  477. package/dist/web/comments.js.map +1 -0
  478. package/dist/web/context.d.ts +162 -28
  479. package/dist/web/context.d.ts.map +1 -1
  480. package/dist/web/context.js +123 -26
  481. package/dist/web/context.js.map +1 -1
  482. package/dist/web/conversation.d.ts +209 -0
  483. package/dist/web/conversation.d.ts.map +1 -0
  484. package/dist/web/conversation.js +557 -0
  485. package/dist/web/conversation.js.map +1 -0
  486. package/dist/web/documents.d.ts +36 -6
  487. package/dist/web/documents.d.ts.map +1 -1
  488. package/dist/web/documents.js +52 -10
  489. package/dist/web/documents.js.map +1 -1
  490. package/dist/web/feed-atom.d.ts +22 -0
  491. package/dist/web/feed-atom.d.ts.map +1 -0
  492. package/dist/web/feed-atom.js +83 -0
  493. package/dist/web/feed-atom.js.map +1 -0
  494. package/dist/web/feed-item.d.ts +145 -0
  495. package/dist/web/feed-item.d.ts.map +1 -0
  496. package/dist/web/feed-item.js +120 -0
  497. package/dist/web/feed-item.js.map +1 -0
  498. package/dist/web/feed-json.d.ts +79 -0
  499. package/dist/web/feed-json.d.ts.map +1 -0
  500. package/dist/web/feed-json.js +53 -0
  501. package/dist/web/feed-json.js.map +1 -0
  502. package/dist/web/feed-rss.d.ts +26 -0
  503. package/dist/web/feed-rss.d.ts.map +1 -0
  504. package/dist/web/feed-rss.js +203 -0
  505. package/dist/web/feed-rss.js.map +1 -0
  506. package/dist/web/feed-source.d.ts +244 -0
  507. package/dist/web/feed-source.d.ts.map +1 -0
  508. package/dist/web/feed-source.js +201 -0
  509. package/dist/web/feed-source.js.map +1 -0
  510. package/dist/web/feed-xml.d.ts +63 -0
  511. package/dist/web/feed-xml.d.ts.map +1 -0
  512. package/dist/web/feed-xml.js +116 -0
  513. package/dist/web/feed-xml.js.map +1 -0
  514. package/dist/web/feeds.d.ts +31 -96
  515. package/dist/web/feeds.d.ts.map +1 -1
  516. package/dist/web/feeds.js +94 -198
  517. package/dist/web/feeds.js.map +1 -1
  518. package/dist/web/health.d.ts +31 -0
  519. package/dist/web/health.d.ts.map +1 -0
  520. package/dist/web/health.js +44 -0
  521. package/dist/web/health.js.map +1 -0
  522. package/dist/web/index.d.ts +30 -10
  523. package/dist/web/index.d.ts.map +1 -1
  524. package/dist/web/index.js +18 -7
  525. package/dist/web/index.js.map +1 -1
  526. package/dist/web/navigation.d.ts +195 -0
  527. package/dist/web/navigation.d.ts.map +1 -0
  528. package/dist/web/navigation.js +300 -0
  529. package/dist/web/navigation.js.map +1 -0
  530. package/dist/web/negotiate.d.ts +31 -0
  531. package/dist/web/negotiate.d.ts.map +1 -1
  532. package/dist/web/negotiate.js +31 -11
  533. package/dist/web/negotiate.js.map +1 -1
  534. package/dist/web/recent.d.ts +43 -0
  535. package/dist/web/recent.d.ts.map +1 -0
  536. package/dist/web/recent.js +37 -0
  537. package/dist/web/recent.js.map +1 -0
  538. package/dist/web/render.d.ts +203 -9
  539. package/dist/web/render.d.ts.map +1 -1
  540. package/dist/web/render.js +341 -17
  541. package/dist/web/render.js.map +1 -1
  542. package/dist/web/routes.d.ts +25 -8
  543. package/dist/web/routes.d.ts.map +1 -1
  544. package/dist/web/routes.js +854 -93
  545. package/dist/web/routes.js.map +1 -1
  546. package/dist/web/sanitize.d.ts +19 -0
  547. package/dist/web/sanitize.d.ts.map +1 -0
  548. package/dist/web/sanitize.js +272 -0
  549. package/dist/web/sanitize.js.map +1 -0
  550. package/dist/web/search.d.ts +79 -0
  551. package/dist/web/search.d.ts.map +1 -0
  552. package/dist/web/search.js +94 -0
  553. package/dist/web/search.js.map +1 -0
  554. package/dist/web/sitemap.d.ts +120 -0
  555. package/dist/web/sitemap.d.ts.map +1 -0
  556. package/dist/web/sitemap.js +199 -0
  557. package/dist/web/sitemap.js.map +1 -0
  558. package/dist/web/taxonomy.d.ts +152 -0
  559. package/dist/web/taxonomy.d.ts.map +1 -0
  560. package/dist/web/taxonomy.js +237 -0
  561. package/dist/web/taxonomy.js.map +1 -0
  562. package/dist/web/templates.d.ts +54 -16
  563. package/dist/web/templates.d.ts.map +1 -1
  564. package/dist/web/templates.js +109 -28
  565. package/dist/web/templates.js.map +1 -1
  566. package/dist/web/themes.d.ts +226 -0
  567. package/dist/web/themes.d.ts.map +1 -0
  568. package/dist/web/themes.js +314 -0
  569. package/dist/web/themes.js.map +1 -0
  570. package/dist/webmention/discovery.d.ts +51 -0
  571. package/dist/webmention/discovery.d.ts.map +1 -0
  572. package/dist/webmention/discovery.js +176 -0
  573. package/dist/webmention/discovery.js.map +1 -0
  574. package/dist/webmention/html.d.ts +71 -0
  575. package/dist/webmention/html.d.ts.map +1 -0
  576. package/dist/webmention/html.js +285 -0
  577. package/dist/webmention/html.js.map +1 -0
  578. package/dist/webmention/index.d.ts +12 -0
  579. package/dist/webmention/index.d.ts.map +1 -0
  580. package/dist/webmention/index.js +8 -0
  581. package/dist/webmention/index.js.map +1 -0
  582. package/dist/webmention/links.d.ts +18 -0
  583. package/dist/webmention/links.d.ts.map +1 -0
  584. package/dist/webmention/links.js +64 -0
  585. package/dist/webmention/links.js.map +1 -0
  586. package/dist/webmention/microformats.d.ts +93 -0
  587. package/dist/webmention/microformats.d.ts.map +1 -0
  588. package/dist/webmention/microformats.js +376 -0
  589. package/dist/webmention/microformats.js.map +1 -0
  590. package/dist/webmention/receive.d.ts +141 -0
  591. package/dist/webmention/receive.d.ts.map +1 -0
  592. package/dist/webmention/receive.js +264 -0
  593. package/dist/webmention/receive.js.map +1 -0
  594. package/dist/webmention/routes.d.ts +37 -0
  595. package/dist/webmention/routes.d.ts.map +1 -0
  596. package/dist/webmention/routes.js +75 -0
  597. package/dist/webmention/routes.js.map +1 -0
  598. package/dist/webmention/service.d.ts +94 -0
  599. package/dist/webmention/service.d.ts.map +1 -0
  600. package/dist/webmention/service.js +207 -0
  601. package/dist/webmention/service.js.map +1 -0
  602. package/package.json +11 -6
  603. package/templates/site/content/_data/site.json +8 -0
  604. package/templates/site/content/pages/about.md +7 -0
  605. package/templates/site/content/posts/2026-01-01-hello-world.md +5 -3
  606. package/templates/site/geekity.config.ts +11 -1
  607. package/templates/site/tsconfig.json +1 -1
  608. package/themes/default/README.md +1216 -50
  609. package/themes/default/layouts/404.njk +2 -3
  610. package/themes/default/layouts/author.njk +36 -0
  611. package/themes/default/layouts/base.njk +161 -31
  612. package/themes/default/layouts/category.njk +27 -0
  613. package/themes/default/layouts/front-page.njk +56 -0
  614. package/themes/default/layouts/home.njk +17 -1
  615. package/themes/default/layouts/page.njk +57 -5
  616. package/themes/default/layouts/post.njk +78 -19
  617. package/themes/default/layouts/search.njk +57 -0
  618. package/themes/default/layouts/tag.njk +3 -6
  619. package/themes/default/mail/comment-digest.html.njk +37 -0
  620. package/themes/default/mail/comment-digest.subject.njk +3 -0
  621. package/themes/default/mail/comment-digest.txt.njk +40 -0
  622. package/themes/default/mail/comment-pending.html.njk +27 -0
  623. package/themes/default/mail/comment-pending.subject.njk +2 -0
  624. package/themes/default/mail/comment-pending.txt.njk +24 -0
  625. package/themes/default/mail/comment-reply.html.njk +21 -0
  626. package/themes/default/mail/comment-reply.subject.njk +2 -0
  627. package/themes/default/mail/comment-reply.txt.njk +22 -0
  628. package/themes/default/mail/contact-message.html.njk +24 -0
  629. package/themes/default/mail/contact-message.subject.njk +5 -0
  630. package/themes/default/mail/contact-message.txt.njk +20 -0
  631. package/themes/default/mail/password-changed.html.njk +18 -0
  632. package/themes/default/mail/password-changed.subject.njk +2 -0
  633. package/themes/default/mail/password-changed.txt.njk +17 -0
  634. package/themes/default/mail/password-reset.html.njk +22 -0
  635. package/themes/default/mail/password-reset.subject.njk +2 -0
  636. package/themes/default/mail/password-reset.txt.njk +18 -0
  637. package/themes/default/mail/test.html.njk +14 -0
  638. package/themes/default/mail/test.subject.njk +2 -0
  639. package/themes/default/mail/test.txt.njk +12 -0
  640. package/themes/default/partials/archive.njk +27 -0
  641. package/themes/default/partials/bio.njk +71 -0
  642. package/themes/default/partials/comment-form.njk +137 -0
  643. package/themes/default/partials/contact-form.njk +88 -0
  644. package/themes/default/partials/conversation.njk +139 -0
  645. package/themes/default/partials/feeds.njk +26 -0
  646. package/themes/default/partials/jsonld.njk +120 -0
  647. package/themes/default/partials/menu.njk +32 -0
  648. package/themes/default/partials/pagination.njk +12 -6
  649. package/themes/default/partials/post-list.njk +51 -18
  650. package/themes/default/partials/search-form.njk +22 -0
  651. package/themes/default/partials/tags.njk +26 -5
  652. package/themes/default/static/highlight.js +4 -0
  653. package/themes/default/static/style.css +1142 -194
  654. package/themes/default/theme.json +9 -0
  655. package/admin/layouts/federation.njk +0 -145
  656. package/admin/layouts/login.njk +0 -23
  657. package/admin/layouts/settings.njk +0 -116
  658. package/admin/layouts/setup.njk +0 -27
  659. package/admin/layouts/users.njk +0 -91
  660. /package/admin/{layouts/_flash.njk → components/flash.njk} +0 -0
@@ -2,11 +2,19 @@ import { mkdir, readFile, rename, rm } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { parseDocument } from '../content/parser.js';
4
4
  import { contentFilePath, freeSlug, saveDocument } from '../content/save.js';
5
+ import { scheduledFor } from '../content/schedule.js';
5
6
  import { defaultPermalink, slugify } from '../content/slug.js';
6
7
  import { DuplicatePermalinkError, isTrashedPath, TRASH_DIRECTORY } from '../content/store.js';
8
+ import { calendarDayIn, DEFAULT_TIMEZONE, toUtcInstant, wallClockIn, zoneLabel, } from '../content/time.js';
7
9
  import { normalizeBody, serializeDocument } from '../content/writer.js';
8
10
  import { isPublicDocument } from '../web/documents.js';
11
+ import { COMMENTS_FRONT_MATTER_KEY } from '../comments/policy.js';
12
+ import { CONTACT_FRONT_MATTER_KEY } from '../contact/form.js';
13
+ import { userForAuthor } from '../web/authors.js';
14
+ import { findUserById, listUsers } from './accounts.js';
9
15
  import { flash } from './flash.js';
16
+ import { formatInTimezone } from './formatting.js';
17
+ import { readSiteSettings } from './settings.js';
10
18
  import { PREVIEW_PATH } from './preview.js';
11
19
  import { ADMIN_PREFIX } from './session.js';
12
20
  import { ADMIN_TEMPLATES } from './templates.js';
@@ -19,7 +27,30 @@ import { UPLOADS_PATH } from './uploads.js';
19
27
  * there, alongside every other key somebody added by hand.
20
28
  */
21
29
  export const EXCLUDE_KEY = 'eleventyExcludeFromCollections';
22
- /** The posts screens: dated, tagged, filed under `posts/`. */
30
+ /**
31
+ * What the editor's Comments field can say: leave it to the site's rules, hold
32
+ * this document open, or close it.
33
+ *
34
+ * The values are the strings the form submits and the template renders as
35
+ * options, spelled once so the two cannot drift.
36
+ */
37
+ export const COMMENT_SETTINGS = { site: '', open: 'open', closed: 'closed' };
38
+ /** A submitted Comments field, or the site default for anything else. */
39
+ function commentSetting(value) {
40
+ return value === COMMENT_SETTINGS.open || value === COMMENT_SETTINGS.closed
41
+ ? value
42
+ : COMMENT_SETTINGS.site;
43
+ }
44
+ /** What a document's front matter already says about comments. */
45
+ function commentSettingOf(document) {
46
+ const own = document.extra[COMMENTS_FRONT_MATTER_KEY];
47
+ if (own === true)
48
+ return COMMENT_SETTINGS.open;
49
+ if (own === false)
50
+ return COMMENT_SETTINGS.closed;
51
+ return COMMENT_SETTINGS.site;
52
+ }
53
+ /** The posts screens: dated, tagged, categorised, filed under `posts/`. */
23
54
  export const POST_KIND = {
24
55
  type: 'post',
25
56
  section: 'posts',
@@ -28,7 +59,9 @@ export const POST_KIND = {
28
59
  plural: 'Posts',
29
60
  dated: true,
30
61
  tagged: true,
62
+ categorised: true,
31
63
  excludable: false,
64
+ contactable: false,
32
65
  };
33
66
  /** The pages screens: standing content, no date prefix and no taxonomy. */
34
67
  export const PAGE_KIND = {
@@ -39,7 +72,9 @@ export const PAGE_KIND = {
39
72
  plural: 'Pages',
40
73
  dated: false,
41
74
  tagged: false,
75
+ categorised: false,
42
76
  excludable: true,
77
+ contactable: true,
43
78
  };
44
79
  /** The URL of the editor for one document. */
45
80
  export function editorPath(kind, slug) {
@@ -50,7 +85,13 @@ export function newEditorPath(kind) {
50
85
  return `${kind.basePath}/new`;
51
86
  }
52
87
  /** The filters, in the order they are shown. */
53
- export const DOCUMENT_FILTERS = ['all', 'published', 'draft', 'trash'];
88
+ export const DOCUMENT_FILTERS = [
89
+ 'all',
90
+ 'published',
91
+ 'scheduled',
92
+ 'draft',
93
+ 'trash',
94
+ ];
54
95
  /** How many rows one page of a listing holds. */
55
96
  export const DOCUMENTS_PER_PAGE = 25;
56
97
  /** The filter a query string asked for, defaulting to everything live. */
@@ -62,7 +103,8 @@ export function listOptionsFor(kind, filter) {
62
103
  return {
63
104
  type: kind.type,
64
105
  trashed: filter === 'trash',
65
- ...(filter === 'published' ? { draft: false } : {}),
106
+ ...(filter === 'published' ? { draft: false, scheduled: false } : {}),
107
+ ...(filter === 'scheduled' ? { draft: false, scheduled: true } : {}),
66
108
  ...(filter === 'draft' ? { draft: true } : {}),
67
109
  };
68
110
  }
@@ -72,7 +114,7 @@ export function listOptionsFor(kind, filter) {
72
114
  * Both `/admin/posts` and `/admin/pages` are this function with a different
73
115
  * {@link DocumentKind}: the screens, the templates and the write path are
74
116
  * shared, and the kind decides what a file is called, whether the form has a
75
- * date and tags, and which section of the navigation marks itself.
117
+ * date and a taxonomy, and which section of the navigation marks itself.
76
118
  */
77
119
  export function mountDocumentScreens(app, options) {
78
120
  const { kind, render } = options;
@@ -89,6 +131,7 @@ export function mountDocumentScreens(app, options) {
89
131
  const rows = found.slice(0, DOCUMENTS_PER_PAGE);
90
132
  return render(c, ADMIN_TEMPLATES.documentList, {
91
133
  section: kind.section,
134
+ child: 'all',
92
135
  kind,
93
136
  filter,
94
137
  filters: DOCUMENT_FILTERS.map((name) => ({
@@ -97,7 +140,7 @@ export function mountDocumentScreens(app, options) {
97
140
  url: listingUrl(kind, name, 1),
98
141
  current: name === filter,
99
142
  })),
100
- documents: rows.map((document) => listRow(kind, document)),
143
+ documents: rows.map((document) => listRow(kind, document, c.var.store.now(), pageRole(kind, document, c))),
101
144
  newUrl: newEditorPath(kind),
102
145
  returnUrl: listingUrl(kind, filter, pageNumber),
103
146
  page: pageNumber,
@@ -111,14 +154,14 @@ export function mountDocumentScreens(app, options) {
111
154
  kind,
112
155
  render,
113
156
  document: undefined,
114
- form: blankForm(kind),
157
+ form: blankForm(kind, siteTimezone(c), c.var.store.now()),
115
158
  }));
116
159
  app.post(newEditorPath(kind), async (c) => saveFromForm(c, { kind, render, document: undefined, body: await c.req.parseBody() }));
117
160
  app.get(`${kind.basePath}/:slug`, (c) => {
118
161
  const document = findBySlug(c.var.store, kind, c.req.param('slug'));
119
162
  if (document === undefined)
120
163
  return c.notFound();
121
- return renderEditor(c, { kind, render, document, form: formFor(document) });
164
+ return renderEditor(c, { kind, render, document, form: formFor(document, siteTimezone(c)) });
122
165
  });
123
166
  app.post(`${kind.basePath}/:slug`, async (c) => {
124
167
  const document = findBySlug(c.var.store, kind, c.req.param('slug'));
@@ -151,9 +194,13 @@ async function saveFromForm(c, options) {
151
194
  permalink: text(body['permalink']).trim(),
152
195
  date: text(body['date']).trim(),
153
196
  tags: text(body['tags']).trim(),
197
+ categories: text(body['categories']).trim(),
154
198
  description: text(body['description']).trim(),
199
+ author: text(body['author']).trim(),
155
200
  draft: body['draft'] !== undefined,
156
201
  exclude: body['exclude'] !== undefined,
202
+ contact: body['contact'] !== undefined,
203
+ comments: commentSetting(text(body['comments'])),
157
204
  body: normalizeBody(text(body['body'])),
158
205
  hash: text(body['hash']),
159
206
  };
@@ -173,19 +220,57 @@ async function saveFromForm(c, options) {
173
220
  }
174
221
  if (form.title === '')
175
222
  return refuse(`A ${kind.singular} needs a title.`);
176
- const date = kind.dated ? (form.date === '' ? new Date().toISOString() : form.date) : undefined;
177
- if (date !== undefined && !/^\d{4}-\d{2}-\d{2}/.test(date)) {
223
+ const timezone = siteTimezone(c);
224
+ // decision-11: what the file gets is a UTC instant, and an offset-less field
225
+ // is the site's own wall clock rather than the server's.
226
+ const typed = kind.dated ? (form.date === '' ? store.now().toISOString() : form.date) : undefined;
227
+ if (typed !== undefined && !/^\d{4}-\d{2}-\d{2}/.test(typed)) {
178
228
  return refuse('A date has to start with a year, a month and a day, like 2026-03-04.');
179
229
  }
230
+ const date = typed === undefined ? undefined : toUtcInstant(typed, timezone);
231
+ if (typed !== undefined && date === undefined) {
232
+ return refuse('That date is not one anybody can read. Try 2026-03-04 09:00.');
233
+ }
180
234
  const slug = slugify(form.slug) || slugify(form.title) || (document?.slug ?? '') || 'untitled';
181
235
  const trashed = document !== undefined && isTrashedPath(document.path);
236
+ // The calendar day the document is filed under: the site zone's day at its
237
+ // date for a new one, and the day already in the filename for one whose date
238
+ // has not moved, so a zone changed later never moves a URL that exists.
239
+ const filed = filedDay({ date, document, timezone });
182
240
  // A new document gets out of the way of anything that already holds its
183
241
  // name; an existing one keeps the slug it was given.
184
242
  const finalSlug = document === undefined
185
- ? await freeSlug({ contentDir, store, type: kind.type, slug, date })
243
+ ? await freeSlug({ contentDir, store, type: kind.type, slug, date: filed })
186
244
  : slug;
187
- const permalink = resolvePermalink({ kind, form, slug: finalSlug, date, document });
188
- const target = documentPath({ kind, slug: finalSlug, date, trashed });
245
+ // decision-13: a published post's permalink is its name in the fediverse as
246
+ // well as on the web, and the two are one promise. Moving it hands every
247
+ // follower a second object and breaks every link somebody already shared, so
248
+ // the editor refuses. A draft has promised nothing yet and may still move;
249
+ // a site that really means to move a published post can still do it in the
250
+ // file, knowing what it costs.
251
+ const promised = promisedDocument(kind, document, store.now());
252
+ if (promised !== undefined) {
253
+ if (finalSlug !== promised.slug) {
254
+ return refuse(`The permalink of a published post is permanent, and the slug names the file it is filed under: this ${kind.singular} keeps the slug ${promised.slug}.`);
255
+ }
256
+ // An empty field is not a request to move: it is a form that carried no
257
+ // permalink, and the promised one is what it keeps. Only a field naming
258
+ // some other URL is somebody asking for the one thing this refuses.
259
+ const submitted = normalizePermalink(form.permalink);
260
+ if (submitted !== undefined && submitted !== promised.permalink) {
261
+ return refuse(`The permalink of a published post is permanent: this ${kind.singular} stays at ${promised.permalink}.`);
262
+ }
263
+ }
264
+ const permalink = promised?.permalink ??
265
+ resolvePermalink({
266
+ kind,
267
+ form,
268
+ slug: finalSlug,
269
+ date: filed,
270
+ document,
271
+ timezone,
272
+ });
273
+ const target = documentPath({ kind, slug: finalSlug, date: filed, trashed });
189
274
  if (document !== undefined) {
190
275
  const conflict = await conflictWith(contentDir, document, form.hash);
191
276
  if (conflict !== undefined) {
@@ -198,14 +283,15 @@ async function saveFromForm(c, options) {
198
283
  const content = {
199
284
  title: form.title,
200
285
  ...(date === undefined ? {} : { date }),
201
- updated: new Date().toISOString(),
286
+ updated: store.now().toISOString(),
202
287
  permalink,
203
288
  tags: kind.tagged ? splitTags(form.tags) : [],
289
+ categories: kind.categorised ? splitTags(form.categories) : [],
204
290
  draft,
205
291
  ...(form.description === '' ? {} : { description: form.description }),
206
- ...optional('author', document?.author ?? currentUsername(c)),
292
+ ...optional('author', chosenAuthor(c, form.author, document)),
207
293
  ...optional('activitypub', document?.activitypub),
208
- extra: resolveExtra(kind, document, form.exclude),
294
+ extra: resolveExtra(kind, document, form),
209
295
  body: form.body,
210
296
  };
211
297
  // The row for the old path goes first: a rename briefly leaves two rows
@@ -215,7 +301,7 @@ async function saveFromForm(c, options) {
215
301
  store.remove(renamedFrom.path);
216
302
  let saved;
217
303
  try {
218
- saved = await saveDocument({ contentDir, store, path: target, content });
304
+ saved = await saveDocument({ contentDir, store, path: target, content, timezone });
219
305
  }
220
306
  catch (error) {
221
307
  // Nothing was written, so the row that was taken out of the way goes back.
@@ -241,13 +327,18 @@ async function saveFromForm(c, options) {
241
327
  next: saved,
242
328
  origin: 'admin',
243
329
  });
244
- flash(c, 'notice', savedMessage(kind, document, saved));
330
+ flash(c, 'notice', savedMessage(kind, document, saved, store.now()));
245
331
  return c.redirect(editorPath(kind, saved.slug), 303);
246
332
  }
247
333
  /** What the flash says after a save, which depends on what the save did. */
248
- function savedMessage(kind, previous, saved) {
334
+ function savedMessage(kind, previous, saved, now) {
249
335
  if (saved.draft)
250
336
  return `Draft saved: ${saved.title}`;
337
+ // A date in the future is not a refusal to publish, it is an instruction
338
+ // about when, and the flash has to say so or the author will think the
339
+ // Publish button did nothing.
340
+ if (scheduledFor(saved, now) !== undefined)
341
+ return `Scheduled: ${saved.title}`;
251
342
  if (previous === undefined || previous.draft)
252
343
  return `Published: ${saved.title}`;
253
344
  return `Updated: ${saved.title}`;
@@ -257,6 +348,21 @@ function documentPath(input) {
257
348
  const relative = contentFilePath({ type: input.kind.type, slug: input.slug, date: input.date });
258
349
  return input.trashed ? `${TRASH_DIRECTORY}/${relative}` : relative;
259
350
  }
351
+ /**
352
+ * The document whose permalink has already been promised, or `undefined` when
353
+ * nothing has been promised yet.
354
+ *
355
+ * A published post, and only a published post. decision-13 makes its permalink
356
+ * its ActivityStreams object id, so the URL is a name its followers, its
357
+ * replies and its feed subscribers are all holding; a draft, a trashed post
358
+ * and one whose date has not arrived have been shown to nobody, and a page
359
+ * federates nothing at all.
360
+ */
361
+ function promisedDocument(kind, document, now) {
362
+ if (document === undefined || kind.type !== 'post')
363
+ return undefined;
364
+ return isPublicDocument(document, now) ? document : undefined;
365
+ }
260
366
  /**
261
367
  * The permalink to write.
262
368
  *
@@ -276,20 +382,60 @@ function resolvePermalink(input) {
276
382
  if (submitted === undefined)
277
383
  return fallback;
278
384
  const document = input.document;
279
- if (document !== undefined && submitted === previousDefaultPermalink(input.kind, document)) {
385
+ if (document !== undefined &&
386
+ submitted === previousDefaultPermalink(input.kind, document, input.timezone)) {
280
387
  return fallback;
281
388
  }
282
389
  return submitted;
283
390
  }
284
- /** The permalink a document would have had if it had never been customised. */
285
- function previousDefaultPermalink(kind, document) {
391
+ /**
392
+ * The permalink a document would have had if it had never been customised:
393
+ * its own slug over the day it is filed under, which is the day in its
394
+ * filename rather than one re-derived from a setting that may have moved.
395
+ */
396
+ function previousDefaultPermalink(kind, document, timezone) {
286
397
  try {
287
- return defaultPermalink({ type: kind.type, slug: document.slug, date: document.date });
398
+ return defaultPermalink({
399
+ type: kind.type,
400
+ slug: document.slug,
401
+ date: filedDay({ date: document.date, document, timezone }),
402
+ });
288
403
  }
289
404
  catch {
290
405
  return undefined;
291
406
  }
292
407
  }
408
+ /**
409
+ * The calendar day a document is filed under, which is what its filename and
410
+ * its `/{yyyy}/{mm}/` permalink are cut from.
411
+ *
412
+ * For a new document it is the day the site's own zone was on at its date, so
413
+ * a post published at half past midnight on 1 October in Berlin is filed under
414
+ * October rather than the September UTC was still on.
415
+ *
416
+ * For a document whose date has not moved it is the day already in its
417
+ * filename. That is the whole of decision-11's promise that changing the
418
+ * timezone setting cannot move an existing URL: the instant is the same, the
419
+ * zone is a lens, and the day this post was filed under was decided once, when
420
+ * it was written, and is on disk where no setting can reach it.
421
+ */
422
+ function filedDay(input) {
423
+ const { document } = input;
424
+ const date = input.date === undefined ? undefined : toUtcInstant(input.date, input.timezone);
425
+ if (date === undefined)
426
+ return undefined;
427
+ const was = document?.date === undefined ? undefined : toUtcInstant(document.date, input.timezone);
428
+ if (was === date) {
429
+ const existing = /(?:^|\/)(\d{4}-\d{2}-\d{2})-/.exec(document?.path ?? '')?.[1];
430
+ if (existing !== undefined)
431
+ return existing;
432
+ }
433
+ return calendarDayIn(date, input.timezone) ?? date;
434
+ }
435
+ /** The site's time zone, which is what every offset-less date in the admin means. */
436
+ function siteTimezone(c) {
437
+ return readSiteSettings(c.var.config.contentDir).timezone;
438
+ }
293
439
  /** A submitted permalink as a URL path, or `undefined` when the field is empty. */
294
440
  function normalizePermalink(value) {
295
441
  const trimmed = value.trim();
@@ -314,17 +460,38 @@ function normalizePermalink(value) {
314
460
  * the `false` is written, because a key somebody wrote by hand is not the
315
461
  * editor's to delete.
316
462
  */
317
- function resolveExtra(kind, document, exclude) {
463
+ function resolveExtra(kind, document, form) {
318
464
  const extra = { ...(document?.extra ?? {}) };
319
- if (!kind.excludable)
320
- return extra;
321
- if (exclude)
322
- extra[EXCLUDE_KEY] = true;
323
- else if (EXCLUDE_KEY in extra)
324
- extra[EXCLUDE_KEY] = false;
465
+ // The post's own answer about comments, or none at all: "site default" is
466
+ // the absence of the key rather than a value, because that is what every
467
+ // reader of it — the CMS, an Eleventy build, a person looking at the file —
468
+ // already understands, and writing `comments: null` would say nothing new.
469
+ if (form.comments === COMMENT_SETTINGS.open)
470
+ extra[COMMENTS_FRONT_MATTER_KEY] = true;
471
+ else if (form.comments === COMMENT_SETTINGS.closed)
472
+ extra[COMMENTS_FRONT_MATTER_KEY] = false;
473
+ else
474
+ delete extra[COMMENTS_FRONT_MATTER_KEY];
475
+ if (kind.excludable) {
476
+ if (form.exclude)
477
+ extra[EXCLUDE_KEY] = true;
478
+ else if (EXCLUDE_KEY in extra)
479
+ extra[EXCLUDE_KEY] = false;
480
+ }
481
+ if (kind.contactable) {
482
+ // Written or removed, never `false`: it is the CMS's own key, and absent
483
+ // and false mean the same thing to everything that reads it (TASK-56).
484
+ if (form.contact)
485
+ extra[CONTACT_FRONT_MATTER_KEY] = true;
486
+ else
487
+ delete extra[CONTACT_FRONT_MATTER_KEY];
488
+ }
325
489
  return extra;
326
490
  }
327
- /** A comma-separated tag field as a list, without the blanks and the repeats. */
491
+ /**
492
+ * A comma-separated taxonomy field as a list, without the blanks and the
493
+ * repeats. Tags and categories are both entered this way.
494
+ */
328
495
  export function splitTags(value) {
329
496
  const tags = [];
330
497
  for (const tag of value.split(',')) {
@@ -339,7 +506,62 @@ function currentUsername(c) {
339
506
  const userId = c.var.session?.userId;
340
507
  if (userId == null)
341
508
  return undefined;
342
- return c.var.admin.getUserById(userId)?.username;
509
+ return findUserById(c.var.config.dataDir, userId)?.username;
510
+ }
511
+ /**
512
+ * What a save writes into the front matter's `author`.
513
+ *
514
+ * The submitted value is resolved through the same rule the public site reads
515
+ * it by ({@link userForAuthor}), and what is written is that user's login: so
516
+ * a file carrying a display name from before decision-14 is rewritten as a
517
+ * username the first time somebody saves it, which is exactly the "until it is
518
+ * next saved" doc-2 promises.
519
+ *
520
+ * A value naming nobody this site has changes nothing. There is no form this
521
+ * CMS renders that could submit one — the select only ever offers the users
522
+ * and, for a file that names a stranger, that file's own value — so the case
523
+ * is either a hand-made request or a user deleted between the load and the
524
+ * save, and neither is a reason to reattribute somebody's post.
525
+ */
526
+ function chosenAuthor(c, submitted, document) {
527
+ const named = userForAuthor(listUsers(c.var.config.dataDir), submitted);
528
+ if (named !== undefined)
529
+ return named.username;
530
+ return document?.author ?? currentUsername(c);
531
+ }
532
+ /**
533
+ * The Author select: every user, whoever the document names, and — for a file
534
+ * naming somebody with no account here — that name too.
535
+ *
536
+ * The stranger's option is what keeps the editor honest. Without it, opening a
537
+ * post written by a colleague whose account has gone and pressing Update would
538
+ * quietly move the post to whoever happened to be first in the list; with it,
539
+ * the form submits back what the file says and changing the attribution stays
540
+ * a thing somebody chose to do.
541
+ *
542
+ * A new document starts on the signed-in user, which is what doc-5 says the
543
+ * editor does and what {@link blankForm} deliberately leaves to a request.
544
+ */
545
+ export function authorChoices(c, current) {
546
+ const users = listUsers(c.var.config.dataDir);
547
+ const named = userForAuthor(users, current);
548
+ const chosen = named?.username ?? (current === '' ? currentUsername(c) : current);
549
+ const choices = users.map((user) => ({
550
+ value: user.username,
551
+ label: authorLabel(user),
552
+ chosen: user.username === chosen,
553
+ }));
554
+ // A name the file holds that is nobody here: offered last, and marked, so it
555
+ // is plain that the post is attributed to somebody this site cannot reach.
556
+ if (named === undefined && current !== '') {
557
+ choices.push({ value: current, label: `${current} (no account here)`, chosen: true });
558
+ }
559
+ return choices;
560
+ }
561
+ /** How one user reads in the select: their name, and the login behind it. */
562
+ function authorLabel(user) {
563
+ const displayName = user.profile?.displayName;
564
+ return displayName === undefined ? user.username : `${displayName} (${user.username})`;
343
565
  }
344
566
  /**
345
567
  * Whether the file has moved on since the form loaded, per doc-1: files win,
@@ -385,16 +607,19 @@ function renderConflict(c, options) {
385
607
  ...(form.date === '' ? {} : { date: form.date }),
386
608
  permalink: form.permalink === '' ? document.permalink : form.permalink,
387
609
  tags: kind.tagged ? splitTags(form.tags) : [],
610
+ categories: kind.categorised ? splitTags(form.categories) : [],
388
611
  draft: form.draft,
389
612
  ...(form.description === '' ? {} : { description: form.description }),
390
613
  ...optional('author', document.author),
391
614
  ...optional('activitypub', document.activitypub),
392
- extra: resolveExtra(kind, document, form.exclude),
615
+ extra: resolveExtra(kind, document, form),
393
616
  body: form.body,
394
617
  });
395
618
  c.status(409);
396
619
  return options.render(c, ADMIN_TEMPLATES.documentConflict, {
397
620
  section: kind.section,
621
+ // Editing something that exists, so the listing is where the menu stands.
622
+ child: 'all',
398
623
  kind,
399
624
  form,
400
625
  // The hash the file has now, so resubmitting this form is a deliberate
@@ -485,32 +710,56 @@ function optional(key, value) {
485
710
  function text(value) {
486
711
  return typeof value === 'string' ? value : '';
487
712
  }
488
- /** The editor for a document that does not exist yet. */
489
- export function blankForm(kind) {
713
+ /**
714
+ * The editor for a document that does not exist yet.
715
+ *
716
+ * The date field is filled in with the clock as the site's own zone reads it,
717
+ * not with an instant: what a form offers is what a form takes back, and per
718
+ * decision-11 an offset-less date in this field means the site's zone.
719
+ */
720
+ export function blankForm(kind, timezone = DEFAULT_TIMEZONE, now = new Date()) {
490
721
  return {
491
722
  title: '',
492
723
  slug: '',
493
724
  permalink: '',
494
- date: kind.dated ? new Date().toISOString() : '',
725
+ date: kind.dated ? wallClockIn(now, timezone) : '',
495
726
  tags: '',
727
+ categories: '',
496
728
  description: '',
729
+ // Empty rather than the signed-in user: `blankForm` is a pure function
730
+ // over a kind and a clock, and who is signed in is a fact about a request.
731
+ // {@link authorChoices} is where the default is applied.
732
+ author: '',
497
733
  draft: false,
498
734
  exclude: false,
735
+ contact: false,
736
+ comments: COMMENT_SETTINGS.site,
499
737
  body: '',
500
738
  hash: '',
501
739
  };
502
740
  }
503
- /** The editor for a document that does. */
504
- export function formFor(document) {
741
+ /**
742
+ * The editor for a document that does.
743
+ *
744
+ * The stored instant is shown as the clock in the site's zone reads it, and
745
+ * {@link wallClockIn} keeps whatever precision the instant has, so a form
746
+ * submitted with the field untouched writes back the very instant it was
747
+ * filled in from.
748
+ */
749
+ export function formFor(document, timezone = DEFAULT_TIMEZONE) {
505
750
  return {
506
751
  title: document.title,
507
752
  slug: document.slug,
508
753
  permalink: document.permalink,
509
- date: document.date ?? '',
754
+ date: document.date === undefined ? '' : wallClockIn(document.date, timezone),
510
755
  tags: document.tags.join(', '),
756
+ categories: document.categories.join(', '),
511
757
  description: document.description ?? '',
758
+ author: document.author ?? '',
512
759
  draft: document.draft,
513
760
  exclude: document.extra[EXCLUDE_KEY] === true,
761
+ contact: document.extra[CONTACT_FRONT_MATTER_KEY] === true,
762
+ comments: commentSettingOf(document),
514
763
  body: document.body,
515
764
  hash: document.hash,
516
765
  };
@@ -526,6 +775,11 @@ export function formFor(document) {
526
775
  function renderEditor(c, options) {
527
776
  const { kind, document, form } = options;
528
777
  const trashed = document !== undefined && isTrashedPath(document.path);
778
+ const now = c.var.store.now();
779
+ const timezone = siteTimezone(c);
780
+ // Printed in the site's own time zone rather than in UTC: an author who
781
+ // scheduled a post for nine in the morning meant their own morning.
782
+ const scheduledAt = document === undefined || trashed || document.draft ? undefined : scheduledFor(document, now);
529
783
  const actions = [];
530
784
  if (document === undefined || document.draft) {
531
785
  actions.push({ value: 'save-draft', label: 'Save draft' });
@@ -542,16 +796,26 @@ function renderEditor(c, options) {
542
796
  c.status(options.status);
543
797
  return options.render(c, ADMIN_TEMPLATES.documentEditor, {
544
798
  section: kind.section,
799
+ // The blank form is Add new; editing one that exists is still All posts,
800
+ // the way WordPress leaves the listing marked while you are in the editor.
801
+ child: document === undefined ? 'new' : 'all',
545
802
  kind,
546
803
  form,
547
804
  actions,
548
805
  trashed,
806
+ commentSettings: COMMENT_SETTINGS,
807
+ // Who this can be attributed to, and who it is attributed to now.
808
+ authors: authorChoices(c, form.author),
549
809
  heading: document === undefined ? `Add ${kind.singular}` : `Edit ${kind.singular}: ${document.title}`,
550
810
  saveUrl: document === undefined ? newEditorPath(kind) : editorPath(kind, document.slug),
551
811
  listUrl: kind.basePath,
552
812
  previewUrl: PREVIEW_PATH,
553
813
  uploadUrl: UPLOADS_PATH,
554
- viewUrl: document !== undefined && isPublicDocument(document) ? document.permalink : undefined,
814
+ viewUrl: document !== undefined && isPublicDocument(document, now) ? document.permalink : undefined,
815
+ // Named beside the date field, because a wall clock with no zone on it is
816
+ // exactly the ambiguity decision-11 exists to remove.
817
+ ...(kind.dated ? { dateZone: zoneLabel(form.date === '' ? now : form.date, timezone) } : {}),
818
+ ...(scheduledAt === undefined ? {} : { scheduledFor: formatInTimezone(scheduledAt, timezone) }),
555
819
  ...(options.error === undefined ? {} : { error: options.error }),
556
820
  });
557
821
  }
@@ -576,13 +840,33 @@ export function findBySlug(store, kind, slug) {
576
840
  }
577
841
  return undefined;
578
842
  }
579
- function listRow(kind, document) {
580
- const isPublic = isPublicDocument(document);
843
+ /**
844
+ * WordPress's own two labels for the pages the Reading setting names, beside
845
+ * the title on the pages screen, so it is plain from the list which page is
846
+ * the front page and which carries the posts.
847
+ */
848
+ export const PAGE_ROLE_LABELS = { homepage: 'Front Page', postsPage: 'Posts Page' };
849
+ /** What one row's page is to the site, if anything. */
850
+ function pageRole(kind, document, c) {
851
+ if (kind.type !== 'page')
852
+ return undefined;
853
+ const settings = readSiteSettings(c.var.config.contentDir);
854
+ if (settings.homepage === '')
855
+ return undefined;
856
+ if (document.slug === settings.homepage)
857
+ return PAGE_ROLE_LABELS.homepage;
858
+ if (document.slug === settings.postsPage)
859
+ return PAGE_ROLE_LABELS.postsPage;
860
+ return undefined;
861
+ }
862
+ function listRow(kind, document, now, role) {
863
+ const isPublic = isPublicDocument(document, now);
581
864
  return {
582
865
  title: document.title,
583
866
  slug: document.slug,
584
867
  author: document.author,
585
868
  tags: document.tags.join(', '),
869
+ categories: document.categories.join(', '),
586
870
  date: document.date,
587
871
  // Both come out of the index, so the listing costs no reads of its own.
588
872
  // A page that has never been saved through the admin has no `updated`, and
@@ -590,13 +874,16 @@ function listRow(kind, document) {
590
874
  updated: document.updated ?? document.date,
591
875
  draft: document.draft,
592
876
  trashed: isTrashedPath(document.path),
877
+ scheduled: scheduledFor(document, now) !== undefined,
593
878
  editUrl: editorPath(kind, document.slug),
594
879
  viewUrl: isPublic ? document.permalink : undefined,
880
+ role,
595
881
  };
596
882
  }
597
883
  const FILTER_LABELS = {
598
884
  all: 'All',
599
885
  published: 'Published',
886
+ scheduled: 'Scheduled',
600
887
  draft: 'Drafts',
601
888
  trash: 'Trash',
602
889
  };