Skip to content
Menu
Titelbild: Bring your own website into the Fediverse – automatically publish blog posts via GoToSocial

Bring your own website into the Fediverse – automatically publish blog posts via GoToSocial

Step-by-step guide: Bring your own website into the Fediverse with GoToSocial and automatically publish blog posts via RSS to Mastodon & Co.

Articles are translated automatically from German and can be corrected by the editor. Read the German blog

Bring Your Own Website to the Fediverse

What is the Goal?

In this tutorial, I will show you how to connect your website directly to the Fediverse so that new blog posts automatically appear on Mastodon and other federated services.

The goal:

  • Your own blog remains the main platform
  • New posts are automatically published in the Fediverse
  • Readers can follow the blog directly
  • No dependency on Facebook, X, or other platforms

What is the Fediverse?

Simply put, the Fediverse is a decentralized social network.

Instead of a single platform, there are many independent servers that can communicate with each other.

Mastodon is just one of many platforms.

You can think of it like email:

Someone with a Gmail address can write to someone at Posteo.

It works similarly in the Fediverse.

A user on Mastodon can subscribe to, comment on, and share accounts on other servers.

Prerequisites

The following should already be available:

  • Domain
  • VPS or Server
  • Debian or Ubuntu
  • Docker + Docker Compose
  • nginx Reverse Proxy
  • HTTPS via Let’s Encrypt
  • RSS feed of your website

In my case:

  • Hugo website
  • Debian VPS
  • nginx Reverse Proxy
  • GoToSocial as a Single-User Instance

What You Should Already Know

This tutorial is aimed more at technically interested users and self-hosters. You don’t have to be an administrator, but knowing some basics will help enormously.

The following things you should roughly master:

  • Connect to a server via SSH
  • Execute basic Linux commands
  • Edit files with an editor like nano
  • Set DNS entries at the domain provider
  • Adjust nginx configurations
  • Start Docker and Docker Compose
  • Create HTTPS certificates via Let’s Encrypt

If some points still mean nothing to you: No problem. You can still follow the tutorial – you might need to look up individual terms briefly along the way.

Prepare Subdomain

I used my own subdomain:

social.deinedomain.tld

Set an A record at the DNS provider to point to the VPS, if necessary. In my case, this is required because this website is hosted here not on a VPS but with a web hoster. Once we have made the DNS entry, check if it works: dig social.deinedomain.tld

The result should look roughly like this:

1
2
3
4
5
6
7
8
; <<>> DiG <<>> social.deinedomain.tld
;; ->>HEADER<<- opcode: QUERY, status: NOERROR

;; QUESTION SECTION:
;social.deinedomain.tld.   IN   A

;; ANSWER SECTION:
social.deinedomain.tld. 3600 IN A 203.0.113.42

It is important that in ANSWER SECTION your server’s IP address appears.

Install GoToSocial

Create directories:

1
2
3
mkdir -p ~/fediblog/gotosocial
cd ~/fediblog/gotosocial
mkdir config data

Create docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
services:
  gotosocial:
    image: superseriousbusiness/gotosocial:latest
    container_name: gotosocial
    restart: unless-stopped

    ports:
      - "127.0.0.1:3000:8080"

    volumes:
      - ./data:/gotosocial/storage
      - ./config:/gotosocial/config

    environment:
      GTS_HOST: social.deinedomain.tld
      GTS_ACCOUNT_DOMAIN: social.deinedomain.tld
      GTS_PROTOCOL: https

      GTS_TRUSTED_PROXIES: 127.0.0.1/32,172.16.0.0/12

      GTS_DB_TYPE: sqlite
      GTS_DB_ADDRESS: /gotosocial/storage/gotosocial.db

      GTS_STORAGE_BACKEND: local
      GTS_STORAGE_LOCAL_BASE_PATH: /gotosocial/storage

      GTS_WEB_ASSET_BASE_DIR: /gotosocial/web/assets

      GTS_LETSENCRYPT_ENABLED: "false"

      GTS_INSTANCE_TITLE: "Mein Blog"
      GTS_INSTANCE_DESCRIPTION: "Mein Blog im Fediverse"

      TZ: Europe/Berlin

Start: docker compose up -d and also check if it runs: docker ps.

