Skip to content

Integrate Gitea/Forgejo Stats into Material for MkDocs

One core feature of Material for MkDocs1 is the display of statistics of a linked repository, such as latest release, fork count and star count.
However, this feature is limited to GitHub2 and GitLab3 repositories and other repositories cannot be added the same way as these two.

A prime example is trying to add statistics from a Gitea4 or Forgejo5 hosted repository, which requires some extra steps to be done here. And this post will explain how!

Notes

This guide will use Codeberg.org6 which uses Forgejo for their repository functionality. The Guide should work fine on Gitea, or any other remote Git Host for that matter, but may require some adjustments for things such as the API paths used and/or the usage of Reverse Proxies (more on that later), so keep that in mind.

I will also use the repository of my plugin, AdvancedServerList7, here for the Javascript example. The actual file can be found here.

How does it work?

Adding support to remote Git Hosts works by adding an additional Javascript to your site8 which does the following things:

  1. Fetch the statistics from the remote Git Repository by performing API Fetch calls.
  2. Store the result within a SessionStorage to reuse later, reducing API calls.
  3. Apply the retrieved statistics by modifying specific HTML used for the Repository stats.

The Javascript file

I'll first show the full Javascript file and then go over individual parts that may be of importance.

Disclaimer

I'm no expert with Javascript, so some info I provide here may not be accurate. Please research Javascript yourself to have the right info.

fetch_stats.js
document$.subscribe(async () => {
  const repo_stats_object = document.querySelector('[data-md-component="source"] .md-source__repository');

  async function applyRepoStats(data){
    const facts = document.createElement("ul");
    facts.className = "md-source__facts";

    const version = document.createElement("li");
    version.className = "md-source__fact md-source__fact--version";

    const stars = document.createElement("li");
    stars.className = "md-source__fact md-source__fact--stars";

    const forks = document.createElement("li");
    forks.className = "md-source__fact md-source__fact--forks";

    version.innerText = data["version"];
    stars.innerText = data["stars"];
    forks.innerText = data["forks"];

    facts.appendChild(version);
    facts.appendChild(stars);
    facts.appendChild(forks);

    repo_stats_object.appendChild(facts);
  }

  async function fetchInfo(){
    const [release, repo] = await Promise.all([
      fetch("https://codeberg.org/api/v1/repos/Andre601/AdvancedServerList/releases/latest").then(_ => _.json()),
      fetch("https://codeberg.org/api/v1/repos/Andre601/AdvancedServerList").then(_ => _.json());
    ]);

    const data = {
      "version": release.tag_name,
      "stars": repo.stars_count,
      "forks": repo.forks_count
    };

    __md_set("__git_repo", data, sessionStorage);
    applyRepoStats(data);
  }

  if(!document.querySelector('[data-md-component="source"] .md-source__facts')){
    const cached = __md_get("__git_repo", sessionStorage);
    if((cached != null) && (cached["version"])){
      applyRepoStats(cached);
    }else{
      fetchInfo();
    }
  }
})

Listening for Page changes/loads

Let's start with the very top part:

fetch_stats.js
document$.subscribe(async () => {
  // ...
})

This part sets up a listener where this JS file will listen for document/page changes such as (re)loading a page. This ensures the data is loaded when required. It is also performed asynchronous for performance reasons.

Create Repo Stats Object

fetch_stats.js
const repo_stats_object = document.querySelector('[data-md-component="source"] .md-source__repository');

If you're using the attr_list extension of Python-Markdown, the format inside querySelector may look familiar to you.
This code snippet is creating a new constant object by searching through the page for any HTML object containing data-md-component="source" and class="md-source__repository".

The only component that should have these parts is the source.html partial of Material for MkDocs9, which is used for displaying the repo Statistics.

Apply Stats to Repo Object

fetch_stats.js
async function applyRepoStats(data){
  const facts = document.createElement("ul");
  facts.className = "md-source__facts";

  const version = document.createElement("li");
  version.className = "md-source__fact md-source__fact--version";

  const stars = document.createElement("li");
  stars.className = "md-source__fact md-source__fact--stars";

  const forks = document.createElement("li");
  forks.className = "md-source__fact md-source__fact--forks";

  version.innerText = data["version"];
  stars.innerText = data["stars"];
  forks.innerText = data["forks"];

  facts.appendChild(version);
  facts.appendChild(stars);
  facts.appendChild(forks);

  repo_stats_object.appendChild(facts);
}

This is a rather big one, so I'll explain it in further steps.
What this method does is effectively apply the received data - provided via the data argument - to the repo_stats_object object we've created earlier.

It does this by creating a ul (unordered List) object which itself contains li (List) objects for the latest release, stars count and forks count.

Do note that if you do not want a specific statistic from being displayed, you can simply remove the corresponding code lines.
As an example, assume I don't want to display the latest release, what I would do is remove the following lines of code:

