Skip to content

Live Blogs

This guide shows how to wire a real-time Live Blog on your frontend using Socket.IO. You do not need API access or an API key to implement this.

In this guide, example JavaScript code is provided. For non-web platforms, a similar mechanism can be replicated depending on your language of choice.

A Live Blog widget that:

  • Renders initial messages on page load
  • Subscribes to real‑time updates over WebSocket
  • Adds basic share buttons for each message
  • Highlights urgent messages and de‑duplicates by message id

Install the Socket.IO client in your app:

Terminal window
npm i socket.io-client

The script targets a container with id chat and a .messages list inside.

<section class="liveBlog" aria-label="Live updates">
<div id="chat" data-time="0">
<div class="messages">
<!-- Optional: server-rendered messages, newest first -->
<!-- Each message should look like the structure below. The script will also create this structure when new messages arrive. -->
<!--
<div class="message" data-id="123">
<div class="message-header">
<div class="time-details"><span class="time">12:30 UTC</span></div>
<ul class="social-share"></ul>
</div>
<div class="message-container">
<img src="..." alt="">
<p class="image_caption">Optional caption</p>
<p>Message text…</p>
</div>
</div>
-->
</div>
</div>
<!-- Add your own CSS to style .message, .urgent, .message-header, etc. -->
<!-- Keep the newest message at the top (prepend). -->
<!-- data-time holds the last seen numeric timestamp (epoch ms). -->
<!-- If you SSR initial messages, set data-time to the newest message timestamp. -->
</section>

Notes

  • data-time is used to request only messages newer than the last one you’ve seen. Use a numeric epoch timestamp in milliseconds.
  • Messages are prepended so the newest appears first.

Import and call initializeBlog(blogId, context, options) on the client (after the page mounts).

import initializeBlog from "./liveBlog"; // path where you place the file below
// A unique blog identifier and a "context" (e.g., your account/site slug) combine to form the socket room id.
// serverUrl must not be changed
// dateSuffix can be changed to your local timezone or modified to reflect the user's timezone (you must implement your own date parsing and conversion)
initializeBlog(
/* blogId: */ 42,
/* context: */ "mysite",
/* options: */ { serverUrl: "https://liveblog.whitebeard.net", dateSuffix: "UTC" }
);

Options

  • serverUrl: Socket server for your CMS live blog. Use the value provided by your CMS.
  • dateSuffix: Short suffix appended to the displayed time.

The script expects each incoming message to look like this:

type IncomingMessage = {
messageId: string | number; // unique id for de-dup/update
timestamp: number; // epoch ms, used to advance chat.dataset.time
date: string; // human-readable time label to display
title: string;
text: string;
image: string; // optional image URL
image_caption: string;
urgent: boolean | "1";
user: string; // name of the backend user that this message is attributed to
user_thumbnail: string; // URL of the user's profile picture
};

On connect, the client joins a room and requests any missed messages since data-time:

  1. connect → compute roomId = context-blogId
  2. emit('join', roomId)
  3. emit('sync', roomId, latestTimestamp)
  4. Server responds with either individual message events or a messages batch (array)

When a message arrives:

  • It’s inserted at the top (or updated if an element with the same data-id exists)
  • data-time is advanced to the latest timestamp

Because the script touches window/DOM, call it inside useEffect to avoid SSR:

import { useEffect } from "react";
import initializeBlog from "./liveBlog";
export default function LiveBlog({ blogId, context }: { blogId: number; context: string }) {
useEffect(() => {
initializeBlog(blogId, context, { serverUrl: "https://liveblog.whitebeard.net" });
}, [blogId, context]);
return (
<section className="liveBlog" aria-label="Live updates">
<div id="chat" data-time="0">
<div className="messages" />
</div>
</section>
);
}

For each message, the script adds Facebook, Twitter, and Email share links. If the relevant global SDKs are present on the page, they will be reprocessed automatically:

  • Twitter: window.twttr?.widgets?.load(el)
  • Facebook: window.FB?.XFBML?.parse()
  • Instagram: window.instgrm?.Embeds?.process()

