⁨Andre601⁩ avatar
Andre_601

Mastodon-based comment system

A Mastodon-based comment system for Material for MkDocs

public ⁨3⁩ ⁨files⁩ 2023-07-17 15:10:42 UTC

_README.md

Raw

Mastodon-based Comment system for Material for MkDocs

This paste contains files to implement a comment system using a Mastodon instance and user.

Requirements

  • Mastodon account
  • Material for MkDocs theme
  • Knowledge in theme extension

Set up

Note: This setup was made using the dark theme of Material for MkDocs. You may need to edit colour values in the comments.md file to make it work with your design.

  1. Follow the Guide for extending the theme and create the necessary folders and files for adding a comment system
    • Do NOT enable and/or create the Giscus <script> part. It's not needed.
  2. Copy and paste the content of comments.html into the file with the same name (<your custom_dir folder>/partials/comments.html)
  3. Create a new CSS file in your assets directory and copy the content of comments.css over. Don't forget to also add it as extra CSS in your mkdocs.yml file!
  4. Add a new section called mastodon in the extra section. If said section doesn't exist, create it. Add user and host to the mastodon section and add necessary values to it. You should have a extra configuration that looks similar to this:
extra:
  mastodon:
    host: example.com # Replace with your Mastodon instance domain
    user: Example     # Replace with your username on the instance
  1. You're done! All you need to do is now create a Post on Mastodon, copy the ID from it and add it as comment_id YAML frontmatter to your page. Also, make sure to add comments: true to the front matter so that the comments.html content is used.

Preview

Here are some previews

No Post set

no post set

Post set, but no comments (Replies)

no comments

Comment

comment

Credits

Original concept and code-snippets: https://danielpecos.com/2022/12/25/mastodon-as-comment-system-for-your-static-blog/

# Mastodon-based Comment system for Material for MkDocs
This paste contains files to implement a comment system using a Mastodon instance and user.

## Requirements

- Mastodon account
- Material for MkDocs theme
- Knowledge in theme extension

## Set up
> Note: This setup was made using the dark theme of Material for MkDocs. You may need to edit colour values in the `comments.md` file to make it work with your design.

