DEV Community

Cover image for Version-Controlled omg.lol: Auto-Syncing Your IndieWeb with GitHub Actions
Brennan K. Brown
Brennan K. Brown

Posted on

Version-Controlled omg.lol: Auto-Syncing Your IndieWeb with GitHub Actions

There's something genuinely delightful about omg.lol. For $20/year, you get a profile page, blog, Mastodon instance, email forwarding, paste hosting, URL shortening, image hosting, and moreβ€”all without surveillance capitalism or dark patterns. It's the kind of service that makes you remember when the Internet was fun.

But as a developer who lives in Git, I wanted more. I wanted every piece of my omg.lol presence version-controlled, with automatic deployment on push. No more copy-pasting into web UIs. No more wondering "wait, what did this look like two weeks ago?"

So I built it. My entire omg.lol ecosystem now lives in a GitHub repository with automatic syncing via GitHub Actions. Every profile update, blog template change, status page edit, all tracked in Git and automatically deployed.

Here's how it works.

One Repo to Rule Them All

The goal was simple: everything that can be synced via the omg.lol API should automatically sync when pushed to GitHub.

This includes:

  • 🌐 Profile page (content, CSS, custom <head>)
  • πŸ“ Weblog (configuration + 4 templates)
  • ⏰ Now page (/now content)
  • πŸ“‹ Paste files (humans.txt, robots.txt, security.txt, .plan)
  • πŸ’¬ Statuslog (bio, CSS, custom <head>)

Each of these lives in its own directory with its own GitHub Actions workflow. Push to any directory, and it syncs automatically.

Repository Structure

omg.lol/
β”œβ”€β”€ .github/workflows/
β”‚   β”œβ”€β”€ sync-profile.yml
β”‚   β”œβ”€β”€ sync-weblog.yml
β”‚   β”œβ”€β”€ sync-now.yml
β”‚   β”œβ”€β”€ sync-pastes.yml
β”‚   └── sync-statuslog.yml
β”œβ”€β”€ web/
β”‚   β”œβ”€β”€ main.md           # Profile content
β”‚   β”œβ”€β”€ custom.css        # Profile styles
β”‚   β”œβ”€β”€ head.html         # Custom <head>
β”‚   └── now.md            # /now page
β”œβ”€β”€ weblog/
β”‚   β”œβ”€β”€ config.yml
β”‚   β”œβ”€β”€ landing-page-template.html
β”‚   β”œβ”€β”€ main-template.html
β”‚   β”œβ”€β”€ page-template.html
β”‚   β”œβ”€β”€ post-template.html
β”‚   └── weblog.css        # Hosted externally
β”œβ”€β”€ paste/
β”‚   β”œβ”€β”€ humans.txt
β”‚   β”œβ”€β”€ robots.txt
β”‚   β”œβ”€β”€ security.txt
β”‚   └── .plan
└── statuslog/
    β”œβ”€β”€ bio.md
    β”œβ”€β”€ custom.css
    └── head.html
Enter fullscreen mode Exit fullscreen mode

Clean. Organized. Every file has a purpose.

The omg.lol API

omg.lol has a well-documented API that covers nearly everything you can do in the web UI. The endpoints we'll use:

  • POST /address/{address}/web - Update profile
  • POST /address/{address}/now - Update now page
  • POST /address/{address}/weblog/configuration - Update weblog config
  • POST /address/{address}/weblog/template/{name} - Update weblog templates
  • POST /address/{address}/pastebin/ - Create/update pastes
  • POST /address/{address}/statuses/bio - Update statuslog bio

All endpoints require a Bearer token (your API key), which we'll store as a GitHub secret.

Setting Up the API Secret

First, get your omg.lol API key from your account settings. Then add it to your GitHub repo:

  1. Go to Settings β†’ Secrets and variables β†’ Actions
  2. Click New repository secret
  3. Name: OMG_LOL_API_KEY
  4. Value: Your API key
  5. Save

This lets workflows access the key via ${{ secrets.OMG_LOL_API_KEY }} without exposing it in your code.

Workflow 1: Profile Sync

The profile workflow is the most complex because it syncs three files simultaneously: content, CSS, and custom head HTML.

name: Sync Profile to omg.lol

