Bearlytics writing stats
Bearlytics shows a snapshot of your recent writing: words, averages, longest and shortest posts, and posting rhythm. All pulled from your blog feed.1 2
Preview
How to use
Add the markup below wherever you want the widget to appear, then add the script and styles to your theme. In the script, replace https://yourblog.com/feed/ with your own feed URL. For Bear Blog, that's your blog address followed by /feed/.
Note that stats reflect whatever your feed includes — typically the latest 10–20 posts, depending on your feed settings.
Markup
<div class="bl-widget">
<p class="bl-label">Last 10 posts</p>
<div class="bl-grid" id="bl-grid">
<div class="bl-loading">Fetching stats…</div>
</div>
</div>
Script
<script>
/* Bearlytics | robertbirming.com */
(function () {
/* --------------------------------
Config – swap in your feed URL
-------------------------------- */
const FEED_URL = "https://yourblog.com/feed/";
/* --------------------------------
Helpers
-------------------------------- */
function countWords(html) {
const stripped = html
.replace(/<pre[\s\S]*?<\/pre>/gi, " ")
.replace(/<code[\s\S]*?<\/code>/gi, " ");
const text = stripped.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
return text ? text.split(" ").length : 0;
}
function fmt(n) {
return n.toLocaleString();
}
function createStatRow(label, value) {
const row = document.createElement("div");
row.className = "bl-row";
const labelEl = document.createElement("span");
labelEl.className = "bl-stat-label";
labelEl.textContent = label;
const valueEl = document.createElement("span");
valueEl.className = "bl-stat-value";
valueEl.textContent = value;
row.append(labelEl, valueEl);
return row;
}
function createLinkedStatRow(label, post) {
const row = document.createElement("div");
row.className = "bl-row";
const link = document.createElement("a");
link.className = "bl-stat-label bl-stat-link";
link.textContent = `${label} ↗`;
link.title = post.title;
link.rel = "noopener noreferrer";
try {
const url = new URL(post.url);
link.href = (url.protocol === "https:" || url.protocol === "http:")
? url.href
: "#";
} catch {
link.href = "#";
}
const valueEl = document.createElement("span");
valueEl.className = "bl-stat-value";
valueEl.textContent = `${fmt(post.words)} words`;
row.append(link, valueEl);
return row;
}
/* --------------------------------
Fetch & render
-------------------------------- */
async function run() {
const grid = document.getElementById("bl-grid");
if (!grid) return;
try {
const res = await fetch(FEED_URL, { cache: "no-store" });
if (!res.ok) throw new Error("Network response was not ok");
const text = await res.text();
const xml = new DOMParser().parseFromString(text, "text/xml");
if (xml.querySelector("parsererror")) throw new Error("Invalid XML");
const isAtom = xml.querySelector("entry") !== null;
const entries = Array.from(xml.querySelectorAll(isAtom ? "entry" : "item"));
let totalWords = 0;
let shortest = null;
let longest = null;
const timestamps = [];
const posts = entries.map(el => {
const content = isAtom
? (el.querySelector("content")?.textContent || "")
: (el.getElementsByTagNameNS("http://purl.org/rss/1.0/modules/content/", "encoded")[0]?.textContent
|| el.querySelector("description")?.textContent || "");
const dateStr = isAtom
? el.querySelector("published")?.textContent
: el.querySelector("pubDate")?.textContent;
const ts = dateStr ? new Date(dateStr).getTime() : NaN;
return {
title: el.querySelector("title")?.textContent || "Untitled",
url: isAtom
? el.querySelector("link[rel='alternate']")?.getAttribute("href") || ""
: el.querySelector("link")?.textContent || "",
words: countWords(content),
ts,
};
}).filter(p => !isNaN(p.ts));
if (!posts.length) {
grid.replaceChildren();
const empty = document.createElement("p");
empty.className = "bl-empty";
empty.textContent = "No posts found.";
grid.appendChild(empty);
return;
}
for (const p of posts) {
totalWords += p.words;
timestamps.push(p.ts);
if (!shortest || p.words < shortest.words) shortest = p;
if (!longest || p.words > longest.words) longest = p;
}
const avgWords = Math.round(totalWords / posts.length);
const sorted = [...timestamps].sort((a, b) => a - b);
const avgGap = posts.length > 1
? Math.round((sorted[sorted.length - 1] - sorted[0]) / 86400000 / (posts.length - 1))
: 0;
const section1 = document.createElement("div");
section1.className = "bl-section";
section1.append(
createStatRow("Total words", fmt(totalWords)),
createStatRow("Avg per post", fmt(avgWords))
);
const divider1 = document.createElement("div");
divider1.className = "bl-divider";
const section2 = document.createElement("div");
section2.className = "bl-section";
section2.append(
createLinkedStatRow("Longest", longest),
createLinkedStatRow("Shortest", shortest)
);
const divider2 = document.createElement("div");
divider2.className = "bl-divider";
const section3 = document.createElement("div");
section3.className = "bl-section";
section3.append(
createStatRow("Avg gap", avgGap + (avgGap === 1 ? " day between posts" : " days between posts"))
);
grid.replaceChildren(section1, divider1, section2, divider2, section3);
} catch (err) {
grid.replaceChildren();
const error = document.createElement("p");
error.className = "bl-error";
error.textContent = "Couldn't load feed.";
grid.appendChild(error);
}
}
run();
})();
</script>
Styles
/* Bearlytics | robertbirming.com */
.bl-widget {
max-width: 28rem;
margin-block: var(--space-block);
padding-block: 1.25rem 1rem;
padding-inline: 1.5rem;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.bl-label {
margin-block: 0 0.75rem;
font-size: var(--font-small);
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text);
}
.bl-grid {
display: flex;
flex-direction: column;
}
.bl-section {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding-block: 0.25rem;
}
.bl-divider {
margin-block: 0.6rem;
border-block-start: 1px solid var(--border);
}
.bl-row {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.75rem;
align-items: baseline;
}
.bl-stat-label,
.bl-stat-value {
font-size: var(--font-small);
}
.bl-stat-label {
color: var(--muted);
white-space: nowrap;
}
.bl-stat-value {
font-variant-numeric: tabular-nums;
text-align: end;
}
.bl-widget a.bl-stat-link,
.bl-widget a.bl-stat-link:visited {
color: var(--muted);
text-decoration: none;
}
.bl-loading,
.bl-empty,
.bl-error {
font-size: var(--font-small);
color: var(--muted);
margin: 0;
}
Happy blogging, and happy writing.
Want more? Check out all available Bearming add-ons.
Built for the Bearming theme. Using a different theme? Add the Bearming tokens to make them work with your setup.↩
Requires JavaScript, available on Bear Blog's premium plan. Works with any Atom or RSS feed, not just Bear Blog.↩