@geekity/cms 0.4.0 → 0.6.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 (156) hide show
  1. package/README.md +246 -110
  2. package/admin/pages/documents/editor.njk +0 -13
  3. package/admin/pages/{settings/federation.njk → federation/settings.njk} +7 -5
  4. package/admin/pages/navigation/menus.njk +132 -0
  5. package/admin/pages/settings/reading.njk +2 -10
  6. package/admin/pages/tools/content-index.njk +89 -0
  7. package/admin/pages/users/edit.njk +25 -2
  8. package/admin/pages/users/list.njk +5 -2
  9. package/admin/static/admin.css +78 -3
  10. package/dist/access-log.d.ts +44 -0
  11. package/dist/access-log.d.ts.map +1 -0
  12. package/dist/access-log.js +71 -0
  13. package/dist/access-log.js.map +1 -0
  14. package/dist/admin/accounts.d.ts +10 -0
  15. package/dist/admin/accounts.d.ts.map +1 -1
  16. package/dist/admin/accounts.js +15 -3
  17. package/dist/admin/accounts.js.map +1 -1
  18. package/dist/admin/documents.d.ts +3 -13
  19. package/dist/admin/documents.d.ts.map +1 -1
  20. package/dist/admin/documents.js +2 -34
  21. package/dist/admin/documents.js.map +1 -1
  22. package/dist/admin/federation.d.ts +10 -0
  23. package/dist/admin/federation.d.ts.map +1 -1
  24. package/dist/admin/federation.js +10 -0
  25. package/dist/admin/federation.js.map +1 -1
  26. package/dist/admin/index.d.ts +3 -3
  27. package/dist/admin/index.d.ts.map +1 -1
  28. package/dist/admin/index.js +2 -2
  29. package/dist/admin/index.js.map +1 -1
  30. package/dist/admin/menu.d.ts.map +1 -1
  31. package/dist/admin/menu.js +19 -2
  32. package/dist/admin/menu.js.map +1 -1
  33. package/dist/admin/navigation.d.ts +155 -0
  34. package/dist/admin/navigation.d.ts.map +1 -0
  35. package/dist/admin/navigation.js +292 -0
  36. package/dist/admin/navigation.js.map +1 -0
  37. package/dist/admin/routes.d.ts.map +1 -1
  38. package/dist/admin/routes.js +9 -0
  39. package/dist/admin/routes.js.map +1 -1
  40. package/dist/admin/session.d.ts +17 -3
  41. package/dist/admin/session.d.ts.map +1 -1
  42. package/dist/admin/session.js +19 -5
  43. package/dist/admin/session.js.map +1 -1
  44. package/dist/admin/settings-federation.d.ts +10 -1
  45. package/dist/admin/settings-federation.d.ts.map +1 -1
  46. package/dist/admin/settings-federation.js +26 -6
  47. package/dist/admin/settings-federation.js.map +1 -1
  48. package/dist/admin/settings-page.d.ts +26 -9
  49. package/dist/admin/settings-page.d.ts.map +1 -1
  50. package/dist/admin/settings-page.js +18 -12
  51. package/dist/admin/settings-page.js.map +1 -1
  52. package/dist/admin/settings-pages.d.ts +14 -3
  53. package/dist/admin/settings-pages.d.ts.map +1 -1
  54. package/dist/admin/settings-pages.js +15 -5
  55. package/dist/admin/settings-pages.js.map +1 -1
  56. package/dist/admin/settings-reading.d.ts +5 -4
  57. package/dist/admin/settings-reading.d.ts.map +1 -1
  58. package/dist/admin/settings-reading.js +6 -5
  59. package/dist/admin/settings-reading.js.map +1 -1
  60. package/dist/admin/settings.d.ts +29 -15
  61. package/dist/admin/settings.d.ts.map +1 -1
  62. package/dist/admin/settings.js +28 -78
  63. package/dist/admin/settings.js.map +1 -1
  64. package/dist/admin/templates.d.ts +3 -1
  65. package/dist/admin/templates.d.ts.map +1 -1
  66. package/dist/admin/templates.js +3 -1
  67. package/dist/admin/templates.js.map +1 -1
  68. package/dist/admin/tools.d.ts +111 -0
  69. package/dist/admin/tools.d.ts.map +1 -0
  70. package/dist/admin/tools.js +182 -0
  71. package/dist/admin/tools.js.map +1 -0
  72. package/dist/admin/users.d.ts +24 -0
  73. package/dist/admin/users.d.ts.map +1 -1
  74. package/dist/admin/users.js +102 -30
  75. package/dist/admin/users.js.map +1 -1
  76. package/dist/cli.d.ts.map +1 -1
  77. package/dist/cli.js +6 -1
  78. package/dist/cli.js.map +1 -1
  79. package/dist/comments/form.d.ts +48 -0
  80. package/dist/comments/form.d.ts.map +1 -1
  81. package/dist/comments/form.js +15 -0
  82. package/dist/comments/form.js.map +1 -1
  83. package/dist/comments/index.d.ts +4 -3
  84. package/dist/comments/index.d.ts.map +1 -1
  85. package/dist/comments/index.js +2 -1
  86. package/dist/comments/index.js.map +1 -1
  87. package/dist/comments/routes.d.ts +3 -1
  88. package/dist/comments/routes.d.ts.map +1 -1
  89. package/dist/comments/routes.js +46 -11
  90. package/dist/comments/routes.js.map +1 -1
  91. package/dist/comments/submission.d.ts +37 -3
  92. package/dist/comments/submission.d.ts.map +1 -1
  93. package/dist/comments/submission.js +59 -20
  94. package/dist/comments/submission.js.map +1 -1
  95. package/dist/comments/viewer.d.ts +21 -0
  96. package/dist/comments/viewer.d.ts.map +1 -0
  97. package/dist/comments/viewer.js +45 -0
  98. package/dist/comments/viewer.js.map +1 -0
  99. package/dist/config.d.ts +35 -0
  100. package/dist/config.d.ts.map +1 -1
  101. package/dist/config.js +3 -0
  102. package/dist/config.js.map +1 -1
  103. package/dist/content/store.d.ts +18 -0
  104. package/dist/content/store.d.ts.map +1 -1
  105. package/dist/content/store.js +18 -0
  106. package/dist/content/store.js.map +1 -1
  107. package/dist/env.d.ts +12 -1
  108. package/dist/env.d.ts.map +1 -1
  109. package/dist/federation/mount.d.ts +4 -0
  110. package/dist/federation/mount.d.ts.map +1 -1
  111. package/dist/federation/mount.js +60 -15
  112. package/dist/federation/mount.js.map +1 -1
  113. package/dist/index.d.ts +5 -5
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +21 -4
  116. package/dist/index.js.map +1 -1
  117. package/dist/web/authors.d.ts +15 -1
  118. package/dist/web/authors.d.ts.map +1 -1
  119. package/dist/web/authors.js +9 -1
  120. package/dist/web/authors.js.map +1 -1
  121. package/dist/web/index.d.ts +4 -4
  122. package/dist/web/index.d.ts.map +1 -1
  123. package/dist/web/index.js +1 -1
  124. package/dist/web/index.js.map +1 -1
  125. package/dist/web/navigation.d.ts +235 -35
  126. package/dist/web/navigation.d.ts.map +1 -1
  127. package/dist/web/navigation.js +333 -57
  128. package/dist/web/navigation.js.map +1 -1
  129. package/dist/web/negotiate.d.ts +22 -0
  130. package/dist/web/negotiate.d.ts.map +1 -1
  131. package/dist/web/negotiate.js +25 -10
  132. package/dist/web/negotiate.js.map +1 -1
  133. package/dist/web/render.d.ts +16 -10
  134. package/dist/web/render.d.ts.map +1 -1
  135. package/dist/web/render.js +21 -14
  136. package/dist/web/render.js.map +1 -1
  137. package/dist/web/routes.d.ts.map +1 -1
  138. package/dist/web/routes.js +11 -2
  139. package/dist/web/routes.js.map +1 -1
  140. package/dist/web/themes.d.ts +21 -0
  141. package/dist/web/themes.d.ts.map +1 -1
  142. package/dist/web/themes.js +37 -0
  143. package/dist/web/themes.js.map +1 -1
  144. package/package.json +1 -1
  145. package/templates/site/content/_data/site.json +7 -0
  146. package/templates/site/content/pages/about.md +7 -0
  147. package/themes/default/README.md +215 -80
  148. package/themes/default/layouts/author.njk +3 -4
  149. package/themes/default/layouts/base.njk +38 -49
  150. package/themes/default/layouts/front-page.njk +1 -2
  151. package/themes/default/partials/bio.njk +20 -22
  152. package/themes/default/partials/comment-form.njk +31 -3
  153. package/themes/default/partials/menu.njk +34 -0
  154. package/themes/default/partials/search-form.njk +5 -0
  155. package/themes/default/static/style.css +54 -13
  156. package/themes/default/theme.json +5 -1
