Why I Built My Own Blog Publishing System
I wanted to start writing more technical posts, but I immediately ran into a problem I didn't really like.
If I wrote everything directly on Medium or DEV, those platforms effectively became the main home for my writing. I wanted my own website to be the original source, while still being able to publish to other platforms without rewriting and copying everything manually.
So instead of installing a full CMS, I built a small publishing system directly into my portfolio.
What I actually needed
The requirements were pretty simple:
- Write posts in Markdown
- Save drafts
- Publish them to my portfolio
- Automatically publish a copy to DEV
- Keep my website as the canonical source
- Make publishing to Medium quick without trying to automate their entire website
I intentionally didn't build user accounts, comments, roles, analytics, scheduling, a block editor, or any of the other stuff that usually comes with a CMS.
There is one user: me.
That made the architecture a lot simpler.
The stack
My portfolio was already built with Vite, React, React Router, and TypeScript, so I kept the existing frontend instead of rebuilding everything around something like Next.js.
For the backend I added:
Node.js
Express 5
TypeScript
PostgreSQL
node-postgres
Zod
Posts are stored as Markdown in PostgreSQL and rendered on the frontend with react-markdown.
I also stuck with plain parameterized SQL and SQL migration files instead of adding an ORM. I like Drizzle and similar tools, but for a database this small it felt like adding another abstraction without really gaining much.
A tiny private admin area
The portfolio now has a private studio where I can manage posts.
The authentication system is intentionally boring.
There are no accounts or users table. I have one admin password stored as a scrypt hash on the server. Logging in creates a signed HttpOnly session cookie.
From the studio I can:
- Create a post
- Save it as a draft
- Preview the Markdown
- Publish it
- Edit an existing post
- See its DEV publishing status
- Retry publishing to DEV if something fails
Draft posts never appear through the public blog API.
Publishing outside my website
The part I cared about most was making my site the source of truth.
When I publish something, the order looks like this:
Publish locally
↓
Article goes live on my domain
↓
Publish the same Markdown to DEV
↓
DEV points its canonical URL back to my website
The DEV API call is intentionally treated as secondary.
If DEV is down or its API fails, my article still gets published normally. I can retry the DEV sync afterward.
That seems like a small detail, but I didn't want an external platform being able to break publishing on my own website.
For Medium I took an even simpler approach. Instead of trying to automate it with browser scripts, the studio gives me the published URL so I can use Medium's importer.
That takes less than a minute and saves me from maintaining some fragile automation.
RSS, sitemap, and SEO
Since the blog lives directly on my portfolio, I also added the boring stuff that makes it behave like a real blog.
Each article gets its own metadata, canonical URL, Open Graph information, and publish/update dates.
The backend also generates:
/blog/rss.xml
/sitemap.xml
/robots.txt
Only published articles are included.
Moving it onto my homelab
The project originally ran through Cloudflare Workers, but I had recently built a small Ubuntu homelab and wanted to start running more of my own infrastructure.
The final setup is now roughly:
GitHub
↓
Dokploy
↓
Docker build
↓
Node + Express container
↓
PostgreSQL container
Public traffic comes through Cloudflare Tunnel into Dokploy's Traefik proxy, so I don't need to expose the application or PostgreSQL directly to the internet.
PostgreSQL stays on the internal Docker network and the application talks to it using its private service address.
The production Docker image is multi-stage, so the first stage installs everything and builds the React and TypeScript code, while the final runtime image contains only what is actually needed to run the application.
One deployment bug that was actually useful
I also managed to waste some time debugging what looked like a backend routing problem.
Every endpoint worked like this:
/api/health → index.html
/api/blog/posts → index.html
At first it looked like Express was accidentally letting the SPA fallback catch all of the API routes.
It wasn't.
After checking the process inside the production container, I found this:
caddy run --config /assets/Caddyfile
Express wasn't running at all.
Dokploy had deployed the project as a static frontend, so Caddy was happily serving the Vite build while the compiled backend was sitting inside the image doing absolutely nothing.
Switching the application to build directly from my Dockerfile fixed it.
It was a good reminder that once Docker, reverse proxies, tunnels, and application routing are involved, it's usually faster to test each layer individually instead of guessing which one is broken.
Was building this necessary?
Probably not.
I could have just written directly on DEV or Medium.
But this gives me something I wanted more: control over the original content and a publishing workflow that fits how I actually work.
I can write an article once, publish it on infrastructure I control, and still use larger platforms for discovery.
More importantly, I deliberately stopped once that workflow worked.
I didn't want to spend three weeks building a CMS just so I could eventually start writing blog posts.
The whole point was to build the tool, then get out of its way and actually use it.











