@geekity/cms 0.3.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 (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +870 -0
  3. package/admin/layouts/_flash.njk +5 -0
  4. package/admin/layouts/base.njk +27 -0
  5. package/admin/layouts/dashboard.njk +78 -0
  6. package/admin/layouts/document-conflict.njk +49 -0
  7. package/admin/layouts/document-editor.njk +167 -0
  8. package/admin/layouts/document-list.njk +92 -0
  9. package/admin/layouts/federation.njk +145 -0
  10. package/admin/layouts/login.njk +23 -0
  11. package/admin/layouts/placeholder.njk +10 -0
  12. package/admin/layouts/settings.njk +116 -0
  13. package/admin/layouts/setup.njk +27 -0
  14. package/admin/layouts/shell.njk +37 -0
  15. package/admin/layouts/users.njk +91 -0
  16. package/admin/static/admin.css +726 -0
  17. package/admin/static/editor.js +30 -0
  18. package/dist/admin/assets.d.ts +26 -0
  19. package/dist/admin/assets.d.ts.map +1 -0
  20. package/dist/admin/assets.js +38 -0
  21. package/dist/admin/assets.js.map +1 -0
  22. package/dist/admin/credentials.d.ts +28 -0
  23. package/dist/admin/credentials.d.ts.map +1 -0
  24. package/dist/admin/credentials.js +39 -0
  25. package/dist/admin/credentials.js.map +1 -0
  26. package/dist/admin/documents.d.ts +134 -0
  27. package/dist/admin/documents.d.ts.map +1 -0
  28. package/dist/admin/documents.js +613 -0
  29. package/dist/admin/documents.js.map +1 -0
  30. package/dist/admin/federation.d.ts +194 -0
  31. package/dist/admin/federation.d.ts.map +1 -0
  32. package/dist/admin/federation.js +299 -0
  33. package/dist/admin/federation.js.map +1 -0
  34. package/dist/admin/flash.d.ts +24 -0
  35. package/dist/admin/flash.d.ts.map +1 -0
  36. package/dist/admin/flash.js +31 -0
  37. package/dist/admin/flash.js.map +1 -0
  38. package/dist/admin/index.d.ts +23 -0
  39. package/dist/admin/index.d.ts.map +1 -0
  40. package/dist/admin/index.js +15 -0
  41. package/dist/admin/index.js.map +1 -0
  42. package/dist/admin/passwords.d.ts +49 -0
  43. package/dist/admin/passwords.d.ts.map +1 -0
  44. package/dist/admin/passwords.js +122 -0
  45. package/dist/admin/passwords.js.map +1 -0
  46. package/dist/admin/preview.d.ts +18 -0
  47. package/dist/admin/preview.d.ts.map +1 -0
  48. package/dist/admin/preview.js +83 -0
  49. package/dist/admin/preview.js.map +1 -0
  50. package/dist/admin/routes.d.ts +54 -0
  51. package/dist/admin/routes.d.ts.map +1 -0
  52. package/dist/admin/routes.js +383 -0
  53. package/dist/admin/routes.js.map +1 -0
  54. package/dist/admin/session.d.ts +44 -0
  55. package/dist/admin/session.d.ts.map +1 -0
  56. package/dist/admin/session.js +69 -0
  57. package/dist/admin/session.js.map +1 -0
  58. package/dist/admin/settings.d.ts +194 -0
  59. package/dist/admin/settings.d.ts.map +1 -0
  60. package/dist/admin/settings.js +470 -0
  61. package/dist/admin/settings.js.map +1 -0
  62. package/dist/admin/store.d.ts +407 -0
  63. package/dist/admin/store.d.ts.map +1 -0
  64. package/dist/admin/store.js +704 -0
  65. package/dist/admin/store.js.map +1 -0
  66. package/dist/admin/templates.d.ts +36 -0
  67. package/dist/admin/templates.d.ts.map +1 -0
  68. package/dist/admin/templates.js +43 -0
  69. package/dist/admin/templates.js.map +1 -0
  70. package/dist/admin/uploads.d.ts +96 -0
  71. package/dist/admin/uploads.d.ts.map +1 -0
  72. package/dist/admin/uploads.js +179 -0
  73. package/dist/admin/uploads.js.map +1 -0
  74. package/dist/admin/users.d.ts +95 -0
  75. package/dist/admin/users.d.ts.map +1 -0
  76. package/dist/admin/users.js +233 -0
  77. package/dist/admin/users.js.map +1 -0
  78. package/dist/cli.d.ts +85 -0
  79. package/dist/cli.d.ts.map +1 -0
  80. package/dist/cli.js +424 -0
  81. package/dist/cli.js.map +1 -0
  82. package/dist/config.d.ts +172 -0
  83. package/dist/config.d.ts.map +1 -0
  84. package/dist/config.js +184 -0
  85. package/dist/config.js.map +1 -0
  86. package/dist/content/document.d.ts +66 -0
  87. package/dist/content/document.d.ts.map +1 -0
  88. package/dist/content/document.js +13 -0
  89. package/dist/content/document.js.map +1 -0
  90. package/dist/content/index.d.ts +17 -0
  91. package/dist/content/index.d.ts.map +1 -0
  92. package/dist/content/index.js +10 -0
  93. package/dist/content/index.js.map +1 -0
  94. package/dist/content/markdown.d.ts +3 -0
  95. package/dist/content/markdown.d.ts.map +1 -0
  96. package/dist/content/markdown.js +39 -0
  97. package/dist/content/markdown.js.map +1 -0
  98. package/dist/content/media.d.ts +65 -0
  99. package/dist/content/media.d.ts.map +1 -0
  100. package/dist/content/media.js +143 -0
  101. package/dist/content/media.js.map +1 -0
  102. package/dist/content/parser.d.ts +24 -0
  103. package/dist/content/parser.d.ts.map +1 -0
  104. package/dist/content/parser.js +165 -0
  105. package/dist/content/parser.js.map +1 -0
  106. package/dist/content/save.d.ts +60 -0
  107. package/dist/content/save.d.ts.map +1 -0
  108. package/dist/content/save.js +103 -0
  109. package/dist/content/save.js.map +1 -0
  110. package/dist/content/slug.d.ts +25 -0
  111. package/dist/content/slug.d.ts.map +1 -0
  112. package/dist/content/slug.js +75 -0
  113. package/dist/content/slug.js.map +1 -0
  114. package/dist/content/store.d.ts +133 -0
  115. package/dist/content/store.d.ts.map +1 -0
  116. package/dist/content/store.js +411 -0
  117. package/dist/content/store.js.map +1 -0
  118. package/dist/content/sync.d.ts +146 -0
  119. package/dist/content/sync.d.ts.map +1 -0
  120. package/dist/content/sync.js +421 -0
  121. package/dist/content/sync.js.map +1 -0
  122. package/dist/content/writer.d.ts +37 -0
  123. package/dist/content/writer.d.ts.map +1 -0
  124. package/dist/content/writer.js +97 -0
  125. package/dist/content/writer.js.map +1 -0
  126. package/dist/env.d.ts +48 -0
  127. package/dist/env.d.ts.map +1 -0
  128. package/dist/env.js +2 -0
  129. package/dist/env.js.map +1 -0
  130. package/dist/federation/actor.d.ts +55 -0
  131. package/dist/federation/actor.d.ts.map +1 -0
  132. package/dist/federation/actor.js +101 -0
  133. package/dist/federation/actor.js.map +1 -0
  134. package/dist/federation/article.d.ts +83 -0
  135. package/dist/federation/article.d.ts.map +1 -0
  136. package/dist/federation/article.js +169 -0
  137. package/dist/federation/article.js.map +1 -0
  138. package/dist/federation/delivery.d.ts +90 -0
  139. package/dist/federation/delivery.d.ts.map +1 -0
  140. package/dist/federation/delivery.js +289 -0
  141. package/dist/federation/delivery.js.map +1 -0
  142. package/dist/federation/federation.d.ts +75 -0
  143. package/dist/federation/federation.d.ts.map +1 -0
  144. package/dist/federation/federation.js +190 -0
  145. package/dist/federation/federation.js.map +1 -0
  146. package/dist/federation/followers.d.ts +43 -0
  147. package/dist/federation/followers.d.ts.map +1 -0
  148. package/dist/federation/followers.js +69 -0
  149. package/dist/federation/followers.js.map +1 -0
  150. package/dist/federation/inbox.d.ts +68 -0
  151. package/dist/federation/inbox.d.ts.map +1 -0
  152. package/dist/federation/inbox.js +171 -0
  153. package/dist/federation/inbox.js.map +1 -0
  154. package/dist/federation/index.d.ts +14 -0
  155. package/dist/federation/index.d.ts.map +1 -0
  156. package/dist/federation/index.js +10 -0
  157. package/dist/federation/index.js.map +1 -0
  158. package/dist/federation/keys.d.ts +27 -0
  159. package/dist/federation/keys.d.ts.map +1 -0
  160. package/dist/federation/keys.js +60 -0
  161. package/dist/federation/keys.js.map +1 -0
  162. package/dist/federation/mount.d.ts +23 -0
  163. package/dist/federation/mount.d.ts.map +1 -0
  164. package/dist/federation/mount.js +71 -0
  165. package/dist/federation/mount.js.map +1 -0
  166. package/dist/federation/paths.d.ts +85 -0
  167. package/dist/federation/paths.d.ts.map +1 -0
  168. package/dist/federation/paths.js +97 -0
  169. package/dist/federation/paths.js.map +1 -0
  170. package/dist/index.d.ts +106 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +148 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/init.d.ts +71 -0
  175. package/dist/init.d.ts.map +1 -0
  176. package/dist/init.js +125 -0
  177. package/dist/init.js.map +1 -0
  178. package/dist/web/assets.d.ts +65 -0
  179. package/dist/web/assets.d.ts.map +1 -0
  180. package/dist/web/assets.js +152 -0
  181. package/dist/web/assets.js.map +1 -0
  182. package/dist/web/context.d.ts +121 -0
  183. package/dist/web/context.d.ts.map +1 -0
  184. package/dist/web/context.js +112 -0
  185. package/dist/web/context.js.map +1 -0
  186. package/dist/web/documents.d.ts +28 -0
  187. package/dist/web/documents.d.ts.map +1 -0
  188. package/dist/web/documents.js +39 -0
  189. package/dist/web/documents.js.map +1 -0
  190. package/dist/web/feeds.d.ts +122 -0
  191. package/dist/web/feeds.d.ts.map +1 -0
  192. package/dist/web/feeds.js +229 -0
  193. package/dist/web/feeds.js.map +1 -0
  194. package/dist/web/index.d.ts +17 -0
  195. package/dist/web/index.d.ts.map +1 -0
  196. package/dist/web/index.js +10 -0
  197. package/dist/web/index.js.map +1 -0
  198. package/dist/web/negotiate.d.ts +219 -0
  199. package/dist/web/negotiate.d.ts.map +1 -0
  200. package/dist/web/negotiate.js +409 -0
  201. package/dist/web/negotiate.js.map +1 -0
  202. package/dist/web/pagination.d.ts +48 -0
  203. package/dist/web/pagination.d.ts.map +1 -0
  204. package/dist/web/pagination.js +30 -0
  205. package/dist/web/pagination.js.map +1 -0
  206. package/dist/web/render.d.ts +72 -0
  207. package/dist/web/render.d.ts.map +1 -0
  208. package/dist/web/render.js +73 -0
  209. package/dist/web/render.js.map +1 -0
  210. package/dist/web/routes.d.ts +31 -0
  211. package/dist/web/routes.d.ts.map +1 -0
  212. package/dist/web/routes.js +366 -0
  213. package/dist/web/routes.js.map +1 -0
  214. package/dist/web/templates.d.ts +34 -0
  215. package/dist/web/templates.d.ts.map +1 -0
  216. package/dist/web/templates.js +106 -0
  217. package/dist/web/templates.js.map +1 -0
  218. package/package.json +68 -0
  219. package/templates/site/content/_data/site.json +8 -0
  220. package/templates/site/content/pages/about.md +11 -0
  221. package/templates/site/content/pages/pages.json +3 -0
  222. package/templates/site/content/posts/2026-01-01-hello-world.md +34 -0
  223. package/templates/site/content/posts/posts.json +4 -0
  224. package/templates/site/geekity.config.ts +11 -0
  225. package/templates/site/gitignore +3 -0
  226. package/templates/site/server.ts +20 -0
  227. package/templates/site/tsconfig.json +18 -0
  228. package/themes/default/README.md +148 -0
  229. package/themes/default/layouts/404.njk +9 -0
  230. package/themes/default/layouts/base.njk +59 -0
  231. package/themes/default/layouts/home.njk +9 -0
  232. package/themes/default/layouts/page.njk +13 -0
  233. package/themes/default/layouts/post.njk +30 -0
  234. package/themes/default/layouts/tag.njk +19 -0
  235. package/themes/default/partials/pagination.njk +14 -0
  236. package/themes/default/partials/post-list.njk +24 -0
  237. package/themes/default/partials/tags.njk +12 -0
  238. package/themes/default/static/style.css +327 -0
@@ -0,0 +1,5 @@
1
+ {# Messages queued by the request that redirected here. They were cleared as
2
+ they were read, so this is the only page that shows them. #}
3
+ {% for entry in flash %}
4
+ <p class="admin-flash admin-flash-{{ entry.kind }}" role="status">{{ entry.message }}</p>
5
+ {% endfor %}
@@ -0,0 +1,27 @@
1
+ {# The admin's base layout. It lives outside the theme search path on purpose:
2
+ a site's theme/ can override any public template, and must never be able to
3
+ shadow the login form.
4
+
5
+ This is the bare page: head, stylesheet and one centred column. Screens that
6
+ need the navigation extend layouts/shell.njk, which extends this and wraps
7
+ the same content block in the admin chrome. Login and setup extend this one
8
+ directly, because a visitor who cannot log in yet has nowhere to navigate. #}
9
+ <!doctype html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14
+ <title>{% block title %}Admin{% endblock %}{% if site.title %} · {{ site.title }}{% endif %}</title>
15
+ <meta name="robots" content="noindex, nofollow" />
16
+ <link rel="stylesheet" href="{{ assetPrefix }}admin.css" />
17
+ </head>
18
+ <body>
19
+ {% block chrome %}{% endblock %}
20
+ {% block main %}
21
+ <main class="admin-plain">
22
+ {% include "layouts/_flash.njk" %}
23
+ {% block content %}{% endblock %}
24
+ </main>
25
+ {% endblock %}
26
+ </body>
27
+ </html>
@@ -0,0 +1,78 @@
1
+ {% extends "layouts/shell.njk" %}
2
+
3
+ {% block title %}Dashboard{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>Dashboard</h1>
7
+
8
+ <div class="admin-panels">
9
+ <section class="admin-panel">
10
+ <h2>At a glance</h2>
11
+ <dl class="admin-counts">
12
+ <div>
13
+ <dt>Published posts</dt>
14
+ <dd>{{ counts.posts }}</dd>
15
+ </div>
16
+ <div>
17
+ <dt>Drafts</dt>
18
+ <dd>{{ counts.drafts }}</dd>
19
+ </div>
20
+ <div>
21
+ <dt>Pages</dt>
22
+ <dd>{{ counts.pages }}</dd>
23
+ </div>
24
+ <div>
25
+ <dt>Followers</dt>
26
+ <dd><a href="{{ federationUrl }}">{{ followers }}</a></dd>
27
+ </div>
28
+ </dl>
29
+ </section>
30
+
31
+ <section class="admin-panel">
32
+ <h2>Recent posts</h2>
33
+ {% if recent.length %}
34
+ <table class="admin-list">
35
+ <thead>
36
+ <tr>
37
+ <th scope="col">Title</th>
38
+ <th scope="col">Date</th>
39
+ <th scope="col">Status</th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ {% for post in recent %}
44
+ <tr>
45
+ <td><a href="{{ post.editUrl }}">{{ post.title }}</a></td>
46
+ <td>{{ post.date | date('html') }}</td>
47
+ <td>
48
+ {% if post.draft %}
49
+ <span class="admin-status admin-status-draft">Draft</span>
50
+ {% else %}
51
+ <span class="admin-status">Published</span>
52
+ {% endif %}
53
+ </td>
54
+ </tr>
55
+ {% endfor %}
56
+ </tbody>
57
+ </table>
58
+ {% else %}
59
+ <p class="admin-empty">Nothing written yet.</p>
60
+ {% endif %}
61
+ </section>
62
+
63
+ <section class="admin-panel">
64
+ <h2>Quick draft</h2>
65
+ <form method="post" action="{{ quickDraftUrl }}">
66
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
67
+
68
+ <label for="quick-draft-title">Title</label>
69
+ <input id="quick-draft-title" name="title" type="text" required />
70
+
71
+ <label for="quick-draft-body">Body</label>
72
+ <textarea id="quick-draft-body" name="body" rows="6"></textarea>
73
+
74
+ <button type="submit">Save draft</button>
75
+ </form>
76
+ </section>
77
+ </div>
78
+ {% endblock %}
@@ -0,0 +1,49 @@
1
+ {# A save that was refused because the file changed after the form loaded.
2
+ Nothing has been written. Both versions are here in full so the decision is
3
+ made by somebody who has seen them, not by whoever saved last. #}
4
+ {% extends "layouts/shell.njk" %}
5
+
6
+ {% block title %}Conflicting edit{% endblock %}
7
+
8
+ {% block content %}
9
+ <h1>This {{ kind.singular }} changed while you were editing it</h1>
10
+
11
+ <p class="admin-error">
12
+ Nothing has been saved. The file on disk is not the one this form was filled in
13
+ from — somebody else, or something else, has written to it.
14
+ </p>
15
+
16
+ <div class="admin-conflict">
17
+ <section>
18
+ <h2>Your version</h2>
19
+ <textarea readonly rows="20" aria-label="The version you submitted">{{ submitted }}</textarea>
20
+ </section>
21
+
22
+ <section>
23
+ <h2>The version on disk</h2>
24
+ <textarea readonly rows="20" aria-label="The version on disk">{{ current }}</textarea>
25
+ </section>
26
+ </div>
27
+
28
+ <p class="admin-conflict-actions">
29
+ <a class="admin-button admin-button-quiet" href="{{ editUrl }}">Discard mine and edit the file on disk</a>
30
+ </p>
31
+
32
+ {# Everything the refused form carried, with the hash the file has now. One
33
+ click is then a deliberate overwrite of a version its author has read. #}
34
+ <form method="post" action="{{ saveUrl }}">
35
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
36
+ <input type="hidden" name="hash" value="{{ freshHash }}" />
37
+ <input type="hidden" name="title" value="{{ form.title }}" />
38
+ <input type="hidden" name="slug" value="{{ form.slug }}" />
39
+ <input type="hidden" name="permalink" value="{{ form.permalink }}" />
40
+ <input type="hidden" name="date" value="{{ form.date }}" />
41
+ <input type="hidden" name="tags" value="{{ form.tags }}" />
42
+ <input type="hidden" name="description" value="{{ form.description }}" />
43
+ {% if form.draft %}
44
+ <input type="hidden" name="draft" value="1" />
45
+ {% endif %}
46
+ <textarea class="admin-hidden-field" name="body" aria-hidden="true" tabindex="-1">{{ form.body }}</textarea>
47
+ <button type="submit" name="action" value="{{ action }}">Keep mine and overwrite the file</button>
48
+ </form>
49
+ {% endblock %}
@@ -0,0 +1,167 @@
1
+ {# The editor behind /admin/posts/{slug} and /admin/pages/{slug}, and behind
2
+ .../new. One template for both kinds: `kind` decides whether there is a date
3
+ and a tag field, and the handler decides which buttons the form carries. #}
4
+ {% extends "layouts/shell.njk" %}
5
+
6
+ {% block title %}{{ heading }}{% endblock %}
7
+
8
+ {% block content %}
9
+ <h1>{{ heading }}</h1>
10
+
11
+ {% if error %}
12
+ <p class="admin-error">{{ error }}</p>
13
+ {% endif %}
14
+
15
+ {% if trashed %}
16
+ <p class="admin-note">This {{ kind.singular }} is in the trash. The public site is not serving it.</p>
17
+ {% endif %}
18
+
19
+ <form class="admin-editor" method="post" action="{{ saveUrl }}">
20
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
21
+ {# The hash of the file this form was filled in from. A save whose hash no
22
+ longer matches what is on disk is refused rather than overwriting
23
+ somebody else's edit (doc-1). #}
24
+ <input type="hidden" name="hash" value="{{ form.hash }}" />
25
+ {# Which layout the preview renders through. The save handler ignores it. #}
26
+ <input type="hidden" name="type" value="{{ kind.type }}" />
27
+
28
+ <div class="admin-editor-main">
29
+ <label for="editor-title">Title</label>
30
+ <input id="editor-title" name="title" type="text" value="{{ form.title }}" required autofocus />
31
+
32
+ <label for="editor-body">Body</label>
33
+ <textarea id="editor-body" name="body" rows="24">{{ form.body }}</textarea>
34
+
35
+ {# Everything the editor's own script needs, on the element it hangs off.
36
+ With JavaScript off this is one button that posts the form to the
37
+ preview in a new tab, which is a preview. With it on, editor.js takes
38
+ the button over, puts CodeMirror on the textarea above and adds the
39
+ tabs and the upload control beside it. #}
40
+ <div
41
+ class="admin-editor-tools"
42
+ id="editor-enhance"
43
+ data-preview-url="{{ previewUrl }}"
44
+ data-upload-url="{{ uploadUrl }}"
45
+ data-kind="{{ kind.type }}"
46
+ >
47
+ <button
48
+ type="submit"
49
+ id="editor-preview-fallback"
50
+ class="admin-button-quiet"
51
+ formaction="{{ previewUrl }}"
52
+ formtarget="_blank"
53
+ formnovalidate
54
+ >Preview</button>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="admin-editor-side">
59
+ <label for="editor-slug">Slug</label>
60
+ <input id="editor-slug" name="slug" type="text" value="{{ form.slug }}" />
61
+
62
+ <label for="editor-permalink">Permalink</label>
63
+ <input id="editor-permalink" name="permalink" type="text" value="{{ form.permalink }}" />
64
+ <p class="admin-hint">Leave it empty to let the {{ kind.singular }} take the URL its slug implies.</p>
65
+
66
+ {% if kind.dated %}
67
+ <label for="editor-date">Date</label>
68
+ <input id="editor-date" name="date" type="text" value="{{ form.date }}" />
69
+ {% endif %}
70
+
71
+ {% if kind.tagged %}
72
+ <label for="editor-tags">Tags</label>
73
+ <input id="editor-tags" name="tags" type="text" value="{{ form.tags }}" />
74
+ <p class="admin-hint">Separated by commas.</p>
75
+ {% endif %}
76
+
77
+ <label for="editor-description">Description</label>
78
+ <input id="editor-description" name="description" type="text" value="{{ form.description }}" />
79
+
80
+ <p class="admin-check">
81
+ <input id="editor-draft" name="draft" type="checkbox" value="1"{% if form.draft %} checked{% endif %} />
82
+ <label for="editor-draft">Draft</label>
83
+ </p>
84
+
85
+ {% if kind.excludable %}
86
+ {# doc-2's eleventyExcludeFromCollections: still published and still at
87
+ its URL, but out of every collection a template lists from. #}
88
+ <p class="admin-check">
89
+ <input id="editor-exclude" name="exclude" type="checkbox" value="1"{% if form.exclude %} checked{% endif %} />
90
+ <label for="editor-exclude">Hide from collections</label>
91
+ </p>
92
+ <p class="admin-hint">Keeps the {{ kind.singular }} out of listings that gather content, without unpublishing it.</p>
93
+ {% endif %}
94
+
95
+ <p class="admin-editor-buttons">
96
+ {% for button in actions %}
97
+ <button type="submit" name="action" value="{{ button.value }}">{{ button.label }}</button>
98
+ {% endfor %}
99
+ {% if viewUrl %}
100
+ <a class="admin-button admin-button-quiet" href="{{ viewUrl }}">View</a>
101
+ {% endif %}
102
+ </p>
103
+ </div>
104
+ </form>
105
+
106
+ {# CodeMirror, the preview tab and the upload control. A module script is
107
+ deferred by definition, so it runs after the form is in the document.
108
+ Nothing above depends on it: with the bundle missing or JavaScript off the
109
+ textarea and the Preview button are the whole editor. #}
110
+ <script type="module" src="{{ assetPrefix }}editor.js"></script>
111
+
112
+ <script>
113
+ /* The one bit of progressive enhancement doc-5 asks for: the slug follows
114
+ the title until somebody types a slug of their own, and the permalink
115
+ follows the slug on the same terms. Without this the form still works —
116
+ an empty slug is derived from the title on the server. */
117
+ (function () {
118
+ var title = document.getElementById('editor-title');
119
+ var slug = document.getElementById('editor-slug');
120
+ var permalink = document.getElementById('editor-permalink');
121
+ if (!title || !slug) return;
122
+
123
+ var date = document.getElementById('editor-date');
124
+ var slugFollows = slug.value === '';
125
+ var permalinkFollows = !permalink || permalink.value === '';
126
+
127
+ function slugify(value) {
128
+ return value
129
+ .normalize('NFKD')
130
+ .toLowerCase()
131
+ .replace(/[\u0300-\u036f]/g, '')
132
+ .replace(/[^a-z0-9]+/g, '-')
133
+ .replace(/^-+|-+$/g, '');
134
+ }
135
+
136
+ /* The same shape defaultPermalink() writes on the server: a post lives
137
+ under the year and month of its date, a page at the top level. */
138
+ function syncPermalink() {
139
+ if (!permalink || !permalinkFollows) return;
140
+ if (slug.value === '') {
141
+ permalink.value = '';
142
+ return;
143
+ }
144
+ var month = date ? /^(\d{4})-(\d{2})/.exec(date.value) : null;
145
+ var prefix = month ? '/' + month[1] + '/' + month[2] + '/' : '/';
146
+ permalink.value = prefix + slug.value + '/';
147
+ }
148
+
149
+ slug.addEventListener('input', function () {
150
+ slugFollows = false;
151
+ syncPermalink();
152
+ });
153
+
154
+ if (permalink) {
155
+ permalink.addEventListener('input', function () {
156
+ permalinkFollows = false;
157
+ });
158
+ }
159
+
160
+ title.addEventListener('input', function () {
161
+ if (!slugFollows) return;
162
+ slug.value = slugify(title.value);
163
+ syncPermalink();
164
+ });
165
+ })();
166
+ </script>
167
+ {% endblock %}
@@ -0,0 +1,92 @@
1
+ {# The listing behind /admin/posts and /admin/pages. One template for both:
2
+ `kind` says which, and whether the table has a date column and a tag one. #}
3
+ {% extends "layouts/shell.njk" %}
4
+
5
+ {% block title %}{{ kind.plural }}{% endblock %}
6
+
7
+ {% block content %}
8
+ <h1>{{ kind.plural }} <a class="admin-button admin-button-small" href="{{ newUrl }}">Add new</a></h1>
9
+
10
+ <ul class="admin-filters">
11
+ {% for entry in filters %}
12
+ <li>
13
+ <a href="{{ entry.url }}"{% if entry.current %} aria-current="true"{% endif %}>{{ entry.label }}</a>
14
+ </li>
15
+ {% endfor %}
16
+ </ul>
17
+
18
+ {% if documents.length %}
19
+ <table class="admin-list">
20
+ <thead>
21
+ <tr>
22
+ <th scope="col">Title</th>
23
+ <th scope="col">Author</th>
24
+ {% if kind.tagged %}
25
+ <th scope="col">Tags</th>
26
+ {% endif %}
27
+ {% if kind.dated %}
28
+ <th scope="col">Date</th>
29
+ {% else %}
30
+ <th scope="col">Updated</th>
31
+ {% endif %}
32
+ <th scope="col">Status</th>
33
+ <th scope="col">Actions</th>
34
+ </tr>
35
+ </thead>
36
+ <tbody>
37
+ {% for document in documents %}
38
+ <tr>
39
+ <td><a href="{{ document.editUrl }}">{{ document.title }}</a></td>
40
+ <td>{{ document.author }}</td>
41
+ {% if kind.tagged %}
42
+ <td>{{ document.tags }}</td>
43
+ {% endif %}
44
+ {% if kind.dated %}
45
+ <td>{{ document.date | date('html') }}</td>
46
+ {% else %}
47
+ <td>{{ document.updated | date('html') }}</td>
48
+ {% endif %}
49
+ <td>
50
+ {% if document.trashed %}
51
+ <span class="admin-status admin-status-trashed">Trash</span>
52
+ {% elif document.draft %}
53
+ <span class="admin-status admin-status-draft">Draft</span>
54
+ {% else %}
55
+ <span class="admin-status">Published</span>
56
+ {% endif %}
57
+ </td>
58
+ <td class="admin-actions">
59
+ <a href="{{ document.editUrl }}">Edit</a>
60
+ <form method="post" action="{{ document.editUrl }}">
61
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
62
+ <input type="hidden" name="return" value="{{ returnUrl }}" />
63
+ {% if document.trashed %}
64
+ <button type="submit" name="action" value="restore">Restore</button>
65
+ {% else %}
66
+ <button type="submit" name="action" value="trash">Move to trash</button>
67
+ {% endif %}
68
+ </form>
69
+ {% if document.viewUrl %}
70
+ <a href="{{ document.viewUrl }}">View</a>
71
+ {% endif %}
72
+ </td>
73
+ </tr>
74
+ {% endfor %}
75
+ </tbody>
76
+ </table>
77
+
78
+ {% if previousUrl or nextUrl %}
79
+ <nav class="admin-pagination" aria-label="Pages">
80
+ {% if previousUrl %}
81
+ <a rel="prev" href="{{ previousUrl }}">Newer</a>
82
+ {% endif %}
83
+ <span>Page {{ page }}</span>
84
+ {% if nextUrl %}
85
+ <a rel="next" href="{{ nextUrl }}">Older</a>
86
+ {% endif %}
87
+ </nav>
88
+ {% endif %}
89
+ {% else %}
90
+ <p class="admin-empty">Nothing here.</p>
91
+ {% endif %}
92
+ {% endblock %}
@@ -0,0 +1,145 @@
1
+ {# /admin/federation: who the site is to the fediverse, who follows it, what
2
+ arrived in the inbox, and how what went out landed. #}
3
+ {% extends "layouts/shell.njk" %}
4
+
5
+ {% block title %}Federation{% endblock %}
6
+
7
+ {% block content %}
8
+ <h1>Federation</h1>
9
+
10
+ <section class="admin-panel admin-actor">
11
+ <h2>This site’s actor</h2>
12
+ <div class="admin-actor-identity">
13
+ {% if actor.avatarUrl %}
14
+ <img class="admin-avatar admin-avatar-large" src="{{ actor.avatarUrl }}" alt="" width="96" height="96" />
15
+ {% else %}
16
+ <span class="admin-avatar admin-avatar-large admin-avatar-blank" aria-hidden="true"></span>
17
+ {% endif %}
18
+ <p class="admin-actor-handle">{{ actor.handle }}</p>
19
+ </div>
20
+ <dl class="admin-actor-fields">
21
+ <div>
22
+ <dt>Name</dt>
23
+ <dd>{{ actor.name }}</dd>
24
+ </div>
25
+ <div>
26
+ <dt>Summary</dt>
27
+ <dd>{% if actor.summary %}{{ actor.summary }}{% else %}<span class="admin-empty">None set.</span>{% endif %}</dd>
28
+ </div>
29
+ <div>
30
+ <dt>Type</dt>
31
+ <dd>{{ actor.type }}</dd>
32
+ </div>
33
+ <div>
34
+ <dt>Actor</dt>
35
+ <dd><a href="{{ actor.actorId }}">{{ actor.actorId }}</a></dd>
36
+ </div>
37
+ <div>
38
+ <dt>Profile</dt>
39
+ <dd><a href="{{ actor.url }}">{{ actor.url }}</a></dd>
40
+ </div>
41
+ <div>
42
+ <dt>Followers</dt>
43
+ <dd>{{ actor.followers }}</dd>
44
+ </div>
45
+ </dl>
46
+ </section>
47
+
48
+ <section class="admin-panel">
49
+ <h2>Followers</h2>
50
+ {% if followers.length %}
51
+ <table class="admin-list">
52
+ <thead>
53
+ <tr>
54
+ <th scope="col">Follower</th>
55
+ <th scope="col">Handle</th>
56
+ <th scope="col">Followed</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ {% for follower in followers %}
61
+ <tr>
62
+ <td class="admin-follower">
63
+ {% if follower.iconUrl %}
64
+ <img class="admin-avatar" src="{{ follower.iconUrl }}" alt="" width="32" height="32" loading="lazy" />
65
+ {% else %}
66
+ <span class="admin-avatar admin-avatar-blank" aria-hidden="true"></span>
67
+ {% endif %}
68
+ <a href="{{ follower.url }}">{{ follower.name }}</a>
69
+ </td>
70
+ <td>{{ follower.handle }}</td>
71
+ <td>{{ follower.followedAt | date('html') }}</td>
72
+ </tr>
73
+ {% endfor %}
74
+ </tbody>
75
+ </table>
76
+ {% else %}
77
+ <p class="admin-empty">Nobody follows the site yet.</p>
78
+ {% endif %}
79
+ </section>
80
+
81
+ <section class="admin-panel">
82
+ <h2>Recent activity</h2>
83
+ {% if inbox.length %}
84
+ <ul class="admin-inbox">
85
+ {% for entry in inbox %}
86
+ <li>
87
+ <span class="admin-inbox-actor"><a href="{{ entry.actorId }}">{{ entry.actorLabel }}</a></span>
88
+ {% if entry.remoteUrl %}
89
+ <a href="{{ entry.remoteUrl }}">{{ entry.action }}</a>
90
+ {% else %}
91
+ {{ entry.action }}
92
+ {% endif %}
93
+ {% if entry.post %}
94
+ <a href="{{ entry.post.editUrl }}">{{ entry.post.title }}</a>
95
+ {% else %}
96
+ <span class="admin-empty">something of ours</span>
97
+ {% endif %}
98
+ <span class="admin-inbox-when">{{ entry.receivedAt | date('html') }}</span>
99
+ </li>
100
+ {% endfor %}
101
+ </ul>
102
+ {% else %}
103
+ <p class="admin-empty">Nothing has arrived yet: no likes, boosts or replies.</p>
104
+ {% endif %}
105
+ </section>
106
+
107
+ <section class="admin-panel">
108
+ <h2>Delivery</h2>
109
+ {% if posts.length %}
110
+ <table class="admin-list">
111
+ <thead>
112
+ <tr>
113
+ <th scope="col">Post</th>
114
+ <th scope="col">Latest activity</th>
115
+ <th scope="col">Sent</th>
116
+ <th scope="col">Queued</th>
117
+ <th scope="col">Failed</th>
118
+ <th scope="col">Actions</th>
119
+ </tr>
120
+ </thead>
121
+ <tbody>
122
+ {% for row in posts %}
123
+ <tr>
124
+ <td><a href="{{ row.post.editUrl }}">{{ row.post.title }}</a></td>
125
+ <td>{{ row.activityType }} <span class="admin-inbox-when">{{ row.createdAt | date('html') }}</span></td>
126
+ <td>{{ row.counts.sent }}</td>
127
+ <td>{{ row.counts.queued }}</td>
128
+ <td>{% if row.counts.failed %}<span class="admin-status admin-status-failed">{{ row.counts.failed }}</span>{% else %}0{% endif %}</td>
129
+ <td class="admin-actions">
130
+ <form method="post" action="{{ redeliverUrl }}">
131
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
132
+ <input type="hidden" name="{{ fields.activityId }}" value="{{ row.activityId }}" />
133
+ <button type="submit">Redeliver</button>
134
+ </form>
135
+ </td>
136
+ </tr>
137
+ {% endfor %}
138
+ </tbody>
139
+ </table>
140
+ <p class="admin-hint">Redelivering sends that activity to every follower the site has now.</p>
141
+ {% else %}
142
+ <p class="admin-empty">Nothing has been delivered yet.</p>
143
+ {% endif %}
144
+ </section>
145
+ {% endblock %}
@@ -0,0 +1,23 @@
1
+ {% extends "layouts/base.njk" %}
2
+
3
+ {% block title %}Log in{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>Log in</h1>
7
+
8
+ {% if error %}
9
+ <p class="error">{{ error }}</p>
10
+ {% endif %}
11
+
12
+ <form method="post" action="{{ loginUrl }}">
13
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
14
+
15
+ <label for="username">Username</label>
16
+ <input id="username" name="username" type="text" autocomplete="username" value="{{ username }}" required autofocus />
17
+
18
+ <label for="password">Password</label>
19
+ <input id="password" name="password" type="password" autocomplete="current-password" required />
20
+
21
+ <button type="submit">Log in</button>
22
+ </form>
23
+ {% endblock %}
@@ -0,0 +1,10 @@
1
+ {% extends "layouts/shell.njk" %}
2
+
3
+ {% block title %}{{ heading }}{% endblock %}
4
+
5
+ {% block content %}
6
+ {# A screen a later task builds. It exists so the navigation goes somewhere
7
+ and the guard covers it from the start. #}
8
+ <h1>{{ heading }}{% if slug %}: {{ slug }}{% endif %}</h1>
9
+ <p class="admin-note">This screen is not built yet.</p>
10
+ {% endblock %}