@@ -0,0 +1,132 @@
1
+ {# /admin/navigation: every menu this site holds, and what renders it.
2
+
3
+ Two lists, and a menu is in exactly one of them. First the areas the active
4
+ theme declares, in the theme's own order and under the theme's own labels,
5
+ each a box to type links into — an area the site has never filled in is an
6
+ empty box rather than a missing one. Then every other menu the site stores,
7
+ under a heading that says this theme renders them nowhere, each with a
8
+ Delete, which is the only way a menu is removed. #}
9
+ {% extends "layouts/shell.njk" %}
10
+ {% import "components/fields.njk" as field %}
11
+
12
+ {% block title %}{{ heading }}{% endblock %}
13
+
14
+ {# One menu: its heading, the box its items are typed into, and the button
15
+ that saves it. `extra` is whatever else belongs under the box — the Delete
16
+ form, on a menu nothing renders. #}
17
+ {% macro box(menu, fields, url, csrfToken, extra='') %}
18
+ <li class="admin-menu-box">
19
+ <h3>{{ menu.label }}</h3>
20
+
21
+ <p class="admin-menu-name">
22
+ <code>menus.{{ menu.name }}</code>
23
+ {% if menu.stored %}
24
+ <span class="admin-status">{{ menu.count }} item{% if menu.count != 1 %}s{% endif %}</span>
25
+ {% else %}
26
+ <span class="admin-status">empty — nothing has been typed here yet</span>
27
+ {% endif %}
28
+ </p>
29
+
30
+ <form method="post" action="{{ url }}">
31
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
32
+ <input type="hidden" name="{{ fields.menu }}" value="{{ menu.name }}" />
33
+ {% set itemsHint %}
34
+ One item per line, written <code>Label | URL</code> &mdash; for example
35
+ <code>About | /about/</code> or
36
+ <code>Mastodon | https://example.social/@me</code>. They are the whole of
37
+ this menu, in this order: nothing else puts a link in it. A page serving
38
+ as the front page goes in as <code>Home | /</code>, where a reader lands.
39
+ End a line with the
40
+ <code>rel</code> values the link carries, a word each &mdash;
41
+ <code>Mastodon | https://example.social/@me | me</code>,
42
+ <code>A source | https://example.com/thing | nofollow noopener</code>.
43
+ <code>me</code> marks <strong>a profile that is you</strong>, which is
44
+ how Mastodon and the rest of the IndieWeb verify that the site and the
45
+ profile it links are the same person; the rest are whatever HTML link
46
+ types you want on it.
47
+ {% endset %}
48
+ {{ field.textarea('menu-' + menu.name, fields.items, 'Items', menu.items,
49
+ error=menu.problem, hint=itemsHint, rows='5', spellcheck='false') }}
50
+ <p class="admin-menu-buttons">
51
+ <button type="submit">Save menu</button>
52
+ </p>
53
+ </form>
54
+
55
+ {{ extra | safe }}
56
+ </li>
57
+ {% endmacro %}
58
+
59
+ {% block content %}
60
+ <h1>{{ heading }}</h1>
61
+
62
+ {% if hasProblems %}
63
+ <p class="admin-field-error">Nothing was saved. See below.</p>
64
+ {% endif %}
65
+
66
+ <h2>Where {{ themeName }} puts a menu</h2>
67
+
68
+ {% if areas.length %}
69
+ <p class="admin-hint">
70
+ These are the areas the active theme declares in its <code>theme.json</code>, in the order it
71
+ declares them. A theme renders a menu by asking for it by name, and an area with nothing in it
72
+ renders nothing at all.
73
+ </p>
74
+
75
+ <ul class="admin-menu-boxes">
76
+ {% for menu in areas %}
77
+ {{ box(menu, fields, navigationUrl, csrfToken) }}
78
+ {% endfor %}
79
+ </ul>
80
+ {% else %}
81
+ <p class="admin-hint">
82
+ {{ themeName }} declares no areas, so it asks for no menu by name. A theme declares them as an
83
+ <code>areas</code> list in its <code>theme.json</code>, each entry a <code>name</code> &mdash;
84
+ the name the template loops over as <code>menus.&lt;name&gt;</code> &mdash; and a
85
+ <code>label</code> to head the box here. A menu added below is still stored, ready for the
86
+ theme that asks for it.
87
+ </p>
88
+ {% endif %}
89
+
90
+ {% if others.length %}
91
+ <h2>Kept, rendered nowhere</h2>
92
+
93
+ <p class="admin-hint">
94
+ This site stores these menus and {{ themeName }} renders none of them: no template loops over
95
+ those names, so nothing on the public site shows them. They are kept, because that is how the
96
+ menu a theme will use gets written before the site switches to it. Delete is the only way one
97
+ is removed.
98
+ </p>
99
+
100
+ <ul class="admin-menu-boxes">
101
+ {% for menu in others %}
102
+ {% set remove %}
103
+ <form class="admin-menu-delete" method="post" action="{{ deleteUrl }}">
104
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
105
+ <input type="hidden" name="{{ fields.menu }}" value="{{ menu.name }}" />
106
+ <button type="submit">Delete this menu</button>
107
+ </form>
108
+ {% endset %}
109
+ {{ box(menu, fields, navigationUrl, csrfToken, remove) }}
110
+ {% endfor %}
111
+ </ul>
112
+ {% endif %}
113
+
114
+ <h2>Add a menu</h2>
115
+
116
+ <form class="admin-menu-add" method="post" action="{{ addUrl }}">
117
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
118
+ {% set nameHint %}
119
+ The name a theme asks for the menu by: lower-case letters, digits and
120
+ underscores, starting with a letter. It is written after the dot &mdash;
121
+ <code>{% raw %}{% for item in menus.footer %}{% endraw %}</code> &mdash; which is why a dash
122
+ is not allowed in one. A name this theme declares puts the menu in its place above; any other
123
+ name is a menu kept and rendered nowhere, which is how you fill one in before switching to the
124
+ theme that will use it.
125
+ {% endset %}
126
+ {{ field.text('menu-name', fields.name, 'Name', newName,
127
+ error=nameProblem, hint=nameHint, spellcheck='false', required=true) }}
128
+ <p class="admin-menu-buttons">
129
+ <button type="submit">Add menu</button>
130
+ </p>
131
+ </form>
132
+ {% endblock %}
@@ -1,5 +1,6 @@
1
1
  {# /admin/settings/reading: what a visitor is shown, and who is told when it
2
- changes. #}
2
+ changes. The site menu was here until TASK-108; it is on /admin/navigation,
3
+ with every other menu the site holds. #}
3
4
  {% extends "layouts/settings-page.njk" %}
4
5
  {% import "components/fields.njk" as field %}
5
6
 
@@ -53,15 +54,6 @@
53
54
  error=problems.postsPerPage, inputmode='numeric', required=true,
54
55
  hint='How many posts a listing page holds, on the home page and on every archive.') }}
