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.
What you’ll build
Section titled “What you’ll build”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
Prerequisites
Section titled “Prerequisites”Install the Socket.IO client in your app:
npm i socket.io-clientExpected HTML structure
Section titled “Expected HTML structure”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-timeis 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.
Initialize the client
Section titled “Initialize the client”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.
Message shape (frontend)
Section titled “Message shape (frontend)”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};Realtime flow
Section titled “Realtime flow”On connect, the client joins a room and requests any missed messages since data-time:
connect→ computeroomId = context-blogIdemit('join', roomId)emit('sync', roomId, latestTimestamp)- Server responds with either individual
messageevents or amessagesbatch (array)
When a message arrives:
- It’s inserted at the top (or updated if an element with the same
data-idexists) data-timeis advanced to the latesttimestamp
Using in React/Next.js
Section titled “Using in React/Next.js”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> );}Social sharing and embeds
Section titled “Social sharing and embeds”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().
Accessibility and UX tips
Section titled “Accessibility and UX tips”- Set
aria-live="polite"on.messagesif you want screen readers to announce updates. - Consider a “Pause live updates” toggle for long events.
- Style
.message.urgentto stand out.
Reference implementation
Section titled “Reference implementation”Below is the complete reference script used in this guide. Save it as liveBlog.js and import it in your app.
// Minimal JS version. Requires: npm i socket.io-clientimport { 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"e=${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);}