chiark / gitweb /
Fix CSS for Mastodon comments
authorColin Watson <cjwatson@debian.org>
Tue, 1 Oct 2024 17:02:18 +0000 (18:02 +0100)
committerColin Watson <cjwatson@debian.org>
Tue, 1 Oct 2024 17:02:18 +0000 (18:02 +0100)
Based heavily on
https://gitlab.com/ognarb/blog/-/raw/master/assets/scss/partials/article.scss,
via
https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/

m.css/pelican-theme/templates/article.html
m.css/pelican-theme/templates/article_comments.html [new file with mode: 0644]
m.css/pelican-theme/templates/article_footer.html
static/tweaks.css

index 859e7508a6fe01d1e6b411d58145811273851798..1ba5d9c3e82f951484ba4c55f27f30934abb9eea 100644 (file)
       {% if article.category.badge or (article.author and article.author.badge) %}
       {{ badges()|rtrim|indent(6) }}
       {% endif %}
+      {% include "article_comments.html" %}
       <footer>
         {% macro footer() %}{% include "article_footer.html" %}{% endmacro %}
         {{ footer()|rtrim|indent(8) }}
diff --git a/m.css/pelican-theme/templates/article_comments.html b/m.css/pelican-theme/templates/article_comments.html
new file mode 100644 (file)
index 0000000..8597237
--- /dev/null
@@ -0,0 +1,194 @@
+{% if MASTODON_HOST and MASTODON_USERNAME and article.metadata['mastodon-id'] %}
+<section id="comments" class="article-content">
+  <h2>Comments</h2>
+  <p>With an account on the Fediverse or Mastodon, you can respond to this <a href="https://{{ MASTODON_HOST }}/@{{ MASTODON_USERNAME }}/{{ article.metadata['mastodon-id'] }}">post</a>. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.</p>
+  <p>Learn how this is implemented <a class="link" href="https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/">here</a>.</p>
+
+  <p id="mastodon-comments-list"><button id="load-comment">Load comments</button></p>
+  <div id="comments-wrapper">
+    <noscript><p>Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit <a href="https://{{ MASTODON_HOST }}/@{{ MASTODON_USERNAME }}/{{ article.metadata['mastodon-id'] }}">the original post</a> on Mastodon.</p></noscript>
+  </div>
+  <noscript>You need JavaScript to view the comments.</noscript>
+  <script src="{{ SITEURL }}/static/purify.min.js"></script>
+  <script type="text/javascript">
+    function escapeHtml(unsafe) {
+      return unsafe
+           .replace(/&/g, "&amp;")
+           .replace(/</g, "&lt;")
+           .replace(/>/g, "&gt;")
+           .replace(/"/g, "&quot;")
+           .replace(/'/g, "&#039;");
+    }
+    function emojify(input, emojis) {
+      let output = input;
+
+      emojis.forEach(emoji => {
+        let picture = document.createElement("picture");
+
+        let source = document.createElement("source");
+        source.setAttribute("srcset", escapeHtml(emoji.url));
+        source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
+
+        let img = document.createElement("img");
+        img.className = "emoji";
+        img.setAttribute("src", escapeHtml(emoji.static_url));
+        img.setAttribute("alt", `:${ emoji.shortcode }:`);
+        img.setAttribute("title", `:${ emoji.shortcode }:`);
+        img.setAttribute("width", "20");
+        img.setAttribute("height", "20");
+
+        picture.appendChild(source);
+        picture.appendChild(img);
+
+        output = output.replace(`:${ emoji.shortcode }:`, picture.outerHTML);
+      });
+
+      return output;
+    }
+
+    function loadComments() {
+      let commentsWrapper = document.getElementById("comments-wrapper");
+      let loadCommentsButton = document.getElementById("load-comment");
+      loadCommentsButton.innerHTML = "Loading";
+      fetch('https://{{ MASTODON_HOST }}/api/v1/statuses/{{ article.metadata['mastodon-id'] }}/context')
+        .then(function(response) {
+          return response.json();
+        })
+        .then(function(data) {
+          let descendants = data['descendants'];
+          if(
+            descendants &&
+            Array.isArray(descendants) &&
+            descendants.length > 0
+          ) {
+            commentsWrapper.innerHTML = "";
+
+            descendants.forEach(function(status) {
+                console.log(descendants)
+              if( status.account.display_name.length > 0 ) {
+                status.account.display_name = escapeHtml(status.account.display_name);
+                status.account.display_name = emojify(status.account.display_name, status.account.emojis);
+              } else {
+                status.account.display_name = status.account.username;
+              };
+
+              let instance = "";
+              if( status.account.acct.includes("@") ) {
+                instance = status.account.acct.split("@")[1];
+              } else {
+                instance = "{{ MASTODON_HOST }}";
+              }
+
+              const isReply = status.in_reply_to_id !== "{{ article.metadata['mastodon-id'] }}";
+
+              let op = false;
+              if( status.account.acct == "{{ MASTODON_USERNAME }}" ) {
+                op = true;
+              }
+
+              status.content = emojify(status.content, status.emojis);
+
+              let avatarSource = document.createElement("source");
+              avatarSource.setAttribute("srcset", escapeHtml(status.account.avatar));
+              avatarSource.setAttribute("media", "(prefers-reduced-motion: no-preference)");
+
+              let avatarImg = document.createElement("img");
+              avatarImg.className = "avatar";
+              avatarImg.setAttribute("src", escapeHtml(status.account.avatar_static));
+              avatarImg.setAttribute("alt", `@${ status.account.username }@${ instance } avatar`);
+
+              let avatarPicture = document.createElement("picture");
+              avatarPicture.appendChild(avatarSource);
+              avatarPicture.appendChild(avatarImg);
+
+              let avatar = document.createElement("a");
+              avatar.className = "avatar-link";
+              avatar.setAttribute("href", status.account.url);
+              avatar.setAttribute("rel", "external nofollow");
+              avatar.setAttribute("title", `View profile at @${ status.account.username }@${ instance }`);
+              avatar.appendChild(avatarPicture);
+
+              let instanceBadge = document.createElement("a");
+              instanceBadge.className = "instance";
+              instanceBadge.setAttribute("href", status.account.url);
+              instanceBadge.setAttribute("title", `@${ status.account.username }@${ instance }`);
+              instanceBadge.setAttribute("rel", "external nofollow");
+              instanceBadge.textContent = instance;
+
+              let display = document.createElement("span");
+              display.className = "display";
+              display.setAttribute("itemprop", "author");
+              display.setAttribute("itemtype", "http://schema.org/Person");
+              display.innerHTML = status.account.display_name;
+
+              let header = document.createElement("header");
+              header.className = "author";
+              header.appendChild(display);
+              header.appendChild(instanceBadge);
+
+              let permalink = document.createElement("a");
+              permalink.setAttribute("href", status.url);
+              permalink.setAttribute("itemprop", "url");
+              permalink.setAttribute("title", `View comment at ${ instance }`);
+              permalink.setAttribute("rel", "external nofollow");
+              permalink.textContent = new Date( status.created_at ).toLocaleString('en-US', {
+                dateStyle: "long",
+                timeStyle: "short",
+              });
+
+              let timestamp = document.createElement("time");
+              timestamp.setAttribute("datetime", status.created_at);
+              timestamp.appendChild(permalink);
+
+              let main = document.createElement("main");
+              main.setAttribute("itemprop", "text");
+              main.innerHTML = status.content;
+
+              let interactions = document.createElement("footer");
+              if(status.favourites_count > 0) {
+                let faves = document.createElement("a");
+                faves.className = "faves";
+                faves.setAttribute("href", `${ status.url }/favourites`);
+                faves.setAttribute("title", `Favorites from ${ instance }`);
+                faves.textContent = status.favourites_count;
+
+                interactions.appendChild(faves);
+              }
+
+              let comment = document.createElement("div");
+              comment.id = `comment-${ status.id }`;
+              comment.className = isReply ? "comment comment-reply" : "comment";
+              comment.setAttribute("itemprop", "comment");
+              comment.setAttribute("itemtype", "http://schema.org/Comment");
+              comment.appendChild(avatar);
+              comment.appendChild(header);
+              comment.appendChild(timestamp);
+              comment.appendChild(main);
+              comment.appendChild(interactions);
+
+              if(op === true) {
+                comment.classList.add("op");
+
+                avatar.classList.add("op");
+                avatar.setAttribute(
+                  "title",
+                  "Blog post author; " + avatar.getAttribute("title")
+                );
+
+                instanceBadge.classList.add("op");
+                instanceBadge.setAttribute(
+                  "title",
+                  "Blog post author: " + instanceBadge.getAttribute("title")
+                );
+              }
+
+              commentsWrapper.innerHTML += DOMPurify.sanitize(comment.outerHTML);
+            });
+          }
+          loadCommentsButton.hidden = true;
+        });
+      }
+      document.getElementById("load-comment").addEventListener("click", loadComments);
+  </script>
+</section>
+{% endif %}
index 67ea9c46418b7afe09d7a181cb5a04be692a2ceb..6282168fd5b007c7816aea4e7d3617bdacf2e2c8 100644 (file)
@@ -1,195 +1 @@
-{% if MASTODON_HOST and MASTODON_USERNAME and article.metadata['mastodon-id'] %}
-<section id="comments" class="article-content">
-  <h2>Comments</h2>
-  <p>With an account on the Fediverse or Mastodon, you can respond to this <a href="https://{{ MASTODON_HOST }}/@{{ MASTODON_USERNAME }}/{{ article.metadata['mastodon-id'] }}">post</a>. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.</p>
-  <p>Learn how this is implemented <a class="link" href="https://carlschwan.eu/2020/12/29/adding-comments-to-your-static-blog-with-mastodon/">here</a>.</p>
-
-  <p id="mastodon-comments-list"><button id="load-comment">Load comments</button></p>
-  <div id="comments-wrapper">
-    <noscript><p>Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit <a href="https://{{ MASTODON_HOST }}/@{{ MASTODON_USERNAME }}/{{ article.metadata['mastodon-id'] }}">the original post</a> on Mastodon.</p></noscript>
-  </div>
-  <noscript>You need JavaScript to view the comments.</noscript>
-  <script src="{{ SITEURL }}/static/purify.min.js"></script>
-  <script type="text/javascript">
-    function escapeHtml(unsafe) {
-      return unsafe
-           .replace(/&/g, "&amp;")
-           .replace(/</g, "&lt;")
-           .replace(/>/g, "&gt;")
-           .replace(/"/g, "&quot;")
-           .replace(/'/g, "&#039;");
-    }
-    function emojify(input, emojis) {
-      let output = input;
-
-      emojis.forEach(emoji => {
-        let picture = document.createElement("picture");
-
-        let source = document.createElement("source");
-        source.setAttribute("srcset", escapeHtml(emoji.url));
-        source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
-
-        let img = document.createElement("img");
-        img.className = "emoji";
-        img.setAttribute("src", escapeHtml(emoji.static_url));
-        img.setAttribute("alt", `:${ emoji.shortcode }:`);
-        img.setAttribute("title", `:${ emoji.shortcode }:`);
-        img.setAttribute("width", "20");
-        img.setAttribute("height", "20");
-
-        picture.appendChild(source);
-        picture.appendChild(img);
-
-        output = output.replace(`:${ emoji.shortcode }:`, picture.outerHTML);
-      });
-
-      return output;
-    }
-
-    function loadComments() {
-      let commentsWrapper = document.getElementById("comments-wrapper");
-      let loadCommentsButton = document.getElementById("load-comment");
-      loadCommentsButton.innerHTML = "Loading";
-      fetch('https://{{ MASTODON_HOST }}/api/v1/statuses/{{ article.metadata['mastodon-id'] }}/context')
-        .then(function(response) {
-          return response.json();
-        })
-        .then(function(data) {
-          let descendants = data['descendants'];
-          if(
-            descendants &&
-            Array.isArray(descendants) &&
-            descendants.length > 0
-          ) {
-            commentsWrapper.innerHTML = "";
-
-            descendants.forEach(function(status) {
-                console.log(descendants)
-              if( status.account.display_name.length > 0 ) {
-                status.account.display_name = escapeHtml(status.account.display_name);
-                status.account.display_name = emojify(status.account.display_name, status.account.emojis);
-              } else {
-                status.account.display_name = status.account.username;
-              };
-
-              let instance = "";
-              if( status.account.acct.includes("@") ) {
-                instance = status.account.acct.split("@")[1];
-              } else {
-                instance = "{{ MASTODON_HOST }}";
-              }
-
-              const isReply = status.in_reply_to_id !== "{{ article.metadata['mastodon-id'] }}";
-
-              let op = false;
-              if( status.account.acct == "{{ MASTODON_USERNAME }}" ) {
-                op = true;
-              }
-
-              status.content = emojify(status.content, status.emojis);
-
-              let avatarSource = document.createElement("source");
-              avatarSource.setAttribute("srcset", escapeHtml(status.account.avatar));
-              avatarSource.setAttribute("media", "(prefers-reduced-motion: no-preference)");
-
-              let avatarImg = document.createElement("img");
-              avatarImg.className = "avatar";
-              avatarImg.setAttribute("src", escapeHtml(status.account.avatar_static));
-              avatarImg.setAttribute("alt", `@${ status.account.username }@${ instance } avatar`);
-
-              let avatarPicture = document.createElement("picture");
-              avatarPicture.appendChild(avatarSource);
-              avatarPicture.appendChild(avatarImg);
-
-              let avatar = document.createElement("a");
-              avatar.className = "avatar-link";
-              avatar.setAttribute("href", status.account.url);
-              avatar.setAttribute("rel", "external nofollow");
-              avatar.setAttribute("title", `View profile at @${ status.account.username }@${ instance }`);
-              avatar.appendChild(avatarPicture);
-
-              let instanceBadge = document.createElement("a");
-              instanceBadge.className = "instance";
-              instanceBadge.setAttribute("href", status.account.url);
-              instanceBadge.setAttribute("title", `@${ status.account.username }@${ instance }`);
-              instanceBadge.setAttribute("rel", "external nofollow");
-              instanceBadge.textContent = instance;
-
-              let display = document.createElement("span");
-              display.className = "display";
-              display.setAttribute("itemprop", "author");
-              display.setAttribute("itemtype", "http://schema.org/Person");
-              display.innerHTML = status.account.display_name;
-
-              let header = document.createElement("header");
-              header.className = "author";
-              header.appendChild(display);
-              header.appendChild(instanceBadge);
-
-              let permalink = document.createElement("a");
-              permalink.setAttribute("href", status.url);
-              permalink.setAttribute("itemprop", "url");
-              permalink.setAttribute("title", `View comment at ${ instance }`);
-              permalink.setAttribute("rel", "external nofollow");
-              permalink.textContent = new Date( status.created_at ).toLocaleString('en-US', {
-                dateStyle: "long",
-                timeStyle: "short",
-              });
-
-              let timestamp = document.createElement("time");
-              timestamp.setAttribute("datetime", status.created_at);
-              timestamp.appendChild(permalink);
-
-              let main = document.createElement("main");
-              main.setAttribute("itemprop", "text");
-              main.innerHTML = status.content;
-
-              let interactions = document.createElement("footer");
-              if(status.favourites_count > 0) {
-                let faves = document.createElement("a");
-                faves.className = "faves";
-                faves.setAttribute("href", `${ status.url }/favourites`);
-                faves.setAttribute("title", `Favorites from ${ instance }`);
-                faves.textContent = status.favourites_count;
-
-                interactions.appendChild(faves);
-              }
-
-              let comment = document.createElement("article");
-              comment.id = `comment-${ status.id }`;
-              comment.className = isReply ? "comment comment-reply" : "comment";
-              comment.setAttribute("itemprop", "comment");
-              comment.setAttribute("itemtype", "http://schema.org/Comment");
-              comment.appendChild(avatar);
-              comment.appendChild(header);
-              comment.appendChild(timestamp);
-              comment.appendChild(main);
-              comment.appendChild(interactions);
-
-              if(op === true) {
-                comment.classList.add("op");
-
-                avatar.classList.add("op");
-                avatar.setAttribute(
-                  "title",
-                  "Blog post author; " + avatar.getAttribute("title")
-                );
-
-                instanceBadge.classList.add("op");
-                instanceBadge.setAttribute(
-                  "title",
-                  "Blog post author: " + instanceBadge.getAttribute("title")
-                );
-              }
-
-              commentsWrapper.innerHTML += DOMPurify.sanitize(comment.outerHTML);
-            });
-          }
-          loadCommentsButton.hidden = true;
-        });
-      }
-      document.getElementById("load-comment").addEventListener("click", loadComments);
-  </script>
-</section>
-{% endif %}
 <p>Posted{% if article.authors %} by {% for author in article.authors %}<a href="{{ author.url|format_siteurl|e }}">{{ author|e }}</a>{% endfor %}{% endif %} on <time datetime="{{ article.date.isoformat() }}">{{ article.locale_date }}</time> in <a href="{{ article.category.url|format_siteurl|e }}">{{ article.category|e }}</a>.{% if article.modified %} <span class="m-label m-success">updated <time datetime="{{ article.modified.isoformat() }}">{{ article.locale_modified }}</time></span>{% endif %}{% if article.archived == 'True' %} <span class="m-label m-warning">archived</span>{% endif %}{% if article.status == 'draft' %} <span class="m-label m-dim">draft</span>{% endif %}{% if article.tags %} Tags: {% for tag in article.tags %}<a href="{{ tag.url|format_siteurl|e }}">{{ tag|e }}</a>{% if not loop.last %}, {% endif %}{% endfor %}.{% endif %}</p>
index d7ff03886abc8267a57e20c9fd6d3aaa35996ecc..017d69ec296e38bb34cea9a6a5a9187e85635821 100644 (file)
@@ -4,3 +4,79 @@
 div.highlight {
         margin-bottom: 1rem;
 }
+
+section#comments .comment {
+        display: grid;
+        column-gap: 1rem;
+        grid-template-areas: "avatar name" "avatar time" "avatar post" "...... interactions";
+        grid-template-columns: min-content;
+        justify-items: start;
+        margin: 0em auto 0em -1em;
+        padding: 0.5em;
+}
+
+section#comments .comment.comment-reply {
+        margin: 0em auto 0em 1em;
+}
+
+section#comments .comment .avatar-link {
+        grid-area: avatar;
+        height: 4rem;
+        position: relative;
+        width: 4rem;
+}
+
+section#comments .comment .avatar-link .avatar {
+        height: 100%;
+        width: 100%;
+}
+
+section#comments .comment .author {
+        align-items: center;
+        display: flex;
+        font-weight: bold;
+        gap: 0.5em;
+        grid-area: name;
+}
+
+section#comments .comment .author .instance {
+        background-color: black;
+        border-radius: 9999px;
+        font-size: smaller;
+        padding: .25em .75em;
+        text-decoration: none;
+}
+
+section#comments .comment .author .instance::hover {
+        opacity: 0.8;
+}
+
+section#comments .comment .author .instance.op::before {
+        content: "✓";
+        font-weight: bold;
+        margin-inline-end: 0.25em;
+        margin-inline-start: -0.25em;
+}
+
+section#comments .comment time {
+        font-size: smaller;
+        opacity: 0.9;
+        grid-area: time;
+        line-height: 1.5rem;
+}
+
+section#comments .comment main {
+        grid-area: post;
+}
+
+section#comments .comment main p {
+        text-indent: 0;
+}
+
+section#comments .comment main p:first-child {
+        margin-top: 0.25em;
+}
+
+section#comments .comment main p:last-child {
+        margin-bottom: 0;
+}