@geekity/cms 0.17.0 → 0.18.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 (250) hide show
  1. package/README.md +162 -50
  2. package/admin/pages/documents/editor.njk +63 -0
  3. package/admin/pages/documents/list.njk +2 -0
  4. package/admin/pages/indieauth/consent.njk +4 -0
  5. package/admin/pages/settings/privacy.njk +37 -0
  6. package/admin/pages/users/apps.njk +109 -3
  7. package/dist/admin/documents.d.ts +29 -1
  8. package/dist/admin/documents.d.ts.map +1 -1
  9. package/dist/admin/documents.js +88 -6
  10. package/dist/admin/documents.js.map +1 -1
  11. package/dist/admin/index.d.ts +1 -0
  12. package/dist/admin/index.d.ts.map +1 -1
  13. package/dist/admin/index.js +1 -0
  14. package/dist/admin/index.js.map +1 -1
  15. package/dist/admin/location-field.d.ts +26 -0
  16. package/dist/admin/location-field.d.ts.map +1 -0
  17. package/dist/admin/location-field.js +90 -0
  18. package/dist/admin/location-field.js.map +1 -0
  19. package/dist/admin/menu.d.ts.map +1 -1
  20. package/dist/admin/menu.js +1 -0
  21. package/dist/admin/menu.js.map +1 -1
  22. package/dist/admin/preview.d.ts.map +1 -1
  23. package/dist/admin/preview.js +18 -5
  24. package/dist/admin/preview.js.map +1 -1
  25. package/dist/admin/read-field.d.ts +23 -0
  26. package/dist/admin/read-field.d.ts.map +1 -0
  27. package/dist/admin/read-field.js +53 -0
  28. package/dist/admin/read-field.js.map +1 -0
  29. package/dist/admin/settings-page.d.ts +3 -3
  30. package/dist/admin/settings-page.js +2 -2
  31. package/dist/admin/settings-pages.d.ts +2 -2
  32. package/dist/admin/settings-pages.d.ts.map +1 -1
  33. package/dist/admin/settings-pages.js +4 -2
  34. package/dist/admin/settings-pages.js.map +1 -1
  35. package/dist/admin/settings-privacy.d.ts +3 -0
  36. package/dist/admin/settings-privacy.d.ts.map +1 -0
  37. package/dist/admin/settings-privacy.js +10 -0
  38. package/dist/admin/settings-privacy.js.map +1 -0
  39. package/dist/admin/settings-reading.js +2 -2
  40. package/dist/admin/settings-reading.js.map +1 -1
  41. package/dist/admin/settings.d.ts +18 -2
  42. package/dist/admin/settings.d.ts.map +1 -1
  43. package/dist/admin/settings.js +27 -0
  44. package/dist/admin/settings.js.map +1 -1
  45. package/dist/admin/templates.d.ts +1 -0
  46. package/dist/admin/templates.d.ts.map +1 -1
  47. package/dist/admin/templates.js +1 -0
  48. package/dist/admin/templates.js.map +1 -1
  49. package/dist/admin/uploads.d.ts.map +1 -1
  50. package/dist/admin/uploads.js +14 -1
  51. package/dist/admin/uploads.js.map +1 -1
  52. package/dist/cli.d.ts +1 -1
  53. package/dist/cli.d.ts.map +1 -1
  54. package/dist/cli.js +34 -0
  55. package/dist/cli.js.map +1 -1
  56. package/dist/comments/routes.js +2 -2
  57. package/dist/comments/routes.js.map +1 -1
  58. package/dist/contact/routes.js +2 -2
  59. package/dist/contact/routes.js.map +1 -1
  60. package/dist/content/location.d.ts +58 -0
  61. package/dist/content/location.d.ts.map +1 -0
  62. package/dist/content/location.js +208 -0
  63. package/dist/content/location.js.map +1 -0
  64. package/dist/content/locations.d.ts +9 -0
  65. package/dist/content/locations.d.ts.map +1 -0
  66. package/dist/content/locations.js +82 -0
  67. package/dist/content/locations.js.map +1 -0
  68. package/dist/content/metadata/gif.d.ts +3 -0
  69. package/dist/content/metadata/gif.d.ts.map +1 -0
  70. package/dist/content/metadata/gif.js +65 -0
  71. package/dist/content/metadata/gif.js.map +1 -0
  72. package/dist/content/metadata/index.d.ts +6 -0
  73. package/dist/content/metadata/index.d.ts.map +1 -0
  74. package/dist/content/metadata/index.js +24 -0
  75. package/dist/content/metadata/index.js.map +1 -0
  76. package/dist/content/metadata/isobmff.d.ts +22 -0
  77. package/dist/content/metadata/isobmff.d.ts.map +1 -0
  78. package/dist/content/metadata/isobmff.js +215 -0
  79. package/dist/content/metadata/isobmff.js.map +1 -0
  80. package/dist/content/metadata/jpeg.d.ts +3 -0
  81. package/dist/content/metadata/jpeg.d.ts.map +1 -0
  82. package/dist/content/metadata/jpeg.js +117 -0
  83. package/dist/content/metadata/jpeg.js.map +1 -0
  84. package/dist/content/metadata/png.d.ts +3 -0
  85. package/dist/content/metadata/png.d.ts.map +1 -0
  86. package/dist/content/metadata/png.js +74 -0
  87. package/dist/content/metadata/png.js.map +1 -0
  88. package/dist/content/metadata/shared.d.ts +17 -0
  89. package/dist/content/metadata/shared.d.ts.map +1 -0
  90. package/dist/content/metadata/shared.js +42 -0
  91. package/dist/content/metadata/shared.js.map +1 -0
  92. package/dist/content/metadata/sweep.d.ts +14 -0
  93. package/dist/content/metadata/sweep.d.ts.map +1 -0
  94. package/dist/content/metadata/sweep.js +54 -0
  95. package/dist/content/metadata/sweep.js.map +1 -0
  96. package/dist/content/metadata/tiff.d.ts +7 -0
  97. package/dist/content/metadata/tiff.d.ts.map +1 -0
  98. package/dist/content/metadata/tiff.js +56 -0
  99. package/dist/content/metadata/tiff.js.map +1 -0
  100. package/dist/content/metadata/webp.d.ts +3 -0
  101. package/dist/content/metadata/webp.d.ts.map +1 -0
  102. package/dist/content/metadata/webp.js +67 -0
  103. package/dist/content/metadata/webp.js.map +1 -0
  104. package/dist/content/post-type.d.ts +4 -2
  105. package/dist/content/post-type.d.ts.map +1 -1
  106. package/dist/content/post-type.js +9 -1
  107. package/dist/content/post-type.js.map +1 -1
  108. package/dist/content/read.d.ts +22 -0
  109. package/dist/content/read.d.ts.map +1 -0
  110. package/dist/content/read.js +67 -0
  111. package/dist/content/read.js.map +1 -0
  112. package/dist/content/store.d.ts +4 -4
  113. package/dist/content/store.d.ts.map +1 -1
  114. package/dist/content/store.js +19 -39
  115. package/dist/content/store.js.map +1 -1
  116. package/dist/content/sync.d.ts.map +1 -1
  117. package/dist/content/sync.js +3 -13
  118. package/dist/content/sync.js.map +1 -1
  119. package/dist/content/visibility.d.ts +11 -0
  120. package/dist/content/visibility.d.ts.map +1 -0
  121. package/dist/content/visibility.js +14 -0
  122. package/dist/content/visibility.js.map +1 -0
  123. package/dist/federation/article.d.ts.map +1 -1
  124. package/dist/federation/article.js +41 -14
  125. package/dist/federation/article.js.map +1 -1
  126. package/dist/federation/delivery.d.ts +3 -1
  127. package/dist/federation/delivery.d.ts.map +1 -1
  128. package/dist/federation/delivery.js +6 -5
  129. package/dist/federation/delivery.js.map +1 -1
  130. package/dist/federation/mount.d.ts.map +1 -1
  131. package/dist/federation/mount.js +3 -1
  132. package/dist/federation/mount.js.map +1 -1
  133. package/dist/index.d.ts +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +5 -1
  136. package/dist/index.js.map +1 -1
  137. package/dist/indexnow.js +2 -2
  138. package/dist/indexnow.js.map +1 -1
  139. package/dist/indieauth/activity-log.d.ts +2 -0
  140. package/dist/indieauth/activity-log.d.ts.map +1 -1
  141. package/dist/indieauth/activity-log.js +12 -2
  142. package/dist/indieauth/activity-log.js.map +1 -1
  143. package/dist/indieauth/app-activity.js +5 -1
  144. package/dist/indieauth/app-activity.js.map +1 -1
  145. package/dist/indieauth/bearer.d.ts.map +1 -1
  146. package/dist/indieauth/bearer.js +17 -16
  147. package/dist/indieauth/bearer.js.map +1 -1
  148. package/dist/indieauth/client-id.d.ts +10 -0
  149. package/dist/indieauth/client-id.d.ts.map +1 -0
  150. package/dist/indieauth/client-id.js +30 -0
  151. package/dist/indieauth/client-id.js.map +1 -0
  152. package/dist/indieauth/connected-apps.d.ts +4 -0
  153. package/dist/indieauth/connected-apps.d.ts.map +1 -1
  154. package/dist/indieauth/connected-apps.js +149 -20
  155. package/dist/indieauth/connected-apps.js.map +1 -1
  156. package/dist/indieauth/consent.d.ts.map +1 -1
  157. package/dist/indieauth/consent.js +5 -1
  158. package/dist/indieauth/consent.js.map +1 -1
  159. package/dist/indieauth/grants.d.ts +2 -7
  160. package/dist/indieauth/grants.d.ts.map +1 -1
  161. package/dist/indieauth/grants.js.map +1 -1
  162. package/dist/indieauth/redeem.d.ts.map +1 -1
  163. package/dist/indieauth/redeem.js +10 -5
  164. package/dist/indieauth/redeem.js.map +1 -1
  165. package/dist/indieauth/request.d.ts +8 -4
  166. package/dist/indieauth/request.d.ts.map +1 -1
  167. package/dist/indieauth/request.js +29 -30
  168. package/dist/indieauth/request.js.map +1 -1
  169. package/dist/indieauth/token.js +1 -1
  170. package/dist/indieauth/token.js.map +1 -1
  171. package/dist/indieauth/tokens.d.ts +28 -7
  172. package/dist/indieauth/tokens.d.ts.map +1 -1
  173. package/dist/indieauth/tokens.js +26 -5
  174. package/dist/indieauth/tokens.js.map +1 -1
  175. package/dist/micropub/create.d.ts +22 -0
  176. package/dist/micropub/create.d.ts.map +1 -1
  177. package/dist/micropub/create.js +107 -8
  178. package/dist/micropub/create.js.map +1 -1
  179. package/dist/micropub/endpoint.d.ts.map +1 -1
  180. package/dist/micropub/endpoint.js +71 -20
  181. package/dist/micropub/endpoint.js.map +1 -1
  182. package/dist/micropub/media.d.ts +4 -2
  183. package/dist/micropub/media.d.ts.map +1 -1
  184. package/dist/micropub/media.js +41 -37
  185. package/dist/micropub/media.js.map +1 -1
  186. package/dist/micropub/update.d.ts +6 -2
  187. package/dist/micropub/update.d.ts.map +1 -1
  188. package/dist/micropub/update.js +31 -3
  189. package/dist/micropub/update.js.map +1 -1
  190. package/dist/notify.d.ts.map +1 -1
  191. package/dist/notify.js +2 -5
  192. package/dist/notify.js.map +1 -1
  193. package/dist/web/absolute-urls.d.ts +2 -0
  194. package/dist/web/absolute-urls.d.ts.map +1 -0
  195. package/dist/web/absolute-urls.js +22 -0
  196. package/dist/web/absolute-urls.js.map +1 -0
  197. package/dist/web/context.d.ts +29 -1
  198. package/dist/web/context.d.ts.map +1 -1
  199. package/dist/web/context.js +33 -0
  200. package/dist/web/context.js.map +1 -1
  201. package/dist/web/conversation.d.ts.map +1 -1
  202. package/dist/web/conversation.js +5 -4
  203. package/dist/web/conversation.js.map +1 -1
  204. package/dist/web/documents.d.ts +19 -10
  205. package/dist/web/documents.d.ts.map +1 -1
  206. package/dist/web/documents.js +28 -13
  207. package/dist/web/documents.js.map +1 -1
  208. package/dist/web/feed-item.d.ts +25 -6
  209. package/dist/web/feed-item.d.ts.map +1 -1
  210. package/dist/web/feed-item.js +31 -7
  211. package/dist/web/feed-item.js.map +1 -1
  212. package/dist/web/feed-json.d.ts +3 -1
  213. package/dist/web/feed-json.d.ts.map +1 -1
  214. package/dist/web/feed-json.js +2 -0
  215. package/dist/web/feed-json.js.map +1 -1
  216. package/dist/web/feed-source.d.ts +6 -0
  217. package/dist/web/feed-source.d.ts.map +1 -1
  218. package/dist/web/feed-source.js.map +1 -1
  219. package/dist/web/feeds.d.ts.map +1 -1
  220. package/dist/web/feeds.js +2 -0
  221. package/dist/web/feeds.js.map +1 -1
  222. package/dist/web/html-attributes.d.ts +3 -0
  223. package/dist/web/html-attributes.d.ts.map +1 -0
  224. package/dist/web/html-attributes.js +15 -0
  225. package/dist/web/html-attributes.js.map +1 -0
  226. package/dist/web/index.d.ts +1 -1
  227. package/dist/web/index.d.ts.map +1 -1
  228. package/dist/web/index.js +1 -1
  229. package/dist/web/index.js.map +1 -1
  230. package/dist/web/negotiate.d.ts +2 -0
  231. package/dist/web/negotiate.d.ts.map +1 -1
  232. package/dist/web/negotiate.js +2 -1
  233. package/dist/web/negotiate.js.map +1 -1
  234. package/dist/web/render.d.ts +8 -0
  235. package/dist/web/render.d.ts.map +1 -1
  236. package/dist/web/render.js +5 -1
  237. package/dist/web/render.js.map +1 -1
  238. package/dist/web/routes.d.ts.map +1 -1
  239. package/dist/web/routes.js +14 -10
  240. package/dist/web/routes.js.map +1 -1
  241. package/dist/webmention/service.d.ts.map +1 -1
  242. package/dist/webmention/service.js +7 -2
  243. package/dist/webmention/service.js.map +1 -1
  244. package/package.json +1 -1
  245. package/themes/default/README.md +15 -0
  246. package/themes/default/layouts/base.njk +2 -0
  247. package/themes/default/layouts/post.njk +7 -1
  248. package/themes/default/partials/location.njk +12 -0
  249. package/themes/default/partials/post-list.njk +2 -0
  250. package/themes/default/partials/read.njk +5 -0
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,27 @@ 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: 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
+ | Client addresses in the access log | stdout, and whatever collects it | Only with `accessLogAddress` on. The collector keeps them. |
1539
1543
 
