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.
- 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.
- Do NOT enable and/or create the Giscus
- Copy and paste the content of
comments.html
into the file with the same name (<your custom_dir folder>/partials/comments.html
) - 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! - Add a new section called
mastodon
in theextra
section. If said section doesn't exist, create it. Adduser
andhost
to themastodon
section and add necessary values to it. You should have aextra
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
- 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 addcomments: true
to the front matter so that the comments.html content is used.
Preview
Here are some previews
No Post set
Post set, but no comments (Replies)
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
: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
{% 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
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 %}