@geekity/cms 0.17.0 → 0.19.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 (297) hide show
  1. package/README.md +163 -50
  2. package/admin/components/public-admin-bar.njk +3 -1
  3. package/admin/pages/documents/editor.njk +286 -158
  4. package/admin/pages/documents/list.njk +2 -0
  5. package/admin/pages/indieauth/consent.njk +4 -0
  6. package/admin/pages/settings/privacy.njk +37 -0
  7. package/admin/pages/users/apps.njk +109 -3
  8. package/admin/static/admin.css +37 -14
  9. package/dist/admin/documents.d.ts +34 -4
  10. package/dist/admin/documents.d.ts.map +1 -1
  11. package/dist/admin/documents.js +161 -23
  12. package/dist/admin/documents.js.map +1 -1
  13. package/dist/admin/editor-layout.d.ts +13 -0
  14. package/dist/admin/editor-layout.d.ts.map +1 -0
  15. package/dist/admin/editor-layout.js +61 -0
  16. package/dist/admin/editor-layout.js.map +1 -0
  17. package/dist/admin/enclosure-field.d.ts +2 -3
  18. package/dist/admin/enclosure-field.d.ts.map +1 -1
  19. package/dist/admin/enclosure-field.js +27 -7
  20. package/dist/admin/enclosure-field.js.map +1 -1
  21. package/dist/admin/index.d.ts +1 -0
  22. package/dist/admin/index.d.ts.map +1 -1
  23. package/dist/admin/index.js +1 -0
  24. package/dist/admin/index.js.map +1 -1
  25. package/dist/admin/location-field.d.ts +27 -0
  26. package/dist/admin/location-field.d.ts.map +1 -0
  27. package/dist/admin/location-field.js +98 -0
  28. package/dist/admin/location-field.js.map +1 -0
  29. package/dist/admin/menu.d.ts.map +1 -1
  30. package/dist/admin/menu.js +1 -0
  31. package/dist/admin/menu.js.map +1 -1
  32. package/dist/admin/photo-field.d.ts +2 -3
  33. package/dist/admin/photo-field.d.ts.map +1 -1
  34. package/dist/admin/photo-field.js +4 -2
  35. package/dist/admin/photo-field.js.map +1 -1
  36. package/dist/admin/preview.d.ts.map +1 -1
  37. package/dist/admin/preview.js +18 -5
  38. package/dist/admin/preview.js.map +1 -1
  39. package/dist/admin/read-field.d.ts +22 -0
  40. package/dist/admin/read-field.d.ts.map +1 -0
  41. package/dist/admin/read-field.js +61 -0
  42. package/dist/admin/read-field.js.map +1 -0
  43. package/dist/admin/settings-page.d.ts +3 -3
  44. package/dist/admin/settings-page.js +2 -2
  45. package/dist/admin/settings-pages.d.ts +2 -2
  46. package/dist/admin/settings-pages.d.ts.map +1 -1
  47. package/dist/admin/settings-pages.js +4 -2
  48. package/dist/admin/settings-pages.js.map +1 -1
  49. package/dist/admin/settings-privacy.d.ts +3 -0
  50. package/dist/admin/settings-privacy.d.ts.map +1 -0
  51. package/dist/admin/settings-privacy.js +10 -0
  52. package/dist/admin/settings-privacy.js.map +1 -0
  53. package/dist/admin/settings-reading.js +2 -2
  54. package/dist/admin/settings-reading.js.map +1 -1
  55. package/dist/admin/settings.d.ts +18 -2
  56. package/dist/admin/settings.d.ts.map +1 -1
  57. package/dist/admin/settings.js +27 -0
  58. package/dist/admin/settings.js.map +1 -1
  59. package/dist/admin/templates.d.ts +1 -0
  60. package/dist/admin/templates.d.ts.map +1 -1
  61. package/dist/admin/templates.js +1 -0
  62. package/dist/admin/templates.js.map +1 -1
  63. package/dist/admin/uploads.d.ts.map +1 -1
  64. package/dist/admin/uploads.js +14 -1
  65. package/dist/admin/uploads.js.map +1 -1
  66. package/dist/cli.d.ts +1 -1
  67. package/dist/cli.d.ts.map +1 -1
  68. package/dist/cli.js +34 -0
  69. package/dist/cli.js.map +1 -1
  70. package/dist/comments/routes.js +2 -2
  71. package/dist/comments/routes.js.map +1 -1
  72. package/dist/contact/routes.js +2 -2
  73. package/dist/contact/routes.js.map +1 -1
  74. package/dist/content/kept-properties.d.ts +5 -0
  75. package/dist/content/kept-properties.d.ts.map +1 -0
  76. package/dist/content/kept-properties.js +12 -0
  77. package/dist/content/kept-properties.js.map +1 -0
  78. package/dist/content/location.d.ts +62 -0
  79. package/dist/content/location.d.ts.map +1 -0
  80. package/dist/content/location.js +240 -0
  81. package/dist/content/location.js.map +1 -0
  82. package/dist/content/locations.d.ts +6 -0
  83. package/dist/content/locations.d.ts.map +1 -0
  84. package/dist/content/locations.js +7 -0
  85. package/dist/content/locations.js.map +1 -0
  86. package/dist/content/metadata/gif.d.ts +3 -0
  87. package/dist/content/metadata/gif.d.ts.map +1 -0
  88. package/dist/content/metadata/gif.js +65 -0
  89. package/dist/content/metadata/gif.js.map +1 -0
  90. package/dist/content/metadata/index.d.ts +6 -0
  91. package/dist/content/metadata/index.d.ts.map +1 -0
  92. package/dist/content/metadata/index.js +24 -0
  93. package/dist/content/metadata/index.js.map +1 -0
  94. package/dist/content/metadata/isobmff.d.ts +22 -0
  95. package/dist/content/metadata/isobmff.d.ts.map +1 -0
  96. package/dist/content/metadata/isobmff.js +215 -0
  97. package/dist/content/metadata/isobmff.js.map +1 -0
  98. package/dist/content/metadata/jpeg.d.ts +3 -0
  99. package/dist/content/metadata/jpeg.d.ts.map +1 -0
  100. package/dist/content/metadata/jpeg.js +117 -0
  101. package/dist/content/metadata/jpeg.js.map +1 -0
  102. package/dist/content/metadata/png.d.ts +3 -0
  103. package/dist/content/metadata/png.d.ts.map +1 -0
  104. package/dist/content/metadata/png.js +74 -0
  105. package/dist/content/metadata/png.js.map +1 -0
  106. package/dist/content/metadata/shared.d.ts +17 -0
  107. package/dist/content/metadata/shared.d.ts.map +1 -0
  108. package/dist/content/metadata/shared.js +42 -0
  109. package/dist/content/metadata/shared.js.map +1 -0
  110. package/dist/content/metadata/sweep.d.ts +14 -0
  111. package/dist/content/metadata/sweep.d.ts.map +1 -0
  112. package/dist/content/metadata/sweep.js +54 -0
  113. package/dist/content/metadata/sweep.js.map +1 -0
  114. package/dist/content/metadata/tiff.d.ts +7 -0
  115. package/dist/content/metadata/tiff.d.ts.map +1 -0
  116. package/dist/content/metadata/tiff.js +56 -0
  117. package/dist/content/metadata/tiff.js.map +1 -0
  118. package/dist/content/metadata/webp.d.ts +3 -0
  119. package/dist/content/metadata/webp.d.ts.map +1 -0
  120. package/dist/content/metadata/webp.js +67 -0
  121. package/dist/content/metadata/webp.js.map +1 -0
  122. package/dist/content/permalink-file.d.ts +7 -0
  123. package/dist/content/permalink-file.d.ts.map +1 -0
  124. package/dist/content/permalink-file.js +77 -0
  125. package/dist/content/permalink-file.js.map +1 -0
  126. package/dist/content/post-type.d.ts +4 -2
  127. package/dist/content/post-type.d.ts.map +1 -1
  128. package/dist/content/post-type.js +9 -1
  129. package/dist/content/post-type.js.map +1 -1
  130. package/dist/content/read.d.ts +22 -0
  131. package/dist/content/read.d.ts.map +1 -0
  132. package/dist/content/read.js +67 -0
  133. package/dist/content/read.js.map +1 -0
  134. package/dist/content/store.d.ts +4 -4
  135. package/dist/content/store.d.ts.map +1 -1
  136. package/dist/content/store.js +19 -39
  137. package/dist/content/store.js.map +1 -1
  138. package/dist/content/sync.d.ts.map +1 -1
  139. package/dist/content/sync.js +3 -13
  140. package/dist/content/sync.js.map +1 -1
  141. package/dist/content/visibility.d.ts +11 -0
  142. package/dist/content/visibility.d.ts.map +1 -0
  143. package/dist/content/visibility.js +14 -0
  144. package/dist/content/visibility.js.map +1 -0
  145. package/dist/federation/article.d.ts +4 -3
  146. package/dist/federation/article.d.ts.map +1 -1
  147. package/dist/federation/article.js +66 -20
  148. package/dist/federation/article.js.map +1 -1
  149. package/dist/federation/citations.d.ts +7 -6
  150. package/dist/federation/citations.d.ts.map +1 -1
  151. package/dist/federation/citations.js +44 -28
  152. package/dist/federation/citations.js.map +1 -1
  153. package/dist/federation/delivery.d.ts +3 -1
  154. package/dist/federation/delivery.d.ts.map +1 -1
  155. package/dist/federation/delivery.js +29 -21
  156. package/dist/federation/delivery.js.map +1 -1
  157. package/dist/federation/mount.d.ts.map +1 -1
  158. package/dist/federation/mount.js +3 -1
  159. package/dist/federation/mount.js.map +1 -1
  160. package/dist/index.d.ts +6 -5
  161. package/dist/index.d.ts.map +1 -1
  162. package/dist/index.js +5 -1
  163. package/dist/index.js.map +1 -1
  164. package/dist/indexnow.js +2 -2
  165. package/dist/indexnow.js.map +1 -1
  166. package/dist/indieauth/activity-log.d.ts +2 -0
  167. package/dist/indieauth/activity-log.d.ts.map +1 -1
  168. package/dist/indieauth/activity-log.js +12 -2
  169. package/dist/indieauth/activity-log.js.map +1 -1
  170. package/dist/indieauth/app-activity.js +5 -1
  171. package/dist/indieauth/app-activity.js.map +1 -1
  172. package/dist/indieauth/bearer.d.ts +10 -6
  173. package/dist/indieauth/bearer.d.ts.map +1 -1
  174. package/dist/indieauth/bearer.js +28 -25
  175. package/dist/indieauth/bearer.js.map +1 -1
  176. package/dist/indieauth/client-id.d.ts +10 -0
  177. package/dist/indieauth/client-id.d.ts.map +1 -0
  178. package/dist/indieauth/client-id.js +30 -0
  179. package/dist/indieauth/client-id.js.map +1 -0
  180. package/dist/indieauth/connected-apps.d.ts +4 -0
  181. package/dist/indieauth/connected-apps.d.ts.map +1 -1
  182. package/dist/indieauth/connected-apps.js +149 -20
  183. package/dist/indieauth/connected-apps.js.map +1 -1
  184. package/dist/indieauth/consent.d.ts.map +1 -1
  185. package/dist/indieauth/consent.js +5 -1
  186. package/dist/indieauth/consent.js.map +1 -1
  187. package/dist/indieauth/grants.d.ts +2 -7
  188. package/dist/indieauth/grants.d.ts.map +1 -1
  189. package/dist/indieauth/grants.js.map +1 -1
  190. package/dist/indieauth/redeem.d.ts.map +1 -1
  191. package/dist/indieauth/redeem.js +10 -5
  192. package/dist/indieauth/redeem.js.map +1 -1
  193. package/dist/indieauth/request.d.ts +8 -4
  194. package/dist/indieauth/request.d.ts.map +1 -1
  195. package/dist/indieauth/request.js +29 -30
  196. package/dist/indieauth/request.js.map +1 -1
  197. package/dist/indieauth/token.js +2 -2
  198. package/dist/indieauth/token.js.map +1 -1
  199. package/dist/indieauth/tokens.d.ts +28 -7
  200. package/dist/indieauth/tokens.d.ts.map +1 -1
  201. package/dist/indieauth/tokens.js +26 -5
  202. package/dist/indieauth/tokens.js.map +1 -1
  203. package/dist/micropub/create.d.ts +26 -0
  204. package/dist/micropub/create.d.ts.map +1 -1
  205. package/dist/micropub/create.js +158 -13
  206. package/dist/micropub/create.js.map +1 -1
  207. package/dist/micropub/endpoint.d.ts +4 -5
  208. package/dist/micropub/endpoint.d.ts.map +1 -1
  209. package/dist/micropub/endpoint.js +83 -25
  210. package/dist/micropub/endpoint.js.map +1 -1
  211. package/dist/micropub/media.d.ts +6 -2
  212. package/dist/micropub/media.d.ts.map +1 -1
  213. package/dist/micropub/media.js +43 -37
  214. package/dist/micropub/media.js.map +1 -1
  215. package/dist/micropub/update.d.ts +13 -3
  216. package/dist/micropub/update.d.ts.map +1 -1
  217. package/dist/micropub/update.js +60 -7
  218. package/dist/micropub/update.js.map +1 -1
  219. package/dist/notify.d.ts.map +1 -1
  220. package/dist/notify.js +2 -5
  221. package/dist/notify.js.map +1 -1
  222. package/dist/web/absolute-urls.d.ts +2 -0
  223. package/dist/web/absolute-urls.d.ts.map +1 -0
  224. package/dist/web/absolute-urls.js +22 -0
  225. package/dist/web/absolute-urls.js.map +1 -0
  226. package/dist/web/admin-bar.d.ts.map +1 -1
  227. package/dist/web/admin-bar.js +25 -0
  228. package/dist/web/admin-bar.js.map +1 -1
  229. package/dist/web/context.d.ts +29 -1
  230. package/dist/web/context.d.ts.map +1 -1
  231. package/dist/web/context.js +33 -0
  232. package/dist/web/context.js.map +1 -1
  233. package/dist/web/conversation.d.ts.map +1 -1
  234. package/dist/web/conversation.js +5 -4
  235. package/dist/web/conversation.js.map +1 -1
  236. package/dist/web/documents.d.ts +34 -10
  237. package/dist/web/documents.d.ts.map +1 -1
  238. package/dist/web/documents.js +41 -15
  239. package/dist/web/documents.js.map +1 -1
  240. package/dist/web/feed-item.d.ts +25 -6
  241. package/dist/web/feed-item.d.ts.map +1 -1
  242. package/dist/web/feed-item.js +31 -7
  243. package/dist/web/feed-item.js.map +1 -1
  244. package/dist/web/feed-json.d.ts +3 -1
  245. package/dist/web/feed-json.d.ts.map +1 -1
  246. package/dist/web/feed-json.js +2 -0
  247. package/dist/web/feed-json.js.map +1 -1
  248. package/dist/web/feed-source.d.ts +6 -0
  249. package/dist/web/feed-source.d.ts.map +1 -1
  250. package/dist/web/feed-source.js.map +1 -1
  251. package/dist/web/feeds.d.ts.map +1 -1
  252. package/dist/web/feeds.js +2 -0
  253. package/dist/web/feeds.js.map +1 -1
  254. package/dist/web/html-attributes.d.ts +3 -0
  255. package/dist/web/html-attributes.d.ts.map +1 -0
  256. package/dist/web/html-attributes.js +15 -0
  257. package/dist/web/html-attributes.js.map +1 -0
  258. package/dist/web/index.d.ts +1 -1
  259. package/dist/web/index.d.ts.map +1 -1
  260. package/dist/web/index.js +1 -1
  261. package/dist/web/index.js.map +1 -1
  262. package/dist/web/negotiate.d.ts +2 -0
  263. package/dist/web/negotiate.d.ts.map +1 -1
  264. package/dist/web/negotiate.js +2 -1
  265. package/dist/web/negotiate.js.map +1 -1
  266. package/dist/web/render.d.ts +13 -4
  267. package/dist/web/render.d.ts.map +1 -1
  268. package/dist/web/render.js +25 -12
  269. package/dist/web/render.js.map +1 -1
  270. package/dist/web/routes.d.ts.map +1 -1
  271. package/dist/web/routes.js +26 -11
  272. package/dist/web/routes.js.map +1 -1
  273. package/dist/webmention/fetch-public.d.ts +3 -1
  274. package/dist/webmention/fetch-public.d.ts.map +1 -1
  275. package/dist/webmention/fetch-public.js +17 -11
  276. package/dist/webmention/fetch-public.js.map +1 -1
  277. package/dist/webmention/reply-context.d.ts +18 -5
  278. package/dist/webmention/reply-context.d.ts.map +1 -1
  279. package/dist/webmention/reply-context.js +118 -19
  280. package/dist/webmention/reply-context.js.map +1 -1
  281. package/dist/webmention/reply-contexts.d.ts +17 -14
  282. package/dist/webmention/reply-contexts.d.ts.map +1 -1
  283. package/dist/webmention/reply-contexts.js +32 -21
  284. package/dist/webmention/reply-contexts.js.map +1 -1
  285. package/dist/webmention/service.d.ts.map +1 -1
  286. package/dist/webmention/service.js +7 -2
  287. package/dist/webmention/service.js.map +1 -1
  288. package/package.json +9 -9
  289. package/themes/default/README.md +45 -7
  290. package/themes/default/layouts/base.njk +2 -0
  291. package/themes/default/layouts/post.njk +12 -7
  292. package/themes/default/partials/citations.njk +12 -3
  293. package/themes/default/partials/cited-page.njk +41 -0
  294. package/themes/default/partials/location.njk +12 -0
  295. package/themes/default/partials/post-list.njk +10 -5
  296. package/themes/default/partials/read.njk +5 -0
  297. package/themes/default/partials/reply-context.njk +2 -9
