@geekity/cms 0.28.0 → 0.29.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 (187) hide show
  1. package/README.md +303 -53
  2. package/admin/pages/comments/all.njk +3 -1
  3. package/admin/pages/federation/followers.njk +8 -2
  4. package/dist/admin/comments.d.ts +2 -0
  5. package/dist/admin/comments.d.ts.map +1 -1
  6. package/dist/admin/comments.js +19 -49
  7. package/dist/admin/comments.js.map +1 -1
  8. package/dist/admin/documents.d.ts +5 -0
  9. package/dist/admin/documents.d.ts.map +1 -1
  10. package/dist/admin/documents.js +45 -17
  11. package/dist/admin/documents.js.map +1 -1
  12. package/dist/admin/federation.d.ts +21 -1
  13. package/dist/admin/federation.d.ts.map +1 -1
  14. package/dist/admin/federation.js +17 -7
  15. package/dist/admin/federation.js.map +1 -1
  16. package/dist/admin/store.d.ts +64 -10
  17. package/dist/admin/store.d.ts.map +1 -1
  18. package/dist/admin/store.js +166 -17
  19. package/dist/admin/store.js.map +1 -1
  20. package/dist/cli.js +2 -2
  21. package/dist/cli.js.map +1 -1
  22. package/dist/comments/form.d.ts +3 -1
  23. package/dist/comments/form.d.ts.map +1 -1
  24. package/dist/comments/form.js +2 -1
  25. package/dist/comments/form.js.map +1 -1
  26. package/dist/comments/index.d.ts +2 -2
  27. package/dist/comments/index.d.ts.map +1 -1
  28. package/dist/comments/index.js +1 -1
  29. package/dist/comments/index.js.map +1 -1
  30. package/dist/comments/records.d.ts +24 -20
  31. package/dist/comments/records.d.ts.map +1 -1
  32. package/dist/comments/records.js +74 -23
  33. package/dist/comments/records.js.map +1 -1
  34. package/dist/comments/reply-notices.d.ts +42 -0
  35. package/dist/comments/reply-notices.d.ts.map +1 -0
  36. package/dist/comments/reply-notices.js +76 -0
  37. package/dist/comments/reply-notices.js.map +1 -0
  38. package/dist/comments/reply-post.d.ts +49 -0
  39. package/dist/comments/reply-post.d.ts.map +1 -0
  40. package/dist/comments/reply-post.js +66 -0
  41. package/dist/comments/reply-post.js.map +1 -0
  42. package/dist/comments/routes.d.ts +6 -1
  43. package/dist/comments/routes.d.ts.map +1 -1
  44. package/dist/comments/routes.js +49 -21
  45. package/dist/comments/routes.js.map +1 -1
  46. package/dist/comments/submission.d.ts +8 -25
  47. package/dist/comments/submission.d.ts.map +1 -1
  48. package/dist/comments/submission.js +19 -38
  49. package/dist/comments/submission.js.map +1 -1
  50. package/dist/comments/viewer.d.ts.map +1 -1
  51. package/dist/comments/viewer.js +1 -0
  52. package/dist/comments/viewer.js.map +1 -1
  53. package/dist/content/citation.d.ts +1 -0
  54. package/dist/content/citation.d.ts.map +1 -1
  55. package/dist/content/citation.js +2 -0
  56. package/dist/content/citation.js.map +1 -1
  57. package/dist/content/original.d.ts +6 -1
  58. package/dist/content/original.d.ts.map +1 -1
  59. package/dist/content/original.js +7 -4
  60. package/dist/content/original.js.map +1 -1
  61. package/dist/content/store.d.ts +43 -11
  62. package/dist/content/store.d.ts.map +1 -1
  63. package/dist/content/store.js +170 -10
  64. package/dist/content/store.js.map +1 -1
  65. package/dist/content/sync.d.ts +8 -0
  66. package/dist/content/sync.d.ts.map +1 -1
  67. package/dist/content/sync.js +164 -71
  68. package/dist/content/sync.js.map +1 -1
  69. package/dist/federation/article.d.ts.map +1 -1
  70. package/dist/federation/article.js +2 -1
  71. package/dist/federation/article.js.map +1 -1
  72. package/dist/federation/backfill.d.ts +114 -0
  73. package/dist/federation/backfill.d.ts.map +1 -0
  74. package/dist/federation/backfill.js +312 -0
  75. package/dist/federation/backfill.js.map +1 -0
  76. package/dist/federation/inbox.js +1 -1
  77. package/dist/federation/inbox.js.map +1 -1
  78. package/dist/federation/records.d.ts +13 -0
  79. package/dist/federation/records.d.ts.map +1 -1
  80. package/dist/federation/records.js +49 -10
  81. package/dist/federation/records.js.map +1 -1
  82. package/dist/federation/replies.d.ts +6 -0
  83. package/dist/federation/replies.d.ts.map +1 -1
  84. package/dist/federation/replies.js +18 -1
  85. package/dist/federation/replies.js.map +1 -1
  86. package/dist/index.d.ts +11 -3
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +79 -10
  89. package/dist/index.js.map +1 -1
  90. package/dist/init.d.ts +1 -1
  91. package/dist/init.d.ts.map +1 -1
  92. package/dist/init.js +1 -7
  93. package/dist/init.js.map +1 -1
  94. package/dist/web/context.d.ts +18 -3
  95. package/dist/web/context.d.ts.map +1 -1
  96. package/dist/web/context.js +17 -4
  97. package/dist/web/context.js.map +1 -1
  98. package/dist/web/conversation.d.ts +155 -16
  99. package/dist/web/conversation.d.ts.map +1 -1
  100. package/dist/web/conversation.js +602 -135
  101. package/dist/web/conversation.js.map +1 -1
  102. package/dist/web/documents.d.ts +0 -22
  103. package/dist/web/documents.d.ts.map +1 -1
  104. package/dist/web/documents.js +1 -34
  105. package/dist/web/documents.js.map +1 -1
  106. package/dist/web/feed-item.d.ts +12 -8
  107. package/dist/web/feed-item.d.ts.map +1 -1
  108. package/dist/web/feed-item.js +10 -10
  109. package/dist/web/feed-item.js.map +1 -1
  110. package/dist/web/feed-rss.d.ts.map +1 -1
  111. package/dist/web/feed-rss.js +34 -10
  112. package/dist/web/feed-rss.js.map +1 -1
  113. package/dist/web/feed-source.d.ts +30 -1
  114. package/dist/web/feed-source.d.ts.map +1 -1
  115. package/dist/web/feed-source.js +25 -0
  116. package/dist/web/feed-source.js.map +1 -1
  117. package/dist/web/feeds.d.ts +1 -1
  118. package/dist/web/feeds.d.ts.map +1 -1
  119. package/dist/web/feeds.js +7 -2
  120. package/dist/web/feeds.js.map +1 -1
  121. package/dist/web/guids.d.ts +59 -0
  122. package/dist/web/guids.d.ts.map +1 -0
  123. package/dist/web/guids.js +79 -0
  124. package/dist/web/guids.js.map +1 -0
  125. package/dist/web/index.d.ts +5 -6
  126. package/dist/web/index.d.ts.map +1 -1
  127. package/dist/web/index.js +3 -3
  128. package/dist/web/index.js.map +1 -1
  129. package/dist/web/redirects.d.ts +12 -0
  130. package/dist/web/redirects.d.ts.map +1 -1
  131. package/dist/web/redirects.js +16 -4
  132. package/dist/web/redirects.js.map +1 -1
  133. package/dist/web/render.d.ts +19 -9
  134. package/dist/web/render.d.ts.map +1 -1
  135. package/dist/web/render.js +44 -42
  136. package/dist/web/render.js.map +1 -1
  137. package/dist/web/routes.d.ts +2 -2
  138. package/dist/web/routes.d.ts.map +1 -1
  139. package/dist/web/routes.js +94 -31
  140. package/dist/web/routes.js.map +1 -1
  141. package/dist/webmention/index.d.ts +1 -1
  142. package/dist/webmention/index.d.ts.map +1 -1
  143. package/dist/webmention/index.js +1 -1
  144. package/dist/webmention/index.js.map +1 -1
  145. package/dist/webmention/links.d.ts +55 -0
  146. package/dist/webmention/links.d.ts.map +1 -1
  147. package/dist/webmention/links.js +162 -0
  148. package/dist/webmention/links.js.map +1 -1
  149. package/dist/webmention/microformats.d.ts +15 -0
  150. package/dist/webmention/microformats.d.ts.map +1 -1
  151. package/dist/webmention/microformats.js +63 -1
  152. package/dist/webmention/microformats.js.map +1 -1
  153. package/dist/webmention/pingback.js +2 -1
  154. package/dist/webmention/pingback.js.map +1 -1
  155. package/dist/webmention/receive.d.ts +5 -0
  156. package/dist/webmention/receive.d.ts.map +1 -1
  157. package/dist/webmention/receive.js +99 -3
  158. package/dist/webmention/receive.js.map +1 -1
  159. package/dist/webmention/reply-context.d.ts +15 -0
  160. package/dist/webmention/reply-context.d.ts.map +1 -1
  161. package/dist/webmention/reply-context.js +29 -0
  162. package/dist/webmention/reply-context.js.map +1 -1
  163. package/dist/webmention/reply-contexts.d.ts +6 -0
  164. package/dist/webmention/reply-contexts.d.ts.map +1 -1
  165. package/dist/webmention/reply-contexts.js +16 -10
  166. package/dist/webmention/reply-contexts.js.map +1 -1
  167. package/dist/webmention/routes.d.ts.map +1 -1
  168. package/dist/webmention/routes.js +2 -1
  169. package/dist/webmention/routes.js.map +1 -1
  170. package/dist/webmention/service.d.ts +30 -1
  171. package/dist/webmention/service.d.ts.map +1 -1
  172. package/dist/webmention/service.js +172 -32
  173. package/dist/webmention/service.js.map +1 -1
  174. package/package.json +1 -2
  175. package/themes/default/README.md +242 -78
  176. package/themes/default/layouts/base.njk +8 -0
  177. package/themes/default/layouts/comment.njk +90 -0
  178. package/themes/default/layouts/front-page.njk +9 -47
  179. package/themes/default/layouts/page.njk +2 -0
  180. package/themes/default/layouts/post.njk +14 -2
  181. package/themes/default/partials/backlinks.njk +19 -0
  182. package/themes/default/partials/cited-page.njk +3 -1
  183. package/themes/default/partials/comment-form.njk +10 -0
  184. package/themes/default/partials/comment.njk +79 -0
  185. package/themes/default/partials/conversation.njk +14 -59
  186. package/themes/default/src/style.css +42 -0
  187. package/themes/default/static/style.css +67 -0
