How I Build a Telegram-Powered Blog with Cloudflare

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.

My Telegram Blog

✅ 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:

  1. Telegram Bot → where you send your posts
  2. Cloudflare Worker + KV → stores your posts as JSON
  3. 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

  1. Open @BotFather
  2. Run /newbot, give it a name & username
  3. Copy the Bot Token (looks like 123456:ABC-XYZ...)
  4. 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

Read more