You can remove or replace these in updateSocial().

  • Set aria-live="polite" on .messages if you want screen readers to announce updates.
  • Consider a “Pause live updates” toggle for long events.
  • Style .message.urgent to stand out.

Below is the complete reference script used in this guide. Save it as liveBlog.js and import it in your app.

liveBlog.js
// Minimal JS version. Requires: npm i socket.io-client
import { io } from "socket.io";
export default function initializeBlog(blogId, context, { serverUrl = "https://liveblog.whitebeard.net", dateSuffix = "UTC" } = {}) {
const chat = document.getElementById("chat");
if (!chat) return;
const messagesEl = chat.querySelector(".messages");
if (!messagesEl) return;
// Add share buttons + (optionally) refresh embeds if SDKs are present
function updateSocial(el) {
const shareList = el.querySelector(".message-header .social-share");
if (!shareList || shareList.querySelector(".facebook")) return;
const textEl = el.querySelector(".message-container p:not(.image_caption)");
const text = (textEl?.textContent || "").replace(/\s\s+/g, " ").trim();
const pageUrl = window.location.href;
const encodedText = encodeURIComponent(text);
const encodedUrl = encodeURIComponent(pageUrl);
shareList.insertAdjacentHTML(
"beforeend",
`
<li class="facebook">
<a href="https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}&display=popup&ref=plugin&src=quote&quote=${encodedText}" target="_blank" rel="noopener noreferrer">
<i class="fa fa-facebook"></i>
</a>
</li>
<li class="twitter">
<a href="https://www.twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedText}" target="_blank" rel="noopener noreferrer">
<i class="fa fa-twitter"></i>
</a>
</li>
<li class="email">
<a href="mailto:?subject=${encodedText}&body=${encodedUrl}">
<i class="fa fa-envelope"></i>
</a>
</li>
`
);
// Optional: refresh embeds if global SDKs exist
try { window.twttr?.widgets?.load(el); } catch {}
try { window.FB?.XFBML?.parse(); } catch {}
try { window.instgrm?.Embeds?.process(); } catch {}
}
// Initialize social buttons for any messages already on the page
document.querySelectorAll("#chat .messages .message").forEach(updateSocial);
function addMessage({ title, text, image = "", image_caption = "", date, messageId, urgent }) {
const isUrgent = urgent === true || urgent === "1";
const wrapper = document.createElement("div");
wrapper.className = `message${isUrgent ? " urgent" : ""}`;
wrapper.dataset.id = messageId;
wrapper.innerHTML = `
<div class="message-header">
<div class="time-details"><span class="time">${date} ${dateSuffix}</span></div>
<ul class="social-share"></ul>
</div>
<div class="message-container">
${image ? `<img src="${image}" alt="">` : ""}
${image_caption ? `<p class="image_caption">${image_caption}</p>` : ""}
<h2>${(title || "").trim()}</p>
<p>${(text || "").trim()}</p>
</div>
`;
updateSocial(wrapper);
const existing = messagesEl.querySelector(`.message[data-id="${CSS.escape(messageId)}"]`);
if (existing) {
existing.replaceWith(wrapper);
} else {
wrapper.style.opacity = "0";
messagesEl.prepend(wrapper);
requestAnimationFrame(() => {
wrapper.style.transition = "opacity 200ms ease";
wrapper.style.opacity = "1";
});
}
messagesEl.scrollTop = 0;
}
function displayMessage(msg) {
addMessage(msg);
const current = Number(chat.dataset.time || "0") || 0;
if (msg.timestamp > current) chat.dataset.time = String(msg.timestamp);
}
// --- Socket.io ---
const socket = io(serverUrl, { transports: ["websocket"] });
socket.on("connect", () => {
const roomId = `${context}-${blogId}`;
const latest = Number(chat.dataset.time || "0") || 0;
socket.emit("join", roomId);
socket.emit("sync", roomId, latest);
});
socket.on("message", displayMessage);
}