55
56
 
56
- {% set navigationHint %}
57
- One item per line, written <code>Label | URL</code> &mdash; for example
58
- <code>About | /about/</code> or <code>Mastodon | https://example.social/@me</code>.
59
- They are the menu in the site header, in this order. A page can put itself
60
- on the end of it by ticking <em>Show in navigation</em> in the editor.
61
- {% endset %}
62
- {{ field.textarea('settings-navigation', fields.navigation, 'Menu', form.navigation,
63
- error=problems.navigation, hint=navigationHint, rows='4', spellcheck='false') }}
64
-
65
57
  {% set notifyHint %}
66
58
  An rssCloud and WebSub server, told whenever a feed changes so subscribers
67
59
  hear at once instead of waiting for their next poll. Every feed advertises
@@ -0,0 +1,89 @@
1
+ {# /admin/tools: what the derived index holds, and the button that reads every
2
+ file back into it without stopping the site (TASK-95).
3
+
4
+ The counts are the point of the screen as much as the button is: an index
5
+ that disagrees with the files is not something the site complains about, so
6
+ the only way to notice is to be shown what it holds. #}
7
+ {% extends "layouts/shell.njk" %}
8
+
9
+ {% block title %}{{ heading }}{% endblock %}
10
+
11
+ {% block content %}
12
+ <h1>{{ heading }}</h1>
13
+
14
+ <p>
15
+ Everything in <code>data/geekity.db</code> is a reading of the files under
16
+ <code>{{ contentDir }}</code>, so it can always be read again. Rebuild it after
17
+ editing content outside the admin &mdash; over ssh, or with a
18
+ <code>git pull</code> while the site was down &mdash; or when what is listed here
19
+ disagrees with what is on disk.
20
+ </p>
21
+
22
+ <table class="admin-list">
23
+ <thead>
24
+ <tr>
25
+ <th scope="col">What the index holds</th>
26
+ <th scope="col">Count</th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ <tr>
31
+ <th scope="row">Documents</th>
32
+ <td>{{ indexed.documents }}</td>
33
+ </tr>
34
+ <tr>
35
+ <th scope="row">Followers</th>
36
+ <td>{{ indexed.followers }}</td>
37
+ </tr>
38
+ <tr>
39
+ <th scope="row">Inbox activities</th>
40
+ <td>{{ indexed.activities }}</td>
41
+ </tr>
42
+ <tr>
43
+ <th scope="row">Comments</th>
44
+ <td>{{ indexed.comments }}</td>
45
+ </tr>
46
+ </tbody>
47
+ </table>
48
+
49
+ {% if confirm %}
50
+ {# Offered rather than done, the way a merge is on the taxonomy screens.
51
+ The window in the middle is the whole reason: for as long as the scan
52
+ runs the site answers 404 for documents whose files are perfectly fine,
53
+ and that is not a thing to discover afterwards. #}
54
+ <form class="admin-settings" method="post" action="{{ rebuildUrl }}">
55
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
56
+ <input type="hidden" name="{{ fields.confirm }}" value="1" />
57
+
58
+ <h2>Rebuild the index now?</h2>
59
+
60
+ <p>
61
+ The index is emptied and every file read again. Until the scan finishes, this
62
+ site answers <strong>404</strong> for its posts and pages &mdash; under a second
63
+ on a small site, longer on a large one &mdash; and it is serving the whole time.
64
+ Nothing is deleted and nothing is sent: you stay signed in, the delivery log and
65
+ the relays are kept, and no follower, feed or linked page is told about a post
66
+ the scan re-indexes.
67
+ </p>
68
+
69
+ <p class="admin-settings-buttons">
70
+ <button type="submit">Rebuild it</button>
71
+ <a href="{{ toolsUrl }}">Cancel</a>
72
+ </p>
73
+ </form>
74
+ {% else %}
75
+ <form class="admin-settings" method="post" action="{{ rebuildUrl }}">
76
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
77
+ <p class="admin-settings-buttons">
78
+ <button type="submit">Rebuild the index</button>
79
+ </p>
80
+ </form>
81
+ {% endif %}
82
+
83
+ <p class="admin-hint">
84
+ This runs on the site as it is: nothing is deleted, no connection is swapped and
85
+ nobody is signed out. <code>geekity rebuild</code> at the command line is the other
86
+ door, and it is for the one case this screen cannot be: a database this version
87
+ refuses to open, where there is no site running to press a button in.
88
+ </p>
89
+ {% endblock %}
@@ -79,6 +79,13 @@
79
79
  field they do not have.
80
80
  </p>
81
81
 
82
+ {% if profileProblems.links %}
83
+ <p class="admin-error">
84
+ Nothing was saved. Fix the Links box below and save again; every other
85
+ box here still holds what you typed.
86
+ </p>
87
+ {% endif %}
88
+
82
89
  {{ field.text('profile-display-name', fields.displayName, 'Display name',
83
90
  account.profile.displayName,
84
91
  hint='Printed instead of the login: it heads the archive and is the byline under their posts.') }}
@@ -96,12 +103,28 @@
96
103
  {{ field.text('profile-location', fields.location, 'Location', account.profile.location,
97
104
  hint='Where they are, as they write it.') }}
98
105
 
106
+ {# What the box takes — the same line a menu item is written on, down to
107
+ the rel values it may end in (TASK-114) — and the one thing about it
108
+ nothing on the screen used to say (TASK-112): these links are published
109
+ with rel="me", which is what a person deciding what to put here needs to
110
+ know. #}
99
111
  {% set linksHint %}
100
112
  One per line, either <code>Label | https://example.com</code> or a bare
101
- URL that labels itself.
113
+ URL that labels itself. A URL is a path like <code>/about/</code> or a
114
+ full <code>http://</code> or <code>https://</code> one, with no spaces in
115
+ it. End a line with the
116
+ <code>rel</code> values the link carries, a word each &mdash;
117
+ <code>Mastodon | https://example.social/@me | me</code>,
118
+ <code>Their post | https://example.com/post | me nofollow author</code>.
119
+ It is the same line the menu boxes on
120
+ <a href="{{ adminUrl }}/navigation">Navigation</a> take.
121
+ Every link here is published with <code>rel="me"</code> whether or not
122
+ you type it: it is how Mastodon verifies a profile field that points back
123
+ at this site, and how IndieAuth knows these are yours. Nothing else in
124
+ Geekity needs it.
102
125
  {% endset %}
103
126
  {{ field.textarea('profile-links', fields.links, 'Links', account.profile.links, rows='4',
104
- hint=linksHint) }}
127
+ hint=linksHint, error=profileProblems.links) }}
105
128
 
