Building a Seamless Writing Pipeline with Hedgedoc, Jekyll, and GitHub OAuth

infrascratchpaddevops

Blog Posting and Markdown Web Editor

Building a Seamless Functional Blog Posting Pipeline Rube Goldberg machine with Hedgedoc, Jekyll, and GitHub OAuth

One of the goals for Scratchpad was simple:
let anyone on the team write a blog post without touching Git, YAML front‑matter, or terminal workflows—while keeping everything private and secure.

This post walks through how we built a full writing pipeline around Hedgedoc, Jekyll, OAuth2 Proxy, and a tiny publisher microservice that syncs posts into the blog automatically.


Why We Needed This

We wanted:

  • A friendly, real‑time Markdown editor (Hedgedoc)
  • Private access controlled through GitHub login
  • Zero Git knowledge required from writers
  • Automatic syncing → Jekyll _posts/ folder
  • A thin, maintainable backend without inventing a CMS

Hedgedoc checked most boxes, but we added authentication and a publishing workflow around it to make everything cohesive.


System Overview

Team Member → md.scratchpad.lol → OAuth2 Login → Hedgedoc  
                       │
                       └── publisher microservice (8090)
                              │
                              └── writes markdown → project-blog/_posts/
                                     │
                                     └── Jekyll builds the site

Everything runs on a single VPS using Docker, Nginx, and LetsEncrypt SSL.


Authentication with GitHub + OAuth2 Proxy

Hedgedoc supports GitHub auth but doesn’t let us restrict who can log in.
That’s where oauth2-proxy comes in.

We configured oauth2-proxy to:

  • use GitHub as the OAuth provider
  • check a whitelist of approved emails (/etc/oauth2-proxy/emails.txt)
  • serve our custom sign-in and error pages
  • proxy requests to Hedgedoc internally

Custom templates live here:

project-blog/assets/auth-page/templates/
├── sign_in.html
└── error.html

Brand assets are served under:

scratchpad.lol/auth-assets/

The login screen now matches our Scratchpad branding with a clean, minimal UI.


Reverse Proxy Structure

We expose two subdomains:

Purpose Domain Points To
Public blog scratchpad.lol Jekyll (port 4000)
Writer’s lab md.scratchpad.lol oauth2-proxy → Hedgedoc

Nginx handles SSL, proxying, and static auth assets.


The Publisher Microservice

To avoid teaching every author how to commit Markdown into Jekyll, we wrote a small microservice using FastAPI.

Location:

project-blog/tools/publisher/

Responsibilities:

  1. Pull a note from Hedgedoc
  2. Convert it into valid Jekyll Markdown
  3. Inject required front‑matter
  4. Save it into _posts/
  5. Commit changes to the repo (optional for automation)

Automatic Markdown Header Injection

Jekyll posts require YAML front‑matter like:

---
title: "My Post Title"
date: 2025-01-18 12:00:00
tags: []
---

The publisher service handles this automatically.

What the publisher does

  1. Takes the Hedgedoc title → becomes title:
  2. Generates date:
  3. Prepends YAML to the final Markdown file

Example output:

---
title: "Title From Hedgedoc"
date: 2025-01-18 12:00:00
tags: []
---

# Title From Hedgedoc
(rest of the Markdown)

Writers never touch YAML.

Publishing error (Edit 02/03/2026)

Seems from time to time, I get an error when publishing:

publish AVlDyzUhA
{
  "detail": "HedgeDoc returned 403 for http://hedgedoc:3000/AVlDyzUhA/download"
}

Publishing error fixed (Edit 02/17/2026)

I fixed the error. The publishing tool (fast api server) didn’t have access to hedgedoc because it was being blocked by either nginx or hedgedoc was blocking. I changed the hedgedoc environment configuration for

CMD_ALLOW_ANONYMOUS: "true"

And now it works. Running the following command returns ok status:

In order to publish, you need to make sure your file is not Private. I suggest LOCKED as a default to make sure nobody edits your file but the publisher can see the hedgedoc file. See the image below for context:


Publish / Unpublish Commands

Two global CLI helpers were added.:

Publish

publish <note-id>

Unpublish

unpublish <note-id>

The note ID comes from the Hedgedoc URL:

https://md.scratchpad.lol/<note-id>

These commands hit the publisher API and sync the file automatically.


Directory Structure Recap

/root/scratchpad/
├── hedgedoc/                      # Docker compose for Hedgedoc & OAuth
│   └── docker-compose.yml
├── project-blog/                  
│   ├── _posts/                    # Auto-updated by publisher
│   ├── assets/auth-page/          # Branding + OAuth templates
│   └── tools/publisher/           # Sync microservice
└── nginx configs / SSL / etc…

The Result

The entire workflow is now seamless:

  1. Writer logs in via GitHub
  2. Opens Hedgedoc
  3. Writes
  4. Runs:

    publish <note-id>
    

And the blog updates instantly—no friction, no YAML, no Git.

Uploading Images

(Edit - Jan 13th 2025) In order to upload images, we need to enable in the docker-compose.yml file using the following ENVIRONMENT vars (I’m lazy just ran them straight in line). We also need to add a config.json specifically to specify the uploads path. And we do this by touching a file in the root hedgedoc dir, mounting the file as volume.

The settings say that only registered users may upload, the files will store on the filesystem (why we need the path), point to our config file with our added volume, and give all uploads readable permissions for jekyll and nginx to read the file.

docker-compose.yaml:

services:
...
  hedgedoc:
    image: quay.io/hedgedoc/hedgedoc:latest
    environment:
    # ...
      CMD_ENABLE_UPLOADS: registered
      CMD_IMAGE_UPLOAD_TYPE: filesystem
      CMD_CONFIG_FILE: /hedgedoc/config.json
      UPLOADS_MODE: "0755" # file perms so jekyll/nginx can read upload files. 
    
    volumes:
      - /root/scratchpad/project-blog/_posts:/hedgedoc/_posts
      - /root/scratchpad/project-blog/assets/uploads/public:/hedgedoc/_uploads
      - ./config.json:/hedgedoc/config.json:ro

What does 0755 mean?

  • In one sentence, it means the owner can do everything; everyone else can read and access, but not modify.

  • The following is a more comprehensive breakdown:

0755 means:

0    = no special permissions

Owner (7 = rwx)
- can read
- can write
- can access / run

Group (5 = r-x)
- can read
- cannot write
- can access / run

Others (5 = r-x)
- can read
- cannot write
- can access / run

I will attempt to upload an image below. If it works, yay.

Edit (02/03/2026) - Uploading images bug

At some point, I was trying to upload images to Hedgedoc by drag and drop, and they wouldn’t upload. Hm. Annoying. First I inspected the webpage console, and immediately saw an error:

inline-attachment.js:274 
 POST https://md.scratchpad.lol/uploadimage 413 (Content Too Large)
m.uploadFile	@	inline-attachment.js:274
m.onDrop	@	inline-attachment.js:400
(anonymous)	@	codemirror.inline-attachment.js:83

Which basically means my file too big. The file was 1MB so if that is too bag, we are cooked. Hedgedoc actually supports for sure 100MB out the box, so something is up.

That really leaves one place for this issue to exist. Our reverse proxy is the bottleneck for file uploads, limiting the max body. We can solve this issue by increasing the max body to 25mb in the nginx sites-available file:

/etc/nginx/sites-available/scratchpad

server {
  listen 443 ssl http2;
  server_name md.scratchpad.lol;

  # need to increase nginx max upload file size to upload to hedgedoc. 
  client_max_body_size 25m;
//.... other stuff
}