@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.
- package/README.md +246 -110
- package/admin/pages/documents/editor.njk +0 -13
- package/admin/pages/{settings/federation.njk → federation/settings.njk} +7 -5
- package/admin/pages/navigation/menus.njk +132 -0
- package/admin/pages/settings/reading.njk +2 -10
- package/admin/pages/tools/content-index.njk +89 -0
- package/admin/pages/users/edit.njk +25 -2
- package/admin/pages/users/list.njk +5 -2
- package/admin/static/admin.css +78 -3
- package/dist/access-log.d.ts +44 -0
- package/dist/access-log.d.ts.map +1 -0
- package/dist/access-log.js +71 -0
- package/dist/access-log.js.map +1 -0
- package/dist/admin/accounts.d.ts +10 -0
- package/dist/admin/accounts.d.ts.map +1 -1
- package/dist/admin/accounts.js +15 -3
- package/dist/admin/accounts.js.map +1 -1
- package/dist/admin/documents.d.ts +3 -13
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +2 -34
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/federation.d.ts +10 -0
- package/dist/admin/federation.d.ts.map +1 -1
- package/dist/admin/federation.js +10 -0
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +2 -2
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +19 -2
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/navigation.d.ts +155 -0
- package/dist/admin/navigation.d.ts.map +1 -0
- package/dist/admin/navigation.js +292 -0
- package/dist/admin/navigation.js.map +1 -0
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +9 -0
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/session.d.ts +17 -3
- package/dist/admin/session.d.ts.map +1 -1
- package/dist/admin/session.js +19 -5
- package/dist/admin/session.js.map +1 -1
- package/dist/admin/settings-federation.d.ts +10 -1
- package/dist/admin/settings-federation.d.ts.map +1 -1
- package/dist/admin/settings-federation.js +26 -6
- package/dist/admin/settings-federation.js.map +1 -1
- package/dist/admin/settings-page.d.ts +26 -9
- package/dist/admin/settings-page.d.ts.map +1 -1
- package/dist/admin/settings-page.js +18 -12
- package/dist/admin/settings-page.js.map +1 -1
- package/dist/admin/settings-pages.d.ts +14 -3
- package/dist/admin/settings-pages.d.ts.map +1 -1
- package/dist/admin/settings-pages.js +15 -5
- package/dist/admin/settings-pages.js.map +1 -1
- package/dist/admin/settings-reading.d.ts +5 -4
- package/dist/admin/settings-reading.d.ts.map +1 -1
- package/dist/admin/settings-reading.js +6 -5
- package/dist/admin/settings-reading.js.map +1 -1
- package/dist/admin/settings.d.ts +29 -15
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +28 -78
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/templates.d.ts +3 -1
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +3 -1
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/tools.d.ts +111 -0
- package/dist/admin/tools.d.ts.map +1 -0
- package/dist/admin/tools.js +182 -0
- package/dist/admin/tools.js.map +1 -0
- package/dist/admin/users.d.ts +24 -0
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +102 -30
- package/dist/admin/users.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/comments/form.d.ts +48 -0
- package/dist/comments/form.d.ts.map +1 -1
- package/dist/comments/form.js +15 -0
- package/dist/comments/form.js.map +1 -1
- package/dist/comments/index.d.ts +4 -3
- package/dist/comments/index.d.ts.map +1 -1
- package/dist/comments/index.js +2 -1
- package/dist/comments/index.js.map +1 -1
- package/dist/comments/routes.d.ts +3 -1
- package/dist/comments/routes.d.ts.map +1 -1
- package/dist/comments/routes.js +46 -11
- package/dist/comments/routes.js.map +1 -1
- package/dist/comments/submission.d.ts +37 -3
- package/dist/comments/submission.d.ts.map +1 -1
- package/dist/comments/submission.js +59 -20
- package/dist/comments/submission.js.map +1 -1
- package/dist/comments/viewer.d.ts +21 -0
- package/dist/comments/viewer.d.ts.map +1 -0
- package/dist/comments/viewer.js +45 -0
- package/dist/comments/viewer.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/content/store.d.ts +18 -0
- package/dist/content/store.d.ts.map +1 -1
- package/dist/content/store.js +18 -0
- package/dist/content/store.js.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/mount.d.ts +4 -0
- package/dist/federation/mount.d.ts.map +1 -1
- package/dist/federation/mount.js +60 -15
- package/dist/federation/mount.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/web/authors.d.ts +15 -1
- package/dist/web/authors.d.ts.map +1 -1
- package/dist/web/authors.js +9 -1
- package/dist/web/authors.js.map +1 -1
- package/dist/web/index.d.ts +4 -4
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +1 -1
- package/dist/web/index.js.map +1 -1
- package/dist/web/navigation.d.ts +235 -35
- package/dist/web/navigation.d.ts.map +1 -1
- package/dist/web/navigation.js +333 -57
- package/dist/web/navigation.js.map +1 -1
- package/dist/web/negotiate.d.ts +22 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +25 -10
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/render.d.ts +16 -10
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +21 -14
- package/dist/web/render.js.map +1 -1
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +11 -2
- package/dist/web/routes.js.map +1 -1
- package/dist/web/themes.d.ts +21 -0
- package/dist/web/themes.d.ts.map +1 -1
- package/dist/web/themes.js +37 -0
- package/dist/web/themes.js.map +1 -1
- package/package.json +1 -1
- package/templates/site/content/_data/site.json +7 -0
- package/templates/site/content/pages/about.md +7 -0
- package/themes/default/README.md +215 -80
- package/themes/default/layouts/author.njk +3 -4
- package/themes/default/layouts/base.njk +38 -49
- package/themes/default/layouts/front-page.njk +1 -2
- package/themes/default/partials/bio.njk +20 -22
- package/themes/default/partials/comment-form.njk +31 -3
- package/themes/default/partials/menu.njk +34 -0
- package/themes/default/partials/search-form.njk +5 -0
- package/themes/default/static/style.css +54 -13
- 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> — 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 —
|
|
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> —
|
|
84
|
+
the name the template loops over as <code>menus.<name></code> — 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 —
|
|
121
|
+
<code>{% raw %}{% for item in menus.footer %}{% endraw %}</code> — 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> — 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 — over ssh, or with a
|
|
18
|
+
<code>git pull</code> while the site was down — 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 — under a second
|
|
63
|
+
on a small site, longer on a large one — 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 —
|
|
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
|
|
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 %}
|
package/admin/static/admin.css
CHANGED
|
@@ -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:
|
|
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
|
-
.
|
|
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"}
|
package/dist/admin/accounts.d.ts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/admin/accounts.ts"],"names":[],"mappings":"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"}
|
package/dist/admin/accounts.js
CHANGED
|
@@ -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) =>
|
|
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 === '' ? {
|
|
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
|
-
|
|
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
|
}
|