Garbage Collector


The little space of a writer, tinkerer, and a coffee addict

Integrate your Mastodon feed on a Hugo-powered website

Integrate your Mastodon feed on a Hugo-powered website
Hugo logo, License Apache 2.0

While I was working on a friend’s website made with Hugo, he asked me if I could integrate his Mastodon profile on the homepage.

After a few research, I was surprised to see, or possibly because I couldn’t find it, that it wasn’t possible to create an integration of the user’s feed like you can have with other social media. So far, I could just find a post integration, but not the entire timeline. So let’s do it ourselves !

The functional spec was the following one :

First, I was thinking about using the API but eventually, I took the opportunity to use the RSS feed of a profile. In any case, we will need some Javascript because we will have to parse the RSS feed content. It would have been the same if the API was used, but with JSON.

On Mastodon, if you can add .rss to a profile URL, you’ll be able to see its timeline formatted in RSS.

For example with mine :

https://fosstodon.org/@Wivik => https://fosstodon.org/@Wivik.rss : rss.png

So we basically have the data available without anything more to do. Let’s go.

Some params for Hugo

My friend’s website has a little complexity : it’s a dual language site in French and English. And in order to avoid hard coding the Mastodon profile URL in the script, I’ve put in Hugo’s params section of hugo.yaml. I’ve also defined a mastodonMaxItems setting to limit how many posts to return.

params:
  mastodon: https://fosstodon.org/@Wivik
  mastodonMaxItems: 5

Then, to manage the multilingual part, I’ve defined variables in i18n/fr.yaml and i18n/en.yaml.

en.yaml :

home:
  viewOnMastodon: "View on Mastodon"
  followOnMastodon: "Follow"
  publishedDateOnMastodon: "Published on"

fr.yaml :

home:
  viewOnMastodon: "Lire sur Mastodon"
  followOnMastodon: "Suivre"
  publishedDateOnMastodon: "PubliΓ© le"

Hugo’s setup : done.

Create the Javascript

Since I’m not a JS developer, I’ve made profitable the free trial I’ve activated for GitHub Copilot Chat. Some prompts later that gave me the basis, I could enhance by myself the rest of it and here is the script. This script has been saved into the template’s assets folder : assets/js/mastodon.js

// fetch RSS of a mastodon profile and return the latest feed

fetch(mastodonProfile + '.rss')
  // get the response content
  .then(response => response.text())
  .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
  .then(data => {

    // put in variables the title, link and picture profile
    const channelTitle = data.querySelector("channel > title").textContent;
    const channelLink = data.querySelector("channel > link").textContent;
    const channelImage = data.querySelector("channel > image > url").textContent;

    // assign the script to a div using mastodon-feed id
    const feed = document.getElementById('mastodon-feed');

    // create the div container
    const headerElement = document.createElement('div');
    headerElement.className = 'header';

    // add the profile picture
    const imageElement = document.createElement('img');
    imageElement.src = channelImage;
    headerElement.appendChild(imageElement);

    // add a lin to the profile name
    const titleElement = document.createElement('a');
    titleElement.href = channelLink;
    titleElement.textContent = channelTitle;
    headerElement.appendChild(titleElement);

    // create a follow button redirecting to the profile using hugo i18n values
    const followElement = document.createElement('a');
    followElement.className = 'follow-button';
    followElement.href = channelLink;
    followElement.textContent = i18n.followOnMastodon;
    headerElement.appendChild(followElement);

    // close the header element
    feed.appendChild(headerElement);

    // loop over the feed content with a content limit
    const items = data.querySelectorAll("item");
    const itemsArray = Array.from(items).slice(0, maxItems);
    itemsArray.forEach(item => {
      // create a div container for the posts
      const statusElement = document.createElement('div');
      statusElement.className = 'status';
      statusElement.innerHTML = item.querySelector("description").textContent;

      // create a div footer for the post url and date
      const footerElement = document.createElement('div');
      footerElement.className = 'post-footer';

      const dateElement = document.createElement('p');
      const pubDate = new Date(item.querySelector("pubDate").textContent);
      dateElement.textContent = `${i18n.publishedDateOnMastodon} ${pubDate.toLocaleDateString()} - ${pubDate.toLocaleTimeString()} | `;
      footerElement.appendChild(dateElement);

      const linkElement = document.createElement('a');
      linkElement.href = item.querySelector("link").textContent;
      linkElement.textContent = i18n.viewOnMastodon;
      footerElement.appendChild(linkElement);

      statusElement.appendChild(footerElement);

      // close the post element
      feed.appendChild(statusElement);

    });
  });