1. Follow the Guide for [extending the theme](https://squidfunk.github.io/mkdocs-material/customization/#extending-the-theme) and create the necessary folders and files for [adding a comment system](https://squidfunk.github.io/mkdocs-material/setup/adding-a-comment-system/)
    - Do NOT enable and/or create the Giscus `<script>` part. It's not needed.
2. Copy and paste the content of `comments.html` into the file with the same name (`<your custom_dir folder>/partials/comments.html`)
3. Create a new CSS file in your assets directory and copy the content of `comments.css` over. Don't forget to also add it as extra CSS in your mkdocs.yml file!
4. Add a new section called `mastodon` in the `extra` section. If said section doesn't exist, create it. Add `user` and `host` to the `mastodon` section and add necessary values to it.
  You should have a `extra` configuration that looks similar to this:  
  ```yaml
  extra:
    mastodon:
      host: example.com # Replace with your Mastodon instance domain
      user: Example     # Replace with your username on the instance
  ```
5. You're done! All you need to do is now create a Post on Mastodon, copy the ID from it and add it as `comment_id` YAML frontmatter to your page. Also, make sure to add `comments: true` to the front matter so that the comments.html content is used.

## Preview
Here are some previews

### No Post set
![no post set](https://user-images.githubusercontent.com/11576465/253985888-7521a64f-5899-49e0-8797-6e7ee262e738.png "Message shown when no post has been set")

### Post set, but no comments (Replies)
![no comments](https://user-images.githubusercontent.com/11576465/253985700-e14a66f8-714f-4aca-987e-e074f0b0e5a3.png "Message shown when no reply has been made yet")

### Comment
![comment](https://user-images.githubusercontent.com/11576465/253999048-31f7b5ad-8adf-4866-8ee1-aa85a4757809.png "How the comment would look like")

## Credits
Original concept and code-snippets: <https://danielpecos.com/2022/12/25/mastodon-as-comment-system-for-your-static-blog/>

comments.css

Raw
:root{
  --mastodon-comment-indent: 40px;
  --mastodon-comment-border-radius: 3px;
  
  --mastodon-comment-bg-color: rgba(0, 0, 0, 0.2);
  --mastodon-comment-border-color: rgba(0, 0, 0, 0.4);
  --mastodon-comment-user-color: #939393;
  
  --mastodon-comment-status--inactive: #5d686f;
  --mastodon-comment-status-replies--active: #448aff;
  --mastodon-comment-status-favourite--active: #ff9100;
  --mastodon-comment-status-reblog--active: #00c853;
}

@media only screen and (max-width: 1024px){
  :root{
    --mastodon-comment-indent: 20px;
  }
}

@media only screen and (max-width: 640px){
  :root{
    --mastodon-comment-indent: 0px;
  }
}

.mastodon-comment{
  background-color: var(--mastodon-comment-bg-color);
  border-radius: var(--mastodon-comment-border-radius);
  border: 1px var(--mastodon-comment-border-color) solid;
  padding: 20px;
  margin-bottom: 1.5rem;
  display: flex;
  flex-direction: column;
}

.mastodon-comment p{
  margin-bottom: 0px;
}

.mastodon-comment .content{
  margin: 15px 20px;
}

.mastodon-comment .content p:first-child{
  margin-top: 0;
  margin-bottom: 0;
}

.mastodon-comment .attachments{
  max-width: 0px 10px;
}

.mastodon-comment .attachments > *{
  max-width: 0px 10px;
}

.mastodon-comment .author{
  padding-top: 0;
  display: flex;
}

.mastodon-comment .author a{
  text-decoration: none;
}

.mastodon-comment .author .avatar img{
  margin-right: 1rem;
  min-width: 60px;
  border-radius: 5px;
}

.mastodon-comment .author .details{
  display: flex;
  flex-direction: column;
}

.mastodon-comment .author .details .name{
  font-weight: bold;
}

.mastodon-comment .author .details .user{
  color: var(--mastodon-comment-user-color);
}

.mastodon-comment .author .date{
  margin-left: auto;
  font-size: small;
}

.mastodon-comment .status > div{
  display: inline-block;
  margin-right: 15px;
}

.mastodon-comment .status a{
  color: var(--mastodon-comment-status--inactive);
  text-decoration: none;
}

.mastodon-comment .status .twemoji.replies.active a{
  color: var(--mastodon-comment-status-replies--active);
}

.mastodon-comment .status .twemoji.reblogs.active a{
  color: var(--mastodon-comment-status-reblog--active);
}

.mastodon-comment .status .twemoji.favourites.active a{
  color: var(--mastodon-comment-status-favourite--active);
}

.mastodon-comment .status svg{
  margin-right: 0.2rem;
  vertical-align: middle;
}

comments.html

Raw
{% if page.meta.comments %}
  <h2 id="__comments">{{ lang.t("meta.comments") }}</h2>
  {% if page.meta.comment_id %}
    <noscript>
      <div class="admonition danger">
        <p class="admonition-title">
          Please enable Javascript to see comments from Mastodon.
        </p>
      </div>
    </noscript>
    <p>
      <a href="https://{{ config.extra.mastodon.host }}/@{{ config.extra.mastodon.user }}/{{ page.meta.comment_id }}">Comment on this blog post</a> using a Fediverse-compatible Account (Mastodon or alike).
    </p>
    
    <p id="mastodon-comments-list"></p>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.1/purify.min.js" integrity="sha512-uHOKtSfJWScGmyyFr2O2+efpDx2nhwHU2v7MVeptzZoiC7bdF6Ny/CmZhN2AwIK1oCFiVQQ5DA/L9FSzyPNu6Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script type="text/javascript">
      var host = '{{ config.extra.mastodon.host }}';
      var user = '{{ config.extra.mastodon.user }}';
      var id = '{{ page.meta.comment_id }}'
    
      function escapeHtml(unsafe) {
        return unsafe
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;")
          .replace(/"/g, "&quot;")
          .replace(/'/g, "&#039;");
      }
    
      var commentsLoaded = false;
    
      function toot_active(toot, what) {
        var count = toot[what+'_count'];
        return count > 0 ? 'active' : '';
      }
    
      function toot_count(toot, what) {
        var count = toot[what+'_count'];
        return count > 0 ? count : '';
      }
    
      function user_account(account) {
        var result =`@${account.acct}`;
        if (account.acct.indexOf('@') === -1) {
          var domain = new URL(account.url)
          result += `@${domain.hostname}`
        }
        return result;
      }
    
      function render_toots(toots, in_reply_to, depth) {
        var tootsToRender = toots
          .filter(toot => toot.in_reply_to_id === in_reply_to)
          .sort((a, b) => a.created_at.localeCompare(b.created_at));
        tootsToRender.forEach(toot => render_toot(toots, toot, depth));
      }
    
      function render_toot(toots, toot, depth) {
        toot.account.display_name = escapeHtml(toot.account.display_name);
        toot.account.emojis.forEach(emoji => {
          toot.account.display_name = toot.account.display_name.replace(`:${emoji.shortcode}:`, `<img src="${escapeHtml(emoji.static_url)}" alt="Emoji ${emoji.shortcode}" height="20" width="20" />`);
        });
        mastodonComment =
          `<div class="mastodon-comment" style="margin-left: calc(var(--mastodon-comment-indent) * ${depth})">
            <div class="author">
              <div class="avatar">
                <img src="${escapeHtml(toot.account.avatar_static)}" height=60 width=60 alt="">
              </div>
              <div class="details">
                <a class="name" href="${toot.account.url}" rel="nofollow">${toot.account.display_name}</a>
                <a class="user" href="${toot.account.url}" rel="nofollow">${user_account(toot.account)}</a>
              </div>
              <a class="date" href="${toot.url}" rel="nofollow">${toot.created_at.substr(0, 10)} ${toot.created_at.substr(11, 8)}</a>
            </div>
            <div class="content">${toot.content}</div>
            <div class="attachments">
              ${toot.media_attachments.map(attachment => {
                if (attachment.type === 'image') {
                  return `<a href="${attachment.url}" rel="nofollow"><img src="${attachment.preview_url}" alt="${attachment.description}" /></a>`;
                } else if (attachment.type === 'video') {
                  return `<video controls><source src="${attachment.url}" type="${attachment.mime_type}"></video>`;
                } else if (attachment.type === 'gifv') {
                  return `<video autoplay loop muted playsinline><source src="${attachment.url}" type="${attachment.mime_type}"></video>`;
                } else if (attachment.type === 'audio') {
                  return `<audio controls><source src="${attachment.url}" type="${attachment.mime_type}"></audio>`;
                } else {
                  return `<a href="${attachment.url}" rel="nofollow">${attachment.type}</a>`;
                }
              }).join('')}
            </div>
            <div class="status">
              <div class="twemoji replies ${toot_active(toot, 'replies')}">
                <a href="${toot.url}" rel="nofollow">{% include ".icons/fontawesome/solid/reply.svg" %}${toot_count(toot, 'replies')}</a>
              </div>
              <div class="twemoji reblogs ${toot_active(toot, 'reblogs')}">
                <a href="${toot.url}" rel="nofollow">{% include ".icons/fontawesome/solid/retweet.svg" %}${toot_count(toot, 'reblogs')}</a>
              </div>
              <div class="twemoji favourites ${toot_active(toot, 'favourites')}">
                <a href="${toot.url}" rel="nofollow">{% include ".icons/fontawesome/solid/star.svg" %}${toot_count(toot, 'favourites')}</a>
              </div>
            </div>
          </div>`;
        document.getElementById('mastodon-comments-list').appendChild(DOMPurify.sanitize(mastodonComment, {'RETURN_DOM_FRAGMENT': true}));
    
        render_toots(toots, toot.id, depth + 1)
      }
    
      function loadComments() {
        if (commentsLoaded) return;
    
        document.getElementById("mastodon-comments-list").innerHTML = "Loading comments from the Fediverse...";
    
        fetch('https://' + host + '/api/v1/statuses/' + id + '/context')
          .then(function(response) {
            return response.json();
          })
          .then(function(data) {
            if(data['descendants'] && Array.isArray(data['descendants']) && data['descendants'].length > 0) {
                document.getElementById('mastodon-comments-list').innerHTML = "";
                render_toots(data['descendants'], id, 0)
            } else {
              document.getElementById('mastodon-comments-list').innerHTML = 
              `<div class="admonition info">
                <p class="admonition-title">
                  No comments found. <a href="https://{{ config.extra.mastodon.host }}/@{{ config.extra.mastodon.user }}/{{ page.meta.comment_id }}">Be the first!</a>
                </p>
              </div>
              `;
            }
    
            commentsLoaded = true;
          });
      }
    
      function respondToVisibility(element, callback) {
        var options = {
          root: null,
        };
    
        var observer = new IntersectionObserver((entries, observer) => {
          entries.forEach(entry => {
            if (entry.intersectionRatio > 0) {
              callback();
            }
          });
        }, options);
    
        observer.observe(element);
      }
    
      var comments = document.getElementById("mastodon-comments-list");
      respondToVisibility(comments, loadComments);
    </script>
  {% else %}
    <div class="admonition warning">
      <p class="admonition-title">
        No Mastodon post configured for this page. Contact {{ config.site_author | default('the post author', true) }} if you want to comment here.
      </p>
    </div>
  {% endif %}
{% endif %}