on:
  push:
    branches: [main, master]
    paths:
      - 'web/**'
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Sync profile content
        run: |
          CONTENT=$(cat web/main.md)
          CSS=$(cat web/custom.css)
          HEAD=$(cat web/head.html)

          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            --header "Content-Type: application/json" \
            "https://api.omg.lol/address/brennan/web" \
            --data "$(jq -n \
              --arg content "$CONTENT" \
              --arg css "$CSS" \
              --arg head "$HEAD" \
              '{publish: true, content: $content, css: $css, head: $head}')"
Enter fullscreen mode Exit fullscreen mode

Key details:

  • paths: - 'web/**' means the workflow only triggers on changes to the web directory
  • workflow_dispatch lets you manually trigger it from the Actions tab
  • jq -n constructs proper JSON from the file contents
  • --fail-with-body shows API error messages if something goes wrong
  • Replace brennan with your omg.lol address

Workflow 2: Weblog Sync

The weblog requires multiple API calls: one for config, one for each template.

name: Sync Weblog to omg.lol

on:
  push:
    branches: [main, master]
    paths:
      - 'weblog/**'
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Sync weblog configuration
        run: |
          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            "https://api.omg.lol/address/brennan/weblog/configuration" \
            --data-binary "@weblog/config.yml"

      - name: Sync Landing Page Template
        run: |
          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            "https://api.omg.lol/address/brennan/weblog/template/landing-page-template" \
            --data-binary "@weblog/landing-page-template.html"

      - name: Sync Main Template
        run: |
          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            "https://api.omg.lol/address/brennan/weblog/template/main-template" \
            --data-binary "@weblog/main-template.html"

      - name: Sync Page Template
        run: |
          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            "https://api.omg.lol/address/brennan/weblog/template/page-template" \
            --data-binary "@weblog/page-template.html"

      - name: Sync Post Template
        run: |
          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            "https://api.omg.lol/address/brennan/weblog/template/post-template" \
            --data-binary "@weblog/post-template.html"
Enter fullscreen mode Exit fullscreen mode

Template naming matters: The API path /weblog/template/landing-page-template must match the template's internal Title: Landing Page Template metadata. Keep them consistent.

Workflow 3: Now Page Sync

Simple and beautiful:

name: Sync Now Page to omg.lol

on:
  push:
    branches: [main, master]
    paths:
      - 'web/now.md'
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Sync now page
        run: |
          CONTENT=$(cat web/now.md)

          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            --header "Content-Type: application/json" \
            "https://api.omg.lol/address/brennan/now" \
            --data "$(jq -n \
              --arg content "$CONTENT" \
              '{content: $content, listed: "1"}')"
Enter fullscreen mode Exit fullscreen mode

The listed: "1" parameter makes your now page appear on nownownow.com, Derek Sivers' directory of now pages.

Workflow 4: Paste Files Sync

Special paste files (humans.txt, robots.txt, security.txt, .plan) need individual uploads:

name: Sync Paste Files to omg.lol

on:
  push:
    branches: [main, master]
    paths:
      - 'paste/**'
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Sync humans.txt
        run: |
          CONTENT=$(cat paste/humans.txt)

          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            --header "Content-Type: application/json" \
            "https://api.omg.lol/address/brennan/pastebin/" \
            --data "$(jq -n \
              --arg content "$CONTENT" \
              '{title: "humans.txt", content: $content}')"

      # Repeat for robots.txt, security.txt, .plan
Enter fullscreen mode Exit fullscreen mode

These files then become accessible at special endpoints like brennan.omg.lol/humans.txt.

Workflow 5: Statuslog Sync

Statuslog can have custom bio content, CSS, and head HTML:

name: Sync Statuslog to omg.lol

on:
  push:
    branches: [main, master]
    paths:
      - 'statuslog/**'
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Sync statuslog bio
        run: |
          CONTENT=$(cat statuslog/bio.md)
          CSS=$(cat statuslog/custom.css)
          HEAD=$(cat statuslog/head.html)

          curl --fail-with-body --location --request POST \
            --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
            --header "Content-Type: application/json" \
            "https://api.omg.lol/address/brennan/statuses/bio" \
            --data "$(jq -n \
              --arg content "$CONTENT" \
              --arg css "$CSS" \
              --arg head "$HEAD" \
              '{content: $content, css: $css, head: $head}')"