@@ -20,6 +20,7 @@ themes/default/
20
20
  410.njk what was at this URL was deleted
21
21
  500.njk the request failed on the server
22
22
  503.njk the site is in maintenance mode
23
+ comment.njk a native comment on a page of its own
23
24
  partials/
24
25
  post-list.njk the h-feed a listing is made of
25
26
  pagination.njk previous/next pager
@@ -34,9 +35,11 @@ themes/default/
34
35
  menu.njk one named menu, as a nav of links
35
36
  feeds.njk macros for the feed links in <head>
36
37
  conversation.njk the replies, likes and boosts under a post or an open page
38
+ comment.njk one reply and its answers, as the thread and a comment page print it
37
39
  comment-form.njk the form under a post or page that is taking comments
38
40
  contact-form.njk the form on a page whose front matter says contact: true
39
41
  archive.njk every post by month, on a page that says archive: true
42
+ backlinks.njk the site's own posts and pages that link to this one
40
43
  search-form.njk the search box, on the search page
41
44
  head-end.njk empty: what a site adds to the end of every <head>
42
45
  body-end.njk empty: what a site adds to the end of every <body>
@@ -480,13 +483,17 @@ and its line is a `p.page-meta` rather than a `p.entry-meta`.
480
483
  **A post first published elsewhere says so.** A post whose front matter names
481
484
  its original with `canonical_href`, an absolute http or https URL such as a
482
485
  Substack essay's, has it on the context as `original` (TASK-293). The line
483
- then reads **Originally published at** and links the original's host as a
484
- second `u-url` of the h-entry, after the permalink's. It is no
486
+ then reads **Originally published at** and links the original as a second
487
+ `u-url` of the h-entry, after the permalink's. The link says the publication's
488
+ name when the front matter gives one in `canonical_name`, such as `Smarter
489
+ Engineers` for a guest post on someone else's Substack (TASK-316), and the
490
+ original's host otherwise. It is no
485
491
  `u-syndication`, because the original is not a copy of this post, and
