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