From 30b88849780a89b29b0bfc8ea678428b6ec6ef30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=96mer=20Faruk=20Demirel?=
Date: Sat, 4 Apr 2026 14:51:26 +0300
Subject: [PATCH] feat: article view count with kv cache implemented
---
...a287016a57770571eee21dc7690000019d584eb91f | 1 +
...3ae993ef635ba2b83929b9f3750000019d584eb91a | 1 +
...31ec1b033fba0574bb30dd3f5e0000019d5849f694 | 1 +
...43724c857ce74a35b86238b75e6e807ff7f.sqlite | Bin 0 -> 16384 bytes
astro.config.mjs | 10 +++
package.json | 3 +-
pnpm-lock.yaml | 3 +
src/components/ArticleViews.astro | 65 ++++++++++++++++++
src/env.d.ts | 13 +---
src/lib/views.ts | 13 ++++
src/middleware.ts | 4 +-
src/pages/api/views/[slug].ts | 56 +++++++++++++++
src/pages/articles/[slug].astro | 5 +-
src/pages/articles/index.astro | 6 +-
src/pages/categories/[slug].astro | 6 +-
src/pages/en/articles/[slug].astro | 5 +-
src/pages/en/articles/index.astro | 6 +-
src/pages/en/categories/[slug].astro | 6 +-
src/pages/en/index.astro | 11 +--
src/pages/en/tags/[slug].astro | 6 +-
src/pages/index.astro | 11 +--
src/pages/tags/[slug].astro | 6 +-
wrangler.toml | 6 ++
23 files changed, 184 insertions(+), 60 deletions(-)
create mode 100644 .wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/1945ae1d9220b9b9595df3b70c7f12606f5505a287016a57770571eee21dc7690000019d584eb91f
create mode 100644 .wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/88a48a5ca411d99cd63c9bb308aa3868125abf3ae993ef635ba2b83929b9f3750000019d584eb91a
create mode 100644 .wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/d30c0dd40129710b5dbf376654872cdd10c68631ec1b033fba0574bb30dd3f5e0000019d5849f694
create mode 100644 .wrangler/state/v3/kv/miniflare-KVNamespaceObject/d1866dd99f5441005a55b0a86f46e43724c857ce74a35b86238b75e6e807ff7f.sqlite
create mode 100644 src/components/ArticleViews.astro
create mode 100644 src/lib/views.ts
create mode 100644 src/pages/api/views/[slug].ts
create mode 100644 wrangler.toml
diff --git a/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/1945ae1d9220b9b9595df3b70c7f12606f5505a287016a57770571eee21dc7690000019d584eb91f b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/1945ae1d9220b9b9595df3b70c7f12606f5505a287016a57770571eee21dc7690000019d584eb91f
new file mode 100644
index 0000000..e440e5c
--- /dev/null
+++ b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/1945ae1d9220b9b9595df3b70c7f12606f5505a287016a57770571eee21dc7690000019d584eb91f
@@ -0,0 +1 @@
+3
\ No newline at end of file
diff --git a/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/88a48a5ca411d99cd63c9bb308aa3868125abf3ae993ef635ba2b83929b9f3750000019d584eb91a b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/88a48a5ca411d99cd63c9bb308aa3868125abf3ae993ef635ba2b83929b9f3750000019d584eb91a
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/88a48a5ca411d99cd63c9bb308aa3868125abf3ae993ef635ba2b83929b9f3750000019d584eb91a
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/d30c0dd40129710b5dbf376654872cdd10c68631ec1b033fba0574bb30dd3f5e0000019d5849f694 b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/d30c0dd40129710b5dbf376654872cdd10c68631ec1b033fba0574bb30dd3f5e0000019d5849f694
new file mode 100644
index 0000000..c793025
--- /dev/null
+++ b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/d30c0dd40129710b5dbf376654872cdd10c68631ec1b033fba0574bb30dd3f5e0000019d5849f694
@@ -0,0 +1 @@
+7
\ No newline at end of file
diff --git a/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/d1866dd99f5441005a55b0a86f46e43724c857ce74a35b86238b75e6e807ff7f.sqlite b/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/d1866dd99f5441005a55b0a86f46e43724c857ce74a35b86238b75e6e807ff7f.sqlite
new file mode 100644
index 0000000000000000000000000000000000000000..44557415355c6f9aa6e8ea0c9514e320590c1740
GIT binary patch
literal 16384
zcmeI%KX21O6aer`9HmwI$8>3j3dvFjT8SW^&u9D8p#-o{MMxn8shy%b-?>;MPO)87
z7NmXzBtA)p?tB5h1!ET$7#KJ=t!h;&VxeMqC!J6Bi|@VrZGL(6vYQmjiYM8a7ZyQx
zP`!>GS{6d6u4hBf%W?g_b{Y7?T1O8)?k<==k+FD!%ysjNUSNO#2!H?xfB*=900@8p
z2!H?xfWTh}6voU-XRUU;l%!G}4af0NrA3yge5j5Nl8hJ0BpoKQ8fiT4hn+!aZTFsp
zyVlLGSUbIHbp2|>jpOCo%*vWhn-^S+RCSN53hEbm2AxOUu)34A-m>Vq{1cL&ys
z{`T`uf6sar?%mhV#b_dQ!P8i8XJGYSce`caD!s0J5Izn2<^UWAPN3_ZCmhPvsLBAOHd&00JNY0w4ea
zAOHd&00JQJzX(j`np?+hg!01!mAy}LCEFv#vouNf+o_UC76jZ4IE}cAv1BZg9*LL`
z#11$offrzhauE}*7$Yk72o>BB0bvdkEG9m+%Ym7sfvW_={LO3=mkCEns`IqR6RAei
zOp@49A7D
z!7}naR!LVLEN+?KkYRpC=2!DXFEBs=1V8`;KmY_l00ck)1V8`;KmY`8N#L%rRlBjX
zF}G@L)-Ku**P8?L%LcxTRaz9~AN{L0ywWm%>F@tDWd1PE%1scBrD8>b+
literal 0
HcmV?d00001
diff --git a/astro.config.mjs b/astro.config.mjs
index 476e2e3..bb062ad 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -13,7 +13,17 @@ export default defineConfig({
output: 'static',
adapter: cloudflare({
imageService: 'cloudflare',
+ routes: {
+ pattern: '/**',
+ exclude: ['/_image/**'],
+ },
}),
+ kvNamespaces: [
+ {
+ binding: 'ARTICLE_VIEWS',
+ id: '4byte-article-views',
+ },
+ ],
integrations: [
UnoCSS(),
mdx(),
diff --git a/package.json b/package.json
index 2b886ff..2257fc1 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,8 @@
"prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1",
"sharp": "^0.34.5",
- "unocss": "^66.6.6"
+ "unocss": "^66.6.6",
+ "wrangler": "^4.80.0"
},
"lint-staged": {
"*.{js,ts,astro,json,md,mdx}": [
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8c492b7..41d1c88 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -108,6 +108,9 @@ importers:
unocss:
specifier: ^66.6.6
version: 66.6.6(@unocss/astro@66.6.6(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(yaml@2.8.2)))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(yaml@2.8.2))
+ wrangler:
+ specifier: ^4.80.0
+ version: 4.80.0
packages:
diff --git a/src/components/ArticleViews.astro b/src/components/ArticleViews.astro
new file mode 100644
index 0000000..445de00
--- /dev/null
+++ b/src/components/ArticleViews.astro
@@ -0,0 +1,65 @@
+---
+interface Props {
+ slug: string
+ initialViews?: number
+}
+
+const { slug, initialViews = 0 } = Astro.props
+---
+
+
+
+ {initialViews.toLocaleString()}
+
+
+
diff --git a/src/env.d.ts b/src/env.d.ts
index c561c96..4c6964b 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -1,19 +1,10 @@
-interface CloudflareEnv {
+export interface CloudflareEnv {
JWT_SECRET: string
GITHUB_CLIENT_ID: string
GITHUB_CLIENT_SECRET: string
GITHUB_CALLBACK_URL: string
DEV?: string
-}
-
-declare global {
- namespace App {
- interface Locals {
- runtime: {
- env: CloudflareEnv
- }
- }
- }
+ ARTICLE_VIEWS: KVNamespace
}
export {}
diff --git a/src/lib/views.ts b/src/lib/views.ts
new file mode 100644
index 0000000..2bc7fde
--- /dev/null
+++ b/src/lib/views.ts
@@ -0,0 +1,13 @@
+import type { CloudflareEnv } from '../env'
+
+export async function getArticleViews(slug: string, env: CloudflareEnv): Promise {
+ const count = await env.ARTICLE_VIEWS.get(slug)
+ return count ? parseInt(count, 10) : 0
+}
+
+export async function incrementArticleViews(slug: string, env: CloudflareEnv): Promise {
+ const current = await getArticleViews(slug, env)
+ const newCount = current + 1
+ await env.ARTICLE_VIEWS.put(slug, newCount.toString())
+ return newCount
+}
diff --git a/src/middleware.ts b/src/middleware.ts
index 78f7d54..8381bf6 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,13 +1,13 @@
import { defineMiddleware } from 'astro/middleware'
import { verifyToken, getCookieName, type SessionUser } from './lib/auth'
-import { env } from 'cloudflare:workers'
+import { env as cfEnv } from 'cloudflare:workers'
export const onRequest = defineMiddleware(async (context, next) => {
const cookieName = getCookieName()
const token = context.cookies.get(cookieName)?.value
if (token) {
- const user = await verifyToken(token, env as any)
+ const user = await verifyToken(token, cfEnv as any)
if (user) {
context.locals.user = user
}
diff --git a/src/pages/api/views/[slug].ts b/src/pages/api/views/[slug].ts
new file mode 100644
index 0000000..19a8380
--- /dev/null
+++ b/src/pages/api/views/[slug].ts
@@ -0,0 +1,56 @@
+export const prerender = false
+
+import type { APIRoute } from 'astro'
+import { env } from 'cloudflare:workers'
+import { getArticleViews, incrementArticleViews } from '../../../lib/views'
+
+export const GET: APIRoute = async ({ params }) => {
+ const slug = params.slug
+ if (!slug) {
+ return new Response(JSON.stringify({ error: 'Slug required' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ const views = await getArticleViews(slug, env as any)
+ return new Response(JSON.stringify({ slug, views }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ })
+}
+
+export const POST: APIRoute = async ({ params, cookies }) => {
+ const slug = params.slug
+ if (!slug) {
+ return new Response(JSON.stringify({ error: 'Slug required' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ const cookieName = `view_tracked_${slug}`
+ const existingCookie = cookies.get(cookieName)
+
+ if (existingCookie) {
+ const views = await getArticleViews(slug, env as any)
+ return new Response(JSON.stringify({ slug, views }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ const views = await incrementArticleViews(slug, env as any)
+
+ cookies.set(cookieName, '1', {
+ path: '/',
+ maxAge: 60 * 60 * 24 * 30,
+ httpOnly: false,
+ sameSite: 'lax',
+ })
+
+ return new Response(JSON.stringify({ slug, views }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ })
+}
diff --git a/src/pages/articles/[slug].astro b/src/pages/articles/[slug].astro
index 945a10d..cf8a8cf 100644
--- a/src/pages/articles/[slug].astro
+++ b/src/pages/articles/[slug].astro
@@ -5,6 +5,7 @@ import GitHubUserCard from '../../components/GitHubUserCard.astro'
import ReadingProgress from '../../components/ReadingProgress.astro'
import TableOfContents from '../../components/TableOfContents.astro'
import ArticleNav from '../../components/ArticleNav.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection, render } from 'astro:content'
import { SITE } from '../../config.mjs'
import { getLangFromUrl, t } from '../../i18n/index.ts'
@@ -132,9 +133,7 @@ const onThisPageLabel = t(labels, 'article.onThisPage')
·
{article.data.date}
·
- {article.data.views.toLocaleString()}
+
{
diff --git a/src/pages/articles/index.astro b/src/pages/articles/index.astro
index e4701f5..44392c6 100644
--- a/src/pages/articles/index.astro
+++ b/src/pages/articles/index.astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../layouts/DocsLayout.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import { getLangFromUrl, t } from '../../i18n/index.ts'
@@ -53,10 +54,7 @@ const labels = await import(`../../data/${lang}/labels.json`).then((m) => m.defa
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/categories/[slug].astro b/src/pages/categories/[slug].astro
index fbdb542..be28a08 100644
--- a/src/pages/categories/[slug].astro
+++ b/src/pages/categories/[slug].astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../layouts/DocsLayout.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import categoriesTr from '../../data/tr/categories.json'
import categoriesEn from '../../data/en/categories.json'
@@ -70,10 +71,7 @@ const catArticles = allArticles.filter(
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/en/articles/[slug].astro b/src/pages/en/articles/[slug].astro
index 565fdee..e893efb 100644
--- a/src/pages/en/articles/[slug].astro
+++ b/src/pages/en/articles/[slug].astro
@@ -5,6 +5,7 @@ import GitHubUserCard from '../../../components/GitHubUserCard.astro'
import ReadingProgress from '../../../components/ReadingProgress.astro'
import TableOfContents from '../../../components/TableOfContents.astro'
import ArticleNav from '../../../components/ArticleNav.astro'
+import ArticleViews from '../../../components/ArticleViews.astro'
import { getCollection, render } from 'astro:content'
import { SITE } from '../../../config.mjs'
import { t } from '../../../i18n/index.ts'
@@ -131,9 +132,7 @@ const onThisPageLabel = t(labels, 'article.onThisPage')
·
{article.data.date}
·
- {article.data.views.toLocaleString()}
+
{
diff --git a/src/pages/en/articles/index.astro b/src/pages/en/articles/index.astro
index aaf7ab4..afe0acc 100644
--- a/src/pages/en/articles/index.astro
+++ b/src/pages/en/articles/index.astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../../layouts/DocsLayout.astro'
+import ArticleViews from '../../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import { t } from '../../../i18n/index.ts'
@@ -53,10 +54,7 @@ const labels = await import(`../../../data/${lang}/labels.json`).then((m) => m.d
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/en/categories/[slug].astro b/src/pages/en/categories/[slug].astro
index 198431c..e6a46f5 100644
--- a/src/pages/en/categories/[slug].astro
+++ b/src/pages/en/categories/[slug].astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../../layouts/DocsLayout.astro'
+import ArticleViews from '../../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import categoriesEn from '../../../data/en/categories.json'
import { t } from '../../../i18n/index.ts'
@@ -61,10 +62,7 @@ const catArticles = allArticles.filter(
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/en/index.astro b/src/pages/en/index.astro
index 730bc8a..e7460d7 100644
--- a/src/pages/en/index.astro
+++ b/src/pages/en/index.astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../layouts/DocsLayout.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import { t } from '../../i18n/index.ts'
@@ -63,10 +64,7 @@ const description = t(labels, 'home.description')
{article.data.excerpt}
-
-
- {article.data.views.toLocaleString()}
-
+
))
@@ -107,10 +105,7 @@ const description = t(labels, 'home.description')
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/en/tags/[slug].astro b/src/pages/en/tags/[slug].astro
index 04ce875..eb34620 100644
--- a/src/pages/en/tags/[slug].astro
+++ b/src/pages/en/tags/[slug].astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../../layouts/DocsLayout.astro'
+import ArticleViews from '../../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import tagsEn from '../../../data/en/tags.json'
import { t } from '../../../i18n/index.ts'
@@ -67,10 +68,7 @@ const tagArticles = allArticles.filter(
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 686648c..1f4d8d6 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../layouts/DocsLayout.astro'
+import ArticleViews from '../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import { getLangFromUrl, t } from '../i18n/index.ts'
@@ -63,10 +64,7 @@ const description = t(labels, 'home.description')
{article.data.excerpt}
-
-
- {article.data.views.toLocaleString()}
-
+
))
@@ -107,10 +105,7 @@ const description = t(labels, 'home.description')
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro
index 8f7db84..564b7eb 100644
--- a/src/pages/tags/[slug].astro
+++ b/src/pages/tags/[slug].astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../layouts/DocsLayout.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import tagsTr from '../../data/tr/tags.json'
import tagsEn from '../../data/en/tags.json'
@@ -77,10 +78,7 @@ const tagArticles = allArticles.filter(
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/wrangler.toml b/wrangler.toml
new file mode 100644
index 0000000..69087c1
--- /dev/null
+++ b/wrangler.toml
@@ -0,0 +1,6 @@
+name = "4byte-astro"
+compatibility_date = "2024-04-04"
+
+[[kv_namespaces]]
+binding = "ARTICLE_VIEWS"
+id = "0c407860ba17425581796410dbce9e19"