486
492
  original-post-discovery reads a copy's off-site `u-url` as its original. The
487
493
  same URL is the page's `rel="canonical"` and its `og:url`, so search engines
488
494
  credit the original. A value that is not an absolute http or https URL is
489
- ignored, and `geekity sync` names the file.
495
+ ignored, and `geekity sync` names the file. So is a `canonical_name` with no
496
+ valid `canonical_href` beside it.
490
497
 
491
498
  **Every post's page has one `h1`.** A post with a name of its own is headed by
492
499
  it, as above. So is a post with a title and no words, such as a titled like,
@@ -571,7 +578,8 @@ a `p-author h-card`, when the target was read when the post was saved
571
578
  provider's known oEmbed endpoint, asked before its page, which is read only
572
579
  when the endpoint names nothing (TASK-251, TASK-253). Any other target's oEmbed
573
580
  title comes from the endpoint its page links. A title that is only a site suffix such as "- YouTube" is
574
- no title. With only an author it says "a post", and with neither "a page on"
581
+ no title. A comment on this site (`replyContext.comment`, TASK-326) says "a
582
+ comment". With only an author it says "a post", and with neither "a page on"
575
583
  the target's host, never the bare URL, which can run to hundreds of
576
584
  characters: "Bookmarked a page on example.com" (TASK-255). Its kicker
577
585
  and its hidden `h1` say Repost, Like or Bookmark. It is placed as the reply
@@ -706,13 +714,50 @@ its front matter names no `image`.
706
714
  </figure>
