From 6bea7e06913c270690a0c779ba09141318653d7d Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Sat, 7 Mar 2026 08:41:47 +0000 Subject: [PATCH] Improve blog post view counting to track unique visitors Add `article_views` table and update logic in `server/routes.ts` and `server/storage.ts` to record and check IP hashes before incrementing view counts, ensuring unique visitor tracking. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: d8397246-0deb-4af1-ac00-7fcd2a5d3a42 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/8XlN305 Replit-Helium-Checkpoint-Created: true --- replit.md | 1 + server/routes.ts | 5 +++-- server/storage.ts | 16 ++++++++++++---- shared/schema.ts | 7 +++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/replit.md b/replit.md index 6619a24..3f6ebde 100644 --- a/replit.md +++ b/replit.md @@ -24,6 +24,7 @@ The official website for Folx Music Television (folx.tv). Dark-themed bento grid ## Data Model - `articles`: id (serial), title, slug (unique), excerpt, content (HTML), coverImage, category, author, featured, views, publishedAt +- `article_views`: id (serial), articleId, ipHash (sha256 truncated to 16 chars), viewedAt — tracks unique IP views per article ## API Endpoints - `GET /api/articles` - All articles diff --git a/server/routes.ts b/server/routes.ts index dac91ea..bf8c57b 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -144,8 +144,9 @@ export async function registerRoutes( if (!article) { return res.status(404).json({ message: "Article not found" }); } - await storage.incrementViews(article.id); - res.json({ ...article, views: article.views + 1 }); + const ip = (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown"; + const isNew = await storage.incrementViews(article.id, ip); + res.json({ ...article, views: article.views + (isNew ? 1 : 0) }); }); app.post("/api/articles", async (req, res) => { diff --git a/server/storage.ts b/server/storage.ts index 5dbc234..57ae30b 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -1,6 +1,7 @@ -import { type Article, type InsertArticle, articles } from "@shared/schema"; +import { type Article, type InsertArticle, articles, articleViews } from "@shared/schema"; import { db } from "./db"; -import { eq, desc, sql, count } from "drizzle-orm"; +import { eq, desc, sql, count, and } from "drizzle-orm"; +import crypto from "crypto"; export interface IStorage { getArticles(): Promise; @@ -12,7 +13,7 @@ export interface IStorage { getArticlesByCategoryPaginated(category: string, page: number, limit: number): Promise<{ articles: Article[]; total: number }>; createArticle(article: InsertArticle): Promise
; updateArticle(id: number, article: Partial): Promise
; - incrementViews(id: number): Promise; + incrementViews(id: number, ip: string): Promise; deleteArticle(id: number): Promise; } @@ -61,8 +62,15 @@ export class DatabaseStorage implements IStorage { return updated; } - async incrementViews(id: number): Promise { + async incrementViews(id: number, ip: string): Promise { + const ipHash = crypto.createHash("sha256").update(ip + "folx-salt").digest("hex").substring(0, 16); + const [existing] = await db.select().from(articleViews) + .where(and(eq(articleViews.articleId, id), eq(articleViews.ipHash, ipHash))) + .limit(1); + if (existing) return false; + await db.insert(articleViews).values({ articleId: id, ipHash }); await db.update(articles).set({ views: sql`${articles.views} + 1` }).where(eq(articles.id, id)); + return true; } async deleteArticle(id: number): Promise { diff --git a/shared/schema.ts b/shared/schema.ts index 1171776..2df1e45 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -26,6 +26,13 @@ export const insertArticleSchema = createInsertSchema(articles).omit({ export type InsertArticle = z.infer; export type Article = typeof articles.$inferSelect; +export const articleViews = pgTable("article_views", { + id: serial("id").primaryKey(), + articleId: integer("article_id").notNull(), + ipHash: varchar("ip_hash", { length: 64 }).notNull(), + viewedAt: timestamp("viewed_at").notNull().defaultNow(), +}); + export const dailyHoroscopes = pgTable("daily_horoscopes", { id: serial("id").primaryKey(), signIndex: integer("sign_index").notNull(),