nginx Reverse Proxy

We create the following file: nano /etc/nginx/sites-available/social-blog and put this in there:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
server {
    server_name social.deinedomain.tld;

    location / {
        proxy_pass http://127.0.0.1:3000;

        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Then the whole thing also needs to be activated:

1
2
3
sudo ln -s /etc/nginx/sites-available/social-blog /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

HTTPS via Certbot

Of course, we also insist on https here, so we get a free SSL certificate from letsencrypt:

sudo certbot --nginx -d social.deinedomain.tld

Create User

1
2
3
4
5
6
docker exec -it gotosocial sh

/gotosocial/gotosocial admin account create \
--username blog \
--email deine@mail.de \
--password DEINPASSWORT

We also need admin rights, because without it, it won’t work. We will see the reason later.

1
2
/gotosocial/gotosocial admin account promote \
--username blog
📌 Warning!

There can still be a stumbling block here! And that is that the account is hidden.

In my case, the account was created like this:

1
2
locked = 1
discoverable = 0

However, this caused federation not to work properly. Because the Fediverse is a Federated Network and we want this function so that then every instance, every user in the Fediverse can see and follow our blog.

So we must check the following: sqlite3 ~/fediblog/gotosocial/data/gotosocial.db. Then: SELECT username, locked, discoverable FROM accounts;

If that comes out as it was with me, then we must correct that:

1
2
3
4
UPDATE accounts
SET locked = 0,
    discoverable = 1
WHERE username = 'blog';

Afterwards we start the whole thing once again: docker restart gotosocial.

Create OAuth App

Goto Social works and is reachable via your domain social.deinedomain.tld.

So we switch to the browser and enter https://social.deinedomain.tld in the address bar.

Webinterface von GotoSocial Log in with the created user data (email address and password), then go to Settings -> Application. There a menu opens where we go to New Application to create a new “App”.

Neue App erstellen The name of the app you find out for yourself. Application website (optional) is, as you can see, optional.

What we want is to enter under Redirect URIs (optional, newline-separated entries). It is already stated at the top in the explanation but here again: urn:ietf:wg:oauth:2.0:oob.

Do not use your own URL. That was one of my biggest sources of error.

We have the basic framework ready with that. Now comes the “fine tuning”. 😄

Auto-post RSS

Let’s create a directory again. We haven’t done that in a long time 😁. I will write down the entire next step here:

1
2
3
4
mkdir ~/fediblog/rss-bot
cd ~/fediblog/rss-bot
python3 -m venv venv
source venv/bin/activate
📝 Note

On Debian-based distributions like Debian, Ubuntu or LMDE, the package python3-venv may need to be installed first: sudo apt install python3-venv Then you can create a Python Virtual Environment:

1
2
python3 -m venv venv
source venv/bin/activate

Once that is done, we install something again:

pip install feedparser requests

Auto-post RSS to Fediverse

After the GoToSocial instance is running, only automation is missing.

Goal is:

As soon as a new blog post appears, it will be automatically published in the Fediverse.

Since Hugo already generates an RSS feed (index.xml), we can simply read this and automatically post new posts.

Create Python Virtual Environment

First we create a separate Python environment.

Create directory:

1
2
mkdir -p ~/fediblog/rss-bot
cd ~/fediblog/rss-bot

Create virtual environment:

1
2
python3 -m venv venv
source venv/bin/activate

Note:

On Debian-based distributions like Debian, Ubuntu or LMDE, the following package may need to be installed first:

sudo apt install python3-venv

Install required Python packages:

pip install feedparser requests

Securely Store Access Token

Instead of entering the Access Token directly into the Python script, we use an environment variable.

This is cleaner and more secure—especially if the script is later publicly documented or versioned.

Open file:

nano ~/.bashrc

Add at the very bottom:

export GTS_ACCESS_TOKEN="DEIN_ACCESS_TOKEN"

Reload shell:

source ~/.bashrc

Check:

echo $GTS_ACCESS_TOKEN

If the token is displayed, everything works.

Create Python Script

Create file:

nano rss_to_gts.py

Insert following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import feedparser
import requests
import json
import os
import re

RSS_URL = "https://deinedomain.tld/index.xml"
MASTODON_URL = "https://social.deinedomain.tld"

ACCESS_TOKEN = os.getenv("GTS_ACCESS_TOKEN")

STATE_FILE = "last_post.json"


def load_last():
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE, "r") as f:
            return json.load(f).get("id")
    return None


def save_last(post_id):
    with open(STATE_FILE, "w") as f:
        json.dump({"id": post_id}, f)


def clean_html(text):
    """Entfernt HTML aus RSS Summary"""
    return re.sub(r"<.*?>", "", text)


feed = feedparser.parse(RSS_URL)

if not feed.entries:
    print("Keine RSS Einträge gefunden.")
    exit()

latest = feed.entries[0]
latest_id = latest.get("id", latest.link)

last_id = load_last()

if latest_id == last_id:
    print("Kein neuer Beitrag.")
    exit()

title = latest.title
link = latest.link
summary = clean_html(latest.get("summary", ""))

# Summary kürzen
summary = summary[:300]

tags = []
if "tags" in latest:
    tags = [f"#{t.term.replace(' ', '')}" for t in latest.tags]

hashtags = " ".join(tags)

post = f"""Neuer Beitrag auf meinem Blog

{title}

{summary}

{link}

{hashtags}
"""

response = requests.post(
    f"{MASTODON_URL}/api/v1/statuses",
    headers={
        "Authorization": f"Bearer {ACCESS_TOKEN}"
    },
    data={
        "status": post,
        "visibility": "public"
    }
)

if response.status_code == 200:
    print("Beitrag veröffentlicht.")
    save_last(latest_id)
else:
    print(response.text)

How does the script work?

The script does the following:

  1. Fetch RSS feed of your website
  2. Determine latest post
  3. Check if it has already been posted
  4. Remove HTML from the summary
  5. Automatically publish the post
  6. Save ID locally to prevent duplicate posts

The file:

last_post.json

remembers the last published post.

This prevents duplicate posts from being created.

Deliberately Use Hugo Summary

A small practical tip:

The script uses:

summary = ""

from your Hugo Frontmatter.

Manage this value deliberately.

Why?

The summary will later become your preview text in the Fediverse.

A good summary usually leads to significantly more interactions than automatically truncated text.

Example:

summary = "So bringst du deine Webseite ins Fediverse und veröffentlichst neue Beiträge automatisch über GoToSocial."

Test Script

Manually start:

python rss_to_gts.py

Possible output:

Beitrag veröffentlicht.

Or:

Kein neuer Beitrag.

Run Automatically via Cronjob

Open cron job:

crontab -e

Add entry:

*/10 * * * * cd /home/USER/fediblog/rss-bot && /home/USER/fediblog/rss-bot/venv/bin/python rss_to_gts.py >> bot.log 2>&1

This checks the RSS feed every 10 minutes.

New posts then appear automatically in the Fediverse.

Final Steps

We are slowly approaching the end of this tutorial. If you have everything running without problems up to here, you now have your blog integrated into the Fediverse via Active Pub. Your page can now be seen from all instances on Mastodon, and every user can follow it.

At the end, we test the federation one more time by opening the page https://social.deinedomain.tld/@blog and searching for the site via the search function on Mastodon @blog@social.deinedomain.tld.

If new posts appear, the federation is working.

Conclusion

With a small GoToSocial instance, you can directly integrate your own website into the Fediverse.

New posts appear automatically in the Fediverse and readers can follow the blog directly—regardless of which platform they use.

For me, this is not just a technical gimmick, but also a step towards digital sovereignty.

Instead of publishing content exclusively on central platforms like Facebook, Instagram, or X, you retain control over your own infrastructure, your own content, and your own reach.

This concretely means:

  • own domain instead of platform dependency
  • open standards instead of proprietary systems
  • no algorithmic reach management
  • no forced profile creation for advertising purposes
  • less tracking and data collection

Of course, the Fediverse is not a completely anonymous space, and federation always also implies data exchange between instances. The decisive difference is however:

You can decide yourself whom you trust, which software you use, and where your own data lies.

Especially for blogs around Open Source, Linux, data protection, or digital sovereignty, this is an exciting alternative to classic social networks.

And perhaps that is exactly the real strength:

Your own website remains the center—social networks are used only for distributing content, instead of becoming the sole platform.