From 678fead13927409e8329d899ef0e2229dd588588 Mon Sep 17 00:00:00 2001 From: isandrel Date: Tue, 6 Jan 2026 22:17:28 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(analytics):=20add=20per-post?= =?UTF-8?q?=20page=20views=20and=20visitor=20statistics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Display PV/UV stats in pill badges on post cards and post pages - Use Umami API with path parameter for per-post filtering - Add 400ms number rolling animation with easing - Implement lazy loading with IntersectionObserver for homepage - Add Swup hooks and DOM guards to prevent duplicate animations - Prefetch share data for synchronized timing with site stats Closes #26 --- src/components/PostCard.astro | 180 ++++++++++++++++++++++++- src/pages/posts/[...slug].astro | 232 +++++++++++++++++++++++++++++--- 2 files changed, 384 insertions(+), 28 deletions(-) diff --git a/src/components/PostCard.astro b/src/components/PostCard.astro index a7a7143041..1ab825edd8 100644 --- a/src/components/PostCard.astro +++ b/src/components/PostCard.astro @@ -2,6 +2,7 @@ import type { CollectionEntry } from "astro:content"; import path from "node:path"; import { Icon } from "astro-icon/components"; +import { umamiConfig } from "../config"; import I18nKey from "../i18n/i18nKey"; import { i18n } from "../i18n/translation"; import { getDir } from "../utils/url-utils"; @@ -41,6 +42,14 @@ const hasCover = image !== undefined && image !== null && image !== ""; const coverWidth = "28%"; const { remarkPluginFrontmatter } = await entry.render(); + +// Pass config to client +const statsConfig = { + enable: umamiConfig.enable, + baseUrl: umamiConfig.baseUrl, + shareId: umamiConfig.shareId, + timezone: umamiConfig.timezone, +}; ---
@@ -64,14 +73,23 @@ const { remarkPluginFrontmatter } = await entry.render(); { description || remarkPluginFrontmatter.excerpt }
- -
-
- {remarkPluginFrontmatter.words} {" " + i18n(remarkPluginFrontmatter.words === 1 ? I18nKey.wordCount : I18nKey.wordsCount)} + +
+
+ + {remarkPluginFrontmatter.words} {i18n(remarkPluginFrontmatter.words === 1 ? I18nKey.wordCount : I18nKey.wordsCount)} +
+
+ + {remarkPluginFrontmatter.minutes} {i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)} +
+
+ + --
-
|
-
- {remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)} +
+ + --
@@ -107,4 +125,152 @@ const { remarkPluginFrontmatter } = await entry.render();
+ + diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro index 7fdfcb8190..2687761c44 100644 --- a/src/pages/posts/[...slug].astro +++ b/src/pages/posts/[...slug].astro @@ -8,12 +8,20 @@ import MainGridLayout from "@layouts/MainGridLayout.astro"; import { getSortedPosts } from "@utils/content-utils"; import { getDir, getPostUrlBySlug } from "@utils/url-utils"; import { Icon } from "astro-icon/components"; -import { licenseConfig } from "src/config"; +import { licenseConfig, umamiConfig } from "src/config"; import ImageWrapper from "../../components/misc/ImageWrapper.astro"; import PostMetadata from "../../components/PostMeta.astro"; import { profileConfig, siteConfig } from "../../config"; import { formatDateToYYYYMMDD } from "../../utils/date-utils"; +// Pass config to client +const statsConfig = { + enable: umamiConfig.enable, + baseUrl: umamiConfig.baseUrl, + shareId: umamiConfig.shareId, + timezone: umamiConfig.timezone, +}; + export async function getStaticPaths() { const blogEntries = await getSortedPosts(); return blogEntries.map((entry) => ({ @@ -51,24 +59,6 @@ const jsonLd = {
- -
-
-
- -
-
{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}
-
-
-
- -
-
- {remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)} -
-
-
-
- {!entry.data.image &&
}
+ +
+
+ + {remarkPluginFrontmatter.words} {i18n(I18nKey.wordsCount)} +
+
+ + {remarkPluginFrontmatter.minutes} {i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)} +
+
+ + -- +
+
+ + -- +
+
+ + {!entry.data.image &&
} + {entry.data.image && @@ -134,3 +145,182 @@ const jsonLd = {
+ + + +