package/README.md CHANGED
@@ -94,12 +94,13 @@ site (or `npx geekity`, or a `package.json` script, which is how the generated
94
94
  | `geekity rebuild` | Delete `data/geekity.db` and build it again from the files. |
95
95
  | `geekity resend --all`, `<slug>...` | Send announced posts to every follower and relay again, as they now read. See [Quote posts](#quote-posts). |
96
96
  | `geekity maintenance on`, `off`, `status` | Take the public site down on purpose with a 503 and `Retry-After`, or bring it back, without a restart. `on --until <time>` names when it should be back. |
97
+ | `geekity strip-metadata` | Remove location and camera metadata from files already in `content/uploads`. See [The media library](#the-media-library). |
97
98
  | `geekity user add <name>` | Create an admin account, so a site can get its first login without the setup screen. |
98
99
  | `geekity import wordpress-actor <name>` | Bring one person across from the WordPress ActivityPub plugin: their key pair, the actor id their followers hold, the plugin's numeric actor id, and their followers. |
99
100
  | `geekity --help`, `-h` | The same table, on the terminal. |
100
101
  | `geekity --version` | The installed version. |
101
102
 
102
- `serve`, `sync`, `rebuild`, `resend`, `maintenance`, `user add` and `import wordpress-actor` take
103
+ `serve`, `sync`, `rebuild`, `resend`, `maintenance`, `strip-metadata`, `user add` and `import wordpress-actor` take
103
104
  `--config <file>`; without it they look for `geekity.config.ts`, then
104
105
  `geekity.config.js`, then `geekity.config.mjs` in the working directory, and run
105
106
  on defaults if there is none.
@@ -267,29 +268,29 @@ export default defineConfig({
267
268
  Every field is optional. Relative directories resolve against the working
268
269
  directory; absolute ones are used as given.
269
270
 
270
- | Field | Default | Environment override | Meaning |
271
- | ------------------ | ------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
272
- | `port` | `3000` | `GEEKITY_PORT`, then `PORT` | Port the HTTP server listens on. `0` picks a free one. |
273
- | `contentDir` | `<cwd>/content` | `GEEKITY_CONTENT_DIR` | Markdown content. |
274
- | `dataDir` | `<cwd>/data` | `GEEKITY_DATA_DIR` | Derived state — the SQLite index, the image variants — and the two things in it that are not derived and must be backed up: `users.json` and, under `keys/`, each user's actor key pairs. |
275
- | `themesDir` | `<cwd>/themes` | `GEEKITY_THEMES_DIR` | The site's themes, one directory per theme, each with a `theme.json`. Which one is in use is the `theme` setting in `site.json`, not a path. Need not exist. |
276
- | `baseUrl` | `http://localhost:<port>` | `GEEKITY_BASE_URL` | Public origin for canonical URLs, feeds and ActivityPub ids. A trailing slash is stripped. |
277
- | `watch` | `true` | `GEEKITY_WATCH` | Watch `contentDir` while serving and keep the index in step. |
278
- | `sessionLifetime` | `1209600` (14 days) | `GEEKITY_SESSION_LIFETIME` | How long an admin login lasts, in seconds. |
279
- | `loginAttempts` | `5` | `GEEKITY_LOGIN_ATTEMPTS` | Failed sign-ins a username or an address may make before it is locked out. |
280
- | `loginLockout` | `900` (15 minutes) | `GEEKITY_LOGIN_LOCKOUT` | How long the first lockout lasts, in seconds. See [Login hardening](#login-hardening). |
281
- | `trustProxy` | `false` | `GEEKITY_TRUST_PROXY` | Believe `X-Forwarded-For` when deciding which address a sign-in came from. |
282
- | `accessLog` | `false`; `true` under `geekity serve` | `GEEKITY_ACCESS_LOG` | One line per request on stdout: method, path with query, status, duration. See [The access log](#the-access-log). |
283
- | `accessLogAddress` | `false` | `GEEKITY_ACCESS_LOG_ADDRESS` | Put the client address on the end of each access-log line. `trustProxy` decides which address that is. |
284
- | `accessLogWriter` | stdout | — | Where the lines go instead. See [The access log](#the-access-log). |
285
- | `maintenance` | `false` | `GEEKITY_MAINTENANCE` | Keep the site in maintenance mode, answering 503, until a restart without it. |
286
- | `compression` | `true` | `GEEKITY_COMPRESSION` | Compress text responses with brotli or gzip. Turn it off behind a proxy that compresses. See [Compression](#compression). |
287
- | `securityHeaders` | [see below](#security-headers) | — | Headers every response carries. A string replaces a default or adds a header, `false` removes one. |
288
- | `onDocumentChange` | none | — | Hook run for every change to the index. See [Hooks](#hooks). |
289
- | `onPublish` | none | — | Hook run when a document becomes visible. See [Hooks](#hooks). |
290
- | `federation` | `{}` | — | Federation stores and guards. See [Federation](#federation). |
291
- | `commentChecker` | Akismet | — | A spam checker of the site's own, which wins over the key in `data/akismet.json`. See [Akismet](#akismet). |
292
- | `mail` | `{}` | — | Mail provider, retries, backoff and logger. See [Email](#email). |
271
+ | Field | Default | Environment override | Meaning |
272
+ | ------------------ | ------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
273
+ | `port` | `3000` | `GEEKITY_PORT`, then `PORT` | Port the HTTP server listens on. `0` picks a free one. |
274
+ | `contentDir` | `<cwd>/content` | `GEEKITY_CONTENT_DIR` | Markdown content. |
275
+ | `dataDir` | `<cwd>/data` | `GEEKITY_DATA_DIR` | Derived state — the SQLite index, the image variants — and the files that are not derived and must be backed up, such as `users.json`, the actor key pairs under `keys/` and `locations.json`. [Two directories](#two-directories-content-and-data) lists them. |
276
+ | `themesDir` | `<cwd>/themes` | `GEEKITY_THEMES_DIR` | The site's themes, one directory per theme, each with a `theme.json`. Which one is in use is the `theme` setting in `site.json`, not a path. Need not exist. |
277
+ | `baseUrl` | `http://localhost:<port>` | `GEEKITY_BASE_URL` | Public origin for canonical URLs, feeds and ActivityPub ids. A trailing slash is stripped. |
278
+ | `watch` | `true` | `GEEKITY_WATCH` | Watch `contentDir` while serving and keep the index in step. |
279
+ | `sessionLifetime` | `1209600` (14 days) | `GEEKITY_SESSION_LIFETIME` | How long an admin login lasts, in seconds. |
280
+ | `loginAttempts` | `5` | `GEEKITY_LOGIN_ATTEMPTS` | Failed sign-ins a username or an address may make before it is locked out. |
281
+ | `loginLockout` | `900` (15 minutes) | `GEEKITY_LOGIN_LOCKOUT` | How long the first lockout lasts, in seconds. See [Login hardening](#login-hardening). |
282
+ | `trustProxy` | `false` | `GEEKITY_TRUST_PROXY` | Believe `X-Forwarded-For` when deciding which address a sign-in came from. |
283
+ | `accessLog` | `false`; `true` under `geekity serve` | `GEEKITY_ACCESS_LOG` | One line per request on stdout: method, path with query, status, duration. See [The access log](#the-access-log). |
284
+ | `accessLogAddress` | `false` | `GEEKITY_ACCESS_LOG_ADDRESS` | Put the client address on the end of each access-log line. `trustProxy` decides which address that is. |
285
+ | `accessLogWriter` | stdout | — | Where the lines go instead. See [The access log](#the-access-log). |
286
+ | `maintenance` | `false` | `GEEKITY_MAINTENANCE` | Keep the site in maintenance mode, answering 503, until a restart without it. |
287
+ | `compression` | `true` | `GEEKITY_COMPRESSION` | Compress text responses with brotli or gzip. Turn it off behind a proxy that compresses. See [Compression](#compression). |
288
+ | `securityHeaders` | [see below](#security-headers) | — | Headers every response carries. A string replaces a default or adds a header, `false` removes one. |
289
+ | `onDocumentChange` | none | — | Hook run for every change to the index. See [Hooks](#hooks). |
290
+ | `onPublish` | none | — | Hook run when a document becomes visible. See [Hooks](#hooks). |
291
+ | `federation` | `{}` | — | Federation stores and guards. See [Federation](#federation). |
292
+ | `commentChecker` | Akismet | — | A spam checker of the site's own, which wins over the key in `data/akismet.json`. See [Akismet](#akismet). |
293
+ | `mail` | `{}` | — | Mail provider, retries, backoff and logger. See [Email](#email). |
293
294
 
294
295
  Precedence is environment variable, then config file, then default, so a host
295
296
  can override anything without editing the site. A boolean environment variable
@@ -352,7 +353,7 @@ the same directory reads all of it, and everything in it is meant to be public:
352
353
  | Path | What it holds |
353
354
  | -------------------------------------------------- | ------------------------------------------------------------------------ |
354
355
  | `content/posts/`, `content/pages/` | The Markdown documents, `_trash/` included. |
355
- | `content/uploads/` | Uploaded files exactly as they arrived. |
356
+ | `content/uploads/` | Uploaded files, with their location and camera metadata removed. |
356
357
  | `content/_data/site.json` | Every site setting. |
357
358
  | `content/_data/federation/followers.json` | Who follows the site. |
358
359
  | `content/_data/federation/inbox/{yyyy}-{mm}.jsonl` | Every activity the inbox was handed, one per line. |
@@ -364,6 +365,7 @@ the same directory reads all of it, and everything in it is meant to be public:
364
365
  | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
365
366
  | `data/users.json` | Usernames and argon2id password hashes. Mode `0600`. |
366
367
  | `data/keys/` | Each user's actor key pairs as JWK files. Mode `0600`. **Losing these breaks federation.** |
368
+ | `data/locations.json` | Where each post was written, keyed by permalink (decision-29). Mode `0600`. |
367
369
  | `data/comment-salt` | What hides commenters' addresses in the published comment files. Mode `0600`. |
368
370
  | `data/comments/{slug}.json` | The emails of that post's commenters, and whether each asked to hear about replies, keyed by comment id. Mode `0600`. |
369
371
  | `data/akismet.json` | The Akismet key, and what `verify-key` last said about it. Mode `0600`. |
@@ -1517,25 +1519,28 @@ This is every piece of personal data the CMS stores, where it is, and how long
1517
1519
  it stays. "Kept" means until somebody deletes it, unless a retention period
1518
1520
  says otherwise.
1519
1521
 
1520
- | What | Where | How long |
1521
- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------ |
1522
- | A commenter's name, website and words | `content/_data/comments/{slug}.json`, public and in git | Kept. Erasing on request replaces the name and drops the website. |
1523
- | A commenter's email, and whether they asked to be told about replies | `data/comments/{slug}.json`, mode `0600`, keyed by comment id | `commentEmailRetentionDays`: forever unless set, 180 on a new site. |
1524
- | A salted hash of the address a comment, webmention or contact message came from (the address itself is never stored) | The comment file, or the contact message file | `addressHashRetentionDays`: forever unless set, 30 on a new site. |
1525
- | A webmention's author name, website, avatar URL and the source page's words | `content/_data/comments/{slug}.json` | Kept, as a copy of a page that is public already. |
1526
- | A contact message: the sender's name, email, subject and message | `data/contact/{id}.json`, mode `0600` | `contactMessageRetentionDays`: forever unless set, 365 on a new site. |
1527
- | The addresses that unsubscribed from reply notices | `data/comment-optouts.json`, mode `0600` | Kept, so the site goes on not writing to them. Erasing on request removes one. |
1528
- | Followers: actor id, handle, display name, avatar URL, profile URL | `content/_data/federation/{username}/followers.json`, in git | Until they unfollow. |
1529
- | Inbound likes, boosts, replies and quotes, with the actor who sent them | `content/_data/federation/inbox/{yyyy}-{mm}.jsonl`, in git | Kept. |
1530
- | What a reply shows of the post it answers: its title, words and author | `content/_data/replyContexts.json`, in git | Kept. |
1531
- | Other actors who liked, boosted, answered or quoted: handle, display name, avatar URL, profile URL | `data/geekity.db`, fetched from their actor document | A cache. Refetched weekly while the inbox log names them. |
1532
- | Remote avatars, shrunk | `data/avatars/` | Deleted by the avatar sweep once nothing shown names them. |
1533
- | Users: username, email, argon2id password hash, profile | `data/users.json`, mode `0600` | Until the user is deleted. |
1534
- | The apps a user signed in to with IndieAuth: app, scopes, hashes of its tokens, when they expire | `data/indieauth-tokens.json`, mode `0600` | Until the user is deleted, or 60 days after the app last refreshed. |
1535
- | Recent IndieAuth and Micropub requests: app, username, what it sent (no secrets, values cut short), the answer | `data/indieauth-activity.json`, mode `0600` | The last 100 requests, none older than 14 days. |
1536
- | An index of all of the above, and sessions, reset tokens and spent link tokens | `data/geekity.db` | A cache of the files. Sessions and tokens are pruned when they expire. |
1537
- | Client addresses in the rate limits | Memory | Until the window passes or the site restarts. |
1538
- | Client addresses in the access log | stdout, and whatever collects it | Only with `accessLogAddress` on. The collector keeps them. |
1522
+ | What | Where | How long |
1523
+ | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
1524
+ | A commenter's name, website and words | `content/_data/comments/{slug}.json`, public and in git | Kept. Erasing on request replaces the name and drops the website. |
1525
+ | A commenter's email, and whether they asked to be told about replies | `data/comments/{slug}.json`, mode `0600`, keyed by comment id | `commentEmailRetentionDays`: forever unless set, 180 on a new site. |
1526
+ | A salted hash of the address a comment, webmention or contact message came from (the address itself is never stored) | The comment file, or the contact message file | `addressHashRetentionDays`: forever unless set, 30 on a new site. |
1527
+ | A webmention's author name, website, avatar URL and the source page's words | `content/_data/comments/{slug}.json` | Kept, as a copy of a page that is public already. |
1528
+ | A contact message: the sender's name, email, subject and message | `data/contact/{id}.json`, mode `0600` | `contactMessageRetentionDays`: forever unless set, 365 on a new site. |
1529
+ | The addresses that unsubscribed from reply notices | `data/comment-optouts.json`, mode `0600` | Kept, so the site goes on not writing to them. Erasing on request removes one. |
1530
+ | Followers: actor id, handle, display name, avatar URL, profile URL | `content/_data/federation/{username}/followers.json`, in git | Until they unfollow. |
1531
+ | Inbound likes, boosts, replies and quotes, with the actor who sent them | `content/_data/federation/inbox/{yyyy}-{mm}.jsonl`, in git | Kept. |
1532
+ | What a reply shows of the post it answers: its title, words and author | `content/_data/replyContexts.json`, in git | Kept. |
1533
+ | Other actors who liked, boosted, answered or quoted: handle, display name, avatar URL, profile URL | `data/geekity.db`, fetched from their actor document | A cache. Refetched weekly while the inbox log names them. |
1534
+ | Remote avatars, shrunk | `data/avatars/` | Deleted by the avatar sweep once nothing shown names them. |
1535
+ | Users: username, email, argon2id password hash, profile | `data/users.json`, mode `0600` | Until the user is deleted. |
1536
+ | The apps a user signed in to with IndieAuth: app, scopes, hashes of its tokens, when they expire | `data/indieauth-tokens.json`, mode `0600` | Until the user is deleted, or 60 days after the app last refreshed. |
1537
+ | Recent IndieAuth and Micropub requests: app, username, what it sent (no secrets, values cut short), the answer | `data/indieauth-activity.json`, mode `0600` | The last 100 requests, none older than 14 days. |
1538
+ | An index of all of the above, and sessions, reset tokens and spent link tokens | `data/geekity.db` | A cache of the files. Sessions and tokens are pruned when they expire. |
1539
+ | Client addresses in the rate limits | Memory | Until the window passes or the site restarts. |
1540
+ | Where and when a photo or video was taken, and the camera or phone (EXIF, XMP, IPTC, a video's `©xyz`) | Removed from `content/uploads/` on upload | Never stored. Older uploads keep it until `geekity strip-metadata`; git history keeps the old bytes until rewritten. |
1541
+ | Where the author wrote a post or checked in: coordinates, accuracy, and the place's name, locality, region and country | `data/locations.json`, mode `0600`, keyed by permalink | Kept until the author clears it. Published only as far as **Settings > Privacy** allows, nothing by default. |
1542
+ | Micropub properties a post was sent that the site does not understand, such as an RSVP | `data/kept-properties.json`, mode `0600`, keyed by permalink | Kept until an update deletes them. Never published. |
1543
+ | Client addresses in the access log | stdout, and whatever collects it | Only with `accessLogAddress` on. The collector keeps them. |
1539
1544
 
1540
1545
  Two services outside the site see personal data when a site turns them on.
1541
1546
  Akismet is sent a commenter's or sender's address, user agent, referrer, name,
@@ -1704,6 +1709,48 @@ cacheable, and a URL that answered differently for one viewer would be cached
1704
1709
  and served to the rest. Use the editor's Preview button, which renders the post
1705
1710
  through the theme's own layout at an admin URL.
1706
1711
 
1712
+ ## Unlisted posts
1713
+
1714
+ An unlisted post is published at its URL and left off every list. Choose
1715
+ Unlisted in the editor's Visibility field, send `visibility=unlisted` from a
1716
+ Micropub app, or write it into the front matter:
1717
+
1718
+ ```yaml
1719
+ visibility: unlisted
1720
+ ```
1721
+
1722
+ Public is the key's absence, and the editor removes the key when you choose
1723
+ Public again. The site has no private posts. A value other than `public` or
1724
+ `unlisted`, such as a hand-typed `visibility: private`, hides the post the way
1725
+ `draft: true` does: its URL answers 404 to anybody not signed in, it is on no
1726
+ list, and a post the followers already hold is withdrawn with a `Delete`. The
1727
+ editor shows the value as it is stored, marks the post Hidden in the list, and
1728
+ keeps the value through a save until you choose Public or Unlisted.
1729
+
1730
+ The rule works for pages too.
1731
+
1732
+ | Where | An unlisted post |
1733
+ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
1734
+ | Its permalink, in every representation | 200, with `X-Robots-Tag: noindex`. The default theme also prints `<meta name="robots" content="noindex">`. |
1735
+ | The home page, tag, category and author archives, the `archive: true` page | Absent, and not counted in a pager or a tag count. |
1736
+ | Previous and next links under other posts, the front page's recent posts | Absent. |
1737
+ | Every RSS, Atom and JSON feed, and the site-wide comments feed | Absent. Saving one pings no notify server. |
1738
+ | The sitemap, search (HTML and JSON) and `llms.txt` | Absent. |
1739
+ | IndexNow | Not submitted. A public post that becomes unlisted is submitted once, so a search engine reads the `noindex`. |
1740
+ | The ActivityPub outbox and featured collection | Absent. |
1741
+ | Followers | Get the `Create` and every `Update`, addressed `to` the followers with Public in `cc`, which Mastodon shows as unlisted. Relays get nothing. |
1742
+ | Webmentions | Sent as for any post: the page is public. |
1743
+
1744
+ Changing a published post between public and unlisted sends its followers an
1745
+ `Update` with the new addressing.
1746
+
1747
+ In code, `isServed(document)` says whether the site serves a document at its
1748
+ URL and `isListed(document)` whether it also lists it. The index answers the
1749
+ same two rules in SQL. An Eleventy build with the example config reads the key
1750
+ too: an unlisted post gets its page, `noindex`, and no place in any collection,
1751
+ and an unrecognized value builds no page. See
1752
+ [Building the same content with Eleventy](#building-the-same-content-with-eleventy).
1753
+
1707
1754
  ## Hooks
1708
1755
 
1709
1756
  Two hooks let a site do something of its own when content changes. Both are
@@ -1868,8 +1915,8 @@ email address, notification preferences, a public profile and the
1868
1915
  [stored actor id](#a-users-stored-actor-id) they were published under
1869
1916
  elsewhere. Written atomically with `0600` permissions and serialised against
1870
1917
  itself, so two admins adding the same name at once cannot both succeed. It is in `data/` rather than `content/` because
1871
- `content/` is published with the site, and it is one of the two things under
1872
- `dataDir` that must be backed up — the users' actor keys are the other.
1918
+ `content/` is published with the site, and it is one of the files under
1919
+ `dataDir` that must be backed up.
1873
1920
 
1874
1921
  Sessions stay in SQLite, where the rest of the cache lives. They name a user by
1875
1922
  id and are joined against the file on every admin request, so a session whose
@@ -2088,6 +2135,60 @@ shows up without a restart. `readAltTexts(contentDir)` and
2088
2135
  `undescribedImages(html, library)` in `src/images/alt-text.ts` are what the
2089
2136
  editor's publish check and the federated `Image` attachments read.
2090
2137
 
2138
+ #### Location and camera metadata
2139
+
2140
+ A phone photo carries where it was taken, when, and on what camera, and the
2141
+ original under `content/uploads` is what the site serves at `/uploads/…`, puts
2142
+ in every `<picture>` as the fallback, and federates as a photo post's
2143
+ attachment. So `storeUpload` removes that metadata before the file is written,
2144
+ for every upload: the editor's control, the media library, the Micropub media
2145
+ endpoint and Micropub photos. It is not a setting.
2146
+
2147
+ | Format | What goes | What stays |
2148
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
2149
+ | JPEG | EXIF (GPS, camera, lens, serial, time), XMP, IPTC, comments, other APPn segments, data after the image | JFIF, the ICC profile, Adobe's colour flag, the orientation |
2150
+ | PNG | `eXIf`, `tEXt`, `zTXt`, `iTXt` (XMP), `tIME` and private chunks | Image and colour chunks, APNG animation, the orientation |
2151
+ | WebP | `EXIF`, `XMP ` and unknown chunks | The bitstream, alpha, animation, ICC profile, the orientation |
2152
+ | AVIF | The contents of the Exif and XMP items, overwritten with an empty EXIF block and an empty XMP packet | The picture, and its `irot`/`imir` orientation |
2153
+ | GIF | Comment extensions and application extensions other than looping and ICC (XMP lives in one) | Frames, timing, looping |
2154
+ | MP4, M4V | `udta` (the `©xyz` location, camera), `meta` (the QuickTime location key) and `uuid` (XMP) boxes, in the movie, its tracks and at the top level | Every other box |
2155
+
2156
+ Nothing is re-encoded. Segments, chunks and blocks are cut out and the pixel
2157
+ data is copied byte for byte, so a stored JPEG or PNG decodes to the same
2158
+ pixels as the upload. A picture taken sideways keeps a minimal EXIF block
2159
+ holding its orientation and nothing else, so it still shows the right way up.
2160
+ AVIF and MP4 keep their layout exactly: the metadata is overwritten in place
2161
+ and a removed box becomes a zero-filled `free` box of the same size, so no
2162
+ offset inside the file moves. A file whose structure cannot be followed far
2163
+ enough to find its metadata is refused with a 415 rather than stored as it is.
2164
+
2165
+ Audio (MP3, M4A, Ogg), WebM, PDF and the text formats are stored as they
2166
+ arrive. An episode's tags are its title and artwork, not where it was
2167
+ recorded.
2168
+
2169
+ Files uploaded before this version still carry their metadata. Strip them
2170
+ with
2171
+
2172
+ ```sh
2173
+ geekity strip-metadata
2174
+ ```
2175
+
2176
+ which rewrites each file under `content/uploads` that has something to remove,
2177
+ prints what it removed, keeps the file's modification time (the library sorts
2178
+ by it), and leaves alone, and names, a file it cannot read, exiting `1`. It is
2179
+ safe to run again: a clean file is left as it is, and a second run reports
2180
+ every file already clean. The derived variants under `data/images` never
2181
+ carried metadata.
2182
+
2183
+ A site that keeps `content/` in git still has the old bytes in its history,
2184
+ and so does every clone and fork of it. Removing them means rewriting that
2185
+ history. One way: run `geekity strip-metadata`, copy `content/uploads`
2186
+ somewhere outside the repository, remove it from every commit with
2187
+ [`git filter-repo`](https://github.com/newren/git-filter-repo)
2188
+ `--path content/uploads --invert-paths`, copy the stripped files back, commit
2189
+ them, and force-push. That changes every commit hash, so anybody else with a
2190
+ clone has to clone again.
2191
+
2091
2192
  ### Image variants
2092
2193
 
2093
2194
  `generateImageVariants(config, uploadPath)` derives one upload's copies with
@@ -2956,6 +3057,16 @@ its own item: RSS as `dc:language`, Atom as `xml:lang` on the `entry` and JSON
2956
3057
  Feed as the item's `language`. A post in the site's language, or whose `lang`
2957
3058
  is not a language tag, adds nothing.
2958
3059
 
3060
+ **Photos.** A post's `photo` front matter prints in every format's content,
3061
+ before the body, which is where the page prints it too. Each photo is a plain
3062
+ `<figure><img src alt></figure>` of the original upload, never the responsive
3063
+ variants, which a reader cannot resolve (decision-10). Its `src` is absolute on
3064
+ the site's base URL. Its `alt` is the post's own text, else the media library's,
3065
+ else empty. So a photo-only post reads in a feed reader as its photos. JSON Feed
3066
+ also names the item's main image as `image`: the post's `image` front matter,
3067
+ else its first photo. A photo is never an RSS `enclosure`, an Atom
3068
+ `rel="enclosure"` link or a JSON Feed attachment. Those carry a recording only.
3069
+
2959
3070
  ### RSS 2.0
2960
3071
 
2961
3072
  The channel carries `title`, `link`, `description` (the tagline), `language`
@@ -3058,7 +3169,7 @@ Feed has no field for a license and says nothing about one.
3058
3169
  [rfc4946]: https://www.rfc-editor.org/rfc/rfc4946
3059
3170
 
3060
3171
  A JSON Feed item carries `id` (the object id), `url` (the permalink), `title`,
3061
- `content_html`, `summary`, `date_published`, `date_modified`, `tags` (the
3172
+ `content_html`, `image` (see Photos above), `summary`, `date_published`, `date_modified`, `tags` (the
3062
3173
  terms) and `authors`; the feed carries `version`, `title`, `home_page_url`,
3063
3174
  `feed_url`, `description`, `authors` and `hubs`. Keys with nothing behind them
3064
3175
  are left out rather than sent empty.
@@ -3349,9 +3460,10 @@ dates — a page with no date in its front matter — carries no `<lastmod>`
3349
3460
  rather than an invented one.
3350
3461
 
3351
3462
  Nothing hidden reaches it. The sitemap is drawn from the same queries the
3352
- listings are and checked against the same `isPublicDocument`, so drafts, the
3353
- trash and posts whose date has not arrived are absent for the same reason
3354
- their permalinks 404.
3463
+ listings are and checked against the same `isListed`, so drafts, the trash and
3464
+ posts whose date has not arrived are absent for the same reason their
3465
+ permalinks 404, and an [unlisted post](#unlisted-posts) is absent for the same
3466
+ reason it is off the home page.
3355
3467
 
3356
3468
  The protocol caps one file at 50,000 URLs. Past that, `/sitemap.xml` becomes a
3357
3469
  `<sitemapindex>` naming `/sitemap-1.xml`, `/sitemap-2.xml` and so on, each
@@ -3684,6 +3796,7 @@ the rules the CMS follows that Eleventy does not know about on its own:
3684
3796
  | Rule | How the config does it |
3685
3797
  | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3686
3798
  | `draft: true` hides a document | An `addPreprocessor` that returns `false` for it. `BUILD_DRAFTS=1` builds drafts anyway, for a local preview. |
3799
+ | `visibility: unlisted` is listed nowhere | An `addPreprocessor` that sets `eleventyExcludeFromCollections` on the document, so a feed, sitemap or archive drawn from collections leaves it out, and sets `noindex` for the layout to print as `<meta name="robots" content="noindex">`. Any value other than `public` or `unlisted` returns `false`, as a draft does. |
3687
3800
  | A file with no `permalink` gets the CMS default | An `addPreprocessor` that fills in `/{yyyy}/{mm}/{slug}/` for a post and `/{slug}/` for a page, slugifying the title exactly as the CMS does. Front matter always wins; the CMS writes `permalink` into every file it saves, so this only matters for hand-authored files. |
3688
3801
  | `content/uploads/` is served at `/uploads/` | `addPassthroughCopy({ 'content/uploads': 'uploads' })`, plus an `ignores` entry for the same path. Without the ignore, an upload that happens to be Markdown would be copied _and_ rendered as a page; the CMS only ever indexes `posts/` and `pages/`. |
3689
3802
  | `content/_data/federation/` is data | `followers.json` is an ordinary data file in a namespaced `_data` subdirectory, so Eleventy hands it over as `federation.followers` without being told. The monthly inbox logs are JSON Lines, which Eleventy has no reader for, so the config registers one with `addDataExtension('jsonl', …)`: each month becomes an entry of `federation.inbox`, keyed by its `{yyyy}-{mm}` name. |
@@ -38,4 +38,6 @@
38
38
  }
39
39
  });
40
40
  })();
41
- </script></template></geekity-admin-bar>
41
+ </script></template></geekity-admin-bar>{% if notice %}
42
+ <p class="geekity-unpublished-notice" style="all: initial; display: block; box-sizing: border-box; margin: 0; padding: 8px 16px; background: #fcf9e8; color: #1d2327; border-bottom: 1px solid #dba617; font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif">{{ notice }}</p>
43
+ {%- endif %}