{% 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) }}
--- /dev/null
+{% 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, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+ 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 %}
-{% 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, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- }
- 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>
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;
+}