fetch_stats.js
async function applyRepoStats(data){
  const facts = document.createElement("ul");
  facts.className = "md-source__facts";

  const version = document.createElement("li");
  version.className = "md-source__fact md-source__fact--version";

  const stars = document.createElement("li");
  stars.className = "md-source__fact md-source__fact--stars";

  const forks = document.createElement("li");
  forks.className = "md-source__fact md-source__fact--forks";

  version.innerText = data["version"];
  stars.innerText = data["stars"];
  forks.innerText = data["forks"];

  facts.appendChild(version);
  facts.appendChild(stars);
  facts.appendChild(forks);

  repo_stats_object.appendChild(facts);
}

It is important to note, that the className need to match the provided ones, as Material for MkDocs uses those to apply the correct icons to the list element.
This also means that if you want to add or use your own icons, you can do so by creating some CSS stylesheet file and apply the necessary changes.

Fetching Statistics from API

fetch_stats.js
async function fetchInfo(){
  const [release, repo] = await Promise.all([
    fetch("https://codeberg.org/api/v1/repos/Andre601/AdvancedServerList/releases/latest").then(_ => _.json()),
    fetch("https://codeberg.org/api/v1/repos/Andre601/AdvancedServerList").then(_ => _.json());
  ]);

  const data = {
    "version": release.tag_name,
    "stars": repo.stars_count,
    "forks": repo.forks_count
  };

  __md_set("__git_repo", data, sessionStorage);
  applyRepoStats(data);
}

This part of the code is actually responsible for retrieving the statistics from your repository.
In my example is it fetching repository info and the latest release and convert both from JSON to a Javascript object using json().

The fetched info is then put into a data object which is being cached using __md_set(...) and the sessionStorage, but also applied to the site by calling applyRepoStats(...).
You can actually put any data in the data object for as long as the returned JSON contains it.

Please also make sure that the URLs used point to the one of your repository and that it actually matches whatever API URL structure it may have.
And as a final note some info on CORS.

Using Reverse Proxy for CORS issues

Depending on the setup of the repository host could fetch requests fail due to CORS settings.
Codeberg had such issues in the past before they actually changed their backend to have Access-Control-Allow-Origin: * applied, allowing fetch via Javascript10.

With all that said, if you encounter errors related to CORS, you most likely will need to use a Reverse Proxy to bypass this restriction.
The easiest way to do this is by using a site called allorigins.win which was designed to combat such an issue. However, it requires changes to your code, namely the following changes are needed:

fetch_stats.js.diff
@@ -28,8 +28,8 @@
async function fetchInfo(){
  const [release, repo] = await Promise.all([
-    fetch("https://codeberg.org/api/v1/repos/Andre601/AdvancedServerList/releases/latest").then(_ => _.json()),
-    fetch("https://codeberg.org/api/v1/repos/Andre601/AdvancedServerList").then(_ => _.json());
+    fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent('https://codeberg.org/api/v1/repo/Andre601/AdvancedServerList/releases/latest)}`).then(_ => _.json())
+    fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent('https://codeberg.org/api/v1/repo/Andre601/AdvancedServerList)}`).then(_ => _.json())
  ]);
}

We effectively use api.allorigins.win/raw as the fetch URL, which would return the raw content of whatever URL we provide it with, which in our case is the API URLs for the repo info and the latest release info.
It is important that we URL encode our URL, as it is used as a query parameter value, which is why we wrap it in ${encodeURIComponent(...)} (Do also note the usage of backticks instead of double quotes. This is required by Javascript to use ${...} in Strings).

If you do not want to use a 3rd-party site, you'll need to setup a Reverse proxy yourself. This can easily be hosted on services such as vercel.com and there should be tutorials on the internet on how to setup an easy reverse proxy for CORS.

Load and Store Data

fetch_stats.js
if(!document.querySelector('[data-md-component="source"] .md-source__facts')){
  const cached = __md_get("__git_repo", sessionStorage);
  if((cached != null) && (cached["version"])){
    applyRepoStats(cached);
  }else{
    fetchInfo();
  }
}

This final code snippet is what actually calls the other functions.
It checks if the Document does not contain any HTML element with data-md-component="source" and class="md-source__facts".
If no such element is found will it first try to obtain the data from cache using __md_get(...). Should this be successful will it use this cached data and call applyRepoStats(...) directly instead of performing a request.
Should it not find data in the cache will fetchInfo() be called, performing the API requests we talked about earlier.

Final steps

The final step would be to add this Javascript file as an additional Javascript file to your mkdocs.yml file:

mkdocs.yml
extra_javascript:
  - assets/js/fetch_stats.js

Conclusion

While a bit difficult and perhaps even tedious, this aproach should allow you to fetch and display statistics on your MkDocs site using the Material for MkDocs theme.
Please note that while I provided this code, I cannot help with any issues comming from it, as my JS knowledge is fairly limited and this was only done with the help of other community members11.

In fact, if there are ways to improve this, let me know!


Footnotes

Comments

Comment system powered by Mastodon.
Leave a comment using Mastodon or another Fediverse-compatible account.


Last update: 10. February 2025 ()