diff --git a/assets/reader-relative-time.js b/assets/reader-relative-time.js
new file mode 100644
index 0000000..45a6ce2
--- /dev/null
+++ b/assets/reader-relative-time.js
@@ -0,0 +1,87 @@
+/**
+ * Relative timestamps — Alpine.js directive that converts absolute
+ * datetime attributes to human-friendly relative strings.
+ *
+ * Usage:
+ *
+ * The server-rendered absolute time stays as fallback for no-JS clients.
+ * Alpine enhances it to relative on hydration, updates every 60s for
+ * recent posts, and shows the absolute time on hover via title attribute.
+ *
+ * Format rules (matching Mastodon/Elk conventions):
+ * < 1 minute: "just now"
+ * < 60 minutes: "Xm" (e.g. "5m")
+ * < 24 hours: "Xh" (e.g. "3h")
+ * < 7 days: "Xd" (e.g. "2d")
+ * same year: "Mar 3"
+ * older: "Mar 3, 2025"
+ */
+
+document.addEventListener("alpine:init", () => {
+ // eslint-disable-next-line no-undef
+ Alpine.directive("relative-time", (el) => {
+ const iso = el.getAttribute("datetime");
+ if (!iso) return;
+
+ const date = new Date(iso);
+ if (Number.isNaN(date.getTime())) return;
+
+ // Store the original formatted text as the title (hover tooltip)
+ const original = el.textContent.trim();
+ if (original && !el.getAttribute("title")) {
+ el.setAttribute("title", original);
+ }
+
+ function update() {
+ el.textContent = formatRelative(date);
+ }
+
+ update();
+
+ // Only set up interval for recent posts (< 24h old)
+ const ageMs = Date.now() - date.getTime();
+ if (ageMs < 86_400_000) {
+ const interval = setInterval(() => {
+ update();
+ // Stop updating once older than 24h
+ if (Date.now() - date.getTime() >= 86_400_000) {
+ clearInterval(interval);
+ }
+ }, 60_000);
+ }
+ });
+});
+
+/**
+ * Format a Date as a relative time string.
+ * @param {Date} date
+ * @returns {string}
+ */
+function formatRelative(date) {
+ const now = Date.now();
+ const diffMs = now - date.getTime();
+ const diffSec = Math.floor(diffMs / 1000);
+
+ if (diffSec < 60) return "just now";
+
+ const diffMin = Math.floor(diffSec / 60);
+ if (diffMin < 60) return `${diffMin}m`;
+
+ const diffHour = Math.floor(diffMin / 60);
+ if (diffHour < 24) return `${diffHour}h`;
+
+ const diffDay = Math.floor(diffHour / 24);
+ if (diffDay < 7) return `${diffDay}d`;
+
+ // Older than 7 days — use formatted date
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+ const month = months[date.getMonth()];
+ const day = date.getDate();
+
+ if (date.getFullYear() === new Date().getFullYear()) {
+ return `${month} ${day}`;
+ }
+
+ return `${month} ${day}, ${date.getFullYear()}`;
+}
diff --git a/package.json b/package.json
index 3dec390..cc32142 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@rmdes/indiekit-endpoint-activitypub",
- "version": "2.5.1",
+ "version": "2.5.2",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [
"indiekit",
diff --git a/views/layouts/ap-reader.njk b/views/layouts/ap-reader.njk
index 96c897c..1b57d4c 100644
--- a/views/layouts/ap-reader.njk
+++ b/views/layouts/ap-reader.njk
@@ -7,6 +7,8 @@
{# Tab components — apExploreTabs #}
+ {# Relative timestamps — converts absolute dates to "5m", "3h", "2d" etc. #}
+
{# Alpine.js for client-side reactivity (CW toggles, interaction buttons, infinite scroll) #}
diff --git a/views/partials/ap-item-card.njk b/views/partials/ap-item-card.njk
index c551cac..b31b578 100644
--- a/views/partials/ap-item-card.njk
+++ b/views/partials/ap-item-card.njk
@@ -60,7 +60,7 @@
{% if item.published %}
-
diff --git a/views/partials/ap-notification-card.njk b/views/partials/ap-notification-card.njk
index 8f729d2..487833f 100644
--- a/views/partials/ap-notification-card.njk
+++ b/views/partials/ap-notification-card.njk
@@ -68,7 +68,7 @@
{# Timestamp #}
{% if item.published %}
-
+
{{ item.published | date("PPp") }}
{% endif %}
diff --git a/views/partials/ap-quote-embed.njk b/views/partials/ap-quote-embed.njk
index 24d7f13..15d1a24 100644
--- a/views/partials/ap-quote-embed.njk
+++ b/views/partials/ap-quote-embed.njk
@@ -15,7 +15,7 @@
{% endif %}
{% if item.quote.published %}
- {{ item.quote.published | date("PPp") }}
+ {{ item.quote.published | date("PPp") }}
{% endif %}
{% if item.quote.name %}