106
129
  <p class="admin-settings-buttons">
107
130
  <button type="submit">Save profile</button>
@@ -26,12 +26,14 @@
26
26
  <tr>
27
27
  <td>
28
28
  <a href="{{ entry.editUrl }}">{{ entry.username }}</a>{% if entry.you %} <span class="admin-status">(you)</span>{% endif %}
29
- <br /><a href="{{ entry.archiveUrl }}">{{ entry.archiveUrl }}</a>
30
29
  </td>
31
30
  <td>{% if entry.email %}{{ entry.email }}{% else %}<span class="admin-status">None</span>{% endif %}</td>
32
31
  <td>{{ entry.createdAt | date('html') }}</td>
32
+ {# Edit, then what can be done, then View: the order and the shape
33
+ `pages/documents/list.njk` uses, down to Edit being a plain link
34
+ rather than a button. A row here is a row there. #}
33
35
  <td class="admin-actions">
34
- <a class="admin-button admin-button-small" href="{{ entry.editUrl }}">Edit</a>
36
+ <a href="{{ entry.editUrl }}">Edit</a>
35
37
  {% if entry.deletable %}
36
38
  <form method="post" action="{{ deleteUserUrl }}">
37
39
  <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
@@ -39,6 +41,7 @@
39
41
  <button type="submit">Delete</button>
40
42
  </form>
41
43
  {% endif %}
44
+ <a href="{{ entry.archiveUrl }}">View</a>
42
45
  </td>
43
46
  </tr>
44
47
  {% endfor %}
@@ -249,20 +249,30 @@ h2 {
249
249
 
250
250
  .admin-counts {
251
251
  display: flex;
252
+ /* A cell that does not fit moves to the next line rather than being
253
+ squeezed until its label breaks; the panel has no breakpoint of its own. */
254
+ flex-wrap: wrap;
252
255
  margin: 0;
253
256
  gap: 1.5rem;
257
+ /* A cell is as tall as its own content, so the tallest of them cannot
258
+ stretch the rest and move where their numbers are drawn. */
259
+ align-items: start;
254
260
  }
255
261
 
262
+ /* A cell is its label and its number, and the markup has to be a `dt` then its
263
+ `dd` — so the two rows are named rather than the cell packed from its bottom
264
+ edge. A number then sits at the top of its cell whatever its label does. */
256
265
  .admin-counts div {
257
- display: flex;
258
- flex-direction: column-reverse;
266
+ display: grid;
259
267
  }
260
268
 
261
269
  .admin-counts dt {
270
+ grid-row: 2;
262
271
  color: var(--admin-muted);
263
272
  }
264
273
 
265
274
  .admin-counts dd {
275
+ grid-row: 1;
266
276
  margin: 0;
267
277
  font-size: 1.75rem;
268
278
  line-height: 1.2;
@@ -687,7 +697,12 @@ button[type='submit']:hover,
687
697
  and how what went out landed. Four panels down the page rather than the
688
698
  dashboard's grid, because three of them hold tables. */
689
699
 
690
- .admin-panel + .admin-panel {
700
+ /* Panels that stack, which is what this screen's do. A panel inside
701
+ `.admin-panels` is in a grid whose `gap` is already the space between its
702
+ cells, in both directions and however the row wraps, so the margin is not
703
+ only unnecessary there but pushes a panel down inside its own cell — which
704
+ is what made the dashboard's two boxes start at different heights. */
705
+ :not(.admin-panels) > .admin-panel + .admin-panel {
691
706
  margin-top: 1.5rem;
692
707
  }
693
708
 
@@ -1219,3 +1234,63 @@ button[type='submit']:hover,
1219
1234
  color: var(--admin-muted);
1220
1235
  overflow-wrap: anywhere;
1221
1236
  }
1237
+
1238
+ /* Navigation. One panel per menu, stacked down the page rather than laid out
1239
+ as cards: a menu is a box of text somebody types into, so it wants the width
1240
+ a paragraph wants rather than a column in a grid. The two lists — the areas
1241
+ the theme declares, and the menus it renders nowhere — are the same panel,
1242
+ because they are the same thing in two different situations. */
1243
+
1244
+ .admin-menu-boxes {
1245
+ margin: 1rem 0 2rem;
1246
+ padding: 0;
1247
+ list-style: none;
1248
+ }
1249
+
1250
+ .admin-menu-box {
1251
+ max-width: 42rem;
1252
+ padding: 0.9rem 1.1rem 1.1rem;
1253
+ background: var(--admin-surface);
1254
+ border: 1px solid var(--admin-border);
1255
+ }
1256
+
1257
+ .admin-menu-box + .admin-menu-box {
1258
+ margin-top: 1rem;
1259
+ }
1260
+
1261
+ .admin-menu-box h3 {
1262
+ margin: 0;
1263
+ font-size: 1.05rem;
1264
+ }
1265
+
1266
+ /* The name a theme asks for the menu by, and how much is in it. The name is
1267
+ the useful half: it is what a template loops over and what `site.json`
1268
+ holds, so it is printed rather than left implied by the label above it. */
1269
+
1270
+ .admin-menu-name {
1271
+ margin: 0.15rem 0 0.6rem;
1272
+ color: var(--admin-muted);
1273
+ font-size: 0.9em;
1274
+ overflow-wrap: anywhere;
1275
+ }
1276
+
1277
+ .admin-menu-buttons {
1278
+ margin: 0.8rem 0 0;
1279
+ }
1280
+
1281
+ /* Delete is the only way a menu is removed, and it is offered only on one the
1282
+ theme renders nowhere. It sits apart from the Save above it, so the two
1283
+ buttons of one panel are not a pair to be pressed carelessly. */
1284
+
1285
+ .admin-menu-delete {
1286
+ margin-top: 0.8rem;
1287
+ padding-top: 0.8rem;
1288
+ border-top: 1px solid var(--admin-field);
1289
+ }
1290
+
1291
+ .admin-menu-add {
1292
+ max-width: 42rem;
1293
+ padding: 0.9rem 1.1rem 1.1rem;
1294
+ background: var(--admin-surface);
1295
+ border: 1px solid var(--admin-border);
1296
+ }
@@ -0,0 +1,44 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { ResolvedConfig } from './config.ts';
3
+ import type { GeekityEnv } from './env.ts';
4
+ /**
5
+ * Where one access-log line goes.
6
+ *
7
+ * The line arrives without a trailing newline; the sink decides the framing.
8
+ * The default sink is stdout — what a container collects, what dockge shows
9
+ * and what journald keeps — and a test hands in one that keeps the lines.
10
+ */
11
+ export type AccessLogWriter = (line: string) => void;
12
+ /** What {@link createAccessLog} needs to know. */
13
+ export interface AccessLogOptions {
14
+ /**
15
+ * Whether a client address goes on the line, and which one is right. Read
16
+ * once, at boot: the log is registered before the context is, so it cannot
17
+ * read the config off a request.
18
+ */
19
+ config: Pick<ResolvedConfig, 'accessLogAddress' | 'trustProxy'>;
20
+ /** Where the lines go. Defaults to stdout. */
21
+ write?: AccessLogWriter;
22
+ }
23
+ /**
24
+ * One line per request: what was asked, what was answered, and how long it
25
+ * took.
26
+ *
27
+ * ```
28
+ * GET /.well-known/webfinger?resource=acct:ada@example.com 200 3.1ms
29
+ * ```
30
+ *
31
+ * The fields are in a fixed order and separated by spaces, so the status is
32
+ * always the third and a line is readable by eye, by `grep` and by `awk`
33
+ * alike. The client address, when the site asks for one, goes last for that
34
+ * reason — turning it on moves nothing else.
35
+ *
36
+ * Only the request line is ever read: no body is touched, so nothing is
37
+ * buffered and no password posted to the login form can reach the log, and no
38
+ * header but the forwarded address a site has said to believe is looked at, so
39
+ * no cookie and no `Authorization` can either. The query string is logged as
40
+ * it arrived, because it is what a peer's WebFinger lookup carries and what a
41
+ * question about who asked for what is answered with.
42
+ */
43
+ export declare function createAccessLog(options: AccessLogOptions): MiddlewareHandler<GeekityEnv>;
44
+ //# sourceMappingURL=access-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-log.d.ts","sourceRoot":"","sources":["../src/access-log.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAG9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAErD,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,kBAAkB,GAAG,YAAY,CAAC,CAAC;IAChE,8CAA8C;IAC9C,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAsBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,iBAAiB,CAAC,UAAU,CAAC,CA4BxF"}
@@ -0,0 +1,71 @@
1
+ import { clientAddress } from './admin/throttle.js';
2
+ /** The default sink: one write per line, the newline included, like the CLI's. */
3
+ function writeToStdout(line) {
4
+ process.stdout.write(`${line}\n`);
5
+ }
6
+ /**
7
+ * The path and query string of a request, taken out of its URL by hand.
8
+ *
9
+ * `new URL(...)` per request would parse and allocate the whole of an address
10
+ * this only ever wants the tail of, and this runs on every request the site
11
+ * answers. There is nothing to escape: Node's HTTP parser refuses a request
12
+ * target with a control character in it, so whatever survives to here is one
13
+ * line's worth of text.
14
+ */
15
+ function requestTarget(url) {
16
+ const afterScheme = url.indexOf('://');
17
+ const start = afterScheme < 0 ? -1 : url.indexOf('/', afterScheme + 3);
18
+ return start < 0 ? '/' : url.slice(start);
19
+ }
20
+ /**
21
+ * One line per request: what was asked, what was answered, and how long it
22
+ * took.
23
+ *
24
+ * ```
25
+ * GET /.well-known/webfinger?resource=acct:ada@example.com 200 3.1ms
26
+ * ```
27
+ *
28
+ * The fields are in a fixed order and separated by spaces, so the status is
29
+ * always the third and a line is readable by eye, by `grep` and by `awk`
30
+ * alike. The client address, when the site asks for one, goes last for that
31
+ * reason — turning it on moves nothing else.
32
+ *
33
+ * Only the request line is ever read: no body is touched, so nothing is
34
+ * buffered and no password posted to the login form can reach the log, and no
35
+ * header but the forwarded address a site has said to believe is looked at, so
36
+ * no cookie and no `Authorization` can either. The query string is logged as
37
+ * it arrived, because it is what a peer's WebFinger lookup carries and what a
38
+ * question about who asked for what is answered with.
39
+ */
40
+ export function createAccessLog(options) {
41
+ const write = options.write ?? writeToStdout;
42
+ const { accessLogAddress, trustProxy } = options.config;
43
+ return async function accessLog(c, next) {
44
+ const started = performance.now();
45
+ let threw = false;
46
+ try {
47
+ await next();
48
+ }
49
+ catch (error) {
50
+ // A handler that threw has no response of its own: the error is on its
51
+ // way up to Hono's error handler, which answers 500, and `c.res` would
52
+ // hand back a freshly made 200 if asked. A request that failed is the
53
+ // one most worth having a line for, so it gets the status it will be
54
+ // answered with rather than the one it never had.
55
+ threw = true;
56
+ throw error;
57
+ }
58
+ finally {
59
+ const elapsed = (performance.now() - started).toFixed(1);
60
+ const status = threw ? 500 : c.res.status;
61
+ let line = `${c.req.method} ${requestTarget(c.req.url)} ${String(status)} ${elapsed}ms`;
62
+ if (accessLogAddress) {
63
+ const address = clientAddress(c, { trustProxy });
64
+ if (address !== undefined)
65
+ line += ` ${address}`;
66
+ }
67
+ write(line);
68
+ }
69
+ };
70
+ }
71
+ //# sourceMappingURL=access-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-log.js","sourceRoot":"","sources":["../src/access-log.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAyBpD,kFAAkF;AAClF,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;AACpC,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;IACvE,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe,CAAC,OAAyB;IACvD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC;IAC7C,MAAM,EAAE,gBAAgB,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAExD,OAAO,KAAK,UAAU,SAAS,CAAC,CAAC,EAAE,IAAI;QACrC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAClC,IAAI,KAAK,GAAG,KAAK,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,IAAI,EAAE,CAAC;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,uEAAuE;YACvE,uEAAuE;YACvE,sEAAsE;YACtE,qEAAqE;YACrE,kDAAkD;YAClD,KAAK,GAAG,IAAI,CAAC;YACb,MAAM,KAAK,CAAC;QACd,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACzD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;YAC1C,IAAI,IAAI,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,OAAO,IAAI,CAAC;YACxF,IAAI,gBAAgB,EAAE,CAAC;gBACrB,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;gBACjD,IAAI,OAAO,KAAK,SAAS;oBAAE,IAAI,IAAI,IAAI,OAAO,EAAE,CAAC;YACnD,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,CAAC;QACd,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -125,6 +125,16 @@ export interface ProfileLink {
125
125
  readonly label: string;
126
126
  /** Where it goes. */
127
127
  readonly href: string;
128
+ /**
129
+ * The `rel` values typed after the URL, as one string, or absent when none
130
+ * were (TASK-114).
131
+ *
132
+ * What was typed and only that. `rel="me"` is added to every profile link
133
+ * when it is published, which is what the box is for, so a link that says
134
+ * nothing here is still rendered `rel="me"` and one that says `me` is
135
+ * rendered it once.
136
+ */
137
+ readonly rel?: string | undefined;
128
138
  }
129
139
  /**
130
140
  * The public face of a user: what their archive is headed with and what their
@@ -1 +1 @@
1
- {"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/admin/accounts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/admin/accounts.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { readFileIfPresentSync, updateFileAtomically, writeFileAtomicallySync, } from '../files/atomic.js';
3
3
  import { deliveryMode, notificationEvent, withNotification, withNotificationMode, } from '../notifications/preferences.js';
4
+ import { relText } from '../web/navigation.js';
4
5
  import { hashPassword, verifyPasswordHash } from './passwords.js';
5
6
  /**
6
7
  * Who may sign in: `data/users.json`, and everything that reads or writes it.
@@ -229,11 +230,16 @@ export function cleanProfile(profile) {
229
230
  const jobTitle = (profile.jobTitle ?? '').trim();
230
231
  const location = (profile.location ?? '').trim();
231
232
  const links = (profile.links ?? [])
232
- .map((link) => ({ label: link.label.trim(), href: link.href.trim() }))
233
+ .map((link) => {
234
+ // The rel values go through the same normaliser a menu item's do, so a
235
+ // hand-edited file and a saved box store one spelling (TASK-114).
236
+ const rel = relText([link.rel ?? '']);
237
+ return { label: link.label.trim(), href: link.href.trim(), ...(rel === '' ? {} : { rel }) };
238
+ })
233
239
  // A link needs somewhere to go; a label it does not have is the URL again,
234
240
  // because a list of blank links is worse than a list of bare addresses.
235
241
  .filter((link) => link.href !== '')
236
- .map((link) => (link.label === '' ? { label: link.href, href: link.href } : link));
242
+ .map((link) => (link.label === '' ? { ...link, label: link.href } : link));
237
243
  const cleaned = {
238
244
  ...(displayName === '' ? {} : { displayName }),
239
245
  ...(bio === '' ? {} : { bio }),
@@ -635,9 +641,15 @@ function linksFrom(value) {
635
641
  const record = entry;
636
642
  const href = record['href'];
637
643
  const label = record['label'];
644
+ const rel = record['rel'];
638
645
  if (typeof href !== 'string')
639
646
  continue;
640
- links.push({ label: typeof label === 'string' ? label : '', href });
647
+ const carried = typeof rel === 'string' ? relText([rel]) : '';
648
+ links.push({
649
+ label: typeof label === 'string' ? label : '',
650
+ href,
651
+ ...(carried === '' ? {} : { rel: carried }),
652
+ });
641
653
  }
642
654
  return links;
643
655
  }