How I Build a Telegram-Powered Blog with Cloudflare

Have you ever wanted a lightweight blog that doesn’t need WordPress or any complex CMS? What if you could just send a Telegram message and instantly publish a blog post online?
That’s exactly what we’re building today — a Telegram-powered blog hosted on Cloudflare Workers + KV + Pages. It’s fast, serverless, and FREE.
✅ Why Use Telegram as a CMS?
Managing a blog should be simple. With Telegram:
- ✅ Write posts anywhere (desktop or phone)
- ✅ Add images & captions in one click
- ✅ No logins, no dashboards — just message your bot
- ✅ Your blog updates instantly
No more heavy CMS systems or clunky dashboards.
✅ How It Works
This setup is serverless and has only 3 moving parts:
- Telegram Bot → where you send your posts
- Cloudflare Worker + KV → stores your posts as JSON
- Tailwind Frontend → a static page that fetches posts
Whenever you send your bot a message:
Telegram → Worker → Cloudflare KV → /posts.json → Blog Frontend
Your blog updates in real-time.
✅ Step 1: Create a Telegram Bot
- Open @BotFather
- Run
/newbot
, give it a name & username - Copy the Bot Token (looks like
123456:ABC-XYZ...
) - Get your Telegram User ID (send
/start
to @userinfobot)
You’ll need both the token & your ID for the Worker.
✅ Step 2: Set Up Cloudflare Worker
We’ll use a Cloudflare Worker to:
- Receive Telegram webhook updates
- Save posts to Cloudflare KV
- Serve
/posts.json
for the frontend
2.1 Create a KV Namespace
Go to Cloudflare Dashboard → Workers KV and create a namespace called TELEGRAM_BLOG
.
2.2 Create Worker & Bind KV
Create a new Worker, bind TELEGRAM_BLOG
as KV, and add Environment Variables:
BOT_TOKEN = your_telegram_bot_token
AUTHORIZED_USER_ID = your_telegram_user_id
2.3 Worker Code
Paste this code:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Allow CORS
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
// Serve posts.json
if (url.pathname === "/posts.json") {
const postsRaw = await env.TELEGRAM_BLOG.get("posts");
return new Response(postsRaw || "[]", {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// Handle Telegram Webhook
if (request.method === "POST") {
const data = await request.json();
if (!data.message) return new Response("No message", { status: 200 });
const msg = data.message;
const senderId = msg.from.id.toString();
if (senderId !== env.AUTHORIZED_USER_ID) {
return new Response("Unauthorized", { status: 200 });
}
const text = msg.text || msg.caption || "Untitled post";
const lines = text.split("\n");
const title = (lines[0] || "Untitled").trim();
const content = lines.slice(1).join("\n").trim();
const date = new Date().toISOString();
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
let imageUrl = null;
if (msg.photo) {
const photo = msg.photo[msg.photo.length - 1];
const fileId = photo.file_id;
const fileInfo = await fetch(
`https://api.telegram.org/bot${env.BOT_TOKEN}/getFile?file_id=${fileId}`
).then(r => r.json());
if (fileInfo.ok) {
imageUrl = `https://api.telegram.org/file/bot${env.BOT_TOKEN}/${fileInfo.result.file_path}`;
}
}
let posts = JSON.parse(await env.TELEGRAM_BLOG.get("posts") || "[]");
posts.push({ title, content, date, slug, image: imageUrl });
await env.TELEGRAM_BLOG.put("posts", JSON.stringify(posts));
return new Response("Post saved", { status: 200 });
}
return new Response("OK", { headers: { "Access-Control-Allow-Origin": "*" } });
}
};
Deploy the Worker.
✅ Step 3: Connect Telegram Webhook
Now tell Telegram where to send messages:
curl -F "url=https://your-worker-url.workers.dev" \
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook
Every message you send to your bot now hits the Worker.
✅ Step 4: Frontend Blog with Tailwind
We’ll build a static HTML page that fetches /posts.json
and displays posts.
Save this as index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Telegram Blog</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = { darkMode: 'class' }</script>
</head>
<body class="bg-gray-50 text-gray-800 dark:bg-gray-900 dark:text-gray-100">
<!-- Sticky Navbar -->
<nav class="sticky top-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur shadow z-50">
<div class="max-w-6xl mx-auto flex justify-between items-center px-4 py-4">
<h1 class="text-2xl font-bold">My Telegram Blog</h1>
<div class="flex gap-3">
<input id="searchInput" placeholder="Search..." class="px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 dark:bg-gray-700 dark:text-white">
<button id="darkToggle" class="p-2 rounded-md bg-gray-200 dark:bg-gray-700">🌙</button>
</div>
</div>
</nav>
<!-- Posts Container -->
<main class="max-w-6xl mx-auto px-4 py-10">
<div id="posts" class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">Loading posts...</div>
</main>
<footer class="text-center py-6 text-gray-400 dark:text-gray-500">Powered by Telegram + Cloudflare Workers</footer>
<script>
let allPosts = [];
async function loadPosts() {
const res = await fetch('/posts.json');
const posts = await res.json();
allPosts = posts.sort((a,b)=> new Date(b.date)-new Date(a.date));
renderPosts(allPosts);
}
function renderPosts(posts) {
const container = document.getElementById('posts');
container.innerHTML = '';
if(posts.length===0){
container.innerHTML='<p class="col-span-full text-gray-400">No posts yet.</p>';
return;
}
posts.forEach(post=>{
const div=document.createElement('article');
div.className='bg-white dark:bg-gray-800 rounded-xl shadow-md p-5 hover:shadow-lg flex flex-col';
div.innerHTML=`
${post.image?`<img src="${post.image}" class="rounded-lg mb-4">`:''}
<h2 class="text-xl font-semibold mb-2">${post.title}</h2>
<p class="text-sm text-gray-500 mb-3">${new Date(post.date).toLocaleString()}</p>
<p class="text-gray-700 dark:text-gray-300">${post.content.replace(/\n/g,'<br>')}</p>
`;
container.appendChild(div);
});
}
document.getElementById('searchInput').addEventListener('input',e=>{
const q=e.target.value.toLowerCase();
const filtered=allPosts.filter(p=>p.title.toLowerCase().includes(q)||p.content.toLowerCase().includes(q));
renderPosts(filtered);
});
const darkToggle=document.getElementById('darkToggle');
if(localStorage.theme==='dark'){document.documentElement.classList.add('dark');}
darkToggle.addEventListener('click',()=>{
document.documentElement.classList.toggle('dark');
localStorage.theme=document.documentElement.classList.contains('dark')?'dark':'light';
});
loadPosts();
</script>
</body>
</html>
Deploy this page on Cloudflare Pages or any static host.
✅ Step 5: Publish a Post
Now, send a message to your Telegram bot:
My First Blog Post
This is my first post powered by Telegram + Cloudflare!
It will:
- Save in Cloudflare KV
- Update
/posts.json
- Instantly show on your blog frontend!
Add an image? Just send a photo with caption — it will also appear in your post.
✅ Why This Is Awesome
- Zero cost: Free Telegram + Cloudflare Workers + Pages
- Serverless: No hosting, no database to manage
- Instant publishing: Post from anywhere
- Beautiful frontend: Tailwind + dark mode + search