@geekity/cms 0.18.0 → 0.20.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 (182) hide show
  1. package/README.md +29 -25
  2. package/admin/components/admin-bar.njk +4 -3
  3. package/admin/components/public-admin-bar.njk +3 -1
  4. package/admin/layouts/shell.njk +1 -0
  5. package/admin/pages/documents/editor.njk +291 -192
  6. package/admin/static/admin.css +123 -13
  7. package/admin/static/location.js +38 -0
  8. package/dist/admin/documents.d.ts +18 -3
  9. package/dist/admin/documents.d.ts.map +1 -1
  10. package/dist/admin/documents.js +150 -21
  11. package/dist/admin/documents.js.map +1 -1
  12. package/dist/admin/editor-layout.d.ts +13 -0
  13. package/dist/admin/editor-layout.d.ts.map +1 -0
  14. package/dist/admin/editor-layout.js +62 -0
  15. package/dist/admin/editor-layout.js.map +1 -0
  16. package/dist/admin/enclosure-field.d.ts +2 -3
  17. package/dist/admin/enclosure-field.d.ts.map +1 -1
  18. package/dist/admin/enclosure-field.js +27 -7
  19. package/dist/admin/enclosure-field.js.map +1 -1
  20. package/dist/admin/headers.d.ts +1 -0
  21. package/dist/admin/headers.d.ts.map +1 -1
  22. package/dist/admin/headers.js +3 -0
  23. package/dist/admin/headers.js.map +1 -1
  24. package/dist/admin/location-field.d.ts +4 -3
  25. package/dist/admin/location-field.d.ts.map +1 -1
  26. package/dist/admin/location-field.js +10 -2
  27. package/dist/admin/location-field.js.map +1 -1
  28. package/dist/admin/media.d.ts.map +1 -1
  29. package/dist/admin/media.js +3 -0
  30. package/dist/admin/media.js.map +1 -1
  31. package/dist/admin/photo-field.d.ts +2 -3
  32. package/dist/admin/photo-field.d.ts.map +1 -1
  33. package/dist/admin/photo-field.js +4 -2
  34. package/dist/admin/photo-field.js.map +1 -1
  35. package/dist/admin/read-field.d.ts +2 -3
  36. package/dist/admin/read-field.d.ts.map +1 -1
  37. package/dist/admin/read-field.js +12 -4
  38. package/dist/admin/read-field.js.map +1 -1
  39. package/dist/config.d.ts +2 -0
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/config.js.map +1 -1
  42. package/dist/content/citation.d.ts +5 -0
  43. package/dist/content/citation.d.ts.map +1 -1
  44. package/dist/content/citation.js +17 -0
  45. package/dist/content/citation.js.map +1 -1
  46. package/dist/content/kept-properties.d.ts +5 -0
  47. package/dist/content/kept-properties.d.ts.map +1 -0
  48. package/dist/content/kept-properties.js +12 -0
  49. package/dist/content/kept-properties.js.map +1 -0
  50. package/dist/content/location.d.ts +4 -0
  51. package/dist/content/location.d.ts.map +1 -1
  52. package/dist/content/location.js +35 -3
  53. package/dist/content/location.js.map +1 -1
  54. package/dist/content/locations.d.ts +2 -5
  55. package/dist/content/locations.d.ts.map +1 -1
  56. package/dist/content/locations.js +2 -77
  57. package/dist/content/locations.js.map +1 -1
  58. package/dist/content/markdown.d.ts +9 -0
  59. package/dist/content/markdown.d.ts.map +1 -1
  60. package/dist/content/markdown.js +37 -1
  61. package/dist/content/markdown.js.map +1 -1
  62. package/dist/content/permalink-file.d.ts +7 -0
  63. package/dist/content/permalink-file.d.ts.map +1 -0
  64. package/dist/content/permalink-file.js +77 -0
  65. package/dist/content/permalink-file.js.map +1 -0
  66. package/dist/content/post-type.d.ts +1 -6
  67. package/dist/content/post-type.d.ts.map +1 -1
  68. package/dist/content/post-type.js +13 -14
  69. package/dist/content/post-type.js.map +1 -1
  70. package/dist/content/slug.d.ts +2 -1
  71. package/dist/content/slug.d.ts.map +1 -1
  72. package/dist/content/slug.js +3 -1
  73. package/dist/content/slug.js.map +1 -1
  74. package/dist/env.d.ts +6 -0
  75. package/dist/env.d.ts.map +1 -1
  76. package/dist/federation/article.d.ts +8 -7
  77. package/dist/federation/article.d.ts.map +1 -1
  78. package/dist/federation/article.js +37 -18
  79. package/dist/federation/article.js.map +1 -1
  80. package/dist/federation/citations.d.ts +7 -6
  81. package/dist/federation/citations.d.ts.map +1 -1
  82. package/dist/federation/citations.js +44 -28
  83. package/dist/federation/citations.js.map +1 -1
  84. package/dist/federation/delivery.d.ts.map +1 -1
  85. package/dist/federation/delivery.js +23 -16
  86. package/dist/federation/delivery.js.map +1 -1
  87. package/dist/index.d.ts +5 -4
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +2 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/indieauth/bearer.d.ts +10 -6
  92. package/dist/indieauth/bearer.d.ts.map +1 -1
  93. package/dist/indieauth/bearer.js +11 -9
  94. package/dist/indieauth/bearer.js.map +1 -1
  95. package/dist/indieauth/token.js +1 -1
  96. package/dist/indieauth/token.js.map +1 -1
  97. package/dist/micropub/content.d.ts +15 -0
  98. package/dist/micropub/content.d.ts.map +1 -0
  99. package/dist/micropub/content.js +254 -0
  100. package/dist/micropub/content.js.map +1 -0
  101. package/dist/micropub/create.d.ts +7 -3
  102. package/dist/micropub/create.d.ts.map +1 -1
  103. package/dist/micropub/create.js +75 -24
  104. package/dist/micropub/create.js.map +1 -1
  105. package/dist/micropub/endpoint.d.ts +4 -5
  106. package/dist/micropub/endpoint.d.ts.map +1 -1
  107. package/dist/micropub/endpoint.js +33 -12
  108. package/dist/micropub/endpoint.js.map +1 -1
  109. package/dist/micropub/media.d.ts +6 -4
  110. package/dist/micropub/media.d.ts.map +1 -1
  111. package/dist/micropub/media.js +7 -5
  112. package/dist/micropub/media.js.map +1 -1
  113. package/dist/micropub/update.d.ts +7 -1
  114. package/dist/micropub/update.d.ts.map +1 -1
  115. package/dist/micropub/update.js +32 -7
  116. package/dist/micropub/update.js.map +1 -1
  117. package/dist/web/admin-bar.d.ts.map +1 -1
  118. package/dist/web/admin-bar.js +27 -1
  119. package/dist/web/admin-bar.js.map +1 -1
  120. package/dist/web/context.d.ts +10 -3
  121. package/dist/web/context.d.ts.map +1 -1
  122. package/dist/web/context.js +12 -2
  123. package/dist/web/context.js.map +1 -1
  124. package/dist/web/documents.d.ts +21 -6
  125. package/dist/web/documents.d.ts.map +1 -1
  126. package/dist/web/documents.js +23 -12
  127. package/dist/web/documents.js.map +1 -1
  128. package/dist/web/feed-item.d.ts +26 -3
  129. package/dist/web/feed-item.d.ts.map +1 -1
  130. package/dist/web/feed-item.js +63 -6
  131. package/dist/web/feed-item.js.map +1 -1
  132. package/dist/web/feed-rss.js +3 -3
  133. package/dist/web/feed-rss.js.map +1 -1
  134. package/dist/web/feed-source.d.ts +7 -0
  135. package/dist/web/feed-source.d.ts.map +1 -1
  136. package/dist/web/feed-source.js.map +1 -1
  137. package/dist/web/feeds.d.ts.map +1 -1
  138. package/dist/web/feeds.js +7 -1
  139. package/dist/web/feeds.js.map +1 -1
  140. package/dist/web/negotiate.d.ts +4 -0
  141. package/dist/web/negotiate.d.ts.map +1 -1
  142. package/dist/web/negotiate.js +5 -1
  143. package/dist/web/negotiate.js.map +1 -1
  144. package/dist/web/render.d.ts +5 -4
  145. package/dist/web/render.d.ts.map +1 -1
  146. package/dist/web/render.js +34 -12
  147. package/dist/web/render.js.map +1 -1
  148. package/dist/web/routes.d.ts.map +1 -1
  149. package/dist/web/routes.js +16 -4
  150. package/dist/web/routes.js.map +1 -1
  151. package/dist/web/sanitize.d.ts +10 -0
  152. package/dist/web/sanitize.d.ts.map +1 -1
  153. package/dist/web/sanitize.js +180 -0
  154. package/dist/web/sanitize.js.map +1 -1
  155. package/dist/web/templates.d.ts.map +1 -1
  156. package/dist/web/templates.js +2 -0
  157. package/dist/web/templates.js.map +1 -1
  158. package/dist/webmention/cited-picture.d.ts +37 -0
  159. package/dist/webmention/cited-picture.d.ts.map +1 -0
  160. package/dist/webmention/cited-picture.js +121 -0
  161. package/dist/webmention/cited-picture.js.map +1 -0
  162. package/dist/webmention/fetch-public.d.ts +26 -8
  163. package/dist/webmention/fetch-public.d.ts.map +1 -1
  164. package/dist/webmention/fetch-public.js +22 -12
  165. package/dist/webmention/fetch-public.js.map +1 -1
  166. package/dist/webmention/reply-context.d.ts +30 -8
  167. package/dist/webmention/reply-context.d.ts.map +1 -1
  168. package/dist/webmention/reply-context.js +238 -27
  169. package/dist/webmention/reply-context.js.map +1 -1
  170. package/dist/webmention/reply-contexts.d.ts +28 -16
  171. package/dist/webmention/reply-contexts.d.ts.map +1 -1
  172. package/dist/webmention/reply-contexts.js +108 -25
  173. package/dist/webmention/reply-contexts.js.map +1 -1
  174. package/package.json +10 -9
  175. package/themes/default/README.md +83 -9
  176. package/themes/default/layouts/post.njk +7 -7
  177. package/themes/default/partials/citations.njk +25 -4
  178. package/themes/default/partials/cited-page.njk +58 -0
  179. package/themes/default/partials/post-list.njk +8 -5
  180. package/themes/default/partials/reply-context.njk +9 -11
  181. package/themes/default/src/style.css +37 -0
  182. package/themes/default/static/style.css +83 -0
