I’ve been running a personal site for a few years now, and every time I sat down to write a new post the workflow felt a little forced. WordPress was the obvious fallback, but the admin UI never quite matched the way I spend my day – bouncing between a terminal, a code editor, and a handful of messaging apps. One rainy afternoon, while scrolling through a chat with a friend, the idea struck: what if the place I already spend most of my time could also be the place I publish from?
The moment the idea clicked
I was on a train, phone in hand, trying to capture a thought about a recent project. Opening my notes app felt clunky, opening the CMS dashboard felt impossible without a laptop. I typed the draft into Telegram, sent it to myself, and thought, that’s a post already. The notion that a bot could act as a thin publishing layer started to feel less like a gimmick and more like a genuine solution to three problems I kept hitting:
- I wanted to write from anywhere, even when I didn’t have a full dev environment.
- I didn’t want to maintain a separate admin interface that duplicated logic already present in my site.
- I needed a publishing workflow that was as lightweight as sending a message.
WordPress, a custom React dashboard, even headless CMS services all introduced friction I wasn’t willing to pay. The bot promised a single point of interaction – my phone – and a tiny amount of code to glue it to the static site generator I already use.
Sketching the feature set
I started with a notebook, scribbling commands that felt natural in a chat context. The list grew quickly, and I grouped them into three buckets: draft handling, publishing actions, and asset management.
- /draft – start a new post, receive a temporary ID.
- /title – set or update the title.
- /body – append or replace the main content.
- /slug – generate a URL‑friendly slug, with an optional custom override.
- /image – upload a picture, get back a CDN URL.
- /schedule – pick a future date, store the timestamp.
- /publish – push the post to the repo and trigger a build.
Seeing the list on paper made it clear that the bot would need a tiny state machine per user, persisting drafts somewhere safe. I chose a simple JSON file per draft stored in a private GitHub repository – no database, no extra hosting, just the same place my site’s source lives.
Building the skeleton
The first prototype was a Node.js script using the telegraf library. I kept the code under 150 lines, just enough to listen for messages, parse commands, and write JSON files. The real magic happened when I hooked the repository’s push event to my Netlify build hook. Every time the bot wrote a new post.json, Netlify rebuilt the site, and the fresh article appeared instantly.
const { Telegraf } = require('telegraf')
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.command('draft', ctx => {
const id = Date.now().toString()
// create empty draft file in repo
ctx.reply(`Draft started – ID ${id}. Use /title to set a heading.`)
})
bot.launch()
I wasn’t trying to build a full‑featured CMS, just a thin layer that let me type, edit, and push. The simplicity was deceptive; each command had edge cases that surfaced later.
The rough patches
Draft persistence
At first I wrote drafts directly to the repo’s drafts/ folder. The first time I tried to edit a draft from two different devices, a merge conflict erupted. Git isn’t built for rapid, concurrent writes from a bot. The fix was to move drafts to a private Gist, which gave me a REST endpoint with versioning and eliminated the conflict window.
Image handling
Telegram lets you send photos, but the files come as temporary URLs. I attempted to forward those URLs straight to Cloudinary, assuming a one‑liner would work. The API rejected the request because the file wasn’t publicly reachable. The workaround was to download the image to the server first, then upload it. That added a few seconds of latency, but the user experience remained acceptable.
Scheduling quirks
I wanted /schedule to accept natural language like “tomorrow at 9am”. I tried the chrono-node package, but its parsing sometimes mis‑interpreted time zones. After a few missed posts, I switched to an explicit ISO format, sacrificing convenience for reliability.
Moments I almost turned back
There were two points where I questioned the whole approach. The first came after the image upload saga; I wondered if a static site was the right home for a bot‑driven workflow. The second arrived when the Gist rate limit throttled my drafts during a weekend writing sprint. Each time I considered falling back to a conventional admin UI, I reminded myself why I started: the joy of publishing from a single place I already trusted.
Both setbacks turned into learning moments. The image bug taught me to treat external services as black boxes and write adapters that could be swapped later. The rate‑limit issue nudged me toward caching draft IDs locally on the bot server, dramatically reducing API calls.
What worked better than expected
- Zero UI friction – typing a command felt as natural as sending a message. No mouse, no login screen.
- Version control as a database – storing drafts in Git (or Gist) gave me a full history without adding a new system.
- Instant feedback – the bot could reply with a preview link right after
/publish, confirming that the post lived where I expected.
What I would change next time
If I were to rebuild this today, I’d probably:
- Replace the Gist store with a lightweight key‑value service like Supabase, giving me real‑time sync and higher rate limits.
- Add a simple markdown preview inside Telegram using the
sendMessagemarkdown mode, so I could see formatting before publishing. - Wrap the command parsing in a tiny state‑machine library to make adding new commands less error‑prone.
A broader take‑away
The whole experiment reminded me that the “right” tool is often the one that lives already in your daily workflow. I didn’t need a polished admin panel; I needed a bridge between the place I think and the place my site lives. Building that bridge forced me to confront assumptions about what a CMS must look like, and the answer turned out to be a handful of chat commands.
If you find yourself fighting a tool because it doesn’t match how you work, ask yourself whether a small, custom layer could solve the friction. You might end up with something that feels less like a product and more like an extension of your own habits.
Closing thoughts
Developers love to champion the latest framework or platform, but sometimes the best solution is the one you cobble together from the services you already love. My Telegram bot isn’t perfect, but it lets me write, edit, and publish without ever leaving a conversation. That simplicity is a reminder: question the conventions that feel comfortable for the crowd, and build what feels comfortable for you.