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
This commit is contained in:
parent
da7de230c8
commit
6bea7e0691
@ -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
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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<Article[]>;
|
||||
@ -12,7 +13,7 @@ export interface IStorage {
|
||||
getArticlesByCategoryPaginated(category: string, page: number, limit: number): Promise<{ articles: Article[]; total: number }>;
|
||||
createArticle(article: InsertArticle): Promise<Article>;
|
||||
updateArticle(id: number, article: Partial<InsertArticle>): Promise<Article | undefined>;
|
||||
incrementViews(id: number): Promise<void>;
|
||||
incrementViews(id: number, ip: string): Promise<boolean>;
|
||||
deleteArticle(id: number): Promise<void>;
|
||||
}
|
||||
|
||||
@ -61,8 +62,15 @@ export class DatabaseStorage implements IStorage {
|
||||
return updated;
|
||||
}
|
||||
|
||||
async incrementViews(id: number): Promise<void> {
|
||||
async incrementViews(id: number, ip: string): Promise<boolean> {
|
||||
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<void> {
|
||||
|
||||
@ -26,6 +26,13 @@ export const insertArticleSchema = createInsertSchema(articles).omit({
|
||||
export type InsertArticle = z.infer<typeof insertArticleSchema>;
|
||||
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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user