1540
1544
  Two services outside the site see personal data when a site turns them on.
1541
1545
  Akismet is sent a commenter's or sender's address, user agent, referrer, name,
@@ -1704,6 +1708,48 @@ cacheable, and a URL that answered differently for one viewer would be cached
1704
1708
  and served to the rest. Use the editor's Preview button, which renders the post
1705
1709
  through the theme's own layout at an admin URL.
1706
1710
 
1711
+ ## Unlisted posts
1712
+
1713
+ An unlisted post is published at its URL and left off every list. Choose
1714
+ Unlisted in the editor's Visibility field, send `visibility=unlisted` from a
1715
+ Micropub app, or write it into the front matter:
1716
+
1717
+ ```yaml
1718
+ visibility: unlisted
1719
+ ```
1720
+
1721
+ Public is the key's absence, and the editor removes the key when you choose
1722
+ Public again. The site has no private posts. A value other than `public` or
1723
+ `unlisted`, such as a hand-typed `visibility: private`, hides the post the way
1724
+ `draft: true` does: its URL answers 404, it is on no list, and a post the
1725
+ followers already hold is withdrawn with a `Delete`. The editor shows the value
1726
+ as it is stored, marks the post Hidden in the list, and keeps the value through
1727
+ a save until you choose Public or Unlisted.
1728
+
1729
+ The rule works for pages too.
1730
+
1731
+ | Where | An unlisted post |
1732
+ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
1733
+ | Its permalink, in every representation | 200, with `X-Robots-Tag: noindex`. The default theme also prints `<meta name="robots" content="noindex">`. |
1734
+ | The home page, tag, category and author archives, the `archive: true` page | Absent, and not counted in a pager or a tag count. |
1735
+ | Previous and next links under other posts, the front page's recent posts | Absent. |
1736
+ | Every RSS, Atom and JSON feed, and the site-wide comments feed | Absent. Saving one pings no notify server. |
1737
+ | The sitemap, search (HTML and JSON) and `llms.txt` | Absent. |
1738
+ | IndexNow | Not submitted. A public post that becomes unlisted is submitted once, so a search engine reads the `noindex`. |
1739
+ | The ActivityPub outbox and featured collection | Absent. |
1740
+ | Followers | Get the `Create` and every `Update`, addressed `to` the followers with Public in `cc`, which Mastodon shows as unlisted. Relays get nothing. |
1741
+ | Webmentions | Sent as for any post: the page is public. |
1742
+
1743
+ Changing a published post between public and unlisted sends its followers an
1744
+ `Update` with the new addressing.
1745
+
1746
+ In code, `isServed(document)` says whether the site serves a document at its
1747
+ URL and `isListed(document)` whether it also lists it. The index answers the
1748
+ same two rules in SQL. An Eleventy build with the example config reads the key
1749
+ too: an unlisted post gets its page, `noindex`, and no place in any collection,
1750
+ and an unrecognized value builds no page. See
1751
+ [Building the same content with Eleventy](#building-the-same-content-with-eleventy).
1752
+
1707
1753
  ## Hooks
1708
1754
 
1709
1755
  Two hooks let a site do something of its own when content changes. Both are
@@ -1868,8 +1914,8 @@ email address, notification preferences, a public profile and the
1868
1914
  [stored actor id](#a-users-stored-actor-id) they were published under
1869
1915
  elsewhere. Written atomically with `0600` permissions and serialised against
1870
1916
  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.
1917
+ `content/` is published with the site, and it is one of the files under
1918
+ `dataDir` that must be backed up.
1873
1919
 
1874
1920
  Sessions stay in SQLite, where the rest of the cache lives. They name a user by
1875
1921
  id and are joined against the file on every admin request, so a session whose
@@ -2088,6 +2134,60 @@ shows up without a restart. `readAltTexts(contentDir)` and
2088
2134
  `undescribedImages(html, library)` in `src/images/alt-text.ts` are what the
2089
2135
  editor's publish check and the federated `Image` attachments read.
2090
2136
 
2137
+ #### Location and camera metadata
2138
+
2139
+ A phone photo carries where it was taken, when, and on what camera, and the
2140
+ original under `content/uploads` is what the site serves at `/uploads/…`, puts
2141
+ in every `<picture>` as the fallback, and federates as a photo post's
2142
+ attachment. So `storeUpload` removes that metadata before the file is written,
2143
+ for every upload: the editor's control, the media library, the Micropub media
2144
+ endpoint and Micropub photos. It is not a setting.
2145
+
2146
+ | Format | What goes | What stays |
2147
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
2148
+ | 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 |
2149
+ | PNG | `eXIf`, `tEXt`, `zTXt`, `iTXt` (XMP), `tIME` and private chunks | Image and colour chunks, APNG animation, the orientation |
2150
+ | WebP | `EXIF`, `XMP ` and unknown chunks | The bitstream, alpha, animation, ICC profile, the orientation |
2151
+ | 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 |
2152
+ | GIF | Comment extensions and application extensions other than looping and ICC (XMP lives in one) | Frames, timing, looping |
2153
+ | 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 |
2154
+
2155
+ Nothing is re-encoded. Segments, chunks and blocks are cut out and the pixel
2156
+ data is copied byte for byte, so a stored JPEG or PNG decodes to the same
2157
+ pixels as the upload. A picture taken sideways keeps a minimal EXIF block
2158
+ holding its orientation and nothing else, so it still shows the right way up.
2159
+ AVIF and MP4 keep their layout exactly: the metadata is overwritten in place
2160
+ and a removed box becomes a zero-filled `free` box of the same size, so no
2161
+ offset inside the file moves. A file whose structure cannot be followed far
2162
+ enough to find its metadata is refused with a 415 rather than stored as it is.
2163
+
2164
+ Audio (MP3, M4A, Ogg), WebM, PDF and the text formats are stored as they
2165
+ arrive. An episode's tags are its title and artwork, not where it was
2166
+ recorded.
2167
+
2168
+ Files uploaded before this version still carry their metadata. Strip them
2169
+ with
2170
+
2171
+ ```sh
2172
+ geekity strip-metadata
2173
+ ```
2174
+
2175
+ which rewrites each file under `content/uploads` that has something to remove,
2176
+ prints what it removed, keeps the file's modification time (the library sorts
2177
+ by it), and leaves alone, and names, a file it cannot read, exiting `1`. It is
2178
+ safe to run again: a clean file is left as it is, and a second run reports
2179
+ every file already clean. The derived variants under `data/images` never
2180
+ carried metadata.
2181
+
2182
+ A site that keeps `content/` in git still has the old bytes in its history,
2183
+ and so does every clone and fork of it. Removing them means rewriting that
2184
+ history. One way: run `geekity strip-metadata`, copy `content/uploads`
2185
+ somewhere outside the repository, remove it from every commit with
2186
+ [`git filter-repo`](https://github.com/newren/git-filter-repo)
2187
+ `--path content/uploads --invert-paths`, copy the stripped files back, commit
2188
+ them, and force-push. That changes every commit hash, so anybody else with a
2189
+ clone has to clone again.
2190
+
2091
2191
  ### Image variants
2092
2192
 
2093
2193
  `generateImageVariants(config, uploadPath)` derives one upload's copies with
@@ -2956,6 +3056,16 @@ its own item: RSS as `dc:language`, Atom as `xml:lang` on the `entry` and JSON
2956
3056
  Feed as the item's `language`. A post in the site's language, or whose `lang`
2957
3057
  is not a language tag, adds nothing.
2958
3058
 
3059
+ **Photos.** A post's `photo` front matter prints in every format's content,
3060
+ before the body, which is where the page prints it too. Each photo is a plain
3061
+ `<figure><img src alt></figure>` of the original upload, never the responsive
3062
+ variants, which a reader cannot resolve (decision-10). Its `src` is absolute on
3063
+ the site's base URL. Its `alt` is the post's own text, else the media library's,
3064
+ else empty. So a photo-only post reads in a feed reader as its photos. JSON Feed
3065
+ also names the item's main image as `image`: the post's `image` front matter,
3066
+ else its first photo. A photo is never an RSS `enclosure`, an Atom
3067
+ `rel="enclosure"` link or a JSON Feed attachment. Those carry a recording only.
3068
+
2959
3069
  ### RSS 2.0
2960
3070
 
2961
3071
  The channel carries `title`, `link`, `description` (the tagline), `language`
@@ -3058,7 +3168,7 @@ Feed has no field for a license and says nothing about one.
3058
3168
  [rfc4946]: https://www.rfc-editor.org/rfc/rfc4946
3059
3169
 
3060
3170
  A JSON Feed item carries `id` (the object id), `url` (the permalink), `title`,
3061
- `content_html`, `summary`, `date_published`, `date_modified`, `tags` (the
3171
+ `content_html`, `image` (see Photos above), `summary`, `date_published`, `date_modified`, `tags` (the
3062
3172
  terms) and `authors`; the feed carries `version`, `title`, `home_page_url`,
3063
3173
  `feed_url`, `description`, `authors` and `hubs`. Keys with nothing behind them
3064
3174
  are left out rather than sent empty.
@@ -3349,9 +3459,10 @@ dates — a page with no date in its front matter — carries no `<lastmod>`
3349
3459
  rather than an invented one.
3350
3460
 
3351
3461
  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.
3462
+ listings are and checked against the same `isListed`, so drafts, the trash and
3463
+ posts whose date has not arrived are absent for the same reason their
3464
+ permalinks 404, and an [unlisted post](#unlisted-posts) is absent for the same
3465
+ reason it is off the home page.
3355
3466
 
3356
3467
  The protocol caps one file at 50,000 URLs. Past that, `/sitemap.xml` becomes a
3357
3468
  `<sitemapindex>` naming `/sitemap-1.xml`, `/sitemap-2.xml` and so on, each
@@ -3684,6 +3795,7 @@ the rules the CMS follows that Eleventy does not know about on its own:
3684
3795
  | Rule | How the config does it |
3685
3796
  | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3686
3797
  | `draft: true` hides a document | An `addPreprocessor` that returns `false` for it. `BUILD_DRAFTS=1` builds drafts anyway, for a local preview. |
3798
+ | `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
3799
  | 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
3800
  | `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
3801
  | `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. |
@@ -86,6 +86,27 @@
86
86
  {% endfor %}
87
87
  </datalist>
88
88
  </fieldset>
89
+ <fieldset class="admin-editor-enclosure admin-editor-location">
90
+ <legend>Location</legend>
91
+ <p class="admin-hint">
92
+ Kept in <code>data/locations.json</code>, not in the post’s file. What readers see of it
93
+ is chosen on Settings &gt; Privacy. Empty every box to remove it.
94
+ </p>
95
+ <label for="editor-location-geo">Coordinates</label>
96
+ <input id="editor-location-geo" name="{{ locationFields.geo }}" type="text" value="{{ form.location.geo }}" placeholder="48.85837, 2.29448" autocomplete="off" spellcheck="false" aria-describedby="editor-location-geo-hint" />
97
+ <p class="admin-hint" id="editor-location-geo-hint">Latitude, longitude, as decimal degrees. A <code>geo:</code> URI works too.</p>
98
+ <label for="editor-location-accuracy">Accuracy</label>
99
+ <input id="editor-location-accuracy" name="{{ locationFields.accuracy }}" type="text" inputmode="numeric" value="{{ form.location.accuracy }}" autocomplete="off" aria-describedby="editor-location-accuracy-hint" />
100
+ <p class="admin-hint" id="editor-location-accuracy-hint">Metres. Leave it empty if you do not know.</p>
101
+ <label for="editor-location-name">Place</label>
102
+ <input id="editor-location-name" name="{{ locationFields.name }}" type="text" value="{{ form.location.name }}" />
103
+ <label for="editor-location-locality">Locality</label>
104
+ <input id="editor-location-locality" name="{{ locationFields.locality }}" type="text" value="{{ form.location.locality }}" />
105
+ <label for="editor-location-region">Region</label>
106
+ <input id="editor-location-region" name="{{ locationFields.region }}" type="text" value="{{ form.location.region }}" />
107
+ <label for="editor-location-country">Country</label>
108
+ <input id="editor-location-country" name="{{ locationFields.country }}" type="text" value="{{ form.location.country }}" />
109
+ </fieldset>
89
110
  <fieldset class="admin-editor-enclosure">
90
111
  <legend>Recording</legend>
91
112
 
@@ -199,6 +220,29 @@
199
220
  <label for="editor-bookmark-of">Bookmark of</label>
200
221
  <input id="editor-bookmark-of" name="bookmark-of" type="url" value="{{ form.bookmarkOf }}" />
201
222
  <p class="admin-hint">The address of a post this one likes, reposts or bookmarks. Leave them empty for a post that cites nothing.</p>
223
+
224
+ <fieldset class="admin-editor-enclosure admin-editor-read">
225
+ <legend>Read</legend>
226
+ <label for="editor-read-status">Read status</label>
227
+ <select id="editor-read-status" name="{{ readFields.status }}">
228
+ <option value=""{% if form.readStatus == "" %} selected{% endif %}>Not a read</option>
229
+ {% for status in readStatuses %}
230
+ <option value="{{ status.value }}"{% if form.readStatus == status.value %} selected{% endif %}>{{ status.label }}</option>
231
+ {% endfor %}
232
+ {% if unrecognizedReadStatus %}
233
+ <option value="{{ unrecognizedReadStatus }}" selected>{{ unrecognizedReadStatus }} (not recognized)</option>
234
+ {% endif %}
235
+ </select>
236
+ <label for="editor-read-of-name">Title</label>
237
+ <input id="editor-read-of-name" name="{{ readFields.name }}" type="text" value="{{ form.readOf.name }}" />
238
+ <label for="editor-read-of-author">Author</label>
239
+ <input id="editor-read-of-author" name="{{ readFields.author }}" type="text" value="{{ form.readOf.author }}" />
240
+ <label for="editor-read-of-uid">Identifier</label>
241
+ <input id="editor-read-of-uid" name="{{ readFields.uid }}" type="text" value="{{ form.readOf.uid }}" placeholder="isbn:9780441478125" autocomplete="off" spellcheck="false" />
242
+ <label for="editor-read-of-url">Address</label>
243
+ <input id="editor-read-of-url" name="{{ readFields.url }}" type="url" value="{{ form.readOf.url }}" />
244
+ <p class="admin-hint">A book or paper this post says its author read. An identifier is an ISBN or a DOI, written isbn:… or doi:…. Choose Not a read and empty every box to remove it. A read is described by what it says, so leave its Description empty.</p>
245
+ </fieldset>
202
246
  {% endif %}
203
247
 
204
248
  <label for="editor-description">Description</label>
@@ -229,6 +273,25 @@
229
273
  <label for="editor-draft">Draft</label>
230
274
  </p>
231
275
 
276
+ <label for="editor-visibility">Visibility</label>
277
+ <select id="editor-visibility" name="visibility" aria-describedby="editor-visibility-hint">
278
+ {% if form.visibility.unrecognized is defined %}
279
+ <option value="{{ form.visibility.unrecognized }}" selected>{{ form.visibility.unrecognized }} (not recognized)</option>
280
+ {% endif %}
281
+ <option value="public"{% if form.visibility == "public" %} selected{% endif %}>Public</option>
282
+ <option value="unlisted"{% if form.visibility == "unlisted" %} selected{% endif %}>Unlisted</option>
283
+ </select>
284
+ <p class="admin-hint" id="editor-visibility-hint">
285
+ {% if form.visibility.unrecognized is defined %}
286
+ The front matter says <code>visibility: {{ form.visibility.unrecognized }}</code>, which this
287
+ site does not recognize. The {{ kind.singular }} is hidden until you choose Public or Unlisted:
288
+ its page answers 404 and it is not listed or sent to your followers.
289
+ {% endif %}
290
+ Unlisted writes <code>visibility: unlisted</code> into the front matter. The {{ kind.singular }}
291
+ keeps its page and still goes to your followers, but is left off the home page, archives,
292
+ feeds, the sitemap, search and llms.txt, and asks search engines not to index it.
293
+ </p>
294
+
232
295
  {% if kind.type == "post" %}
233
296
  {# TASK-207: shown at the top of the author's fediverse profile, from
234
297
  the featured collection their actor links. #}
@@ -69,6 +69,8 @@
69
69
  <span class="admin-status admin-status-draft">Draft</span>
70
70
  {% elif document.scheduled %}
71
71
  <span class="admin-status admin-status-scheduled">Scheduled</span>
72
+ {% elif document.hidden %}
73
+ <span class="admin-status admin-status-draft">Hidden</span>
72
74
  {% else %}
73
75
  <span class="admin-status">Published</span>
74
76
  {% endif %}
@@ -26,6 +26,10 @@
26
26
  {% endif %}
27
27
  </dl>
28
28
 
29
+ {% if withoutPkce %}
30
+ <p class="admin-hint">This app does not use PKCE, which protects a sign-in if the code it is handed is intercepted. It is let in only because it is on your list of apps allowed without PKCE, under Users &gt; Connected apps.</p>
31
+ {% endif %}
32
+
29
33
  {% if loopback %}
30
34
  <p class="admin-hint">This app runs on your own computer or network. Approve only if you started this sign-in yourself.</p>
31
35
  {% endif %}
@@ -0,0 +1,37 @@
1
+ {% extends "layouts/settings-page.njk" %}
2
+ {% import "components/fields.njk" as field %}
3
+
4
+ {% block settingsProblems %}
5
+ {{ field.problem('settings-location-sharing', problems.locationSharing) }}
6
+ {% endblock %}
7
+
8
+ {% block settingsFields %}
9
+ <h2>Location</h2>
10
+
11
+ {% set locationHint %}
12
+ A Micropub app such as Quill can send where a post was written, and the
13
+ editor has fields for it. The site keeps it in <code>data/locations.json</code>,
14
+ never in the post's file, so it stays out of <code>content/</code> and its git
15
+ history whatever is chosen here. Exact coordinates can say where you live or
16
+ are right now. Changing this changes every post at once, and nothing is
17
+ rewritten.
18
+ {% endset %}
19
+ {% call field.select('settings-location-sharing', fields.locationSharing, 'Location on posts',
20
+ error=problems.locationSharing, hint=locationHint, autofocus=true) %}
21
+ {% for option in [
22
+ ['none', 'Keep it, publish nothing'],
23
+ ['place', 'Publish the place only: its name, locality, region and country'],
24
+ ['exact', 'Publish the exact coordinates too']] %}
25
+ <option value="{{ option[0] }}"{% if option[0] == form.locationSharing %} selected{% endif %}>{{ option[1] }}</option>
26
+ {% endfor %}
27
+ {% endcall %}
28
+
29
+ <h2>Uploads</h2>
30
+
31
+ <p class="admin-hint">
32
+ Location and camera metadata is always removed from pictures and videos as
33
+ they are uploaded, whatever is chosen above. Files uploaded before this
34
+ version keep theirs until <code>geekity strip-metadata</code> is run over
35
+ <code>content/uploads/</code>; the package README describes the command.
36
+ </p>
37
+ {% endblock %}