Enter fullscreen mode Exit fullscreen mode

The Developer Experience

Once set up, the workflow is effortless:

# Update your profile
vim web/main.md
git add web/main.md
git commit -m "Update about section"
git push

# GitHub Actions automatically syncs it to omg.lol
# Check the Actions tab to see the workflow run
Enter fullscreen mode Exit fullscreen mode

No more:

  • Opening multiple web UIs
  • Copy-pasting between local files and web forms
  • Wondering what changed
  • Losing track of old versions

Everything lives in Git. Every change is tracked. Every deployment is automatic.


omg.lol is different. It's small by design, built by Adam Newbold who isn't chasing unicorn valuations or VC money. He's just building good tools for people who care about the independent web.

And because omg.lol provides a proper API, we can treat it like infrastructure. Version-controlled infrastructure. GitOps for your personal web presence.

This is what the independent web looks like: tools that respect you enough to let you own your workflow.

Performance Notes

Each workflow runs in ~30 seconds. The API calls are fast. GitHub Actions provides 2,000 free minutes per month for private repos (unlimited for public), so unless you're pushing hundreds of times daily, you'll never hit limits.

Edge Cases

Line endings: If you're on Windows, configure Git to preserve Unix line endings:

git config --global core.autocrlf input
Enter fullscreen mode Exit fullscreen mode

Large files: The API has reasonable size limits. Don't put megabyte-sized paste files in your repo.

Template metadata: Weblog template files need proper metadata at the top:

Type: Template
Title: Landing Page Template
Enter fullscreen mode Exit fullscreen mode

The Title must match the API endpoint name exactly.

Character encoding: Everything should be UTF-8. The API handles it correctly, but double-check your editor settings.

Stylesheet as a Paste

Initially, I kept weblog.css in the weblog/ directory and planned to manually upload it to paste.lol whenever it changed. Then I realized: paste.lol files are just pastes. Why not move the stylesheet to the paste/ directory and let it auto-sync?

- name: Sync stylesheet.css
  run: |
    CONTENT=$(cat paste/stylesheet.css)

    curl --fail-with-body --location --request POST \
      --header "Authorization: Bearer ${{ secrets.OMG_LOL_API_KEY }}" \
      --header "Content-Type: application/json" \
      "https://api.omg.lol/address/brennan/pastebin/" \
      --data "$(jq -n \
        --arg content "$CONTENT" \
        '{title: "stylesheet.css", content: $content}')"
Enter fullscreen mode Exit fullscreen mode

Now my weblog templates reference https://brennan.paste.lol/stylesheet.css/raw, and every CSS change auto-syncs. No exceptions. No manual steps.

This is the kind of thinking that makes automation actually work: question every manual step. If something feels tedious, there's (hopefully) an API endpoint for it.

What I Can't Auto-Sync (Yet)

Some omg.lol features don't have API endpoints (or I haven't found them):

  • Individual weblog posts (these are managed through the web UI or email)
  • Profile picture uploads (supported, but I haven't implemented it)
  • DNS records (manageable via API, but I do it rarely enough that manual is fine)
  • PURL creation/management (API exists, might add this later)

The Bigger Picture

Twenty dollars a year gets you:

  • Profile page + blog + Mastodon instance
  • Email forwarding + paste hosting + URL shortening
  • Image hosting + code hosting + IRC + XMPP
  • No ads. No surveillance. No dark patterns.

And now, with this setup, everything is version-controlled and auto-deployed.

This is what happens when you combine ethical services with open APIs and workflows. You get actual ownership. Real control.

Get Started

  1. Sign up for omg.lol ($20/year)
  2. Clone my repo as a template
  3. Add your API key as a GitHub secret
  4. Update the workflows with your omg.lol address
  5. Push changes and watch the magic happen

The independent web is alive. The tools exist. The community thrives.

All you have to do is join it.


Brennan Kenneth Brown is a Queer MΓ©tis author and web developer based in Calgary, Alberta. He founded Write Club, a creative collective that has raised funds for literacy nonprofits. He runs Berry House, a values-driven studio building accessible JAMstack websites while offering pro bono support to marginalized communities.

Find me: omg.lol profile | weblog | statuslog | Mastodon | GitHub

Support my work: Ko-fi | Patreon | GitHub Sponsors

Top comments (0)