How I Added a Live Runtime Widget to My Astro Blog
Adding a runtime counter that tracks how long your site has been alive is a surprisingly nice way to show visitors that the blog keeps evolving. Below is the exact recipe I followed to wire up a Beijing-time runtime ticker in an Astro + Yukina stack—you can reuse it for any Astro site with minor tweaks.
Prerequisites
- Astro 4+ (this repo runs 5.3) with TypeScript enabled
- Tailwind utilities available via
@astrojs/tailwind - A shared layout/footer component rendered on every page (mine lives in
src/components/Footer.astro) - The baseline timestamp you want to track, e.g., October 8, 2025 at 8:00 AM Beijing time
Step 1 · Build a Runtime Component
Create src/components/RuntimeTicker.astro and drop in the following code:
---
const startDateIso = "2025-10-08T08:00:00+08:00";
const runtimeId = `runtime-counter-${Astro.componentInstance}`;
const readableStart = "Oct 8, 2025 · 8:00 AM";
---
<div
id={runtimeId}
data-start={startDateIso}
class="runtime-card mt-6 w-full max-w-md rounded-2xl border border-[var(--primary-color-lighten)]/40 bg-[var(--card-color)] p-4 text-center text-sm text-[var(--text-color-lighten)]"
>
<p class="uppercase tracking-wide">Runtime since {readableStart}</p>
<p class="runtime-value mt-1 text-lg font-semibold text-[var(--text-color)]" data-runtime>
0 y 0 m 0 d 0 h
</p>
</div>
<script is:inline define:vars={{ runtimeId }}>
(() => {
const container = document.getElementById(runtimeId);
const target = container?.querySelector("[data-runtime]");
const startIso = container?.dataset.start ?? null;
const startTime = startIso ? new Date(startIso) : null;
if (!container || !target || !startTime || Number.isNaN(startTime.valueOf())) return;
const formatter = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Shanghai",
hour12: false,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
const toParts = (date) => {
const base = { year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 0 };
formatter.formatToParts(date).forEach((part) => {
if (part.type !== "literal" && part.type in base) base[part.type] = Number(part.value);
});
return base;
};
const daysInMonth = (year, month) => new Date(year, month, 0).getDate();
const diffCalendar = (startParts, endParts) => {
const current = { ...endParts };
const start = { ...startParts };
if (current.second < start.second) { current.second += 60; current.minute -= 1; }
if (current.minute < start.minute) { current.minute += 60; current.hour -= 1; }
if (current.hour < start.hour) { current.hour += 24; current.day -= 1; }
if (current.day < start.day) {
current.month -= 1;
if (current.month < 1) { current.month = 12; current.year -= 1; }
current.day += daysInMonth(current.year, current.month);
}
if (current.month < start.month) { current.month += 12; current.year -= 1; }
return {
years: Math.max(0, current.year - start.year),
months: Math.max(0, current.month - start.month),
days: Math.max(0, current.day - start.day),
hours: Math.max(0, current.hour - start.hour),
};
};
const startParts = toParts(startTime);
const render = () => {
if (Date.now() <= startTime.getTime()) {
target.textContent = "0 y 0 m 0 d 0 h";
return;
}
const diff = diffCalendar(startParts, toParts(new Date()));
target.textContent = `${diff.years} y ${diff.months} m ${diff.days} d ${diff.hours} h`;
};
render();
const timer = window.setInterval(render, 60 * 1000);
document.addEventListener("astro:before-swap", () => window.clearInterval(timer), { once: true });
})();
</script>
Highlights:
- The component renders a styled card with a placeholder value.
- The inline script converts both the fixed start timestamp and the browser’s real-time clock into Asia/Shanghai calendar parts using
Intl.DateTimeFormat, so Beijing time stays accurate regardless of the visitor’s locale. - A manual borrow algorithm ensures the Y/M/D/H breakdown respects calendar boundaries, e.g., month lengths and leap years.
Step 2 · Mount the Widget Everywhere
Import and render RuntimeTicker inside your global footer or layout after existing copyright.
---
import RuntimeTicker from "./RuntimeTicker.astro";
---
<footer>
<!-- existing footer markup -->
<RuntimeTicker />
</footer>
Placing it at the bottom keeps the ticker visible without interrupting your article flow.
Step 3 · Verify Locally
Run the dev server and ensure the counter increments in real time.
pnpm dev
Steps I follow during QA:
- Load any page and scroll to the footer; the widget should show a non-zero value (unless you set a future start date).
- Keep the tab open for a couple of minutes—the hour count should bump exactly when the Beijing-time difference crosses another hour.
- Navigate between routes; thanks to the
astro:before-swaplistener, you should not see duplicated intervals.
Step 4 · Ship with Confidence
Before merging, make sure production builds succeed:
pnpm build && pnpm astro check
Once deployed, the ticker requires no backend calls—it runs purely on the client and depends only on the visitor’s clock plus the Asia/Shanghai conversion, so it works even on static hosting. If you ever need a new start date, change startDateIso and the readableStart string together to keep the UI honest.
Happy blogging, and enjoy watching your site’s runtime grow! 🚀