Integrate with the template

In the homepage template, I’ve added the following parts to integrate the Mastodon RSS feed.

The Hugo template with resources.Get has been taken from the file partials/head/js.html and adapted to import my script. It seems to check the script integrity for the live version.

The div element with the id mastodon-feed is the placeholder in which the Javascript code will integrate the dynamic part.

  <p>
    <div id="mastodon-feed" class="mastodon-feed"></div>
    <script>
      var i18n = {
        followOnMastodon: "{{ i18n "home.followOnMastodon" }}",
        viewOnMastodon: "{{ i18n "home.viewOnMastodon" }}",
        publishedDateOnMastodon: "{{ i18n "home.publishedDateOnMastodon" }}",
      };
      var mastodonProfile = "{{ .Site.Params.mastodon }}";
      var maxItems = "{{ default 5 .Site.Params.mastodonMaxItems }}"

    </script>
    {{- with resources.Get "js/mastodon.js" }}
    {{- if eq hugo.Environment "development" }}
      {{- with . | js.Build }}
        <script src="{{ .RelPermalink }}"></script>
      {{- end }}
    {{- else }}
      {{- $opts := dict "minify" true }}
      {{- with . | js.Build $opts | fingerprint }}
        <script src="{{ .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous"></script>
      {{- end }}
    {{- end }}
  {{- end }}

  </p>

The important parts I’ve added are the variables :

    <script>
      var i18n = {
        followOnMastodon: "{{ i18n "home.followOnMastodon" }}",
        viewOnMastodon: "{{ i18n "home.viewOnMastodon" }}",
        publishedDateOnMastodon: "{{ i18n "home.publishedDateOnMastodon" }}",
      };
      var mastodonProfile = "{{ .Site.Params.mastodon }}";
      var maxItems = "{{ default 5 .Site.Params.mastodonMaxItems }}"
    </script>

This is how you can pass the i18n translated values of the labels and Hugo’s settings. Same for the profile’s URL. The Javascript will append it with .rss.

The CSS part

Since I wanted it to be properly integrated with the rest of the design, the CSS was made in this way.

/* mastodon feed */

.mastodon-feed {
  margin-left: auto;
  margin-right: auto;
}

.mastodon-feed div.header {
  background-color: var(--header-bg-color);
  max-height: 4.9rem;
  padding: 0.1rem;
  display: flex;
  flex-direction: row;
  align-items: center;
  color: var(--body-font-color);
}

.mastodon-feed div.header img {
  max-width: 45px;
  border-radius: 50%;
  margin: 0.5rem;
  flex: 1;
}

.mastodon-feed div.header a:link, .mastodon-feed div.header a:visited {
  color: var(--a-menu-link-color);
  font-size: large;
  flex: 1;
}

.mastodon-feed div.header a.follow-button {
  flex: 1;
  text-align: center;
  padding: 0.5rem 1rem;
  margin-right: 0.5rem;
  max-width: 5rem;
  color: var(--body-font-color);
  background-color: var(--a-menu-bg-color-hover);
  text-decoration: none;
  border-radius: 5px;
}

.mastodon-feed div.header a.follow-button:hover {
  background-color: var(--a-mastodon-follow-bg-color-hover);
}

.mastodon-feed div.header a:hover {
  text-decoration: none;
}

.mastodon-feed div.status {
  border: 1px solid var(--div-details-border-color);
  padding: 1rem;
  padding-bottom:0;
  margin-bottom: 1rem;
}

.mastodon-feed div.status div.post-footer {
  display: flex;
  flex-direction: row;
  align-items: center;
  border-top: 1px solid var(--div-details-border-color);
  font-size: smaller;
}

Test drive

Let’s boot up a test template with the expected settings (and some random CSS colors) ! I’ve set the value of params.mastodonMaxItems to 2 to ensure the limit applies.

test-fr.png

test-en.png

Et voilΓ  !


πŸ“‘ Table of Contents

πŸ“š Read my latest book

Follow me on Mastodon

🏷️ All Tags πŸ“„ All Posts πŸ—Ί Sitemap RSS Feed