@@ -6,11 +6,56 @@
6
6
 
7
7
  {% block title %}{{ heading }}{% endblock %}
8
8
 
9
+ {% import "components/fields.njk" as field %}
10
+
11
+ {# The legend repeats the summary so every box in it is still announced with
12
+ the name of its group. #}
13
+ {% macro group(name, open, class='admin-editor-group') %}
14
+ <details class="{{ class }}"{% if open %} open{% endif %}>
15
+ <summary>{{ name }}</summary>
16
+ <fieldset>
17
+ <legend class="admin-visually-hidden">{{ name }}</legend>
18
+ {{ caller() }}
19
+ </fieldset>
20
+ </details>
21
+ {% endmacro %}
22
+
23
+ {% macro invalid(id, hint='') -%}
24
+ {% if errorField == id %} aria-invalid="true" aria-describedby="{{ id }}-error{% if hint %} {{ hint }}{% endif %}"{% elif hint %} aria-describedby="{{ hint }}"{% endif %}
25
+ {%- endmacro %}
26
+
27
+ {% macro problem(id) %}
28
+ {% if errorField == id %}<p class="admin-field-error" id="{{ id }}-error">{{ error }}</p>{% endif %}
29
+ {% endmacro %}
30
+
31
+ {% macro citedCard(property) %}
32
+ {% set card = citedPreviews[property] %}
33
+ {% if card %}
34
+ <div class="admin-cited-card">
35
+ <img src="{{ card.picture.src }}" alt="" width="{{ card.picture.width }}" height="{{ card.picture.height }}" loading="lazy">
36
+ <p class="admin-cited-title">{{ card.name }}</p>
37
+ <input type="checkbox" id="editor-preview-{{ property }}" name="preview" value="hide"{% if form.previewHidden %} checked{% endif %}>
38
+ <label class="admin-cited-remove" for="editor-preview-{{ property }}"><span aria-hidden="true">×</span><span class="admin-visually-hidden">Remove the preview of {{ card.name }}</span></label>
39
+ <p class="admin-cited-removed">Removed. The post shows the plain citation until you select the X again.</p>
40
+ {%- if card.described %}
41
+ <label for="editor-cited-alt">Alt text of the reposted image</label>
42
+ <input id="editor-cited-alt" name="cited-alt" type="text" value="{{ form.citedAlt or form.title }}"{{ invalid('editor-cited-alt', 'editor-cited-alt-hint') }} />
43
+ {{ problem('editor-cited-alt') }}
44
+ <p class="admin-hint" id="editor-cited-alt-hint">What the image shows, for readers who cannot see it. It starts as the post’s title.</p>
45
+ {%- endif %}
46
+ </div>
47
+ {% endif %}
48
+ {% endmacro %}
49
+
9
50
  {% block content %}
10
51
  <h1>{{ heading }}</h1>
11
52
 
12
- {% if error %}
13
- <p class="admin-error">{{ error }}</p>
53
+ {% if error and errorField %}
54
+ {% call field.summary('Nothing was saved. Fix the field marked below and try again.') %}
55
+ {{ field.problem(errorField, error) }}
56
+ {% endcall %}
57
+ {% elif error %}
58
+ {{ field.summary(error) }}
14
59
  {% endif %}
15
60
 
16
61
  {% if trashed %}
@@ -33,7 +78,8 @@
33
78
  <div class="admin-editor-main">
34
79
  <label for="editor-title">Title</label>
35
80
  {#- A post may go without one, and is then a note; a page is always named. -#}
36
- <input id="editor-title" name="title" type="text" value="{{ form.title }}"{% if kind.type == "page" %} required{% endif %} autofocus />
81
+ <input id="editor-title" name="title" type="text" value="{{ form.title }}"{% if kind.type == "page" %} required{% endif %} autofocus{{ invalid('editor-title') }} />
82
+ {{ problem('editor-title') }}
37
83
  {% if kind.type == "post" %}
38
84
  <p class="admin-hint">Leave it empty to write a note: the post is then its words, and takes its slug from the first of them.</p>
39
85
  {% endif %}
@@ -64,39 +110,42 @@
64
110
  </div>
65
111
 
66
112
  {% if kind.type == "post" %}
67
- <fieldset class="admin-editor-enclosure admin-editor-photos">
68
- <legend>Photos</legend>
113
+ {% call group('Photos', open.photos) %}
69
114
  <p class="admin-hint">
70
115
  Each is an image in the media library or an address starting https://. Leave the alt text
71
116
  empty to use the media library’s. Empty an address to remove that photo.
72
117
  </p>
73
118
  {% for row in photoRows %}
74
119
  {% set n = loop.index0 %}
75
- <fieldset class="admin-editor-alternate">
76
- <legend>{% if row.url %}Photo {{ loop.index }}{% else %}Add a photo{% endif %}</legend>
120
+ {% call group('Photo ' + loop.index if row.url else 'Add a photo', row.url, 'admin-editor-group admin-editor-row') %}
77
121
  <label for="editor-photo-url-{{ n }}">Address</label>
78
- <input id="editor-photo-url-{{ n }}" name="{{ photoFields.url }}{{ n }}" type="text" value="{{ row.url }}" list="editor-photo-uploads" autocomplete="off" />
122
+ <input id="editor-photo-url-{{ n }}" name="{{ photoFields.url }}{{ n }}" type="text" value="{{ row.url }}" list="editor-photo-uploads" autocomplete="off"{{ invalid('editor-photo-url-' + n) }} />
123
+ {{ problem('editor-photo-url-' + n) }}
79
124
  <label for="editor-photo-alt-{{ n }}">Alt text</label>
80
125
  <input id="editor-photo-alt-{{ n }}" name="{{ photoFields.alt }}{{ n }}" type="text" value="{{ row.alt }}"{% if row.libraryAlt %} placeholder="{{ row.libraryAlt }}"{% endif %} />
81
- </fieldset>
126
+ {% endcall %}
82
127
  {% endfor %}
83
128
  <datalist id="editor-photo-uploads">
84
129
  {% for url in photoChoices %}
85
130
  <option value="{{ url }}"></option>
86
131
  {% endfor %}
87
132
  </datalist>
88
- </fieldset>
89
- <fieldset class="admin-editor-enclosure admin-editor-location">
90
- <legend>Location</legend>
133
+ {% endcall %}
134
+
135
+ {% call group('Location', open.location) %}
91
136
  <p class="admin-hint">
92
137
  Kept in <code>data/locations.json</code>, not in the post’s file. What readers see of it
93
138
  is chosen on Settings &gt; Privacy. Empty every box to remove it.
94
139
  </p>
140
+ <button type="button" id="editor-location-here" class="admin-location-here" hidden>Use my location</button>
141
+ <p id="editor-location-status" class="admin-hint" role="status"></p>
95
142
  <label for="editor-location-geo">Coordinates</label>
96
- <input id="editor-location-geo" name="{{ locationFields.geo }}" type="text" value="{{ form.location.geo }}" placeholder="48.85837, 2.29448" autocomplete="off" spellcheck="false" aria-describedby="editor-location-geo-hint" />
143
+ <input id="editor-location-geo" name="{{ locationFields.geo }}" type="text" value="{{ form.location.geo }}" placeholder="48.85837, 2.29448" autocomplete="off" spellcheck="false"{{ invalid('editor-location-geo', 'editor-location-geo-hint') }} />
144
+ {{ problem('editor-location-geo') }}
97
145
  <p class="admin-hint" id="editor-location-geo-hint">Latitude, longitude, as decimal degrees. A <code>geo:</code> URI works too.</p>
98
146
  <label for="editor-location-accuracy">Accuracy</label>
99
- <input id="editor-location-accuracy" name="{{ locationFields.accuracy }}" type="text" inputmode="numeric" value="{{ form.location.accuracy }}" autocomplete="off" aria-describedby="editor-location-accuracy-hint" />
147
+ <input id="editor-location-accuracy" name="{{ locationFields.accuracy }}" type="text" inputmode="numeric" value="{{ form.location.accuracy }}" autocomplete="off"{{ invalid('editor-location-accuracy', 'editor-location-accuracy-hint') }} />
148
+ {{ problem('editor-location-accuracy') }}
100
149
  <p class="admin-hint" id="editor-location-accuracy-hint">Metres. Leave it empty if you do not know.</p>
101
150
  <label for="editor-location-name">Place</label>
102
151
  <input id="editor-location-name" name="{{ locationFields.name }}" type="text" value="{{ form.location.name }}" />
@@ -106,126 +155,215 @@
106
155
  <input id="editor-location-region" name="{{ locationFields.region }}" type="text" value="{{ form.location.region }}" />
107
156
  <label for="editor-location-country">Country</label>
108
157
  <input id="editor-location-country" name="{{ locationFields.country }}" type="text" value="{{ form.location.country }}" />
109
- </fieldset>
110
- <fieldset class="admin-editor-enclosure">
111
- <legend>Recording</legend>
158
+ <p class="admin-check">
159
+ <input id="editor-location-checkin" name="{{ locationFields.checkin }}" type="checkbox" value="1"{% if form.location.checkin %} checked{% endif %} aria-describedby="editor-location-checkin-hint" />
160
+ <label for="editor-location-checkin">A check-in</label>
161
+ </p>
162
+ <p class="admin-hint" id="editor-location-checkin-hint">A Micropub app sent it as a checkin, and reads it back as one. Readers see it as any other location.</p>
163
+ {% endcall %}
112
164
 
165
+ {% call group('Recording', open.recording) %}
113
166
  <label for="editor-enclosure-url">Audio or video file</label>
114
- <select id="editor-enclosure-url" name="{{ enclosureFields.url }}" aria-describedby="editor-enclosure-url-hint">
167
+ <select id="editor-enclosure-url" name="{{ enclosureFields.url }}"{{ invalid('editor-enclosure-url', 'editor-enclosure-url-hint') }}>
115
168
  <option value=""{% if form.enclosure.url == "" %} selected{% endif %}>None</option>
116
169
  {% for choice in enclosureChoices.media %}
117
170
  <option value="{{ choice.url }}"{% if choice.url == form.enclosure.url %} selected{% endif %}>{{ choice.label }}</option>
118
171
  {% endfor %}
119
172
  </select>
173
+ {{ problem('editor-enclosure-url') }}
120
174
  <p class="admin-hint" id="editor-enclosure-url-hint">
121
175
  Upload the file to the media library first. Feeds carry it as the post’s enclosure and the
122
176
  post page plays it. None removes the recording, its transcript and its other versions.
123
177
  </p>
124
178
 
125
179
  <label for="editor-enclosure-duration">Duration</label>
126
- <input id="editor-enclosure-duration" name="{{ enclosureFields.duration }}" type="text" inputmode="numeric" value="{{ form.enclosure.duration }}" autocomplete="off" aria-describedby="editor-enclosure-duration-hint" />
180
+ <input id="editor-enclosure-duration" name="{{ enclosureFields.duration }}" type="text" inputmode="numeric" value="{{ form.enclosure.duration }}" autocomplete="off"{{ invalid('editor-enclosure-duration', 'editor-enclosure-duration-hint') }} />
181
+ {{ problem('editor-enclosure-duration') }}
127
182
  <p class="admin-hint" id="editor-enclosure-duration-hint">Seconds, or 30:34, or 1:02:03. Leave it empty if you do not know.</p>
128
183
 
129
184
  <label for="editor-enclosure-transcript">Transcript or captions</label>
130
- <input id="editor-enclosure-transcript" name="{{ enclosureFields.transcriptUrl }}" type="text" value="{{ form.enclosure.transcriptUrl }}" list="editor-transcript-uploads" autocomplete="off" aria-describedby="editor-enclosure-transcript-hint" />
185
+ <input id="editor-enclosure-transcript" name="{{ enclosureFields.transcriptUrl }}" type="text" value="{{ form.enclosure.transcriptUrl }}" list="editor-transcript-uploads" autocomplete="off"{{ invalid('editor-enclosure-transcript', 'editor-enclosure-transcript-hint') }} />
186
+ {{ problem('editor-enclosure-transcript') }}
131
187
  <datalist id="editor-transcript-uploads">
132
188
  {% for url in enclosureChoices.transcripts %}
133
189
  <option value="{{ url }}"></option>
134
190
  {% endfor %}
135
191
  </datalist>
136
192
  <label for="editor-enclosure-transcript-type">Transcript type</label>
137
- <select id="editor-enclosure-transcript-type" name="{{ enclosureFields.transcriptType }}" aria-describedby="editor-enclosure-transcript-hint">
193
+ <select id="editor-enclosure-transcript-type" name="{{ enclosureFields.transcriptType }}"{{ invalid('editor-enclosure-transcript-type', 'editor-enclosure-transcript-hint') }}>
138
194
  <option value=""{% if form.enclosure.transcriptType == "" %} selected{% endif %}>From the file</option>
139
195
  {% for type in transcriptTypes %}
140
196
  <option value="{{ type }}"{% if type == form.enclosure.transcriptType %} selected{% endif %}>{{ type }}</option>
141
197
  {% endfor %}
142
198
  </select>
199
+ {{ problem('editor-enclosure-transcript-type') }}
143
200
  <p class="admin-hint" id="editor-enclosure-transcript-hint">
144
201
  A .vtt, .srt or .txt file in the media library, or an address starting https://. A linked
145
202
  transcript needs its type.
146
203
  </p>
147
204
 
148
- <h2 class="admin-editor-enclosure-heading">Other versions</h2>
149
- <p class="admin-hint">
150
- A video of an audio episode, a smaller file, another language. Each is a file in the media
151
- library or an address starting https://, which needs its type. Empty an address to remove
152
- that version.
153
- </p>
154
- {% for row in alternateRows %}
155
- {% set n = loop.index0 %}
156
- <fieldset class="admin-editor-alternate">
157
- <legend>{% if row.url %}Version {{ loop.index }}{% else %}Add a version{% endif %}</legend>
158
- <label for="editor-alternate-url-{{ n }}">Address</label>
159
- <input id="editor-alternate-url-{{ n }}" name="{{ enclosureFields.alternate.url }}{{ n }}" type="text" value="{{ row.url }}" list="editor-media-uploads" autocomplete="off" />
160
- <label for="editor-alternate-type-{{ n }}">Type</label>
161
- <input id="editor-alternate-type-{{ n }}" name="{{ enclosureFields.alternate.type }}{{ n }}" type="text" value="{{ row.type }}" placeholder="video/mp4" autocomplete="off" spellcheck="false" />
162
- <label for="editor-alternate-title-{{ n }}">Title</label>
163
- <input id="editor-alternate-title-{{ n }}" name="{{ enclosureFields.alternate.title }}{{ n }}" type="text" value="{{ row.title }}" maxlength="32" />
164
- <label for="editor-alternate-height-{{ n }}">Height</label>
165
- <input id="editor-alternate-height-{{ n }}" name="{{ enclosureFields.alternate.height }}{{ n }}" type="text" inputmode="numeric" value="{{ row.height }}" autocomplete="off" />
166
- <label for="editor-alternate-lang-{{ n }}">Language</label>
167
- <input id="editor-alternate-lang-{{ n }}" name="{{ enclosureFields.alternate.lang }}{{ n }}" type="text" value="{{ row.lang }}" autocomplete="off" spellcheck="false" />
168
- </fieldset>
169
- {% endfor %}
170
- <datalist id="editor-media-uploads">
171
- {% for choice in enclosureChoices.media %}
172
- <option value="{{ choice.url }}"></option>
205
+ {% call group('Other versions', open.versions) %}
206
+ <p class="admin-hint">
207
+ A video of an audio episode, a smaller file, another language. Each is a file in the media
208
+ library or an address starting https://, which needs its type. Empty an address to remove
209
+ that version.
210
+ </p>
211
+ {% for row in alternateRows %}
212
+ {% set n = loop.index0 %}
213
+ {% call group('Version ' + loop.index if row.url else 'Add a version', row.url, 'admin-editor-group admin-editor-row') %}
214
+ <label for="editor-alternate-url-{{ n }}">Address</label>
215
+ <input id="editor-alternate-url-{{ n }}" name="{{ enclosureFields.alternate.url }}{{ n }}" type="text" value="{{ row.url }}" list="editor-media-uploads" autocomplete="off"{{ invalid('editor-alternate-url-' + n) }} />
216
+ {{ problem('editor-alternate-url-' + n) }}
217
+ <label for="editor-alternate-type-{{ n }}">Type</label>
218
+ <input id="editor-alternate-type-{{ n }}" name="{{ enclosureFields.alternate.type }}{{ n }}" type="text" value="{{ row.type }}" placeholder="video/mp4" autocomplete="off" spellcheck="false"{{ invalid('editor-alternate-type-' + n) }} />
219
+ {{ problem('editor-alternate-type-' + n) }}
220
+ <label for="editor-alternate-title-{{ n }}">Title</label>
221
+ <input id="editor-alternate-title-{{ n }}" name="{{ enclosureFields.alternate.title }}{{ n }}" type="text" value="{{ row.title }}" maxlength="32"{{ invalid('editor-alternate-title-' + n) }} />
222
+ {{ problem('editor-alternate-title-' + n) }}
223
+ <label for="editor-alternate-height-{{ n }}">Height</label>
224
+ <input id="editor-alternate-height-{{ n }}" name="{{ enclosureFields.alternate.height }}{{ n }}" type="text" inputmode="numeric" value="{{ row.height }}" autocomplete="off"{{ invalid('editor-alternate-height-' + n) }} />
225
+ {{ problem('editor-alternate-height-' + n) }}
226
+ <label for="editor-alternate-lang-{{ n }}">Language</label>
227
+ <input id="editor-alternate-lang-{{ n }}" name="{{ enclosureFields.alternate.lang }}{{ n }}" type="text" value="{{ row.lang }}" autocomplete="off" spellcheck="false"{{ invalid('editor-alternate-lang-' + n) }} />
228
+ {{ problem('editor-alternate-lang-' + n) }}
229
+ {% endcall %}
173
230
  {% endfor %}
174
- </datalist>
175
- </fieldset>
231
+ <datalist id="editor-media-uploads">
232
+ {% for choice in enclosureChoices.media %}
233
+ <option value="{{ choice.url }}"></option>
234
+ {% endfor %}
235
+ </datalist>
236
+ {% endcall %}
237
+ {% endcall %}
176
238
  {% endif %}
177
239
  </div>
178
240
 
179
241
  <div class="admin-editor-side">
180
- <label for="editor-slug">Slug</label>
181
- <input id="editor-slug" name="slug" type="text" value="{{ form.slug }}" />
182
-
183
- <label for="editor-permalink">Permalink</label>
184
- <input id="editor-permalink" name="permalink" type="text" value="{{ form.permalink }}" />
185
- <p class="admin-hint">Leave it empty to let the {{ kind.singular }} take the URL its slug implies.</p>
186
-
187
- {% if kind.dated %}
188
- <label for="editor-date">Date</label>
189
- <input id="editor-date" name="date" type="text" value="{{ form.date }}" />
190
- {# The file holds a UTC instant; this field is that instant as the
191
- site's own clock reads it, so the zone has to be on the screen. #}
192
- <p class="admin-hint">Wall-clock time in {{ dateZone }}. Stored as UTC.</p>
193
- {% endif %}
242
+ {% call group('Publishing', open.publishing) %}
243
+ <p class="admin-check">
244
+ <input id="editor-draft" name="draft" type="checkbox" value="1"{% if form.draft %} checked{% endif %} />
245
+ <label for="editor-draft">Draft</label>
246
+ </p>
194
247
 
195
- {% if kind.tagged %}
196
- <label for="editor-tags">Tags</label>
197
- <input id="editor-tags" name="tags" type="text" value="{{ form.tags }}" />
198
- <p class="admin-hint">Separated by commas.</p>
199
- {% endif %}
248
+ {% if kind.dated %}
249
+ <label for="editor-date">Date</label>
250
+ <input id="editor-date" name="date" type="text" value="{{ form.date }}"{{ invalid('editor-date') }} />
251
+ {{ problem('editor-date') }}
252
+ {# The file holds a UTC instant; this field is that instant as the
253
+ site's own clock reads it, so the zone has to be on the screen. #}
254
+ <p class="admin-hint">Wall-clock time in {{ dateZone }}. Stored as UTC.</p>
255
+ {% endif %}
256
+
257
+ <label for="editor-visibility">Visibility</label>
258
+ <select id="editor-visibility" name="visibility" aria-describedby="editor-visibility-hint">
259
+ {% if form.visibility.unrecognized is defined %}
260
+ <option value="{{ form.visibility.unrecognized }}" selected>{{ form.visibility.unrecognized }} (not recognized)</option>
261
+ {% endif %}
262
+ <option value="public"{% if form.visibility == "public" %} selected{% endif %}>Public</option>
263
+ <option value="unlisted"{% if form.visibility == "unlisted" %} selected{% endif %}>Unlisted</option>
264
+ </select>
265
+ <p class="admin-hint" id="editor-visibility-hint">
266
+ {% if form.visibility.unrecognized is defined %}
267
+ The front matter says <code>visibility: {{ form.visibility.unrecognized }}</code>, which this
268
+ site does not recognize. The {{ kind.singular }} is hidden until you choose Public or Unlisted:
269
+ its page answers 404 and it is not listed or sent to your followers.
270
+ {% endif %}
271
+ Unlisted writes <code>visibility: unlisted</code> into the front matter. The {{ kind.singular }}
272
+ keeps its page and still goes to your followers, but is left off the home page, archives,
273
+ feeds, the sitemap, search and llms.txt, and asks search engines not to index it.
274
+ </p>
275
+
276
+ {% if kind.type == "post" %}
277
+ {# TASK-207: shown at the top of the author's fediverse profile, from
278
+ the featured collection their actor links. #}
279
+ <p class="admin-check">
280
+ <input id="editor-pinned" name="pinned" type="checkbox" value="1"{% if form.pinned %} checked{% endif %}{{ invalid('editor-pinned') }} />
281
+ <label for="editor-pinned">Pinned</label>
282
+ </p>
283
+ {{ problem('editor-pinned') }}
284
+ <p class="admin-hint">
285
+ Writes <code>pinned:</code> and the moment you pinned it into the front matter. Pinned posts
286
+ show first on your fediverse profile, newest pin first, up to five.
287
+ </p>
288
+ {% endif %}
200
289
 
201
- {% if kind.categorised %}
202
- <label for="editor-categories">Categories</label>
203
- <input id="editor-categories" name="categories" type="text" value="{{ form.categories }}" />
204
- <p class="admin-hint">Separated by commas.</p>
290
+ {# Who the front matter says wrote this (TASK-67). A select of the site's
291
+ users rather than a free box, because after decision-14 the value is a
292
+ person rather than a string: it decides whose archive the post is on
293
+ and, once the actors land, whose followers hear about it. A file that
294
+ still names somebody who has no account keeps an option of its own, so
295
+ opening the editor cannot silently reattribute it. #}
296
+ <label for="editor-author">Author</label>
297
+ <select id="editor-author" name="author">
298
+ {% for choice in authors %}
299
+ <option value="{{ choice.value }}"{% if choice.chosen %} selected{% endif %}>{{ choice.label }}</option>
300
+ {% endfor %}
301
+ </select>
302
+ {% endcall %}
303
+
304
+ {% if kind.tagged or kind.categorised %}
305
+ {% call group('Tags and categories', open.taxonomy) %}
306
+ {% if kind.tagged %}
307
+ <label for="editor-tags">Tags</label>
308
+ <input id="editor-tags" name="tags" type="text" value="{{ form.tags }}" />
309
+ <p class="admin-hint">Separated by commas.</p>
310
+ {% endif %}
311
+
312
+ {% if kind.categorised %}
313
+ <label for="editor-categories">Categories</label>
314
+ <input id="editor-categories" name="categories" type="text" value="{{ form.categories }}" />
315
+ <p class="admin-hint">Separated by commas.</p>
316
+ {% endif %}
317
+ {% endcall %}
205
318
  {% endif %}
206
319
 
320
+ {% call group('Address', open.address) %}
321
+ <label for="editor-slug">Slug</label>
322
+ <input id="editor-slug" name="slug" type="text" value="{{ form.slug }}"{{ invalid('editor-slug') }} />
323
+ {{ problem('editor-slug') }}
324
+
325
+ <label for="editor-permalink">Permalink</label>
326
+ <input id="editor-permalink" name="permalink" type="text" value="{{ form.permalink }}" />
327
+ <p class="admin-hint">Leave it empty to let the {{ kind.singular }} take the URL its slug implies.</p>
328
+ {% endcall %}
329
+
207
330
  {% if kind.type == "post" %}
208
- {#- The mf2 `in-reply-to`: a URL here makes the post a reply, threads
209
- it in the fediverse and sends that page a webmention. -#}
210
- <label for="editor-in-reply-to">In reply to</label>
211
- <input id="editor-in-reply-to" name="in-reply-to" type="url" value="{{ form.inReplyTo }}" />
212
- <p class="admin-hint">The address of the post this one answers. Leave it empty for a post that answers nobody.</p>
213
-
214
- {#- The mf2 `like-of`, `repost-of` and `bookmark-of` (TASK-169): each
215
- cites a page the way a reply does, and sends it a webmention. -#}
216
- <label for="editor-like-of">Like of</label>
217
- <input id="editor-like-of" name="like-of" type="url" value="{{ form.likeOf }}" />
218
- <label for="editor-repost-of">Repost of</label>
219
- <input id="editor-repost-of" name="repost-of" type="url" value="{{ form.repostOf }}" />
220
- <label for="editor-bookmark-of">Bookmark of</label>
221
- <input id="editor-bookmark-of" name="bookmark-of" type="url" value="{{ form.bookmarkOf }}" />
222
- <p class="admin-hint">The address of a post this one likes, reposts or bookmarks. Leave them empty for a post that cites nothing.</p>
223
-
224
- <fieldset class="admin-editor-enclosure admin-editor-read">
225
- <legend>Read</legend>
331
+ {% call group('Responding to', open.responding) %}
332
+ {#- The mf2 `in-reply-to`: a URL here makes the post a reply, threads
333
+ it in the fediverse and sends that page a webmention. -#}
334
+ <label for="editor-in-reply-to">In reply to</label>
335
+ <input id="editor-in-reply-to" name="in-reply-to" type="url" value="{{ form.inReplyTo }}"{{ invalid('editor-in-reply-to') }} />
336
+ {{ problem('editor-in-reply-to') }}
337
+ {{ citedCard('in-reply-to') }}
338
+ <p class="admin-hint">The address of the post this one answers. Leave it empty for a post that answers nobody.</p>
339
+
340
+ {#- The mf2 `like-of`, `repost-of` and `bookmark-of` (TASK-169): each
341
+ cites a page the way a reply does, and sends it a webmention. -#}
342
+ <label for="editor-like-of">Like of</label>
343
+ <input id="editor-like-of" name="like-of" type="url" value="{{ form.likeOf }}"{{ invalid('editor-like-of') }} />
344
+ {{ problem('editor-like-of') }}
345
+ {{ citedCard('like-of') }}
346
+ <label for="editor-repost-of">Repost of</label>
347
+ <input id="editor-repost-of" name="repost-of" type="url" value="{{ form.repostOf }}"{{ invalid('editor-repost-of') }} />
348
+ {{ problem('editor-repost-of') }}
349
+ {{ citedCard('repost-of') }}
350
+ <label for="editor-bookmark-of">Bookmark of</label>
351
+ <input id="editor-bookmark-of" name="bookmark-of" type="url" value="{{ form.bookmarkOf }}"{{ invalid('editor-bookmark-of') }} />
352
+ {{ problem('editor-bookmark-of') }}
353
+ {{ citedCard('bookmark-of') }}
354
+ <p class="admin-hint">The address of a post this one likes, reposts or bookmarks. Leave them empty for a post that cites nothing.</p>
355
+ {% if form.previewHidden and not (citedPreviews | length) %}
356
+ <input type="hidden" name="preview" value="hide">
357
+ {% endif %}
358
+ {% if form.citedAlt and not describesCitedImage %}
359
+ <input type="hidden" name="cited-alt" value="{{ form.citedAlt }}">
360
+ {% endif %}
361
+ {% endcall %}
362
+
363
+ {% call group('Read', open.read) %}
226
364
  <label for="editor-read-status">Read status</label>
227
- <select id="editor-read-status" name="{{ readFields.status }}">
228
- <option value=""{% if form.readStatus == "" %} selected{% endif %}>Not a read</option>
365
+ <select id="editor-read-status" name="{{ readFields.status }}"{{ invalid('editor-read-status') }}>
366
+ <option value=""{% if form.readStatus == "" %} selected{% endif %}>Not a read post</option>
229
367
  {% for status in readStatuses %}
230
368
  <option value="{{ status.value }}"{% if form.readStatus == status.value %} selected{% endif %}>{{ status.label }}</option>
231
369
  {% endfor %}
@@ -233,83 +371,39 @@
233
371
  <option value="{{ unrecognizedReadStatus }}" selected>{{ unrecognizedReadStatus }} (not recognized)</option>
234
372
  {% endif %}
235
373
  </select>
374
+ {{ problem('editor-read-status') }}
236
375
  <label for="editor-read-of-name">Title</label>
237
- <input id="editor-read-of-name" name="{{ readFields.name }}" type="text" value="{{ form.readOf.name }}" />
376
+ <input id="editor-read-of-name" name="{{ readFields.name }}" type="text" value="{{ form.readOf.name }}"{{ invalid('editor-read-of-name') }} />
377
+ {{ problem('editor-read-of-name') }}
238
378
  <label for="editor-read-of-author">Author</label>
239
379
  <input id="editor-read-of-author" name="{{ readFields.author }}" type="text" value="{{ form.readOf.author }}" />
240
380
  <label for="editor-read-of-uid">Identifier</label>
241
381
  <input id="editor-read-of-uid" name="{{ readFields.uid }}" type="text" value="{{ form.readOf.uid }}" placeholder="isbn:9780441478125" autocomplete="off" spellcheck="false" />
242
382
  <label for="editor-read-of-url">Address</label>
243
- <input id="editor-read-of-url" name="{{ readFields.url }}" type="url" value="{{ form.readOf.url }}" />
244
- <p class="admin-hint">A book or paper this post says its author read. An identifier is an ISBN or a DOI, written isbn:… or doi:…. Choose Not a read and empty every box to remove it. A read is described by what it says, so leave its Description empty.</p>
245
- </fieldset>
383
+ <input id="editor-read-of-url" name="{{ readFields.url }}" type="url" value="{{ form.readOf.url }}"{{ invalid('editor-read-of-url') }} />
384
+ {{ problem('editor-read-of-url') }}
385
+ <p class="admin-hint">A book or paper this post says its author read. An identifier is an ISBN or a DOI, written isbn:… or doi:…. Choose Not a read post and empty every box to remove it. A read is described by what it says, so leave its Description empty.</p>
386
+ {% endcall %}
246
387
  {% endif %}
247
388
 
248
- <label for="editor-description">Description</label>
249
- <input id="editor-description" name="description" type="text" value="{{ form.description }}" />
250
-
251
- {#- The `lang` front matter (TASK-154): a post or page written in another
252
- language than the site's says so, for screen readers, translation
253
- and fediverse language filters. -#}
254
- <label for="editor-lang">Language</label>
255
- <input id="editor-lang" name="lang" type="text" value="{{ form.lang }}" autocomplete="off" spellcheck="false" aria-describedby="editor-lang-hint" />
256
- <p class="admin-hint" id="editor-lang-hint">A language tag, such as fr or pt-BR. Leave it empty for the site’s language, {{ siteLanguage }}.</p>
257
-
258
- {# Who the front matter says wrote this (TASK-67). A select of the site's
259
- users rather than a free box, because after decision-14 the value is a
260
- person rather than a string: it decides whose archive the post is on
261
- and, once the actors land, whose followers hear about it. A file that
262
- still names somebody who has no account keeps an option of its own, so
263
- opening the editor cannot silently reattribute it. #}
264
- <label for="editor-author">Author</label>
265
- <select id="editor-author" name="author">
266
- {% for choice in authors %}
267
- <option value="{{ choice.value }}"{% if choice.chosen %} selected{% endif %}>{{ choice.label }}</option>
268
- {% endfor %}
269
- </select>
270
-
271
- <p class="admin-check">
272
- <input id="editor-draft" name="draft" type="checkbox" value="1"{% if form.draft %} checked{% endif %} />
273
- <label for="editor-draft">Draft</label>
274
- </p>
275
-
276
- <label for="editor-visibility">Visibility</label>
277
- <select id="editor-visibility" name="visibility" aria-describedby="editor-visibility-hint">
278
- {% if form.visibility.unrecognized is defined %}
279
- <option value="{{ form.visibility.unrecognized }}" selected>{{ form.visibility.unrecognized }} (not recognized)</option>
280
- {% endif %}
281
- <option value="public"{% if form.visibility == "public" %} selected{% endif %}>Public</option>
282
- <option value="unlisted"{% if form.visibility == "unlisted" %} selected{% endif %}>Unlisted</option>
283
- </select>
284
- <p class="admin-hint" id="editor-visibility-hint">
285
- {% if form.visibility.unrecognized is defined %}
286
- The front matter says <code>visibility: {{ form.visibility.unrecognized }}</code>, which this
287
- site does not recognize. The {{ kind.singular }} is hidden until you choose Public or Unlisted:
288
- its page answers 404 and it is not listed or sent to your followers.
289
- {% endif %}
290
- Unlisted writes <code>visibility: unlisted</code> into the front matter. The {{ kind.singular }}
291
- keeps its page and still goes to your followers, but is left off the home page, archives,
292
- feeds, the sitemap, search and llms.txt, and asks search engines not to index it.
293
- </p>
389
+ {% call group('Summary and language', open.summary) %}
390
+ <label for="editor-description">Description</label>
391
+ <input id="editor-description" name="description" type="text" value="{{ form.description }}"{{ invalid('editor-description') }} />
392
+ {{ problem('editor-description') }}
294
393
 
295
- {% if kind.type == "post" %}
296
- {# TASK-207: shown at the top of the author's fediverse profile, from
297
- the featured collection their actor links. #}
298
- <p class="admin-check">
299
- <input id="editor-pinned" name="pinned" type="checkbox" value="1"{% if form.pinned %} checked{% endif %} />
300
- <label for="editor-pinned">Pinned</label>
301
- </p>
302
- <p class="admin-hint">
303
- Writes <code>pinned:</code> and the moment you pinned it into the front matter. Pinned posts
304
- show first on your fediverse profile, newest pin first, up to five.
305
- </p>
306
- {% endif %}
394
+ {#- The `lang` front matter (TASK-154): a post or page written in another
395
+ language than the site's says so, for screen readers, translation
396
+ and fediverse language filters. -#}
397
+ <label for="editor-lang">Language</label>
398
+ <input id="editor-lang" name="lang" type="text" value="{{ form.lang }}" autocomplete="off" spellcheck="false"{{ invalid('editor-lang', 'editor-lang-hint') }} />
399
+ {{ problem('editor-lang') }}
400
+ <p class="admin-hint" id="editor-lang-hint">A language tag, such as fr or pt-BR. Leave it empty for the site’s language, {{ siteLanguage }}.</p>
401
+ {% endcall %}
307
402
 
308
403
  {% if kind.type == "post" and syndicationTargets.length %}
309
404
  {# TASK-155: the targets content/_data/syndicationTargets.json declares.
310
405
  A target whose tag the post carries is selected without a tick. #}
311
- <fieldset class="admin-editor-syndication">
312
- <legend>Syndicate to</legend>
406
+ {% call group('Syndicate to', open.syndication) %}
313
407
  {% for target in syndicationTargets %}
314
408
  <p class="admin-check">
315
409
  <input id="editor-{{ target.field }}" name="{{ target.field }}" type="checkbox" value="1"{% if target.checked %} checked{% endif %} />
@@ -321,47 +415,49 @@
321
415
  sends it a webmention when it is published or updated. A post tagged with a target’s tag is
322
416
  sent there without a tick.
323
417
  </p>
324
- </fieldset>
325
- {% endif %}
326
-
327
- {# What this document says about comments, if anything. Three answers
328
- rather than two: the site's own rules, always open, always closed. #}
329
- <label for="editor-comments">Comments</label>
330
- <select id="editor-comments" name="comments">
331
- <option value="{{ commentSettings.site }}"{% if form.comments == commentSettings.site %} selected{% endif %}>Follow the site settings</option>
332
- <option value="{{ commentSettings.open }}"{% if form.comments == commentSettings.open %} selected{% endif %}>Open</option>
333
- <option value="{{ commentSettings.closed }}"{% if form.comments == commentSettings.closed %} selected{% endif %}>Closed</option>
334
- </select>
335
- <p class="admin-hint">
336
- Writes <code>comments: true</code> or <code>comments: false</code> into the front matter,
337
- which beats the site's switch and its closing window in both directions. Fediverse replies
338
- are shown either way.
339
- </p>
340
-
341
- {% if kind.excludable %}
342
- {# doc-2's eleventyExcludeFromCollections: still published and still at
343
- its URL, but out of every collection a template lists from. #}
344
- <p class="admin-check">
345
- <input id="editor-exclude" name="exclude" type="checkbox" value="1"{% if form.exclude %} checked{% endif %} />
346
- <label for="editor-exclude">Hide from collections</label>
347
- </p>
348
- <p class="admin-hint">Keeps the {{ kind.singular }} out of listings that gather content, without unpublishing it.</p>
418
+ {% endcall %}
349
419
  {% endif %}
350
420
 
351
- {% if kind.contactable %}
352
- {# The page offers a contact form under its content (TASK-56). Where a
353
- message goes is the contact address on the Settings screen; it is
354
- never on this page or in its HTML. #}
355
- <p class="admin-check">
356
- <input id="editor-contact" name="contact" type="checkbox" value="1"{% if form.contact %} checked{% endif %} />
357
- <label for="editor-contact">Contact form</label>
358
- </p>
421
+ {% call group('Display and discussion', open.display) %}
422
+ {# What this document says about comments, if anything. Three answers
423
+ rather than two: the site's own rules, always open, always closed. #}
424
+ <label for="editor-comments">Comments</label>
425
+ <select id="editor-comments" name="comments">
426
+ <option value="{{ commentSettings.site }}"{% if form.comments == commentSettings.site %} selected{% endif %}>Follow the site settings</option>
427
+ <option value="{{ commentSettings.open }}"{% if form.comments == commentSettings.open %} selected{% endif %}>Open</option>
428
+ <option value="{{ commentSettings.closed }}"{% if form.comments == commentSettings.closed %} selected{% endif %}>Closed</option>
429
+ </select>
359
430
  <p class="admin-hint">
360
- Writes <code>contact: true</code> into the front matter and puts a name, email, subject
361
- and message form under this {{ kind.singular }}. Messages arrive on the Messages screen
362
- and are emailed to the contact address on Settings.
431
+ Writes <code>comments: true</code> or <code>comments: false</code> into the front matter,
432
+ which beats the site's switch and its closing window in both directions. Fediverse replies
433
+ are shown either way.
363
434
  </p>
364
- {% endif %}
435
+
436
+ {% if kind.excludable %}
437
+ {# doc-2's eleventyExcludeFromCollections: still published and still at
438
+ its URL, but out of every collection a template lists from. #}
439
+ <p class="admin-check">
440
+ <input id="editor-exclude" name="exclude" type="checkbox" value="1"{% if form.exclude %} checked{% endif %} />
441
+ <label for="editor-exclude">Hide from collections</label>
442
+ </p>
443
+ <p class="admin-hint">Keeps the {{ kind.singular }} out of listings that gather content, without unpublishing it.</p>
444
+ {% endif %}
445
+
446
+ {% if kind.contactable %}
447
+ {# The page offers a contact form under its content (TASK-56). Where a
448
+ message goes is the contact address on the Settings screen; it is
449
+ never on this page or in its HTML. #}
450
+ <p class="admin-check">
451
+ <input id="editor-contact" name="contact" type="checkbox" value="1"{% if form.contact %} checked{% endif %} />
452
+ <label for="editor-contact">Contact form</label>
453
+ </p>
454
+ <p class="admin-hint">
455
+ Writes <code>contact: true</code> into the front matter and puts a name, email, subject
456
+ and message form under this {{ kind.singular }}. Messages arrive on the Messages screen
457
+ and are emailed to the contact address on Settings.
458
+ </p>
459
+ {% endif %}
460
+ {% endcall %}
365
461
 
366
462
  <p class="admin-editor-buttons">
367
463
  {% for button in actions %}
@@ -395,4 +491,7 @@
395
491
  {# The slug and permalink auto-fill, in a file of its own so the policy never
396
492
  has to allow an inline script. #}
397
493
  <script defer src="{{ assetPrefix }}slug.js"></script>
494
+ {% if kind.type == 'post' %}
495
+ <script defer src="{{ assetPrefix }}location.js"></script>
496
+ {% endif %}
398
497
  {% endblock %}