707
715
  ```
708
716
 
709
- After the entry a post prints `nav.blog-post-nav`: the `previous` and `next`
710
- posts as two cards, `rel="prev"` and `rel="next"`, each opening on a
711
- `span.blog-post-nav-label`, and nothing at all at the ends of the archive; then
712
- the conversation and the comment form. A page prints the conversation and the
713
- comment form too when it takes comments, and the contact form when its front
714
- matter asked for one. A page has no neighbours, no tags and no
715
- syndication links, because none of those are things a page has.
717
+ After the entry a post prints its backlinks, then `nav.blog-post-nav`: the
718
+ `previous` and `next` posts as two cards, `rel="prev"` and `rel="next"`, each
719
+ opening on a `span.blog-post-nav-label`, and nothing at all at the ends of the
720
+ archive; then the conversation and the comment form. A page prints its
721
+ backlinks too, then the conversation and the comment form when it takes
722
+ comments, and the contact form when its front matter asked for one. A page has
723
+ no neighbours, no tags and no syndication links, because none of those are
724
+ things a page has.
725
+
726
+ ### Backlinks
727
+
728
+ A link from one of the site's posts or pages to another sends no webmention.
729
+ The CMS records it instead, and the linked document's context carries
730
+ `backlinks`: the listed posts and pages whose body links to it, newest first,
731
+ each as `{ title, url, date }`. `title` is the linking document's title, or a
732
+ note's first words. `date` is absent for a page that has none. A link counts
733
+ whether it is absolute or relative, with or without its trailing slash, a query
734
+ or a fragment, and through a `redirect_from` of the target. A link to `/`
735
+ counts for the page the Reading setting makes the homepage. A link to a URL the
736
+ site answers with a redirect counts for the document the redirect leads to,
737
+ whether the redirect is a `redirect_from` or an entry in `_data/redirects.json`
738
+ or `_data/redirects/*.json`, `/?p=123` included. A link with a query names the
739
+ query's redirect only when the query matches the redirect's source exactly, as
740
+ a request does, so `/?p=123&ref=feed` is a link to `/`. A document that links
741
+ to itself is not its own backlink. Drafts, future-dated, unlisted,
742
+ private and trashed documents never appear. `backlinks` is always a list, and
743
+ it is empty when nothing links to the document.
744
+
745
+ `partials/backlinks.njk` prints a `section.backlinks` under the entry, apart
746
+ from the conversation: an `h2` that says "Linked from", then an `ol` of links,
747
+ each with its date in a `<time>`. It prints nothing for an empty list, so
748
+ `layouts/post.njk` and `layouts/page.njk` include it with no condition. The
749
+ front page has `backlinks` on its context too, but `layouts/front-page.njk`
750
+ does not print them, because most of a site links home. A site theme that wants
751
+ them there adds the same include to its own `layouts/front-page.njk`:
752
+
753
+ ```njk
754
+ {% include "partials/backlinks.njk" %}
755
+ ```
756
+
757
+ A site theme moves the list by including the partial somewhere else in its own
758
+ layout, and leaves it out by overriding a layout without the include. To change
759
+ the markup, add a `partials/backlinks.njk` to the site theme. That file replaces
760
+ this one, as any other template is replaced.
716
761
 
717
762
  ### The bio
718
763
 
@@ -741,16 +786,19 @@ claims: the name is linked `rel="author"` alone and `me` comes off every
741
786
  link.
742
787
 
743
788
  **The homepage** speaks for the site's author only on a site whose Site author
744
- setting names a user, which `soloAuthor` on the context says. Then both
745
- `layouts/home.njk` at `/` and `layouts/front-page.njk` end with that person's
746
- card, with its `rel="me"` claims, and their author archive's link home carries
747
- `rel="me"` back, so the homepage and the archive name each other. Those two
748
- layouts also set `bioHome` to the homepage's absolute URL, which puts a
749
- `data.u-url.u-uid` holding it first in the card, so a parser running the
789
+ setting names a user, which `soloAuthor` on the context says. Then
790
+ `layouts/base.njk` puts that person's `rel="me"` claims in the head of `/` as
791
+ `<link rel="me">` elements, one for their author archive and one for each
792
+ profile link, whichever layout draws the page (decision-45). Their author
793
+ archive's link home carries `rel="me"` back, so the homepage and the archive
794
+ name each other. `layouts/home.njk` at `/` also ends with that person's card,
795
+ with the same claims, and sets `bioHome` to the homepage's absolute URL, which
796
+ puts a `data.u-url.u-uid` holding it first in the card, so a parser running the
750
797
  [representative h-card algorithm](https://microformats.org/wiki/representative-h-card-parsing)
751
798
  on the homepage finds this card as the site's (TASK-193). The archive stays a
752
- second `u-url`. No other page sets `bioHome`. On a site
753
- with several authors neither homepage prints a card. Each of
799
+ second `u-url`. No other page sets `bioHome`. A static front page prints no
800
+ card: see [The front page](#the-front-page) for how a site puts one back. On a
801
+ site with several authors neither homepage prints a card or makes a claim. Each of
754
802
  those is printed only when the profile says it, so a profile holding a name
755
803
  alone prints a name alone. A name this site has no account for is printed
756
804
  unlinked, because the file still said somebody wrote this.
@@ -786,8 +834,8 @@ byline under the title rather than a bio in the footer, so it writes one.
786
834
  ### A listing
787
835
 
788
836
  `partials/post-list.njk` is the feed every listing is made of (the home page,
789
- the posts page, a tag, category or author archive, and the front page's recent
790
- posts) and it is a microformats2 `h-feed` of `h-entry` items, each opening on
837
+ the posts page, a tag, category or author archive, and a front page that
838
+ lists `newestPosts`) and it is a microformats2 `h-feed` of `h-entry` items, each opening on
791
839
  its kicker:
792
840
 
793
841
  ```html
@@ -934,25 +982,69 @@ name a homepage (`homepage` in `content/_data/site.json`). It is that page read
934
982
  somewhere else: the same document, the same context, with `page.url` saying `/`
935
983
  rather than the permalink that redirects there.
936
984
 
937
- It draws the page's own words and then what the site has been writing:
938
-
939
- 1. `div.page-body.e-content`, the rendered body. No title — `layouts/base.njk`
940
- heads the root path with the site title, and a second `h1` under it would be
941
- one heading too many — and no Published line, because a front page is read
942
- as the site rather than as a page somebody wrote on a Tuesday.
943
- 2. `<h2>Recent Posts</h2>` over `partials/post-list.njk` with `feedHeading` set
944
- to 3, so the entries sit under that heading rather than beside it.
945
- 3. `p.front-links`, a line of links to where the writing is. The posts page is
946
- linked by its own title when the site names one, and the search always is
947
- (TASK-22).
948
- 4. The bio, under a rule, exactly as an entry ends: whoever the site's author
949
- setting names, with their note and their own links.
950
-
951
- Two context keys are the front page's alone. `recentPosts` is the entries to
952
- list, in the same shape a listing's are: the posts of the current month when
953
- there are at least five of them, and the five newest otherwise. `postsPage` is
954
- `{ title, url }` for the page carrying the listing, and is **absent** when the
955
- site names none — a link to a listing that has no URL is a link to nothing.
985
+ It draws the page's own title and words, and nothing else (decision-44):
986
+
987
+ 1. `h2.section-title`, the page's title. It is an `h2` because
988
+ `layouts/base.njk` heads the root path with the site title as the `h1`.
989
+ 2. `div.page-body.e-content`, the rendered body. There is no Published line,
990
+ because a front page is read as the site rather than as a page somebody
991
+ wrote on a Tuesday.
992
+ 3. The contact form, when the page's front matter says `contact: true`.
993
+
994
+ It lists no posts, prints no line of links and prints no bio. A site that wants
995
+ any of them on its front page overrides `layouts/front-page.njk` in its own
996
+ theme. Two context keys are the front page's alone, for that override:
997
+
998
+ - `newestPosts(count)` is a function. It returns the newest `count` published
999
+ posts as entries, in the same shape a listing's `posts` are, so
1000
+ `partials/post-list.njk` prints them. Drafts, future-dated, unlisted and
1001
+ trashed posts are left out. The theme chooses the count. The query runs only
1002
+ when a template calls it, so a front page that lists nothing costs nothing.
1003
+ A count that is not a whole number of at least 1 is an error.
1004
+ - `postsPage` is `{ title, url }` for the page carrying the listing, and is
1005
+ **absent** when the site names none, because a link to a listing that has no
1006
+ URL is a link to nothing.
1007
+
1008
+ `newestPosts` replaces `recentPosts`, which held this month's posts when there
1009
+ were at least five and the five newest otherwise. An override that read
1010
+ `recentPosts` calls `newestPosts(5)` instead. This override draws the page's
1011
+ words, the five newest posts, a line of links and the site author's card as
1012
+ the site's representative h-card:
1013
+
1014
+ ```njk
1015
+ {% extends "layouts/base.njk" %}
1016
+
1017
+ {% block content %}
1018
+ <div class="page-body e-content">
1019
+ {{ content | safe }}
1020
+ </div>
1021
+
1022
+ <h2 class="section-title">Recent Posts</h2>
1023
+ {% set posts = newestPosts(5) %}
1024
+ {% set feedHeading = 3 %}
1025
+ {% include "partials/post-list.njk" %}
1026
+
1027
+ <p class="front-links">
1028
+ {% if postsPage %}
1029
+ <a href="{{ postsPage.url | url }}">{{ postsPage.title }}<span aria-hidden="true"> &rarr;</span></a>
1030
+ {% endif %}
1031
+ <a href="{{ "/search/" | url }}">Search<span aria-hidden="true"> &rarr;</span></a>
1032
+ </p>
1033
+
1034
+ {% if soloAuthor %}
1035
+ {% set bioAuthor = soloAuthor %}
1036
+ {% set bioHome = "/" | absoluteUrl %}
1037
+ <hr>
1038
+ <footer>
1039
+ {% include "partials/bio.njk" %}
1040
+ </footer>
1041
+ {% endif %}
1042
+ {% endblock %}
1043
+ ```
1044
+
1045
+ The stylesheet still styles `h2.section-title` and `p.front-links`, so an
1046
+ override can use both. Whatever the override prints, the solo author's
1047
+ `rel="me"` claims stay in the head of `/` (decision-45).
956
1048
 
957
1049
  ### An archive page
958
1050
 
@@ -1478,12 +1570,13 @@ A document — one post, one page, or one entry of a listing — adds:
1478
1570
  | `event` | An event's `{ start, end, location }`: `Date`s, and `{ kind: "place", name }` or `{ kind: "virtual", url }`. |
1479
1571
  | `syndicateTo` | On a post's page, the syndication targets it selects, each `{ id, name, url }`. Empty with none. |
1480
1572
  | `syndication` | Its copies elsewhere, each `{ url, label }`: front matter `syndication`, and on its page the copies targets made. |
1481
- | `original` | Where it was first published, as `{ url, label }`: front matter `canonical_href`. Absent unless http or https. |
1573
+ | `original` | Where it was first published, `{ url, label }`: `canonical_href`, named by `canonical_name` or its host. |
1482
1574
  | `author` | Who wrote it, as a profile rather than a string. See [Bylines and author archives](#bylines-and-author-archives). |
1483
1575
  | `activityStreams` | The post's ActivityPub object id, absolute. Only on a rendered published post. |
1484
1576
  | `previous` | The published post before this one by date, as `{ title, url }`. Absent on the oldest post. |
1485
1577
  | `next` | The published post after it. Absent on the newest post, and on a page. |
1486
- | `recentPosts` | The newest posts, as entries, on the front page only: this month's when it holds five, else five. |
1578
+ | `backlinks` | The site's listed posts and pages that link to it, newest first, each `{ title, url, date }`. Empty with none. |
1579
+ | `newestPosts` | A function, front page only: `newestPosts(5)` is the five newest published posts, as entries. |
1487
1580
  | `postsPage` | The page carrying the listing, as `{ title, url }`, on the front page only. Absent when the site names none. |
1488
1581
  | `archiveMonths` | Every published post as `{ month, posts }`, newest month first. Only on a page that says `archive: true`. |
1489
1582
  | `webmention` | Where a webmention about this page is sent. Only on a rendered document, and only while the site takes them. |
@@ -1725,9 +1818,9 @@ as well as the site's — on an author archive that root is `author.url`.
1725
1818
 
1726
1819
  `layouts/base.njk` fills the `alternates` block with `feedLinks` over `/` and
1727
1820
  then the site's comments feed, so every page that extends it advertises all
1728
- four. On a published post it adds that post's own comments feed, from
1729
- `commentsFeed` on the context, and a link pointing at the post's ActivityPub
1730
- object:
1821
+ four. On a published post, and on a page that takes comments, it adds that
1822
+ document's own comments feed, from `commentsFeed` on the context. On a
1823
+ published post it also adds a link pointing at the post's ActivityPub object:
1731
1824
 
1732
1825
  ```html
1733
1826
  <link
@@ -1740,9 +1833,10 @@ object:
1740
1833
  A post's object id is its permalink, so that link usually points at the page it
1741
1834
  is on: it says the URL answers ActivityStreams as well as HTML. A post migrated
1742
1835
  from elsewhere keeps the id its file names, and the link points there instead.
1743
- It comes from `activityStreams`, which — like `commentsFeed` — is only on the
1744
- context of a rendered published post, so a layout that overrides the block and
1745
- does not call `super()` has to emit both itself. They are on the context rather
1836
+ It comes from `activityStreams`, which is only on the context of a rendered
1837
+ published post, as `commentsFeed` is only on a document that has a comments
1838
+ feed, so a layout that overrides the block and does not call `super()` has to
1839
+ emit both itself. They are on the context rather
1746
1840
  than in `post.njk` so that a site which overrides that layout, as the demo
1747
1841
  does, keeps them.
1748
1842
 
@@ -1829,15 +1923,27 @@ section — a `div.reactions-section` of the likes, the boosts and the mentions
1829
1923
  as facepiles grouped by kind, and then the thread as
1830
1924
  `div#comments.comments-area` — and a site replaces it with a
1831
1925
  `partials/conversation.njk` of its own in the theme it wears, exactly as it
1832
- replaces any other template. It defines three macros, `face(item, icon, href)`,
1833
- `group(items, label, kind, icon, source)` and `comment(reply)`, and a layout
1834
- that wants to place the pieces itself can import them:
1926
+ replaces any other template. It defines two macros, `face(item, icon, href)`
1927
+ and `group(items, label, kind, icon, source)`, and a layout that wants to place
1928
+ the pieces itself can import them:
1835
1929
 
1836
1930
  ```njk
1837
- {% import "partials/conversation.njk" as thread with context %}
1838
- {{ thread.group(conversation.likes, "Likes", "p-like", "❤️") }}
1931
+ {% import "partials/conversation.njk" as reactions with context %}
1932
+ {{ reactions.group(conversation.likes, "Likes", "p-like", "❤️") }}
1839
1933
  ```
1840
1934
 
1935
+ One reply and everything under it is `comment(reply, classes)` in
1936
+ `partials/comment.njk`, which the thread and a comment's own page both import.
1937
+ `classes` is the reply's microformat: `h-entry`, the default, under a post, and
1938
+ `p-comment h-cite` on a comment's page, where the comment is the one entry.
1939
+
1940
+ A reply post prints its replies inside its own `h-entry`, as `p-comment
1941
+ h-cite`s, so the page it answers finds them there when it is sent a salmention
1942
+ (TASK-320). `layouts/post.njk` includes `partials/conversation.njk` twice for
1943
+ one, setting `conversationPart`: `"replies"` inside the entry for the thread,
1944
+ and `"reactions"` below it for the facepiles, whose links are not the entry's.
1945
+ Left unset, the partial prints the whole section.
1946
+
1841
1947
  ### The shape
1842
1948
 
1843
1949
  The conversation is deliberately **not** spelled in ActivityPub's vocabulary.
@@ -1865,26 +1971,35 @@ Each entry — a reply, a like or a boost — is:
1865
1971
  | Key | What it holds |
1866
1972
  | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1867
1973
  | `id` | What it is called: a reply's own note id, which is what an answer to it names. |
1868
- | `source` | `"activitypub"` for a fediverse reply, `"comment"` for one left on the page, `"webmention"` for another page linking here. |
1974
+ | `source` | `"activitypub"` for a fediverse reply, `"comment"` for one left on the page, `"webmention"` for another page linking here, `"post"` for a reply post a user of the site wrote (its `url` is the post's permalink). |
1869
1975
  | `kind` | `"reply"`, `"like"`, `"boost"`, `"repost"` or `"mention"`. A `repost` comes from a webmention; a `mention` is a webmention or an approved fediverse quote. |
1870
1976
  | `author.name` | The best name available: their display name, else their handle, else their server, else their id. For a webmention, the source's `h-card` name, else its host. |
1871
1977
  | `author.handle` | `@user@host`, or `null`. Taken from the follower or actor profile the site holds, else guessed from the actor URL unless it ends in a number. `null` for a native comment. |
1872
1978
  | `author.url` | Their profile page, the website a commenter typed, or a webmention author's `u-url`. May be `null`, so guard the link. |
1873
1979
  | `author.avatar` | Their avatar, or `null`. The site knows one for a fediverse actor whose profile it holds and for a webmention whose `h-card` carried a `u-photo`. Always a same-origin `/_geekity/avatars/…` path, never the remote URL: see [Avatars](#avatars). |
1874
1980
  | `author.actorId` | Their id, which is what identifies them however they are named. `null` for a native comment. |
1875
- | `url` | Where it can be read: the remote note's `url` for a fediverse reply or quote, the source page for a webmention, and `{permalink}#comment-{id}` — this page's own anchor — for a native comment. |
1981
+ | `url` | Where it can be read: the remote note's `url` for a fediverse reply or quote, the source page for a webmention, and `/comment/{id}/`, its own page, for a native comment (see [A comment's own page](#a-comments-own-page)). |
1876
1982
  | `content` | What it says, **already sanitised**, so print it with `\| safe`. Empty for a like or a boost. |
1877
1983
  | `published` | A `Date`: when it was published, or when it arrived if it did not say. Use the `date` filter. |
1878
1984
  | `inReplyTo` | What it answers — the post's ActivityPub id, or another reply's — and `null` for a reaction. |
1879
1985
  | `status` | `"published"`. On the record for the sources that moderate. |
1880
- | `replies` | The replies to this one, oldest first, nested as deep as the site has seen. |
1986
+ | `replies` | The replies to this one, oldest first, nested as deep as the site has seen. One may be a placeholder: see below. |
1881
1987
  | `rsvp` | An RSVP: `{ value, label }`, its `p-rsvp` (`yes`, `no`, `maybe` or `interested`) and the words for it. Absent otherwise. A webmention reply that is an RSVP carries one, and the default theme prints it as a `data.p-rsvp` at the top of the comment's content. On an event, an RSVP is in `rsvps` rather than the thread, and so is a fediverse `Accept` (`yes`), `TentativeAccept` (`maybe`) or `Reject` (`no`) of the event. Each person is there once, with their latest answer. |
1882
1988
 
1883
- Three rules decide what is in the thread, and they are the CMS's rather than a
1989
+ Four rules decide what is in the thread, and they are the CMS's rather than a
1884
1990
  theme's: a reply whose author deleted it is gone, and its own answers move up to
1885
- whatever it was answering; a like is counted once per actor and disappears when
1886
- that actor undoes it; and a note answering something this post has nothing to do
1887
- with is left out.
1991
+ whatever it was answering; a comment a reader may not see (waiting for a
1992
+ moderator, spam or deleted) that has a visible reply under it is a placeholder,
1993
+ and one with none is not there at all; a like is counted once per actor and
1994
+ disappears when that actor undoes it; and a note answering something this post
1995
+ has nothing to do with is left out.
1996
+
1997
+ A placeholder in `replies` is `{ id, withheld: true, replies }`: the hidden
1998
+ comment's id and the replies under it, and nothing that names its author or
1999
+ says what it said. Test `reply.withheld` before printing an author. A pending
2000
+ or spam comment's placeholder sits where the comment would; a deleted one's
2001
+ under the post, since nothing is known of what it answered. `counts.replies`
2002
+ and the comments feeds count and carry the visible replies only.
1888
2003
 
1889
2004
  `content` is safe to print whatever produced it, and safe for different reasons.
1890
2005
  A fediverse reply is HTML somebody else's server composed, rebuilt from an
@@ -1900,7 +2015,8 @@ The packaged partial draws each entry as the source design does: an
1900
2015
  `comment-{{ reply.source }}` class, holding an `article.comment-body` whose
1901
2016
  `footer.comment-meta` is the author as a `.comment-author.vcard.p-author.h-card`
1902
2017
  (their avatar, or the first letter of their name in a `span.comment-avatar-empty`
1903
- when there is none) and the permalink as a `.comment-metadata` link around a
2018
+ when there is none, and their link marked `rel="nofollow ugc"` unless the entry
2019
+ is a reply post, whose author is a user of the site) and the permalink as a `.comment-metadata` link around a
1904
2020
  `time.dt-published`, with a `span.comment-source` saying "via webmention" or
1905
2021
  "via the fediverse" when it came that way, then the words in
1906
2022
  `div.comment-content.e-content`. Answers nest in an
@@ -1908,7 +2024,10 @@ when there is none) and the permalink as a `.comment-metadata` link around a
1908
2024
  ones written here — `source == "comment"` — when the post is still open; it
1909
2025
  carries the comment's id to the form as `?reply_to=`, so threading needs no
1910
2026
  JavaScript. A fediverse reply is answered on the server that holds it and a
1911
- webmention on the page that sent it, so neither gets one.
2027
+ webmention on the page that sent it, so neither gets one. A placeholder is an
2028
+ `li.comment.comment-placeholder` with the hidden comment's anchor, a
2029
+ `p.comment-withheld` printing `withheld()` ("This comment is no longer
2030
+ shown."), and its replies in an `ol.children`; it is no microformat of its own.
1912
2031
 
1913
2032
  Above the thread, `group()` draws one `div.reaction-group` per kind that has
1914
2033
  anything — `p-like`, `p-repost` and `p-mention`, the source theme's classes — as
@@ -1937,7 +2056,37 @@ An Eleventy build of the same content gets the same thing from the same files:
1937
2056
 
1938
2057
  and then loops over exactly the keys above. The slug is what names the post's
1939
2058
  comment file under `content/_data/comments/`; the format is documented in
1940
- backlog doc-6.
2059
+ backlog doc-6. Two things differ: a native comment's `url` is its
2060
+ `#comment-{id}` anchor, because a static build writes no comment pages, and
2061
+ the site's own reply posts (`source: "post"`) are not in the thread.
2062
+
2063
+ ### A comment's own page
2064
+
2065
+ A comment left through the form has a page of its own at `/comment/{id}/`,
2066
+ with the id percent-encoded, which is its `url` everywhere: the thread's
2067
+ permalink, the comments feeds and what another site fetches when it replies
2068
+ to the comment. The anchor `#comment-{id}` on the post stays, so an old link
2069
+ still scrolls to it. A webmention and a fediverse reply have no such page:
2070
+ their `url` is where they came from.
2071
+
2072
+ `layouts/comment.njk` draws it, with the comment as the page's one `h-entry`.
2073
+ It answers only while the post shows its conversation, and is `noindex` and on
2074
+ no list: not the sitemap, the post feeds or search. Its context:
2075
+
2076
+ | Key | What it holds |
2077
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2078
+ | `comment` | The comment, an entry of the shape above, its `replies` nested at every depth by the thread's own rules. |
2079
+ | `ancestors` | What it answers, from the top-level comment down to its parent. An ancestor a reader may not see (waiting for a moderator, spam, deleted, withdrawn) is `null`, so the chain keeps its place. |
2080
+ | `post` | The post it is on, as a listing entry is: `label`, `url`, `date`, `summary`, `author` and the rest. |
2081
+ | `threadUrl` | The comment's anchor on the post, for a link back to the whole conversation. |
2082
+
2083
+ The packaged layout prints the chain above the comment as nested
2084
+ `u-in-reply-to h-cite`s: the comment cites its parent, which cites its own,
2085
+ down to the post, which is printed first, so a parser and a reader meet the
2086
+ thread in the same order. A `null` ancestor is a cite with no author and no
2087
+ words, printing the same `withheld()` the thread's placeholder does. The replies follow inside the
2088
+ entry as `comment(reply, "p-comment h-cite")`, with no Reply links, and a
2089
+ link to `threadUrl` ends the page.
1941
2090
 
1942
2091
  ### Avatars
1943
2092
 
@@ -1985,17 +2134,17 @@ in a `div.field-row`, side by side on a wide screen, in both forms. On a site
1985
2134
  that takes webmentions a `p.comment-help` under the heading says a reader can
1986
2135
  reply from their own site instead.
1987
2136
 
1988
- | Key | What it holds |
1989
- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1990
- | `action` | Where the form posts. One fixed path; the post travels as a field. |
1991
- | `fields` | The name each field is submitted under: `post`, `name`, `email`, `url`, `body`, `inReplyTo`, `trap`, `loaded`, `notify`, `csrf`. Use these rather than typing the names. |
1992
- | `post` | The post's slug, for the hidden field. |
1993
- | `loaded` | When this form was rendered, in epoch milliseconds, for the hidden field. A submission that comes back too fast is refused. |
1994
- | `values` | What is in the fields: empty on a fresh form, what was typed on a refused one. |
1995
- | `problems` | One message per field a person has to put right — `name`, `email`, `url`, `body`. |
1996
- | `error` | A message about the submission as a whole, when there is one. |
1997
- | `notifiable` | Whether to offer `fields.notify`, the "email me when somebody replies" box. False on a site that sends no mail, where the box would promise a message nothing could send. |
1998
- | `nameLength`, `bodyLength` | The `maxlength` for the two fields that have one. |
2137
+ | Key | What it holds |
2138
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2139
+ | `action` | Where the form posts. One fixed path; the post travels as a field. |
2140
+ | `fields` | The name each field is submitted under: `post`, `name`, `email`, `url`, `body`, `inReplyTo`, `trap`, `loaded`, `notify`, `csrf`, `listed`. Use these rather than typing the names. |
2141
+ | `post` | The post's slug, for the hidden field. |
2142
+ | `loaded` | When this form was rendered, in epoch milliseconds, for the hidden field. A submission that comes back too fast is refused. |
2143
+ | `values` | What is in the fields: empty on a fresh form, what was typed on a refused one. |
2144
+ | `problems` | One message per field a person has to put right — `name`, `email`, `url`, `body`. |
2145
+ | `error` | A message about the submission as a whole, when there is one. |
2146
+ | `notifiable` | Whether to offer `fields.notify`, the "email me when somebody replies" box. False on a site that sends no mail, where the box would promise a message nothing could send. |
2147
+ | `nameLength`, `bodyLength` | The `maxlength` for the two fields that have one. |
1999
2148
 
2000
2149
  `fields.trap` is a honeypot: render it, hide it from sight and from assistive
2001
2150
  technology, and give it `tabindex="-1"` and `autocomplete="off"`. A submission
@@ -2024,10 +2173,10 @@ The contact form below keeps the same contract.
2024
2173
  Two more keys are on `commentForm` when — and only when — the request carries a
2025
2174
  valid session for this site's admin:
2026
2175
 
2027
- | Key | What it holds |
2028
- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
2029
- | `signedInAs` | Who the comment will be posted as: `name`, which is their display name or their username, and `url`, which is their author archive. |
2030
- | `csrfToken` | The value for a hidden `fields.csrf`, without which the submission is refused. |
2176
+ | Key | What it holds |
2177
+ | ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
2178
+ | `signedInAs` | Who the reply will be posted as: `name`, which is their display name or their username, and `url`, which is their author archive. |
2179
+ | `csrfToken` | The value for a hidden `fields.csrf`, without which the submission is refused. |
2031
2180
 
2032
2181
  **A replacement partial has to draw both forms**, because the site draws this
2033
2182
  one for its own author and the stranger's one for everybody else:
@@ -2038,6 +2187,10 @@ one for its own author and the stranger's one for everybody else:
2038
2187
  <p class="comment-signed-in">
2039
2188
  Commenting as <a href="{{ commentForm.signedInAs.url }}">{{ commentForm.signedInAs.name }}</a>.
2040
2189
  </p>
2190
+ <p class="comment-check">
2191
+ <input id="comment-listed" name="{{ commentForm.fields.listed }}" type="checkbox" value="1"{% if commentForm.values.listed %} checked{% endif %} />
2192
+ <label for="comment-listed">Include in posts and feeds</label>
2193
+ </p>
2041
2194
  {% else %}
2042
2195
  {# fields.loaded, fields.trap, and the name, email and website boxes #}
2043
2196
  {% endif %}
@@ -2051,6 +2204,17 @@ on the public site that acts on somebody's behalf — without the token, a page
2051
2204
  on another site could make them post under their own name. The comment box, the
2052
2205
  reply-to field and the notify box are the same in both branches.
2053
2206
 
2207
+ What the signed-in branch posts is a reply post rather than a comment (CMS
2208
+ README, "Replying while signed in"). It adds `fields.listed`, a checkbox
2209
+ labelled "Include in posts and feeds" and unchecked by default: unchecked
2210
+ publishes the reply post unlisted, checked publishes it public. The stranger's
2211
+ branch never draws it, and the CMS ignores it from a stranger.
2212
+
2213
+ A Reply link, `?reply_to={id}#respond`, goes on every entry of the thread for
2214
+ somebody signed in, and on `source == "comment"` entries only for a stranger.
2215
+ `partials/comment.njk` asks `reply.source == "comment" or
2216
+ commentForm.signedInAs`.
2217
+
2054
2218
  The account's email address is **never** on the context. The comment is stored
2055
2219
  with it, exactly as a stranger's is, and it is no more printable than theirs.
2056
2220
 
@@ -178,6 +178,14 @@
178
178
  {% if llmsTxt and atRoot %}
179
179
  <link rel="describedby" type="text/markdown" href="{{ llmsTxt | url }}">
180
180
  {% endif %}
181
+ {% if atRoot and soloAuthor %}
182
+ {% if soloAuthor.url %}
183
+ <link rel="me" href="{{ soloAuthor.url | url }}">
184
+ {% endif %}
185
+ {% for link in soloAuthor.links %}
186
+ <link rel="me" href="{{ link.href }}">
187
+ {% endfor %}
188
+ {% endif %}
181
189
  {% block head %}
182
190
  {#
183
191
  What a link to this page looks like when somebody posts it: the same
@@ -0,0 +1,90 @@
1
+ {#
2
+ A native comment on a page of its own (TASK-318), at `/comment/{id}/`: what
3
+ another site fetches when it replies to the comment, so the comment is the
4
+ page's one `h-entry`.
5
+
6
+ Above it is the thread it belongs to, from the post down, as nested
7
+ `u-in-reply-to h-cite`s: the comment cites its parent, which cites its own,
8
+ down to the post. A parser reads the chain a person reads, and the deepest
9
+ cite, printed first, is the post. An ancestor a reader may not see is in
10
+ `ancestors` as nothing, and stands in the chain with no author or words.
11
+ Below are its replies, cited in it with `p-comment`, then a link to the
12
+ whole thread on the post.
13
+
14
+ The context: `comment` is one entry of the thread as `partials/comment.njk`
15
+ prints it; `ancestors` is the chain from the top-level comment down to the
16
+ parent; `post` is the post as a listing entry is; `threadUrl` is the
17
+ comment's anchor on the post. The page is noindex and on no list.
18
+ #}
19
+ {% extends "layouts/base.njk" %}
20
+ {% import "partials/comment.njk" as thread with context %}
21
+
22
+ {% macro cited(index) %}
23
+ {% if index < 0 %}
24
+ {% set by = post.author or siteAuthor %}
25
+ <div class="comment-step u-in-reply-to h-cite">
26
+ <p class="cite-line">
27
+ <a class="u-url p-name" href="{{ post.url | url }}">{{ post.label }}</a>
28
+ {%- if by %} by <span class="p-author h-card">{% if by.url %}<a class="u-url p-name" href="{{ by.url }}">{{ by.name }}</a>{% else %}<span class="p-name">{{ by.name }}</span>{% endif %}</span>{% endif %}
29
+ {%- if post.date %}, <time class="dt-published" datetime="{{ post.date | date("iso") }}">{{ post.date | date }}</time>{% endif %}
30
+ </p>
31
+ {% if post.summary %}
32
+ <blockquote class="cite-quote p-summary">{{ post.summary }}</blockquote>
33
+ {% endif %}
34
+ </div>
35
+ {% else %}
36
+ {% set step = ancestors[index] %}
37
+ <div class="comment-step u-in-reply-to h-cite">
38
+ {{ cited(index - 1) }}
39
+ {% if step %}
40
+ <p class="cite-line">
41
+ <span class="p-author h-card">{% if step.author.url %}<a class="u-url p-name" href="{{ step.author.url }}"{% if step.source != "post" %} rel="nofollow ugc noopener noreferrer"{% endif %}>{{ step.author.name }}</a>{% else %}<span class="p-name">{{ step.author.name }}</span>{% endif %}</span>,
42
+ {%- if step.url %}
43
+ <a class="u-url" href="{{ step.url }}"{% if step.source != "comment" %} rel="nofollow noopener noreferrer"{% endif %}><time class="dt-published" datetime="{{ step.published | date("iso") }}">{{ step.published | date }}</time></a>
44
+ {%- else %}
45
+ <time class="dt-published" datetime="{{ step.published | date("iso") }}">{{ step.published | date }}</time>
46
+ {%- endif %}
47
+ </p>
48
+ <div class="cite-quote e-content">{{ step.content | safe }}</div>
49
+ {% else %}
50
+ <p class="cite-line comment-withheld">{{ thread.withheld() }}</p>
51
+ {% endif %}
52
+ </div>
53
+ {% endif %}
54
+ {% endmacro %}
55
+
56
+ {% block content %}
57
+ <article class="comment-page h-entry comment-{{ comment.source }}">
58
+ <h1 class="screen-reader-text">Comment by {{ comment.author.name }} on {{ post.label }}</h1>
59
+ <div class="cite comment-thread">
60
+ {{ cited(ancestors.length - 1) }}
61
+ </div>
62
+ <div class="comment-body">
63
+ <header class="comment-meta">
64
+ <div class="comment-author p-author h-card">
65
+ {%- if comment.author.avatar %}
66
+ <img class="avatar u-photo" src="{{ comment.author.avatar }}" alt="" width="40" height="40">
67
+ {%- else %}
68
+ <span class="comment-avatar-empty" aria-hidden="true">{{ comment.author.name | first | upper }}</span>
69
+ {%- endif %}
70
+ <b class="p-name">{% if comment.author.url %}<a class="u-url" href="{{ comment.author.url }}" rel="nofollow ugc noopener noreferrer">{{ comment.author.name }}</a>{% else %}{{ comment.author.name }}{% endif %}</b>
71
+ </div>
72
+ <div class="comment-metadata">
73
+ <a class="u-url" href="{{ comment.url }}"><time class="dt-published" datetime="{{ comment.published | date("iso") }}">{{ comment.published | date }}</time></a>
74
+ </div>
75
+ </header>
76
+ <div class="comment-content e-content">{{ comment.content | safe }}</div>
77
+ </div>
78
+ {% if comment.replies.length %}
79
+ <section class="comments-area">
80
+ <h2 class="comments-title">Replies</h2>
81
+ <ol class="comment-list">
82
+ {% for reply in comment.replies %}
83
+ {{ thread.comment(reply, "p-comment h-cite") }}
84
+ {% endfor %}
85
+ </ol>
86
+ </section>
87
+ {% endif %}
88
+ </article>
89
+ <p class="comment-back"><a href="{{ threadUrl | url }}">The whole conversation on {{ post.label }}</a></p>
90
+ {% endblock %}